diff --git a/cad/assembly.scad b/cad/assembly.scad new file mode 100644 index 0000000..2131cc5 --- /dev/null +++ b/cad/assembly.scad @@ -0,0 +1,118 @@ +// ============================================ +// SaltyLab — Full Assembly Visualization +// Shows all parts in position on 2020 spine +// ============================================ +include + +// Spine height +spine_h = 500; + +// Component heights (center of each mount on spine) +h_motor = 0; +h_battery = 50; +h_esc = 100; +h_fc = 170; +h_jetson = 250; +h_realsense = 350; +h_lidar = 430; + +// Colors for visualization +module spine() { + color("silver") + translate([-extrusion_w/2, -extrusion_w/2, 0]) + cube([extrusion_w, extrusion_w, spine_h]); +} + +module wheel(side) { + color("DimGray") + translate([side * 140, 0, 0]) + rotate([0, 90, 0]) + cylinder(d=200, h=50, center=true, $fn=60); +} + +// --- Assembly --- + +// Spine +spine(); + +// Wheels +wheel(-1); +wheel(1); + +// Motor mount plate (at base) +color("DodgerBlue", 0.7) + translate([0, 0, h_motor]) + import("motor_mount_plate.stl"); + +// Battery shelf +color("OrangeRed", 0.7) + translate([0, 0, h_battery]) + rotate([0, 0, 0]) + cube([180, 80, 40], center=true); + +// ESC +color("Green", 0.7) + translate([0, 0, h_esc]) + cube([80, 50, 15], center=true); + +// FC (tiny!) +color("Purple", 0.9) + translate([0, 0, h_fc]) + cube([36, 36, 5], center=true); + +// Jetson Nano +color("LimeGreen", 0.7) + translate([0, 0, h_jetson]) + cube([100, 80, 29], center=true); + +// RealSense D435i +color("Gray", 0.8) + translate([0, -40, h_realsense]) + cube([90, 25, 25], center=true); + +// RPLIDAR A1 +color("Cyan", 0.7) + translate([0, 0, h_lidar]) + cylinder(d=70, h=41, center=true, $fn=40); + +// Kill switch (accessible on front) +color("Red") + translate([0, -60, h_esc + 30]) + cylinder(d=22, h=10, $fn=30); + +// LED ring +color("White", 0.3) + translate([0, 0, h_jetson - 20]) + difference() { + cylinder(d=120, h=15, $fn=60); + translate([0, 0, -1]) + cylinder(d=110, h=17, $fn=60); + } + +// Bumpers +color("Orange", 0.5) { + translate([0, -75, 25]) + cube([350, 30, 50], center=true); + translate([0, 75, 25]) + cube([350, 30, 50], center=true); +} + +// Handle (top) +color("Yellow", 0.7) + translate([0, 0, spine_h + 10]) + cube([100, 20, 25], center=true); + +// Tether point +color("Red", 0.8) + translate([0, 0, spine_h - 20]) { + difference() { + cylinder(d=30, h=8, $fn=30); + translate([0, 0, -1]) + cylinder(d=15, h=10, $fn=30); + } + } + +echo("=== SaltyLab Assembly ==="); +echo(str("Total height: ", spine_h + 30, "mm")); +echo(str("Width (axle-axle): ", 280 + 50*2, "mm")); +echo(str("Depth: ~", 150, "mm")); diff --git a/cad/battery_shelf.scad b/cad/battery_shelf.scad new file mode 100644 index 0000000..0b5a5a4 --- /dev/null +++ b/cad/battery_shelf.scad @@ -0,0 +1,77 @@ +// ============================================ +// SaltyLab — Battery Shelf +// 200×100×40mm PETG +// Holds 36V battery pack low on the frame +// Mounts to 2020 extrusion spine +// ============================================ +include + +shelf_w = 200; +shelf_d = 100; +shelf_h = 40; +floor_h = 3; // Bottom plate + +// Battery pocket (with tolerance) +pocket_w = batt_w + tol*2; +pocket_d = batt_d + tol*2; +pocket_h = batt_h + 5; // Slightly taller than battery + +// Velcro strap slots +strap_w = 25; +strap_h = 3; + +module battery_shelf() { + difference() { + union() { + // Floor + translate([-shelf_w/2, -shelf_d/2, 0]) + cube([shelf_w, shelf_d, floor_h]); + + // Walls (3 sides — front open for wires) + // Left wall + translate([-shelf_w/2, -shelf_d/2, 0]) + cube([wall, shelf_d, shelf_h]); + // Right wall + translate([shelf_w/2 - wall, -shelf_d/2, 0]) + cube([wall, shelf_d, shelf_h]); + // Back wall + translate([-shelf_w/2, shelf_d/2 - wall, 0]) + cube([shelf_w, wall, shelf_h]); + + // Front lip (low, keeps battery from sliding out) + translate([-shelf_w/2, -shelf_d/2, 0]) + cube([shelf_w, wall, 10]); + + // 2020 extrusion mount tabs (top of back wall) + for (x = [-30, 30]) { + translate([x - 10, shelf_d/2 - wall, shelf_h - 15]) + cube([20, wall + 10, 15]); + } + } + + // Extrusion bolt holes (M5) through back mount tabs + for (x = [-30, 30]) { + translate([x, shelf_d/2 + 5, shelf_h - 7.5]) + rotate([90, 0, 0]) + cylinder(d=m5_clear, h=wall + 15, $fn=30); + } + + // Velcro strap slots (2x through floor for securing battery) + for (x = [-50, 50]) { + translate([x - strap_w/2, -20, -1]) + cube([strap_w, strap_h, floor_h + 2]); + } + + // Weight reduction holes in floor + for (x = [-30, 30]) { + translate([x, 0, -1]) + cylinder(d=20, h=floor_h + 2, $fn=30); + } + + // Wire routing slot (front wall, centered) + translate([-20, -shelf_d/2 - 1, floor_h]) + cube([40, wall + 2, 15]); + } +} + +battery_shelf(); diff --git a/cad/bumper.scad b/cad/bumper.scad new file mode 100644 index 0000000..f55b8bb --- /dev/null +++ b/cad/bumper.scad @@ -0,0 +1,75 @@ +// ============================================ +// SaltyLab — Bumper (Front/Rear) +// 350×50×30mm TPU +// Absorbs falls, protects frame and floor +// ============================================ +include + +bumper_w = 350; +bumper_h = 50; +bumper_d = 30; +bumper_wall = 2.5; + +// Honeycomb crush structure for energy absorption +hex_size = 8; +hex_wall = 1.2; + +module honeycomb_cell(size, height) { + difference() { + cylinder(d=size, h=height, $fn=6); + translate([0, 0, -1]) + cylinder(d=size - hex_wall*2, h=height + 2, $fn=6); + } +} + +module bumper() { + difference() { + union() { + // Outer shell (curved front face) + hull() { + translate([-bumper_w/2, 0, 0]) + cube([bumper_w, 1, bumper_h]); + translate([-bumper_w/2 + 10, bumper_d - 5, 5]) + cube([bumper_w - 20, 1, bumper_h - 10]); + } + } + + // Hollow interior (leave outer shell) + hull() { + translate([-bumper_w/2 + bumper_wall, bumper_wall, bumper_wall]) + cube([bumper_w - bumper_wall*2, 1, bumper_h - bumper_wall*2]); + translate([-bumper_w/2 + 10 + bumper_wall, bumper_d - 5 - bumper_wall, 5 + bumper_wall]) + cube([bumper_w - 20 - bumper_wall*2, 1, bumper_h - 10 - bumper_wall*2]); + } + + // Mounting bolt holes (M5, through back face, 4 points) + for (x = [-120, -40, 40, 120]) { + translate([x, -1, bumper_h/2]) + rotate([-90, 0, 0]) + cylinder(d=m5_clear, h=10, $fn=25); + } + } + + // Internal honeycomb ribs for crush absorption + intersection() { + // Bound to bumper volume + hull() { + translate([-bumper_w/2 + bumper_wall, bumper_wall, bumper_wall]) + cube([bumper_w - bumper_wall*2, 1, bumper_h - bumper_wall*2]); + translate([-bumper_w/2 + 15, bumper_d - 8, 8]) + cube([bumper_w - 30, 1, bumper_h - 16]); + } + + // Honeycomb grid + for (x = [-170:hex_size*1.5:170]) { + for (z = [0:hex_size*1.3:60]) { + offset_x = (floor(z / (hex_size*1.3)) % 2) * hex_size * 0.75; + translate([x + offset_x, 0, z]) + rotate([-90, 0, 0]) + honeycomb_cell(hex_size, bumper_d); + } + } + } +} + +bumper(); diff --git a/cad/dimensions.scad b/cad/dimensions.scad new file mode 100644 index 0000000..dd04a3d --- /dev/null +++ b/cad/dimensions.scad @@ -0,0 +1,73 @@ +// ============================================ +// SaltyLab — Common Dimensions & Constants +// ============================================ + +// --- 2020 Aluminum Extrusion --- +extrusion_w = 20; +extrusion_slot = 6; // T-slot width +extrusion_bore = 5; // Center bore M5 + +// --- Hub Motors (8" hoverboard) --- +motor_axle_dia = 12; +motor_axle_len = 45; +motor_axle_flat = 10; // Flat-to-flat if D-shaft +motor_body_dia = 200; // ~8 inches +motor_bolt_circle = 0; // Axle-only mount (clamp style) + +// --- Drone FC (30.5mm standard) --- +fc_hole_spacing = 25.5; // GEP-F722 AIO v2 (not standard 30.5!) +fc_hole_dia = 3.2; // M3 clearance +fc_board_size = 36; // Typical FC PCB +fc_standoff_h = 5; // Rubber standoff height + +// --- Jetson Nano --- +jetson_w = 100; +jetson_d = 80; +jetson_h = 29; // With heatsink +jetson_hole_x = 86; // Mounting hole spacing X +jetson_hole_y = 58; // Mounting hole spacing Y +jetson_hole_dia = 2.7; // M2.5 clearance + +// --- RealSense D435i --- +rs_w = 90; +rs_d = 25; +rs_h = 25; +rs_tripod_offset = 0; // 1/4-20 centered bottom +rs_mount_dia = 6.5; // 1/4-20 clearance + +// --- RPLIDAR A1 --- +lidar_dia = 70; +lidar_h = 41; +lidar_mount_circle = 67; // Bolt circle diameter +lidar_hole_count = 4; +lidar_hole_dia = 2.7; // M2.5 + +// --- Kill Switch (22mm panel mount) --- +kill_sw_dia = 22; +kill_sw_depth = 35; // Behind-panel depth + +// --- Battery (typical 36V hoverboard pack) --- +batt_w = 180; +batt_d = 80; +batt_h = 40; + +// --- Hoverboard ESC --- +esc_w = 80; +esc_d = 50; +esc_h = 15; + +// --- ESP32-C3 (typical dev board) --- +esp_w = 25; +esp_d = 18; +esp_h = 5; + +// --- WS2812B strip --- +led_strip_w = 10; // 10mm wide strip + +// --- General --- +wall = 3; // Default wall thickness +m3_clear = 3.2; +m3_insert = 4.2; // Heat-set insert hole +m25_clear = 2.7; +m5_clear = 5.3; +tol = 0.2; // Print tolerance per side diff --git a/cad/esc_mount.scad b/cad/esc_mount.scad new file mode 100644 index 0000000..a3c24ff --- /dev/null +++ b/cad/esc_mount.scad @@ -0,0 +1,70 @@ +// ============================================ +// SaltyLab — ESC Mount +// 150×100×15mm PETG +// Hoverboard ESC, mounts to 2020 extrusion +// ============================================ +include + +mount_w = 150; +mount_d = 100; +mount_h = 15; +base_h = 3; + +module esc_mount() { + difference() { + union() { + // Base plate + translate([-mount_w/2, -mount_d/2, 0]) + cube([mount_w, mount_d, base_h]); + + // ESC retaining walls (low lip on 3 sides) + // Left + translate([-mount_w/2, -mount_d/2, 0]) + cube([wall, mount_d, mount_h]); + // Right + translate([mount_w/2 - wall, -mount_d/2, 0]) + cube([wall, mount_d, mount_h]); + // Back + translate([-mount_w/2, mount_d/2 - wall, 0]) + cube([mount_w, wall, mount_h]); + + // Front clips (snap-fit tabs to hold ESC) + for (x = [-30, 30]) { + translate([x - 5, -mount_d/2, 0]) + cube([10, wall, mount_h]); + // Clip overhang + translate([x - 5, -mount_d/2, mount_h - 2]) + cube([10, wall + 3, 2]); + } + + // 2020 mount tabs (back) + for (x = [-25, 25]) { + translate([x - 10, mount_d/2 - wall, 0]) + cube([20, wall + 8, base_h + 8]); + } + } + + // Extrusion bolt holes (M5) + for (x = [-25, 25]) { + translate([x, mount_d/2 + 3, base_h + 4]) + rotate([90, 0, 0]) + cylinder(d=m5_clear, h=wall + 12, $fn=30); + } + + // Ventilation holes in base + for (x = [-40, -20, 0, 20, 40]) { + for (y = [-25, 0, 25]) { + translate([x, y, -1]) + cylinder(d=8, h=base_h + 2, $fn=20); + } + } + + // Wire routing slots (front and back) + translate([-15, -mount_d/2 - 1, base_h]) + cube([30, wall + 2, 10]); + translate([-15, mount_d/2 - wall - 1, base_h]) + cube([30, wall + 2, 10]); + } +} + +esc_mount(); diff --git a/cad/esp32c3_mount.scad b/cad/esp32c3_mount.scad new file mode 100644 index 0000000..c31f566 --- /dev/null +++ b/cad/esp32c3_mount.scad @@ -0,0 +1,57 @@ +// ============================================ +// SaltyLab — ESP32-C3 Mount +// 30×25×10mm PETG +// Tiny mount for LED controller MCU +// ============================================ +include + +mount_w = 30; +mount_d = 25; +mount_h = 10; +base_h = 2; + +module esp32c3_mount() { + difference() { + union() { + // Base + translate([-mount_w/2, -mount_d/2, 0]) + cube([mount_w, mount_d, base_h]); + + // Retaining walls (3 sides, front open for USB) + translate([-mount_w/2, -mount_d/2, 0]) + cube([wall, mount_d, mount_h]); + translate([mount_w/2 - wall, -mount_d/2, 0]) + cube([wall, mount_d, mount_h]); + translate([-mount_w/2, mount_d/2 - wall, 0]) + cube([mount_w, wall, mount_h]); + + // Clip tabs (front corners) + for (x = [-mount_w/2, mount_w/2 - wall]) { + translate([x, -mount_d/2, mount_h - 2]) + cube([wall, 4, 2]); + } + + // Zip-tie slot wings + for (x = [-mount_w/2 - 4, mount_w/2 + 1]) { + translate([x, -5, 0]) + cube([3, 10, base_h]); + } + } + + // Board pocket (recessed) + translate([-esp_w/2 - tol, -esp_d/2 - tol, base_h]) + cube([esp_w + tol*2, esp_d + tol*2, mount_h]); + + // Zip-tie slots + for (x = [-mount_w/2 - 4, mount_w/2 + 1]) { + translate([x, -2, -1]) + cube([3, 4, base_h + 2]); + } + + // USB port clearance (front) + translate([-5, -mount_d/2 - 1, base_h]) + cube([10, wall + 2, 5]); + } +} + +esp32c3_mount(); diff --git a/cad/fc_mount.scad b/cad/fc_mount.scad new file mode 100644 index 0000000..a3c2474 --- /dev/null +++ b/cad/fc_mount.scad @@ -0,0 +1,86 @@ +// ============================================ +// SaltyLab — Flight Controller Mount +// Vibration-isolated, 30.5mm pattern +// TPU dampers + PETG frame +// ============================================ +include + +// FC mount attaches to 2020 extrusion via T-slot +// Rubber/TPU grommets isolate FC from frame vibration + +mount_w = 45; // Overall width +mount_d = 45; // Overall depth +mount_h = 15; // Total height (base + standoffs) +base_h = 4; // Base plate thickness + +// TPU grommet dimensions +grommet_od = 7; +grommet_id = 3.2; // M3 clearance +grommet_h = 5; // Soft mount height + +module fc_mount() { + difference() { + union() { + // Base plate + translate([-mount_w/2, -mount_d/2, 0]) + cube([mount_w, mount_d, base_h]); + + // Standoff posts (PETG, FC sits on TPU grommets on top) + for (x = [-fc_hole_spacing/2, fc_hole_spacing/2]) { + for (y = [-fc_hole_spacing/2, fc_hole_spacing/2]) { + translate([x, y, 0]) + cylinder(d=8, h=base_h + grommet_h, $fn=30); + } + } + + // 2020 extrusion clamp tabs (sides) + for (side = [-1, 1]) { + translate([side * (extrusion_w/2 + wall), -15, 0]) + cube([wall, 30, base_h + 10]); + } + } + + // FC mounting holes (M3 through standoffs) + for (x = [-fc_hole_spacing/2, fc_hole_spacing/2]) { + for (y = [-fc_hole_spacing/2, fc_hole_spacing/2]) { + translate([x, y, -1]) + cylinder(d=fc_hole_dia, h=base_h + grommet_h + 2, $fn=25); + } + } + + // Extrusion channel (20mm wide slot through base) + translate([-extrusion_w/2 - tol, -20, -1]) + cube([extrusion_w + tol*2, 40, base_h + 2]); + + // Clamp bolt holes (M3, horizontal through side tabs) + for (side = [-1, 1]) { + translate([side * (extrusion_w/2 + wall + 1), 0, base_h + 5]) + rotate([0, 90, 0]) + cylinder(d=m3_clear, h=wall + 2, center=true, $fn=25); + } + + // Center cutout for airflow / weight reduction + translate([0, 0, -1]) + cylinder(d=15, h=base_h + 2, $fn=30); + } +} + +// TPU grommet (print separately in TPU) +module tpu_grommet() { + difference() { + cylinder(d=grommet_od, h=grommet_h, $fn=30); + translate([0, 0, -1]) + cylinder(d=grommet_id, h=grommet_h + 2, $fn=25); + } +} + +// Show assembled +fc_mount(); + +// Show grommets in position (for visualization) +%for (x = [-fc_hole_spacing/2, fc_hole_spacing/2]) { + for (y = [-fc_hole_spacing/2, fc_hole_spacing/2]) { + translate([x, y, base_h]) + tpu_grommet(); + } +} diff --git a/cad/handle.scad b/cad/handle.scad new file mode 100644 index 0000000..ada9b47 --- /dev/null +++ b/cad/handle.scad @@ -0,0 +1,59 @@ +// ============================================ +// SaltyLab — Carry Handle +// 150×30×30mm PETG +// Comfortable grip, mounts on top of spine +// ============================================ +include + +handle_w = 150; +handle_h = 30; +grip_dia = 25; // Comfortable grip diameter +grip_len = 100; // Grip section length + +module handle() { + difference() { + union() { + // Grip bar (rounded for comfort) + translate([-grip_len/2, 0, handle_h]) + rotate([0, 90, 0]) + cylinder(d=grip_dia, h=grip_len, $fn=40); + + // Left support leg + hull() { + translate([-handle_w/2, -10, 0]) + cube([20, 20, 3]); + translate([-grip_len/2, 0, handle_h]) + rotate([0, 90, 0]) + cylinder(d=grip_dia, h=5, $fn=40); + } + + // Right support leg + hull() { + translate([handle_w/2 - 20, -10, 0]) + cube([20, 20, 3]); + translate([grip_len/2 - 5, 0, handle_h]) + rotate([0, 90, 0]) + cylinder(d=grip_dia, h=5, $fn=40); + } + } + + // 2020 extrusion slot (center of base) + translate([-extrusion_w/2 - tol, -extrusion_w/2 - tol, -1]) + cube([extrusion_w + tol*2, extrusion_w + tol*2, 5]); + + // M5 bolt holes for extrusion (2x) + for (x = [-30, 30]) { + translate([x, 0, -1]) + cylinder(d=m5_clear, h=5, $fn=25); + } + + // Finger grooves on grip + for (x = [-30, -10, 10, 30]) { + translate([x, 0, handle_h]) + rotate([0, 90, 0]) + cylinder(d=grip_dia + 4, h=5, center=true, $fn=40); + } + } +} + +handle(); diff --git a/cad/jetson_shelf.scad b/cad/jetson_shelf.scad new file mode 100644 index 0000000..64adbd6 --- /dev/null +++ b/cad/jetson_shelf.scad @@ -0,0 +1,69 @@ +// ============================================ +// SaltyLab — Jetson Nano Shelf +// 120×100×15mm PETG +// Mounts Jetson Nano to 2020 extrusion +// ============================================ +include + +shelf_w = 120; +shelf_d = 100; +shelf_h = 15; +base_h = 3; +standoff_h = 8; // Clearance for Jetson underside components + +module jetson_shelf() { + difference() { + union() { + // Base plate + translate([-shelf_w/2, -shelf_d/2, 0]) + cube([shelf_w, shelf_d, base_h]); + + // Jetson standoffs (M2.5, 86mm × 58mm pattern) + for (x = [-jetson_hole_x/2, jetson_hole_x/2]) { + for (y = [-jetson_hole_y/2, jetson_hole_y/2]) { + translate([x, y, 0]) + cylinder(d=6, h=base_h + standoff_h, $fn=25); + } + } + + // 2020 extrusion clamp (back edge) + translate([-15, shelf_d/2 - wall, 0]) + cube([30, wall + 10, base_h + 12]); + + // Side rails for Jetson alignment + for (x = [-jetson_w/2 - wall, jetson_w/2]) { + translate([x, -jetson_d/2, base_h + standoff_h]) + cube([wall, jetson_d, 4]); + } + } + + // Jetson M2.5 holes (through standoffs) + for (x = [-jetson_hole_x/2, jetson_hole_x/2]) { + for (y = [-jetson_hole_y/2, jetson_hole_y/2]) { + translate([x, y, -1]) + cylinder(d=jetson_hole_dia, h=base_h + standoff_h + 2, $fn=25); + } + } + + // Extrusion bolt hole (M5, through back clamp) + translate([0, shelf_d/2 + 3, base_h + 6]) + rotate([90, 0, 0]) + cylinder(d=m5_clear, h=wall + 15, $fn=30); + + // Extrusion channel slot + translate([-extrusion_w/2 - tol, shelf_d/2 - wall - 1, -1]) + cube([extrusion_w + tol*2, wall + 2, base_h + 2]); + + // Ventilation / cable routing + for (x = [-25, 0, 25]) { + translate([x, 0, -1]) + cylinder(d=15, h=base_h + 2, $fn=25); + } + + // USB/Ethernet/GPIO access cutouts (front edge) + translate([-jetson_w/2, -shelf_d/2 - 1, base_h]) + cube([jetson_w, wall + 2, shelf_h]); + } +} + +jetson_shelf(); diff --git a/cad/kill_switch_mount.scad b/cad/kill_switch_mount.scad new file mode 100644 index 0000000..1182818 --- /dev/null +++ b/cad/kill_switch_mount.scad @@ -0,0 +1,56 @@ +// ============================================ +// SaltyLab — Kill Switch Mount +// 60×60×40mm PETG +// 22mm panel-mount emergency stop button +// Mounts to 2020 extrusion, easily reachable +// ============================================ +include + +mount_w = 60; +mount_d = 60; +mount_h = 40; +panel_h = 3; // Panel face thickness + +module kill_switch_mount() { + difference() { + union() { + // Main body (angled face for visibility) + hull() { + translate([-mount_w/2, 0, 0]) + cube([mount_w, mount_d, 1]); + translate([-mount_w/2, 5, mount_h]) + cube([mount_w, mount_d - 5, 1]); + } + + // 2020 extrusion mount bracket (back) + translate([-15, mount_d, 0]) + cube([30, 10, 20]); + } + + // Kill switch hole (22mm, through angled face) + translate([0, mount_d/2, mount_h/2]) + rotate([10, 0, 0]) // Slight angle for ergonomics + cylinder(d=kill_sw_dia + tol, h=panel_h + 2, center=true, $fn=50); + + // Interior cavity (hollow for switch body) + translate([-kill_sw_dia/2 - 3, 5, 3]) + cube([kill_sw_dia + 6, mount_d - 10, mount_h - 3]); + + // Wire exit hole (bottom) + translate([0, mount_d/2, -1]) + cylinder(d=10, h=5, $fn=25); + + // Extrusion bolt holes (M5, through back bracket) + for (z = [7, 15]) { + translate([-20, mount_d + 5, z]) + rotate([90, 0, 0]) + cylinder(d=m5_clear, h=15, center=true, $fn=25); + } + + // Label recess ("EMERGENCY STOP" — flat area for sticker) + translate([-25, 5, mount_h - 1]) + cube([50, 15, 1.5]); + } +} + +kill_switch_mount(); diff --git a/cad/led_diffuser_ring.scad b/cad/led_diffuser_ring.scad new file mode 100644 index 0000000..921de94 --- /dev/null +++ b/cad/led_diffuser_ring.scad @@ -0,0 +1,53 @@ +// ============================================ +// SaltyLab — LED Diffuser Ring +// Ø120×15mm Clear PETG 30% infill +// Wraps around frame, holds WS2812B strip +// Print in clear/natural PETG for diffusion +// ============================================ +include + +ring_od = 120; +ring_id = 110; // Inner diameter (strip sits inside) +ring_h = 15; +strip_channel_w = led_strip_w + 1; // Strip channel +strip_channel_d = 3; // Depth for strip + +module led_diffuser_ring() { + difference() { + // Outer ring + cylinder(d=ring_od, h=ring_h, $fn=80); + + // Inner hollow + translate([0, 0, -1]) + cylinder(d=ring_id, h=ring_h + 2, $fn=80); + + // LED strip channel (groove on inner wall) + translate([0, 0, (ring_h - strip_channel_w)/2]) + difference() { + cylinder(d=ring_id + 2, h=strip_channel_w, $fn=80); + cylinder(d=ring_id - strip_channel_d*2, h=strip_channel_w, $fn=80); + } + + // Wire entry slot + translate([ring_od/2 - 5, -3, ring_h/2 - 3]) + cube([10, 6, 6]); + + // 2020 extrusion clearance (center) + translate([-extrusion_w/2 - 5, -extrusion_w/2 - 5, -1]) + cube([extrusion_w + 10, extrusion_w + 10, ring_h + 2]); + } + + // Mounting tabs (clip onto extrusion, 4x) + for (angle = [0, 90, 180, 270]) { + rotate([0, 0, angle]) + translate([extrusion_w/2 + 1, -5, 0]) + difference() { + cube([3, 10, ring_h]); + translate([-1, 2, ring_h/2]) + rotate([0, 90, 0]) + cylinder(d=m3_clear, h=5, $fn=20); + } + } +} + +led_diffuser_ring(); diff --git a/cad/lidar_standoff.scad b/cad/lidar_standoff.scad new file mode 100644 index 0000000..27e523b --- /dev/null +++ b/cad/lidar_standoff.scad @@ -0,0 +1,61 @@ +// ============================================ +// SaltyLab — LIDAR Standoff +// Ø80×80mm ASA +// Raises RPLIDAR above all other components +// for unobstructed 360° scan +// Connects sensor_tower_top to 2020 extrusion +// ============================================ +include + +standoff_od = 80; +standoff_h = 80; +wall_t = 3; + +module lidar_standoff() { + difference() { + union() { + // Main cylinder + cylinder(d=standoff_od, h=standoff_h, $fn=60); + + // Bottom flange (bolts to extrusion bracket below) + cylinder(d=standoff_od + 10, h=4, $fn=60); + } + + // Hollow interior + translate([0, 0, wall_t]) + cylinder(d=standoff_od - wall_t*2, h=standoff_h, $fn=60); + + // Cable routing hole (bottom) + translate([0, 0, -1]) + cylinder(d=20, h=wall_t + 2, $fn=30); + + // Ventilation / weight reduction slots (4x around circumference) + for (angle = [0, 90, 180, 270]) { + rotate([0, 0, angle]) + translate([0, standoff_od/2, standoff_h/2]) + rotate([90, 0, 0]) + hull() { + translate([0, -15, 0]) + cylinder(d=10, h=wall_t + 2, center=true, $fn=25); + translate([0, 15, 0]) + cylinder(d=10, h=wall_t + 2, center=true, $fn=25); + } + } + + // Bottom flange bolt holes (M5, 4x for mounting) + for (angle = [45, 135, 225, 315]) { + rotate([0, 0, angle]) + translate([standoff_od/2, 0, -1]) + cylinder(d=m5_clear, h=6, $fn=25); + } + + // Top mating holes (M3, align with sensor_tower_top) + for (angle = [0, 90, 180, 270]) { + rotate([0, 0, angle]) + translate([standoff_od/2 - wall_t - 3, 0, standoff_h - 8]) + cylinder(d=m3_clear, h=10, $fn=25); + } + } +} + +lidar_standoff(); diff --git a/cad/motor_mount_plate.scad b/cad/motor_mount_plate.scad new file mode 100644 index 0000000..a3cb2c4 --- /dev/null +++ b/cad/motor_mount_plate.scad @@ -0,0 +1,94 @@ +// ============================================ +// SaltyLab — Motor Mount Plate +// 350×150×6mm PETG +// Mounts both 8" hub motors + 2020 extrusion spine +// ============================================ +include + +plate_w = 350; // Width (axle to axle direction) +plate_d = 150; // Depth (front to back) +plate_h = 6; // Thickness + +// Motor axle positions (centered, symmetric) +motor_spacing = 280; // Center-to-center axle distance + +// Extrusion spine mount (centered, 2x M5 bolts) +spine_offset_y = 0; // Centered front-to-back +spine_bolt_spacing = 60; // Two bolts along spine + +// Motor clamp dimensions +clamp_w = 30; +clamp_h = 25; // Height above plate for clamping axle +clamp_gap = motor_axle_dia + tol*2; // Slot for axle +clamp_bolt_offset = 10; // M5 clamp bolt offset from center + +module motor_clamp() { + difference() { + // Clamp block + translate([-clamp_w/2, -clamp_w/2, 0]) + cube([clamp_w, clamp_w, plate_h + clamp_h]); + + // Axle hole (through, slightly oversized) + translate([0, 0, plate_h + clamp_h/2 + 5]) + rotate([0, 90, 0]) + cylinder(d=clamp_gap, h=clamp_w+2, center=true, $fn=40); + + // Clamp slit (allows tightening) + translate([0, 0, plate_h + clamp_h/2 + 5]) + cube([clamp_w+2, 1.5, clamp_h], center=true); + + // Clamp bolt holes (M5, horizontal through clamp ears) + translate([0, clamp_bolt_offset, plate_h + clamp_h/2 + 5]) + rotate([0, 90, 0]) + cylinder(d=m5_clear, h=clamp_w+2, center=true, $fn=30); + translate([0, -clamp_bolt_offset, plate_h + clamp_h/2 + 5]) + rotate([0, 90, 0]) + cylinder(d=m5_clear, h=clamp_w+2, center=true, $fn=30); + } +} + +module motor_mount_plate() { + difference() { + union() { + // Main plate + translate([-plate_w/2, -plate_d/2, 0]) + cube([plate_w, plate_d, plate_h]); + + // Left motor clamp + translate([-motor_spacing/2, 0, 0]) + motor_clamp(); + + // Right motor clamp + translate([motor_spacing/2, 0, 0]) + motor_clamp(); + + // Reinforcement ribs (bottom) + for (x = [-100, 0, 100]) { + translate([x - 2, -plate_d/2, 0]) + cube([4, plate_d, plate_h]); + } + } + + // Extrusion spine bolt holes (M5, 2x along center) + for (y = [-spine_bolt_spacing/2, spine_bolt_spacing/2]) { + translate([0, y, -1]) + cylinder(d=m5_clear, h=plate_h+2, $fn=30); + // Counterbore for bolt head + translate([0, y, plate_h - 2.5]) + cylinder(d=10, h=3, $fn=30); + } + + // Weight reduction holes + for (x = [-70, 70]) { + for (y = [-40, 40]) { + translate([x, y, -1]) + cylinder(d=25, h=plate_h+2, $fn=40); + } + } + + // Corner rounding (chamfer edges) + // (simplified — round in slicer or add minkowski) + } +} + +motor_mount_plate(); diff --git a/cad/realsense_bracket.scad b/cad/realsense_bracket.scad new file mode 100644 index 0000000..6f67753 --- /dev/null +++ b/cad/realsense_bracket.scad @@ -0,0 +1,64 @@ +// ============================================ +// SaltyLab — RealSense D435i Bracket +// 100×50×40mm PETG +// Adjustable tilt mount on 2020 extrusion +// ============================================ +include + +bracket_w = 100; +bracket_d = 50; +bracket_h = 40; + +// Camera cradle +cradle_w = rs_w + wall*2 + tol*2; +cradle_d = rs_d + wall + tol*2; +cradle_h = rs_h + 5; + +module realsense_bracket() { + // Extrusion clamp base + difference() { + union() { + // Clamp block + translate([-20, -20, 0]) + cube([40, 40, 15]); + + // Tilt arm (vertical, supports camera above) + translate([-wall, -wall, 0]) + cube([wall*2, wall*2, bracket_h]); + + // Camera cradle at top + translate([-cradle_w/2, -cradle_d/2, bracket_h - 5]) { + difference() { + cube([cradle_w, cradle_d, cradle_h]); + + // Camera pocket + translate([wall, -1, 3]) + cube([rs_w + tol*2, rs_d + tol*2 + 1, rs_h + tol*2]); + } + } + + // Tripod mount boss (1/4-20 bolt from bottom of cradle) + translate([0, 0, bracket_h - 5]) + cylinder(d=15, h=3, $fn=30); + } + + // 2020 extrusion channel + translate([-extrusion_w/2 - tol, -extrusion_w/2 - tol, -1]) + cube([extrusion_w + tol*2, extrusion_w + tol*2, 17]); + + // Clamp bolt (M5, through side) + translate([-25, 0, 7.5]) + rotate([0, 90, 0]) + cylinder(d=m5_clear, h=50, $fn=30); + + // Camera 1/4-20 bolt hole (from bottom of cradle) + translate([0, 0, bracket_h - 6]) + cylinder(d=rs_mount_dia, h=10, $fn=30); + + // Cable routing slot (back of cradle) + translate([-10, cradle_d/2 - wall - 1, bracket_h]) + cube([20, wall + 2, cradle_h - 2]); + } +} + +realsense_bracket(); diff --git a/cad/sensor_tower_top.scad b/cad/sensor_tower_top.scad new file mode 100644 index 0000000..73eccca --- /dev/null +++ b/cad/sensor_tower_top.scad @@ -0,0 +1,58 @@ +// ============================================ +// SaltyLab — Sensor Tower Top +// 120×120×10mm ASA +// Mounts RPLIDAR A1 on top of 2020 spine +// ============================================ +include + +top_w = 120; +top_d = 120; +top_h = 10; +base_h = 4; + +module sensor_tower_top() { + difference() { + union() { + // Circular plate (RPLIDAR needs 360° clearance) + cylinder(d=top_w, h=base_h, $fn=60); + + // RPLIDAR standoffs (4x M2.5 on 67mm bolt circle) + for (i = [0:3]) { + angle = i * 90 + 45; // 45° offset + translate([cos(angle) * lidar_mount_circle/2, + sin(angle) * lidar_mount_circle/2, 0]) + cylinder(d=6, h=top_h, $fn=25); + } + + // 2020 extrusion socket (bottom center) + translate([-extrusion_w/2 - wall, -extrusion_w/2 - wall, -15]) + cube([extrusion_w + wall*2, extrusion_w + wall*2, 15]); + } + + // RPLIDAR M2.5 through-holes + for (i = [0:3]) { + angle = i * 90 + 45; + translate([cos(angle) * lidar_mount_circle/2, + sin(angle) * lidar_mount_circle/2, -1]) + cylinder(d=lidar_hole_dia, h=top_h + 2, $fn=25); + } + + // Center hole (RPLIDAR motor shaft clearance + cable routing) + translate([0, 0, -1]) + cylinder(d=25, h=base_h + 2, $fn=40); + + // 2020 extrusion socket (square hole) + translate([-extrusion_w/2 - tol, -extrusion_w/2 - tol, -16]) + cube([extrusion_w + tol*2, extrusion_w + tol*2, 16]); + + // Set screw holes for extrusion (M3, 2x perpendicular) + for (angle = [0, 90]) { + rotate([0, 0, angle]) + translate([0, extrusion_w/2 + wall, -7.5]) + rotate([90, 0, 0]) + cylinder(d=m3_clear, h=wall + 5, $fn=25); + } + } +} + +sensor_tower_top(); diff --git a/cad/tether_anchor.scad b/cad/tether_anchor.scad new file mode 100644 index 0000000..c62c751 --- /dev/null +++ b/cad/tether_anchor.scad @@ -0,0 +1,46 @@ +// ============================================ +// SaltyLab — Tether Anchor Point +// 50×50×20mm PETG 100% infill +// For ceiling tether during balance testing +// Must be STRONG — 100% infill mandatory +// ============================================ +include + +anchor_w = 50; +anchor_d = 50; +anchor_h = 20; +ring_dia = 30; // Carabiner ring outer +ring_hole = 15; // Carabiner hook clearance +ring_h = 8; + +module tether_anchor() { + difference() { + union() { + // Base (clamps to 2020 extrusion) + translate([-anchor_w/2, -anchor_d/2, 0]) + cube([anchor_w, anchor_d, anchor_h - ring_h]); + + // Tether ring (stands up from base) + translate([0, 0, anchor_h - ring_h]) + cylinder(d=ring_dia, h=ring_h, $fn=50); + } + + // Ring hole (for carabiner) + translate([0, 0, anchor_h - ring_h - 1]) + cylinder(d=ring_hole, h=ring_h + 2, $fn=40); + + // 2020 extrusion channel (through base) + translate([-extrusion_w/2 - tol, -extrusion_w/2 - tol, -1]) + cube([extrusion_w + tol*2, extrusion_w + tol*2, anchor_h - ring_h + 2]); + + // Clamp bolt holes (M5, through sides) + for (angle = [0, 90]) { + rotate([0, 0, angle]) + translate([0, anchor_d/2 + 1, (anchor_h - ring_h)/2]) + rotate([90, 0, 0]) + cylinder(d=m5_clear, h=anchor_d + 2, $fn=25); + } + } +} + +tether_anchor(); diff --git a/docs/AGENTS.md b/docs/AGENTS.md new file mode 100644 index 0000000..f3ac992 --- /dev/null +++ b/docs/AGENTS.md @@ -0,0 +1,281 @@ +# AGENTS.md — SaltyLab Agent Onboarding + +You're working on **SaltyLab**, a self-balancing two-wheeled indoor robot. Read this entire file before touching anything. + +## Project Overview + +A hoverboard-based balancing robot with two compute layers: +1. **FC (Flight Controller)** — MAMBA F722S (STM32F722RET6 + MPU6000 IMU). Runs a lean C balance loop at up to 8kHz. Talks UART to the hoverboard ESC. This is the safety-critical layer. +2. **Jetson Nano** — AI brain. ROS2, SLAM, person tracking. Sends velocity commands to FC via UART. Not safety-critical — FC operates independently. + +``` +Jetson (speed+steer via UART1) ←→ ELRS RC (UART3, kill switch) + │ + ▼ + MAMBA F722S (MPU6000 IMU, PID balance) + │ + ▼ UART2 + Hoverboard ESC (FOC) → 2× 8" hub motors +``` + +## ⚠️ SAFETY — READ THIS OR PEOPLE GET HURT + +This is not a toy. 8" hub motors + 36V battery can crush fingers, break toes, and launch the frame. Every firmware change must preserve these invariants: + +1. **Motors NEVER spin on power-on.** Requires deliberate arming: hold button 3s while upright. +2. **Tilt cutoff at ±25°** — motors to zero, require manual re-arm. No retry, no recovery. +3. **Hardware watchdog (50ms)** — if firmware hangs, motors cut. +4. **RC kill switch** — dedicated ELRS channel, checked every loop iteration. Always overrides. +5. **Jetson UART timeout (200ms)** — if Jetson disconnects, motors cut. +6. **Speed hard cap** — firmware limit, start at 10%. Increase only after proven stable. +7. **Never test untethered** until PID is stable for 5+ minutes on a tether. + +**If you break any of these, you are removed from the project.** + +## Repository Layout + +``` +firmware/ # STM32 HAL firmware (PlatformIO) +├── src/ +│ ├── main.c # Entry point, clock config, main loop +│ ├── icm42688.c # ICM-42688-P SPI driver (backup IMU — currently broken) +│ ├── bmp280.c # Barometer driver (disabled) +│ └── status.c # LED + buzzer status patterns +├── include/ +│ ├── config.h # Pin definitions, constants +│ ├── icm42688.h +│ ├── mpu6000.h # MPU6000 driver header (primary IMU) +│ ├── hoverboard.h # Hoverboard ESC UART protocol +│ ├── crsf.h # ELRS CRSF protocol +│ ├── bmp280.h +│ └── status.h +├── lib/USB_CDC/ # USB CDC stack (serial over USB) +│ ├── src/ # CDC implementation, USB descriptors, PCD config +│ └── include/ +└── platformio.ini # Build config + +cad/ # OpenSCAD parametric parts (16 files) +├── dimensions.scad # ALL measurements live here — single source of truth +├── assembly.scad # Full robot assembly visualization +├── motor_mount_plate.scad +├── battery_shelf.scad +├── fc_mount.scad # Vibration-isolated FC mount +├── jetson_shelf.scad +├── esc_mount.scad +├── sensor_tower_top.scad +├── lidar_standoff.scad +├── realsense_bracket.scad +├── bumper.scad # TPU bumpers (front + rear) +├── handle.scad +├── kill_switch_mount.scad +├── tether_anchor.scad +├── led_diffuser_ring.scad +└── esp32c3_mount.scad + +ui/ # Web UI (Three.js + WebSerial) +└── index.html # 3D board visualization, real-time IMU data + +SALTYLAB.md # Master design doc — architecture, wiring, build phases +SALTYLAB-DETAILED.md # Power budget, weight budget, detailed schematics +PLATFORM.md # Hardware platform reference +``` + +## Hardware Quick Reference + +### MAMBA F722S Flight Controller + +| Spec | Value | +|------|-------| +| MCU | STM32F722RET6 (Cortex-M7, 216MHz, 512KB flash, 256KB RAM) | +| Primary IMU | MPU6000 (WHO_AM_I = 0x68) | +| IMU Bus | SPI1: PA5=SCK, PA6=MISO, PA7=MOSI, CS=PA4 | +| IMU EXTI | PC4 (data ready interrupt) | +| IMU Orientation | CW270 (Betaflight convention) | +| Secondary IMU | ICM-42688-P (on same SPI1, CS unknown — currently non-functional) | +| Betaflight Target | DIAT-MAMBAF722_2022B | +| USB | OTG FS (PA11/PA12), enumerates as /dev/cu.usbmodemSALTY0011 | +| VID/PID | 0x0483/0x5740 | +| LEDs | PC15 (LED1), PC14 (LED2), active low | +| Buzzer | PB2 (inverted push-pull) | +| Battery ADC | PC1=VBAT, PC3=CURR (ADC3) | +| DFU | Hold yellow BOOT button + plug USB (or send 'R' over CDC) | + +### UART Assignments + +| UART | Pins | Connected To | Baud | +|------|------|-------------|------| +| USART1 | PA9/PA10 | Jetson Nano | 115200 | +| USART2 | PA2/PA3 | Hoverboard ESC | 115200 | +| USART3 | PB10/PB11 | ELRS Receiver | 420000 (CRSF) | +| UART4 | — | Spare | — | +| UART5 | — | Spare | — | + +### Motor/ESC + +- 2× 8" pneumatic hub motors (36V, hoverboard type) +- Hoverboard ESC with FOC firmware +- UART protocol: `{0xABCD, int16 speed, int16 steer, uint16 checksum}` at 115200 +- Speed range: -1000 to +1000 + +### Physical Dimensions (from `cad/dimensions.scad`) + +| Part | Key Measurement | +|------|----------------| +| FC mounting holes | 25.5mm spacing (NOT standard 30.5mm!) | +| FC board size | ~36mm square | +| Hub motor body | Ø200mm (~8") | +| Motor axle | Ø12mm, 45mm long | +| Jetson Nano | 100×80×29mm, M2.5 holes at 86×58mm | +| RealSense D435i | 90×25×25mm, 1/4-20 tripod mount | +| RPLIDAR A1 | Ø70×41mm, 4× M2.5 on Ø67mm circle | +| Kill switch hole | Ø22mm panel mount | +| Battery pack | ~180×80×40mm | +| Hoverboard ESC | ~80×50×15mm | +| 2020 extrusion | 20mm square, M5 center bore | +| Frame width | ~350mm (axle to axle) | +| Frame height | ~500-550mm total | +| Target weight | <8kg (current estimate: 7.4kg) | + +### 3D Printed Parts (16 files in `cad/`) + +| Part | Material | Infill | +|------|----------|--------| +| motor_mount_plate (350×150×6mm) | PETG | 80% | +| battery_shelf | PETG | 60% | +| esc_mount | PETG | 40% | +| jetson_shelf | PETG | 40% | +| sensor_tower_top | ASA | 80% | +| lidar_standoff (Ø80×80mm) | ASA | 40% | +| realsense_bracket | PETG | 60% | +| fc_mount (vibration isolated) | TPU+PETG | — | +| bumper front + rear (350×50×30mm) | TPU | 30% | +| handle | PETG | 80% | +| kill_switch_mount | PETG | 80% | +| tether_anchor | PETG | 100% | +| led_diffuser_ring (Ø120×15mm) | Clear PETG | 30% | +| esp32c3_mount | PETG | 40% | + +## Firmware Architecture + +### Critical Lessons Learned (DON'T REPEAT THESE) + +1. **SysTick_Handler with HAL_IncTick() is MANDATORY** — without it, HAL_Delay() and every HAL timeout hangs forever. This bricked us multiple times. +2. **DCache breaks SPI on STM32F7** — disable DCache or use cache-aligned DMA buffers with clean/invalidate. We disable it. +3. **`-(int)0 == 0`** — checking `if (-result)` to detect errors doesn't work when result is 0 (success and failure look the same). Always use explicit error codes. +4. **NEVER auto-run untested code on_boot** — we bricked the NSPanel 3x doing this. Test manually first. +5. **USB CDC needs ReceivePacket() primed in CDC_Init** — without it, the OUT endpoint never starts listening. No data reception. + +### DFU Reboot (Betaflight Method) + +The firmware supports reboot-to-DFU via USB command: +1. Send `R` byte over USB CDC +2. Firmware writes `0xDEADBEEF` to RTC backup register 0 +3. `NVIC_SystemReset()` — clean hardware reset +4. On boot, `checkForBootloader()` (called after `HAL_Init()`) reads the magic +5. If magic found: clears it, remaps system memory, jumps to STM32 bootloader at `0x1FF00000` +6. Board appears as DFU device, ready for `dfu-util` flash + +### Build & Flash + +```bash +cd firmware/ +python3 -m platformio run # Build +dfu-util -a 0 -s 0x08000000:leave -D .pio/build/f722/firmware.bin # Flash +``` + +Dev machine: mbpm4 (seb@192.168.87.40), PlatformIO project at `~/Projects/saltylab-firmware/` + +### Clock Configuration + +``` +HSE 8MHz → PLL (M=8, N=432, P=2, Q=9) → SYSCLK 216MHz +PLLSAI (N=384, P=8) → CLK48 48MHz (USB) +APB1 = HCLK/4 = 54MHz +APB2 = HCLK/2 = 108MHz +Fallback: HSI 16MHz if HSE fails (PLL M=16) +``` + +## Current Status & Known Issues + +### Working +- USB CDC serial streaming (50Hz JSON: `{"ax":...,"ay":...,"az":...,"gx":...,"gy":...,"gz":...}`) +- Clock config with HSE + HSI fallback +- Reboot-to-DFU via USB 'R' command +- LED status patterns (status.c) +- Web UI with WebSerial + Three.js 3D visualization + +### Broken / In Progress +- **ICM-42688-P SPI reads return all zeros** — was the original IMU target, but SPI communication completely non-functional despite correct pin config. May be dead silicon. Switched to MPU6000 as primary. +- **MPU6000 driver** — header exists but implementation needs completion +- **PID balance loop** — not yet implemented +- **Hoverboard ESC UART** — protocol defined, driver not written +- **ELRS CRSF receiver** — protocol defined, driver not written +- **Barometer (BMP280)** — I2C init hangs, disabled + +### TODO (Priority Order) +1. Get MPU6000 streaming accel+gyro data +2. Implement complementary filter (pitch angle) +3. Write hoverboard ESC UART driver +4. Write PID balance loop with safety checks +5. Wire ELRS receiver, implement CRSF parser +6. Bench test (ESC disconnected, verify PID output) +7. First tethered balance test at 10% speed +8. Jetson UART integration +9. LED subsystem (ESP32-C3) + +## Communication Protocols + +### Jetson → FC (UART1, 50Hz) +```c +struct { uint8_t header=0xAA; int16_t speed; int16_t steer; uint8_t mode; uint8_t checksum; }; +// mode: 0=idle, 1=balance, 2=follow, 3=RC +``` + +### FC → Hoverboard ESC (UART2, loop rate) +```c +struct { uint16_t start=0xABCD; int16_t speed; int16_t steer; uint16_t checksum; }; +// speed/steer: -1000 to +1000 +``` + +### FC → Jetson Telemetry (UART1 TX, 50Hz) +``` +T:12.3,P:45,L:100,R:-80,S:3\n +// T=tilt°, P=PID output, L/R=motor commands, S=state (0-3) +``` + +### FC → USB CDC (50Hz JSON) +```json +{"ax":123,"ay":-456,"az":16384,"gx":10,"gy":-5,"gz":3,"t":250,"p":0,"bt":0} +// Raw IMU values (int16), t=temp×10, p=pressure, bt=baro temp +``` + +## LED Subsystem (ESP32-C3) + +ESP32-C3 eavesdrops on FC→Jetson telemetry (listen-only tap on UART1 TX). No extra FC UART needed. + +| State | Pattern | Color | +|-------|---------|-------| +| Disarmed | Slow breathe | White | +| Arming | Fast blink | Yellow | +| Armed idle | Solid | Green | +| Turning | Sweep direction | Orange | +| Braking | Flash rear | Red | +| Fault | Triple flash | Red | +| RC lost | Alternating flash | Red/Blue | + +## Printing (Bambu Lab) + +- **X1C** (192.168.87.190) — for structural PETG/ASA parts +- **A1** (192.168.86.161) — for TPU bumpers and prototypes +- LAN access codes and MQTT details in main workspace MEMORY.md +- STL export from OpenSCAD, slice in Bambu Studio + +## Rules for Agents + +1. **Read SALTYLAB.md fully** before making any design decisions +2. **Never remove safety checks** from firmware — add more if needed +3. **All measurements go in `cad/dimensions.scad`** — single source of truth +4. **Test firmware on bench before any motor test** — ESC disconnected, verify outputs on serial +5. **One variable at a time** — don't change PID and speed limit in the same test +6. **Document what you change** — update this file if you add pins, change protocols, or discover hardware quirks +7. **Ask before wiring changes** — wrong connections can fry the FC ($50+ board) diff --git a/docs/PLATFORM.md b/docs/PLATFORM.md new file mode 100644 index 0000000..35d7940 --- /dev/null +++ b/docs/PLATFORM.md @@ -0,0 +1,222 @@ +# SaltyRover — Modular Platform Design 🧂🛞 + +## Design Philosophy +- **Modular:** Standardized mounting points for swappable top decks +- **Printable:** Main structural brackets on Bambu X1C (256x256x256mm) and A1 (256x256x256mm) +- **Repairable:** Bolt-together, no permanent welds/glue on structural parts +- **Weatherproof-ish:** Splash resistant for outdoor use, not submarine + +## Base Platform ("Skateboard") + +### Frame +``` + FRONT + ┌─────────────────┐ + │ ┌─M1─┐ ┌─M2─┐ │ M1-M4: 6.5" hub motors + │ │ │ │ │ │ ESC1 drives M1+M2 (front) + │ └────┘ └────┘ │ ESC2 drives M3+M4 (rear) + │ │ + │ ┌──────────────┐ │ + │ │ BATTERY │ │ Center-mounted battery bay + │ │ BAY │ │ Fits 2x hoverboard packs (2P) + │ └──────────────┘ │ + │ │ + │ ┌─ESC1─┐┌─ESC2─┐ │ ESCs flanking center + │ └──────┘└──────┘ │ + │ ┌──5V──┐┌─12V──┐ │ DC-DC converters + │ └──────┘└──────┘ │ + │ │ + │ ┌─M3─┐ ┌─M4─┐ │ + │ │ │ │ │ │ + │ └────┘ └────┘ │ + └─────────────────┘ + REAR + + Overall: ~600mm L × 450mm W × ~120mm H (base only) +``` + +### Dimensions +- **Length:** 600mm (motor center to motor center ~500mm, +50mm overhang each end) +- **Width:** 450mm (constrained by motor axle-to-axle, ~350mm inner + motor housings) +- **Ground clearance:** ~50mm (bottom of frame to ground) +- **Wheelbase:** 500mm (front axle to rear axle) +- **Track width:** 350mm (left wheel center to right wheel center) + +### Frame Construction +- **Main rails (x2):** Aluminum extrusion 2040 V-slot, 600mm length + - Or: 40x20mm aluminum rectangular tube + - Or: 3D printed with steel rod reinforcement +- **Cross members (x3):** Front, center, rear — aluminum or printed +- **Motor mounts (x4):** 3D printed brackets, bolted to frame rails + - Must accommodate 6.5" hub motor axle (standard hoverboard M10 axle) + - Axle clamp style — two-piece with bolts for easy wheel swap + +### Modular Top Deck Interface +``` + ┌─────────────────────┐ + │ ○ ○ ○ ○ │ ← M5 threaded inserts, 100mm grid + │ │ + │ ○ ○ ○ ○ │ Standard mounting pattern: + │ │ - 400mm × 300mm grid + │ ○ ○ ○ ○ │ - M5 bolt holes on 100mm centers + │ │ - 16 mount points total + │ ○ ○ ○ ○ │ + └─────────────────────┘ +``` + +**Top deck connector:** +- 16x M5 threaded inserts in frame top rails +- 100mm grid spacing +- Any top deck just needs matching bolt holes +- Power connector: XT30 (5V + 12V + GND) standardized position at rear-center +- Data connector: USB-A hub mounted to frame, accessible from top + +## Top Deck Configurations + +### Config 1: "Follow Bot" (Primary) +``` + ┌─────────────────────┐ + │ [RPLIDAR A1M8] │ ← Top-mounted, unobstructed 360° + │ ╱spinning╲ │ Raised on 100mm standoff + │ │ + │ [RealSense D435i] │ ← Front-facing, angled down ~10° + │ │ Height: ~400mm from ground + │ [Jetson Nano] │ ← Center, in ventilated enclosure + │ [WiFi/4G module] │ Noctua fan draws air through + │ │ + │ [Speaker] [LEDs] │ ← Rear: audio feedback + status + │ [E-STOP button] │ Big red mushroom button + └─────────────────────┘ +``` + +**Parts:** +- Sensor tower: 3D printed, 100mm tall, mounts LIDAR on top +- RealSense bracket: 3D printed, adjustable tilt +- Jetson enclosure: 3D printed, ventilated, vibration dampened +- LED strip ring: NeoPixel/WS2812B around sensor tower (status indication) + +### Config 2: "Cargo Hauler" +``` + ┌─────────────────────┐ + │ ┌─────────────────┐ │ + │ │ │ │ Flat cargo platform + │ │ CARGO AREA │ │ 400 × 300 × 150mm + │ │ (open top) │ │ With tie-down points + │ │ │ │ + │ └─────────────────┘ │ + │ [GPS] [Beacon] │ Minimal autonomy — follows beacon + └─────────────────────┘ +``` + +### Config 3: "Camera Rig" +``` + ┌─────────────────────┐ + │ [Gimbal] │ 2-axis stabilized camera mount + │ [Action Cam] │ GoPro / Insta360 + │ │ + │ [Jetson + storage] │ Records while following + │ [Large battery] │ Extended runtime for filming + └─────────────────────┘ +``` + +### Config 4: "Security Patrol" +``` + ┌─────────────────────┐ + │ [RPLIDAR] │ Autonomous waypoint patrol + │ [PTZ Camera] │ Pan-tilt-zoom camera + │ [Spotlight] │ High-power LED + │ [Jetson + 4G] │ Streams to Frigate + │ [Siren/Speaker] │ + └─────────────────────┘ +``` + +## 3D Printed Parts List (Config 1: Follow Bot) + +All designed for Bambu X1C/A1 build plate (256x256mm max). + +| Part | Size (mm) | Material | Infill | Qty | +|------|-----------|----------|--------|-----| +| Motor mount bracket | 80×60×40 | PETG/ASA | 60% | 4 | +| Motor mount clamp top | 80×40×15 | PETG/ASA | 60% | 4 | +| Cross member front | 350×40×20 | PETG/ASA | 80% | 1 | +| Cross member center | 350×60×20 | PETG/ASA | 80% | 1 | +| Cross member rear | 350×40×20 | PETG/ASA | 80% | 1 | +| Battery tray | 250×150×30 | PETG | 40% | 1 | +| Battery strap anchor | 40×20×15 | PETG | 100% | 4 | +| ESC mount tray | 150×100×15 | PETG | 40% | 2 | +| DC-DC mount | 80×60×15 | PETG | 40% | 2 | +| Sensor tower base | 120×120×10 | ASA | 80% | 1 | +| Sensor tower tube | Ø80×100 | ASA | 40% | 1 | +| LIDAR mount plate | Ø90×5 | ASA | 100% | 1 | +| RealSense bracket | 100×50×60 | PETG | 60% | 1 | +| Jetson enclosure bottom | 120×100×25 | PETG | 40% | 1 | +| Jetson enclosure top | 120×100×25 | PETG | 40% | 1 | +| E-stop mount | 50×50×30 | PETG | 60% | 1 | +| Wire management clips | 20×15×10 | PETG | 100% | 10 | +| Fender/splash guard | 200×80×60 | ASA | 30% | 4 | + +**Material notes:** +- **ASA** for outdoor/exposed parts (UV resistant, weather resistant) +- **PETG** for structural internal parts (strong, slight flex) +- Avoid PLA — warps in summer sun + +## Electrical Wiring + +``` +PACK1 ═╤═ PACK2 (parallel, XT60) + │ + ├──→ ESC1 ──→ M1 (front-left) + M2 (front-right) + │ │ + │ └── UART TX/RX ──→ Jetson GPIO + │ + ├──→ ESC2 ──→ M3 (rear-left) + M4 (rear-right) + │ │ + │ └── UART TX/RX ──→ Jetson GPIO + │ + ├──→ DC-DC 36V→5V ──→ Jetson Nano (barrel jack 5V/4A) + │ ──→ USB hub (sensors) + │ + ├──→ DC-DC 36V→12V ──→ LED strips + │ ──→ Speaker amp + │ ──→ 4G modem + │ + └──→ E-STOP (normally closed, inline with main power) +``` + +### ESC UART Protocol (FOC firmware) +- Baud: 115200 (or 9600, configurable) +- Each ESC: `steer` + `speed` as int16 values (-1000 to +1000) +- ESC1 (front): Jetson UART1 +- ESC2 (rear): Jetson UART2 (or USB-serial adapter) + +### Differential Drive Control +``` +Left speed = throttle - steering +Right speed = throttle + steering + +For 4WD: front and rear ESCs get same commands +(or: rear slightly less for better turning) +``` + +## Assembly Order +1. Cut/prepare frame rails (aluminum extrusion or tube) +2. Print all brackets and mounts +3. Assemble frame with cross members +4. Mount motors to brackets, attach to frame +5. Install battery tray, strap packs +6. Mount ESCs and DC-DC converters +7. Wire power distribution (XT60 splitters) +8. Install E-stop inline +9. Mount top deck with sensor tower +10. Wire data connections (UART, USB) +11. First test: power on, spin motors manually via serial terminal +12. Flash follow-bot software to Jetson +13. Outdoor test in parking lot + +## Next Steps +- [ ] Measure exact motor axle dimensions and spacing +- [ ] Choose frame material (aluminum extrusion vs printed vs hybrid) +- [ ] Design motor mount bracket in CAD (FreeCAD/Fusion360) +- [ ] Print test motor mount, verify fit +- [ ] Design and print sensor tower +- [ ] Bench test: Jetson → UART → ESC → single motor spinning diff --git a/docs/SALTYLAB-DETAILED.md b/docs/SALTYLAB-DETAILED.md new file mode 100644 index 0000000..6d91a34 --- /dev/null +++ b/docs/SALTYLAB-DETAILED.md @@ -0,0 +1,454 @@ +# SaltyLab — Detailed Build Plan 🔬⚖️ + +Self-balancing two-wheeled indoor robot with AI brain. + +--- + +## 1. Battery Analysis + +### Pack Specs (Begode Master V1 packs) +- **Configuration per pack:** 10S (35V nominal, 42V full, 30V cutoff) +- **Chemistry:** Li-ion 18650 or 21700 +- **Estimated capacity per pack:** ~450-500Wh (based on Master V1 total ~1800Wh ÷ 4 packs) + - If 10S4P with 21700 5000mAh cells: 36V × 20Ah = **720Wh** + - If 10S3P with 21700 5000mAh cells: 36V × 15Ah = **540Wh** + - If 10S4P with 18650 3500mAh cells: 36V × 14Ah = **504Wh** + - **Need to verify:** check cell count visible on pack, or weigh it + +### SaltyLab Battery Config: Single Pack +- **Voltage:** 35V nominal (fits hoverboard ESC: designed for 36V/10S) +- **Capacity:** ~500Wh (conservative estimate) +- **Weight:** ~2-3kg per pack + +### Why Single Pack is Enough +- SaltyLab is indoor-only, short missions +- One pack gives 2-4 hours runtime (see estimates below) +- Keep other 3 packs for SaltyRider and SaltyTank + +--- + +## 2. Power Budget & Range Estimation + +### Component Power Draw + +| Component | Voltage | Current | Power (W) | Notes | +|-----------|---------|---------|-----------|-------| +| Jetson Nano | 5V | 2-4A | 10-20W | AI inference mode: ~15W avg | +| RealSense D435i | 5V (USB) | 0.7A | 3.5W | Depth + RGB streaming | +| RPLIDAR A1M8 | 5V | 0.5A | 2.5W | Spinning at 5.5Hz | +| BNO055 IMU | 3.3V | 0.01A | 0.04W | Negligible | +| ESC (idle/balance) | 36V | 0.3A | 10W | Maintaining balance, no movement | +| LEDs + misc | 12V | 0.5A | 6W | Status LEDs, speaker | +| DC-DC losses | — | — | ~5W | ~85% efficiency on converters | +| **Subtotal (idle/balancing)** | | | **~47W** | | + +### Motor Power (Moving) + +| Activity | Per Motor | Total (2 motors) | Notes | +|----------|-----------|-------------------|-------| +| Balancing in place | 5-15W | 10-30W | Continuous micro-corrections | +| Slow indoor movement (2 km/h) | 15-25W | 30-50W | Walking pace | +| Normal indoor (5 km/h) | 30-50W | 60-100W | Brisk walk | +| Fast / acceleration | 80-150W | 160-300W | Bursts, turning | +| Climbing threshold/ramp | 100-200W | 200-400W | Short duration | + +### Total Power by Use Case + +| Mode | Electronics | Motors | Total | Notes | +|------|-------------|--------|-------|-------| +| **Idle (balancing)** | 47W | 20W | **~67W** | Standing still | +| **Slow patrol** | 47W | 40W | **~87W** | Gentle movement | +| **Normal follow** | 47W | 80W | **~127W** | Following person around house | +| **Active (turning, accel)** | 47W | 200W | **~247W** | Bursts | + +### Range Estimates (Single 500Wh Pack) + +| Mode | Avg Power | Runtime | Distance | +|------|-----------|---------|----------| +| Idle (balancing) | 67W | **7.5 hours** | 0 km (stationary) | +| Slow patrol (2 km/h) | 87W | **5.7 hours** | ~11 km | +| Normal follow (5 km/h) | 127W | **3.9 hours** | ~20 km | +| Mixed indoor use | ~100W avg | **5 hours** | ~15 km | +| Aggressive (lots of turning) | 180W avg | **2.8 hours** | ~8 km | + +**Bottom line: 3-5 hours of indoor use on a single pack.** More than enough. + +### Weight Budget + +| Component | Weight (g) | Notes | +|-----------|-----------|-------| +| Battery pack (1x) | 2500 | Estimated, weigh to verify | +| 2x 8" hub motors | 2400 | ~1200g each with tire | +| ESC board | 150 | Single board | +| Jetson Nano + heatsink | 280 | With Noctua fan | +| RealSense D435i | 72 | Very light | +| RPLIDAR A1M8 | 170 | With motor | +| BNO055 breakout | 5 | Tiny | +| DC-DC converters (2x) | 300 | 150g each | +| Frame + brackets | 1200 | Aluminum + 3D printed | +| Wiring + connectors | 200 | | +| Bumpers (TPU) | 150 | | +| **TOTAL** | **~7.4 kg** | Target: under 8 kg | + +--- + +## 3. Detailed 2D Schematics + +### 3.1 Base Plate — Top View + +``` + 350mm + ←─────────────────────────────────────→ + + ┌─────────────────────────────────────┐ ─┬─ + │ ○ ○ │ │ + │ ┌───────────────────────────┐ │ │ + │ │ MOTOR MOUNT PLATE │ │ │ + │ │ │ │ │ + │ │ ┌─────┐ ┌─────┐ │ │ │ + │ │ │AXLE │ │AXLE │ │ │ │ 200mm + │ │ │ L │ │ R │ │ │ │ + │ │ └─────┘ └─────┘ │ │ │ + │ │ ↑ 250mm ↑ │ │ │ + │ │ └─────────────┘ │ │ │ + │ │ track width │ │ │ + │ └───────────────────────────┘ │ │ + │ ○ ○ │ │ + └─────────────────────────────────────┘ ─┴─ + + ○ = M5 mounting holes for vertical spine (4 corners) + + Axle holes: Ø14mm (standard hoverboard axle) + Plate thickness: 6mm PETG + + Motor mount detail: + ┌──────────────┐ + │ ┌────────┐ │ + │ │ Ø14mm │ │ Two-piece clamp: + │ │ axle │ │ Bottom: part of base plate + │ │ hole │ │ Top: clamp plate with 2x M6 bolts + │ └────────┘ │ + │ ○ ○ │ ○ = M6 clamp bolt holes + └──────────────┘ + 80mm +``` + +### 3.2 Side View — Full Assembly + +``` + FRONT → + + 550mm ┬ ┌─────────┐ + │ │ RPLIDAR │ Ø80mm, 360° clear + │ │ A1M8 │ + 500mm │ ├─────────┤ + │ │ │ ← LIDAR standoff tube (80mm tall) + │ │ │ + 420mm │ ├─────────┤ + │ │RealSense│ ← Tilted down 10°, front-facing + │ │ D435i │ Adjustable bracket + 380mm │ ├─────────┤ + │ │ │ + │ │ JETSON │ ← Noctua fan, ventilation slots + │ │ NANO │ + 300mm │ ├─────────┤ + │ │ BNO055 │ ← IMU, vibration-isolated mount + 280mm │ ├─────────┤ + │ │ │ + │ │ ESC + │ ← ESC board + DC-DC converters + │ │ DC-DCs │ + 200mm │ ├─────────┤ + │ │ │ + │ │ BATTERY │ ← Heaviest component, lowest position + │ │ PACK │ Strapped to spine with velcro + │ │ │ + 80mm │ ├─────────┤ + │ │ BASE │ ← Motor mount plate (6mm) + │ │ PLATE │ + 40mm │ ├────┬────┤ + │ │ │ │ + ┴ └────┘ └── 8" wheel (Ø203mm) + ═════════════ + GROUND (0mm) + + Ground clearance: ~40mm (bottom of plate to ground) + Wheel contact to axle center: ~100mm (8" diameter / 2) + Axle height from ground: ~100mm +``` + +### 3.3 Front View + +``` + ←── 350mm ──→ + + ┌─────────┐ ─┬─ 550mm + │ RPLIDAR │ │ + ├─────────┤ │ + │ ┃ │ ← spine │ + │ ┃ │ (2020 │ + │ ┃ │ extrusion│ + │ ┃ │ or │ + │ ┃ │ aluminum │ + │ ┃ │ tube) │ + │ ┃ │ │ + ├───┃─────┤ │ + ┌─────┐ │ ┃ │ ┌─────┐│ + │ │ │ ┃ │ │ ││ + │ 8" │ │ ┃ │ │ 8" ││ 100mm + │ L │───┤ ┃ ├───│ R ││ (axle) + │ │ │ ┃ │ │ ││ + │ │ └───┃─────┘ │ │┴─ 0mm + └─────┘ ┃ └─────┘ + ═══════════════════════════════════ + + ←65→←──── 250mm ────→←65→ + mm (track width) mm + + Total width with tires: ~380mm + Fits through standard doorway (760mm) ✓ +``` + +### 3.4 Spine Detail — Side View + +``` + ┌──┐ ← 20×20mm aluminum extrusion (2020 V-slot) + │ │ or 25×25mm square aluminum tube + │ │ + │ │──── Shelf bracket (3D printed, bolts to T-slot) + │ │ Each shelf: 120mm wide × 100-150mm deep + │ │ + │ │──── Shelf bracket + │ │ + │ │──── Shelf bracket + │ │ + │ │──── Shelf bracket + │ │ + ├──┤ + │ │──── Base plate connection (L-brackets, 4x M5) + └──┘ + + Spine length: 470mm (from base plate to LIDAR mount) + + Shelf positions (from base plate): + 0mm — Base plate + 30mm — Battery shelf (holds pack on its side) + 150mm — ESC + DC-DC shelf + 250mm — Jetson Nano shelf + 300mm — BNO055 (attached to spine directly) + 370mm — RealSense bracket (front-facing arm) + 420mm — LIDAR standoff begins + 500mm — LIDAR mount plate +``` + +### 3.5 Wiring Diagram + +``` + BATTERY PACK (35V nominal, 10S Li-ion) + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │(+) (−)│ + │ │ + ├──[E-STOP (NC)]───────────┤ + │ │ + XT60 ├────────┬────────┬────────┤ XT60 + │ │ │ │ + │ ┌───┴───┐ │ │ + │ │DC-DC │ │ │ + │ │36V→5V │ │ │ + │ │ 4A │ │ │ + │ └───┬───┘ │ │ + │ 5V │ │ │ + │ ┌───┴────┐ │ │ + │ │USB Hub │ │ │ + │ │Jetson │ │ │ + │ │RealSns │ │ │ + │ │RPLIDAR │ │ │ + │ └────────┘ │ │ + │ │ │ + │ ┌────┴───┐ │ │ + │ │DC-DC │ │ │ + │ │36V→12V │ │ │ + │ │ 2A │ │ │ + │ └───┬────┘ │ │ + │ 12V │ │ │ + │ ┌───┴────┐ │ │ + │ │LEDs │ │ │ + │ │Speaker │ │ │ + │ └────────┘ │ │ + │ │ │ + ┌────┴─────────────────┴────────┴────┐ + │ HOVERBOARD ESC │ + │ (FOC firmware) │ + │ │ + │ I2C: SDA──BNO055──SCL │ + │ UART: TX──Jetson──RX │ + │ │ + │ PHASE L: ─── 8" LEFT MOTOR │ + │ HALL L: ─── (5 wire: hall A/B/C │ + │ + 5V + GND) │ + │ │ + │ PHASE R: ─── 8" RIGHT MOTOR │ + │ HALL R: ─── (5 wire) │ + └────────────────────────────────────┘ +``` + +--- + +## 4. Phased Build Plan + +### Phase 1: Rolling Skeleton (Days 1-3) +**Goal:** Prove balance works. + +**Tasks:** +- [ ] Measure 8" motor axle diameter with calipers +- [ ] Design motor mount plate in CAD (FreeCAD or TinkerCAD) +- [ ] Print motor mount plate on Bambu X1C (PETG, 80% infill, ~4h print) +- [ ] Print axle clamp tops (x2) +- [ ] Mount both 8" motors to plate +- [ ] Mount ESC to plate with standoffs +- [ ] Wire BNO055 to ESC I2C (4 wires: VCC, GND, SDA, SCL) +- [ ] Wire battery to ESC (XT60) +- [ ] Modify FOC firmware: add BNO055 I2C read, replace gyro board input +- [ ] Flash ESC with updated firmware +- [ ] **SAFETY:** Tie rope from ceiling to plate as fall catch +- [ ] Power on, tune PID (start with Kp=20, Ki=0, Kd=0, increase gradually) +- [ ] Achieve stable free-standing balance (no rope) + +**Parts needed:** motor mount plate, 2x clamp tops, M6 bolts, M3 standoffs +**Risk:** PID tuning can take hours of iteration. Be patient. + +### Phase 2: Spine + Brain (Days 4-7) +**Goal:** Add Jetson, achieve remote-controlled movement while balancing. + +**Tasks:** +- [ ] Cut aluminum extrusion/tube to 470mm for spine +- [ ] Print shelf brackets (4x) and L-brackets for base connection +- [ ] Assemble spine onto base plate +- [ ] Mount battery to lowest shelf (velcro straps) +- [ ] Mount ESC + DC-DC converters +- [ ] Mount Jetson Nano on shelf, connect 5V power +- [ ] Wire Jetson UART → ESC UART +- [ ] Install JetPack 4.6 on Jetson (if not already) +- [ ] Write serial bridge: Jetson Python → ESC UART commands +- [ ] Test: keyboard control (WASD) → speed/steer commands → balanced movement +- [ ] Tune speed response (acceleration limits, max speed for indoor) +- [ ] Add E-stop button (inline with battery positive) + +**Software deliverable:** `saltylab_teleop.py` — keyboard-controlled balancing bot + +### Phase 3: Eyes + Ears (Days 8-12) +**Goal:** See the world. Map a room. Detect people. + +**Tasks:** +- [ ] Print RealSense bracket (adjustable tilt) +- [ ] Print LIDAR standoff tube + mount plate +- [ ] Mount RealSense D435i (front-facing, ~10° down tilt) +- [ ] Mount RPLIDAR A1M8 (top of spine, 360° clear) +- [ ] Install ROS2 on Jetson +- [ ] Install and test `realsense-ros` (verify depth stream) +- [ ] Install and test `rplidar_ros` (verify laser scan) +- [ ] Run `slam_toolbox` — drive around room, build 2D map +- [ ] Test person detection with SSD-MobileNet-v2 (TensorRT) +- [ ] Implement follow mode: + - Detect person in RGB frame + - Get distance from depth frame + - PID to maintain 1.5m following distance + - LIDAR for obstacle avoidance + +**Software deliverable:** `saltylab_follow.py` — person-following balanced bot + +### Phase 4: Polish + Personality (Days 13-17) +**Goal:** Make it feel alive. Make it SaltyLab. + +**Tasks:** +- [ ] Print proper enclosures for all electronics +- [ ] Print TPU bumpers (front + rear) +- [ ] Print carry handle +- [ ] Add NeoPixel LED ring around LIDAR mount (status indication) + - Blue breathing: idle/balancing + - Green: following + - Yellow: exploring/mapping + - Red: error/low battery +- [ ] Add small speaker (USB or I2S to amp) + - Boot sound + - Acknowledge commands with beeps/chirps + - Optional: TTS via Jetson ("I see you", "battery low") +- [ ] WiFi dashboard: live camera feed + map + battery status +- [ ] Battery voltage monitoring (ADC on ESC → Jetson via UART) +- [ ] Low battery return behavior (stop and beep) +- [ ] Integrate with Home Assistant (MQTT: location, battery, status) + +### Phase 5: House Mapping + Autonomy (Days 18-24) +**Goal:** SaltyLab knows your house. + +**Tasks:** +- [ ] Map every room (drive around manually, SLAM builds full floor plan) +- [ ] Save map, set up `nav2` for autonomous navigation +- [ ] Define waypoints: lab, living room, kitchen, hallway +- [ ] Patrol mode: visit waypoints on schedule +- [ ] Person detection + greeting ("hey Tee", "hi Inka") +- [ ] Integration with Bermuda BLE: know where people are, go to them +- [ ] Charging dock design (future: auto-dock when low) + +--- + +## 5. Speed & Performance Specs + +### Target Performance + +| Parameter | Value | Notes | +|-----------|-------|-------| +| Max speed (indoor) | 5 km/h | Software limited for safety | +| Normal follow speed | 2-3 km/h | Walking pace | +| Turning radius | 0 (pivot) | Differential drive, spins in place | +| Ground clearance | 40mm | Clears door thresholds (~15mm) | +| Max incline | ~10° | Limited by motor torque + balance | +| Operating time | 3-5 hours | Single 500Wh pack | +| Charge time | ~2-3 hours | Using one of the existing chargers | +| Weight | ~7.5 kg | Easy to pick up with handle | +| Width | 380mm | Fits all doorways | +| Height | 550mm | Below table height | + +### Motor Specs (8" hub motor, estimated) +| Parameter | Value | +|-----------|-------| +| Nominal voltage | 36V | +| Rated power | 250-350W per motor | +| No-load RPM | ~250 RPM | +| Wheel circumference | ~0.64m (Ø203mm) | +| Max wheel speed | 160 m/min = 9.6 km/h | +| Continuous torque | ~2-3 Nm | +| Stall torque | ~8-10 Nm | + +--- + +## 6. Safety Considerations + +| Hazard | Mitigation | +|--------|-----------| +| Falls over | TPU bumpers, max tilt cutoff (30°), low CoG | +| Runs away | Software speed limit (5 km/h), E-stop button | +| Pinches/crushes | No exposed gears, motor covers | +| Battery fire | BMS on pack, fused main power, no charging unattended | +| Hits furniture | LIDAR obstacle avoidance, bumper sensors (future) | +| Scares the cat | Slow acceleration, no sudden movements | + +--- + +## 7. Shopping List (Items NOT in Inventory) + +| Item | Price (CAD) | Source | Notes | +|------|------------|--------|-------| +| 2020 aluminum extrusion 500mm | ~$8 | Amazon/AliExpress | Spine | +| T-slot nuts + M5 bolts (pack) | ~$12 | Amazon | For shelf mounting | +| M6 bolts + nuts (axle clamps) | ~$5 | Hardware store | 4x sets | +| NeoPixel ring (24 LED) | ~$8 | Amazon | Status indication | +| Small speaker + amp (MAX98357A) | ~$10 | Amazon/Adafruit | I2S audio | +| E-stop mushroom button | ~$5 | Amazon | Safety | +| XT60 splitter/distribution | ~$8 | Amazon | Power wiring | +| Misc: heat shrink, zip ties, wire | ~$10 | — | Always need more | +| **TOTAL** | **~$66** | | Everything else: already owned | + +--- + +*Last updated: 2026-02-27* +*Project: SaltyRover / SaltyLab* diff --git a/docs/SALTYLAB.md b/docs/SALTYLAB.md new file mode 100644 index 0000000..49bc664 --- /dev/null +++ b/docs/SALTYLAB.md @@ -0,0 +1,420 @@ +# SaltyLab — Self-Balancing Indoor Bot 🔬 + +Two-wheeled, self-balancing robot for indoor AI/SLAM experiments. + +## ⚠️ SAFETY — TOP PRIORITY + +**This robot can cause serious injury.** 8" hub motors with 36V power can crush toes, break fingers, and launch the frame if control is lost. Every design decision must prioritize safety. + +### Mandatory Safety Systems +1. **Hardware kill switch** — physical big red button, wired inline with battery. Cuts ALL power instantly. Must be reachable without approaching the wheels. +2. **Software tilt cutoff** — if pitch exceeds ±25° (not 30°), motors go to zero immediately. No retry, no recovery. Requires manual re-arm. +3. **Startup arming sequence** — motors NEVER spin on power-on. Requires deliberate arming: hold button for 3 seconds while robot is upright and stable. +4. **Watchdog timeout** — if FC firmware hangs or crashes, hardware watchdog resets to safe state (motors off) within 50ms. +5. **Current limiting** — hoverboard ESC max current set conservatively. Start low, increase gradually. +6. **Tether during development** — ceiling rope/strap during ALL balance testing. No free-standing tests until PID is proven stable for 5+ minutes tethered. +7. **Speed limiting** — firmware hard cap on max speed. Start at 10% throttle, increase in 10% increments only after stable testing. +8. **Remote kill** — Jetson can send emergency stop via UART. If Jetson disconnects (UART timeout >200ms), FC cuts motors automatically. +9. **Bumpers** — TPU bumpers on all sides, mandatory before any untethered operation. +10. **Test area** — clear 3m radius, no pets/kids/cables. Shoes mandatory. +11. **RC kill channel** — ELRS receiver connected to FC UART. Dedicated switch on radio = instant disarm. Works independently of Jetson. Always have radio in hand during testing. + +### Safety Rules for Development +- **Never reach near wheels while powered** — even "stopped" motors can spike +- **Never test new firmware untethered** — tether FIRST, always +- **Never increase speed and change PID in the same test** — one variable at a time +- **Log everything** — FC sends telemetry (pitch, PID output, motor commands) to Jetson for post-crash analysis +- **Two people for early tests** — one at the kill switch, one observing + +## Parts + +| Part | Status | +|------|--------| +| 2x 8" pneumatic hub motors (36 PSI) | ✅ Have | +| 1x hoverboard ESC (FOC firmware) | ✅ Have | +| 1x Drone FC (STM32F745 + MPU-6000) | ✅ Have — balance brain | +| 1x Jetson Nano + Noctua fan | ✅ Have | +| 1x RealSense D435i | ✅ Have | +| 1x RPLIDAR A1M8 | ✅ Have | +| 1x battery pack (36V) | ✅ Have | +| 1x DC-DC 5V converter | ✅ Have | +| 1x DC-DC 12V converter | ✅ Have | +| 1x ESP32-C3 (LED controller) | ⬜ Need (~$3) | +| WS2812B LED strip (60/m) | ⬜ Need | +| BNO055 9-DOF IMU | ✅ Have (spare/backup) | +| MPU6050 | ✅ Have (spare/backup) | +| 1x Big red kill switch (NC, inline with battery) | ⬜ Need | +| 1x Arming button (momentary, with LED) | ⬜ Need | +| 1x Ceiling tether strap + carabiner | ⬜ Need | +| 1x BetaFPV ELRS 2.4GHz 1W TX module | ✅ Have — RC control + kill switch | +| 1x ELRS receiver (matching) | ✅ Have — mounts on FC UART | + +### Drone FC Details — GEPRC GEP-F7 AIO +- **MCU:** STM32F722RET6 (216MHz Cortex-M7, 512KB flash, 256KB RAM) +- **IMU:** TDK ICM-42688-P (6-axis, 32kHz gyro, ultra-low noise, SPI) ← the good one! +- **Flash:** 8MB Winbond W25Q64 (blackbox, unused) +- **OSD:** AT7456E (unused) +- **4-in-1 ESC:** Built into AIO board (unused — we use hoverboard ESC) +- **DFU mode:** Hold yellow BOOT button while plugging USB +- **Firmware:** Custom balance firmware (PlatformIO + STM32 HAL) +- **UART pads (confirmed from silkscreen):** + - T1/R1 (bottom) → USART1 (PA9/PA10) → Jetson + - T2/R2 (right top) → USART2 (PA2/PA3) → Hoverboard ESC + - T3/R3 (bottom) → USART3 (PB10/PB11) → ELRS receiver + - T4/R4 (bottom) → UART4 → spare + - T5/R5 (right bottom) → UART5 → spare + +## Architecture + +``` + ┌──────────────┐ + │ RPLIDAR A1 │ ← 360° scan, top-mounted + └──────┬───────┘ + ┌──────┴───────┐ + │ RealSense │ ← Forward-facing depth+RGB + │ D435i │ + ├──────────────┤ + │ Jetson Nano │ ← AI brain: navigation, person tracking + │ │ Sends velocity commands via UART + ├──────────────┤ + │ Drone FC │ ← Balance brain: IMU + PID @ 8kHz + │ F745+MPU6000 │ Custom firmware, UART out to ESC + ├──────────────┤ + │ Battery 36V │ + │ + DC-DCs │ + ├──────┬───────┤ + ┌─────┤ ESC (FOC) ├─────┐ + │ │ Hoverboard │ │ + │ └──────────────┘ │ + ┌──┴──┐ ┌──┴──┐ + │ 8" │ │ 8" │ + │ LEFT│ │RIGHT│ + └─────┘ └─────┘ +``` + +## Self-Balancing Control — Custom Firmware on Drone FC + +### Why a Drone FC? +The F745 board is just a premium STM32 dev board with a high-quality IMU (MPU-6000) already soldered on, proper voltage regulation, and multiple UARTs broken out. We write a lean custom balance firmware (~50 lines of C). + +### Architecture +``` +Jetson (speed+steer via UART1) + │ + ▼ +Drone FC (F745 + MPU-6000) + │ - Reads IMU @ 8kHz (SPI) + │ - Runs PID balance loop + │ - Mixes balance correction + Jetson commands + │ - Outputs speed+steer via UART2 + ▼ +Hoverboard ESC (FOC firmware) + │ - Receives UART commands + │ - Drives hub motors + ▼ +Left + Right wheels +``` + +- **No motor outputs used** — FC talks UART directly to hoverboard ESC +- **Custom firmware only** — no third-party flight software +- **Dead motor output irrelevant** — not using any PWM channels + +### Wiring + +``` +Jetson UART1 Drone FC (UART1) +──────────── ──────────────── +TX (Pin 8) ──→ RX +RX (Pin 10) ──→ TX +GND ──→ GND + +Drone FC (UART2) Hoverboard ESC +──────────────── ────────────── +TX ──→ RX (serial input) +GND ──→ GND +5V (BEC) ←── ESC 5V out (powers FC) + +ELRS Receiver Drone FC (UART3) +───────────── ──────────────── +TX ──→ RX +RX ←── TX (for telemetry/binding) +GND ──→ GND +5V ←── 5V +``` + +### Custom Firmware (STM32 C) + +```c +// Core balance loop — runs in timer interrupt @ 1-8kHz +void balance_loop(void) { + // 1. Read pitch angle from MPU-6000 (complementary filter) + float pitch = get_pitch_angle(); // SPI read + filter + + // 2. Get velocity command from Jetson (updated async via UART1 RX) + float target_speed = jetson_cmd.speed; // -1000 to 1000 + float target_steer = jetson_cmd.steer; // -1000 to 1000 + + // 3. PID on pitch error + // Target angle shifts with speed command (lean forward = go forward) + float target_angle = target_speed * SPEED_TO_ANGLE_FACTOR; + float error = target_angle - pitch; + + integral += error * dt; + integral = clamp(integral, -MAX_I, MAX_I); // anti-windup + float derivative = (error - prev_error) / dt; + prev_error = error; + + float output = Kp * error + Ki * integral + Kd * derivative; + + // 4. Mix balance + steering → hoverboard ESC UART command + int16_t left = clamp(output + target_steer, -1000, 1000); + int16_t right = clamp(output - target_steer, -1000, 1000); + + // 5. Send to hoverboard ESC via UART2 + send_hoverboard_cmd(left, right); + + // 6. Safety: kill motors if tipped beyond recovery + if (fabs(pitch) > MAX_TILT_DEG) { + send_hoverboard_cmd(0, 0); + disarm(); + } + + // 7. Safety: RC kill switch (ELRS channel, checked every loop) + if (rc_channels.arm_switch == DISARMED) { + send_hoverboard_cmd(0, 0); + disarm(); + } + + // 8. Safety: kill if Jetson UART heartbeat lost + if (millis() - jetson_last_rx > JETSON_TIMEOUT_MS) { + send_hoverboard_cmd(0, 0); + disarm(); + } + + // 8. Safety: clamp output to max allowed speed + left = clamp(left, -max_speed_limit, max_speed_limit); + right = clamp(right, -max_speed_limit, max_speed_limit); +} +``` + +### Hoverboard ESC UART Protocol +```c +typedef struct { + uint16_t start; // 0xABCD + int16_t speed; // -1000 to 1000 (left) + int16_t steer; // -1000 to 1000 (right) + uint16_t checksum; // XOR of all bytes +} HoverboardCmd; +// 115200 baud, send at loop rate +``` + +### Jetson → FC Protocol (simple custom) +```c +typedef struct { + uint8_t header; // 0xAA + int16_t speed; // -1000 to 1000 + int16_t steer; // -1000 to 1000 + uint8_t mode; // 0=idle, 1=balance, 2=follow, 3=RC + uint8_t checksum; +} JetsonCmd; +// 115200 baud, ~50Hz from Jetson is plenty +``` + +### PID Tuning +| Param | Starting Value | Notes | +|-------|---------------|-------| +| Kp | 30-50 | Main balance response | +| Ki | 0.5-2 | Drift correction | +| Kd | 0.5-2 | Damping oscillation | +| Loop rate | 1-8 kHz | Start at 1kHz, increase if needed | +| Max tilt | ±25° | Beyond this = cut motors, require re-arm | +| JETSON_TIMEOUT_MS | 200 | Kill motors if Jetson stops talking | +| max_speed_limit | 100 | Start at 10% (100/1000), increase gradually | +| SPEED_TO_ANGLE_FACTOR | 0.01-0.05 | How much lean per speed unit | + +## LED Subsystem (ESP32-C3) + +### Architecture +The ESP32-C3 eavesdrops on the FC→Jetson telemetry UART line (listen-only, one wire). +No extra UART needed on the FC — zero firmware change. + +``` +FC UART1 TX ──┬──→ Jetson RX + └──→ ESP32-C3 RX (listen-only, same wire) + │ + └──→ WS2812B strip (via RMT peripheral) +``` + +### Telemetry Format (already sent by FC at 50Hz) +``` +T:12.3,P:45,L:100,R:-80,S:3\n + ^-- State byte: 0=disarmed, 1=arming, 2=armed, 3=fault +``` +ESP32-C3 parses the `S:` field and `L:/R:` for turn detection. + +### LED Patterns +| State | Pattern | Color | +|-------|---------|-------| +| Disarmed | Slow breathe | White | +| Arming | Fast blink | Yellow | +| Armed idle | Solid | Green | +| Turning left | Sweep left | Orange | +| Turning right | Sweep right | Orange | +| Braking | Flash rear | Red | +| Fault | Triple flash | Red | +| RC signal lost | Alternating flash | Red/Blue | + +### Turn/Brake Detection (on ESP32-C3) +``` +if (L - R > threshold) → turning right +if (R - L > threshold) → turning left +if (L < -threshold && R < -threshold) → braking +``` + +### Wiring +``` +FC UART1 TX pin ──→ ESP32-C3 GPIO RX (e.g. GPIO20) +ESP32-C3 GPIO8 ──→ WS2812B data in +ESC 5V BEC ──→ ESP32-C3 5V + WS2812B 5V +GND ──→ Common ground +``` + +### Dev Tools +- **Flashing:** STM32CubeProgrammer via USB (DFU mode) or SWD +- **IDE:** PlatformIO + STM32 HAL, or STM32CubeIDE +- **Debug:** SWD via ST-Link (or use FC's USB as virtual COM for printf debug) + +## Physical Design + +### Frame: Vertical Tower +``` + SIDE VIEW FRONT VIEW + + ┌───────────┐ ┌─────────────────┐ + │ RPLIDAR │ ~500mm │ RPLIDAR │ + ├───────────┤ ├─────────────────┤ + │ RealSense │ ~400mm │ [RealSense] │ + ├───────────┤ ├─────────────────┤ + │ Jetson │ ~300mm │ [Jetson] │ + ├───────────┤ ├─────────────────┤ + │ Drone FC │ ~200mm │ [Drone FC] │ + ├───────────┤ ├─────────────────┤ + │ Battery │ ~100mm │ [Battery] │ + │ + ESC │ LOW! │ [ESC+DCDC] │ + ├─────┬─────┤ ├──┬──────────┬───┤ + │ │ │ │ │ │ │ + ─┘ └─────┘─ ─┘ 8" 8" └──┘─ + ═══════════════ ═══ ═══ + GROUND L R +``` + +### Key Dimensions +- **Height:** ~500-550mm total (sensor tower top) +- **Width:** ~350mm (axle to axle, constrained by motors) +- **Depth:** ~150-200mm (thin profile for doorways) +- **Weight target:** <10kg including battery +- **Center of gravity:** AS LOW AS POSSIBLE — battery + ESC at bottom + +### Critical: Center of Mass +- Battery is the heaviest component → mount at axle height or below +- Jetson + sensors are light → can go higher +- Lower CoG = easier to balance, less aggressive PID needed +- If CoG is too high → oscillations, falls easily + +### Frame Material +- **Main spine:** Aluminum extrusion 2020, vertical +- **Motor mount plate:** 3D printed PETG, 6mm thick, reinforced +- **Component shelves:** 3D printed PETG, bolt to spine +- **Fender/bumper:** 3D printed TPU (flexible, absorbs falls) + +### 3D Printed Parts +| Part | Size (mm) | Material | Qty | +|------|-----------|----------|-----| +| Motor mount plate | 350×150×6 | PETG 80% | 1 | +| Battery shelf | 200×100×40 | PETG 60% | 1 | +| ESC mount | 150×100×15 | PETG 40% | 1 | +| Jetson shelf | 120×100×15 | PETG 40% | 1 | +| Sensor tower top | 120×120×10 | ASA 80% | 1 | +| LIDAR standoff | Ø80×80 | ASA 40% | 1 | +| RealSense bracket | 100×50×40 | PETG 60% | 1 | +| FC mount (vibration isolated) | 30×30×15 | TPU+PETG | 1 | +| Bumper front | 350×50×30 | TPU 30% | 1 | +| Bumper rear | 350×50×30 | TPU 30% | 1 | +| Handle (for carrying) | 150×30×30 | PETG 80% | 1 | +| Kill switch mount | 60×60×40 | PETG 80% | 1 | +| Tether anchor point | 50×50×20 | PETG 100% | 1 | +| LED diffuser ring | Ø120×15 | Clear PETG 30% | 1 | +| ESP32-C3 mount | 30×25×10 | PETG 40% | 1 | + +## Software Stack + +### Jetson Nano +- **OS:** JetPack 4.6.1 (Ubuntu 18.04) +- **ROS2 Humble** (or Foxy) for: + - `nav2` — navigation stack + - `slam_toolbox` — 2D SLAM from LIDAR + - `realsense-ros` — depth camera + - `rplidar_ros` — LIDAR driver +- **Person following:** SSD-MobileNet-v2 via TensorRT (~20 FPS) +- **Balance commands:** ROS topic → UART bridge to drone FC + +### Modes +1. **Idle** — self-balancing in place, waiting for command +2. **RC** — manual control via ELRS radio (primary testing mode) +3. **Follow** — tracks person with RealSense, follows at set distance +4. **Explore** — autonomous SLAM mapping, builds house map +5. **Patrol** — follows waypoints on saved map +6. **Dock** — returns to charging station (future) + +**Mode priority:** RC override always wins. If radio sends stick input, it overrides Jetson commands. Kill switch overrides everything. + +## Build Order + +### Phase 1: Balance (Week 1) +**Safety first — no motor spins without kill switch + tether in place.** +- [ ] Install hardware kill switch inline with 36V battery (NC — press to kill) +- [ ] Set up ceiling tether point above test area (rated for >15kg) +- [ ] Clear test area: 3m radius, no loose items, shoes on +- [ ] Set up PlatformIO project for STM32F745 (STM32 HAL) +- [ ] Write MPU-6000 SPI driver (read gyro+accel, complementary filter) +- [ ] Write PID balance loop with ALL safety checks: + - ±25° tilt cutoff → disarm, require manual re-arm + - Watchdog timer (50ms hardware WDT) + - Speed limit at 10% (max_speed_limit = 100) + - Arming sequence (3s hold while upright) +- [ ] Write hoverboard ESC UART output (speed+steer protocol) +- [ ] Flash firmware via USB DFU (boot0 jumper on FC) +- [ ] Write ELRS CRSF receiver driver (UART3, parse channels + arm switch) +- [ ] Bind ELRS TX ↔ RX, verify channel data on serial monitor +- [ ] Map radio: CH1=steer, CH2=speed, CH5=arm/disarm switch +- [ ] **Bench test first** — FC powered but ESC disconnected, verify IMU reads + PID output + RC channels on serial monitor +- [ ] Wire FC UART2 → hoverboard ESC UART +- [ ] Build minimal frame: motor plate + battery + ESC + FC +- [ ] Power FC from ESC 5V BEC +- [ ] **First balance test — TETHERED, kill switch in hand, 10% speed limit** +- [ ] Tune PID at 10% speed until stable tethered for 5+ minutes +- [ ] Gradually increase speed limit (10% increments, 5 min stable each) + +### Phase 2: Brain (Week 2) +- [ ] Mount Jetson + power (DC-DC 5V) +- [ ] Set up JetPack + ROS2 +- [ ] Add Jetson UART RX to FC firmware (receive speed+steer commands) +- [ ] Wire Jetson UART1 → FC UART1 +- [ ] Python serial bridge: send speed+steer, read telemetry +- [ ] Test: keyboard teleoperation while balancing + +### Phase 3: Senses (Week 3) +- [ ] Mount RealSense + RPLIDAR +- [ ] SLAM mapping of a room +- [ ] Person detection + tracking (SSD-MobileNet-v2 via TensorRT) +- [ ] Follow mode: maintain 1.5m distance from person + +### Phase 4: Polish (Week 4) +- [ ] Print proper enclosures, bumpers, diffuser ring +- [ ] Wire ESP32-C3 to FC telemetry TX line (listen-only tap) +- [ ] Flash ESP32-C3: parse telemetry, drive WS2812B via RMT +- [ ] Mount LED strip around frame with diffuser +- [ ] Test all LED patterns: disarmed/arming/armed/turning/fault +- [ ] Speaker for audio feedback +- [ ] WiFi status dashboard (ESP32-C3 can serve this too) +- [ ] Emergency stop button diff --git a/docs/board-viz.html b/docs/board-viz.html new file mode 100644 index 0000000..f75c237 --- /dev/null +++ b/docs/board-viz.html @@ -0,0 +1,275 @@ + + + + +GEPRC GEP-F722-45A AIO — Board Layout + + + +

