chore: merge CAD files and design docs from seb/saltylab seed repo
Consolidating seb/saltylab into saltylab-firmware before deleting the seed repo. - 16 OpenSCAD CAD models → cad/ - Design docs (SALTYLAB.md, PLATFORM.md, AGENTS.md, board-viz.html) → docs/
This commit is contained in:
parent
6d316514da
commit
02217443ea
118
cad/assembly.scad
Normal file
118
cad/assembly.scad
Normal file
@ -0,0 +1,118 @@
|
||||
// ============================================
|
||||
// SaltyLab — Full Assembly Visualization
|
||||
// Shows all parts in position on 2020 spine
|
||||
// ============================================
|
||||
include <dimensions.scad>
|
||||
|
||||
// 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"));
|
||||
77
cad/battery_shelf.scad
Normal file
77
cad/battery_shelf.scad
Normal file
@ -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 <dimensions.scad>
|
||||
|
||||
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();
|
||||
75
cad/bumper.scad
Normal file
75
cad/bumper.scad
Normal file
@ -0,0 +1,75 @@
|
||||
// ============================================
|
||||
// SaltyLab — Bumper (Front/Rear)
|
||||
// 350×50×30mm TPU
|
||||
// Absorbs falls, protects frame and floor
|
||||
// ============================================
|
||||
include <dimensions.scad>
|
||||
|
||||
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();
|
||||
73
cad/dimensions.scad
Normal file
73
cad/dimensions.scad
Normal file
@ -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
|
||||
70
cad/esc_mount.scad
Normal file
70
cad/esc_mount.scad
Normal file
@ -0,0 +1,70 @@
|
||||
// ============================================
|
||||
// SaltyLab — ESC Mount
|
||||
// 150×100×15mm PETG
|
||||
// Hoverboard ESC, mounts to 2020 extrusion
|
||||
// ============================================
|
||||
include <dimensions.scad>
|
||||
|
||||
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();
|
||||
57
cad/esp32c3_mount.scad
Normal file
57
cad/esp32c3_mount.scad
Normal file
@ -0,0 +1,57 @@
|
||||
// ============================================
|
||||
// SaltyLab — ESP32-C3 Mount
|
||||
// 30×25×10mm PETG
|
||||
// Tiny mount for LED controller MCU
|
||||
// ============================================
|
||||
include <dimensions.scad>
|
||||
|
||||
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();
|
||||
86
cad/fc_mount.scad
Normal file
86
cad/fc_mount.scad
Normal file
@ -0,0 +1,86 @@
|
||||
// ============================================
|
||||
// SaltyLab — Flight Controller Mount
|
||||
// Vibration-isolated, 30.5mm pattern
|
||||
// TPU dampers + PETG frame
|
||||
// ============================================
|
||||
include <dimensions.scad>
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
59
cad/handle.scad
Normal file
59
cad/handle.scad
Normal file
@ -0,0 +1,59 @@
|
||||
// ============================================
|
||||
// SaltyLab — Carry Handle
|
||||
// 150×30×30mm PETG
|
||||
// Comfortable grip, mounts on top of spine
|
||||
// ============================================
|
||||
include <dimensions.scad>
|
||||
|
||||
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();
|
||||
69
cad/jetson_shelf.scad
Normal file
69
cad/jetson_shelf.scad
Normal file
@ -0,0 +1,69 @@
|
||||
// ============================================
|
||||
// SaltyLab — Jetson Nano Shelf
|
||||
// 120×100×15mm PETG
|
||||
// Mounts Jetson Nano to 2020 extrusion
|
||||
// ============================================
|
||||
include <dimensions.scad>
|
||||
|
||||
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();
|
||||
56
cad/kill_switch_mount.scad
Normal file
56
cad/kill_switch_mount.scad
Normal file
@ -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 <dimensions.scad>
|
||||
|
||||
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();
|
||||
53
cad/led_diffuser_ring.scad
Normal file
53
cad/led_diffuser_ring.scad
Normal file
@ -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 <dimensions.scad>
|
||||
|
||||
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();
|
||||
61
cad/lidar_standoff.scad
Normal file
61
cad/lidar_standoff.scad
Normal file
@ -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 <dimensions.scad>
|
||||
|
||||
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();
|
||||
94
cad/motor_mount_plate.scad
Normal file
94
cad/motor_mount_plate.scad
Normal file
@ -0,0 +1,94 @@
|
||||
// ============================================
|
||||
// SaltyLab — Motor Mount Plate
|
||||
// 350×150×6mm PETG
|
||||
// Mounts both 8" hub motors + 2020 extrusion spine
|
||||
// ============================================
|
||||
include <dimensions.scad>
|
||||
|
||||
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();
|
||||
64
cad/realsense_bracket.scad
Normal file
64
cad/realsense_bracket.scad
Normal file
@ -0,0 +1,64 @@
|
||||
// ============================================
|
||||
// SaltyLab — RealSense D435i Bracket
|
||||
// 100×50×40mm PETG
|
||||
// Adjustable tilt mount on 2020 extrusion
|
||||
// ============================================
|
||||
include <dimensions.scad>
|
||||
|
||||
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();
|
||||
58
cad/sensor_tower_top.scad
Normal file
58
cad/sensor_tower_top.scad
Normal file
@ -0,0 +1,58 @@
|
||||
// ============================================
|
||||
// SaltyLab — Sensor Tower Top
|
||||
// 120×120×10mm ASA
|
||||
// Mounts RPLIDAR A1 on top of 2020 spine
|
||||
// ============================================
|
||||
include <dimensions.scad>
|
||||
|
||||
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();
|
||||
46
cad/tether_anchor.scad
Normal file
46
cad/tether_anchor.scad
Normal file
@ -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 <dimensions.scad>
|
||||
|
||||
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();
|
||||
281
docs/AGENTS.md
Normal file
281
docs/AGENTS.md
Normal file
@ -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)
|
||||
222
docs/PLATFORM.md
Normal file
222
docs/PLATFORM.md
Normal file
@ -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
|
||||
454
docs/SALTYLAB-DETAILED.md
Normal file
454
docs/SALTYLAB-DETAILED.md
Normal file
@ -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*
|
||||
420
docs/SALTYLAB.md
Normal file
420
docs/SALTYLAB.md
Normal file
@ -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
|
||||
275
docs/board-viz.html
Normal file
275
docs/board-viz.html
Normal file
@ -0,0 +1,275 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>GEPRC GEP-F722-45A AIO — Board Layout</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { background: #1a1a2e; color: #eee; font-family: 'Courier New', monospace; display: flex; flex-direction: column; align-items: center; padding: 20px; }
|
||||
h1 { color: #e94560; margin-bottom: 5px; font-size: 1.4em; }
|
||||
.subtitle { color: #888; margin-bottom: 20px; font-size: 0.85em; }
|
||||
.container { display: flex; gap: 30px; align-items: flex-start; flex-wrap: wrap; justify-content: center; }
|
||||
.board-wrap { position: relative; }
|
||||
.board { width: 400px; height: 340px; background: #1a472a; border: 3px solid #333; border-radius: 8px; position: relative; box-shadow: 0 0 20px rgba(0,0,0,0.5); }
|
||||
.board::before { content: 'GEPRC GEP-F722-45A AIO'; position: absolute; top: 8px; left: 50%; transform: translateX(-50%); color: #fff3; font-size: 10px; letter-spacing: 2px; }
|
||||
|
||||
/* Mounting holes */
|
||||
.mount { width: 10px; height: 10px; background: #111; border: 2px solid #555; border-radius: 50%; position: absolute; }
|
||||
.mount.tl { top: 15px; left: 15px; }
|
||||
.mount.tr { top: 15px; right: 15px; }
|
||||
.mount.bl { bottom: 15px; left: 15px; }
|
||||
.mount.br { bottom: 15px; right: 15px; }
|
||||
|
||||
/* MCU */
|
||||
.mcu { width: 80px; height: 80px; background: #222; border: 1px solid #555; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); display: flex; align-items: center; justify-content: center; font-size: 9px; color: #aaa; text-align: center; line-height: 1.3; }
|
||||
.mcu .dot { width: 5px; height: 5px; background: #666; border-radius: 50%; position: absolute; top: 4px; left: 4px; }
|
||||
|
||||
/* IMU */
|
||||
.imu { width: 32px; height: 32px; background: #333; border: 1px solid #e94560; position: absolute; top: 85px; left: 60px; display: flex; align-items: center; justify-content: center; font-size: 7px; color: #e94560; }
|
||||
.imu::after { content: 'CW90°'; position: absolute; bottom: -14px; color: #e94560; font-size: 8px; white-space: nowrap; }
|
||||
|
||||
/* Arrow showing CW90 rotation */
|
||||
.rotation-arrow { position: absolute; top: 72px; left: 55px; color: #e94560; font-size: 18px; }
|
||||
|
||||
/* Pads */
|
||||
.pad { position: absolute; display: flex; align-items: center; gap: 4px; font-size: 10px; cursor: pointer; }
|
||||
.pad .dot { width: 12px; height: 12px; border-radius: 50%; border: 2px solid; display: flex; align-items: center; justify-content: center; font-size: 7px; font-weight: bold; }
|
||||
.pad:hover .label { color: #fff; }
|
||||
.pad .label { transition: color 0.2s; }
|
||||
.pad .sublabel { font-size: 8px; color: #888; }
|
||||
|
||||
/* UART colors */
|
||||
.uart1 .dot { background: #2196F3; border-color: #64B5F6; }
|
||||
.uart2 .dot { background: #FF9800; border-color: #FFB74D; }
|
||||
.uart3 .dot { background: #9C27B0; border-color: #CE93D8; }
|
||||
.uart4 .dot { background: #4CAF50; border-color: #81C784; }
|
||||
.uart5 .dot { background: #F44336; border-color: #EF9A9A; }
|
||||
|
||||
/* Component dots */
|
||||
.comp { position: absolute; font-size: 9px; display: flex; align-items: center; gap: 4px; }
|
||||
.comp .icon { width: 10px; height: 10px; border-radius: 2px; }
|
||||
|
||||
/* LED */
|
||||
.led-blue { position: absolute; width: 8px; height: 8px; background: #2196F3; border-radius: 50%; box-shadow: 0 0 8px #2196F3; top: 45px; right: 50px; }
|
||||
.led-label { position: absolute; top: 36px; right: 30px; font-size: 8px; color: #64B5F6; }
|
||||
|
||||
/* Boot button */
|
||||
.boot-btn { position: absolute; width: 16px; height: 10px; background: #b8860b; border: 1px solid #daa520; border-radius: 2px; bottom: 45px; right: 40px; }
|
||||
.boot-label { position: absolute; bottom: 32px; right: 30px; font-size: 8px; color: #daa520; }
|
||||
|
||||
/* USB */
|
||||
.usb { position: absolute; width: 30px; height: 14px; background: #444; border: 2px solid #777; border-radius: 3px; bottom: -3px; left: 50%; transform: translateX(-50%); }
|
||||
.usb-label { position: absolute; bottom: 14px; left: 50%; transform: translateX(-50%); font-size: 8px; color: #999; }
|
||||
|
||||
/* Connector pads along edges */
|
||||
/* Bottom row: T1 R1 T3 R3 */
|
||||
.pad-t1 { bottom: 20px; left: 40px; }
|
||||
.pad-r1 { bottom: 20px; left: 80px; }
|
||||
.pad-t3 { bottom: 20px; left: 140px; }
|
||||
.pad-r3 { bottom: 20px; left: 180px; }
|
||||
|
||||
/* Right side: T2 R2 */
|
||||
.pad-t2 { right: 20px; top: 80px; flex-direction: row-reverse; }
|
||||
.pad-r2 { right: 20px; top: 110px; flex-direction: row-reverse; }
|
||||
|
||||
/* Top row: T4 R4 T5 R5 */
|
||||
.pad-t4 { top: 30px; left: 40px; }
|
||||
.pad-r4 { top: 30px; left: 80px; }
|
||||
.pad-t5 { top: 30px; right: 100px; flex-direction: row-reverse; }
|
||||
.pad-r5 { top: 30px; right: 55px; flex-direction: row-reverse; }
|
||||
|
||||
/* ESC pads (motor outputs - not used) */
|
||||
.esc-pads { position: absolute; left: 20px; top: 140px; }
|
||||
.esc-pads .esc-label { font-size: 8px; color: #555; }
|
||||
|
||||
/* Legend */
|
||||
.legend { background: #16213e; padding: 15px 20px; border-radius: 8px; min-width: 280px; }
|
||||
.legend h2 { color: #e94560; font-size: 1.1em; margin-bottom: 10px; border-bottom: 1px solid #333; padding-bottom: 5px; }
|
||||
.legend-item { display: flex; align-items: center; gap: 8px; margin: 6px 0; font-size: 12px; }
|
||||
.legend-item .swatch { width: 14px; height: 14px; border-radius: 50%; flex-shrink: 0; }
|
||||
.legend-item .arrow { color: #888; font-size: 10px; }
|
||||
.legend-section { margin-top: 12px; padding-top: 8px; border-top: 1px solid #333; }
|
||||
.legend-section h3 { font-size: 0.9em; color: #888; margin-bottom: 6px; }
|
||||
|
||||
/* Orientation guide */
|
||||
.orient { margin-top: 20px; background: #16213e; padding: 15px 20px; border-radius: 8px; width: 100%; max-width: 710px; }
|
||||
.orient h2 { color: #4CAF50; font-size: 1.1em; margin-bottom: 10px; }
|
||||
.orient-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||
.orient-item { font-size: 12px; padding: 6px 10px; background: #1a1a2e; border-radius: 4px; }
|
||||
.orient-item .dir { color: #4CAF50; font-weight: bold; }
|
||||
|
||||
/* Axis overlay */
|
||||
.axis { position: absolute; }
|
||||
.axis-x { top: 50%; right: -60px; color: #F44336; font-size: 12px; font-weight: bold; }
|
||||
.axis-y { bottom: -30px; left: 50%; transform: translateX(-50%); color: #4CAF50; font-size: 12px; font-weight: bold; }
|
||||
.axis-arrow-x { position: absolute; top: 50%; right: -45px; transform: translateY(-50%); width: 30px; height: 2px; background: #F44336; }
|
||||
.axis-arrow-x::after { content: '▶'; position: absolute; right: -12px; top: -8px; color: #F44336; }
|
||||
.axis-arrow-y { position: absolute; bottom: -20px; left: 50%; transform: translateX(-50%); width: 2px; height: 20px; background: #4CAF50; }
|
||||
.axis-arrow-y::after { content: '▼'; position: absolute; bottom: -14px; left: -5px; color: #4CAF50; }
|
||||
|
||||
.note { margin-top: 15px; color: #888; font-size: 11px; text-align: center; max-width: 710px; }
|
||||
.note em { color: #e94560; font-style: normal; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🤖 GEPRC GEP-F722-45A AIO — SaltyLab Pinout</h1>
|
||||
<p class="subtitle">STM32F722RET6 + ICM-42688-P | Betaflight target: GEPR-GEPRC_F722_AIO</p>
|
||||
|
||||
<div class="container">
|
||||
<div class="board-wrap">
|
||||
<div class="board">
|
||||
<!-- Mounting holes -->
|
||||
<div class="mount tl"></div>
|
||||
<div class="mount tr"></div>
|
||||
<div class="mount bl"></div>
|
||||
<div class="mount br"></div>
|
||||
|
||||
<!-- MCU -->
|
||||
<div class="mcu"><div class="dot"></div>STM32<br>F722RET6<br>216MHz</div>
|
||||
|
||||
<!-- IMU -->
|
||||
<div class="imu">ICM<br>42688</div>
|
||||
<div class="rotation-arrow">↻</div>
|
||||
|
||||
<!-- LED -->
|
||||
<div class="led-blue"></div>
|
||||
<div class="led-label">LED PC4</div>
|
||||
|
||||
<!-- Boot button -->
|
||||
<div class="boot-btn"></div>
|
||||
<div class="boot-label">BOOT 🟡</div>
|
||||
|
||||
<!-- USB -->
|
||||
<div class="usb"></div>
|
||||
<div class="usb-label">USB-C (DFU)</div>
|
||||
|
||||
<!-- UART Pads - Bottom -->
|
||||
<div class="pad pad-t1 uart1">
|
||||
<div class="dot">T</div>
|
||||
<span class="label">T1<br><span class="sublabel">PA9</span></span>
|
||||
</div>
|
||||
<div class="pad pad-r1 uart1">
|
||||
<div class="dot">R</div>
|
||||
<span class="label">R1<br><span class="sublabel">PA10</span></span>
|
||||
</div>
|
||||
<div class="pad pad-t3 uart3">
|
||||
<div class="dot">T</div>
|
||||
<span class="label">T3<br><span class="sublabel">PB10</span></span>
|
||||
</div>
|
||||
<div class="pad pad-r3 uart3">
|
||||
<div class="dot">R</div>
|
||||
<span class="label">R3<br><span class="sublabel">PB11</span></span>
|
||||
</div>
|
||||
|
||||
<!-- UART Pads - Right -->
|
||||
<div class="pad pad-t2 uart2">
|
||||
<span class="label">T2<br><span class="sublabel">PA2</span></span>
|
||||
<div class="dot">T</div>
|
||||
</div>
|
||||
<div class="pad pad-r2 uart2">
|
||||
<span class="label">R2<br><span class="sublabel">PA3</span></span>
|
||||
<div class="dot">R</div>
|
||||
</div>
|
||||
|
||||
<!-- UART Pads - Top -->
|
||||
<div class="pad pad-t4 uart4">
|
||||
<div class="dot">T</div>
|
||||
<span class="label">T4<br><span class="sublabel">PC10</span></span>
|
||||
</div>
|
||||
<div class="pad pad-r4 uart4">
|
||||
<div class="dot">R</div>
|
||||
<span class="label">R4<br><span class="sublabel">PC11</span></span>
|
||||
</div>
|
||||
<div class="pad pad-t5 uart5">
|
||||
<span class="label">T5<br><span class="sublabel">PC12</span></span>
|
||||
<div class="dot">T</div>
|
||||
</div>
|
||||
<div class="pad pad-r5 uart5">
|
||||
<span class="label">R5<br><span class="sublabel">PD2</span></span>
|
||||
<div class="dot">R</div>
|
||||
</div>
|
||||
|
||||
<!-- ESC motor pads label -->
|
||||
<div class="esc-pads">
|
||||
<div class="esc-label">M1-M4 (unused)<br>PC6-PC9</div>
|
||||
</div>
|
||||
|
||||
<!-- Board axes -->
|
||||
<div class="axis-arrow-x"></div>
|
||||
<div class="axis axis-x">X →<br><span style="font-size:9px;color:#888">board right</span></div>
|
||||
<div class="axis-arrow-y"></div>
|
||||
<div class="axis axis-y">Y ↓ (board forward = tilt axis)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legend">
|
||||
<h2>🔌 UART Assignments</h2>
|
||||
<div class="legend-item">
|
||||
<div class="swatch" style="background:#2196F3"></div>
|
||||
<span><b>USART1</b> T1/R1 → Jetson Nano</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="swatch" style="background:#FF9800"></div>
|
||||
<span><b>USART2</b> T2 → Hoverboard ESC (TX only)</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="swatch" style="background:#9C27B0"></div>
|
||||
<span><b>I2C2</b> T3/R3 → Baro/Mag (reserved)</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="swatch" style="background:#4CAF50"></div>
|
||||
<span><b>UART4</b> T4/R4 → ELRS RX (CRSF)</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="swatch" style="background:#F44336"></div>
|
||||
<span><b>UART5</b> T5/R5 → Debug/spare</span>
|
||||
</div>
|
||||
|
||||
<div class="legend-section">
|
||||
<h3>📡 SPI Bus</h3>
|
||||
<div class="legend-item">
|
||||
<span>SPI1: PA5/PA6/PA7 → IMU (CS: <em style="color:#e94560">PA15</em>)</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span>SPI2: PB13-15 → OSD MAX7456</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span>SPI3: PB3-5 → Flash W25Q128</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legend-section">
|
||||
<h3>⚡ Other</h3>
|
||||
<div class="legend-item">
|
||||
<span>🔵 LED: PC4 | 📢 Beeper: PC15</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span>🔋 VBAT: PC2 | ⚡ Current: PC1</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span>💡 LED Strip: PA1 (WS2812)</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span>📍 EXTI (IMU data-ready): PA8</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="orient">
|
||||
<h2>🧭 IMU Orientation (CW90° from chip to board)</h2>
|
||||
<div class="orient-grid">
|
||||
<div class="orient-item"><span class="dir">Board Forward</span> (tilt for balance) = Chip's +Y axis</div>
|
||||
<div class="orient-item"><span class="dir">Board Right</span> = Chip's -X axis</div>
|
||||
<div class="orient-item"><span class="dir">Board Pitch Rate</span> = -Gyro X (raw)</div>
|
||||
<div class="orient-item"><span class="dir">Board Accel Forward</span> = Accel Y (raw)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="note">
|
||||
⚠️ Pad positions are <em>approximate</em> — 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.
|
||||
</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user