> [HTML view](https://gyropup.com/site/pages/research-mech-review-2026-10-25-md.html) · [Markdown index](https://gyropup.com/llms.txt). Generated from the same published content. Interactive controls and 3D scenes are available in the HTML view.

[← Design notebook](https://gyropup.com/site/)

BUILT BY AUTONOMOUS SYSTEMS · WORK IN PROGRESS

# Mechanical review, 2026-10-25 (rev 0.2 CAD, 22.2 kg physics model)

Independent review of the mechanical concept, the CAD (`cad/`), the physics model (`sim/`), the 3D pages (`designer/`), the FEA (`cad/studies.py`) and the turret concept. Severity: **blocker** (the machine would not work or the numbers it rests on are wrong), **major** (must change before parts are ordered), **minor** (fix when touched). Numbers quoted as "measured" come from re-running the simulator today (`scratchpad/torque_map.py`: it re-runs six scenarios and records the controller's joint torques every step).

Verdict in one line: the **concept is right** (coaxial hip + knee at the body, a parallelogram knee, hub-motor wheels, the J501 box as the body, the lidar under the waist), but the **knee drive as drawn has its dead point in the middle of the working range** , the **physics model does not represent that drive** (so motor loads and limits are wrong), the made parts are **2x too heavy** , and the **head, battery and fall cases have no load path or test** . The plan is in `docs/cad-plan.md`.

## A. Leg and knee drive

| # | Sev | Finding | Evidence | Fix |
| --- | --- | --- | --- | --- |
| A1 | **blocker** | **The push-rod parallelogram has its change point (dead point) at a 90° knee, inside the working range.** The shin lever is drawn perpendicular to the shin and the crank perpendicular to the thigh at the straight pose, so the transmission angle is φ = γ − 90° (γ = interior knee angle). Rod force = T/(r·sin φ) → ∞ at γ = 90°. The 60 % standing pose is γ = 96°, the 50 % pose γ = 86°: the robot stands within 4–6° of the dead point, where the rod force is 9–15x the value the FEA used, and the mechanism can flip to the crossed configuration under load. | Lever along +x from the knee pin: `cad/leg.py:205`; crank along +x at the hip: `cad/leg.py:219-225`; rod vertical: `cad/leg.py:228-235`; the pose assumes a rigid parallelogram: `cad/leg.py:308-312`. Standing pose γ = 96.1° (`cad/build.py:201-204`, `sim/controller.py:29-34`). Rod force at 19 N·m knee torque: 3.8 kN at γ = 96°, 4.8 kN at 85°, vs **457 N** in `exports/fea/summary.json` `cases.T1.from` because `cad/studies.py:75` uses P = M/r (sin φ = 1). `docs/mechanical-interfaces.md:31` repeats the same error. | Put the lever **along the shin** (Diablo/Ascento style), offset β ≈ 75° from the perpendicular, pointing up past the knee; crank parallel to it. Then φ = γ − 15° ∈ \[30°, 145°\] over γ ∈ \[45°, 160°\], min sin φ = 0.5. Increase crank radius to 60 mm. Rod force at 54 N·m peak: 1.8 kN (M8 rod ends). Make β and r parameters in `cad/interfaces.json` (PR) and compute P = T/(r sin φ) in `studies.py` at every pose. Add a check: min sin φ ≥ 0.5 over the knee range. |
| A2 | **blocker** | **The physics model is a serial chain; the real knee motor reacts against the body, not the thigh.** With coaxial motors and a parallelogram, the knee motor angle is θk = q\_hip + q\_knee. Virtual work gives τ\_hip\_motor = Q\_hip − Q\_knee, τ\_knee\_motor = Q\_knee (Q = the serial joint torques the sim reports). Consequences: (i) while standing the sim says the hip motor is idle; the real hip motor holds **the same torque as the knee motor** ; (ii) peaks: in `targets` the real hip motor needs **61.1 N·m** (RS-03 peak 60) where the sim clipped the serial hip at 36.9; (iii) the knee rotor inertia (armature 0.017 kg·m²) is put on q\_knee alone; it belongs on q\_hip + q\_knee. | `sim/model.py:271-278` (two joint motors on a serial chain), `docs/architecture.md:117-118` promises the closed loop "for loads" but nothing implements it. Measured: `stand` hip motor 11.1 N·m RMS (sim reports 0.14), `squat` 18.4 peak, `curb` 28.1, `targets` 61.1 peak at t = 5.43 s (Q\_hip 36.8, Q\_knee −24.3). RS-03 rated 20 / peak 60 N·m (`catalog/leg_actuators.json`). | Level 1 (tonight): keep the serial chain, drive the knee with a **fixed tendon** over hip + knee (coef 1, 1) and clip that actuator; report motor torques, not joint torques. Level 2: add the crank (hinge on the base, armature = knee rotor) and the rod (connect equality to the shin lever) so the rod force comes out of the constraint. Thermal: both RS-03s sit at 11 N·m continuous (56 % of rated) at 22 kg; at 18 kg about 9 N·m. |
| A3 | major | **Knee pin is a single-sided cantilever, not the clevis the standard describes.** The thigh hub (y 188–218) and the shin hub (y 230–258) are side by side with a 12 mm gap; the Ø12 pin bridges it. The shin's lever load (the rod force, A1) and the ground load both go through this pin into a 30 mm bore in aluminium. | Stack: `exports/mech-layout.json` (`thigh`, `shin`); `docs/mechanical-interfaces.md:21` says "thigh clevis Ø12 H7"; `cad/leg.py:181-194` builds a single hub. FEA loads the bore radially only (`cad/studies.py:128-138`) and fixes the shin at its bearings (`:140-148`): pin bending and bore bearing stress are never computed. | Make the shin's knee end a **fork** straddling the thigh hub (two 6001-2RS, one per cheek, 36 mm apart), with the lever on the outer cheek in the crank plane. Pin: ISO 7379 shoulder screw 12 h8 × M10, or a 12 h6 ground pin with circlips. Add a pin study (bending over the gap, bore bearing stress ≤ 0.5 σy). |
| A4 | major | **Track and width are three different numbers.** The design says 330 mm, the designer's layout 430 mm (hipX 167, wheelX 215), the CAD 588 mm (wheel centre 294, outer face 316: a 632 mm wide robot vs Diablo's 371 mm). The knee motor sits outboard, in the wheel's plane, which forces knee ≥ 42° (deepest crouch = hip-to-axle 153 mm) and leaves 11–17 mm between tyre and motor at full crouch. | `designs/default.json:11`, `designer/model.js:177-181`, `exports/mech-layout.json:70-77`, `cad/leg.py:94-100`. | Accept ~560–590 mm as the consequence of a 206 mm body + two RS-03 per side + outboard wheel, but make it **one number** (the CAD's) that the designer and the design file read back, and make the trade explicit in the overnight loop (width vs roll stability vs yaw inertia). Reduce 20–30 mm by deleting the yoke wall (A6) and the 2 mm gaps. Keep knee\_min at 48° for 25 mm tyre-to-motor clearance. |
| A5 | major | **Loads on the leg have no fall, landing, kerb-drop or impact case.** The design floor is 2.5x static vertical and 0.5 g lateral; the worst simulated wheel force is 280 N (1.3x static). No case for: a 25–50 mm kerb drop at 1 m/s, a hop landing, an unpowered fall from standing (the turret and box corner hit at ~3 m/s), a kick to the leg, a side load into the wheel's edge. No study at all for the push-rod and rod ends, the crank, the knee pin (A3), bolt shear/bearing at AO-L (54 N·m on 8 × M5: 1.1 kN per dowel if preload is lost), the HM bolts, or the chassis wall that carries the yoke (6 × M6 tapped into 3 mm sheet: ~1.5 threads engaged). | `cad/studies.py:61-63` (FLOOR), `:150-153` and `:196-199` (only T1, T2, S1, A1, P1, Y1). `docs/architecture.md:69` lists the fall case as a scenario; `sim/scenarios.py:80-101` has none. HM tapped in 3 mm: `cad/build.py:76`. | Add scenarios: `fall_front`, `fall_back`, `fall_side` (motors off from 0.6 and 1.0 height), `kerb_drop_50`, `hop_landing_80mm`, `kick` (300 N·s per kg impulse at the knee). Add studies: rod (Euler buckling and rod-end static), crank (in-plane bending), knee pin, AO-L bolt group (friction + dowel shear), HM group, side plate/wall, head plate. Fatigue: 6061 has no endurance limit; keep working stress ≤ 70 MPa at the 1x-static cycle (thigh T2 is already 128 MPa at the floor case, which is an event, not a cycle). |
| A6 | major | **Made parts are 6.9 kg (31 % of the robot); the yokes alone are 1.9 kg.** Yoke 952 g, motor plate 418 g (x4), chassis 912 g, adapters 106 g (x4), thigh 377 g, shin 309 g: solid blocks with no pockets. The physics model went from 17.0 to 22.2 kg when the CAD masses replaced the estimate, which is why every torque margin shrank. | `exports/bom-mechanical.json`, `exports/mass-properties.json` (`made_total_g` 6918.7), `build/sim/default/model.json` (`printed_cad_delta_kg` 5.13). | Target 3.6 kg (see plan): two 6 mm 6061 side plates replace the chassis walls + yoke walls; the yoke becomes a bent 3 mm 5052 hat; motor plates pocketed to ~200 g; adapters hollowed to ~70 g (FEA SF 22); links as 40 × 30 × 2 tube with bonded/pinned end fittings. |
| A7 | minor | Link wall thickness disagrees everywhere: docs and design say 3 mm, the CAD uses 2.5 mm (thigh) and 2.0 mm (shin). | `docs/mechanical-interfaces.md:227` ("3 mm walls"), `designs/default.json:12-16`, `cad/leg.py:28-36` (`PARAMS`). | One source: `design.geometry.link_section` → `cad/leg.py`. |
| A8 | minor | The FEA table in the docs is stale: it reports yoke SF 2.73 and thigh T2 5.29; the current run gives 2.46 and 2.15 (mesh 3 mm). | `docs/mechanical-interfaces.md:64-72` vs `exports/fea/summary.json` `results`. | Generate the table from `summary.json` in `tools/write_docs`\-style, never by hand. |

## B. Body, head, battery, lidar, service, safety, weather

| # | Sev | Finding | Evidence | Fix |
| --- | --- | --- | --- | --- |
| B1 | major | **The head payload's load path runs through the J501 printed enclosure.** The head saddle bolts to the J501 lid screws (M3, printed lid); the J501 box stands on the chassis flanges on four M3 standoffs. In **every** fall from standing the turret hits the ground first (0.64 m from the axle vs 0.49 for the box top and 0.32 for the face), so the fall load goes turret → saddle → printed lid → box → 4 × M3 → chassis. Recoil and pan slews also go through the lid. | `cad/build.py:115-140` (`head_saddle`: "Open frame on the J501 lid screws"), `:65-71` (M3 holes in the flanges), `cad/parameters.json` `j501_box.standoff`. Lidar-under decision moved the crash point to the head: `docs/decisions.yaml` D20. | A **frame** that owns both ends: the two side plates run from the tray floor to a head plate above the box; the box sits inside on isolators and carries nothing. TPU bumpers on the head plate corners and a sacrificial hopper lid. The head plate is the fixed payload interface (B4). |
| B2 | major | **The battery cannot be swapped.** The chassis is a closed 3 mm box with the J501 on top; the FLEXVOLT pack and its dock adapter sit inside with no door. D23/D25 chose tool packs because they are "easy to swap". | `cad/build.py:54-92` (five closed faces, front window 70 × 36 only), `:95-112` (cradle inside). | Rear-loading: the dock adapter at the front of the tray, the pack slides in on its rail through a hinged rear door (also carries the rear camera cradle), latched by the adapter's own latch. Alternative for the loop: a rear "backpack" pack outside the frame (swap in 5 s, CoM moves 40 mm back). |
| B3 | major | **Docking (D18) and the lidar under the waist (D20) conflict.** D18 lands "a contact plate under the battery tray" on dock contacts by crouching; the lidar now hangs 80 mm below that floor and is the lowest part. | `docs/decisions.yaml` D18 vs D20; lidar bottom = trayBottom − 80 (`cad/build.py:274-275`, `sim/scenarios.py:276`). | Move the charge contacts to the frame's rear lower edge (vacuum-robot style, engaged by reversing) or to the side plates' lower edges straddling the lidar; the crouch still sets the height. Decide before the dock is modelled. |
| B4 | major | **The head plate interface is not fixed** , so the turret cannot be designed against it: "bolt pattern to be fixed", 4 × M4 on an 80 mm square in heat-set inserts in PLA, no connector, no cable path, no CoM/recoil/fall load case. | `docs/head-payload.md:17`, `cad/parameters.json` `head_plate`, `sim/model.py:298-300` (turret joints at `top + 6 mm`). | Fix it in the plan: 4 × M5 on 80 × 80 + 2 × Ø5 dowels + Ø45 centre hole, 4 mm 6061 plate on the frame, a Deutsch DT 12-way, and the fall/recoil/slew cases in FEA. |
| B5 | major | **Kid pinch points exist in the CAD with no guard.** Knee scissor closes to 45° between two 40 mm bars; crank tip (r 57) sweeps 20 mm from the posts (r 77): a finger gap; tyre-to-knee-motor gap closes from 240 mm to 17 mm; the rod end eyes run 1.5 mm from the thigh face. The safety study lists these; the CAD has nothing. | `cad/leg.py` (no guards), `research/turret-and-safety.md:506-510`. ISO 13854 finger gap 25 mm, or ≤ 4 mm. | Printed TPU knee web (inside of the knee), a closed wheel arch/knee-motor boot (also the water boot, B6), a crank cover disc. Software: crouch rate and joint torque limits when a person is within 1 m. Verify gaps at every pose in the clash check with a 25 mm probe. |
| B6 | major | **Not weather-ready.** RS-03 is IP52 and its XT30/GH1.25 connectors are in radial pockets on the rear cover, i.e. at the outermost face of the robot (y = 310); the chassis floor has open drain/vent slots under the battery; the J501's fan chimney is on top, directly under the turret ring; no gaskets or glands anywhere. | `catalog/interfaces.json` (`robstride-rs-03` `ip_rating`, `connectors.exit`), `cad/build.py:86-91` (slots), `:116` (ring raised "so the cooler grille keeps breathing"). | Rate v1 as **dry-outdoors / IP43-ish** : TPU boots over both motor rear covers, glands (PG9) at every CP, a drip skirt under the turret ring, a snorkel on the chimney, conformal coat on boards, "no wet grass" rule (already in the launcher study). IP54 is a v2 goal. |
| B7 | major | **Pack voltage vs motor limit.** A full 15S FLEXVOLT is 60 V by DeWalt's own definition (4.0 V/cell "MAX"), up to 63 V off the charger; the RS-03's absolute maximum is 60 V. A regen clamp cannot lower a fresh pack's open-circuit voltage. Not mechanical, but it decides the motor (RS-03/RS-04 both 60 V max; AK70-10 52 V). | `designs/default.json:72`, `docs/decisions.yaml` D25 vs D23. | Either revert to D23 (two packs in 20 V mode in series, 40 V: passed everything, regen margin, the user owns the packs) or arm only below 58 V after a pre-discharge. Decide before buying motors. |
| B8 | minor | The J501 box depth is 181 in the catalog and the design, 192 in `cad/parameters.json`; the standoff pattern comes from the 192 number. | `cad/parameters.json` `j501_box.depth` vs `catalog/sensors_compute.json` `enclosure_dimensions_mm`. | Measure the box; one number in the catalog, read by both. |
| B9 | minor | Lidar view is what D20 accepted: ±15° band hits the thighs (11° per side) and, below 50 % height, the wheels (~30° per side). Fine, but the loop should score it. | Geometry from `exports/mech-layout.json`. | Add an occlusion metric (fraction of the band blocked at each height). |

## C. Physics model vs CAD (every mismatch found)

| # | Sev | Mismatch | Evidence | Fix |
| --- | --- | --- | --- | --- |
| C1 | **blocker** | Knee drive: serial joint instead of a base-grounded parallelogram (A2). Motor torques, limits, thermal load and rotor inertia are mis-assigned. | `sim/model.py:271-278` | A2 fix (tendon now, closed loop next). |
| C2 | **blocker** | **Collision shape of the body is a 181 × 206 × 216 box.** The leg modules (yokes, four motors, plates) span y ±104 to ±316 and have **no collision geometry** ; nor does the under-waist lidar, the cameras or the head saddle. Any fall or side contact passes through the hips. The thigh/shin boxes are 30 × 40 at their frame origins (the shin's real section is 41 mm further out). | `sim/model.py:253-257` (one box, lidar only when not `under`), `:264-265`. | Collision geoms from the CAD: convex hulls per link (`exports/links/*.stl` decimated), plus the motors as cylinders at the stack positions; `contype/conaffinity` so legs can touch the body (needed for the fall case). |
| C3 | major | Inertias are primitives (solid boxes/cylinders), not the CAD's tensors; the push-rod mass is missing (it is a "bought" part and not in `mass-properties.json`); the crank + knee output adapter mass is put on the thigh (it is on the base-grounded crank). Hub motor 60/40 rotor split and leg rotor inertia are guesses. | `sim/model.py:163-186`, `:172-178`; `build/sim/default/model.json` `estimates`. | Export mass, CoM and inertia tensor per link from the CAD solids (`cadagent.robot` has the B-rep moments), bought parts at CAD positions with catalog mass; weigh the real RS-03, BotWheel and J501 box (B8). |
| C4 | major | Joint limits: hip −0.6…2.2 rad is not derived from the CAD (the thigh hub vs the yoke deck and posts; the crank vs the posts), and the knee minimum (45°) is a design constant, not the clash-derived 42°. | `sim/model.py:271`, `cad/leg.py:94-100`. | The clash sweep writes the joint limits into `mech-layout.json`; the model reads them. |
| C5 | major | Scenario coverage: no fall, kerb drop, hop landing, side push, uneven ground or grass friction; loads are logged only at joints (`cfrc_int`), not at the rod, pin, bolt groups or head plate. | `sim/scenarios.py:80-101`, `:234-251`. | A5 scenarios; log the rod constraint force and the turret's `cfrc_int`. |
| C6 | minor | The base's visual motors are drawn at y = ±134 (one cylinder) while the CAD has hip at 146 and knee at 282; harmless because the page uses the CAD rig, but the MuJoCo video does not match. | `sim/model.py:236-237`. | Draw from `mech-layout.json`. |
| C7 | minor | Hip and wheel offsets do come from the CAD stack when it matches the design (good); but if `exports/mech-layout.json` is from another design the model silently falls back to the designer's 430 mm track with no warning in the summary. | `sim/model.py:92-99`, `:119-125`. | Make the fallback a hard failure in the loop (`trial_eval`). |
| C8 | minor | RL results and sim2sim metrics predate the CAD masses and include the deleted tail (`run2_tail_19kg`); `rl/results/trained_model` is modified and uncommitted. | `rl/results/`, `git status`. | Retrain finalists only, after C1–C3. |

## D. 3D simulator view vs CAD and physics

| # | Sev | Finding | Evidence | Fix |
| --- | --- | --- | --- | --- |
| D1 | major | **Four implementations of the leg kinematics** that can drift apart: `cad/leg.py:283-300` (pose), `designer/cadrig.js:85-91` (re-derives the crank pin and rod pose from `crank_r` alone), `designer/robot3d.js:332-350` (procedural legs, no rod), and MuJoCo. When the lever geometry changes (A1) the rig's rod pose is wrong until someone edits JS. | files cited | The physics writes **every body's pose** per frame (base, thigh, shin, wheel, crank, rod, pan, tilt as qpos or xpos/xquat); the page only sets transforms on meshes named by link. No kinematics in JS. |
| D2 | major | The procedural robot (`robot3d.js`) is still what any non-default design shows and what the summary mass/track readouts come from; it draws a lidar crown, a single motor cylinder per side, box legs with the wheel at the designer's 430 mm track: a different robot from the CAD. | `designer/robot3d.js:143-152`, `:283-318`, `:373-374`. | Delete the procedural robot from the sim page; build the CAD rig for every design in the loop (the CAD takes ~2 min; cache per design hash). Keep a wireframe placeholder only while the build runs. |
| D3 | major | The turret is always procedural (`turret3d.js`) with made-up proportions, and the physics turret is two primitive boxes with assumed 40/60 mass split and 5/3 N·m motors. Neither is the launcher study's concept. | `designer/turret3d.js:15-57`, `sim/model.py:280-305`. | Turret CAD → `links.pan`, `links.tilt`, `links.cartridge` in the manifest; masses from the CAD; joints from `cad/interfaces.json` (TM). |
| D4 | minor | **Stale geometry on disk** : `exports/view/` still holds `tail_*.stl`, `hip_yoke_*.stl`, `battery_2.stl` (20:08 build) beside today's parts; `exports/tail_*.step`, `hip_yoke.step`, `tray.step` likewise. The kit viewer shows a tail the user had removed. `build.py` only deletes `asm_*.step` and `*.stl`. | `ls -la exports/view`, `cad/build.py:232-235`. | `build.py` wipes `exports/{view,links}` and every `*.step` it does not write; commit the clean set. |
| D5 | minor | The CAD rig mirrors the left leg for the right (fine) but the manifest has no joint limits, lever angle or interface frames, so the page cannot show pose limits or the pinch probe. | `exports/links/manifest.json`. | Add `joints` (axes, limits), `interfaces` (AO-L, KC, WH, HM, TM frames) to the manifest. |

## E. FEA, materials, the interface standard

| # | Sev | Finding | Evidence | Fix |
| --- | --- | --- | --- | --- |
| E1 | major | The right material call for the structure is **aluminium where a bearing or a motor bolts on, tube for the links, sheet for the frame** — the studies proved printed PA6-CF cannot do the yoke or thigh (SF 0.26 / 0.11) and that is correct. But "CNC 6061 from solid" for every part is the expensive, heavy way to get aluminium: the yoke, chassis, crank and wheel adapter are flat-pattern parts (SendCutSend, 2–4 days); only the motor plates (bearing seat) and the link end fittings need machining. | `exports/bom-mechanical.json` (`how` is "CNC 6061-T6" for 9 of 15 parts). | Plan's part tree: laser-cut 6061/5052 for plates and brackets, 40 × 30 × 2 tube links with 4 machined end fittings, CNC only the 4 motor plates and the 4 output adapters (or merge the adapters into the link hubs, E3). PA-CF/PET-CF for covers, cradles, the turret and the hopper; TPU for guards. CF plate (5 mm) is a v2 for the side plates (−40 % mass) once the loads are measured. |
| E2 | major | Boundary conditions flatter the parts: the thigh is clamped over its whole hub face (a bolted joint is softer and the dowels carry shock); the yoke is fixed at the HM bolts but the 3 mm wall behind them is not modelled; the plate is fixed at the posts and tab. No mesh-convergence check (one 3 mm mesh). | `cad/studies.py:128-129`, `:179`, `:190`. | Model bolt groups as preloaded contact or at least a compliant layer; include the wall/side plate in the yoke study; run 2 mm and 4 mm meshes for the governing case and report the difference. |
| E3 | minor | **GMI is a good idea applied one layer too thick.** Keeping standard faces (AO-L, KC, WH, HM, CP, PR, TM) as drawing conventions is right and should stay. Two things are over-built for this stage: (i) a separate output adapter per motor (4 CNC parts, 8 × M5 + 2 dowels + a spigot + a bearing journal each) when the link's own hub can carry the motor pattern and the journal; (ii) AO-S/AC-S small faces and three-post "box" rules nothing uses yet. Also every adapter is "assumed = False" only for the RS-03; four of the six motors are still on vendor drawings only. | `cad/interfaces.json` (AO-S, AC-S, TM unused), `cad/gmi.py:97-128`, `research/actuator-interfaces.md`. | Keep GMI as the **face definitions and the load spec** ; allow a link hub to _be_ the adapter (one part) while still presenting AO-L; drop AO-S/AC-S until the turret needs them (it does not: servos). Confirm RS-03's rear M4 threads on a real unit (it changes the hip mount, see plan). |
| E4 | minor | Printed-part allowables: the designer screens PLA links at 10.5 MPa but every structural print in the plan is PET-CF/PA-CF; the materials table carries PA6-CF but not its wet strength (−44 %). | `designer/model.js` stress screen, `catalog/fabrication.json` `nylon_moisture`. | Use PET-CF (74 MPa, low moisture uptake) for printed structure; PA6-CF only annealed and dried. |

## F. Turret

| # | Sev | Finding | Evidence | Fix |
| --- | --- | --- | --- | --- |
| F1 | major | "Cable through the pan axis" is impossible with a DM-J4310 on the axis: none of the candidate actuators is hollow. The study already moved to a ±170° loop; the pan motor choice did not follow. | `research/actuator-interfaces.md:92`, `research/turret-launcher-study.md:492`. | Pan = STS3215 on a printed 3:1 ring gear around a 61810-2RS (50 × 65 × 7) with an **open 45 mm centre** for a clock-spring loop; hard stops at ±170°. Saves 217 g and $103 vs the DM-J4310; pan speed 90°/s is enough for a kid's range (study §2.1). |
| F2 | major | The pan ring is centred over the J501's fan chimney; the hopper and foam dust sit in the exhaust. | `cad/build.py:116`, `j501-agx` `chimney` parameters. | Offset the pan axis 35 mm forward of the box centre (still over the wheel axle: the box is centred on it), put a deflector/snorkel on the chimney, and keep the hopper's lid sealed with a spring flap (study §8.4). |
| F3 | major | The head is the right place (highest muzzle, 360° pan, character), **if** B1 is fixed; it is the crash bar in every fall and sits on the hot exhaust. The alternative (a chin turret under the face) is lower, protected and needs no pan ring, but loses 150 mm of muzzle height (range from 4 m to ~3 m at the same cap) and the "pup head". | geometry above; `docs/head-payload.md:19` (range). | Keep the head. Make the turret the designed sacrificial element: TPU bumper ring, replaceable printed hopper lid, the launcher cartridge detachable by one latch so a broken cartridge is a $30 print. |
| F4 | minor | Sim turret numbers are placeholders: 5/3 N·m servos, 40/60 split, pan ±3.1 rad, recoil 0.05 N·s per shot, 1 g darts. The concept is 1.83 g balls at 21.3 m/s (0.039 N·s), 1.16 kg, pan ±2.97 rad. | `sim/model.py:284-305`, `sim/turret.py:25-28`. | Parameterise from the turret CAD and `catalog/turret.json`; add a ball projectile (sphere, drag). |

## What is right and should stay

-   Coaxial hip + knee at the body with a parallelogram (fix the lever, A1): light legs, both big motors on the body.
-   The support bearing in the motor plate (RobStride's 45 × 58 × 7 output bearing has no moment rating).
-   Aluminium for anything a motor or bearing bolts to; the FEA numbers that killed the printed yoke and thigh.
-   Lidar under the waist; cameras front and rear; the VN-100 rigid on the body.
-   The cable rules and the service-loop check (`cad/cables.py`), extended to the new lever geometry.
-   The closed-loop toolchain itself (`trial_eval.py` → CAD → sim → FEA → cables) is the right skeleton for tonight's loop.

Created by [Valpatel Software LLC](https://valpatel.com/). © 2026. All rights reserved. Repository materials retain their stated licenses.