🤖 GEPRC GEP-F722-45A AIO — SaltyLab Pinout

+

STM32F722RET6 + ICM-42688-P | Betaflight target: GEPR-GEPRC_F722_AIO

+ +
+
+
+ +
+
+
+
+ + +
STM32
F722RET6
216MHz
+ + +
ICM
42688
+
+ + +
+
LED PC4
+ + +
+
BOOT 🟡
+ + +
+
USB-C (DFU)
+ + +
+
T
+ T1
PA9
+
+
+
R
+ R1
PA10
+
+
+
T
+ T3
PB10
+
+
+
R
+ R3
PB11
+
+ + +
+ T2
PA2
+
T
+
+
+ R2
PA3
+
R
+
+ + +
+
T
+ T4
PC10
+
+
+
R
+ R4
PC11
+
+
+ T5
PC12
+
T
+
+
+ R5
PD2
+
R
+
+ + +
+
M1-M4 (unused)
PC6-PC9
+
+ + +
+
X →
board right
+
+
Y ↓ (board forward = tilt axis)
+
+
+ +
+

🔌 UART Assignments

+
+
+ USART1 T1/R1 → Jetson Nano +
+
+
+ USART2 T2 → Hoverboard ESC (TX only) +
+
+
+ I2C2 T3/R3 → Baro/Mag (reserved) +
+
+
+ UART4 T4/R4 → ELRS RX (CRSF) +
+
+
+ UART5 T5/R5 → Debug/spare +
+ +
+

📡 SPI Bus

+
+ SPI1: PA5/PA6/PA7 → IMU (CS: PA15) +
+
+ SPI2: PB13-15 → OSD MAX7456 +
+
+ SPI3: PB3-5 → Flash W25Q128 +
+
+ +
+

⚡ Other

+
+ 🔵 LED: PC4 | 📢 Beeper: PC15 +
+
+ 🔋 VBAT: PC2 | ⚡ Current: PC1 +
+
+ 💡 LED Strip: PA1 (WS2812) +
+
+ 📍 EXTI (IMU data-ready): PA8 +
+
+
+
+ +
+

🧭 IMU Orientation (CW90° from chip to board)

+
+
Board Forward (tilt for balance) = Chip's +Y axis
+
Board Right = Chip's -X axis
+
Board Pitch Rate = -Gyro X (raw)
+
Board Accel Forward = Accel Y (raw)
+
+
+ +

+ ⚠️ Pad positions are approximate — check the physical board silkscreen for exact locations. + The CW90 rotation is handled in firmware (mpu6000.c). USB-C at bottom edge for DFU flashing. +

+ + +