Skip to content

servo per-layer calibration gating + chute aiming geometry model & calibration UI #155

Merged
spencerhhubert merged 11 commits into
mainfrom
spencer/servo-layer-calibration-shared
May 30, 2026
Merged

servo per-layer calibration gating + chute aiming geometry model & calibration UI #155
spencerhhubert merged 11 commits into
mainfrom
spencer/servo-layer-calibration-shared

Conversation

@spencerhhubert

Copy link
Copy Markdown
Contributor

Stacked on the now-merged StallGuard branch (#150); the diff against main is just the servo/chute calibration work below.

Servo per-layer calibration gating

PCA9685 servos now require explicit per-layer calibration before they can move. Layers stay uncalibrated (angles=None) and no-op until both open/closed angles are locked in via the UI. Uncalibrated layers are skipped during sorting and never moved on boot. Adds the shared ServoLayerCalibrator component used by both the storage-layers settings page and the setup onboarding wizard.

Chute aiming calibration: geometry model + calibration UI (WIP)

The chute rotates to point a falling piece at the correct bin in the tower. The old aiming math used ad-hoc constants (first_bin_center + pillar_width_deg, section count hardcoded at 60°) that predate the current hardware (moved home switch, all-bins-reachable geometry) and did not generalize across layouts with different bin counts.

Replaced with a clean, bin-count-independent geometry:

bin_center = theta0 + section*(360/N) + (i + 0.5)*(W / K)

calibrated from three invariants: num_sections (N), section_width_deg (W), first_section_offset_deg (theta0). Bins are equal slots aimed at their midpoints, so 1/2/3/5-bin layers all center correctly from one calibration.

Key changes

  • chute.py — rewritten to the theta0/W/N midpoint model; angleForVirtualBin() is the single source of truth. Legacy first_bin_center/pillar_width_deg kept as derived props so chute_stress + the old /settings/chute page still work.
  • parse_user_toml.pyChuteCalibrationConfig gains the canonical keys; loader prefers them and derives from legacy keys when absent (migration).
  • hardware.py — endpoints to save geometry, derive theta0/W from a measured first→last bin span, jog to an absolute angle, test-aim at a virtual bin, and a reachability check across the installed layout.
  • local_state.py — new chute_calibrations table + CRUD. Each calibration/manual save is stored as an instance; the active instance's geometry is what gets written to the TOML. Supports a history list + "lock in" to re-activate.
  • frontend /settings/chute-aiming — read-only parameters (edit behind hover), a gated calibration wizard (home → jog to first bin → jog to last bin + bin count → derive & lock in), an arrow-key jog pad (up/down fine 0.25°, left/right coarse 2°), a calibration history list, and a verify-aim circle.

Status

WIP, lightly tested. Backend unit tests pass (test_chute_aiming.py) and endpoints/routes were smoke-checked on dark-brown-axle, but chute calibration has not been run on real hardware (no motor verification, no real first→last measurement yet). Open question: whether the jog + test-aim moves use normal default acceleration.

Also folds in a few unrelated working-tree default flips outstanding on this shared branch (classification channel + feeder mode defaults, machine_setup default).

🤖 Generated with Claude Code

spencerhhubert and others added 11 commits May 29, 2026 16:35
When a Pico is already in bootloader mode, the block device appears
(/dev/sda1, label RPI-RP2) but may not be auto-mounted. The previous
code only checked mounted paths, so it fell through to serial scanning
and bailed with "No Pico boards found over USB". Now checks the block
device before scanning serial ports.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Telemetry DB (stepper_telemetry.py): runs + samples tables in local_state.sqlite,
  self-creating, with downsampling-on-read and a retention prune.
- StallGuard sweep endpoint now records each run + full per-sample context and
  returns run_id; added loaded/label params (loaded -> stall_test).
- Query router (server/routers/telemetry.py): list/summary/run/samples/delete.
- Frontend page (settings/stepper-stallguard) + StallGuardChart.svelte: per-motor
  summary, run list, canvas SG load-curve chart, sweep controls, save-SGTHRS-to-TOML.
- Firmware (sorter_interface_firmware.cpp): leave stepper nEN HIGH at boot, enable
  per-channel on first move / explicit enable so motors don't hold at boot.
  NOTE: this file also carries pre-existing GET_VERSION work, entangled in the
  same working-tree edit; included here since it can't be split non-interactively.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Prevents machine-specific config from being swept into commits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TMC_UART_BUS_COUNT was a `const uint8_t`, but it gates `#if
TMC_UART_BUS_COUNT > 1` blocks that construct and route the second TMC
UART bus. The preprocessor cannot see a C++ const (it evaluates as 0),
so `#if 0 > 1` was false and the entire second bus was compiled out on
v1-2 — every channel silently fell back to bus 0. Make it a #define so
the conditional sees the real count. v1-1 and skr stay at 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bake git describe, short commit, UTC build time, and the hw/role
variant into the firmware at configure time and expose them through the
already-present GET_VERSION command handler. Add the matching host-side
MCUDevice.get_version() so the backend can read what is actually
flashed on each board.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…fix DRV_STATUS CPU spike

TMC2209 chips can reset (motor power rail cycling after Pico boot) and
return to hardware defaults: I_SCALE_ANALOG=1 (current from VREF, not
IRUN) and MSTEP_REG_SELECT=0 (microsteps from MS pins, not CHOPCONF).
Backend now writes GCONF=0x1C0 (PD_DISABLE | MSTEP_REG_SELECT |
MULTISTEP_FILT) at the top of each stepper's init sequence, before
applying microsteps/current from machine.toml.

Also remove continuous DRV_STATUS refresh from the 500ms StepperSidebar
poll — it was reading all 15 TMC registers over USB serial every 500ms
whenever the driver panel was open, causing ~500% CPU on the Pi. The
panel still refreshes on open and after saving settings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
UART bus1 (uart1, ch4) is not currently working, so the chute stepper
was unreachable over UART. Move it to bus0 where UART is confirmed
functional. c_channel_2_rotor takes the lone bus1 slot (ch4) since it
is less dependent on UART control right now.

New channel order on bus0: chute(ch0), c1_rotor(ch1), c3_rotor(ch2),
carousel(ch3). c2_rotor on bus1 ch4.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PCA9685 servos require explicit per-layer calibration before moving.
Servos stay uncalibrated (angles=None) and no-op until both open/closed
angles are locked in via the UI; uncalibrated layers are skipped during
sorting and never moved on boot. Adds the shared ServoLayerCalibrator
component used by the storage-layers settings page and setup wizard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ly tested)

