Compare commits

...

10 Commits

29 changed files with 207064 additions and 5864 deletions

View File

@ -1,14 +1,23 @@
# Assistant Configuration
This file contains configuration and commands for the Claude assistant working on the CMtec CMDR Joystick 25.
## Global Rules
- Rust emdedded
- Always describe what you thinking and your plan befor starting to change files.
- Make sure code have max 5 indentation levels
- Use classes, arrays, structs, etc for clean organization
- Use arrays, structs, etc for clean organization
- Make sure the codebase is manageable and easily readable
- Always check code (compile/check)
- Always fix compile warnings
- Do not try to deploy project to hardware
- Remember to update CLAUDE.md about current progress, notes and recent changes. But always wait for confirmation that the code work as intended.
- Use "just" for check, test, flash etc
- Use file structure described in this file
## Firmware File Structure Blueprint (RP2040 / RP2350)
- `src/hardware.rs`**Required.** Centralize pin assignments, clock constants, peripheral aliases, timer intervals, and other board-specific configuration. Nothing outside this module hardcodes MCU pin numbers or magic frequencies.
- `src/board.rs`**Required.** Board bring-up; owns peripheral wiring (clocks, GPIO, comms, sensors, USB), exposes `Board`/`BoardParts` (or equivalent). Keep granular comments explaining each hardware init block.
- `src/main.rs`**Required.** Thin firmware entry; fetch initialized parts, load persisted configuration, configure timers, and run the primary control loop (USB/event poll, scheduling, report generation). Runtime orchestration only.
- Feature modules stay single-purpose (e.g., `inputs.rs`, `sensors.rs`, `storage.rs`, `status.rs`, `usb_report.rs`, `usb_device.rs`). Each should include unit tests with short intent comments capturing edge cases and data packing, runnable in host mode.
- Utility crates (`mapping.rs`, `calibration.rs`, etc.) should avoid cross-module side effects—prefer explicit data passed through `BoardParts`/state structs.
- Comments document why a block exists or which hardware behaviour it mirrors; avoid repeating obvious code but provide enough context for re-use across RP-series projects.

72
Justfile Normal file
View File

@ -0,0 +1,72 @@
set export := true
default: deps check
# Check and install dependencies via rustup
deps:
@just _setup-rustup
@just _setup-rust-toolchain
@just _setup-targets
@just _setup-cargo-binutils
@just _check-python
_setup-rustup:
@if ! command -v rustup >/dev/null 2>&1; then \
echo "Installing rustup..."; \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y; \
echo "Please run: source ~/.cargo/env"; \
echo "Then run 'just deps' again"; \
exit 1; \
fi
_setup-rust-toolchain:
@if ! rustc -vV >/dev/null 2>&1; then \
echo "Installing stable Rust toolchain..."; \
rustup default stable; \
fi
_setup-targets:
@if ! rustup target list --installed | grep -q "thumbv6m-none-eabi"; then \
echo "Installing thumbv6m-none-eabi target..."; \
rustup target add thumbv6m-none-eabi; \
fi
@if ! rustup component list --installed | grep -q "llvm-tools"; then \
echo "Installing llvm-tools component..."; \
rustup component add llvm-tools; \
fi
_setup-cargo-binutils:
@if ! cargo objcopy --version >/dev/null 2>&1; then \
echo "Installing cargo-binutils..."; \
cargo install cargo-binutils; \
fi
_check-python:
@command -v python3 >/dev/null 2>&1 || (echo "Missing: python3 - please install via nix" && exit 1)
check: deps
cd rp2040 && cargo check --target thumbv6m-none-eabi
test:
cd rp2040 && cargo test --lib --target x86_64-unknown-linux-gnu --features std
build-uf2:
cd rp2040 && cargo build --release --target thumbv6m-none-eabi
cd rp2040 && cargo objcopy --release --target thumbv6m-none-eabi -- -O binary target/thumbv6m-none-eabi/release/cmdr-joystick.bin
cd rp2040 && python3 uf2conv.py target/thumbv6m-none-eabi/release/cmdr-joystick.bin --base 0x10000000 --family 0xe48bff56 --convert --output target/firmware.uf2
clean:
cargo clean --manifest-path rp2040/Cargo.toml
flash mount="" timeout="10":
@just build-uf2
MOUNT="{{mount}}" python3 tools/copy_uf2.py --source rp2040/target/firmware.uf2 --timeout {{timeout}}
flash-ssh target mount="/Volumes/RPI-RP2" key="" port="22":
@just build-uf2
target="{{target}}"
mount="{{mount}}"
key_arg=""
if [ -n "{{key}}" ]; then key_arg="-i {{key}}"; fi
ssh $key_arg -p {{port}} "$target" "mkdir -p \"$mount\""
scp $key_arg -P {{port}} rp2040/target/firmware.uf2 "$target:$mount/"

View File

@ -1,4 +1,4 @@
# CMDR Joystick 25
# CMDR Joystick
USB HID joystick firmware + hardware: 2 halleffect gimbals, 2 physical hat
switches, and a 5x5 button matrix (plus 2 extra buttons). The firmware exposes
@ -96,6 +96,8 @@ Config Layer (holding CONFIG button)
- USB interrupt endpoint configured for 1 ms poll interval (1 kHz reports)
- Input scan, smoothing, processing, and mapping now execute back-to-back
- Enhanced button debounce: 15-scan threshold (3ms) with anti-bounce protection to prevent double presses
- Smart HAT switch filtering: disables all HAT buttons when multiple directions are detected to prevent spurious inputs
- First activity after idle forces immediate USB packet without waiting for the next tick
- Existing idle timeout preserved (5 s) to avoid unnecessary host wake-ups
@ -109,7 +111,7 @@ Config Layer (holding CONFIG button)
- 1x Bottom case (3D printed)
- 1x Top plate (3D printed)
- 2x Hat swith top (3D printed) [stl](/mCAD/Hat_Castle_Short_scale_99_99_130.stl)
- 1x Custom PCB (CMDR Joystick 25 rev A)
- 1x Custom PCB (CMDR Joystick rev A)
- ![pcb_top](/eCAD/cmdr-joystick/cmdr-joystick_rev_a_board_top.png)
- ![pcb_bottom](/eCAD/cmdr-joystick/cmdr-joystick_rev_a_board_bottom.png)
- Gerber files: [zip](/eCAD/cmdr-joystick/cmdr-joystick_rev_a_gerber.zip)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"board": {
"active_layer": 37,
"active_layer_preset": "All Layers",
"active_layer": 5,
"active_layer_preset": "",
"auto_track_width": false,
"hidden_netclasses": [],
"hidden_nets": [],
@ -10,6 +10,7 @@
"opacity": {
"images": 0.6,
"pads": 1.0,
"shapes": 1.0,
"tracks": 1.0,
"vias": 1.0,
"zones": 0.6
@ -28,43 +29,27 @@
"zones": true
},
"visible_items": [
0,
1,
2,
3,
4,
5,
8,
9,
10,
11,
12,
13,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
32,
33,
34,
35,
36,
39,
40
"vias",
"footprint_text",
"footprint_anchors",
"ratsnest",
"grid",
"footprints_front",
"footprints_back",
"footprint_values",
"footprint_references",
"tracks",
"drc_errors",
"drawing_sheet",
"bitmaps",
"pads",
"zones",
"drc_warnings",
"locked_item_shadows",
"conflict_shadows",
"shapes"
],
"visible_layers": "fffffff_ffffffff",
"visible_layers": "ffffffff_ffffffff_ffffffff_ffffffff",
"zone_display_mode": 0
},
"git": {
@ -75,9 +60,72 @@
},
"meta": {
"filename": "cmdr-joystick.kicad_prl",
"version": 3
"version": 5
},
"net_inspector_panel": {
"col_hidden": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false
],
"col_order": [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9
],
"col_widths": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"custom_group_rules": [],
"expanded_rows": [],
"filter_by_net_name": true,
"filter_by_netclass": true,
"filter_text": "",
"group_by_constraint": false,
"group_by_netclass": false,
"show_unconnected_nets": false,
"show_zero_pad_nets": false,
"sort_ascending": true,
"sorting_column": 0
},
"open_jobsets": [],
"project": {
"files": []
},
"schematic": {
"selection_filter": {
"graphics": true,
"images": true,
"labels": true,
"lockedItems": false,
"otherItems": true,
"pins": true,
"symbols": true,
"text": true,
"wires": true
}
}
}

View File

@ -3,14 +3,17 @@
"3dviewports": [],
"design_settings": {
"defaults": {
"board_outline_line_width": 0.09999999999999999,
"copper_line_width": 0.19999999999999998,
"apply_defaults_to_fp_fields": false,
"apply_defaults_to_fp_shapes": false,
"apply_defaults_to_fp_text": false,
"board_outline_line_width": 0.1,
"copper_line_width": 0.2,
"copper_text_italic": false,
"copper_text_size_h": 1.5,
"copper_text_size_v": 1.5,
"copper_text_thickness": 0.3,
"copper_text_upright": false,
"courtyard_line_width": 0.049999999999999996,
"courtyard_line_width": 0.05,
"dimension_precision": 4,
"dimension_units": 3,
"dimensions": {
@ -21,7 +24,7 @@
"text_position": 0,
"units_format": 1
},
"fab_line_width": 0.09999999999999999,
"fab_line_width": 0.1,
"fab_text_italic": false,
"fab_text_size_h": 1.0,
"fab_text_size_v": 1.0,
@ -66,15 +69,20 @@
"copper_edge_clearance": "error",
"copper_sliver": "warning",
"courtyards_overlap": "error",
"creepage": "error",
"diff_pair_gap_out_of_range": "error",
"diff_pair_uncoupled_length_too_long": "error",
"drill_out_of_range": "error",
"duplicate_footprints": "warning",
"extra_footprint": "warning",
"footprint": "error",
"footprint_filters_mismatch": "ignore",
"footprint_symbol_mismatch": "warning",
"footprint_type_mismatch": "ignore",
"hole_clearance": "error",
"hole_near_hole": "error",
"hole_to_hole": "error",
"holes_co_located": "warning",
"invalid_outline": "error",
"isolated_copper": "warning",
"item_on_disabled_layer": "error",
@ -84,9 +92,11 @@
"lib_footprint_mismatch": "warning",
"malformed_courtyard": "error",
"microvia_drill_out_of_range": "error",
"mirrored_text_on_front_layer": "warning",
"missing_courtyard": "ignore",
"missing_footprint": "warning",
"net_conflict": "warning",
"nonmirrored_text_on_back_layer": "warning",
"npth_inside_courtyard": "ignore",
"padstack": "warning",
"pth_inside_courtyard": "ignore",
@ -98,10 +108,13 @@
"solder_mask_bridge": "error",
"starved_thermal": "error",
"text_height": "warning",
"text_on_edge_cuts": "error",
"text_thickness": "warning",
"through_hole_pad_without_hole": "error",
"too_many_vias": "error",
"track_angle": "error",
"track_dangling": "warning",
"track_segment_length": "error",
"track_width": "error",
"tracks_crossing": "error",
"unconnected_items": "error",
@ -114,59 +127,64 @@
"min_clearance": 0.0,
"min_connection": 0.0,
"min_copper_edge_clearance": 0.0,
"min_groove_width": 0.0,
"min_hole_clearance": 0.25,
"min_hole_to_hole": 0.25,
"min_microvia_diameter": 0.19999999999999998,
"min_microvia_drill": 0.09999999999999999,
"min_microvia_diameter": 0.2,
"min_microvia_drill": 0.1,
"min_resolved_spokes": 2,
"min_silk_clearance": 0.0,
"min_text_height": 0.7999999999999999,
"min_text_height": 0.8,
"min_text_thickness": 0.08,
"min_through_hole_diameter": 0.3,
"min_track_width": 0.0,
"min_via_annular_width": 0.09999999999999999,
"min_via_annular_width": 0.1,
"min_via_diameter": 0.5,
"solder_mask_clearance": 0.0,
"solder_mask_min_width": 0.0,
"solder_mask_to_copper_clearance": 0.0,
"solder_mask_to_copper_clearance": 0.005,
"use_height_for_length_calcs": true
},
"teardrop_options": [
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 5,
"td_on_pad_in_zone": false,
"td_onpadsmd": true,
"td_onpthpad": true,
"td_onroundshapesonly": false,
"td_onsmdpad": true,
"td_ontrackend": false,
"td_onviapad": true
"td_onvia": true
}
],
"teardrop_parameters": [
{
"td_curve_segcount": 0,
"td_allow_use_two_tracks": true,
"td_curve_segcount": 1,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_round_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_curve_segcount": 0,
"td_allow_use_two_tracks": true,
"td_curve_segcount": 1,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_rect_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_curve_segcount": 0,
"td_allow_use_two_tracks": true,
"td_curve_segcount": 1,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_track_end",
"td_width_to_size_filter_ratio": 0.9
}
@ -174,6 +192,32 @@
"track_widths": [
0.0
],
"tuning_pattern_settings": {
"diff_pair_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 1.0
},
"diff_pair_skew_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
},
"single_track_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
}
},
"via_dimensions": [
{
"diameter": 0.0,
@ -189,6 +233,7 @@
"mfg": "",
"mpn": ""
},
"layer_pairs": [],
"layer_presets": [],
"viewports": []
},
@ -383,10 +428,15 @@
"duplicate_sheet_names": "error",
"endpoint_off_grid": "warning",
"extra_units": "error",
"footprint_filter": "ignore",
"footprint_link_issues": "warning",
"four_way_junction": "ignore",
"global_label_dangling": "warning",
"hier_label_mismatch": "error",
"label_dangling": "error",
"label_multiple_wires": "warning",
"lib_symbol_issues": "warning",
"lib_symbol_mismatch": "warning",
"missing_bidi_pin": "warning",
"missing_input_pin": "warning",
"missing_power_pin": "error",
@ -399,9 +449,15 @@
"pin_not_driven": "error",
"pin_to_pin": "warning",
"power_pin_not_driven": "error",
"same_local_global_label": "warning",
"similar_label_and_power": "warning",
"similar_labels": "warning",
"similar_power": "warning",
"simulation_model_issue": "error",
"single_global_label": "ignore",
"unannotated": "error",
"unconnected_wire_endpoint": "warning",
"undefined_netclass": "error",
"unit_value_mismatch": "error",
"unresolved_variable": "error",
"wire_dangling": "error"
@ -413,7 +469,7 @@
},
"meta": {
"filename": "cmdr-joystick.kicad_pro",
"version": 1
"version": 3
},
"net_settings": {
"classes": [
@ -428,6 +484,7 @@
"microvia_drill": 0.1,
"name": "Default",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 2147483647,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.25,
"via_diameter": 0.8,
@ -445,6 +502,7 @@
"microvia_drill": 0.1,
"name": "+3.3V",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 0,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.5,
"via_diameter": 0.8,
@ -462,6 +520,7 @@
"microvia_drill": 0.1,
"name": "GND",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 1,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.5,
"via_diameter": 0.8,
@ -470,7 +529,7 @@
}
],
"meta": {
"version": 3
"version": 4
},
"net_colors": null,
"netclass_assignments": null,
@ -505,6 +564,78 @@
},
"schematic": {
"annotate_start_num": 0,
"bom_export_filename": "${PROJECTNAME}.csv",
"bom_fmt_presets": [],
"bom_fmt_settings": {
"field_delimiter": ",",
"keep_line_breaks": false,
"keep_tabs": false,
"name": "CSV",
"ref_delimiter": ",",
"ref_range_delimiter": "",
"string_delimiter": "\""
},
"bom_presets": [],
"bom_settings": {
"exclude_dnp": false,
"fields_ordered": [
{
"group_by": false,
"label": "Reference",
"name": "Reference",
"show": true
},
{
"group_by": false,
"label": "Qty",
"name": "${QUANTITY}",
"show": true
},
{
"group_by": true,
"label": "Value",
"name": "Value",
"show": true
},
{
"group_by": true,
"label": "DNP",
"name": "${DNP}",
"show": true
},
{
"group_by": true,
"label": "Exclude from BOM",
"name": "${EXCLUDE_FROM_BOM}",
"show": true
},
{
"group_by": true,
"label": "Exclude from Board",
"name": "${EXCLUDE_FROM_BOARD}",
"show": true
},
{
"group_by": true,
"label": "Footprint",
"name": "Footprint",
"show": true
},
{
"group_by": false,
"label": "Datasheet",
"name": "Datasheet",
"show": true
}
],
"filter_string": "",
"group_symbols": true,
"include_excluded_from_bom": true,
"name": "Default Editing",
"sort_asc": true,
"sort_field": "Reference"
},
"connection_grid_size": 50.0,
"drawing": {
"dashed_lines_dash_length_ratio": 12.0,
"dashed_lines_gap_length_ratio": 3.0,
@ -518,6 +649,11 @@
"intersheets_ref_suffix": "",
"junction_size_choice": 3,
"label_size_ratio": 0.375,
"operating_point_overlay_i_precision": 3,
"operating_point_overlay_i_range": "~A",
"operating_point_overlay_v_precision": 3,
"operating_point_overlay_v_range": "~V",
"overbar_offset_ratio": 1.23,
"pin_symbol_size": 25.0,
"text_offset_ratio": 0.15
},
@ -529,10 +665,12 @@
"net_format_name": "",
"page_layout_descr_file": "",
"plot_directory": "",
"space_save_all_events": true,
"spice_current_sheet_as_root": false,
"spice_external_command": "spice \"%I\"",
"spice_model_current_sheet_as_root": true,
"spice_save_all_currents": false,
"spice_save_all_dissipations": false,
"spice_save_all_voltages": false,
"subpart_first_id": 65,
"subpart_id_separator": 0
@ -540,7 +678,7 @@
"sheets": [
[
"5b501981-46e2-4084-afad-38073ca78ebd",
""
"Root"
]
],
"text_variables": {}

View File

@ -1,13 +1,15 @@
(kicad_sch
(version 20231120)
(version 20250114)
(generator "eeschema")
(generator_version "8.0")
(generator_version "9.0")
(uuid "5b501981-46e2-4084-afad-38073ca78ebd")
(paper "A4")
(lib_symbols
(symbol "Connector_Generic:Conn_01x03"
(pin_names
(offset 1.016) hide)
(offset 1.016)
(hide yes)
)
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
@ -74,8 +76,19 @@
)
(symbol "Conn_01x03_1_1"
(rectangle
(start -1.27 -2.413)
(end 0 -2.667)
(start -1.27 3.81)
(end 1.27 -3.81)
(stroke
(width 0.254)
(type default)
)
(fill
(type background)
)
)
(rectangle
(start -1.27 2.667)
(end 0 2.413)
(stroke
(width 0.1524)
(type default)
@ -96,8 +109,8 @@
)
)
(rectangle
(start -1.27 2.667)
(end 0 2.413)
(start -1.27 -2.413)
(end 0 -2.667)
(stroke
(width 0.1524)
(type default)
@ -106,17 +119,6 @@
(type none)
)
)
(rectangle
(start -1.27 3.81)
(end 1.27 -3.81)
(stroke
(width 0.254)
(type default)
)
(fill
(type background)
)
)
(pin passive line
(at -5.08 2.54 0)
(length 3.81)
@ -172,10 +174,13 @@
)
)
)
(embedded_fonts no)
)
(symbol "Connector_Generic:Conn_01x04"
(pin_names
(offset 1.016) hide)
(offset 1.016)
(hide yes)
)
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
@ -242,19 +247,19 @@
)
(symbol "Conn_01x04_1_1"
(rectangle
(start -1.27 -4.953)
(end 0 -5.207)
(start -1.27 3.81)
(end 1.27 -6.35)
(stroke
(width 0.1524)
(width 0.254)
(type default)
)
(fill
(type none)
(type background)
)
)
(rectangle
(start -1.27 -2.413)
(end 0 -2.667)
(start -1.27 2.667)
(end 0 2.413)
(stroke
(width 0.1524)
(type default)
@ -275,8 +280,8 @@
)
)
(rectangle
(start -1.27 2.667)
(end 0 2.413)
(start -1.27 -2.413)
(end 0 -2.667)
(stroke
(width 0.1524)
(type default)
@ -286,14 +291,14 @@
)
)
(rectangle
(start -1.27 3.81)
(end 1.27 -6.35)
(start -1.27 -4.953)
(end 0 -5.207)
(stroke
(width 0.254)
(width 0.1524)
(type default)
)
(fill
(type background)
(type none)
)
)
(pin passive line
@ -369,10 +374,13 @@
)
)
)
(embedded_fonts no)
)
(symbol "Connector_Generic:Conn_01x06"
(pin_names
(offset 1.016) hide)
(offset 1.016)
(hide yes)
)
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
@ -439,41 +447,19 @@
)
(symbol "Conn_01x06_1_1"
(rectangle
(start -1.27 -7.493)
(end 0 -7.747)
(start -1.27 6.35)
(end 1.27 -8.89)
(stroke
(width 0.1524)
(width 0.254)
(type default)
)
(fill
(type none)
(type background)
)
)
(rectangle
(start -1.27 -4.953)
(end 0 -5.207)
(stroke
(width 0.1524)
(type default)
)
(fill
(type none)
)
)
(rectangle
(start -1.27 -2.413)
(end 0 -2.667)
(stroke
(width 0.1524)
(type default)
)
(fill
(type none)
)
)
(rectangle
(start -1.27 0.127)
(end 0 -0.127)
(start -1.27 5.207)
(end 0 4.953)
(stroke
(width 0.1524)
(type default)
@ -494,8 +480,8 @@
)
)
(rectangle
(start -1.27 5.207)
(end 0 4.953)
(start -1.27 0.127)
(end 0 -0.127)
(stroke
(width 0.1524)
(type default)
@ -505,14 +491,36 @@
)
)
(rectangle
(start -1.27 6.35)
(end 1.27 -8.89)
(start -1.27 -2.413)
(end 0 -2.667)
(stroke
(width 0.254)
(width 0.1524)
(type default)
)
(fill
(type background)
(type none)
)
)
(rectangle
(start -1.27 -4.953)
(end 0 -5.207)
(stroke
(width 0.1524)
(type default)
)
(fill
(type none)
)
)
(rectangle
(start -1.27 -7.493)
(end 0 -7.747)
(stroke
(width 0.1524)
(type default)
)
(fill
(type none)
)
)
(pin passive line
@ -624,10 +632,213 @@
)
)
)
(embedded_fonts no)
)
(symbol "Connector_Generic:Conn_02x02_Odd_Even"
(pin_names
(offset 1.016)
(hide yes)
)
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
(property "Reference" "J"
(at 1.27 2.54 0)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Value" "Conn_02x02_Odd_Even"
(at 1.27 -5.08 0)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Footprint" ""
(at 0 0 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Datasheet" "~"
(at 0 0 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Description" "Generic connector, double row, 02x02, odd/even pin numbering scheme (row 1 odd numbers, row 2 even numbers), script generated (kicad-library-utils/schlib/autogen/connector/)"
(at 0 0 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "ki_keywords" "connector"
(at 0 0 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "ki_fp_filters" "Connector*:*_2x??_*"
(at 0 0 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(symbol "Conn_02x02_Odd_Even_1_1"
(rectangle
(start -1.27 1.27)
(end 3.81 -3.81)
(stroke
(width 0.254)
(type default)
)
(fill
(type background)
)
)
(rectangle
(start -1.27 0.127)
(end 0 -0.127)
(stroke
(width 0.1524)
(type default)
)
(fill
(type none)
)
)
(rectangle
(start -1.27 -2.413)
(end 0 -2.667)
(stroke
(width 0.1524)
(type default)
)
(fill
(type none)
)
)
(rectangle
(start 3.81 0.127)
(end 2.54 -0.127)
(stroke
(width 0.1524)
(type default)
)
(fill
(type none)
)
)
(rectangle
(start 3.81 -2.413)
(end 2.54 -2.667)
(stroke
(width 0.1524)
(type default)
)
(fill
(type none)
)
)
(pin passive line
(at -5.08 0 0)
(length 3.81)
(name "Pin_1"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "1"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin passive line
(at -5.08 -2.54 0)
(length 3.81)
(name "Pin_3"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "3"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin passive line
(at 7.62 0 180)
(length 3.81)
(name "Pin_2"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "2"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin passive line
(at 7.62 -2.54 180)
(length 3.81)
(name "Pin_4"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "4"
(effects
(font
(size 1.27 1.27)
)
)
)
)
)
(embedded_fonts no)
)
(symbol "Connector_Generic:Conn_02x05_Odd_Even"
(pin_names
(offset 1.016) hide)
(offset 1.016)
(hide yes)
)
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
@ -694,30 +905,19 @@
)
(symbol "Conn_02x05_Odd_Even_1_1"
(rectangle
(start -1.27 -4.953)
(end 0 -5.207)
(start -1.27 6.35)
(end 3.81 -6.35)
(stroke
(width 0.1524)
(width 0.254)
(type default)
)
(fill
(type none)
(type background)
)
)
(rectangle
(start -1.27 -2.413)
(end 0 -2.667)
(stroke
(width 0.1524)
(type default)
)
(fill
(type none)
)
)
(rectangle
(start -1.27 0.127)
(end 0 -0.127)
(start -1.27 5.207)
(end 0 4.953)
(stroke
(width 0.1524)
(type default)
@ -738,8 +938,8 @@
)
)
(rectangle
(start -1.27 5.207)
(end 0 4.953)
(start -1.27 0.127)
(end 0 -0.127)
(stroke
(width 0.1524)
(type default)
@ -749,19 +949,8 @@
)
)
(rectangle
(start -1.27 6.35)
(end 3.81 -6.35)
(stroke
(width 0.254)
(type default)
)
(fill
(type background)
)
)
(rectangle
(start 3.81 -4.953)
(end 2.54 -5.207)
(start -1.27 -2.413)
(end 0 -2.667)
(stroke
(width 0.1524)
(type default)
@ -771,8 +960,8 @@
)
)
(rectangle
(start 3.81 -2.413)
(end 2.54 -2.667)
(start -1.27 -4.953)
(end 0 -5.207)
(stroke
(width 0.1524)
(type default)
@ -782,8 +971,8 @@
)
)
(rectangle
(start 3.81 0.127)
(end 2.54 -0.127)
(start 3.81 5.207)
(end 2.54 4.953)
(stroke
(width 0.1524)
(type default)
@ -804,8 +993,30 @@
)
)
(rectangle
(start 3.81 5.207)
(end 2.54 4.953)
(start 3.81 0.127)
(end 2.54 -0.127)
(stroke
(width 0.1524)
(type default)
)
(fill
(type none)
)
)
(rectangle
(start 3.81 -2.413)
(end 2.54 -2.667)
(stroke
(width 0.1524)
(type default)
)
(fill
(type none)
)
)
(rectangle
(start 3.81 -4.953)
(end 2.54 -5.207)
(stroke
(width 0.1524)
(type default)
@ -832,42 +1043,6 @@
)
)
)
(pin passive line
(at 7.62 -5.08 180)
(length 3.81)
(name "Pin_10"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "10"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin passive line
(at 7.62 5.08 180)
(length 3.81)
(name "Pin_2"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "2"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin passive line
(at -5.08 2.54 0)
(length 3.81)
@ -886,24 +1061,6 @@
)
)
)
(pin passive line
(at 7.62 2.54 180)
(length 3.81)
(name "Pin_4"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "4"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin passive line
(at -5.08 0 0)
(length 3.81)
@ -922,24 +1079,6 @@
)
)
)
(pin passive line
(at 7.62 0 180)
(length 3.81)
(name "Pin_6"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "6"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin passive line
(at -5.08 -2.54 0)
(length 3.81)
@ -958,6 +1097,78 @@
)
)
)
(pin passive line
(at -5.08 -5.08 0)
(length 3.81)
(name "Pin_9"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "9"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin passive line
(at 7.62 5.08 180)
(length 3.81)
(name "Pin_2"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "2"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin passive line
(at 7.62 2.54 180)
(length 3.81)
(name "Pin_4"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "4"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin passive line
(at 7.62 0 180)
(length 3.81)
(name "Pin_6"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "6"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin passive line
(at 7.62 -2.54 180)
(length 3.81)
@ -977,16 +1188,16 @@
)
)
(pin passive line
(at -5.08 -5.08 0)
(at 7.62 -5.08 180)
(length 3.81)
(name "Pin_9"
(name "Pin_10"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "9"
(number "10"
(effects
(font
(size 1.27 1.27)
@ -995,9 +1206,12 @@
)
)
)
(embedded_fonts no)
)
(symbol "Device:C"
(pin_numbers hide)
(pin_numbers
(hide yes)
)
(pin_names
(offset 0.254)
)
@ -1070,7 +1284,7 @@
(symbol "C_0_1"
(polyline
(pts
(xy -2.032 -0.762) (xy 2.032 -0.762)
(xy -2.032 0.762) (xy 2.032 0.762)
)
(stroke
(width 0.508)
@ -1082,7 +1296,7 @@
)
(polyline
(pts
(xy -2.032 0.762) (xy 2.032 0.762)
(xy -2.032 -0.762) (xy 2.032 -0.762)
)
(stroke
(width 0.508)
@ -1131,9 +1345,12 @@
)
)
)
(embedded_fonts no)
)
(symbol "Device:R"
(pin_numbers hide)
(pin_numbers
(hide yes)
)
(pin_names
(offset 0)
)
@ -1252,10 +1469,15 @@
)
)
)
(embedded_fonts no)
)
(symbol "Diode:1N4148W"
(pin_numbers hide)
(pin_names hide)
(pin_numbers
(hide yes)
)
(pin_names
(hide yes)
)
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
@ -1351,10 +1573,10 @@
)
(polyline
(pts
(xy 1.27 0) (xy -1.27 0)
(xy 1.27 1.27) (xy 1.27 -1.27) (xy -1.27 0) (xy 1.27 1.27)
)
(stroke
(width 0)
(width 0.254)
(type default)
)
(fill
@ -1363,10 +1585,10 @@
)
(polyline
(pts
(xy 1.27 1.27) (xy 1.27 -1.27) (xy -1.27 0) (xy 1.27 1.27)
(xy 1.27 0) (xy -1.27 0)
)
(stroke
(width 0.254)
(width 0)
(type default)
)
(fill
@ -1412,6 +1634,7 @@
)
)
)
(embedded_fonts no)
)
(symbol "Mechanical:MountingHole"
(pin_names
@ -1494,6 +1717,7 @@
)
)
)
(embedded_fonts no)
)
(symbol "Memory_EEPROM:M24C02-FMN"
(exclude_from_sim no)
@ -1628,6 +1852,24 @@
)
)
)
(pin power_in line
(at 0 7.62 270)
(length 2.54)
(name "VCC"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "8"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin power_in line
(at 0 -7.62 90)
(length 2.54)
@ -1700,28 +1942,13 @@
)
)
)
(pin power_in line
(at 0 7.62 270)
(length 2.54)
(name "VCC"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "8"
(effects
(font
(size 1.27 1.27)
)
)
)
)
)
(embedded_fonts no)
)
(symbol "Transistor_FET:2N7002E"
(pin_names hide)
(pin_names
(hide yes)
)
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
@ -1792,18 +2019,6 @@
)
)
(symbol "2N7002E_0_1"
(polyline
(pts
(xy 0.254 0) (xy -2.54 0)
)
(stroke
(width 0)
(type default)
)
(fill
(type none)
)
)
(polyline
(pts
(xy 0.254 1.905) (xy 0.254 -1.905)
@ -1818,7 +2033,19 @@
)
(polyline
(pts
(xy 0.762 -1.27) (xy 0.762 -2.286)
(xy 0.254 0) (xy -2.54 0)
)
(stroke
(width 0)
(type default)
)
(fill
(type none)
)
)
(polyline
(pts
(xy 0.762 2.286) (xy 0.762 1.27)
)
(stroke
(width 0.254)
@ -1842,7 +2069,7 @@
)
(polyline
(pts
(xy 0.762 2.286) (xy 0.762 1.27)
(xy 0.762 -1.27) (xy 0.762 -2.286)
)
(stroke
(width 0.254)
@ -1852,30 +2079,6 @@
(type none)
)
)
(polyline
(pts
(xy 2.54 2.54) (xy 2.54 1.778)
)
(stroke
(width 0)
(type default)
)
(fill
(type none)
)
)
(polyline
(pts
(xy 2.54 -2.54) (xy 2.54 0) (xy 0.762 0)
)
(stroke
(width 0)
(type default)
)
(fill
(type none)
)
)
(polyline
(pts
(xy 0.762 -1.778) (xy 3.302 -1.778) (xy 3.302 1.778) (xy 0.762 1.778)
@ -1900,6 +2103,63 @@
(type outline)
)
)
(circle
(center 1.651 0)
(radius 2.794)
(stroke
(width 0.254)
(type default)
)
(fill
(type none)
)
)
(polyline
(pts
(xy 2.54 2.54) (xy 2.54 1.778)
)
(stroke
(width 0)
(type default)
)
(fill
(type none)
)
)
(circle
(center 2.54 1.778)
(radius 0.254)
(stroke
(width 0)
(type default)
)
(fill
(type outline)
)
)
(circle
(center 2.54 -1.778)
(radius 0.254)
(stroke
(width 0)
(type default)
)
(fill
(type outline)
)
)
(polyline
(pts
(xy 2.54 -2.54) (xy 2.54 0) (xy 0.762 0)
)
(stroke
(width 0)
(type default)
)
(fill
(type none)
)
)
(polyline
(pts
(xy 2.794 0.508) (xy 2.921 0.381) (xy 3.683 0.381) (xy 3.81 0.254)
@ -1924,39 +2184,6 @@
(type none)
)
)
(circle
(center 1.651 0)
(radius 2.794)
(stroke
(width 0.254)
(type default)
)
(fill
(type none)
)
)
(circle
(center 2.54 -1.778)
(radius 0.254)
(stroke
(width 0)
(type default)
)
(fill
(type outline)
)
)
(circle
(center 2.54 1.778)
(radius 0.254)
(stroke
(width 0)
(type default)
)
(fill
(type outline)
)
)
)
(symbol "2N7002E_1_1"
(pin input line
@ -1977,24 +2204,6 @@
)
)
)
(pin passive line
(at 2.54 -5.08 90)
(length 2.54)
(name "S"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "2"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin passive line
(at 2.54 5.08 270)
(length 2.54)
@ -2013,10 +2222,31 @@
)
)
)
(pin passive line
(at 2.54 -5.08 90)
(length 2.54)
(name "S"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "2"
(effects
(font
(size 1.27 1.27)
)
)
)
)
)
(embedded_fonts no)
)
(symbol "Transistor_FET:Si2371EDS"
(pin_names hide)
(pin_names
(hide yes)
)
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
@ -2087,6 +2317,18 @@
)
)
(symbol "Si2371EDS_0_1"
(polyline
(pts
(xy 0.254 1.905) (xy 0.254 -1.905)
)
(stroke
(width 0.254)
(type default)
)
(fill
(type none)
)
)
(polyline
(pts
(xy 0.254 0) (xy -2.54 0)
@ -2101,7 +2343,31 @@
)
(polyline
(pts
(xy 0.254 1.905) (xy 0.254 -1.905)
(xy 0.762 2.286) (xy 0.762 1.27)
)
(stroke
(width 0.254)
(type default)
)
(fill
(type none)
)
)
(polyline
(pts
(xy 0.762 1.778) (xy 3.302 1.778) (xy 3.302 -1.778) (xy 0.762 -1.778)
)
(stroke
(width 0)
(type default)
)
(fill
(type none)
)
)
(polyline
(pts
(xy 0.762 0.508) (xy 0.762 -0.508)
)
(stroke
(width 0.254)
@ -2123,10 +2389,9 @@
(type none)
)
)
(polyline
(pts
(xy 0.762 0.508) (xy 0.762 -0.508)
)
(circle
(center 1.651 0)
(radius 2.794)
(stroke
(width 0.254)
(type default)
@ -2135,54 +2400,6 @@
(type none)
)
)
(polyline
(pts
(xy 0.762 2.286) (xy 0.762 1.27)
)
(stroke
(width 0.254)
(type default)
)
(fill
(type none)
)
)
(polyline
(pts
(xy 2.54 2.54) (xy 2.54 1.778)
)
(stroke
(width 0)
(type default)
)
(fill
(type none)
)
)
(polyline
(pts
(xy 2.54 -2.54) (xy 2.54 0) (xy 0.762 0)
)
(stroke
(width 0)
(type default)
)
(fill
(type none)
)
)
(polyline
(pts
(xy 0.762 1.778) (xy 3.302 1.778) (xy 3.302 -1.778) (xy 0.762 -1.778)
)
(stroke
(width 0)
(type default)
)
(fill
(type none)
)
)
(polyline
(pts
(xy 2.286 0) (xy 1.27 0.381) (xy 1.27 -0.381) (xy 2.286 0)
@ -2195,6 +2412,52 @@
(type outline)
)
)
(polyline
(pts
(xy 2.54 2.54) (xy 2.54 1.778)
)
(stroke
(width 0)
(type default)
)
(fill
(type none)
)
)
(circle
(center 2.54 1.778)
(radius 0.254)
(stroke
(width 0)
(type default)
)
(fill
(type outline)
)
)
(circle
(center 2.54 -1.778)
(radius 0.254)
(stroke
(width 0)
(type default)
)
(fill
(type outline)
)
)
(polyline
(pts
(xy 2.54 -2.54) (xy 2.54 0) (xy 0.762 0)
)
(stroke
(width 0)
(type default)
)
(fill
(type none)
)
)
(polyline
(pts
(xy 2.794 -0.508) (xy 2.921 -0.381) (xy 3.683 -0.381) (xy 3.81 -0.254)
@ -2219,39 +2482,6 @@
(type none)
)
)
(circle
(center 1.651 0)
(radius 2.794)
(stroke
(width 0.254)
(type default)
)
(fill
(type none)
)
)
(circle
(center 2.54 -1.778)
(radius 0.254)
(stroke
(width 0)
(type default)
)
(fill
(type outline)
)
)
(circle
(center 2.54 1.778)
(radius 0.254)
(stroke
(width 0)
(type default)
)
(fill
(type outline)
)
)
)
(symbol "Si2371EDS_1_1"
(pin input line
@ -2272,24 +2502,6 @@
)
)
)
(pin passive line
(at 2.54 -5.08 90)
(length 2.54)
(name "S"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "2"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin passive line
(at 2.54 5.08 270)
(length 2.54)
@ -2308,8 +2520,27 @@
)
)
)
(pin passive line
(at 2.54 -5.08 90)
(length 2.54)
(name "S"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "2"
(effects
(font
(size 1.27 1.27)
)
)
)
)
)
(embedded_fonts no)
)
(symbol "cmdr-joystick:rp2040zero_upside_down"
(exclude_from_sim no)
(in_bom yes)
@ -2389,186 +2620,6 @@
)
)
)
(pin bidirectional line
(at -13.97 -35.56 0)
(length 2.54)
(name "GP9"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "10"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin bidirectional line
(at -13.97 -38.1 0)
(length 2.54)
(name "GP10"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "11"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin bidirectional line
(at -13.97 -40.64 0)
(length 2.54)
(name "GP11"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "12"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin bidirectional line
(at -13.97 -43.18 0)
(length 2.54)
(name "GP12"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "13"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin bidirectional line
(at -13.97 -45.72 0)
(length 2.54)
(name "GP13"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "14"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin bidirectional line
(at 12.7 -45.72 180)
(length 2.54)
(name "GP14"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "15"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin bidirectional line
(at 12.7 -43.18 180)
(length 2.54)
(name "GP15"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "16"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin bidirectional line
(at 12.7 -17.78 180)
(length 2.54)
(name "GP26"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "17"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin bidirectional line
(at 12.7 -15.24 180)
(length 2.54)
(name "GP27"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "18"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin bidirectional line
(at 12.7 -12.7 180)
(length 2.54)
(name "GP28"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "19"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin bidirectional line
(at -13.97 -12.7 0)
(length 2.54)
@ -2587,78 +2638,6 @@
)
)
)
(pin bidirectional line
(at 12.7 -10.16 180)
(length 2.54)
(name "GP29"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "20"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin power_out line
(at 2.54 -2.54 270)
(length 2.54)
(name "3V3"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "21"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin power_out line
(at 0 -52.07 90)
(length 2.54)
(name "GND"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "22"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin power_in line
(at -2.54 -2.54 270)
(length 2.54)
(name "5V"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "23"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin bidirectional line
(at -13.97 -15.24 0)
(length 2.54)
@ -2785,8 +2764,261 @@
)
)
)
(pin bidirectional line
(at -13.97 -35.56 0)
(length 2.54)
(name "GP9"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "10"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin bidirectional line
(at -13.97 -38.1 0)
(length 2.54)
(name "GP10"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "11"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin bidirectional line
(at -13.97 -40.64 0)
(length 2.54)
(name "GP11"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "12"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin bidirectional line
(at -13.97 -43.18 0)
(length 2.54)
(name "GP12"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "13"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin bidirectional line
(at -13.97 -45.72 0)
(length 2.54)
(name "GP13"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "14"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin power_in line
(at -2.54 -2.54 270)
(length 2.54)
(name "5V"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "23"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin power_out line
(at 0 -52.07 90)
(length 2.54)
(name "GND"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "22"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin power_out line
(at 2.54 -2.54 270)
(length 2.54)
(name "3V3"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "21"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin bidirectional line
(at 12.7 -10.16 180)
(length 2.54)
(name "GP29"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "20"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin bidirectional line
(at 12.7 -12.7 180)
(length 2.54)
(name "GP28"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "19"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin bidirectional line
(at 12.7 -15.24 180)
(length 2.54)
(name "GP27"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "18"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin bidirectional line
(at 12.7 -17.78 180)
(length 2.54)
(name "GP26"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "17"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin bidirectional line
(at 12.7 -43.18 180)
(length 2.54)
(name "GP15"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "16"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin bidirectional line
(at 12.7 -45.72 180)
(length 2.54)
(name "GP14"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "15"
(effects
(font
(size 1.27 1.27)
)
)
)
)
)
(embedded_fonts no)
)
(symbol "power:+3.3V"
(power)
(pin_names
@ -2863,7 +3095,7 @@
)
(polyline
(pts
(xy 0 0) (xy 0 2.54)
(xy 0 2.54) (xy 0.762 1.27)
)
(stroke
(width 0)
@ -2875,7 +3107,7 @@
)
(polyline
(pts
(xy 0 2.54) (xy 0.762 1.27)
(xy 0 0) (xy 0 2.54)
)
(stroke
(width 0)
@ -2889,7 +3121,8 @@
(symbol "+3.3V_1_1"
(pin power_in line
(at 0 0 90)
(length 0) hide
(length 0)
(hide yes)
(name "+3.3V"
(effects
(font
@ -2906,6 +3139,7 @@
)
)
)
(embedded_fonts no)
)
(symbol "power:+5V"
(power)
@ -2983,7 +3217,7 @@
)
(polyline
(pts
(xy 0 0) (xy 0 2.54)
(xy 0 2.54) (xy 0.762 1.27)
)
(stroke
(width 0)
@ -2995,7 +3229,7 @@
)
(polyline
(pts
(xy 0 2.54) (xy 0.762 1.27)
(xy 0 0) (xy 0 2.54)
)
(stroke
(width 0)
@ -3009,7 +3243,8 @@
(symbol "+5V_1_1"
(pin power_in line
(at 0 0 90)
(length 0) hide
(length 0)
(hide yes)
(name "+5V"
(effects
(font
@ -3026,6 +3261,7 @@
)
)
)
(embedded_fonts no)
)
(symbol "power:GND"
(power)
@ -3105,7 +3341,8 @@
(symbol "GND_1_1"
(pin power_in line
(at 0 0 270)
(length 0) hide
(length 0)
(hide yes)
(name "GND"
(effects
(font
@ -3122,8 +3359,130 @@
)
)
)
(embedded_fonts no)
)
)
(text "Gimbal Y2 (Right)"
(exclude_from_sim no)
(at 223.52 76.2 0)
(effects
(font
(size 2.27 2.27)
)
(justify left bottom)
)
(uuid "0666d517-3399-4e5a-a88e-28cb046bab53")
)
(text "Right \nHat Switch\n20-24"
(exclude_from_sim no)
(at 50.8 160.02 0)
(effects
(font
(size 2.27 2.27)
)
(justify right bottom)
)
(uuid "28c6deef-0288-400d-b950-adf2dba1adf7")
)
(text "ELRS TX\n"
(exclude_from_sim no)
(at 254 132.08 0)
(effects
(font
(size 2.27 2.27)
)
(justify left bottom)
)
(uuid "666e68ae-3562-4b09-b825-c9f5445176d4")
)
(text "Right\nTop\nButtons\n10-14\n"
(exclude_from_sim no)
(at 38.1 110.49 0)
(effects
(font
(size 2.27 2.27)
)
(justify right bottom)
)
(uuid "76352af5-c5ca-48fd-bebd-b8161cb04ff3")
)
(text "Left \nHat Switch\n15-19"
(exclude_from_sim no)
(at 50.8 139.7 0)
(effects
(font
(size 2.27 2.27)
)
(justify right bottom)
)
(uuid "921cb360-d960-4007-bfc3-e8983d17359f")
)
(text "Gimbal X2 (Right)"
(exclude_from_sim no)
(at 223.52 64.77 0)
(effects
(font
(size 2.27 2.27)
)
(justify left bottom)
)
(uuid "9ef99c88-97e7-430c-98f1-8b504d8de253")
)
(text "Left\nTop\nButtons\n5-9"
(exclude_from_sim no)
(at 36.83 82.55 0)
(effects
(font
(size 2.27 2.27)
)
(justify right bottom)
)
(uuid "9febdcbd-7a0f-4f31-b915-6903ca937f26")
)
(text "Bottom\nChassis\nButtons\n5-6"
(exclude_from_sim no)
(at 36.83 32.258 0)
(effects
(font
(size 2.27 2.27)
)
(justify right bottom)
)
(uuid "a01c3eae-3b54-4f4d-8544-9bb283418e68")
)
(text "Bottom\nChassis\nButtons\n0-4"
(exclude_from_sim no)
(at 36.83 54.61 0)
(effects
(font
(size 2.27 2.27)
)
(justify right bottom)
)
(uuid "c7b70dbb-f435-4a57-aa9c-8cf50da328cb")
)
(text "Gimbal X1 (Left)"
(exclude_from_sim no)
(at 223.52 41.91 0)
(effects
(font
(size 2.27 2.27)
)
(justify left bottom)
)
(uuid "d87b3a29-cc14-4431-b108-be87ff886dfb")
)
(text "Gimbal Y1 (Left)"
(exclude_from_sim no)
(at 223.52 53.34 0)
(effects
(font
(size 2.27 2.27)
)
(justify left bottom)
)
(uuid "ed3a0309-c84e-411e-aaa8-eac48814b746")
)
(junction
(at 97.79 71.12)
(diameter 0)
@ -3256,6 +3615,12 @@
(color 0 0 0 0)
(uuid "5b1e52ec-e5c1-4338-b386-b5c2d81ad7ed")
)
(junction
(at 40.64 24.13)
(diameter 0)
(color 0 0 0 0)
(uuid "63844187-657a-4fea-97f7-46763266139a")
)
(junction
(at 41.91 50.8)
(diameter 0)
@ -3280,6 +3645,12 @@
(color 0 0 0 0)
(uuid "80b9b240-83f2-4205-8a8b-dd8408d7c939")
)
(junction
(at 92.71 45.72)
(diameter 0)
(color 0 0 0 0)
(uuid "81c28855-7c0b-4806-9f5d-0386f4f72642")
)
(junction
(at 92.71 73.66)
(diameter 0)
@ -3346,6 +3717,12 @@
(color 0 0 0 0)
(uuid "b77ee733-b0ea-4468-af81-4eeef3e05d6c")
)
(junction
(at 90.17 43.18)
(diameter 0)
(color 0 0 0 0)
(uuid "b93b8bb6-b0e4-4d98-b6cc-77758c859d05")
)
(junction
(at 95.25 135.89)
(diameter 0)
@ -3428,10 +3805,6 @@
(at 165.1 124.46)
(uuid "00ab5998-bff9-45da-8c86-2a50db2c2b90")
)
(no_connect
(at 140.97 45.72)
(uuid "baa7f766-2871-41cf-81d3-67fcad28f98c")
)
(wire
(pts
(xy 218.44 132.08) (xy 224.79 132.08)
@ -3532,6 +3905,16 @@
)
(uuid "0ded7b45-3a77-42b7-bea6-065ae269600e")
)
(wire
(pts
(xy 55.88 24.13) (xy 67.31 24.13)
)
(stroke
(width 0)
(type default)
)
(uuid "0e010c6e-4690-432d-97e1-98b2cd2f5098")
)
(wire
(pts
(xy 200.66 113.03) (xy 203.2 113.03)
@ -3582,6 +3965,16 @@
)
(uuid "174186dd-383b-40e2-a98a-27eb33a59b32")
)
(wire
(pts
(xy 40.64 26.67) (xy 40.64 24.13)
)
(stroke
(width 0)
(type default)
)
(uuid "17b24c28-e602-48eb-92c5-1138973e1139")
)
(wire
(pts
(xy 90.17 99.06) (xy 90.17 130.81)
@ -3612,6 +4005,16 @@
)
(uuid "1dd93b3c-9798-4dba-aa76-7cfe2eaf8f5d")
)
(wire
(pts
(xy 120.65 45.72) (xy 120.65 20.32)
)
(stroke
(width 0)
(type default)
)
(uuid "1e1df1b6-d2c1-493e-96f2-175cb587f0af")
)
(wire
(pts
(xy 140.97 53.34) (xy 118.11 53.34)
@ -3742,6 +4145,16 @@
)
(uuid "276ac9ec-ca3f-4ccc-970e-5137f354a728")
)
(wire
(pts
(xy 90.17 24.13) (xy 90.17 43.18)
)
(stroke
(width 0)
(type default)
)
(uuid "27e51e00-246f-4787-a420-019cbc1e374a")
)
(wire
(pts
(xy 205.74 74.93) (xy 210.82 74.93)
@ -3962,6 +4375,16 @@
)
(uuid "443508a4-1ea7-4f0b-8576-df4cdb389e90")
)
(wire
(pts
(xy 74.93 26.67) (xy 92.71 26.67)
)
(stroke
(width 0)
(type default)
)
(uuid "45fb3eb4-f681-4981-8480-6b69b0458b67")
)
(wire
(pts
(xy 63.5 161.29) (xy 76.2 161.29)
@ -3972,6 +4395,16 @@
)
(uuid "47125bf3-20b3-4f69-a167-7f586e4f0e51")
)
(wire
(pts
(xy 120.65 20.32) (xy 40.64 20.32)
)
(stroke
(width 0)
(type default)
)
(uuid "47c1de4d-563b-4827-83f9-db72ace93f3a")
)
(wire
(pts
(xy 43.18 71.12) (xy 41.91 71.12)
@ -4052,6 +4485,16 @@
)
(uuid "4ec8ca24-dd9d-4444-9c35-18f203382609")
)
(wire
(pts
(xy 43.18 26.67) (xy 40.64 26.67)
)
(stroke
(width 0)
(type default)
)
(uuid "4f9d939c-88c4-464e-85a9-1c941ed6ab37")
)
(wire
(pts
(xy 55.88 78.74) (xy 67.31 78.74)
@ -4062,6 +4505,16 @@
)
(uuid "511008af-9282-402b-914a-c6d0e45387fa")
)
(wire
(pts
(xy 40.64 24.13) (xy 43.18 24.13)
)
(stroke
(width 0)
(type default)
)
(uuid "5178c3ef-e7a2-4516-90c1-188bd509cadf")
)
(wire
(pts
(xy 92.71 66.04) (xy 140.97 66.04)
@ -4192,6 +4645,16 @@
)
(uuid "687ef4c8-f0d0-482a-a38e-d4472923058e")
)
(wire
(pts
(xy 74.93 24.13) (xy 90.17 24.13)
)
(stroke
(width 0)
(type default)
)
(uuid "69b13b44-605b-44a5-98ba-5b4ca660ef87")
)
(wire
(pts
(xy 63.5 151.13) (xy 76.2 151.13)
@ -4442,6 +4905,16 @@
)
(uuid "888fcbff-8854-48b0-9ea0-16b8929051e1")
)
(wire
(pts
(xy 40.64 20.32) (xy 40.64 24.13)
)
(stroke
(width 0)
(type default)
)
(uuid "88ee4bdf-c741-469c-b3cd-492b156f9b29")
)
(wire
(pts
(xy 138.43 40.64) (xy 140.97 40.64)
@ -4862,6 +5335,16 @@
)
(uuid "b290186a-46fd-4ff2-8061-aa12fcd561b2")
)
(wire
(pts
(xy 55.88 26.67) (xy 67.31 26.67)
)
(stroke
(width 0)
(type default)
)
(uuid "b2c5b7ec-5c1d-4af8-8531-58046a6f247c")
)
(wire
(pts
(xy 234.95 113.03) (xy 228.6 113.03)
@ -5052,6 +5535,16 @@
)
(uuid "c29ec434-c89d-4a73-8bf8-3ec9c475503b")
)
(wire
(pts
(xy 140.97 45.72) (xy 120.65 45.72)
)
(stroke
(width 0)
(type default)
)
(uuid "c2d5abd0-e0ca-4aa4-92bb-caa693aa3cc4")
)
(wire
(pts
(xy 83.82 140.97) (xy 100.33 140.97)
@ -5182,6 +5675,16 @@
)
(uuid "d013716e-a931-481c-89e5-cc1c74001c93")
)
(wire
(pts
(xy 92.71 26.67) (xy 92.71 45.72)
)
(stroke
(width 0)
(type default)
)
(uuid "d2486cf2-e14e-4edf-a1f2-fc0dfcd6bfd3")
)
(wire
(pts
(xy 205.74 40.64) (xy 205.74 52.07)
@ -5482,119 +5985,8 @@
)
(uuid "fef1ec51-1355-49b9-9ba4-ac3e3fc7562e")
)
(text "Gimbal Y2 (Right)"
(exclude_from_sim no)
(at 223.52 76.2 0)
(effects
(font
(size 2.27 2.27)
)
(justify left bottom)
)
(uuid "0666d517-3399-4e5a-a88e-28cb046bab53")
)
(text "Right \nHat Switch\n20-24"
(exclude_from_sim no)
(at 50.8 160.02 0)
(effects
(font
(size 2.27 2.27)
)
(justify right bottom)
)
(uuid "28c6deef-0288-400d-b950-adf2dba1adf7")
)
(text "ELRS TX\n"
(exclude_from_sim no)
(at 254 132.08 0)
(effects
(font
(size 2.27 2.27)
)
(justify left bottom)
)
(uuid "666e68ae-3562-4b09-b825-c9f5445176d4")
)
(text "Right\nTop\nButtons\n10-14\n"
(exclude_from_sim no)
(at 38.1 110.49 0)
(effects
(font
(size 2.27 2.27)
)
(justify right bottom)
)
(uuid "76352af5-c5ca-48fd-bebd-b8161cb04ff3")
)
(text "Left \nHat Switch\n15-19"
(exclude_from_sim no)
(at 50.8 139.7 0)
(effects
(font
(size 2.27 2.27)
)
(justify right bottom)
)
(uuid "921cb360-d960-4007-bfc3-e8983d17359f")
)
(text "Gimbal X2 (Right)"
(exclude_from_sim no)
(at 223.52 64.77 0)
(effects
(font
(size 2.27 2.27)
)
(justify left bottom)
)
(uuid "9ef99c88-97e7-430c-98f1-8b504d8de253")
)
(text "Left\nTop\nButtons\n5-9"
(exclude_from_sim no)
(at 36.83 82.55 0)
(effects
(font
(size 2.27 2.27)
)
(justify right bottom)
)
(uuid "9febdcbd-7a0f-4f31-b915-6903ca937f26")
)
(text "Bottom\nChassis\nButtons\n0-4"
(exclude_from_sim no)
(at 36.83 54.61 0)
(effects
(font
(size 2.27 2.27)
)
(justify right bottom)
)
(uuid "c7b70dbb-f435-4a57-aa9c-8cf50da328cb")
)
(text "Gimbal X1 (Left)"
(exclude_from_sim no)
(at 223.52 41.91 0)
(effects
(font
(size 2.27 2.27)
)
(justify left bottom)
)
(uuid "d87b3a29-cc14-4431-b108-be87ff886dfb")
)
(text "Gimbal Y1 (Left)"
(exclude_from_sim no)
(at 223.52 53.34 0)
(effects
(font
(size 2.27 2.27)
)
(justify left bottom)
)
(uuid "ed3a0309-c84e-411e-aaa8-eac48814b746")
)
(label "COL_B"
(at 102.87 66.04 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5605,7 +5997,6 @@
)
(label "ROW_E"
(at 102.87 148.59 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5616,7 +6007,6 @@
)
(label "14"
(at 57.15 109.22 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5627,7 +6017,6 @@
)
(label "20"
(at 64.77 151.13 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5638,7 +6027,6 @@
)
(label "13"
(at 57.15 106.68 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5649,7 +6037,6 @@
)
(label "ROW_A"
(at 102.87 33.02 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5660,7 +6047,6 @@
)
(label "5"
(at 57.15 71.12 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5671,7 +6057,6 @@
)
(label "12"
(at 57.15 104.14 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5682,7 +6067,6 @@
)
(label "ROW_C"
(at 102.87 88.9 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5691,9 +6075,18 @@
)
(uuid "42ca3c1b-bbde-4182-bffb-22b88412990a")
)
(label "25"
(at 57.15 24.13 0)
(effects
(font
(size 1.27 1.27)
)
(justify left bottom)
)
(uuid "53f48e32-a548-4738-916b-949c45c9ba6a")
)
(label "SCL"
(at 168.91 71.12 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5704,7 +6097,6 @@
)
(label "ROW_B"
(at 102.87 58.42 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5715,7 +6107,6 @@
)
(label "COL_E"
(at 102.87 73.66 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5726,7 +6117,6 @@
)
(label "16"
(at 64.77 133.35 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5737,7 +6127,6 @@
)
(label "COL_D"
(at 102.87 71.12 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5748,7 +6137,6 @@
)
(label "1"
(at 57.15 45.72 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5759,7 +6147,6 @@
)
(label "24"
(at 64.77 161.29 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5770,7 +6157,6 @@
)
(label "6"
(at 57.15 73.66 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5781,7 +6167,6 @@
)
(label "2"
(at 57.15 48.26 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5792,7 +6177,6 @@
)
(label "17"
(at 64.77 135.89 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5803,7 +6187,6 @@
)
(label "9"
(at 57.15 81.28 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5814,7 +6197,6 @@
)
(label "COL_C"
(at 102.87 68.58 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5825,7 +6207,6 @@
)
(label "3"
(at 57.15 50.8 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5836,7 +6217,6 @@
)
(label "15"
(at 64.77 130.81 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5845,9 +6225,18 @@
)
(uuid "a62949b1-4474-4ed4-8759-09439e5b4e57")
)
(label "ROW_F"
(at 102.87 20.32 0)
(effects
(font
(size 1.27 1.27)
)
(justify left bottom)
)
(uuid "a9e223f9-ffb2-4c91-a5be-434b50fffb87")
)
(label "21"
(at 64.77 153.67 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5858,7 +6247,6 @@
)
(label "ROW_D"
(at 102.87 128.27 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5869,7 +6257,6 @@
)
(label "23"
(at 64.77 158.75 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5880,7 +6267,6 @@
)
(label "18"
(at 64.77 138.43 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5889,9 +6275,18 @@
)
(uuid "bb843c91-2a51-41f1-b9b5-35c508fa8c2b")
)
(label "26"
(at 57.15 26.67 0)
(effects
(font
(size 1.27 1.27)
)
(justify left bottom)
)
(uuid "bfad85d3-6ad3-4abb-a160-37a46102177f")
)
(label "4"
(at 57.15 53.34 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5902,7 +6297,6 @@
)
(label "19"
(at 64.77 140.97 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5913,7 +6307,6 @@
)
(label "7"
(at 57.15 76.2 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5924,7 +6317,6 @@
)
(label "COL_A"
(at 102.87 63.5 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5935,7 +6327,6 @@
)
(label "SDA"
(at 168.91 73.66 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5946,7 +6337,6 @@
)
(label "11"
(at 57.15 101.6 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5957,7 +6347,6 @@
)
(label "10"
(at 57.15 99.06 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5968,7 +6357,6 @@
)
(label "22"
(at 64.77 156.21 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5979,7 +6367,6 @@
)
(label "8"
(at 57.15 78.74 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -5990,7 +6377,6 @@
)
(label "0"
(at 57.15 43.18 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
@ -7034,6 +7420,92 @@
)
)
)
(symbol
(lib_id "Diode:1N4148W")
(at 71.12 26.67 180)
(unit 1)
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
(dnp no)
(uuid "46ecf311-7740-4812-ac20-812de8d3e4cf")
(property "Reference" "D27"
(at 67.31 25.4 0)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Value" "1N4148W"
(at 71.12 21.59 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Footprint" "Diode_SMD:D_SOD-123"
(at 71.12 22.225 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Datasheet" "https://www.vishay.com/docs/85748/1n4148w.pdf"
(at 71.12 26.67 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Description" ""
(at 71.12 26.67 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Sim.Device" "D"
(at 71.12 21.59 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Sim.Pins" "1=K 2=A"
(at 71.12 24.13 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(pin "1"
(uuid "e0d45ce7-ada9-49c5-9121-632d5025127a")
)
(pin "2"
(uuid "2137e308-87bc-4171-bb81-2eb2925ceae1")
)
(instances
(project "cmdr-joystick"
(path "/5b501981-46e2-4084-afad-38073ca78ebd"
(reference "D27")
(unit 1)
)
)
)
)
(symbol
(lib_id "Diode:1N4148W")
(at 71.12 76.2 180)
@ -9550,6 +10022,91 @@
)
)
)
(symbol
(lib_id "Diode:1N4148W")
(at 71.12 24.13 180)
(unit 1)
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
(dnp no)
(uuid "b7fe8caf-41aa-4e02-a5d9-1eea6f8f65a1")
(property "Reference" "D26"
(at 67.31 22.86 0)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Value" "1N4148W"
(at 71.12 19.05 0)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Footprint" "Diode_SMD:D_SOD-123"
(at 71.12 19.685 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Datasheet" "https://www.vishay.com/docs/85748/1n4148w.pdf"
(at 71.12 24.13 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Description" ""
(at 71.12 24.13 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Sim.Device" "D"
(at 71.12 19.05 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Sim.Pins" "1=K 2=A"
(at 71.12 21.59 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(pin "1"
(uuid "3247e64c-a674-403e-b9ca-9f5a27772c9a")
)
(pin "2"
(uuid "1763e96e-860d-4f23-9a5a-9b2434fb5056")
)
(instances
(project "cmdr-joystick"
(path "/5b501981-46e2-4084-afad-38073ca78ebd"
(reference "D26")
(unit 1)
)
)
)
)
(symbol
(lib_id "power:+3.3V")
(at 157.48 26.67 0)
@ -10641,6 +11198,81 @@
)
)
)
(symbol
(lib_id "Connector_Generic:Conn_02x02_Odd_Even")
(at 48.26 24.13 0)
(unit 1)
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
(dnp no)
(fields_autoplaced yes)
(uuid "f38f8351-71f5-4607-80f0-a42807da220c")
(property "Reference" "J11"
(at 49.53 17.78 0)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Value" "Conn_02x02_Odd_Even"
(at 49.53 20.32 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Footprint" "Connector_PinHeader_2.54mm:PinHeader_2x02_P2.54mm_Vertical"
(at 48.26 24.13 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Datasheet" "~"
(at 48.26 24.13 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Description" "Generic connector, double row, 02x02, odd/even pin numbering scheme (row 1 odd numbers, row 2 even numbers), script generated (kicad-library-utils/schlib/autogen/connector/)"
(at 48.26 24.13 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(pin "1"
(uuid "dff31445-6af1-4699-a9fd-126f8275a9e8")
)
(pin "4"
(uuid "6eb5391f-f8ad-422f-ada0-ea6481f1e331")
)
(pin "3"
(uuid "c534bd37-80f3-4d24-9a85-f5a058c29572")
)
(pin "2"
(uuid "6555abe8-115d-4871-8cf0-932ecb7a2675")
)
(instances
(project ""
(path "/5b501981-46e2-4084-afad-38073ca78ebd"
(reference "J11")
(unit 1)
)
)
)
)
(symbol
(lib_id "Diode:1N4148W")
(at 71.12 48.26 180)
@ -10930,4 +11562,5 @@
(page "1")
)
)
(embedded_fonts no)
)

View File

@ -0,0 +1 @@
{"ARCHIVE_NAME": "CMDR_keyboard_rev_b", "EXTRA_LAYERS": "", "ALL_ACTIVE_LAYERS": false, "EXTEND_EDGE_CUT": false, "ALTERNATIVE_EDGE_CUT": false, "AUTO TRANSLATE": true, "AUTO FILL": true, "EXCLUDE DNP": false}

Binary file not shown.

View File

@ -1,544 +0,0 @@
#!/bin/bash
# CMtec CMDR Joystick 25 Install Script
# Supports building, testing, and deploying RP2040 firmware
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Show usage information
show_usage() {
echo -e "${GREEN}CMtec CMDR Joystick 25 Install Script${NC}"
echo "========================================"
echo
echo "Usage: $0 [flash|test|check|clean] [options]"
echo
echo "Commands:"
echo " flash Build and flash RP2040 firmware"
echo " test Run comprehensive test suite"
echo " check Quick compilation and lint checks"
echo " clean Clean build artifacts and temp files"
echo
echo "flash options:"
echo " --local Build + flash locally (RP2040 mass storage)"
echo " --ssh Build + transfer UF2 via SSH"
echo " --mount PATH Remote RP2040 mount (default /Volumes/RPI-RP2)"
echo
echo "SSH options (when using --ssh):"
echo " --target user@host Combined user and host"
echo " --user USER SSH username"
echo " --host HOST SSH hostname or IP"
echo " --port PORT SSH port (default 22)"
echo " --key PATH SSH private key path"
echo
echo "Examples:"
echo " $0 # Show this help"
echo " $0 test # Run all tests"
echo " $0 check # Quick compilation check"
echo " $0 flash --local # Build and flash firmware locally"
echo " $0 flash --ssh --target user@host --mount /Volumes/RPI-RP2"
echo " $0 clean # Clean build artifacts"
exit "${1:-0}"
}
# Parse command line arguments
COMMAND="${1:-}"
shift 1 || true
# If no command provided, show usage
if [ -z "$COMMAND" ]; then
show_usage 0
fi
case "$COMMAND" in
flash | test | check | clean) ;;
-h | --help | help)
show_usage 0
;;
*)
echo -e "${RED}Error: Unknown command '$COMMAND'${NC}"
echo
show_usage 1
;;
esac
echo -e "${GREEN}CMtec CMDR Joystick 25 Install Script${NC}"
echo "========================================"
echo -e "${BLUE}Mode: $COMMAND${NC}"
echo
# Get script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Ensure cargo-binutils is available for `cargo objcopy`
ensure_cargo_binutils() {
if ! command -v cargo-objcopy >/dev/null 2>&1; then
echo "Installing cargo-binutils and llvm-tools-preview (required for UF2 conversion)..."
rustup component add llvm-tools-preview || true
if ! cargo install cargo-binutils; then
echo -e "${RED}Error: Failed to install cargo-binutils${NC}"
echo "Install manually: rustup component add llvm-tools-preview && cargo install cargo-binutils"
exit 1
fi
fi
}
# Function to check prerequisites
check_prerequisites() {
echo "Checking prerequisites..."
if ! command -v cargo &>/dev/null; then
echo -e "${RED}Error: cargo (Rust toolchain) not found${NC}"
echo "Install Rust: https://rustup.rs/"
exit 1
fi
if ! command -v python3 &>/dev/null; then
echo -e "${RED}Error: python3 not found${NC}"
echo "Install Python 3"
exit 1
fi
# Check for required Rust target
if ! rustup target list --installed | grep -q "thumbv6m-none-eabi"; then
echo "Installing Rust target thumbv6m-none-eabi..."
rustup target add thumbv6m-none-eabi
fi
# Check for x86_64 target for testing
if ! rustup target list --installed | grep -q "x86_64-unknown-linux-gnu"; then
echo "Installing Rust target x86_64-unknown-linux-gnu (for testing)..."
rustup target add x86_64-unknown-linux-gnu
fi
echo -e "${GREEN}${NC} Prerequisites check complete"
echo
}
# Function to run comprehensive test suite
run_tests() {
echo "Running comprehensive test suite..."
echo
check_prerequisites
# Change to RP2040 directory
RP2040_DIR="$SCRIPT_DIR/rp2040"
if [ ! -d "$RP2040_DIR" ]; then
echo -e "${RED}Error: RP2040 directory not found at $RP2040_DIR${NC}"
exit 1
fi
cd "$RP2040_DIR"
# Test 1: Cargo check for embedded target
echo "Testing embedded target compilation..."
if ! cargo check --target thumbv6m-none-eabi; then
echo -e "${RED}${NC} Embedded target compilation check failed"
exit 1
fi
echo -e "${GREEN}${NC} Embedded target compilation check passed"
# Test 2: Host target tests (17 comprehensive tests)
echo "Running host target test suite..."
if ! cargo test --lib --target x86_64-unknown-linux-gnu --features std; then
echo -e "${RED}${NC} Host target tests failed"
exit 1
fi
echo -e "${GREEN}${NC} Host target tests passed (17 tests)"
# Test 3: Clippy for code quality (warnings allowed for now)
echo "Running clippy code quality checks..."
if ! cargo clippy --target thumbv6m-none-eabi; then
echo -e "${RED}${NC} Clippy code quality check failed"
exit 1
fi
echo -e "${GREEN}${NC} Clippy code quality check passed"
# Test 4: Release build test
echo "Testing release build..."
if ! cargo build --release --target thumbv6m-none-eabi; then
echo -e "${RED}${NC} Release build failed"
exit 1
fi
echo -e "${GREEN}${NC} Release build passed"
cd "$SCRIPT_DIR"
echo
echo -e "${GREEN}All tests completed successfully!${NC}"
echo
echo "Test summary:"
echo -e "${GREEN}${NC} Embedded target compilation check"
echo -e "${GREEN}${NC} Host target test suite (17 tests: expo + storage modules)"
echo -e "${GREEN}${NC} Clippy code quality checks"
echo -e "${GREEN}${NC} Release build verification"
echo
echo -e "${GREEN}Codebase is ready for deployment.${NC}"
}
# Function to run quick checks
run_check() {
echo "Running quick compilation and lint checks..."
echo
check_prerequisites
# Change to RP2040 directory
RP2040_DIR="$SCRIPT_DIR/rp2040"
if [ ! -d "$RP2040_DIR" ]; then
echo -e "${RED}Error: RP2040 directory not found at $RP2040_DIR${NC}"
exit 1
fi
cd "$RP2040_DIR"
# Quick cargo check
echo "Checking embedded target compilation..."
if ! cargo check --target thumbv6m-none-eabi; then
echo -e "${RED}${NC} Compilation check failed"
exit 1
fi
echo -e "${GREEN}${NC} Compilation check passed"
# Quick clippy check (warnings allowed for now)
echo "Running clippy..."
if ! cargo clippy --target thumbv6m-none-eabi; then
echo -e "${RED}${NC} Clippy check failed"
exit 1
fi
echo -e "${GREEN}${NC} Clippy check passed"
cd "$SCRIPT_DIR"
echo
echo -e "${GREEN}Quick checks completed successfully!${NC}"
}
# Function to clean build artifacts
clean_build() {
echo "Cleaning build artifacts and temporary files..."
echo
RP2040_DIR="$SCRIPT_DIR/rp2040"
if [ ! -d "$RP2040_DIR" ]; then
echo -e "${RED}Error: RP2040 directory not found at $RP2040_DIR${NC}"
exit 1
fi
cd "$RP2040_DIR"
# Clean cargo artifacts
echo "Cleaning cargo build artifacts..."
cargo clean
echo -e "${GREEN}${NC} Cargo artifacts cleaned"
# Remove UF2 and binary files
if [ -f "firmware.uf2" ]; then
rm -f firmware.uf2
echo -e "${GREEN}${NC} Removed firmware.uf2"
fi
if [ -f "target/thumbv6m-none-eabi/release/cmdr-joystick-25.bin" ]; then
rm -f target/thumbv6m-none-eabi/release/cmdr-joystick-25.bin
echo -e "${GREEN}${NC} Removed binary files"
fi
# Clean any temp files
rm -f /tmp/cmdr_*.tmp 2>/dev/null || true
cd "$SCRIPT_DIR"
echo
echo -e "${GREEN}Cleanup completed!${NC}"
}
# Function to build firmware locally
build_firmware() {
RP2040_DIR="$SCRIPT_DIR/rp2040"
if [ ! -d "$RP2040_DIR" ]; then
echo -e "${RED}Error: RP2040 directory not found at $RP2040_DIR${NC}"
exit 1
fi
check_prerequisites
# Build firmware
echo "Building RP2040 firmware..."
cd "$RP2040_DIR"
if ! cargo build --release --target thumbv6m-none-eabi; then
echo -e "${RED}Error: Failed to build firmware${NC}"
exit 1
fi
echo -e "${GREEN}${NC} Firmware build complete"
echo
# Create UF2 file
echo "Converting to UF2 format..."
ensure_cargo_binutils
if ! cargo objcopy --release --target thumbv6m-none-eabi -- -O binary target/thumbv6m-none-eabi/release/cmdr-joystick-25.bin; then
echo -e "${RED}Error: Failed to create binary file${NC}"
exit 1
fi
# Check for uf2conv.py script
if [ ! -f "uf2conv.py" ]; then
echo "Downloading uf2conv.py..."
if ! curl -L -o uf2conv.py "https://raw.githubusercontent.com/microsoft/uf2/master/utils/uf2conv.py"; then
echo -e "${RED}Error: Failed to download uf2conv.py${NC}"
exit 1
fi
chmod +x uf2conv.py
fi
if ! python3 uf2conv.py -b 0x10000000 -f 0xe48bff56 -c -o firmware.uf2 target/thumbv6m-none-eabi/release/cmdr-joystick-25.bin; then
echo -e "${RED}Error: Failed to convert to UF2 format${NC}"
exit 1
fi
echo -e "${GREEN}${NC} UF2 conversion complete"
echo
}
# Function to build and transfer firmware via SSH
flash_firmware_ssh() {
RP2040_DIR="$SCRIPT_DIR/rp2040"
if [ ! -d "$RP2040_DIR" ]; then
echo -e "${RED}Error: RP2040 directory not found at $RP2040_DIR${NC}"
exit 1
fi
# Parse CLI args for SSH
SSH_HOST=""; SSH_USER=""; SSH_PORT="22"; SSH_KEY=""; REMOTE_MOUNT="/Volumes/RPI-RP2"
while [ $# -gt 0 ]; do
case "$1" in
--target) shift; TARGET="$1"; SSH_USER="${TARGET%@*}"; SSH_HOST="${TARGET#*@}" ;;
--host) shift; SSH_HOST="$1" ;;
--user) shift; SSH_USER="$1" ;;
--port) shift; SSH_PORT="$1" ;;
--key) shift; SSH_KEY="$1" ;;
--mount) shift; REMOTE_MOUNT="$1" ;;
*) echo -e "${YELLOW}Warning: Unknown SSH option: $1${NC}" ;;
esac
shift || true
done
if [ -z "$SSH_HOST" ] || [ -z "$SSH_USER" ]; then
echo -e "${RED}Error: provide SSH credentials via --target user@host or --user/--host${NC}"
exit 1
fi
# Build firmware locally first
build_firmware
echo "Testing SSH connection to $SSH_USER@$SSH_HOST:$SSH_PORT..."
mkdir -p "${HOME}/.ssh"
SSH_CONTROL_PATH="${HOME}/.ssh/cmdr_%C"
local SSH_OPTS=(
-p "$SSH_PORT"
-o "StrictHostKeyChecking=accept-new"
-o "ControlMaster=auto"
-o "ControlPersist=60s"
-o "ControlPath=$SSH_CONTROL_PATH"
-o "PreferredAuthentications=publickey,password,keyboard-interactive"
)
if [ -n "$SSH_KEY" ]; then
SSH_OPTS+=( -i "$SSH_KEY" )
fi
# Test SSH connection
if ! ssh "${SSH_OPTS[@]}" -o ConnectTimeout=10 -fN "$SSH_USER@$SSH_HOST" 2>/dev/null; then
echo -e "${RED}Error: Cannot connect to $SSH_USER@$SSH_HOST:$SSH_PORT${NC}"
echo "Tips:"
echo "- Verify network reachability and SSH port"
echo "- Ensure SSH keys are properly configured"
exit 1
fi
echo -e "${GREEN}${NC} SSH connection successful"
# Check if remote mount exists
echo "Checking remote RP2040 mount path: $REMOTE_MOUNT"
if ! ssh "${SSH_OPTS[@]}" "$SSH_USER@$SSH_HOST" "test -d \"$REMOTE_MOUNT\""; then
echo -e "${RED}Remote mount not found: $REMOTE_MOUNT${NC}"
echo -e "${YELLOW}Ensure the RP2040 is in BOOTSEL mode and mounted on the remote host.${NC}"
echo "Common paths: /Volumes/RPI-RP2 (macOS), /media/\$USER/RPI-RP2 (Linux)"
exit 1
fi
echo "Transferring firmware.uf2 to remote host..."
local SCP_OPTS=( -P "$SSH_PORT" -o "StrictHostKeyChecking=accept-new" -o "ControlPath=$SSH_CONTROL_PATH" )
if [ -n "$SSH_KEY" ]; then
SCP_OPTS+=( -i "$SSH_KEY" )
fi
cd "$RP2040_DIR"
if ! scp "${SCP_OPTS[@]}" firmware.uf2 "$SSH_USER@$SSH_HOST:$REMOTE_MOUNT/firmware.uf2"; then
echo -e "${RED}${NC} Failed to transfer firmware to remote RP2040"
exit 1
fi
echo -e "${GREEN}${NC} Transferred firmware.uf2 to $SSH_USER@$SSH_HOST:$REMOTE_MOUNT"
cd "$SCRIPT_DIR"
echo
echo -e "${GREEN}SSH firmware deployment completed!${NC}"
echo "Remote: $SSH_USER@$SSH_HOST:$REMOTE_MOUNT/firmware.uf2"
echo -e "${YELLOW}The remote RP2040 should now restart with new firmware.${NC}"
}
# Function to flash firmware locally
flash_firmware_local() {
build_firmware
RP2040_DIR="$SCRIPT_DIR/rp2040"
cd "$RP2040_DIR"
# Detect OS and set up mount detection
OS="$(uname -s)"
# Wait for and flash to RP2040
echo "Waiting for RP2040 in bootloader mode..."
echo -e "${YELLOW}Put your RP2040 into bootloader mode (hold BOOTSEL button while plugging in USB)${NC}"
case "$OS" in
Linux*)
# Wait for RPI-RP2 mount point with timeout
MAX_WAIT=120
waited=0
while [ ! -d "/media/RPI-RP2" ] && [ ! -d "/media/$USER/RPI-RP2" ] && [ ! -d "/run/media/$USER/RPI-RP2" ]; do
sleep 1
waited=$((waited + 1))
if [ "$waited" -ge "$MAX_WAIT" ]; then
echo -e "${RED}Timeout waiting for RP2040 mount (RPI-RP2)${NC}"
echo "Ensure the device is in BOOTSEL mode and mounted (try replugging with BOOTSEL held)."
echo "Expected mount at one of: /media/RPI-RP2, /media/$USER/RPI-RP2, /run/media/$USER/RPI-RP2"
exit 1
fi
done
# Find the actual mount point
if [ -d "/media/RPI-RP2" ]; then
MOUNT_POINT="/media/RPI-RP2"
elif [ -d "/media/$USER/RPI-RP2" ]; then
MOUNT_POINT="/media/$USER/RPI-RP2"
elif [ -d "/run/media/$USER/RPI-RP2" ]; then
MOUNT_POINT="/run/media/$USER/RPI-RP2"
else
echo -e "${RED}Error: Could not find RPI-RP2 mount point${NC}"
exit 1
fi
echo -e "${GREEN}${NC} Found RP2040 at $MOUNT_POINT"
# Flash firmware
echo "Flashing firmware..."
cp firmware.uf2 "$MOUNT_POINT/"
echo -e "${GREEN}${NC} Firmware flashed successfully!"
;;
Darwin*)
# Wait for RPI-RP2 volume with timeout
MAX_WAIT=120
waited=0
while [ ! -d "/Volumes/RPI-RP2" ]; do
sleep 1
waited=$((waited + 1))
if [ "$waited" -ge "$MAX_WAIT" ]; then
echo -e "${RED}Timeout waiting for RP2040 mount at /Volumes/RPI-RP2${NC}"
echo "Ensure the device is in BOOTSEL mode and mounted (try replugging with BOOTSEL held)."
exit 1
fi
done
echo -e "${GREEN}${NC} Found RP2040 at /Volumes/RPI-RP2"
# Flash firmware
echo "Flashing firmware..."
cp firmware.uf2 "/Volumes/RPI-RP2/"
echo -e "${GREEN}${NC} Firmware flashed successfully!"
;;
*)
echo -e "${RED}Error: Unsupported operating system for local flashing${NC}"
echo "This script supports Linux and macOS for local flashing."
exit 1
;;
esac
cd "$SCRIPT_DIR"
echo
echo -e "${GREEN}Firmware installation completed!${NC}"
echo
echo "Firmware file: $RP2040_DIR/firmware.uf2"
echo -e "${GREEN}RP2040 firmware deployed successfully!${NC}"
echo -e "${YELLOW}The device should now restart with new firmware.${NC}"
}
# Detect OS
case "$(uname -s)" in
Linux*)
OS="Linux"
;;
Darwin*)
OS="macOS"
;;
*)
echo -e "${RED}Error: Unsupported operating system${NC}"
echo "This script supports Linux and macOS only."
exit 1
;;
esac
echo "Detected OS: $OS"
if [ "$COMMAND" = "flash" ]; then
echo "RP2040 firmware directory: $SCRIPT_DIR/rp2040"
elif [ "$COMMAND" = "test" ]; then
echo "Testing Rust embedded firmware with comprehensive test suite"
fi
echo
# Execute command
case "$COMMAND" in
flash)
MODE=""
REM_ARGS=()
for arg in "$@"; do
case "$arg" in
--local) MODE="local" ;;
--ssh) MODE="ssh" ;;
*) REM_ARGS+=("$arg") ;;
esac
done
if [ "$MODE" = "local" ]; then
flash_firmware_local
elif [ "$MODE" = "ssh" ]; then
flash_firmware_ssh "${REM_ARGS[@]}"
else
echo -e "${RED}Error: specify --local or --ssh for flash${NC}"
show_usage 1
fi
;;
test)
run_tests
;;
check)
run_check
;;
clean)
clean_build
;;
esac

View File

@ -1,8 +1,8 @@
[package]
name = "cmdr-joystick-25"
name = "cmdr-joystick"
version = "0.2.0"
edition = "2021"
# Firmware crate for the CMDR Joystick 25 (RP2040)
# Firmware crate for the CMDR Joystick (RP2040)
[dependencies]
# Core embedded + RP2040 HAL stack (aligned with rp2040-hal v0.11)
@ -65,7 +65,7 @@ overflow-checks = false
path = "src/lib.rs"
[[bin]]
name = "cmdr-joystick-25"
name = "cmdr-joystick"
path = "src/main.rs"
bench = false
test = false

View File

@ -1,4 +1,4 @@
//! Axis processing for CMDR Joystick 25
//! Axis processing for CMDR Joystick
//!
//! Responsibilities
//! - Apply gimbal mode compensation (M10/M7) to raw ADC readings
@ -54,6 +54,7 @@ pub struct GimbalAxis {
impl Default for GimbalAxis {
fn default() -> Self {
// Apply new calibration limits supplied by the calibration manager.
GimbalAxis {
value: AXIS_CENTER,
value_before_hold: AXIS_CENTER,
@ -143,6 +144,7 @@ pub struct VirtualAxis {
impl Default for VirtualAxis {
fn default() -> Self {
// Create a virtual axis starting at center with the supplied step size.
VirtualAxis {
value: AXIS_CENTER,
step: 5,
@ -206,6 +208,7 @@ pub struct AxisManager {
impl Default for AxisManager {
fn default() -> Self {
// Delegate to `new` so the default stays aligned with explicit constructor logic.
Self::new()
}
}
@ -462,6 +465,7 @@ mod tests {
#[test]
fn test_gimbal_axis_default() {
// Factory defaults should leave every axis field pointing at the center span.
let axis = GimbalAxis::default();
assert_eq!(axis.value, AXIS_CENTER);
assert_eq!(axis.min, ADC_MIN);
@ -473,6 +477,7 @@ mod tests {
#[test]
fn test_gimbal_axis_new() {
// `new` should be a thin wrapper over `Default` without changing members.
let axis = GimbalAxis::new();
assert_eq!(axis.value, AXIS_CENTER);
assert_eq!(axis.min, ADC_MIN);
@ -483,6 +488,7 @@ mod tests {
#[test]
fn test_gimbal_axis_with_calibration() {
// Axis constructed with explicit calibration should adopt all provided bounds.
let axis = GimbalAxis::with_calibration(100, 3900, 2000);
assert_eq!(axis.min, 100);
assert_eq!(axis.max, 3900);
@ -491,6 +497,7 @@ mod tests {
#[test]
fn test_gimbal_axis_activity_detection() {
// Activity flag should only flip when the axis value actually changes.
let mut axis = GimbalAxis::new();
// Initially no activity (same as previous)
@ -506,6 +513,7 @@ mod tests {
#[test]
fn test_gimbal_axis_throttle_hold_processing() {
// Throttle hold remapping should cover remap, center latch, and pending hold cases.
let mut axis = GimbalAxis::new();
axis.set_hold(1500); // Set hold value below center
@ -541,6 +549,7 @@ mod tests {
#[test]
fn test_virtual_axis_default() {
// Virtual axis should boot at center with the configured step size.
let virtual_axis = VirtualAxis::default();
assert_eq!(virtual_axis.value, AXIS_CENTER);
assert_eq!(virtual_axis.step, 5);
@ -548,6 +557,7 @@ mod tests {
#[test]
fn test_virtual_axis_movement_up() {
// An upward press should increment the virtual axis and flag activity.
let mut virtual_axis = VirtualAxis::new(10);
// Test upward movement
@ -558,6 +568,7 @@ mod tests {
#[test]
fn test_virtual_axis_movement_down() {
// A downward press should decrement the virtual axis and flag activity.
let mut virtual_axis = VirtualAxis::new(10);
// Test downward movement
@ -568,6 +579,7 @@ mod tests {
#[test]
fn test_virtual_axis_return_to_center() {
// Without inputs the virtual axis should ease back toward the center value.
let mut virtual_axis = VirtualAxis::new(10);
virtual_axis.value = AXIS_CENTER + 20;
@ -579,6 +591,7 @@ mod tests {
#[test]
fn test_virtual_axis_direction_compensation() {
// Reversing direction should recenter before stepping to avoid large jumps.
let mut virtual_axis = VirtualAxis::new(10);
virtual_axis.value = AXIS_CENTER - 100;
@ -591,6 +604,7 @@ mod tests {
#[test]
fn test_axis_manager_creation() {
// Manager construction should initialize all axes and state to defaults.
let manager = AxisManager::new();
assert_eq!(manager.axes.len(), NBR_OF_GIMBAL_AXIS);
assert_eq!(manager.gimbal_mode, GIMBAL_MODE_M10);
@ -600,6 +614,7 @@ mod tests {
#[test]
fn test_gimbal_compensation_m10() {
// M10 gimbal compensation must invert the correct pair of axes.
let manager = AxisManager::new(); // Default is M10
let mut raw_values = [1000, 1500, 2000, 2500];
@ -614,6 +629,7 @@ mod tests {
#[test]
fn test_gimbal_compensation_m7() {
// M7 gimbal compensation must invert the complementary axes.
let mut manager = AxisManager::new();
manager.set_gimbal_mode(GIMBAL_MODE_M7);
let mut raw_values = [1000, 1500, 2000, 2500];
@ -629,6 +645,7 @@ mod tests {
#[test]
fn test_axis_activity_detection() {
// Manager activity flag should reflect when any axis value changes.
let mut manager = AxisManager::new();
// No activity initially
@ -644,6 +661,7 @@ mod tests {
#[test]
fn test_calculate_axis_value_boundaries() {
// Axis conversion should clamp inputs below min or above max calibration values.
let expo_lut = ExpoLUT::new(0.0); // No expo for testing
// Test min boundary
@ -657,6 +675,7 @@ mod tests {
#[test]
fn test_calculate_axis_value_deadzone() {
// Inputs inside the center deadzone should resolve to the exact center value.
let expo_lut = ExpoLUT::new(0.0); // No expo for testing
// Test center deadzone
@ -666,6 +685,7 @@ mod tests {
#[test]
fn test_calculate_axis_value_degenerate_calibration() {
// Degenerate calibration inputs should return the provided center without panicking.
let expo_lut = ExpoLUT::new(0.0);
// When calibration collapses to a single point (min=max=center),

213
rp2040/src/board.rs Normal file
View File

@ -0,0 +1,213 @@
//! Board bring-up and peripheral wiring for the CMDR Joystick firmware.
use crate::button_matrix::{ButtonMatrix, MatrixPins};
use crate::hardware::{self, BoardPins};
use crate::status::StatusLed;
use cortex_m::delay::Delay;
use cortex_m::interrupt;
use eeprom24x::{addr_size, page_size, unique_serial, Eeprom24x};
use rp2040_hal::adc::{Adc, AdcPin};
use rp2040_hal::clocks::Clock;
use rp2040_hal::gpio::{self, Pin, PullNone};
use rp2040_hal::i2c::I2C;
use rp2040_hal::pac;
use rp2040_hal::pio::PIOExt;
use rp2040_hal::sio::Sio;
use rp2040_hal::timer::Timer;
use rp2040_hal::watchdog::Watchdog;
use rp2040_hal::{clocks::init_clocks_and_plls, gpio::FunctionSioInput};
use static_cell::StaticCell;
use usb_device::class_prelude::UsbBusAllocator;
pub type JoystickMatrix = ButtonMatrix<
MatrixPins<{ hardware::BUTTON_ROWS }, { hardware::BUTTON_COLS }>,
{ hardware::BUTTON_ROWS },
{ hardware::BUTTON_COLS },
{ hardware::NUMBER_OF_BUTTONS },
>;
pub type JoystickStatusLed = StatusLed<pac::PIO0, rp2040_hal::pio::SM0, hardware::StatusLedPin>;
type BoardI2c = I2C<pac::I2C1, (hardware::I2cSdaPin, hardware::I2cSclPin)>;
type BoardEeprom = Eeprom24x<BoardI2c, page_size::B32, addr_size::TwoBytes, unique_serial::No>;
/// Strongly-typed collection of ADC-capable pins for each physical gimbal axis.
pub struct AxisAnalogPins {
pub left_x: AdcPin<Pin<gpio::bank0::Gpio29, FunctionSioInput, PullNone>>,
pub left_y: AdcPin<Pin<gpio::bank0::Gpio28, FunctionSioInput, PullNone>>,
pub right_x: AdcPin<Pin<gpio::bank0::Gpio27, FunctionSioInput, PullNone>>,
pub right_y: AdcPin<Pin<gpio::bank0::Gpio26, FunctionSioInput, PullNone>>,
}
impl AxisAnalogPins {
fn new(inputs: hardware::AxisInputs) -> Self {
// Wrap the raw GPIO inputs into ADC-capable pins for each physical axis.
let left_x = AdcPin::new(inputs.left_x).unwrap();
let left_y = AdcPin::new(inputs.left_y).unwrap();
let right_x = AdcPin::new(inputs.right_x).unwrap();
let right_y = AdcPin::new(inputs.right_y).unwrap();
Self {
left_x,
left_y,
right_x,
right_y,
}
}
}
/// Aggregates the runtime peripherals used by the joystick firmware.
pub struct Board {
button_matrix: JoystickMatrix,
status_led: JoystickStatusLed,
delay: Delay,
timer: Timer,
adc: Adc,
axis_pins: AxisAnalogPins,
left_extra_button: hardware::ExtraButtonPin,
right_extra_button: hardware::ExtraButtonPin,
eeprom: BoardEeprom,
usb_bus: &'static UsbBusAllocator<rp2040_hal::usb::UsbBus>,
}
/// Board components handed off to the application after initialization.
pub struct BoardParts {
pub button_matrix: JoystickMatrix,
pub status_led: JoystickStatusLed,
pub delay: Delay,
pub timer: Timer,
pub adc: Adc,
pub axis_pins: AxisAnalogPins,
pub left_extra_button: hardware::ExtraButtonPin,
pub right_extra_button: hardware::ExtraButtonPin,
pub eeprom: BoardEeprom,
pub usb_bus: &'static UsbBusAllocator<rp2040_hal::usb::UsbBus>,
}
impl Board {
pub fn new() -> Self {
// Acquire RP2040 peripheral handles before configuration begins.
let mut pac = pac::Peripherals::take().unwrap();
let core = pac::CorePeripherals::take().unwrap();
let mut watchdog = Watchdog::new(pac.WATCHDOG);
// Bring up the primary system and USB clocks using the external crystal.
let clocks = init_clocks_and_plls(
hardware::XTAL_FREQ_HZ,
pac.XOSC,
pac.CLOCKS,
pac.PLL_SYS,
pac.PLL_USB,
&mut pac.RESETS,
&mut watchdog,
)
.ok()
.unwrap();
let sio = Sio::new(pac.SIO);
// Split GPIO banks and translate them into the strongly typed board pins.
let raw_pins = gpio::Pins::new(
pac.IO_BANK0,
pac.PADS_BANK0,
sio.gpio_bank0,
&mut pac.RESETS,
);
let pins = BoardPins::new(raw_pins);
let matrix_pins = MatrixPins::new(pins.matrix_rows, pins.matrix_cols);
// Create the button matrix scanner with firmware debounce parameters.
let mut button_matrix = ButtonMatrix::new(
matrix_pins,
hardware::MATRIX_DEBOUNCE_SCANS,
hardware::MIN_PRESS_SPACING_SCANS,
);
button_matrix.init_pins();
let (mut pio, sm0, _, _, _) = pac.PIO0.split(&mut pac.RESETS);
// Configure the WS2812 status LED using a dedicated PIO state machine.
let status_led = StatusLed::new(
pins.status_led,
&mut pio,
sm0,
clocks.peripheral_clock.freq(),
);
// Set up timers for scheduling and a blocking delay helper.
let timer = Timer::new(pac.TIMER, &mut pac.RESETS, &clocks);
let delay = Delay::new(core.SYST, clocks.system_clock.freq().to_Hz());
// Build the I²C bus and EEPROM driver used for calibration persistence.
let i2c = I2C::i2c1(
pac.I2C1,
pins.i2c_sda,
pins.i2c_scl,
hardware::i2c::frequency(),
&mut pac.RESETS,
hardware::i2c::system_clock(),
);
let eeprom = Eeprom24x::new_24x32(i2c, hardware::i2c::EEPROM_ADDRESS);
// Bring up the ADC block and wrap each axis input pin.
let adc = Adc::new(pac.ADC, &mut pac.RESETS);
let axis_pins = AxisAnalogPins::new(pins.axis_inputs);
// Prepare a global USB bus allocator for the joystick HID device.
let usb_bus = usb_allocator(
pac.USBCTRL_REGS,
pac.USBCTRL_DPRAM,
clocks.usb_clock,
&mut pac.RESETS,
);
Self {
button_matrix,
status_led,
delay,
timer,
adc,
axis_pins,
left_extra_button: pins.left_extra_button,
right_extra_button: pins.right_extra_button,
eeprom,
usb_bus,
}
}
pub fn into_parts(self) -> BoardParts {
BoardParts {
button_matrix: self.button_matrix,
status_led: self.status_led,
delay: self.delay,
timer: self.timer,
adc: self.adc,
axis_pins: self.axis_pins,
left_extra_button: self.left_extra_button,
right_extra_button: self.right_extra_button,
eeprom: self.eeprom,
usb_bus: self.usb_bus,
}
}
}
fn usb_allocator(
usbctrl_regs: pac::USBCTRL_REGS,
usbctrl_dpram: pac::USBCTRL_DPRAM,
usb_clock: rp2040_hal::clocks::UsbClock,
resets: &mut pac::RESETS,
) -> &'static UsbBusAllocator<rp2040_hal::usb::UsbBus> {
// Lazily create the shared USB bus allocator so HID endpoints can borrow it.
static USB_BUS: StaticCell<UsbBusAllocator<rp2040_hal::usb::UsbBus>> = StaticCell::new();
// Wire up the USB bus allocator, HID class, and joystick endpoint once and reuse it.
interrupt::free(|_| {
USB_BUS.init_with(|| {
UsbBusAllocator::new(rp2040_hal::usb::UsbBus::new(
usbctrl_regs,
usbctrl_dpram,
usb_clock,
true,
resets,
))
})
})
}

30
rp2040/src/bootloader.rs Normal file
View File

@ -0,0 +1,30 @@
//! Bootloader helpers shared between power-on checks and runtime button chords.
use crate::status::{StatusLed, StatusMode};
use cortex_m::asm;
use rp2040_hal::gpio::AnyPin;
use rp2040_hal::pio::{PIOExt, StateMachineIndex};
/// Returns `true` when the power-on matrix snapshot requests bootloader entry.
///
/// The original firmware required the front-left-lower button to be held during
/// power-up to jump straight into ROM boot.
pub fn startup_requested(buttons: &[bool; crate::hardware::NUMBER_OF_BUTTONS]) -> bool {
buttons[crate::mapping::BUTTON_FRONT_LEFT_LOWER]
}
/// Puts the RP2040 into the ROM bootloader after updating the status LED.
pub fn enter<P, SM, I>(status_led: &mut StatusLed<P, SM, I>) -> !
where
P: PIOExt,
SM: StateMachineIndex,
I: AnyPin<Function = P::PinFunction>,
{
status_led.update(StatusMode::Bootloader);
let gpio_activity_pin_mask: u32 = 0;
let disable_interface_mask: u32 = 0;
rp2040_hal::rom_data::reset_to_usb_boot(gpio_activity_pin_mask, disable_interface_mask);
loop {
asm::nop();
}
}

View File

@ -1,119 +1,172 @@
//! Button matrix scanner for CMDR Joystick 25
//! Button matrix scanner for CMDR Joystick.
//!
//! Scans a row/column matrix and produces a debounced boolean state for each
//! button. Designed for small matrices on microcontrollers where timing is
//! deterministic and GPIO is plentiful.
//!
//! - Rows are configured as inputs with pullups
//! - Columns are configured as pushpull outputs
//! - Debounce is handled perbutton using a simple counter
//! - A tiny intercolumn delay is inserted to allow signals to settle
//! Mirrors the refactor performed for the keyboard firmware: the matrix owns
//! concrete pins, exposes a small `MatrixPinAccess` trait, and keeps the
//! debouncing + minimum press spacing behaviour identical to the original
//! joystick implementation.
use core::convert::Infallible;
use cortex_m::delay::Delay;
use embedded_hal::digital::{InputPin, OutputPin};
use rp2040_hal::gpio::{DynPinId, FunctionSioInput, FunctionSioOutput, Pin, PullNone, PullUp};
/// Button matrix driver
///
/// Generics
/// - `R`: number of rows
/// - `C`: number of columns
/// - `N`: total number of buttons (usually `R * C`)
///
/// Example
/// ```ignore
/// // 4 rows, 6 columns, 24 buttons, 5-scan debounce
/// let mut matrix: ButtonMatrix<4, 6, 24> = ButtonMatrix::new(row_pins, col_pins, 5);
/// matrix.init_pins();
/// loop {
/// matrix.scan_matrix(&mut delay);
/// let states = matrix.buttons_pressed();
/// // use `states`
/// }
/// ```
pub struct ButtonMatrix<'a, const R: usize, const C: usize, const N: usize> {
rows: &'a mut [&'a mut dyn InputPin<Error = Infallible>; R],
cols: &'a mut [&'a mut dyn OutputPin<Error = Infallible>; C],
pressed: [bool; N],
debounce: u8,
debounce_counter: [u8; N],
/// Abstraction over the matrix pins so the scanner can work with either the
/// concrete RP2040 pins or test doubles.
pub trait MatrixPinAccess<const ROWS: usize, const COLS: usize> {
fn init_columns(&mut self);
fn set_column_low(&mut self, column: usize);
fn set_column_high(&mut self, column: usize);
fn read_row(&mut self, row: usize) -> bool;
}
impl<'a, const R: usize, const C: usize, const N: usize> ButtonMatrix<'a, R, C, N> {
/// Creates a new button matrix.
///
/// Arguments
/// - `rows`: array of row pins (inputs with pullups)
/// - `cols`: array of column pins (pushpull outputs)
/// - `debounce`: number of consecutive scans a change must persist before it is accepted
pub fn new(
rows: &'a mut [&'a mut dyn InputPin<Error = Infallible>; R],
cols: &'a mut [&'a mut dyn OutputPin<Error = Infallible>; C],
debounce: u8,
) -> Self {
/// Concrete matrix pins backed by RP2040 GPIO using dynamic pin IDs.
type RowPin = Pin<DynPinId, FunctionSioInput, PullUp>;
type ColPin = Pin<DynPinId, FunctionSioOutput, PullNone>;
pub struct MatrixPins<const ROWS: usize, const COLS: usize> {
rows: [RowPin; ROWS],
cols: [ColPin; COLS],
}
impl<const ROWS: usize, const COLS: usize> MatrixPins<ROWS, COLS> {
pub fn new(rows: [RowPin; ROWS], cols: [ColPin; COLS]) -> Self {
Self { rows, cols }
}
}
impl<const ROWS: usize, const COLS: usize> MatrixPinAccess<ROWS, COLS> for MatrixPins<ROWS, COLS> {
fn init_columns(&mut self) {
// Default all columns high so rows can be strobed one at a time.
for column in self.cols.iter_mut() {
let _ = column.set_high();
}
}
fn set_column_low(&mut self, column: usize) {
// Pull the active column low before scanning its rows.
let _ = self.cols[column].set_low();
}
fn set_column_high(&mut self, column: usize) {
// Release the column after scanning so other columns remain idle.
let _ = self.cols[column].set_high();
}
fn read_row(&mut self, row: usize) -> bool {
// Treat any low level as a pressed switch, defaulting to false on IO errors.
self.rows[row].is_low().unwrap_or(false)
}
}
/// Row/column scanned button matrix driver with debounce counters and minimum
/// spacing between subsequent presses of the same key.
pub struct ButtonMatrix<P, const ROWS: usize, const COLS: usize, const BUTTONS: usize>
where
P: MatrixPinAccess<ROWS, COLS>,
{
pins: P,
pressed: [bool; BUTTONS],
debounce_threshold: u8,
debounce_counter: [u8; BUTTONS],
last_press_scan: [u32; BUTTONS],
min_press_gap_scans: u32,
scan_counter: u32,
}
impl<P, const ROWS: usize, const COLS: usize, const BUTTONS: usize>
ButtonMatrix<P, ROWS, COLS, BUTTONS>
where
P: MatrixPinAccess<ROWS, COLS>,
{
pub fn new(pins: P, debounce_threshold: u8, min_press_gap_scans: u32) -> Self {
debug_assert_eq!(BUTTONS, ROWS * COLS);
Self {
rows,
cols,
pressed: [false; N],
debounce,
debounce_counter: [0; N],
pins,
pressed: [false; BUTTONS],
debounce_threshold,
debounce_counter: [0; BUTTONS],
last_press_scan: [0; BUTTONS],
min_press_gap_scans,
scan_counter: 0,
}
}
/// Initialize the matrix GPIOs (set all columns high).
///
/// Call once before the first scan.
pub fn init_pins(&mut self) {
for col in self.cols.iter_mut() {
col.set_high().unwrap();
self.pins.init_columns();
}
pub fn prime(&mut self, delay: &mut Delay, passes: usize) {
for _ in 0..passes {
self.scan_matrix(delay);
}
}
/// Scan the matrix and update each button's debounced state.
///
/// Call at a fixed cadence. The simple debounce uses a perbutton counter: only
/// when a changed level is observed for `debounce` consecutive scans is the
/// new state committed.
///
/// Arguments
/// - `delay`: short delay implementation used to let signals settle between columns
pub fn scan_matrix(&mut self, delay: &mut Delay) {
for col_index in 0..self.cols.len() {
self.cols[col_index].set_low().unwrap();
self.scan_counter = self.scan_counter.wrapping_add(1);
for column in 0..COLS {
self.pins.set_column_low(column);
delay.delay_us(1);
self.process_column(col_index);
self.cols[col_index].set_high().unwrap();
self.process_column(column);
self.pins.set_column_high(column);
delay.delay_us(1);
}
}
/// Process a single column: drive low, sample rows, update debounce state, then release high.
///
/// Arguments
/// - `col_index`: index of the column being scanned
fn process_column(&mut self, col_index: usize) {
for row_index in 0..self.rows.len() {
let button_index: usize = col_index + (row_index * C);
let current_state = self.rows[row_index].is_low().unwrap();
pub fn buttons_pressed(&self) -> [bool; BUTTONS] {
self.pressed
}
if current_state == self.pressed[button_index] {
self.debounce_counter[button_index] = 0;
fn process_column(&mut self, column: usize) {
// Drive a single column scan to update button press history.
for row in 0..ROWS {
let index = column + (row * COLS);
let current_state = self.pins.read_row(row);
if current_state == self.pressed[index] {
self.debounce_counter[index] = 0;
continue;
}
self.debounce_counter[button_index] += 1;
if self.debounce_counter[button_index] >= self.debounce {
self.pressed[button_index] = current_state;
self.debounce_counter[index] = self.debounce_counter[index].saturating_add(1);
if self.debounce_counter[index] < self.debounce_threshold {
continue;
}
self.debounce_counter[index] = 0;
if current_state {
if self.should_register_press(index) {
self.pressed[index] = true;
}
} else {
self.pressed[index] = false;
}
}
}
/// Return a copy of the debounced pressed state for all buttons.
///
/// For small `N` this copy is cheap. If needed, the API could be extended to
/// return a reference in the future.
pub fn buttons_pressed(&mut self) -> [bool; N] {
self.pressed
fn should_register_press(&mut self, index: usize) -> bool {
// Decide if a press should register given debounce timing.
let elapsed = self.scan_counter.wrapping_sub(self.last_press_scan[index]);
let can_register = self.last_press_scan[index] == 0 || elapsed >= self.min_press_gap_scans;
if can_register {
self.last_press_scan[index] = self.scan_counter;
}
can_register
}
#[cfg(all(test, feature = "std"))]
pub(crate) fn process_column_for_test(&mut self, column: usize) {
self.process_column(column);
}
#[cfg(all(test, feature = "std"))]
pub(crate) fn set_scan_counter(&mut self, value: u32) {
self.scan_counter = value;
}
#[cfg(all(test, feature = "std"))]
pub(crate) fn bump_scan_counter(&mut self) {
self.scan_counter = self.scan_counter.wrapping_add(1);
}
}
@ -121,105 +174,71 @@ pub fn buttons_pressed(&mut self) -> [bool; N] {
mod tests {
use super::*;
use core::cell::Cell;
use embedded_hal::digital::ErrorType;
use std::rc::Rc;
struct MockInputPin {
state: Rc<Cell<bool>>,
#[derive(Clone)]
struct MockPins {
row_state: Rc<Cell<bool>>,
column_state: Rc<Cell<bool>>,
}
impl MockInputPin {
fn new(state: Rc<Cell<bool>>) -> Self {
Self { state }
impl MockPins {
fn new(row_state: Rc<Cell<bool>>, column_state: Rc<Cell<bool>>) -> Self {
// Build a button matrix scanner with default state tracking arrays.
Self {
row_state,
column_state,
}
}
}
impl ErrorType for MockInputPin {
type Error = Infallible;
impl MatrixPinAccess<1, 1> for MockPins {
fn init_columns(&mut self) {
// Simulate the hardware by driving the single column high by default.
self.column_state.set(true);
}
impl InputPin for MockInputPin {
fn is_high(&mut self) -> Result<bool, Self::Error> {
Ok(!self.state.get())
fn set_column_low(&mut self, _column: usize) {
// Drop the mock column low to emulate scanning behaviour.
self.column_state.set(false);
}
fn is_low(&mut self) -> Result<bool, Self::Error> {
Ok(self.state.get())
fn set_column_high(&mut self, _column: usize) {
// Release the mock column back to the idle high state.
self.column_state.set(true);
}
fn read_row(&mut self, _row: usize) -> bool {
// Return the mocked row state so tests can control pressed/unpressed.
self.row_state.get()
}
}
struct MockOutputPin {
state: Rc<Cell<bool>>,
}
impl MockOutputPin {
fn new(state: Rc<Cell<bool>>) -> Self {
Self { state }
}
}
impl ErrorType for MockOutputPin {
type Error = Infallible;
}
impl OutputPin for MockOutputPin {
fn set_high(&mut self) -> Result<(), Self::Error> {
self.state.set(true);
Ok(())
}
fn set_low(&mut self) -> Result<(), Self::Error> {
self.state.set(false);
Ok(())
}
}
fn matrix_fixture() -> (
ButtonMatrix<'static, 1, 1, 1>,
fn fixture() -> (
ButtonMatrix<MockPins, 1, 1, 1>,
Rc<Cell<bool>>,
Rc<Cell<bool>>,
) {
let row_state = Rc::new(Cell::new(false));
let col_state = Rc::new(Cell::new(false));
let row_pin: &'static mut dyn InputPin<Error = Infallible> =
Box::leak(Box::new(MockInputPin::new(row_state.clone())));
let col_pin: &'static mut dyn OutputPin<Error = Infallible> =
Box::leak(Box::new(MockOutputPin::new(col_state.clone())));
let rows: &'static mut [&'static mut dyn InputPin<Error = Infallible>; 1] =
Box::leak(Box::new([row_pin]));
let cols: &'static mut [&'static mut dyn OutputPin<Error = Infallible>; 1] =
Box::leak(Box::new([col_pin]));
let matrix = ButtonMatrix::new(rows, cols, 2);
(matrix, row_state, col_state)
let row = Rc::new(Cell::new(false));
let column = Rc::new(Cell::new(true));
let pins = MockPins::new(row.clone(), column.clone());
let matrix = ButtonMatrix::new(pins, 2, 3);
(matrix, row, column)
}
#[test]
fn init_pins_sets_columns_high() {
let (mut matrix, _row_state, col_state) = matrix_fixture();
assert!(!col_state.get());
matrix.init_pins();
assert!(col_state.get());
}
fn debounce_requires_consecutive_scans() {
// Debounce logic should require two consecutive pressed scans before registering.
let (mut matrix, row, _column) = fixture();
matrix.set_scan_counter(1);
#[test]
fn process_column_obeys_debounce() {
let (mut matrix, row_state, _col_state) = matrix_fixture();
let mut states = matrix.buttons_pressed();
assert!(!states[0]);
row_state.set(true);
matrix.process_column(0);
matrix.process_column(0);
states = matrix.buttons_pressed();
assert!(states[0]);
row.set(true);
matrix.bump_scan_counter();
matrix.process_column_for_test(0);
assert!(!matrix.buttons_pressed()[0]);
row_state.set(false);
matrix.process_column(0);
matrix.process_column(0);
states = matrix.buttons_pressed();
assert!(!states[0]);
matrix.bump_scan_counter();
matrix.process_column_for_test(0);
assert!(matrix.buttons_pressed()[0]);
}
}

View File

@ -1,4 +1,4 @@
//! Button processing for CMDR Joystick 25
//! Button processing for CMDR Joystick
//!
//! Responsibilities
//! - Integrate button matrix results and extra pins
@ -8,7 +8,7 @@
//! - Evaluate special combinations (bootloader, calibration, etc.)
//! - Expose a compact state consumed by USB report generation
use crate::button_matrix::ButtonMatrix;
use crate::button_matrix::{ButtonMatrix, MatrixPins};
use crate::hardware::{AXIS_CENTER, BUTTON_COLS, BUTTON_ROWS, NUMBER_OF_BUTTONS};
use crate::mapping::*;
use embedded_hal::digital::InputPin;
@ -17,6 +17,13 @@ use rp2040_hal::timer::Timer;
// Total buttons including the two extra (nonmatrix) buttons
pub const TOTAL_BUTTONS: usize = NUMBER_OF_BUTTONS + 2;
pub type JoystickButtonMatrix = ButtonMatrix<
MatrixPins<{ BUTTON_ROWS }, { BUTTON_COLS }>,
{ BUTTON_ROWS },
{ BUTTON_COLS },
{ NUMBER_OF_BUTTONS },
>;
// ==================== BUTTON STRUCT ====================
#[derive(Copy, Clone, Default)]
@ -60,6 +67,7 @@ pub struct ButtonManager {
impl Default for ButtonManager {
fn default() -> Self {
// Build a button manager with default-initialized buttons and state flags.
Self::new()
}
}
@ -75,10 +83,7 @@ impl ButtonManager {
}
/// Update button states from the button matrix snapshot.
pub fn update_from_matrix(
&mut self,
matrix: &mut ButtonMatrix<BUTTON_ROWS, BUTTON_COLS, NUMBER_OF_BUTTONS>,
) {
pub fn update_from_matrix(&mut self, matrix: &mut JoystickButtonMatrix) {
for (index, key) in matrix.buttons_pressed().iter().enumerate() {
self.buttons[index].pressed = *key;
}
@ -98,40 +103,41 @@ impl ButtonManager {
/// Filter HAT switches so only a single direction (or center) can be active.
pub fn filter_hat_switches(&mut self) {
// Filter left hat switch buttons
for i in BUTTON_TOP_LEFT_HAT_UP..=BUTTON_TOP_LEFT_HAT_LEFT {
if (BUTTON_TOP_LEFT_HAT_UP..=BUTTON_TOP_LEFT_HAT_LEFT)
.filter(|&j| j != i)
.any(|j| self.buttons[j].pressed)
{
self.buttons[i].pressed = false;
}
const LEFT_HAT_DIRECTIONS: [usize; 4] = [
BUTTON_TOP_LEFT_HAT_UP,
BUTTON_TOP_LEFT_HAT_RIGHT,
BUTTON_TOP_LEFT_HAT_DOWN,
BUTTON_TOP_LEFT_HAT_LEFT,
];
const RIGHT_HAT_DIRECTIONS: [usize; 4] = [
BUTTON_TOP_RIGHT_HAT_UP,
BUTTON_TOP_RIGHT_HAT_RIGHT,
BUTTON_TOP_RIGHT_HAT_DOWN,
BUTTON_TOP_RIGHT_HAT_LEFT,
];
self.reconcile_hat(&LEFT_HAT_DIRECTIONS, BUTTON_TOP_LEFT_HAT);
self.reconcile_hat(&RIGHT_HAT_DIRECTIONS, BUTTON_TOP_RIGHT_HAT);
}
// Fix button state for center hat press on left hat
if self.buttons[BUTTON_TOP_LEFT_HAT_UP..=BUTTON_TOP_LEFT_HAT_LEFT]
fn reconcile_hat(&mut self, directions: &[usize; 4], center: usize) {
// Normalize hat inputs by clearing the center and conflicting directions.
let pressed_count = directions
.iter()
.any(|b| b.pressed)
{
self.buttons[BUTTON_TOP_LEFT_HAT].pressed = false;
.filter(|&&index| self.buttons[index].pressed)
.count();
if pressed_count == 0 {
return;
}
// Filter right hat switch buttons
for i in BUTTON_TOP_RIGHT_HAT_UP..=BUTTON_TOP_RIGHT_HAT_LEFT {
if (BUTTON_TOP_RIGHT_HAT_UP..=BUTTON_TOP_RIGHT_HAT_LEFT)
.filter(|&j| j != i)
.any(|j| self.buttons[j].pressed)
{
self.buttons[i].pressed = false;
}
}
self.buttons[center].pressed = false;
// Fix button state for center hat press on right hat
if self.buttons[BUTTON_TOP_RIGHT_HAT_UP..=BUTTON_TOP_RIGHT_HAT_LEFT]
.iter()
.any(|b| b.pressed)
{
self.buttons[BUTTON_TOP_RIGHT_HAT].pressed = false;
if pressed_count >= 2 {
for &index in directions.iter() {
self.buttons[index].pressed = false;
}
}
}
@ -232,6 +238,7 @@ impl ButtonManager {
/// Get a change event for a button: Some(true)=press, Some(false)=release, None=no change.
fn get_button_press_event(&self, button_index: usize) -> Option<bool> {
// Report the updated pressed state whenever it differs from the previous sample.
let button = &self.buttons[button_index];
if button.pressed != button.previous_pressed {
Some(button.pressed)
@ -261,6 +268,7 @@ impl ButtonManager {
/// - Short press: on release (and if long not activated), activate `usb_button`
/// - USB press lifetime: autorelease after a minimal hold so the host sees a pulse
fn update_button_press_type(button: &mut Button, current_time: u32) {
// Transition a single button between short/long press USB outputs.
const LONG_PRESS_THRESHOLD: u32 = 200;
// Pressing button
@ -330,12 +338,14 @@ mod tests {
#[test]
fn test_button_manager_creation() {
// Button manager should allocate an entry for every physical button.
let manager = ButtonManager::new();
assert_eq!(manager.buttons.len(), TOTAL_BUTTONS);
}
#[test]
fn test_button_default_state() {
// Default button instances start unpressed with no lingering USB state.
let button = Button::default();
assert!(!button.pressed);
assert!(!button.previous_pressed);
@ -345,6 +355,7 @@ mod tests {
#[test]
fn test_special_action_combinations() {
// The bootloader combo should trigger when all required buttons are pressed.
let mut manager = ButtonManager::new();
// Test bootloader combination
@ -358,6 +369,7 @@ mod tests {
#[test]
fn test_calibration_combination() {
// Calibration combo should generate the start-calibration action.
let mut manager = ButtonManager::new();
// Test calibration combination
@ -371,6 +383,7 @@ mod tests {
#[test]
fn test_throttle_hold_center() {
// Throttle hold combo should capture the centered axis value when centered.
let mut manager = ButtonManager::new();
manager.buttons[TH_BUTTON].pressed = true;
manager.buttons[TH_BUTTON].previous_pressed = false;
@ -381,6 +394,7 @@ mod tests {
#[test]
fn test_throttle_hold_value() {
// Off-center throttle hold should capture the live axis value for hold.
let mut manager = ButtonManager::new();
manager.buttons[TH_BUTTON].pressed = true;
manager.buttons[TH_BUTTON].previous_pressed = false;
@ -392,6 +406,7 @@ mod tests {
#[test]
fn test_virtual_throttle_toggle() {
// Virtual throttle button should emit the toggle action when pressed.
let mut manager = ButtonManager::new();
manager.buttons[VT_BUTTON].pressed = true;
manager.buttons[VT_BUTTON].previous_pressed = false;
@ -402,6 +417,7 @@ mod tests {
#[test]
fn test_hat_switch_filtering_left() {
// Left hat should clear center and conflicting directions when multiple inputs are active.
let mut manager = ButtonManager::new();
// Press multiple directional buttons on left hat
@ -410,15 +426,17 @@ mod tests {
manager.filter_hat_switches();
// Only one should remain (implementation filters out conflicting ones)
// Multiple directions cancel the hat output completely
let pressed_count = (BUTTON_TOP_LEFT_HAT_UP..=BUTTON_TOP_LEFT_HAT_LEFT)
.filter(|&i| manager.buttons[i].pressed)
.count();
assert!(pressed_count <= 1);
assert_eq!(pressed_count, 0);
assert!(!manager.buttons[BUTTON_TOP_LEFT_HAT].pressed);
}
#[test]
fn test_hat_switch_filtering_right() {
// Right hat should behave the same way, disabling conflicts and the center button.
let mut manager = ButtonManager::new();
// Press multiple directional buttons on right hat
@ -427,15 +445,17 @@ mod tests {
manager.filter_hat_switches();
// Only one should remain (implementation filters out conflicting ones)
// Multiple directions cancel the hat output completely
let pressed_count = (BUTTON_TOP_RIGHT_HAT_UP..=BUTTON_TOP_RIGHT_HAT_LEFT)
.filter(|&i| manager.buttons[i].pressed)
.count();
assert!(pressed_count <= 1);
assert_eq!(pressed_count, 0);
assert!(!manager.buttons[BUTTON_TOP_RIGHT_HAT].pressed);
}
#[test]
fn test_hat_center_button_filtering() {
// Pressing a direction should suppress the corresponding hat center button.
let mut manager = ButtonManager::new();
// Press directional button and center button
@ -446,10 +466,50 @@ mod tests {
// Center button should be disabled when directional is pressed
assert!(!manager.buttons[BUTTON_TOP_LEFT_HAT].pressed);
// But single direction should remain active
assert!(manager.buttons[BUTTON_TOP_LEFT_HAT_UP].pressed);
}
#[test]
fn test_hat_switch_single_direction_allowed() {
// A single direction press must remain active for both hats.
let mut manager = ButtonManager::new();
// Press only one directional button on left hat
manager.buttons[BUTTON_TOP_LEFT_HAT_UP].pressed = true;
manager.filter_hat_switches();
// Single direction should remain active
assert!(manager.buttons[BUTTON_TOP_LEFT_HAT_UP].pressed);
// Test same for right hat
let mut manager = ButtonManager::new();
manager.buttons[BUTTON_TOP_RIGHT_HAT_DOWN].pressed = true;
manager.filter_hat_switches();
// Single direction should remain active
assert!(manager.buttons[BUTTON_TOP_RIGHT_HAT_DOWN].pressed);
}
#[test]
fn test_hat_center_button_works_alone() {
// When no direction is pressed, the center button should report as pressed.
let mut manager = ButtonManager::new();
// Press only center button (no directions)
manager.buttons[BUTTON_TOP_LEFT_HAT].pressed = true;
manager.filter_hat_switches();
// Center button should remain active when no directions are pressed
assert!(manager.buttons[BUTTON_TOP_LEFT_HAT].pressed);
}
#[test]
fn test_button_press_type_short_press() {
// Short presses should emit the primary USB button and flag a USB change.
let mut button = Button::default();
button.usb_button = 1;
button.enable_long_press = false;
@ -470,6 +530,7 @@ mod tests {
#[test]
fn test_button_press_type_long_press() {
// Long presses should switch to the alternate USB button and mark handled state.
let mut button = Button::default();
button.usb_button = 1;
button.usb_button_long = 2;
@ -490,6 +551,7 @@ mod tests {
#[test]
fn test_button_press_type_long_press_auto_release_once() {
// Non-hold long presses should auto-release once after triggering the long press.
let mut button = Button::default();
button.usb_button_long = 2;
button.enable_long_press = true;
@ -521,6 +583,7 @@ mod tests {
#[test]
fn test_timer_integration_method_exists() {
// Document that the timer-backed helper stays callable without hardware wiring.
let manager = ButtonManager::new();
// This test verifies the timer integration method signature and basic functionality

View File

@ -1,4 +1,4 @@
//! Calibration management for CMDR Joystick 25
//! Calibration management for CMDR Joystick
//!
//! Responsibilities
//! - Manage the calibration lifecycle (start/active/stop)
@ -176,6 +176,7 @@ impl CalibrationManager {
/// Reset each axis calibration to its current smoothed center.
fn reset_axis_calibration(
// Recenter all axis calibration values using current smoother readings.
&self,
axes: &mut [GimbalAxis; NBR_OF_GIMBAL_AXIS],
smoothers: &[DynamicSmootherEcoI32; NBR_OF_GIMBAL_AXIS],
@ -191,6 +192,7 @@ impl CalibrationManager {
impl Default for CalibrationManager {
fn default() -> Self {
// Construct a calibration manager using the Default implementation.
Self::new()
}
}
@ -205,6 +207,7 @@ mod tests {
#[test]
fn test_calibration_manager_creation() {
// Report whether calibration is currently active.
let manager = CalibrationManager::new();
assert!(!manager.is_active());
assert_eq!(manager.get_gimbal_mode(), GIMBAL_MODE_M10);
@ -212,6 +215,7 @@ mod tests {
#[test]
fn test_calibration_state_management() {
// Start and stop transitions should flip the active flag accordingly.
let mut manager = CalibrationManager::new();
// Initially inactive
@ -228,6 +232,7 @@ mod tests {
#[test]
fn test_gimbal_mode_management() {
// Manual mode setter should swap between M10 and M7 without side effects.
let mut manager = CalibrationManager::new();
// Default mode
@ -244,6 +249,7 @@ mod tests {
#[test]
fn test_dynamic_calibration_inactive() {
// Inactive calibration should ignore updates and leave min/max untouched.
let manager = CalibrationManager::new();
let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
@ -271,6 +277,7 @@ mod tests {
#[test]
fn test_dynamic_calibration_active() {
// Active calibration should track new lows/highs as smoothed values change.
let mut manager = CalibrationManager::new();
manager.start_calibration();
@ -319,6 +326,7 @@ mod tests {
#[test]
fn test_mode_selection_inactive() {
// Mode change commands should fail when calibration mode is not active.
let mut manager = CalibrationManager::new();
let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
@ -341,6 +349,7 @@ mod tests {
#[test]
fn test_load_axis_calibration_success() {
// Successful EEPROM reads should populate the axis calibration tuple.
let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
// Mock EEPROM data simulating successful calibration read
@ -373,6 +382,7 @@ mod tests {
#[test]
fn test_load_axis_calibration_failure() {
// Failed EEPROM reads should leave default calibration values intact.
let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
let original_axes = axes;
@ -391,6 +401,7 @@ mod tests {
#[test]
fn test_load_gimbal_mode_success() {
// Reading the stored gimbal mode should return the persisted value.
// Mock successful EEPROM read for M7 mode
let mut read_fn = |addr: u32| {
match addr {
@ -405,6 +416,7 @@ mod tests {
#[test]
fn test_load_gimbal_mode_failure() {
// Read failures should fall back to the default M10 mode.
// Mock EEPROM read failure
let mut read_fn = |_addr: u32| Err(());
@ -414,6 +426,7 @@ mod tests {
#[test]
fn test_update_calibration_inactive() {
// When calibration is inactive, mode/set/save helpers should be no-ops.
let mut manager = CalibrationManager::new();
let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
@ -442,6 +455,7 @@ mod tests {
#[test]
fn test_process_mode_selection_m10_command() {
// Active M10 command should set mode and recenter axes using smoother values.
let mut manager = CalibrationManager::new();
manager.start_calibration();
let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
@ -469,6 +483,7 @@ mod tests {
#[test]
fn test_process_mode_selection_m7_command() {
// Active M7 command should likewise set mode and recenter axes.
let mut manager = CalibrationManager::new();
manager.start_calibration();
let mut axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
@ -496,6 +511,7 @@ mod tests {
#[test]
fn test_save_calibration_command() {
// Successful saves should write EEPROM data and exit calibration mode.
let mut manager = CalibrationManager::new();
manager.start_calibration();
let axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
@ -515,6 +531,7 @@ mod tests {
#[test]
fn test_save_calibration_failure_keeps_active() {
// Write failures should keep calibration active for retrials.
let mut manager = CalibrationManager::new();
manager.start_calibration();
let axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];
@ -529,6 +546,7 @@ mod tests {
#[test]
fn test_save_calibration_inactive() {
// Save attempts while inactive should no-op without touching storage.
let mut manager = CalibrationManager::new(); // Note: not starting calibration
let axes = [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS];

View File

@ -79,6 +79,7 @@ mod tests {
#[test]
fn test_generate_expo_lut_boundaries() {
// Ensure expo LUT boundary entries clamp to ADC range.
let lut = generate_expo_lut(0.5);
assert_eq!(lut[0], ADC_MIN);
assert_eq!(lut[ADC_MAX as usize], ADC_MAX);
@ -86,6 +87,7 @@ mod tests {
#[test]
fn test_generate_expo_lut_center_point() {
// Midpoint of the expo LUT stays near the physical center.
let lut = generate_expo_lut(0.5);
let center_index = (ADC_MAX / 2) as usize;
let center_value = lut[center_index];
@ -94,6 +96,7 @@ mod tests {
#[test]
fn test_generate_expo_lut_different_factors() {
// Different expo factors should yield distinct transfer functions at the same index.
let lut_linear = generate_expo_lut(0.0);
let lut_expo = generate_expo_lut(1.0);
let quarter_point = (ADC_MAX / 4) as usize;
@ -102,6 +105,7 @@ mod tests {
#[test]
fn test_apply_expo_curve_no_expo() {
// With zero expo factor the lookup table should behave linearly.
let lut = generate_expo_lut(0.0);
let input_value = 1000u16;
let result = apply_expo_curve(input_value, &lut);
@ -111,6 +115,7 @@ mod tests {
#[test]
fn test_apply_expo_curve_with_expo() {
// Non-zero expo factor should change outputs relative to the linear LUT.
let lut_linear = generate_expo_lut(0.0);
let lut_expo = generate_expo_lut(0.5);
let test_value = 1000u16;
@ -123,6 +128,7 @@ mod tests {
#[test]
fn test_apply_expo_curve_center_unchanged() {
// Expo mapping should leave the center point near the mechanical center.
let lut = generate_expo_lut(0.5);
let result = apply_expo_curve(AXIS_CENTER, &lut);
// Center point should remain close to center
@ -131,21 +137,25 @@ mod tests {
#[test]
fn test_constrain_within_range() {
// Values inside limits should be returned unchanged by constrain.
assert_eq!(constrain(50u16, 0u16, 100u16), 50u16);
}
#[test]
fn test_constrain_above_range() {
// Values above the upper bound should clamp to the max.
assert_eq!(constrain(150u16, 0u16, 100u16), 100u16);
}
#[test]
fn test_constrain_below_range() {
// Values below the lower bound should clamp to the min.
assert_eq!(constrain(0u16, 50u16, 100u16), 50u16);
}
#[test]
fn test_expo_integration_real_world_values() {
// Representative axis values should always map within the ADC domain.
let lut = generate_expo_lut(0.3);
let test_values = [500u16, 1000u16, 2000u16, 3000u16];

View File

@ -1,190 +1,195 @@
//! Hardware configuration for CMDR Joystick 25 (RP2040)
//! Hardware configuration for CMDR Joystick (RP2040)
//!
//! Centralizes board constants, GPIO mappings, timing cadences and helper
//! macros to keep hardware details out of business logic.
//! Mirrors the structure introduced for the CMDR Keyboard firmware so that
//! bring-up, pin management, and timing constants follow the same layout.
use rp2040_hal::gpio::Pins;
use rp2040_hal::gpio::{
self, DynPinId, FunctionI2C, FunctionPio0, FunctionSioInput, FunctionSioOutput, Pin, PullNone,
PullUp,
};
// ==================== CRYSTAL AND USB CONSTANTS ====================
/// External crystal frequency (Hz).
pub const XTAL_FREQ_HZ: u32 = 12_000_000u32;
/// USB Vendor ID.
pub const XTAL_FREQ_HZ: u32 = 12_000_000;
/// USB Vendor ID/Product ID.
pub const USB_VID: u16 = 0x1209;
/// USB Product ID.
pub const USB_PID: u16 = 0x0002;
// ==================== JOYSTICK CONSTANTS ====================
/// Button matrix geometry (rows).
/// Button matrix geometry (rows/cols) and count.
pub const BUTTON_ROWS: usize = 5;
/// Button matrix geometry (columns).
pub const BUTTON_COLS: usize = 5;
/// Total number of matrix buttons.
pub const NUMBER_OF_BUTTONS: usize = BUTTON_ROWS * BUTTON_COLS;
/// ADC raw minimum (12bit).
/// ADC characteristics.
pub const ADC_MIN: u16 = 0;
/// ADC raw maximum (12bit).
pub const ADC_MAX: u16 = 4095;
/// Logical axis center.
pub const AXIS_CENTER: u16 = (ADC_MIN + ADC_MAX) / 2;
/// Number of physical gimbal axes.
pub const NBR_OF_GIMBAL_AXIS: usize = 4;
/// Debounce threshold (in scans) for the matrix.
pub const DEBOUNCE: u8 = 10;
/// Bytes reserved in EEPROM for calibration data + gimbal mode.
/// Debounce thresholds.
pub const MATRIX_DEBOUNCE_SCANS: u8 = 15;
pub const MIN_PRESS_SPACING_SCANS: u32 = 25; // ~5ms @ 200µs cadence
/// EEPROM storage length (calibration data + gimbal mode).
pub const EEPROM_DATA_LENGTH: usize = 25;
// ==================== GPIO PIN DEFINITIONS ====================
/// Logical mapping between board functions and GPIO numbers.
pub mod pins {
/// Extra buttons (TX/RX pins)
pub const LEFT_EXTRA_BUTTON_PIN: u8 = 1;
pub const RIGHT_EXTRA_BUTTON_PIN: u8 = 0;
/// Button matrix row pins
pub const BUTTON_ROW_PIN_0: u8 = 6;
pub const BUTTON_ROW_PIN_1: u8 = 8;
pub const BUTTON_ROW_PIN_2: u8 = 4;
pub const BUTTON_ROW_PIN_3: u8 = 7;
pub const BUTTON_ROW_PIN_4: u8 = 5;
/// Button matrix column pins
pub const BUTTON_COL_PIN_0: u8 = 9;
pub const BUTTON_COL_PIN_1: u8 = 10;
pub const BUTTON_COL_PIN_2: u8 = 11;
pub const BUTTON_COL_PIN_3: u8 = 12;
pub const BUTTON_COL_PIN_4: u8 = 13;
/// ADC pins for gimbal axes
pub const ADC_LEFT_X_PIN: u8 = 29;
pub const ADC_LEFT_Y_PIN: u8 = 28;
pub const ADC_RIGHT_X_PIN: u8 = 27;
pub const ADC_RIGHT_Y_PIN: u8 = 26;
/// Status LED pin
pub const STATUS_LED_PIN: u8 = 16;
/// I2C pins for EEPROM
pub const I2C_SDA_PIN: u8 = 14;
pub const I2C_SCL_PIN: u8 = 15;
}
// ==================== I2C CONFIGURATION ====================
/// I2C frequency and system clock helpers for the EEPROM bus.
pub mod i2c {
use fugit::{Rate, RateExtU32};
pub const I2C_FREQUENCY_HZ: u32 = 400_000;
pub fn i2c_frequency() -> Rate<u32, 1, 1> {
I2C_FREQUENCY_HZ.Hz()
}
pub const SYSTEM_CLOCK_HZ: u32 = 125_000_000;
pub fn system_clock() -> Rate<u32, 1, 1> {
SYSTEM_CLOCK_HZ.Hz()
}
}
// ==================== TIMER INTERVALS ====================
/// Cadences for periodic firmware tasks.
pub mod timers {
/// Status LED update interval (ms).
pub const STATUS_LED_INTERVAL_MS: u32 = 40;
/// Button matrix scan interval (µs).
pub const SCAN_INTERVAL_US: u32 = 200;
/// USB HID report interval (ms).
pub const USB_UPDATE_INTERVAL_MS: u32 = 1;
/// USB activity timeout (ms) - stop sending reports after this period of inactivity.
pub const USB_ACTIVITY_TIMEOUT_MS: u32 = 5_000; // 5 seconds
}
// ==================== USB DEVICE CONFIGURATION ====================
/// USB string descriptors.
pub mod usb {
pub const MANUFACTURER: &str = "CMtec";
pub const PRODUCT: &str = "CMDR Joystick 25";
pub const PRODUCT: &str = "CMDR Joystick";
pub const SERIAL_NUMBER: &str = "0001";
}
// ==================== PIN ACCESS MACROS ====================
/// Macro to access typed GPIO pins using board constants.
/// Avoids scattering raw GPIO numbers; each arm references the constant it maps.
#[macro_export]
macro_rules! get_pin {
($pins:expr, left_extra_button) => {{
const _: u8 = $crate::hardware::pins::LEFT_EXTRA_BUTTON_PIN;
$pins.gpio1
}};
($pins:expr, right_extra_button) => {{
const _: u8 = $crate::hardware::pins::RIGHT_EXTRA_BUTTON_PIN;
$pins.gpio0
}};
($pins:expr, button_row_0) => {{
const _: u8 = $crate::hardware::pins::BUTTON_ROW_PIN_0;
$pins.gpio6
}};
($pins:expr, button_row_1) => {{
const _: u8 = $crate::hardware::pins::BUTTON_ROW_PIN_1;
$pins.gpio8
}};
($pins:expr, button_row_2) => {{
const _: u8 = $crate::hardware::pins::BUTTON_ROW_PIN_2;
$pins.gpio4
}};
($pins:expr, button_row_3) => {{
const _: u8 = $crate::hardware::pins::BUTTON_ROW_PIN_3;
$pins.gpio7
}};
($pins:expr, button_row_4) => {{
const _: u8 = $crate::hardware::pins::BUTTON_ROW_PIN_4;
$pins.gpio5
}};
($pins:expr, button_col_0) => {{
const _: u8 = $crate::hardware::pins::BUTTON_COL_PIN_0;
$pins.gpio9
}};
($pins:expr, button_col_1) => {{
const _: u8 = $crate::hardware::pins::BUTTON_COL_PIN_1;
$pins.gpio10
}};
($pins:expr, button_col_2) => {{
const _: u8 = $crate::hardware::pins::BUTTON_COL_PIN_2;
$pins.gpio11
}};
($pins:expr, button_col_3) => {{
const _: u8 = $crate::hardware::pins::BUTTON_COL_PIN_3;
$pins.gpio12
}};
($pins:expr, button_col_4) => {{
const _: u8 = $crate::hardware::pins::BUTTON_COL_PIN_4;
$pins.gpio13
}};
($pins:expr, adc_left_x) => {{
const _: u8 = $crate::hardware::pins::ADC_LEFT_X_PIN;
$pins.gpio29
}};
($pins:expr, adc_left_y) => {{
const _: u8 = $crate::hardware::pins::ADC_LEFT_Y_PIN;
$pins.gpio28
}};
($pins:expr, adc_right_x) => {{
const _: u8 = $crate::hardware::pins::ADC_RIGHT_X_PIN;
$pins.gpio27
}};
($pins:expr, adc_right_y) => {{
const _: u8 = $crate::hardware::pins::ADC_RIGHT_Y_PIN;
$pins.gpio26
}};
($pins:expr, status_led) => {{
const _: u8 = $crate::hardware::pins::STATUS_LED_PIN;
$pins.gpio16
}};
($pins:expr, i2c_sda) => {{
const _: u8 = $crate::hardware::pins::I2C_SDA_PIN;
$pins.gpio14
}};
($pins:expr, i2c_scl) => {{
const _: u8 = $crate::hardware::pins::I2C_SCL_PIN;
$pins.gpio15
}};
/// Timing cadences.
pub mod timers {
pub const STATUS_LED_INTERVAL_MS: u32 = 40;
pub const SCAN_INTERVAL_US: u32 = 200;
pub const USB_UPDATE_INTERVAL_MS: u32 = 1;
pub const USB_ACTIVITY_TIMEOUT_MS: u32 = 5_000;
}
/// I2C helpers.
pub mod i2c {
use eeprom24x::SlaveAddr;
use fugit::{Rate, RateExtU32};
pub const FREQUENCY_HZ: u32 = 400_000;
pub const SYSTEM_CLOCK_HZ: u32 = 125_000_000;
pub fn frequency() -> Rate<u32, 1, 1> {
FREQUENCY_HZ.Hz()
}
pub fn system_clock() -> Rate<u32, 1, 1> {
SYSTEM_CLOCK_HZ.Hz()
}
pub const EEPROM_ADDRESS: SlaveAddr = SlaveAddr::Alternative(false, false, false);
}
/// Raw GPIO constants retained for documentation/reference.
pub mod pins {
pub const LEFT_EXTRA_BUTTON: u8 = 1;
pub const RIGHT_EXTRA_BUTTON: u8 = 0;
pub const BUTTON_ROW_0: u8 = 6;
pub const BUTTON_ROW_1: u8 = 8;
pub const BUTTON_ROW_2: u8 = 4;
pub const BUTTON_ROW_3: u8 = 7;
pub const BUTTON_ROW_4: u8 = 5;
pub const BUTTON_COL_0: u8 = 9;
pub const BUTTON_COL_1: u8 = 10;
pub const BUTTON_COL_2: u8 = 11;
pub const BUTTON_COL_3: u8 = 12;
pub const BUTTON_COL_4: u8 = 13;
pub const ADC_LEFT_X: u8 = 29;
pub const ADC_LEFT_Y: u8 = 28;
pub const ADC_RIGHT_X: u8 = 27;
pub const ADC_RIGHT_Y: u8 = 26;
pub const STATUS_LED: u8 = 16;
pub const I2C_SDA: u8 = 14;
pub const I2C_SCL: u8 = 15;
}
/// Matrix row pins (dynamic to simplify scanning code).
pub type MatrixRowPin = Pin<DynPinId, FunctionSioInput, PullUp>;
/// Matrix column pins (dynamic push-pull outputs).
pub type MatrixColPin = Pin<DynPinId, FunctionSioOutput, PullNone>;
/// Extra buttons (pull-up inputs).
pub type ExtraButtonPin = Pin<DynPinId, FunctionSioInput, PullUp>;
/// Status LED pin configured for PIO output.
pub type StatusLedPin = Pin<gpio::bank0::Gpio16, FunctionPio0, PullNone>;
/// I2C SDA/SCL pins after reconfiguration.
pub type I2cSdaPin = Pin<gpio::bank0::Gpio14, FunctionI2C, PullUp>;
pub type I2cSclPin = Pin<gpio::bank0::Gpio15, FunctionI2C, PullUp>;
/// Analog axis input pins (remain as SIO inputs until wrapped by `AdcPin`).
pub struct AxisInputs {
pub left_x: Pin<gpio::bank0::Gpio29, FunctionSioInput, PullNone>,
pub left_y: Pin<gpio::bank0::Gpio28, FunctionSioInput, PullNone>,
pub right_x: Pin<gpio::bank0::Gpio27, FunctionSioInput, PullNone>,
pub right_y: Pin<gpio::bank0::Gpio26, FunctionSioInput, PullNone>,
}
/// Bundle returned by `split_board_pins`.
pub struct BoardPins {
pub matrix_rows: [MatrixRowPin; BUTTON_ROWS],
pub matrix_cols: [MatrixColPin; BUTTON_COLS],
pub left_extra_button: ExtraButtonPin,
pub right_extra_button: ExtraButtonPin,
pub axis_inputs: AxisInputs,
pub status_led: StatusLedPin,
pub i2c_sda: I2cSdaPin,
pub i2c_scl: I2cSclPin,
}
impl BoardPins {
pub fn new(pins: Pins) -> Self {
let row0 = pins.gpio6.into_pull_up_input().into_dyn_pin();
let row1 = pins.gpio8.into_pull_up_input().into_dyn_pin();
let row2 = pins.gpio4.into_pull_up_input().into_dyn_pin();
let row3 = pins.gpio7.into_pull_up_input().into_dyn_pin();
let row4 = pins.gpio5.into_pull_up_input().into_dyn_pin();
let col0 = pins
.gpio9
.into_push_pull_output()
.into_pull_type::<PullNone>()
.into_dyn_pin();
let col1 = pins
.gpio10
.into_push_pull_output()
.into_pull_type::<PullNone>()
.into_dyn_pin();
let col2 = pins
.gpio11
.into_push_pull_output()
.into_pull_type::<PullNone>()
.into_dyn_pin();
let col3 = pins
.gpio12
.into_push_pull_output()
.into_pull_type::<PullNone>()
.into_dyn_pin();
let col4 = pins
.gpio13
.into_push_pull_output()
.into_pull_type::<PullNone>()
.into_dyn_pin();
let left_extra = pins.gpio1.into_pull_up_input().into_dyn_pin();
let right_extra = pins.gpio0.into_pull_up_input().into_dyn_pin();
let axis_inputs = AxisInputs {
left_x: pins.gpio29.into_floating_input(),
left_y: pins.gpio28.into_floating_input(),
right_x: pins.gpio27.into_floating_input(),
right_y: pins.gpio26.into_floating_input(),
};
let status_led = pins
.gpio16
.into_function::<FunctionPio0>()
.into_pull_type::<PullNone>();
let i2c_sda = pins
.gpio14
.into_function::<FunctionI2C>()
.into_pull_type::<PullUp>();
let i2c_scl = pins
.gpio15
.into_function::<FunctionI2C>()
.into_pull_type::<PullUp>();
Self {
matrix_rows: [row0, row1, row2, row3, row4],
matrix_cols: [col0, col1, col2, col3, col4],
left_extra_button: left_extra,
right_extra_button: right_extra,
axis_inputs,
status_led,
i2c_sda,
i2c_scl,
}
}
}

321
rp2040/src/joystick.rs Normal file
View File

@ -0,0 +1,321 @@
//! Runtime state for the CMDR Joystick.
//!
//! This mirrors the `KeyboardState` abstraction from the keyboard refactor and
//! concentrates axis/button/calibration logic alongside USB bookkeeping.
use crate::axis::AxisManager;
use crate::buttons::{ButtonManager, SpecialAction};
use crate::calibration::CalibrationManager;
use crate::expo::ExpoLUT;
use crate::hardware;
use crate::status::SystemState;
use crate::usb_joystick_device::JoystickReport;
use crate::usb_report::get_joystick_report;
use core::fmt::Debug;
use dyn_smooth::DynamicSmootherEcoI32;
use embedded_hal::digital::InputPin;
use rp2040_hal::timer::Timer;
use usb_device::device::UsbDeviceState;
pub struct UsbState {
pub initialized: bool,
pub active: bool,
pub suspended: bool,
pub send_pending: bool,
pub wake_on_input: bool,
pub idle_mode: bool,
pub activity: bool,
activity_elapsed_ms: u32,
}
impl UsbState {
pub const fn new() -> Self {
Self {
initialized: false,
active: false,
suspended: false,
send_pending: false,
wake_on_input: false,
idle_mode: false,
activity: false,
activity_elapsed_ms: 0,
}
}
pub fn on_poll(&mut self) {
// Called on every USB poll; mark device active and wake from idle.
if !self.initialized {
self.initialized = true;
}
if !self.active {
self.mark_activity();
}
self.active = true;
}
pub fn mark_activity(&mut self) {
// Flag that input activity occurred and reset idle counters.
self.activity = true;
self.activity_elapsed_ms = 0;
self.idle_mode = false;
self.send_pending = true;
}
pub fn handle_input_activity(&mut self) {
// Treat input changes as activity and request wake from suspend.
self.mark_activity();
if self.suspended && self.wake_on_input {
self.wake_on_input = false;
}
}
pub fn on_suspend_change(&mut self, state: UsbDeviceState) {
// Update suspend bookkeeping when USB state changes.
let was_suspended = self.suspended;
self.suspended = state == UsbDeviceState::Suspend;
match (was_suspended, self.suspended) {
(true, false) => {
self.mark_activity();
self.wake_on_input = false;
}
(false, true) => {
self.idle_mode = true;
self.activity = false;
self.send_pending = false;
self.wake_on_input = true;
}
_ => {}
}
}
pub fn advance_idle_timer(&mut self, interval_ms: u32) {
// Age the activity timer, transitioning to idle after the timeout.
if !self.activity {
return;
}
self.activity_elapsed_ms = self.activity_elapsed_ms.saturating_add(interval_ms);
if self.activity_elapsed_ms >= hardware::timers::USB_ACTIVITY_TIMEOUT_MS {
self.activity = false;
self.activity_elapsed_ms = 0;
self.idle_mode = true;
self.send_pending = false;
}
}
pub fn acknowledge_report(&mut self) {
// Reset pending state once the host accepts a report.
self.send_pending = false;
}
}
pub struct JoystickState {
axis_manager: AxisManager,
button_manager: ButtonManager,
calibration_manager: CalibrationManager,
smoother: [DynamicSmootherEcoI32; hardware::NBR_OF_GIMBAL_AXIS],
expo_primary: ExpoLUT,
expo_virtual: ExpoLUT,
vt_enable: bool,
usb: UsbState,
}
impl JoystickState {
pub fn new() -> Self {
// Initialize managers, smoothers, expo curves, and USB state.
Self {
axis_manager: AxisManager::new(),
button_manager: ButtonManager::new(),
calibration_manager: CalibrationManager::new(),
smoother: AxisManager::create_smoothers(),
expo_primary: ExpoLUT::new(0.3),
expo_virtual: ExpoLUT::new(0.6),
vt_enable: false,
usb: UsbState::new(),
}
}
pub fn load_calibration<R>(&mut self, mut read_fn: R)
where
R: FnMut(u32) -> Result<u8, ()>,
{
// Fetch stored axis calibration and gimbal mode from persistent storage.
CalibrationManager::load_axis_calibration(&mut self.axis_manager.axes, &mut read_fn);
let gimbal_mode = CalibrationManager::load_gimbal_mode(&mut read_fn);
self.axis_manager.set_gimbal_mode(gimbal_mode);
self.calibration_manager.set_gimbal_mode(gimbal_mode);
}
pub fn update_button_states<L, R>(
&mut self,
matrix: &mut crate::board::JoystickMatrix,
left_button: &mut L,
right_button: &mut R,
) where
L: InputPin,
R: InputPin,
L::Error: Debug,
R::Error: Debug,
{
// Refresh matrix-driven and discrete button inputs, then normalize hats.
self.button_manager.update_from_matrix(matrix);
self.button_manager
.update_extra_buttons(left_button, right_button);
self.button_manager.filter_hat_switches();
}
pub fn finalize_button_logic(&mut self, timer: &Timer) -> bool {
// Update per-button timers and USB state using the shared hardware timer.
self.button_manager.process_button_logic_with_timer(timer)
}
pub fn check_special_action(&self) -> SpecialAction {
// Inspect button state for bootloader/calibration/hold command combinations.
self.button_manager.check_special_combinations(
self.axis_manager.get_value_before_hold(),
self.calibration_manager.is_active(),
)
}
pub fn handle_special_action<W>(&mut self, action: SpecialAction, mut write_page: W)
where
W: FnMut(u32, &[u8]) -> Result<(), ()>,
{
// Execute the requested special action, updating calibration/throttle state as needed.
match action {
SpecialAction::Bootloader => {}
SpecialAction::StartCalibration => {
for (index, axis) in self.axis_manager.axes.iter_mut().enumerate() {
let centered = self.smoother[index].value() as u16;
axis.center = centered;
axis.min = centered;
axis.max = centered;
}
self.axis_manager.clear_throttle_hold();
self.calibration_manager.start_calibration();
}
SpecialAction::CancelCalibration => {
self.calibration_manager.stop_calibration();
}
SpecialAction::ThrottleHold(value) => {
self.axis_manager.set_throttle_hold(value);
}
SpecialAction::VirtualThrottleToggle => {
self.vt_enable = !self.vt_enable;
}
SpecialAction::CalibrationSetModeM10 => {
if self
.calibration_manager
.set_gimbal_mode_m10(&mut self.axis_manager.axes, &self.smoother)
{
self.axis_manager
.set_gimbal_mode(self.calibration_manager.get_gimbal_mode());
self.axis_manager.clear_throttle_hold();
}
}
SpecialAction::CalibrationSetModeM7 => {
if self
.calibration_manager
.set_gimbal_mode_m7(&mut self.axis_manager.axes, &self.smoother)
{
self.axis_manager
.set_gimbal_mode(self.calibration_manager.get_gimbal_mode());
self.axis_manager.clear_throttle_hold();
}
}
SpecialAction::CalibrationSave => {
self.calibration_manager
.save_calibration(&self.axis_manager.axes, &mut |page, data| {
write_page(page, data)
});
}
SpecialAction::None => {}
}
}
pub fn update_calibration_tracking(&mut self) {
self.calibration_manager
.update_dynamic_calibration(&mut self.axis_manager.axes, &self.smoother);
}
pub fn tick_smoothers(&mut self, raw: &mut [u16; hardware::NBR_OF_GIMBAL_AXIS]) {
self.axis_manager.apply_gimbal_compensation(raw);
self.axis_manager.update_smoothers(&mut self.smoother, raw);
}
pub fn process_axes(&mut self) -> bool {
self.axis_manager
.process_axis_values(&self.smoother, &self.expo_primary)
}
pub fn update_virtual_axes(&mut self) -> bool {
self.axis_manager
.update_virtual_axes(self.button_manager.buttons(), self.vt_enable)
}
pub fn vt_enable(&self) -> bool {
self.vt_enable
}
pub fn usb_state(&mut self) -> &mut UsbState {
&mut self.usb
}
pub fn axis_manager(&mut self) -> &mut AxisManager {
&mut self.axis_manager
}
pub fn button_manager(&mut self) -> &mut ButtonManager {
&mut self.button_manager
}
pub fn expo_virtual(&self) -> &ExpoLUT {
&self.expo_virtual
}
pub fn smoother(&self) -> &[DynamicSmootherEcoI32; hardware::NBR_OF_GIMBAL_AXIS] {
&self.smoother
}
pub fn calibration_manager(&self) -> &CalibrationManager {
&self.calibration_manager
}
pub fn system_state(&self) -> SystemState {
SystemState {
usb_active: self.usb.active,
usb_initialized: self.usb.initialized,
usb_suspended: self.usb.suspended,
idle_mode: self.usb.idle_mode,
calibration_active: self.calibration_manager.is_active(),
throttle_hold_enable: self.axis_manager.throttle_hold_enable,
vt_enable: self.vt_enable,
}
}
pub fn build_report(&mut self) -> JoystickReport {
let virtual_ry = self.axis_manager.get_virtual_ry_value(&self.expo_virtual);
let virtual_rz = self.axis_manager.get_virtual_rz_value(&self.expo_virtual);
get_joystick_report(
self.button_manager.buttons_mut(),
&mut self.axis_manager.axes,
virtual_ry,
virtual_rz,
&self.vt_enable,
)
}
pub fn empty_report() -> JoystickReport {
JoystickReport {
x: 0,
y: 0,
z: 0,
rx: 0,
ry: 0,
rz: 0,
slider: 0,
hat: 8,
buttons: 0,
}
}
}

View File

@ -1,11 +1,18 @@
#![cfg_attr(not(feature = "std"), no_std)]
//! CMDR Joystick 25 firmware library for RP2040.
//! CMDR Joystick firmware library for RP2040.
//!
//! This crate provides the reusable building blocks that power the main
//! firmware: axis processing, button handling, calibration and storage, USB
//! HID reporting, and hardware/status abstractions.
#[cfg(not(feature = "std"))]
pub mod board;
#[cfg(not(feature = "std"))]
pub mod bootloader;
#[cfg(not(feature = "std"))]
pub mod joystick;
/// Axis processing for gimbal and virtual axes (smoothing, expo, holds).
pub mod axis;
/// Row/column scanned button matrix driver with debouncing.
@ -29,6 +36,11 @@ pub mod usb_joystick_device;
/// Convert runtime state into USB HID joystick reports.
pub mod usb_report;
#[cfg(not(feature = "std"))]
pub use board::{AxisAnalogPins, Board, BoardParts, JoystickMatrix, JoystickStatusLed};
#[cfg(not(feature = "std"))]
pub use joystick::{JoystickState, UsbState};
/// Re-exports for convenient access in `main` and downstream consumers.
pub use axis::{AxisManager, GimbalAxis, VirtualAxis};
pub use calibration::CalibrationManager;

View File

@ -1,273 +1,53 @@
//! CMDR Joystick 25 RP2040 main firmware
//!
//! Overview
//! - 4 gimbal axes (LX, LY, RX, RY) with smoothing, calibration and expo
//! - 2 virtual axes (RY/RZ) driven by buttons with direction compensation
//! - 5x5 button matrix + 2 extra buttons, with debounce and short/long press
//! - USB HID joystick: 7 axes, 32 buttons, 8way HAT
//! - EEPROMbacked calibration and gimbal mode (M10/M7)
//! - WS2812 status LED for state indication
//!
//! Modules
//! - hardware.rs: pins, clocks, timers, helpers
//! - axis.rs: gimbal/virtual axis processing and throttle hold
//! - button_matrix.rs + buttons.rs: scanning, debouncing, press types, special actions
//! - calibration.rs + storage.rs: runtime calibration and persistence
//! - usb_report.rs + usb_joystick_device.rs: HID descriptor and report generation
//! - status.rs: WS2812 driver and status model
//!
//! Modes: Normal, Calibration, Throttle Hold, Virtual Throttle, Bootloader
//!
//! Timing: scan 200 µs, process 1200 µs, USB 10 ms, LED 250 ms
//! Project: CMtec CMDR Joystick
//! Date: 2025-03-09
//! Author: Christoffer Martinsson
//! Email: cm@cmtec.se
//! License: Please refer to LICENSE in root directory
#![no_std]
#![no_main]
mod axis;
mod button_matrix;
mod buttons;
mod calibration;
mod expo;
mod hardware;
mod mapping;
mod status;
mod storage;
mod usb_joystick_device;
mod usb_report;
use axis::AxisManager;
use button_matrix::ButtonMatrix;
use buttons::{ButtonManager, SpecialAction};
use calibration::CalibrationManager;
use core::convert::Infallible;
use core::panic::PanicInfo;
use cortex_m::delay::Delay;
use eeprom24x::{Eeprom24x, SlaveAddr};
use embedded_hal::digital::{InputPin, OutputPin};
use cmdr_joystick::buttons::SpecialAction;
use cmdr_joystick::hardware::{self, timers};
use cmdr_joystick::status::StatusMode;
use cmdr_joystick::usb_joystick_device::JoystickConfig;
use cmdr_joystick::{bootloader, Board, BoardParts, JoystickState};
use embedded_hal_0_2::adc::OneShot;
use embedded_hal_0_2::timer::CountDown;
use fugit::ExtU32;
use hardware::timers;
use mapping::*;
use rp2040_hal::{
adc::Adc,
adc::AdcPin,
clocks::{init_clocks_and_plls, Clock},
gpio::Pins,
i2c::I2C,
pac,
pio::PIOExt,
timer::Timer,
watchdog::Watchdog,
Sio,
};
use status::{StatusLed, StatusMode, SystemState};
use usb_device::class_prelude::*;
use panic_halt as _;
use usb_device::prelude::*;
use usb_device::device::UsbDeviceState;
use usb_joystick_device::JoystickConfig;
use usb_report::get_joystick_report;
use usbd_human_interface_device::prelude::*;
use usbd_human_interface_device::prelude::{UsbHidClassBuilder, UsbHidError};
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
/// Boot loader configuration for RP2040 ROM.
///
/// The linker places this boot block at the start of our program image to help the ROM
/// bootloader initialize our code. This specific boot loader supports W25Q080 flash memory.
// Embed the boot2 image for the W25Q080 flash; required for RP2040 to boot from external flash.
#[link_section = ".boot2"]
#[no_mangle]
#[used]
pub static BOOT2_FIRMWARE: [u8; 256] = rp2040_boot2::BOOT_LOADER_W25Q080;
use expo::ExpoLUT;
/// Hardware configuration imports from the hardware abstraction layer.
use hardware::{ADC_MAX, ADC_MIN};
use hardware::{BUTTON_COLS, BUTTON_ROWS, NUMBER_OF_BUTTONS};
/// Additional hardware constants for button debouncing.
use hardware::DEBOUNCE;
#[cfg(not(test))]
#[rp2040_hal::entry]
fn main() -> ! {
// Hardware initialization and peripheral setup for joystick operation
// Acquire exclusive access to RP2040 peripherals
let mut pac = pac::Peripherals::take().unwrap();
// Initialize watchdog timer (required for clock configuration)
let mut watchdog = Watchdog::new(pac.WATCHDOG);
// Configure system clocks and phase-locked loops for stable operation
let clocks = init_clocks_and_plls(
hardware::XTAL_FREQ_HZ,
pac.XOSC,
pac.CLOCKS,
pac.PLL_SYS,
pac.PLL_USB,
&mut pac.RESETS,
&mut watchdog,
)
.ok()
.unwrap();
let core = pac::CorePeripherals::take().unwrap();
// Initialize SIO (Single-cycle I/O) for high-performance GPIO operations
let sio = Sio::new(pac.SIO);
// Configure GPIO pins to their default operational state
let pins = Pins::new(
pac.IO_BANK0,
pac.PADS_BANK0,
sio.gpio_bank0,
&mut pac.RESETS,
);
let i2c = I2C::i2c1(
pac.I2C1,
get_pin!(pins, i2c_sda).reconfigure(), // sda
get_pin!(pins, i2c_scl).reconfigure(), // scl
hardware::i2c::i2c_frequency(),
&mut pac.RESETS,
hardware::i2c::system_clock(),
);
let i2c_address = SlaveAddr::Alternative(false, false, false);
let mut eeprom = Eeprom24x::new_24x32(i2c, i2c_address);
// ADC configuration: prepare 12-bit ADC channels for all four gimbal axes
// Initialize 12-bit ADC with 4 channels for gimbal axes
let mut adc = Adc::new(pac.ADC, &mut pac.RESETS);
// Configure ADC input pins for 4-axis gimbal (Left X/Y, Right X/Y)
let mut adc_pin_left_x = AdcPin::new(get_pin!(pins, adc_left_x).into_floating_input()).unwrap();
let mut adc_pin_left_y = AdcPin::new(get_pin!(pins, adc_left_y).into_floating_input()).unwrap();
let mut adc_pin_right_x =
AdcPin::new(get_pin!(pins, adc_right_x).into_floating_input()).unwrap();
let mut adc_pin_right_y =
AdcPin::new(get_pin!(pins, adc_right_y).into_floating_input()).unwrap();
// # Button Matrix Configuration\n //\n // Configure the 5x5 button matrix using row/column scanning technique.\n // Rows are configured as pull-up inputs, columns as push-pull outputs.\n // This allows scanning 25 buttons with only 10 GPIO pins.\n\n // Configure button matrix row pins (inputs with pull-up resistors)
let button_matrix_row_pins: &mut [&mut dyn InputPin<Error = Infallible>; BUTTON_ROWS] = &mut [
&mut get_pin!(pins, button_row_0).into_pull_up_input(),
&mut get_pin!(pins, button_row_1).into_pull_up_input(),
&mut get_pin!(pins, button_row_2).into_pull_up_input(),
&mut get_pin!(pins, button_row_3).into_pull_up_input(),
&mut get_pin!(pins, button_row_4).into_pull_up_input(),
];
// Configure button matrix column pins (push-pull outputs for scanning)
let button_matrix_col_pins: &mut [&mut dyn OutputPin<Error = Infallible>; BUTTON_COLS] = &mut [
&mut get_pin!(pins, button_col_0).into_push_pull_output(),
&mut get_pin!(pins, button_col_1).into_push_pull_output(),
&mut get_pin!(pins, button_col_2).into_push_pull_output(),
&mut get_pin!(pins, button_col_3).into_push_pull_output(),
&mut get_pin!(pins, button_col_4).into_push_pull_output(),
];
// Initialize button matrix scanner with debouncing
let mut button_matrix: ButtonMatrix<BUTTON_ROWS, BUTTON_COLS, NUMBER_OF_BUTTONS> =
ButtonMatrix::new(button_matrix_row_pins, button_matrix_col_pins, DEBOUNCE);
// Configure matrix pins for scanning operation
button_matrix.init_pins();
// Configure additional buttons outside the matrix (total: 27 buttons)
let mut left_extra_button = get_pin!(pins, left_extra_button).into_pull_up_input();
let mut right_extra_button = get_pin!(pins, right_extra_button).into_pull_up_input();
// Status LED initialization: WS2812 via PIO for runtime status indication
// Initialize WS2812 status LED using PIO state machine
let (mut pio, sm0, _, _, _) = pac.PIO0.split(&mut pac.RESETS);
let mut status_led = StatusLed::new(
get_pin!(pins, status_led).into_function(),
&mut pio,
sm0,
clocks.peripheral_clock.freq(),
);
// Initial LED state (red) indicates system initialization
status_led.update(StatusMode::Error);
let mut delay = Delay::new(core.SYST, clocks.system_clock.freq().to_Hz());
// Bootloader entry check: early matrix scan; hold frontleftlower to enter USB massstorage bootloader
// Scan button matrix multiple times to ensure stable debounced readings
for _ in 0..10 {
// Multiple scans ensure debounce algorithm captures stable button states
button_matrix.scan_matrix(&mut delay);
}
if button_matrix.buttons_pressed()[BUTTON_FRONT_LEFT_LOWER] {
status_led.update(StatusMode::Bootloader);
let gpio_activity_pin_mask: u32 = 0;
let disable_interface_mask: u32 = 0;
rp2040_hal::rom_data::reset_to_usb_boot(gpio_activity_pin_mask, disable_interface_mask);
}
// Timer configuration: cadence for LED updates, scans, processing and USB
// Initialize hardware timer peripheral
let timer = Timer::new(pac.TIMER, &mut pac.RESETS, &clocks);
let mut status_led_count_down = timer.count_down();
status_led_count_down.start(timers::STATUS_LED_INTERVAL_MS.millis());
// Removed unused millisecond countdown timer
let mut scan_count_down = timer.count_down();
scan_count_down.start(timers::SCAN_INTERVAL_US.micros());
let mut usb_update_count_down = timer.count_down();
usb_update_count_down.start(timers::USB_UPDATE_INTERVAL_MS.millis());
let mut usb_activity: bool = false;
let mut usb_active: bool = false;
let mut usb_initialized: bool = false;
let mut usb_suspended: bool = false;
let mut usb_send_pending: bool = false;
let mut vt_enable: bool = false;
let mut idle_mode: bool = false;
let mut usb_activity_timeout_count: u32 = 0;
let mut wake_on_input: bool = false;
let mut axis_manager = AxisManager::new();
let mut button_manager = ButtonManager::new();
let mut calibration_manager = CalibrationManager::new();
// Signal processing: expo LUTs and smoothing filters
// Create exponential curve lookup tables (avoids floating-point math in real-time)
let expo_lut = ExpoLUT::new(0.3);
let expo_lut_virtual = ExpoLUT::new(0.6);
// Initialize digital smoothing filters for each gimbal axis
let mut smoother = AxisManager::create_smoothers();
// USB HID configuration (fullspeed joystick class)
// Initialize USB bus allocator for RP2040
let usb_bus = UsbBusAllocator::new(rp2040_hal::usb::UsbBus::new(
pac.USBCTRL_REGS,
pac.USBCTRL_DPRAM,
clocks.usb_clock,
true,
&mut pac.RESETS,
));
// Firmware entry point initializes hardware before handing off to RTIC.
let BoardParts {
mut button_matrix,
mut status_led,
mut delay,
timer,
mut adc,
mut axis_pins,
mut left_extra_button,
mut right_extra_button,
mut eeprom,
usb_bus,
} = Board::new().into_parts();
// Build the HID joystick class on the shared USB bus.
let mut usb_hid_joystick = UsbHidClassBuilder::new()
.add_device(JoystickConfig::default())
.build(&usb_bus);
.build(usb_bus);
let mut usb_dev =
UsbDeviceBuilder::new(&usb_bus, UsbVidPid(hardware::USB_VID, hardware::USB_PID))
UsbDeviceBuilder::new(usb_bus, UsbVidPid(hardware::USB_VID, hardware::USB_PID))
.strings(&[StringDescriptors::default()
.manufacturer(hardware::usb::MANUFACTURER)
.product(hardware::usb::PRODUCT)
@ -275,285 +55,141 @@ fn main() -> ! {
.unwrap()
.build();
// Calibration data initialization: load axis calibration and gimbal mode from EEPROM
let mut state = JoystickState::new();
status_led.update(StatusMode::Error);
// Load calibration data from EEPROM using CalibrationManager
button_matrix.prime(&mut delay, 10);
let initial_pressed = button_matrix.buttons_pressed();
if bootloader::startup_requested(&initial_pressed) {
bootloader::enter(&mut status_led);
}
{
// Load persisted calibration values from EEPROM if available.
let mut read_fn = |addr: u32| eeprom.read_byte(addr).map_err(|_| ());
CalibrationManager::load_axis_calibration(&mut axis_manager.axes, &mut read_fn);
let gimbal_mode = CalibrationManager::load_gimbal_mode(&mut read_fn);
axis_manager.set_gimbal_mode(gimbal_mode);
calibration_manager.set_gimbal_mode(gimbal_mode);
state.load_calibration(&mut read_fn);
}
// Set up periodic timers for scanning, status updates, and USB activity.
let mut scan_tick = timer.count_down();
scan_tick.start(timers::SCAN_INTERVAL_US.micros());
let mut status_tick = timer.count_down();
status_tick.start(timers::STATUS_LED_INTERVAL_MS.millis());
let mut usb_tick = timer.count_down();
usb_tick.start(timers::USB_UPDATE_INTERVAL_MS.millis());
let mut status_time_ms: u32 = 0;
let mut suspended_scan_counter: u8 = 0;
// Main control loop: service USB, process inputs, and emit reports.
loop {
// Main control loop: poll USB, scan inputs, process data, send reports
// Handle USB device polling and maintain connection state
// Service the USB stack and HID class when data is pending.
if usb_dev.poll(&mut [&mut usb_hid_joystick]) {
if !usb_initialized {
usb_initialized = true;
}
if !usb_active {
usb_activity = true; // Force initial report
idle_mode = false;
usb_activity_timeout_count = 0;
usb_send_pending = true;
}
usb_active = true;
state.usb_state().on_poll();
}
// Check USB device state for suspend/resume handling
let usb_state = usb_dev.state();
let was_suspended = usb_suspended;
usb_suspended = usb_state == UsbDeviceState::Suspend;
state.usb_state().on_suspend_change(usb_state);
// Handle USB resume transition
if was_suspended && !usb_suspended {
// Device was suspended and is now resumed
usb_activity = true;
idle_mode = false;
usb_activity_timeout_count = 0;
usb_send_pending = true;
wake_on_input = false;
// Periodically refresh the status LED animation.
if status_tick.wait().is_ok() {
status_time_ms = status_time_ms.saturating_add(timers::STATUS_LED_INTERVAL_MS);
status_led.update_from_system_state(state.system_state(), status_time_ms);
}
// Handle USB suspend transition
if !was_suspended && usb_suspended {
// Device has just been suspended - enter power saving mode
idle_mode = true;
usb_activity = false;
usb_send_pending = false;
wake_on_input = true;
// Reduce LED update frequency to save power when suspended
// LED will be off anyway (Suspended mode), so slow updates are fine
}
// Skip high-frequency scanning when suspended to save power
// Only scan periodically to detect wake-up inputs
let should_scan = if usb_suspended {
// When suspended, reduce scan frequency by factor of 10 (every ~2ms instead of 200μs)
static mut SUSPENDED_SCAN_COUNTER: u8 = 0;
unsafe {
SUSPENDED_SCAN_COUNTER = (SUSPENDED_SCAN_COUNTER + 1) % 10;
SUSPENDED_SCAN_COUNTER == 0
}
// Slow the scan cadence when USB is suspended to save power.
let should_scan = if state.usb_state().suspended {
suspended_scan_counter = suspended_scan_counter.wrapping_add(1) % 10;
suspended_scan_counter == 0
} else {
suspended_scan_counter = 0;
true
};
if scan_count_down.wait().is_ok() && should_scan {
// ## High-Frequency Input Sampling (~5 kHz)
//
// Sample all inputs at high frequency for responsive control:
// - Button matrix scanning with debouncing
// - ADC reading from all 4 gimbal axes
// - Digital filtering for noise reduction
// Scan 5x5 button matrix for input changes
if should_scan && scan_tick.wait().is_ok() {
// Scan buttons, read analog axes, and update state machines.
button_matrix.scan_matrix(&mut delay);
// Read raw 12-bit ADC values from all 4 gimbal potentiometers
let mut raw_values = [
adc.read(&mut adc_pin_left_x).unwrap(),
adc.read(&mut adc_pin_left_y).unwrap(),
adc.read(&mut adc_pin_right_x).unwrap(),
adc.read(&mut adc_pin_right_y).unwrap(),
adc.read(&mut axis_pins.left_x).unwrap(),
adc.read(&mut axis_pins.left_y).unwrap(),
adc.read(&mut axis_pins.right_x).unwrap(),
adc.read(&mut axis_pins.right_y).unwrap(),
];
state.tick_smoothers(&mut raw_values);
// Apply hardware-specific axis compensation (M10/M7 differences)
axis_manager.apply_gimbal_compensation(&mut raw_values);
// Apply digital smoothing filters to reduce ADC noise and jitter
axis_manager.update_smoothers(&mut smoother, &raw_values);
// ## Immediate Data Processing (formerly 1000 Hz)
//
// Process all input data right after sampling for minimal latency.
// Update button states from matrix scan and extra buttons
button_manager.update_from_matrix(&mut button_matrix);
button_manager.update_extra_buttons(&mut left_extra_button, &mut right_extra_button);
button_manager.filter_hat_switches();
// Process special button combinations for system control
let action = button_manager.check_special_combinations(
axis_manager.get_value_before_hold(),
calibration_manager.is_active(),
);
match action {
SpecialAction::Bootloader => {
status_led.update(StatusMode::Bootloader);
let gpio_activity_pin_mask: u32 = 0;
let disable_interface_mask: u32 = 0;
rp2040_hal::rom_data::reset_to_usb_boot(
gpio_activity_pin_mask,
disable_interface_mask,
state.update_button_states(
&mut button_matrix,
&mut left_extra_button,
&mut right_extra_button,
);
// Evaluate special button combinations (bootloader, calibration, etc.).
let action = state.check_special_action();
if matches!(action, SpecialAction::Bootloader) {
if !state.usb_state().suspended {
let clear_report = JoystickState::empty_report();
for _ in 0..3 {
match usb_hid_joystick.device().write_report(&clear_report) {
Ok(_) => break,
Err(UsbHidError::WouldBlock) => {
let _ = usb_hid_joystick.tick();
}
SpecialAction::StartCalibration => {
for (index, item) in axis_manager.axes.iter_mut().enumerate() {
item.center = smoother[index].value() as u16;
item.min = item.center;
item.max = item.center;
}
axis_manager.clear_throttle_hold(); // Clear throttle hold when cancelling calibration
calibration_manager.start_calibration();
}
SpecialAction::CancelCalibration => {
calibration_manager.stop_calibration();
}
SpecialAction::ThrottleHold(hold_value) => {
axis_manager.set_throttle_hold(hold_value);
}
SpecialAction::VirtualThrottleToggle => {
vt_enable = !vt_enable;
}
SpecialAction::CalibrationSetModeM10 => {
// Set gimbal mode to M10 and reset calibration
if calibration_manager.set_gimbal_mode_m10(&mut axis_manager.axes, &smoother) {
axis_manager.set_gimbal_mode(calibration_manager.get_gimbal_mode());
axis_manager.clear_throttle_hold(); // Clear holds after mode change
Err(_) => break,
}
}
SpecialAction::CalibrationSetModeM7 => {
// Set gimbal mode to M7 and reset calibration
if calibration_manager.set_gimbal_mode_m7(&mut axis_manager.axes, &smoother) {
axis_manager.set_gimbal_mode(calibration_manager.get_gimbal_mode());
axis_manager.clear_throttle_hold(); // Clear holds after mode change
}
}
SpecialAction::CalibrationSave => {
// Save calibration data and end calibration mode
calibration_manager
.save_calibration(&axis_manager.axes, &mut |page: u32, data: &[u8]| {
eeprom.write_page(page, data).map_err(|_| ())
});
}
SpecialAction::None => {}
bootloader::enter(&mut status_led);
} else if !matches!(action, SpecialAction::None) {
let mut write_page =
|page: u32, data: &[u8]| eeprom.write_page(page, data).map_err(|_| ());
state.handle_special_action(action, &mut write_page);
}
// Always update calibration for dynamic min/max tracking when active
calibration_manager.update_dynamic_calibration(&mut axis_manager.axes, &smoother);
// Track calibration extrema and process axis/virtual/button logic.
state.update_calibration_tracking();
// Process gimbal axes through calibration, expo curves, and scaling
if axis_manager.process_axis_values(&smoother, &expo_lut) {
usb_activity = true;
usb_activity_timeout_count = 0; // Reset timeout on real input activity
idle_mode = false;
usb_send_pending = true;
if state.process_axes() {
state.usb_state().handle_input_activity();
}
// Wake from USB suspend if input detected
if wake_on_input && usb_suspended {
// TODO: Implement remote wakeup if supported by host
wake_on_input = false;
if state.update_virtual_axes() {
state.usb_state().handle_input_activity();
}
if state.finalize_button_logic(&timer) {
state.usb_state().handle_input_activity();
}
}
// Update virtual axes based on front button states
if axis_manager.update_virtual_axes(button_manager.buttons(), vt_enable) {
usb_activity = true;
usb_activity_timeout_count = 0; // Reset timeout on real input activity
idle_mode = false;
usb_send_pending = true;
// Wake from USB suspend if input detected
if wake_on_input && usb_suspended {
// TODO: Implement remote wakeup if supported by host
wake_on_input = false;
}
// Advance USB idle timers and decide when to send reports.
let usb_tick_elapsed = usb_tick.wait().is_ok();
if usb_tick_elapsed {
state
.usb_state()
.advance_idle_timer(timers::USB_UPDATE_INTERVAL_MS);
}
// Process button logic (press types, timing, USB mapping)
if button_manager.process_button_logic_with_timer(&timer) {
usb_activity = true;
usb_activity_timeout_count = 0; // Reset timeout on real input activity
idle_mode = false;
usb_send_pending = true;
// Wake from USB suspend if input detected
if wake_on_input && usb_suspended {
// TODO: Implement remote wakeup if supported by host
wake_on_input = false;
}
}
}
if status_led_count_down.wait().is_ok() {
// ## Status LED Updates (100Hz)
//
// Update status LED to reflect current system state:
// - Green: Normal operation with USB connection
// - Blue: Calibration mode active
// - Yellow: Throttle hold or Virtual Throttle enabled
// - Red: Error state or disconnected
// - Purple: Bootloader mode
let system_state = SystemState {
usb_active,
usb_initialized,
usb_suspended,
idle_mode,
calibration_active: calibration_manager.is_active(),
throttle_hold_enable: axis_manager.throttle_hold_enable,
vt_enable,
};
status_led.update_from_system_state(
system_state,
(timer.get_counter().ticks() / 1000) as u32,
);
}
// ## USB HID Report Transmission (up to 1 kHz)
//
// Transmit USB HID reports only when there is input activity.
// This power-management approach prevents the computer from staying
// awake unnecessarily while maintaining responsive control.
//
// The report includes:
// - All 7 analog axes with proper scaling
// - 32-button bitmask with USB mapping
// - 8-direction HAT switch state
// - Virtual throttle mode handling
// Only transmit USB reports when input activity is detected and not suspended
let usb_tick = usb_update_count_down.wait().is_ok();
if usb_activity && (usb_tick || usb_send_pending) && !usb_suspended {
let mut send_report = || {
let virtual_ry_value = axis_manager.get_virtual_ry_value(&expo_lut_virtual);
let virtual_rz_value = axis_manager.get_virtual_rz_value(&expo_lut_virtual);
match usb_hid_joystick.device().write_report(&get_joystick_report(
button_manager.buttons_mut(),
&mut axis_manager.axes,
virtual_ry_value,
virtual_rz_value,
&vt_enable,
)) {
// Emit a new HID report when activity is pending and USB is ready.
if state.usb_state().activity
&& (usb_tick_elapsed || state.usb_state().send_pending)
&& !state.usb_state().suspended
{
let report = state.build_report();
match usb_hid_joystick.device().write_report(&report) {
Err(UsbHidError::WouldBlock) => {}
Ok(_) => {
usb_send_pending = false;
state.usb_state().acknowledge_report();
}
Err(e) => {
Err(error) => {
status_led.update(StatusMode::Error);
core::panic!("Failed to write joystick report: {:?}", e);
panic!("Failed to write joystick report: {:?}", error);
}
}
};
if usb_tick {
usb_activity_timeout_count += timers::USB_UPDATE_INTERVAL_MS;
if usb_activity_timeout_count >= timers::USB_ACTIVITY_TIMEOUT_MS {
usb_activity = false;
usb_activity_timeout_count = 0;
idle_mode = true;
usb_send_pending = false;
} else {
send_report();
}
} else {
send_report();
}
} else if usb_tick && usb_active && !usb_suspended {
// Only update idle mode for non-suspended devices
idle_mode = true;
} else if usb_tick_elapsed && state.usb_state().active && !state.usb_state().suspended {
state.usb_state().idle_mode = true;
}
}
}

View File

@ -1,4 +1,4 @@
//! Button-to-USB mapping for CMDR Joystick 25
//! Button-to-USB mapping for CMDR Joystick
//!
//! This module defines the hardware button indices (matrix layout and extras)
//! and their mapping to USB joystick button numbers and HAT directions.
@ -171,6 +171,7 @@ mod tests {
#[test]
fn front_buttons_have_expected_mappings() {
// Front panel buttons map to the expected USB button ids.
let mut buttons = [Button::default(); TOTAL_BUTTONS];
configure_button_mappings(&mut buttons);
@ -181,6 +182,7 @@ mod tests {
#[test]
fn long_press_flags_set_correctly() {
// Long-press flags are configured for buttons that need them at runtime.
let mut buttons = [Button::default(); TOTAL_BUTTONS];
configure_button_mappings(&mut buttons);
@ -192,11 +194,15 @@ mod tests {
#[test]
fn hat_buttons_map_to_expected_ids() {
// Hat direction buttons should map to the numerical HID hat constants.
let mut buttons = [Button::default(); TOTAL_BUTTONS];
configure_button_mappings(&mut buttons);
assert_eq!(buttons[BUTTON_TOP_RIGHT_HAT_UP].usb_button, USB_HAT_UP);
assert_eq!(buttons[BUTTON_TOP_RIGHT_HAT_RIGHT].usb_button, USB_HAT_RIGHT);
assert_eq!(
buttons[BUTTON_TOP_RIGHT_HAT_RIGHT].usb_button,
USB_HAT_RIGHT
);
assert_eq!(buttons[BUTTON_TOP_RIGHT_HAT_DOWN].usb_button, USB_HAT_DOWN);
assert_eq!(buttons[BUTTON_TOP_RIGHT_HAT_LEFT].usb_button, USB_HAT_LEFT);
}

View File

@ -1,4 +1,4 @@
//! WS2812 status LED driver for CMDR Joystick 25
//! WS2812 status LED driver for CMDR Joystick
//!
//! Provides a small helper around `ws2812_pio` to display firmware state on a
//! single WS2812 RGB LED using RP2040 PIO. The driver offers simple modes
@ -64,9 +64,11 @@ struct ModeDescriptor {
const HEARTBEAT_POWER_MS: u32 = 800;
const HEARTBEAT_IDLE_MS: u32 = 3200;
const HEARTBEAT_PAUSE_MS: u32 = 3000;
impl LedEffect {
fn update_interval_ms(self) -> u32 {
// Resolve the LED descriptor for the requested status mode.
match self {
LedEffect::Solid => 0,
LedEffect::Blink { period_ms } => period_ms / 2,
@ -75,6 +77,7 @@ impl LedEffect {
}
fn color_for(self, base: RGB8, elapsed_ms: u32) -> RGB8 {
// Compute the base status mode given system state flags.
match self {
LedEffect::Solid => base,
LedEffect::Blink { period_ms } => {
@ -90,7 +93,12 @@ impl LedEffect {
}
LedEffect::Heartbeat { period_ms } => {
let period = period_ms.max(1);
let phase = elapsed_ms % period;
let cycle = period.saturating_add(HEARTBEAT_PAUSE_MS);
let phase = elapsed_ms % cycle;
if phase >= period {
return COLOR_OFF;
}
let half = (period / 2).max(1);
let ramp = if phase < half {
((phase * 255) / half) as u8
@ -197,15 +205,99 @@ const fn descriptor_for(mode: StatusMode, base_mode: StatusMode) -> ModeDescript
}
fn scale_color(base: RGB8, brightness: u8) -> RGB8 {
let scale = brightness as u16;
if brightness == 0 {
return COLOR_OFF;
}
let components = [base.r, base.g, base.b];
let mut scaled = [0u8; 3];
let mut remainders = [0u16; 3];
let mut total_floor: u16 = 0;
let mut total_base: u16 = 0;
let brightness_u16 = brightness as u16;
for (index, &component) in components.iter().enumerate() {
total_base += component as u16;
if component == 0 {
continue;
}
let value = component as u32 * brightness as u32;
let div = (value / 255) as u16;
let rem = (value % 255) as u16;
scaled[index] = div as u8;
remainders[index] = rem;
total_floor += div;
}
if total_base == 0 {
return COLOR_OFF;
}
let mut target_total = ((total_base as u32 * brightness_u16 as u32) + 127) / 255;
if target_total > total_base as u32 {
target_total = total_base as u32;
}
let mut extra = target_total.saturating_sub(total_floor as u32) as u16;
while extra > 0 {
let mut best_index: Option<usize> = None;
let mut best_remainder = 0u16;
for idx in 0..components.len() {
if components[idx] == 0 {
continue;
}
if remainders[idx] == 0 {
continue;
}
if scaled[idx] as u16 >= components[idx] as u16 {
continue;
}
if remainders[idx] > best_remainder {
best_remainder = remainders[idx];
best_index = Some(idx);
}
}
let Some(idx) = best_index else {
break;
};
scaled[idx] += 1;
remainders[idx] = 0;
extra -= 1;
}
if extra > 0 {
for idx in 0..components.len() {
if extra == 0 {
break;
}
if components[idx] == 0 {
continue;
}
if scaled[idx] as u16 >= components[idx] as u16 {
continue;
}
scaled[idx] += 1;
extra -= 1;
}
}
RGB8 {
r: ((base.r as u16 * scale) / 255) as u8,
g: ((base.g as u16 * scale) / 255) as u8,
b: ((base.b as u16 * scale) / 255) as u8,
r: scaled[0],
g: scaled[1],
b: scaled[2],
}
}
fn determine_base_mode(system_state: SystemState) -> StatusMode {
// Compute the base status mode based on USB/calibration/idle state.
if system_state.usb_suspended {
StatusMode::Suspended
} else if system_state.calibration_active {
@ -331,6 +423,7 @@ where
/// Write a single color to the LED.
fn write_color(&mut self, color: RGB8) {
// Push the color to the WS2812 LED, ignoring transient IO errors.
let _ = self.ws2812_direct.write([color].iter().copied());
}
}
@ -354,6 +447,7 @@ mod tests {
#[test]
fn idle_mode_uses_base_color_with_heartbeat() {
// Idle state should inherit the base color while enforcing a heartbeat pattern.
let state = SystemState {
usb_active: false,
usb_initialized: true,
@ -376,6 +470,7 @@ mod tests {
#[test]
fn power_mode_uses_fast_heartbeat() {
// Power mode descriptor should use the fast heartbeat cadence and green.
let descriptor = descriptor_for(StatusMode::Power, StatusMode::Normal);
if let LedEffect::Heartbeat { period_ms } = descriptor.effect {
assert_eq!(period_ms, HEARTBEAT_POWER_MS);
@ -387,6 +482,7 @@ mod tests {
#[test]
fn calibration_has_priority_over_idle() {
// Calibration activity should override idle when both flags are set.
let state = SystemState {
usb_active: true,
usb_initialized: true,
@ -403,6 +499,7 @@ mod tests {
#[test]
fn heartbeat_effect_fades() {
// Heartbeat should ramp up and down around the target color.
let base = StatusMode::Normal;
let descriptor = descriptor_for(StatusMode::Idle, base);
let LedEffect::Heartbeat { period_ms } = descriptor.effect else {
@ -420,8 +517,30 @@ mod tests {
assert_eq!(end.g, 0);
}
#[test]
fn heartbeat_pause_keeps_led_off() {
// Added pause should hold the LED dark between breaths while allowing a clean restart.
let base = StatusMode::Normal;
let descriptor = descriptor_for(StatusMode::Idle, base);
let LedEffect::Heartbeat { period_ms } = descriptor.effect else {
panic!("Idle should use heartbeat effect");
};
let pause_sample = descriptor
.effect
.color_for(descriptor.color, period_ms + HEARTBEAT_PAUSE_MS / 2);
assert_eq!(pause_sample, COLOR_OFF);
let cycle = period_ms + HEARTBEAT_PAUSE_MS;
let restart = descriptor
.effect
.color_for(descriptor.color, cycle + period_ms / 4);
assert!(restart.g > 0);
}
#[test]
fn blink_effect_toggles() {
// Blink descriptor should alternate between the color and off state.
let descriptor = descriptor_for(StatusMode::NormalFlash, StatusMode::NormalFlash);
let LedEffect::Blink { period_ms } = descriptor.effect else {
panic!("NormalFlash should use blink effect");
@ -433,8 +552,30 @@ mod tests {
assert_eq!(off, COLOR_OFF);
}
#[test]
fn low_brightness_preserves_color_mix() {
// Low brightness scaling must not drop any non-zero channel from the blend.
let base = COLOR_ORANGE;
let dimmed = scale_color(base, 50);
assert!(dimmed.r > 0);
assert!(dimmed.g > 0);
assert_eq!(dimmed.b, 0);
assert!(dimmed.r <= base.r);
assert!(dimmed.g <= base.g);
assert_eq!(dimmed.r as u16 * base.g as u16, dimmed.g as u16 * base.r as u16);
}
#[test]
fn zero_brightness_turns_off_led() {
// Zero brightness should fully blank the LED regardless of the base color.
let base = COLOR_BLUE;
let off = scale_color(base, 0);
assert_eq!(off, COLOR_OFF);
}
#[test]
fn determine_base_mode_before_usb() {
// Before USB comes up the controller should stay in Power mode.
let state = SystemState {
usb_active: false,
usb_initialized: false,
@ -450,6 +591,7 @@ mod tests {
#[test]
fn usb_suspend_takes_priority() {
// USB suspend should trump other status priorities.
let state = SystemState {
usb_active: true,
usb_initialized: true,

View File

@ -1,4 +1,4 @@
//! EEPROM storage for CMDR Joystick 25
//! EEPROM storage for CMDR Joystick
//!
//! Provides helpers to read/write peraxis calibration and gimbal mode to an
//! external 24xseries EEPROM. The API is closurebased to allow testing on
@ -79,6 +79,7 @@ pub fn write_calibration_data(
/// Read a u16 value from EEPROM in littleendian (low then high byte) format.
fn read_u16_with_closure(
// Fetch a little-endian u16 by reading two consecutive EEPROM bytes.
read_byte_fn: &mut dyn FnMut(u32) -> Result<u8, ()>,
low_addr: u32,
high_addr: u32,
@ -171,6 +172,7 @@ mod tests {
#[test]
fn test_boundary_values() {
// Pack axis tuples into EEPROM layout and write the gimbal mode byte.
let mut buffer = [0u8; 4];
// Test minimum value (manual packing)

View File

@ -37,6 +37,7 @@ pub trait Try {
type Ok;
type Error;
fn into_result(self) -> Result<Self::Ok, Self::Error>;
// Trait shim replicating `core::Try` for use in no_std contexts.
}
impl<T> Try for Option<T> {
@ -45,6 +46,7 @@ impl<T> Try for Option<T> {
#[inline]
fn into_result(self) -> Result<T, NoneError> {
// Convert an optional value into a result with a unit error.
self.ok_or(NoneError)
}
}
@ -55,6 +57,7 @@ impl<T, E> Try for Result<T, E> {
#[inline]
fn into_result(self) -> Self {
// `Result` already matches the desired signature; return it untouched.
self
}
}
@ -165,12 +168,16 @@ impl<'a, B: UsbBus> DeviceClass<'a> for Joystick<'a, B> {
type I = Interface<'a, B, InBytes32, OutNone, ReportSingle>;
fn interface(&mut self) -> &mut Self::I {
// Expose the HID interface so the USB stack can enqueue reports.
&mut self.interface
}
fn reset(&mut self) {}
fn reset(&mut self) {
// Nothing to reset for this simple HID device.
}
fn tick(&mut self) -> Result<(), UsbHidError> {
// Flush pending HID data and poll the USB stack for new requests.
Ok(())
}
}
@ -181,6 +188,7 @@ pub struct JoystickConfig<'a> {
impl Default for JoystickConfig<'_> {
fn default() -> Self {
// Construct the HID interface with the default joystick descriptor and endpoints.
Self::new(
unwrap!(unwrap!(InterfaceBuilder::new(JOYSTICK_DESCRIPTOR))
.boot_device(InterfaceProtocol::None)
@ -203,6 +211,7 @@ impl<'a, B: UsbBus + 'a> UsbAllocatable<'a, B> for JoystickConfig<'a> {
type Allocated = Joystick<'a, B>;
fn allocate(self, usb_alloc: &'a UsbBusAllocator<B>) -> Self::Allocated {
// Allocate the HID interface using the provided USB bus allocator.
Self::Allocated {
interface: Interface::new(usb_alloc, self.interface),
}

View File

@ -1,4 +1,4 @@
//! USB HID report generation for CMDR Joystick 25
//! USB HID report generation for CMDR Joystick
//!
//! Converts processed axis values and button states into a `JoystickReport`
//! that matches the HID descriptor defined in `usb_joystick_device.rs`.
@ -184,6 +184,7 @@ mod tests {
#[test]
fn test_joystick_report_basic_axes() {
// Remap helper scales values between integer ranges with clamping.
let mut buttons = [Button::default(); TOTAL_BUTTONS];
let mut axes = [GimbalAxis::new(); 4];
@ -214,6 +215,7 @@ mod tests {
#[test]
fn test_virtual_throttle_mode() {
// Slider combines throttle hold and virtual throttle for report serialization.
let mut buttons = [Button::default(); TOTAL_BUTTONS];
let mut axes = [GimbalAxis::new(); 4];
@ -237,6 +239,7 @@ mod tests {
#[test]
fn test_virtual_throttle_below_center() {
// When VT mode is enabled below center, slider should invert along the left half of travel.
let mut buttons = [Button::default(); TOTAL_BUTTONS];
let mut axes = [GimbalAxis::new(); 4];
@ -261,6 +264,7 @@ mod tests {
#[test]
fn test_button_mapping_regular_buttons() {
// Regular buttons should set their HID bitmask without disturbing hat state.
let mut buttons = [Button::default(); TOTAL_BUTTONS];
let mut axes = [GimbalAxis::new(); 4];
@ -285,6 +289,7 @@ mod tests {
#[test]
fn test_hat_switch_mapping() {
// A single hat direction should map to the appropriate HID hat value.
let mut buttons = [Button::default(); TOTAL_BUTTONS];
let mut axes = [GimbalAxis::new(); 4];
@ -307,6 +312,7 @@ mod tests {
#[test]
fn test_long_press_button_handling() {
// Buttons configured for long press should surface the alternate USB id.
let mut buttons = [Button::default(); TOTAL_BUTTONS];
let mut axes = [GimbalAxis::new(); 4];
@ -327,6 +333,7 @@ mod tests {
#[test]
fn test_usb_changed_flag_reset() {
// Packaging a report should clear the usb_changed flags once consumed.
let mut buttons = [Button::default(); TOTAL_BUTTONS];
let mut axes = [GimbalAxis::new(); 4];
@ -347,6 +354,7 @@ mod tests {
#[test]
fn test_edge_case_hat_values() {
// Additional hat directions should map to the correct encoded value.
let mut buttons = [Button::default(); TOTAL_BUTTONS];
let mut axes = [GimbalAxis::new(); 4];
@ -368,6 +376,7 @@ mod tests {
#[test]
fn test_multiple_buttons_and_hat() {
// Report should accommodate simultaneous button presses and hat direction.
let mut buttons = [Button::default(); TOTAL_BUTTONS];
let mut axes = [GimbalAxis::new(); 4];

110
tools/copy_uf2.py Executable file
View File

@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""Copy a UF2 artifact to a detected RP2040 mass-storage mount."""
from __future__ import annotations
import argparse
import os
import shutil
import sys
import time
from pathlib import Path
INFO_FILE = "INFO_UF2.TXT"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--source", type=Path, required=True, help="Path to the UF2 file to copy")
parser.add_argument("--timeout", type=float, default=10.0, help="Seconds to wait for the mount")
parser.add_argument(
"--mount",
type=str,
default=os.environ.get("MOUNT", ""),
help="Explicit mount point (default: auto-detect)",
)
return parser.parse_args()
def candidate_paths(explicit: str, user: str) -> list[Path]:
paths: list[Path] = []
if explicit:
paths.append(Path(explicit))
roots = [
Path("/Volumes"),
Path("/media"),
Path(f"/media/{user}"),
Path("/run/media"),
Path(f"/run/media/{user}"),
]
for root in roots:
if not root.exists() or not root.is_dir():
continue
for child in root.iterdir():
if child.is_dir():
paths.append(child)
return paths
def choose_mount(explicit: str, user: str) -> Path | None:
candidates = candidate_paths(explicit, user)
if explicit:
path = Path(explicit)
return path if path.exists() and path.is_dir() else None
info_candidates = [path for path in candidates if (path / INFO_FILE).exists()]
if info_candidates:
return info_candidates[0]
for path in candidates:
if path.exists() and path.is_dir():
return path
return None
def main() -> int:
args = parse_args()
source = args.source
if not source.exists():
print(f"UF2 source file not found: {source}", file=sys.stderr)
return 1
explicit_mount = args.mount.strip()
user = os.environ.get("USER", "")
deadline = time.time() + float(args.timeout)
while time.time() <= deadline:
mount = choose_mount(explicit_mount, user)
if mount is not None:
if not mount.exists() or not mount.is_dir():
time.sleep(1)
continue
destination = mount / source.name
try:
shutil.copy2(source, destination)
try:
if hasattr(os, "sync"):
os.sync()
except Exception:
pass
time.sleep(0.5)
except Exception as exc: # noqa: BLE001
print(f"Failed to copy UF2 to {destination}: {exc}", file=sys.stderr)
return 1
print(f"Copied {source} to {destination}")
return 0
time.sleep(1)
if explicit_mount:
print(
f"Mount point '{explicit_mount}' not found within {args.timeout} seconds",
file=sys.stderr,
)
else:
print(
"Unable to detect RP2040 UF2 mount. Pass one via mount=/path",
file=sys.stderr,
)
return 1
if __name__ == "__main__": # pragma: no cover
sys.exit(main())