What this is for
----------------
The chute rotates to point a falling piece at the correct bin in the bin
tower. Previously the aiming math used ad-hoc constants (first_bin_center +
pillar_width_deg, with section count hardcoded at 60deg) that predate the
current hardware (moved home switch, all-bins-reachable geometry) and did NOT
generalize across layouts with different bin counts.

This replaces that with a clean, bin-count-independent geometry:
  bin_center = theta0 + section*(360/N) + (i + 0.5)*(W / K)
calibrated from three invariants: num_sections (N), section_width_deg (W),
first_section_offset_deg (theta0). Bins are equal slots aimed at their
midpoints, so 1/2/3/5-bin layers all center correctly from one calibration.

- chute.py: rewritten to the theta0/W/N midpoint model; angleForVirtualBin()
  is the single source of truth. Legacy first_bin_center/pillar_width_deg kept
  as derived props so chute_stress + the old /settings/chute page still work.
- parse_user_toml.py: ChuteCalibrationConfig gains the canonical keys; loader
  prefers them and derives from legacy keys when absent (migration).
- hardware.py: endpoints to save geometry, derive theta0/W from a measured
  first->last bin span, jog to an absolute angle, and test-aim at a virtual
  bin; plus a reachability check across the installed layout.
- local_state.py: new chute_calibrations table + CRUD. Each calibration/manual
  save is stored as an instance; the active instance's geometry is what gets
  written to the TOML. Supports a history list + "lock in" to re-activate.
- frontend /settings/chute-aiming: parameters shown read-only (edit hidden
  behind hover), a vertical gated calibration wizard (home -> jog to first bin
  -> jog to last bin + bin count -> derive & lock in), a drawn arrow-key jog
  pad (up/down fine 0.25deg, left/right coarse 2deg, real arrow keys bound),
  a calibration history list, and a verify-aim circle.

Status: WIP, only lightly tested. Backend unit tests pass and endpoints/route
were smoke-checked on dark-brown-axle, but the calibration has NOT been run on
real hardware (no motor verification, no real first->last measurement yet).
Open question still being chased: whether the jog + test-aim moves use normal
default acceleration.

Also folds in a few unrelated working-tree default flips that were outstanding
on this shared branch (classification channel + feeder mode defaults,
machine_setup default) per request to commit everything.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve api.py conflict by keeping both telemetry and tailscale routers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented May 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sorter-v2-docs Ready Ready Preview, Comment May 30, 2026 10:16am
sorteros-setup Ready Ready Preview, Comment May 30, 2026 10:16am

@spencerhhubert spencerhhubert changed the title servo per-layer calibration gating + chute aiming geometry model & calibration UI (WIP) servo per-layer calibration gating + chute aiming geometry model & calibration UI May 30, 2026
@spencerhhubert spencerhhubert merged commit 09d510b into main May 30, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant