From a70d8e08340c0742cac4219c5e74a07d0e04ecb8 Mon Sep 17 00:00:00 2001 From: bostromdev Date: Sun, 2 Aug 2026 00:54:01 -0400 Subject: [PATCH] freeze Revision 1 hardware baseline for CAD --- HARDWARE.md | 142 +++--------------- Measurements/README.md | 4 + Measurements/fusion360-assistant-prompt.md | 36 +++-- Measurements/fusion360-bom-template.md | 14 +- Measurements/fusion360-build-order.md | 21 ++- Measurements/fusion360-design-spec.md | 15 +- Measurements/fusion360-parameters.md | 3 + docs/architecture/overview.md | 6 + docs/firmware/protocol.md | 5 + docs/hardware/assembly-order.md | 71 +++++++++ docs/hardware/gpio-map.md | 33 ++++ docs/hardware/owned-hardware.md | 49 ++++++ docs/hardware/power-tree.md | 42 ++++++ docs/hardware/reference-architecture.md | 109 ++++++++++++++ docs/hardware/tmc2209-v1.3.md | 19 +++ docs/hardware/wire-standard.md | 57 +++++++ docs/software/file-formats.md | 5 + firmware/config/provisional-esp32dev-v1.json | 139 ----------------- .../config/radiance3d-owned-hardware.json | 107 +++++++++++++ firmware/controller/CMakeLists.txt | 2 +- firmware/controller/host/CMakeLists.txt | 4 +- .../controller/main/controller_runtime.cpp | 3 + firmware/controller/src/axis_controller.cpp | 13 +- firmware/controller/src/hardware_config.cpp | 4 +- firmware/controller/src/motion_controller.cpp | 8 +- .../controller/test/test_motion/test_main.cpp | 4 +- .../test/test_physical/test_main.cpp | 6 +- scripts/check_repository.py | 89 ++++++++++- scripts/generate_hardware_profile_header.py | 13 +- 29 files changed, 725 insertions(+), 298 deletions(-) create mode 100644 docs/architecture/overview.md create mode 100644 docs/firmware/protocol.md create mode 100644 docs/hardware/assembly-order.md create mode 100644 docs/hardware/gpio-map.md create mode 100644 docs/hardware/owned-hardware.md create mode 100644 docs/hardware/power-tree.md create mode 100644 docs/hardware/reference-architecture.md create mode 100644 docs/hardware/tmc2209-v1.3.md create mode 100644 docs/hardware/wire-standard.md create mode 100644 docs/software/file-formats.md delete mode 100644 firmware/config/provisional-esp32dev-v1.json create mode 100644 firmware/config/radiance3d-owned-hardware.json diff --git a/HARDWARE.md b/HARDWARE.md index 4530f6b..55e756c 100644 --- a/HARDWARE.md +++ b/HARDWARE.md @@ -1,118 +1,24 @@ -# Hardware - -Verified facts only. Physical dimensions live in -[`Measurements/`](Measurements/README.md) and are not repeated here. - -## Components - -| Part | Model | Qty | -|---|---|---:| -| Stepper motor | YEJMKJ / LYLANMO `42HDB0014NC-24B` | 2 | -| Stepper driver | BIGTREETECH TMC2209 V1.3 | 2 | -| Controller | ELEGOO ESP32 devkit, ESP-WROOM-32, USB-C | 1 | -| RF detector | AD8317 EVAL BD (NWDZ V1.0) | 1 | -| Buck converter | LM2596-based module | 1 | -| Motor supply | 12 V battery, inline fuse, master disconnect | 1 | - -## Motor nameplate - -From the label, legible in `IMG_5249.HEIC`: - -| | | -|---|---| -| Holding torque | 0.13 N·m | -| Phase resistance | 3.5 Ω | -| Phase inductance | 3.4 mH | -| Rated phase current | 1.0 A | -| Step angle | 1.8° (200 full steps/rev) | -| Frame | 42 × 42 × 21 mm | - -## Motor phase wiring - -| Wire | Phase | -|---|---| -| Black | A+ | -| Green | A− | -| Red | B+ | -| Blue | B− | - -**Coil A = black + green. Coil B = red + blue.** Splitting a pair across the two driver -outputs makes the motor buzz, jitter or lock instead of turning, and it is not obvious -by ear. - -Check with a multimeter before energising: about 3.5 Ω within a pair, open circuit -between pairs. - -Never connect or disconnect a motor while the drivers are powered. - -## GPIO baseline - -Working baseline for the current firmware. Verify against the actual board before -final wiring. - -| Signal | Azimuth | Elevation | Notes | -|---|---|---|---| -| STEP | GPIO25 | GPIO18 | 3.3 V logic | -| DIR | GPIO26 | GPIO19 | 3.3 V logic | -| ENABLE (EN/ENN) | GPIO27 | GPIO23 | Active low — confirm carrier polarity | -| UART TX → PDN_UART | GPIO22 / UART1 | GPIO17 / UART2 | One-wire; may need a resistor | -| UART RX ← PDN_UART | GPIO21 / UART1 | GPIO16 / UART2 | | -| Home switch | GPIO32 | GPIO33 | Configurable; see homing note below | -| Emergency stop | GPIO13 | shared | Active low | - -Logic ground and motor-power ground must share a common reference. VM is 12 V to the -drivers only — never to the ESP32. - -## Power - -```text -12 V battery -├── inline fuse (3–5 A) -├── master disconnect -├── TMC2209 VM ×2 -└── LM2596 buck → 5.0 V → ESP32 only -``` - -Set the buck output to exactly 5.0 V with a multimeter before connecting an ESP32. Do -not trust an onboard display as the only check. - -Bulk capacitance (100–220 µF, 25 V minimum) close to each driver's VM/GND, plus 0.1 µF -local decoupling. Steppers regenerate current when decelerating, so VM transients -appear even when the command is static. - -Keep motor-current returns short and away from logic, switch and RF returns. - -USB and the buck rail can both energise the board. Check the board's power circuit -before connecting both at once. - -## Motor current - -Commissioning starts at **650 mA RMS**. Motor ceiling is **1000 mA RMS**. - -Progression: 500–650 → 650–800 → ~900 only if needed. Never exceed the ceiling. A 12 V -supply does not put 12 V across a winding — the TMC2209 chops to regulate current — but -wrong RMS current still overheats or under-drives the motor. - -## Homing - -The firmware currently homes by driving to a switch (`home_switch_pin`). There is no -sensorless/StallGuard implementation. Three options if switches are not wanted: - -1. **Skip homing.** Set zero by hand. Works now, no parts, no code. Zero moves on every - power cycle, so scans are not comparable to each other. -2. **StallGuard sensorless homing.** The TMC2209 supports it and no switch is needed, - but it is not written yet and needs mechanical hard stops. A light antenna is the - hard case for stall detection. -3. **One microswitch per axis.** Firmware already supports it. Most repeatable. - -Position is open-loop step counting. It is only trusted after a successful home. Reset, -fault, e-stop or a missed step invalidates it. - -## Safety - -Physical commissioning starts with USB power only, motor power disconnected, and -accessible power isolation that does not depend on firmware. - -Simulator tests prove none of: wiring correctness, GPIO correctness, motor direction, -driver current, step timing under load, switch polarity, mechanical clearance, -e-stop latency, RF accuracy. +# Radiance3D owned hardware baseline + +This repository uses only the owned-hardware profile in +[`firmware/config/radiance3d-owned-hardware.json`](firmware/config/radiance3d-owned-hardware.json). +The hardware photographs in [`Part Numbers:Views/`]() are the +visual authority; this file is an index, not a substitute for inspection. + +**Verified Hardware** is owned, photographed, measured, or electrically confirmed. +**Design Intent** is conceptual Fusion guidance only; final placement is determined +during CAD. The [reference architecture](docs/hardware/reference-architecture.md) +keeps those categories separate. + +Read the hardware documentation in this order: + +1. [Owned hardware and BOM](docs/hardware/owned-hardware.md) +2. [Power tree](docs/hardware/power-tree.md) +3. [GPIO map](docs/hardware/gpio-map.md) +4. [Wire and harness standard](docs/hardware/wire-standard.md) +5. [Assembly, routing, and commissioning](docs/hardware/assembly-order.md) +6. [Conceptual Fusion reference architecture](docs/hardware/reference-architecture.md) + +The firmware uses ESP-IDF. It has no calibrated RF-power conversion and has not yet +been validated on the physical mechanism. Firmware soft limits are deliberately +conservative placeholders until the final cable route is measured. diff --git a/Measurements/README.md b/Measurements/README.md index dc7a135..b03919d 100644 --- a/Measurements/README.md +++ b/Measurements/README.md @@ -2,6 +2,10 @@ This folder is the **canonical source for every physical dimension in Radiance3D**. +The enclosure has not yet been designed. Measurements support conceptual Fusion work; +the [reference architecture](../docs/hardware/reference-architecture.md) defines the +official non-dimensioned placement baseline. + Every number here was measured with a digital caliper on the actual parts in hand and is traceable to a photograph in this folder. Hardware, mechanical, assembly, BOM and CAD documents elsewhere in the repository must link here rather than repeat these diff --git a/Measurements/fusion360-assistant-prompt.md b/Measurements/fusion360-assistant-prompt.md index 97677da..cb57ea7 100644 --- a/Measurements/fusion360-assistant-prompt.md +++ b/Measurements/fusion360-assistant-prompt.md @@ -1,5 +1,9 @@ # Fusion 360 Assistant prompt +The enclosure has not yet been designed. Treat all listed component locations as +conceptual only and follow [`../docs/hardware/reference-architecture.md`](../docs/hardware/reference-architecture.md) +for the official non-dimensioned placement baseline. + Paste everything inside the code block below into Fusion 360 Assistant. It is written to be self-contained — it carries the measured dimensions with it, so the assistant does not need access to this repository. @@ -18,9 +22,11 @@ Source of the numbers: [`nema17.md`](nema17.md), [`tmc2209-v1.3.md`](tmc2209-v1. You are helping me design the first Radiance3D pan-and-tilt prototype in Fusion 360. Radiance3D is an antenna radiation-pattern measurement platform. The mechanism rotates -an antenna under test (AUT) in azimuth (pan) and elevation (tilt) while a stationary -receiver measures it. Positioning repeatability matters more than speed, and the -structure must not distort the radiation pattern more than necessary. +the AD8317-mounted antenna under test (AUT) in azimuth (pan) and elevation (tilt) while +an external stationary 5.8 GHz VTX transmits. The AUT threads directly onto the +vertically mounted AD8317 SMA; no RG316 jumper connects the AUT and detector. +Positioning repeatability matters more than speed, and the structure must not distort +the radiation pattern more than necessary. ===================================================================== NON-NEGOTIABLE RULES @@ -38,21 +44,26 @@ NON-NEGOTIABLE RULES If a print comes out tight, the fix is dPrintClearance or dBearingFitAllowance, and nothing else. Overwriting a measurement with a print allowance destroys the record of what the hardware actually is. -3. Each major part is a separate Fusion component, named as listed below. -4. The MEASURED dimensions below are the source of truth. They were taken with +3. The enclosure is not designed yet: all locations are conceptual, not dimensioned. + Place ESP32 and both drivers/bucks in the stationary base; place only the tilt motor, + vertically mounted AD8317, and antenna mount on the rotating platform. The pan motor + is fixed in the base and the target motion is one controlled 360° turn, not unlimited + continuous rotation. Model a managed moving harness, with lengths TBD AFTER CAD. +4. Each major part is a separate Fusion component, named as listed below. +5. The MEASURED dimensions below are the source of truth. They were taken with calipers on the actual parts. Do not replace them with datasheet or "standard" values, even where a standard value looks rounder or more familiar. -5. Never guess a critical fit dimension. Anything marked PROVISIONAL is a placeholder +6. Never guess a critical fit dimension. Anything marked PROVISIONAL is a placeholder for a measurement I have not taken yet. If a feature's fit depends on a PROVISIONAL value, say so out loud before modelling it, and design that feature so the parameter can change without rebuilding the part. -6. Design for PETG on a fused-filament printer: +7. Design for PETG on a fused-filament printer: - avoid overhangs steeper than 45 degrees where a design change can prevent them - minimise support material; state the intended print orientation for each part - use structural fillets at load-bearing intersections and ribs instead of thick solid walls - use heat-set threaded inserts wherever a fastener will be removed more than once -7. Protect the motor shafts. A motor shaft must not carry a side load or a cantilevered +8. Protect the motor shafts. A motor shaft must not carry a side load or a cantilevered mass. Every rotating axis is carried on its own bearings; the motor supplies torque through a coupler only. 7a. Both motors have a connector on the rear end cap that stands 9.38 mm proud, with a @@ -61,13 +72,14 @@ NON-NEGOTIABLE RULES connector must stay reachable for wiring. When you recommend a motor orientation, state where that 21.4 mm goes and what it costs in stack height. Do not recommend an orientation without pricing it. -8. Place the pan and tilt axes as close together as practical, and put the antenna's +9. Place the pan and tilt axes as close together as practical, and put the antenna's active centre as close as practical to the point where the two axes intersect. Offset between the antenna phase centre and the axis intersection is a measurement error, not just an aesthetic issue. Report any offset you cannot eliminate. -9. Route cables so they never bind, stretch or wrap as the axes move. Coax bend radius - is a hard constraint, not a guideline. -10. Work on ONE major component at a time. Do not start the next component until I +10. Route the moving silicone harness so it never binds, stretches, or wraps through + one controlled 360° pan turn; its bend radius is a hard constraint, not a guideline. + The external VTX coax is off-scanner and there is no AUT-to-detector coax jumper. +11. Work on ONE major component at a time. Do not start the next component until I have reviewed and approved the current one. ===================================================================== diff --git a/Measurements/fusion360-bom-template.md b/Measurements/fusion360-bom-template.md index fabc2fc..e9ae284 100644 --- a/Measurements/fusion360-bom-template.md +++ b/Measurements/fusion360-bom-template.md @@ -1,5 +1,9 @@ # Fusion 360 BOM template +The enclosure has not yet been designed. All placements are conceptual only; use +[`../docs/hardware/reference-architecture.md`](../docs/hardware/reference-architecture.md) +as the official non-dimensioned baseline. + Use this as the starting BOM for the first prototype. Because the relevant hardware is already in the project, this can stay as a simple mechanical checklist first; fill in part numbers and suppliers only if you want a formal procurement list. ## Mechanical @@ -21,15 +25,17 @@ Use this as the starting BOM for the first prototype. Because the relevant hardw |---|---:|---|---|---| | ESP32 board | 1 | ELEGOO ESP32 devkit | TBD | Existing hardware | | TMC2209 driver | 2 | Stepper driver module | TBD | Existing hardware | -| Buck converter | 1 | LM2596 module | TBD | Measure before tray finalization | +| Buck converter A | 1 | ZX-052 V2.0, ESP32 5 V branch | TBD | Existing hardware; stationary base | +| Buck converter B | 1 | ZX-052 V2.0, AD8317 5 V branch | TBD | Existing hardware; stationary base | ## RF and antenna | Item | Qty | Description | Part number | Notes | |---|---:|---|---|---| -| Antenna under test | 1 | Antenna to be mounted | TBD | Measure before cradle finalization | -| Coax | 1 | RF cable to antenna | TBD | Check bend radius | -| RF detector | 1 | AD8317 evaluation board | TBD | Existing hardware | +| Antenna under test | 1 | Threads directly onto AD8317 SMA | TBD | Measure before direct-SMA mount finalization | +| RF detector | 1 | AD8317 evaluation board, vertically mounted | TBD | Existing hardware; rotating platform | +| External VTX | 1 | Stationary external 5.8 GHz source | TBD | Off-scanner; not an enclosure component | +| RG316 jumper, antenna to detector | 0 | Not used | — | Direct SMA connection only | ## Miscellaneous diff --git a/Measurements/fusion360-build-order.md b/Measurements/fusion360-build-order.md index ed2c8b6..26a4af0 100644 --- a/Measurements/fusion360-build-order.md +++ b/Measurements/fusion360-build-order.md @@ -1,5 +1,9 @@ # Fusion 360 build order — first prototype +The enclosure has not yet been designed. All locations in this document are conceptual +placement intent only; use the [reference architecture](../docs/hardware/reference-architecture.md) +as the official non-dimensioned baseline before choosing CAD dimensions. + This is the practical next-step plan for the first Radiance3D prototype. It is intentionally scoped so you can build the structure one component at a time without guessing. ## 0. Before modeling anything @@ -16,6 +20,10 @@ Before you model the first part, confirm these items from the existing measureme Create the stationary base first. This defines the envelope for everything else. +The 12 V source is permanently off-board beneath the workbench. Do not model a battery +compartment, retention feature, or battery-weight allowance. Model only an accessible, +strain-relieved `+12V IN` / `GND IN` entry and its route to internal distribution. + ### Goals - Provide a stable platform for the pan axis. - Reserve space for the pan motor, bearings, and the electronics tray. @@ -57,7 +65,9 @@ Once the base is reviewed, add the pan motor mount. ## 3. Build Pan_Platform third -Now model the rotating platform that carries the tilt assembly. +Now model the rotating platform that carries only the tilt motor, vertically mounted +AD8317 detector, and antenna mount. The AUT threads directly onto the detector SMA; +do not add an antenna-to-detector coax jumper. ### Goals - Create the pan platform as a bearing-supported rotating part. @@ -164,13 +174,18 @@ Cable routing should be designed as part of the structure, not added later. ### Goals - Prevent cable binding or stretching through motion. -- Respect the minimum bend radius of the coax or cable being used. +- Model RF paths as RG316 50 Ω coax with a centre-and-shield envelope; model silicone + electrical harnesses separately, never as the same cable type. +- Respect the measured minimum bend radius of the RG316 coax and the bend clearance of + each silicone harness. - Keep the cable path away from fasteners and the moving bearing faces. ### Model requirements - Include clips or guides in the frame and base. - Leave generous bend radius room. -- Prevent the cable from wrapping around the axis as it moves. +- Prevent either cabling system from wrapping around the axis as it moves. +- Label CAD routes with `RF-###` for coax and `PWR-###`, `SIG-###`, or `MTR-###` for + silicone harnesses. ## 10. Build Limit_Switch_Mounts tenth diff --git a/Measurements/fusion360-design-spec.md b/Measurements/fusion360-design-spec.md index 26d39b7..4169fd5 100644 --- a/Measurements/fusion360-design-spec.md +++ b/Measurements/fusion360-design-spec.md @@ -1,12 +1,17 @@ # Fusion 360 design spec — precision pass +The enclosure has not yet been designed. All component locations are conceptual only; +the [reference architecture](../docs/hardware/reference-architecture.md) is the official +non-dimensioned placement baseline for Fusion work. + This document is a tighter handoff for the first Radiance3D pan-and-tilt prototype. It turns the broad CAD brief into a concrete implementation plan that is easier to execute in Fusion 360 and easier to review. ## 1. Target architecture The first prototype should use a two-axis, two-bearing mechanism with the motor shafts protected from side load: -- Pan axis: a rotating platform carried on bearings in the base, driven by the pan motor through a coupler. +- Pan axis: a rotating platform carried on bearings in the base, driven by the fixed + stationary-base pan motor through a coupler; target capability is one controlled 360° turn. - Tilt axis: a tilt support frame carried on bearings and driven by the tilt motor through a coupler. - The motor shaft should never be the primary radial support for the axis. The bearings carry the load; the motor supplies torque only. - The antenna active centre should be placed as close as practical to the pan/tilt axis intersection. The target is $0\,\text{mm}$ offset. Any remaining offset must be recorded as a design parameter rather than hidden. @@ -67,8 +72,12 @@ The antenna active centre should be brought as close as practical to the pan/til ### 4.4 Cable path -The cable route should be planned before the frame is finalized. Cable routing must account for: -- coax bend radius +The cable route should be planned before the frame is finalized. RF and electrical +cabling must be modelled as separate routes: RG316 50 Ω coax is represented with centre +conductor and shield for RF-### paths, while PWR-###, SIG-###, and MTR-### are silicone +wire harnesses. Cable routing must account for: +- bend radius of the moving silicone harness; the external VTX RG316 path is not a + scanner-mounted antenna-to-detector jumper - connector reach - axis travel - strain relief diff --git a/Measurements/fusion360-parameters.md b/Measurements/fusion360-parameters.md index 8f8c74c..8963dba 100644 --- a/Measurements/fusion360-parameters.md +++ b/Measurements/fusion360-parameters.md @@ -1,5 +1,8 @@ # Fusion 360 parameters +The enclosure has not yet been designed. These parameters support conceptual Fusion +work only; they do not establish final component locations or enclosure dimensions. + Proposed Fusion 360 user parameters for the Radiance3D pan-and-tilt prototype. Every parameter is tagged with its **type**, and the types are never mixed silently: diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md new file mode 100644 index 0000000..bce09cd --- /dev/null +++ b/docs/architecture/overview.md @@ -0,0 +1,6 @@ +# Architecture overview + +Radiance3D has an ESP-IDF controller, a portable controller simulator, and a Python +host client. The owned-hardware configuration is generated into the firmware build from +`firmware/config/radiance3d-owned-hardware.json`; hardware boundaries and commissioning +requirements are documented in [the hardware index](../../HARDWARE.md). diff --git a/docs/firmware/protocol.md b/docs/firmware/protocol.md new file mode 100644 index 0000000..92cb03a --- /dev/null +++ b/docs/firmware/protocol.md @@ -0,0 +1,5 @@ +# Firmware protocol + +The controller exposes its versioned motion protocol over USB serial. `MOTOR HOME` is +rejected on the owned profile because no physical home switches are installed. Motion +commands remain bounded by the profile's cable-protection soft limits. diff --git a/docs/hardware/assembly-order.md b/docs/hardware/assembly-order.md new file mode 100644 index 0000000..b0bdce5 --- /dev/null +++ b/docs/hardware/assembly-order.md @@ -0,0 +1,71 @@ +# Assembly, cable routing, and commissioning + +## Assembly order + +1. Print and inspect the complete mount. +2. Install both YEJMKJ motors. +3. Install the ESP32, both TMC2209 modules, both ZX-052 modules, and AD8317 in their + designed positions; compare orientation, connectors, and heatsinks to the photo library. +4. Provide the accessible, strain-relieved `+12V IN` / `GND IN` enclosure entry and + terminate PWR-000 at the internal distribution point; do not install or allocate a battery. +5. Move pan and tilt manually through the intended ranges. +6. Route string or sacrificial silicone wire for PWR/SIG/MTR paths and sacrificial + RG316 for RF paths; add the intended service loops and record safe final lengths + before cutting any final harness. +7. Cut, terminate, label, continuity-test, and strain-relieve the final harnesses. +8. Set and meter both buck outputs at 5.0 V without electronics connected. +9. Connect electronics, inspect again, then perform powered motion testing. + +## Routing requirement + +**Design Intent — conceptual architecture only. Final placement determined during CAD.** +The moving silicone harness is intended to carry only tilt-motor and AD8317 electrical +wiring across the pan axis. A controlled 360° turn, service loop, strain relief, cable +guide, bend radius, and return strategy are CAD/commissioning requirements—not known +geometry. The external VTX and its RG316 path are stationary and off-scanner. + +Unlimited continuous pan rotation is not allowed. The intended mechanical capability is +one controlled 360° turn. Firmware limits are not changed by this documentation baseline +and remain provisional pending physical test. The required future control concept is the +**Firmware-defined return-to-home strategy to prevent cumulative cable twist**; its +implementation remains deferred. + +After an electrical test, a printed clip, cable tie, clamp, or removable hot-glue strain +relief is preferred. Adhesive must not enter connectors, adjustment points, heatsinks, +or moving surfaces; permanent cyanoacrylate is not structural retention for a serviceable +connector. + +## Commissioning checklist + +- [ ] Compare each installed board and connector orientation to its repository photo. +- [ ] Verify no raw 12 V path reaches ESP32 5V/VIN, AD8317 +5V, or any GPIO. +- [ ] Confirm PWR-000 enters through an accessible strain relief and terminates at the + internal distribution point; confirm no battery compartment exists. +- [ ] Meter Buck A and Buck B independently at 5.0 V; confirm their VOUT+ terminals are not joined. +- [ ] Verify common ground continuity and motor phase mapping end to end. +- [ ] Confirm every harness has labels at both ends and a matching table row. +- [ ] Confirm RF-001 is the off-scanner VTX coax path and that no RF path is shown or + built with silicone wire. +- [ ] Confirm the AUT directly threads onto the AD8317 SMA with no RG316 jumper. +- [ ] Confirm all PWR/SIG/MTR harnesses are silicone wire, never RG316. +- [ ] Build and run simulator/repository checks before flashing. +- [ ] Verify PDN/UART communication and current settings with motors disconnected. +- [ ] Install heatsinks, connect motors while unpowered, then make a low-speed direction test. +- [ ] Verify one controlled 360° pan turn and the future Firmware-defined return-to-home + strategy to prevent cumulative cable twist after CAD and physical testing. +- [ ] Capture AD8317 raw ADC and voltage data; do not report dBm without calibration. + +## Failure modes and troubleshooting + +| Symptom | Safe first checks | +|---|---| +| ESP32 will not boot | remove motor power; check that only regulated 5 V reaches VIN and bootstrap pins are untouched | +| Motor buzzes or locks | power off; check A1→Black, A2→Yellow/Green, B1→Red, B2→Blue continuity | +| Motor skips or driver overheats | reduce current/load; verify heatsink and motion acceleration; do not increase above 800 mA RMS | +| UART diagnostics fail | power off; check PDN pin identity, address 0/1, UART topology, and common ground | +| ADC reading is unstable | separate RF/analog harness from VM/phase wiring; confirm RF 5 V and analog ground | +| Cable rubs or tightens | stop immediately; revise routing and measured soft limits before further motion | + +Unresolved bench measurements: rear OLED pin ownership; AD8317 input/output connector +labels and calibration curve; ZX-052 electrical limits; final mount travel; final cable +lengths; driver PDN/UART topology; and all physical clearances. None may be guessed. diff --git a/docs/hardware/gpio-map.md b/docs/hardware/gpio-map.md new file mode 100644 index 0000000..a18799c --- /dev/null +++ b/docs/hardware/gpio-map.md @@ -0,0 +1,33 @@ +# Final proposed GPIO map + +## Verified Hardware + +This is the sole firmware GPIO map, encoded in +`firmware/config/radiance3d-owned-hardware.json`. It uses only header labels visible +in the controller photo. GPIO21 and GPIO22 are reserved for the rear OLED and are not +available to the motion system. GPIO0, 2, 4, 5, 12, and 15 are avoided because they +can affect boot; flash-connected GPIO6–11 are never used. + +| Function | GPIO | Direction | Harness | Status | +|---|---:|---|---|---| +| Pan STEP | 25 | output | SIG-001 | assigned | +| Pan DIR | 26 | output | SIG-001 | assigned | +| Pan ENN | 27 | output | SIG-001 | active-low, confirm carrier marking | +| Pan PDN UART TX/RX | 17 / 16 | output / input | SIG-001 | address 0 | +| Tilt STEP | 18 | output | SIG-002 | assigned | +| Tilt DIR | 19 | output | SIG-002 | assigned | +| Tilt ENN | 23 | output | SIG-002 | active-low, confirm carrier marking | +| Tilt PDN UART TX/RX | 14 / 39 | output / input-only | SIG-002 | address 1 | +| AD8317 analog output | 36 | ADC1 input-only | SIG-003 | silicone wire; ADC1; Wi-Fi-safe | +| E-stop input | 13 | input | not in owned BOM | reserved, uninstalled | +| OLED | 21 / 22 | reserved | board-integrated | bench-confirm actual rear OLED wiring | + +No home switch is installed or assigned. The firmware rejects a homing request until +physical switches are added and documented; StallGuard is diagnostics only, not a +homing substitute. Firmware limits remain intentionally provisional and are not the +mechanical architecture specification. **Design Intent:** the mechanical system targets +one controlled 360° pan turn with a managed moving harness; final limits require CAD +and physical testing. + +GPIO39 is input-only and is used only as UART RX. GPIO36 is input-only and is used only +as ADC1. No assigned output uses an input-only pin, and no function shares a pin. diff --git a/docs/hardware/owned-hardware.md b/docs/hardware/owned-hardware.md new file mode 100644 index 0000000..ac58e70 --- /dev/null +++ b/docs/hardware/owned-hardware.md @@ -0,0 +1,49 @@ +# Owned hardware manifest and BOM + +The JSON hardware profile is the machine-readable source of truth. Photo filenames +are relative to `Part Numbers:Views/`; a missing photo is an audit finding, not an +invitation to substitute a generic render. + +The [conceptual Fusion reference architecture](reference-architecture.md) is the +official **Design Intent** baseline. The enclosure is not yet designed; it contains no +final dimensions or final component locations. Everything in the tables below is +**Verified Hardware** unless explicitly labelled Design Intent. + +| Item | Manufacturer / model | Qty | Electrical role | Photo | Firmware / harness | +|---|---|---:|---|---|---| +| Controller | ELEGOO ESP32 DevKit-style, ESP-WROOM-32, USB-C, rear OLED | 1 | 5 V input, 3.3 V logic | `ESP-32-elegoo top view(part #EL-SM-012).jpeg` | PWR-005; SIG-001–004 | +| Driver | BIGTREETECH TMC2209 V1.3 | 2 | 12 V VM, STEP/DIR/PDN_UART | `TMC2209 v1.3 top view with heatsink.jpeg` | PWR-001/002; SIG-001/002; MTR-001/002 | +| Motor | YEJMKJ 42HDB0014NC | 2 | 1.0 A/phase bipolar | `NEMA-17(part #YEJMKJ).jpeg` | MTR-001, MTR-002 | +| RF detector | AD8317 EVB / module V1.0 | 1 | regulated 5 V, analog output, direct-SMA AUT interface | **Missing from photo library** | PWR-006; SIG-003 | +| Buck A | ZX-052 V2.0 | 1 | 12 V to 5.0 V digital branch | **Missing from photo library** | PWR-003, PWR-005 | +| Buck B | ZX-052 V2.0 | 1 | 12 V to 5.0 V RF branch | **Missing from photo library** | PWR-004, PWR-006 | +| External power interface | +12V IN / GND IN from off-board 12 V bench source | 1 | strain-relieved enclosure entry | **No photo recorded** | PWR-000 | + +| Cable | Model | Qty | Permitted use | Harness namespace | +|---|---|---:|---|---| +| Silicone wire | 18/22/26 AWG, five colors | owned stock | all DC, motor, GPIO, and low-voltage signals | PWR, SIG, MTR | +| RF coax | RG316, 50 Ω | owned stock | RF transmitter, antenna, detector input, 50 Ω interconnects only | RF | + +No fuse, switch, connector, clamp, or protection component is included in this owned-parts +BOM. An inline fuse and accessible disconnect are recommended safety additions only. +The external bench source is not an enclosure component and has no internal allocation. + +## Component facts + +- Each motor is 42 × 42 × 21 mm, 1.8°/step (200 full steps/rev), 3.5 Ω/phase, + 1.0 A/phase, approximately 0.13 N·m holding torque. Label wiring is Black A+, + Green A−, Red B+, Blue B−. +- Both drivers are TMC2209 V1.3. Production control is STEP/DIR with enable and + PDN/UART diagnostics. The heatsink is installed only after inspection, before + powered motion testing. +- The AD8317 is powered from the isolated RF/analog 5 V branch. Its output is measured + on ADC1 GPIO36; raw counts and volts are measurements, while dBm is an inference + requiring a saved bench calibration. +- **Design Intent:** the external stationary 5.8 GHz VTX is not mounted on the scanner. + The AUT is intended to screw directly onto the AD8317 SMA; no antenna-to-detector + RG316 jumper is in the baseline. + +Datasheets are external reference material: ESP32-WROOM-32 datasheet, Trinamic +TMC2209 datasheet, AD8317 datasheet, and each board maker's documentation must be +consulted before a value not visible in the photos is used. In particular, do not infer +ZX-052 current capability, AD8317 connector pin order, or OLED wiring from this table. diff --git a/docs/hardware/power-tree.md b/docs/hardware/power-tree.md new file mode 100644 index 0000000..a31ff51 --- /dev/null +++ b/docs/hardware/power-tree.md @@ -0,0 +1,42 @@ +# Power tree + +## Verified Hardware + +```text +Off-board 12 V bench power source (beneath workbench) + └─ PWR-000 external two-conductor entry: +12V IN / GND IN + └─ accessible, strain-relieved enclosure entry point + └─ 18 AWG internal distribution point (Red +12 V / Black ground) + ├─ PWR-001 → TMC2209 pan VM and GND + ├─ PWR-002 → TMC2209 tilt VM and GND + ├─ PWR-003 → ZX-052 A VIN+ / VIN− + │ └─ PWR-005 → regulated 5.0 V → ESP32 5V/VIN and GND + └─ PWR-004 → ZX-052 B VIN+ / VIN− + └─ PWR-006 → regulated 5.0 V → AD8317 +5V and GND + +All ground returns meet at the defined common reference. No VOUT+ connection exists +between the two ZX-052 converters. +``` + +The 12 V battery is a permanent off-board laboratory source, not a Radiance3D part. +The enclosure provides no battery compartment, battery retention, or battery-weight +allowance. It exposes only the accessible `+12V IN` / `GND IN` entry with appropriate +strain relief before the internal distribution point. + +Both buck outputs must measure 5.0 V with a multimeter before loads are attached. The +ESP32 and AD8317 never receive raw 12 V. Keep the RF branch physically separated from +motor VM wiring; run its analog return with the detector harness to the common reference, +not through a motor phase or high-current branch. A recommended (not owned-BOM) fuse and +disconnect belong upstream of the distribution point. + +The detector's `VOUT` is a low-voltage electrical signal (`SIG-003`, silicone wire), +not an RF feedline. The AUT threads directly onto the vertically mounted detector SMA; +there is no scanner-mounted RF coax jumper. + +## AD8317 acquisition contract + +The profile records ADC1 GPIO36, a five-sample median followed by an eight-sample mean, +and calibration coefficients initially `null`. Firmware or host acquisition must store +raw ADC count, converted ADC voltage, timestamp, filter configuration, and calibration +revision. It must label any dBm result as inferred and refuse a numeric conversion until +bench-derived coefficients are committed. diff --git a/docs/hardware/reference-architecture.md b/docs/hardware/reference-architecture.md new file mode 100644 index 0000000..0b7b510 --- /dev/null +++ b/docs/hardware/reference-architecture.md @@ -0,0 +1,109 @@ +# Reference architecture — CAD design intent + +## Status and terminology + +**Conceptual architecture only. Final placement determined during CAD.** The enclosure +has not yet been designed, so this document identifies what connects and the intended +subsystem boundaries—not dimensions, mounting coordinates, cable lengths, or final +board orientations. + +### Verified Hardware + +Verified Hardware is physically owned, photographed or measured, or electrically +confirmed. The authoritative list, photographs, electrical facts, and harness standards +remain in [owned hardware](owned-hardware.md), [power tree](power-tree.md), and +[electrical/RF cabling standard](wire-standard.md). + +### Design Intent + +Design Intent is the planned enclosure layout, cable routing, conceptual subsystem +placement, and board orientation that Autodesk Fusion will resolve. It is not a claim +that any mechanical arrangement already exists. + +## PRIMARY DESIGN OBJECTIVE + +Radiance3D is a precision RF measurement instrument. Every mechanical decision should +prioritize, in order: + +1. Measurement repeatability +2. Mechanical rigidity +3. RF consistency +4. Calibration repeatability +5. Serviceability +6. Manufacturability + +Cosmetic appearance is secondary. + +## Conceptual subsystem boundaries + +```text + external stationary 5.8 GHz VTX + │ RF transmission through air + ▼ + AUT threads directly onto AD8317 SMA + │ + ┌───────────────────────────────────┐ + │ DESIGN INTENT: ROTATING PLATFORM │ + │ • tilt motor │ + │ • AD8317 detector │ + │ • antenna mount / AUT │ + └───────────────────────────────────┘ + │ + controlled rotation intent │ moving silicone harness only + (one 360° turn, not unlimited│ • tilt motor wiring + continuous rotation) │ • AD8317 5 V/GND and VOUT/AGND + │ + ┌───────────────────────────────────┐ + │ DESIGN INTENT: STATIONARY BASE │ + │ • fixed pan motor │ + │ • ESP32 DevKit │ + │ • TMC2209 pan + tilt drivers │ + │ • ZX-052 Buck A and Buck B │ + │ • internal 12 V distribution │ + │ • +12V IN / GND IN entry │ + └───────────────────────────────────┘ + │ + off-board 12 V bench source +``` + +### Electrical architecture — Verified Hardware + +The external two-conductor 12 V input feeds internal distribution, two TMC2209 VM/GND +branches, and the two independent ZX-052 5 V branches. Grounds share one defined +reference and the two 5 V outputs are never connected together. These are electrical +rules, not enclosure-placement instructions. + +### RF architecture — Verified Hardware and Design Intent + +The external stationary 5.8 GHz VTX is not mounted on the scanner. **Design Intent:** +the AUT directly threads onto the AD8317 SMA, with no detector-to-antenna RG316 jumper. +The AD8317 `VOUT` is `SIG-003` silicone wire back to the stationary ESP32 ADC. Exact +detector orientation, AUT clearance, and VTX test geometry remain CAD/bench work. + +### Mechanical intent — Design Intent + +The stationary-base and rotating-platform contents above are intended subsystem +boundaries. The pan motor is intended to remain in the stationary base; the moving +harness is intended to carry only tilt-motor and AD8317 wiring. One controlled 360° +rotation is the architecture target, not unlimited continuous rotation. + +Final CAD determines mounting locations, enclosure form, cable-guide geometry, service +loop size, strain-relief method, bend radii, bearings, clearances, and all cable lengths. +The return-to-home behavior required to prevent cumulative cable twist is described as +the **Firmware-defined return-to-home strategy to prevent cumulative cable twist**. Its +implementation and safe limits remain deferred until physical testing. + +## Future CAD work + +Fusion design will translate this conceptual architecture into measured interfaces and +then document dimensions, hardware retention, cable routing, and service access. No +unknown geometry is validated by this repository before that work exists. + +## Future Expansion + +### Not Required for Revision 1 + +The following are informational possibilities only. They do not change Revision 1 +hardware, firmware, GPIO, electrical architecture, or BOM: slip ring support, +alternate detector modules, alternate ESP32 variants, camera mounting, different motors, +protective covers, larger antennas, and additional sensors. diff --git a/docs/hardware/tmc2209-v1.3.md b/docs/hardware/tmc2209-v1.3.md new file mode 100644 index 0000000..5342a26 --- /dev/null +++ b/docs/hardware/tmc2209-v1.3.md @@ -0,0 +1,19 @@ +# TMC2209 V1.3 configuration + +## Verified Hardware + +Both axes use BIGTREETECH TMC2209 V1.3 drivers. The profile sets 16 microsteps with +interpolation enabled, 650 mA RMS commissioning current, 800 mA RMS hard ceiling, +and 30% hold current. RMS is the regulated winding-current value; it is not the peak +coil current. Do not alter the sense-resistor setting or infer a current from a trim-pot +position without confirming the board revision and the datasheet equation. + +Pan is PDN/UART address 0; tilt is address 1. The physical PDN/UART topology and any +required series/resistor network must be confirmed against the photographed carrier +before UART is connected. STEP/DIR motion remains the operational interface. StallGuard +telemetry is not a physical home sensor and cannot authorize a homing routine. + +Before powered movement: inspect every terminal, verify the phase harness, set the +current in firmware, attach the heatsink without covering an adjustment point, and +then start at the configured 650 mA RMS. Stop if a driver or motor becomes excessively +hot, faults, or loses steps. Bench testing must confirm direction before any scan. diff --git a/docs/hardware/wire-standard.md b/docs/hardware/wire-standard.md new file mode 100644 index 0000000..5ca99d7 --- /dev/null +++ b/docs/hardware/wire-standard.md @@ -0,0 +1,57 @@ +# Electrical and RF cabling standard + +## Verified Hardware + +Radiance3D has two non-interchangeable cabling systems. + +- **Silicone wire** carries every DC, motor, and low-voltage signal. Available sizes + are 18, 22, and 26 AWG in Black, Red, White, Yellow, and Blue. +- **RG316 50 Ω coax** carries RF signal paths only. Every RF drawing depicts its centre + conductor and shield, never an ordinary two-conductor wire. + +Silicone wire must never replace RG316 for an RF path. RG316 must never carry power, +ground distribution, GPIO, UART, STEP, DIR, EN, ADC, E-stop, limit-switch, or motor +connections. Color is never a sole identifier: every conductor has a harness ID, +signal name, from/to endpoint, and labels at both ends. + +## Silicone-wire standard + +| Gauge | Approved use | Color convention | +|---|---|---| +| 18 AWG | external 12 V entry; internal distribution-to-buck inputs; practical VM/GND branches | Red +12 V; Black power ground | +| 22 AWG | motor extensions; regulated 5 V/GND branches | motor: Black A+, Yellow A− labelled `A- / MOTOR GREEN`, Red B+, Blue B−; supply: Red +5 V, Black GND | +| 26 AWG | STEP, DIR, EN, PDN_UART, ADC, E-stop, future limit switches and diagnostics | White primary data/STEP/UART; Yellow DIR/EN; Blue analog/auxiliary; Black signal ground | + +## Electrical harnesses — silicone wire + +| Harness | Endpoints and signals | Gauge / color | Required labels | +|---|---|---|---| +| PWR-000 | off-board bench source → enclosure `+12V IN` / `GND IN` entry | 18 AWG Red/Black | `+12V IN`, `GND IN`, source/enclosure | +| PWR-001 | distribution → pan driver VM/GND | 18 AWG Red/Black | `PWR-001 +12V`, `PWR-001 GND` | +| PWR-002 | distribution → tilt driver VM/GND | 18 AWG Red/Black | `PWR-002 +12V`, `PWR-002 GND` | +| PWR-003 | distribution → Buck A VIN+/VIN− | 18 AWG Red/Black | endpoint labels | +| PWR-004 | distribution → Buck B VIN+/VIN− | 18 AWG Red/Black | endpoint labels | +| PWR-005 | Buck A VOUT+/VOUT− → ESP32 5V/VIN/GND | 22 AWG Red/Black | `5V`, `GND`, source/destination | +| PWR-006 | Buck B VOUT+/VOUT− → AD8317 +5V/GND | 22 AWG Red/Black | `5V`, `GND`, source/destination | +| SIG-001 | GPIO25 STEP, 26 DIR, 27 ENN, 17 TX, 16 RX, GND → pan driver | 26 AWG | each signal plus endpoints | +| SIG-002 | GPIO18 STEP, 19 DIR, 23 ENN, 14 TX, 39 RX, GND → tilt driver | 26 AWG | each signal plus endpoints | +| SIG-003 | AD8317 VOUT/GND → GPIO36/GND | 26 AWG Blue/Black | `AD8317 VOUT → GPIO36`, `AGND` | +| SIG-004 | future E-stop or limit switch only | 26 AWG | signal, endpoints, pull configuration | +| MTR-001 | pan driver A1/A2/B1/B2 → motor Black/Yellow/Red/Blue | 22 AWG Black/Yellow/Red/Blue | `A+`, `A- / MOTOR GREEN`, `B+`, `B-` | +| MTR-002 | tilt driver A1/A2/B1/B2 → motor Black/Yellow/Red/Blue | 22 AWG Black/Yellow/Red/Blue | `A+`, `A- / MOTOR GREEN`, `B+`, `B-` | + +Motor terminal mapping is mandatory: A1/A+ → Black, A2/A− → Yellow extension +(motor Green), B1/B+ → Red, B2/B− → Blue. Never connect or disconnect a motor while +its driver is powered. + +## RF harnesses — RG316 50 Ω coax + +| Harness | RF path | Construction and drawing rule | Status | +|---|---|---|---| +| RF-001 | external stationary VTX output → its external transmit antenna | RG316 centre conductor is RF signal; braided shield is the RF return/shield | off-scanner path; connector/endpoints require bench confirmation | + +The AUT threads directly onto the vertically mounted AD8317 SMA: there is no baseline +RG316 jumper or `RF-002` path between antenna and detector. Do not use the AD8317 +`VOUT` analog connection as an RF harness: it is `SIG-003` silicone wire. Any future +RF path receives the next `RF-###` identifier and is drawn as coax with separate centre +and shield representations. diff --git a/docs/software/file-formats.md b/docs/software/file-formats.md new file mode 100644 index 0000000..f1c66c8 --- /dev/null +++ b/docs/software/file-formats.md @@ -0,0 +1,5 @@ +# File formats + +Scan records use the versioned JSON schema in `data/schemas/scan-v1.schema.json`. +Simulated data is explicitly labelled. AD8317 acquisition records must distinguish raw +ADC counts and volts from any calibration-derived RF-power estimate. diff --git a/firmware/config/provisional-esp32dev-v1.json b/firmware/config/provisional-esp32dev-v1.json deleted file mode 100644 index 492b888..0000000 --- a/firmware/config/provisional-esp32dev-v1.json +++ /dev/null @@ -1,139 +0,0 @@ -{ - "status": "version-1-hardware-baseline-pending-validation", - "controller": { - "board": "ESP32 development board", - "module": "ESP-WROOM-32", - "logic_voltage_v": 3.3, - "transport": "USB serial", - "transport_note": "Wi-Fi and Bluetooth are available but intentionally unused in Version 1", - "gpio_mapping_status": "provisional-until-board-revision-confirmed", - "protocol_version": 1, - "usb_serial_baud": 115200, - "emergency_stop_pin": 13, - "emergency_stop_active_low": true, - "emergency_stop_pullup": true, - "emergency_stop_pulldown": false, - "emergency_stop_debounce_ms": 10 - }, - "power": { - "motor_supply_voltage_v": 12.0, - "motor_supply_source": "standalone 12 V automotive battery, not connected to a running vehicle", - "motor_supply_fuse_a": 3.0, - "motor_supply_distribution": "12 V battery -> inline fuse -> master disconnect -> TMC2209 VM rails and LM2596 buck converter", - "esp32_supply_voltage_v": 5.0, - "esp32_supply_source": "LM2596 buck converter for logic electronics only", - "motors_powered_through_buck": false, - "buck_converters": [ - "SELOKY LM2596", - "LYLANMO LM2596S" - ] - }, - "drivers": { - "driver_family": "BIGTREETECH TMC2209 V1.3", - "driver_interface": "UART controlled STEP/DIR", - "driver_diagnostics_enabled": true, - "enable_active_low_configurable": true, - "enable_active_low": true, - "uart_baud": 115200, - "uart_timeout_ms": 20, - "sense_resistor_milliohms": 110, - "single_wire_pdn_uart": true, - "write_echo_expected": true, - "current_configured_in_rms_ma": true, - "ifcnt_write_verification_required": true, - "verify": [ - "R10 configuration", - "UART wiring", - "sense resistor value", - "board revision", - "carrier pinout" - ], - "electrical_profile_status": "provisional; confirm R10 and PDN_UART topology before energizing motors" - }, - "motors": { - "manufacturer": "YEJMKJ / LYLANMO", - "type": "NEMA 17 bipolar 4-wire 1.8 degree full-step", - "full_steps_per_revolution": 200, - "rated_phase_current_a": 1.0, - "phase_resistance_ohm": 3.5, - "holding_torque_nm": 0.13, - "dimensions_mm": { - "width": 42, - "height": 42, - "depth": 21 - }, - "notes": "Selected hardware values from the Version 1 build, not generic placeholders" - }, - "axes": { - "azimuth": { - "driver": "tmc2209", - "driver_profile": "BIGTREETECH TMC2209 V1.3", - "uart_channel": 1, - "uart_address": 0, - "uart_tx_pin": 22, - "uart_rx_pin": 21, - "step_pin": 25, - "direction_pin": 26, - "enable_pin": 27, - "home_switch_pin": 32, - "home_switch_normally_closed": true, - "home_switch_pullup": true, - "home_switch_pulldown": false, - "homing_direction_negative": true, - "home_switch_debounce_ms": 10, - "motor_full_steps_per_revolution": 200, - "microsteps": 16, - "gear_ratio": 1.0, - "direction_inverted": false, - "minimum_angle_deg": 0.0, - "maximum_angle_deg": 360.0, - "home_offset_deg": 0.0, - "commissioning_current_ma": 650, - "maximum_rms_current_ma": 1000, - "hold_current_percent": 30, - "max_speed_deg_s": 10.0, - "acceleration_deg_s2": 20.0, - "home_speed_deg_s": 5.0, - "slow_home_speed_deg_s": 1.0, - "homing_backoff_deg": 3.0, - "settling_time_ms": 250, - "maximum_bench_test_steps": 3200, - "motion_timeout_ms": 60000 - }, - "elevation": { - "driver": "tmc2209", - "driver_profile": "BIGTREETECH TMC2209 V1.3", - "uart_channel": 2, - "uart_address": 0, - "uart_tx_pin": 17, - "uart_rx_pin": 16, - "step_pin": 18, - "direction_pin": 19, - "enable_pin": 23, - "home_switch_pin": 33, - "home_switch_normally_closed": true, - "home_switch_pullup": true, - "home_switch_pulldown": false, - "homing_direction_negative": true, - "home_switch_debounce_ms": 10, - "motor_full_steps_per_revolution": 200, - "microsteps": 16, - "gear_ratio": 1.0, - "direction_inverted": false, - "minimum_angle_deg": -90.0, - "maximum_angle_deg": 90.0, - "home_offset_deg": 0.0, - "commissioning_current_ma": 650, - "maximum_rms_current_ma": 1000, - "hold_current_percent": 40, - "max_speed_deg_s": 8.0, - "acceleration_deg_s2": 15.0, - "home_speed_deg_s": 4.0, - "slow_home_speed_deg_s": 1.0, - "homing_backoff_deg": 3.0, - "settling_time_ms": 250, - "maximum_bench_test_steps": 3200, - "motion_timeout_ms": 60000 - } - } -} diff --git a/firmware/config/radiance3d-owned-hardware.json b/firmware/config/radiance3d-owned-hardware.json new file mode 100644 index 0000000..5f224a6 --- /dev/null +++ b/firmware/config/radiance3d-owned-hardware.json @@ -0,0 +1,107 @@ +{ + "profile_id": "radiance3d-owned-hardware-v1", + "status": "bench-confirmation-required", + "visual_reference_root": "Part Numbers:Views", + "controller": { + "manufacturer": "ELEGOO", + "board": "ESP32 DevKit-style USB-C board with rear OLED", + "module": "ESP-WROOM-32", + "photo": "ESP-32-elegoo top view(part #EL-SM-012).jpeg", + "logic_voltage_v": 3.3, + "input_voltage_v": 5.0, + "reserved_oled_pins": [21, 22], + "protocol_version": 1, + "usb_serial_baud": 115200, + "emergency_stop_pin": 13, + "emergency_stop_active_low": true, + "emergency_stop_pullup": true, + "emergency_stop_pulldown": false, + "emergency_stop_debounce_ms": 10 + }, + "power": { + "external_input": { + "location": "permanent bench power source beneath workbench", + "conductors": ["+12V IN", "GND IN"], + "assembly_entry_requirement": "accessible strain-relieved external 12V power entry" + }, + "motor_supply_voltage_v": 12.0, + "digital_control_voltage_v": 5.0, + "rf_analog_voltage_v": 5.0, + "buck_converters": [ + {"model": "ZX-052 V2.0", "branch": "digital-control", "input": "internal-12V-distribution", "output": "ESP32-VIN"}, + {"model": "ZX-052 V2.0", "branch": "rf-analog", "input": "internal-12V-distribution", "output": "AD8317-5V"} + ], + "outputs_paralleled": false, + "common_ground_required": true + }, + "mechanical_architecture": { + "status": "conceptual-for-Autodesk-Fusion-design; not dimensioned", + "enclosure_designed": false, + "stationary_base": ["ESP32 DevKit", "two BIGTREETECH TMC2209 V1.3 drivers", "ZX-052 Buck Converter A", "ZX-052 Buck Converter B", "external +12V/GND input", "internal power distribution"], + "rotating_platform": ["tilt motor", "AD8317 detector", "antenna mount"], + "pan_rotation": "one controlled 360 degree rotation; not unlimited continuous rotation", + "moving_harness": "tilt motor and AD8317 wiring only; lengths TBD AFTER CAD", + "rf_measurement": "external stationary 5.8GHz VTX; AUT directly threads onto vertically mounted AD8317 SMA; no RG316 jumper" + }, + "drivers": { + "driver_family": "BIGTREETECH TMC2209 V1.3", + "quantity": 2, + "photo": "TMC2209 v1.3 top view with heatsink.jpeg", + "driver_interface": "STEP/DIR with PDN_UART diagnostics", + "uart_baud": 115200, + "uart_timeout_ms": 20, + "sense_resistor_milliohms": 110, + "single_wire_pdn_uart": false, + "write_echo_expected": false, + "enable_active_low": true, + "interpolation": true, + "microsteps": 16, + "cooling": "install supplied heatsink after electrical inspection and before powered motion testing" + }, + "motors": { + "manufacturer": "YEJMKJ", + "model": "42HDB0014NC", + "quantity": 2, + "photo": "NEMA-17(part #YEJMKJ).jpeg", + "full_steps_per_revolution": 200, + "rated_phase_current_a": 1.0, + "phase_resistance_ohm": 3.5, + "holding_torque_nm": 0.13, + "dimensions_mm": [42, 42, 21], + "phase_wiring": {"A+": "Black", "A-": "Green", "B+": "Red", "B-": "Blue"} + }, + "detector": { + "model": "AD8317 EVB / module V1.0", + "quantity": 1, + "photo": null, + "supply_voltage_v": 5.0, + "adc_pin": 36, + "adc_unit": "ADC1", + "filter": {"kind": "median_then_mean", "median_samples": 5, "mean_samples": 8}, + "calibration": {"enabled": false, "volts_per_db": null, "intercept_dbm": null, "note": "Raw ADC and volts are valid; inferred dBm requires bench calibration."} + }, + "axes": { + "azimuth": { + "driver": "BIGTREETECH TMC2209 V1.3", "driver_profile": "BIGTREETECH TMC2209 V1.3", "uart_channel": 2, "uart_address": 0, + "uart_tx_pin": 17, "uart_rx_pin": 16, "step_pin": 25, "direction_pin": 26, "enable_pin": 27, + "home_switch_pin": -1, "home_switch_normally_closed": false, "home_switch_pullup": false, "home_switch_pulldown": false, + "homing_direction_negative": true, "home_switch_debounce_ms": 10, + "motor_full_steps_per_revolution": 200, "microsteps": 16, "gear_ratio": 1.0, "direction_inverted": false, + "minimum_angle_deg": -90.0, "maximum_angle_deg": 90.0, "home_offset_deg": 0.0, + "commissioning_current_ma": 650, "maximum_rms_current_ma": 800, "hold_current_percent": 30, + "max_speed_deg_s": 10.0, "acceleration_deg_s2": 20.0, "home_speed_deg_s": 5.0, "slow_home_speed_deg_s": 1.0, + "homing_backoff_deg": 3.0, "settling_time_ms": 250, "maximum_bench_test_steps": 3200, "motion_timeout_ms": 60000 + }, + "elevation": { + "driver": "BIGTREETECH TMC2209 V1.3", "driver_profile": "BIGTREETECH TMC2209 V1.3", "uart_channel": 1, "uart_address": 1, + "uart_tx_pin": 14, "uart_rx_pin": 39, "step_pin": 18, "direction_pin": 19, "enable_pin": 23, + "home_switch_pin": -1, "home_switch_normally_closed": false, "home_switch_pullup": false, "home_switch_pulldown": false, + "homing_direction_negative": true, "home_switch_debounce_ms": 10, + "motor_full_steps_per_revolution": 200, "microsteps": 16, "gear_ratio": 1.0, "direction_inverted": false, + "minimum_angle_deg": -45.0, "maximum_angle_deg": 45.0, "home_offset_deg": 0.0, + "commissioning_current_ma": 650, "maximum_rms_current_ma": 800, "hold_current_percent": 30, + "max_speed_deg_s": 8.0, "acceleration_deg_s2": 15.0, "home_speed_deg_s": 4.0, "slow_home_speed_deg_s": 1.0, + "homing_backoff_deg": 3.0, "settling_time_ms": 250, "maximum_bench_test_steps": 3200, "motion_timeout_ms": 60000 + } + } +} diff --git a/firmware/controller/CMakeLists.txt b/firmware/controller/CMakeLists.txt index 866688f..08bba73 100644 --- a/firmware/controller/CMakeLists.txt +++ b/firmware/controller/CMakeLists.txt @@ -16,7 +16,7 @@ endif() find_package(Python3 REQUIRED COMPONENTS Interpreter) set(RADIANCE3D_PROFILE - "${CMAKE_CURRENT_LIST_DIR}/../config/provisional-esp32dev-v1.json") + "${CMAKE_CURRENT_LIST_DIR}/../config/radiance3d-owned-hardware.json") set(RADIANCE3D_GENERATED_INCLUDE_DIR "${CMAKE_BINARY_DIR}/generated") file(MAKE_DIRECTORY "${RADIANCE3D_GENERATED_INCLUDE_DIR}") set(RADIANCE3D_GENERATED_PROFILE_HEADER diff --git a/firmware/controller/host/CMakeLists.txt b/firmware/controller/host/CMakeLists.txt index 3397ae2..56460de 100644 --- a/firmware/controller/host/CMakeLists.txt +++ b/firmware/controller/host/CMakeLists.txt @@ -11,13 +11,13 @@ file(MAKE_DIRECTORY "${RADIANCE3D_GENERATED_INCLUDE_DIR}") set(RADIANCE3D_GENERATED_PROFILE_HEADER "${RADIANCE3D_GENERATED_INCLUDE_DIR}/hardware_profile_generated.hpp") set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS - "${RADIANCE3D_CONTROLLER_DIR}/../config/provisional-esp32dev-v1.json" + "${RADIANCE3D_CONTROLLER_DIR}/../config/radiance3d-owned-hardware.json" "${RADIANCE3D_REPOSITORY_DIR}/scripts/generate_hardware_profile_header.py") execute_process( COMMAND "${Python3_EXECUTABLE}" "${RADIANCE3D_REPOSITORY_DIR}/scripts/generate_hardware_profile_header.py" - --profile "${RADIANCE3D_CONTROLLER_DIR}/../config/provisional-esp32dev-v1.json" + --profile "${RADIANCE3D_CONTROLLER_DIR}/../config/radiance3d-owned-hardware.json" --output "${RADIANCE3D_GENERATED_PROFILE_HEADER}" RESULT_VARIABLE RADIANCE3D_PROFILE_RESULT ERROR_VARIABLE RADIANCE3D_PROFILE_ERROR diff --git a/firmware/controller/main/controller_runtime.cpp b/firmware/controller/main/controller_runtime.cpp index f4189ae..e19b89c 100644 --- a/firmware/controller/main/controller_runtime.cpp +++ b/firmware/controller/main/controller_runtime.cpp @@ -510,6 +510,9 @@ bool configure_input_interrupts() { IsrInput* const inputs[] = {&state.emergency_input, &state.azimuth_home_input, &state.elevation_home_input}; for (std::size_t index = 0; index < 3; ++index) { + if (pins[index] < 0) { + continue; + } const gpio_num_t pin = static_cast(pins[index]); if (gpio_set_intr_type(pin, GPIO_INTR_ANYEDGE) != ESP_OK || gpio_isr_handler_add(pin, input_isr, inputs[index]) != ESP_OK) { diff --git a/firmware/controller/src/axis_controller.cpp b/firmware/controller/src/axis_controller.cpp index 610568f..2221bee 100644 --- a/firmware/controller/src/axis_controller.cpp +++ b/firmware/controller/src/axis_controller.cpp @@ -112,10 +112,11 @@ bool AxisController::initialize() { state_.enabled = false; state_.trust_loss_reason = TrustLossReason::startup; if (config_.name == nullptr || config_.name[0] == '\0' || - config_.home_switch_pin < 0 || !config_.motion.valid() || + !config_.motion.valid() || config_.motion.motor_rms_current_ma == 0 || - !platform_.configure_pin(config_.home_switch_pin, - config_.home_switch_input_mode) || + (config_.home_switch_pin >= 0 && + !platform_.configure_pin(config_.home_switch_pin, + config_.home_switch_input_mode)) || !driver_.initialize()) { state_.fault = driver_.is_connected() ? FaultCode::invalid_configuration @@ -173,6 +174,9 @@ void AxisController::mark_position_untrusted(const TrustLossReason reason) { } bool AxisController::home_switch_active_raw() const { + if (config_.home_switch_pin < 0) { + return false; + } const bool level_high = platform_.read_pin(config_.home_switch_pin); return config_.motion.homing.switch_normally_closed ? level_high : !level_high; @@ -321,6 +325,9 @@ MotionResult AxisController::bench_move_steps( } MotionResult AxisController::start_homing(const std::uint32_t command_id) { + if (config_.home_switch_pin < 0) { + return MotionResult{false, FaultCode::invalid_configuration}; + } if (state_.moving) { return MotionResult{false, FaultCode::invalid_argument}; } diff --git a/firmware/controller/src/hardware_config.cpp b/firmware/controller/src/hardware_config.cpp index 6e5c9d4..0671326 100644 --- a/firmware/controller/src/hardware_config.cpp +++ b/firmware/controller/src/hardware_config.cpp @@ -120,7 +120,7 @@ GpioValidationResult validate_esp32_gpio( config.emergency_stop_pin, }; for (std::size_t index = 0; index < pins.size(); ++index) { - if (index == pins.size() - 1 && pins[index] < 0) { + if (pins[index] < 0) { continue; } if (!valid_gpio(pins[index])) { @@ -131,7 +131,7 @@ GpioValidationResult validate_esp32_gpio( result.bootstrapping_pin_mask |= 1ULL << pins[index]; } for (std::size_t other = index + 1; other < pins.size(); ++other) { - if (other == pins.size() - 1 && pins[other] < 0) { + if (pins[other] < 0) { continue; } if (pins[index] == pins[other]) { diff --git a/firmware/controller/src/motion_controller.cpp b/firmware/controller/src/motion_controller.cpp index c575eac..dc7d528 100644 --- a/firmware/controller/src/motion_controller.cpp +++ b/firmware/controller/src/motion_controller.cpp @@ -89,12 +89,12 @@ bool ControllerConfig::valid() const { ControllerConfig provisional_simulator_config() { ControllerConfig config; - config.azimuth.minimum_angle_deg = 0.0; - config.azimuth.maximum_angle_deg = 360.0; + config.azimuth.minimum_angle_deg = -90.0; + config.azimuth.maximum_angle_deg = 90.0; config.azimuth.home_offset_deg = 0.0; config.azimuth.motor_rms_current_ma = 400; - config.elevation.minimum_angle_deg = -90.0; - config.elevation.maximum_angle_deg = 90.0; + config.elevation.minimum_angle_deg = -45.0; + config.elevation.maximum_angle_deg = 45.0; config.elevation.home_offset_deg = 0.0; config.elevation.motor_rms_current_ma = 400; return config; diff --git a/firmware/controller/test/test_motion/test_main.cpp b/firmware/controller/test/test_motion/test_main.cpp index 7ff4042..dfbd20d 100644 --- a/firmware/controller/test/test_motion/test_main.cpp +++ b/firmware/controller/test/test_motion/test_main.cpp @@ -49,8 +49,8 @@ void test_motion_requires_homing_and_uses_configured_limits() { TEST_ASSERT_TRUE(engine.handle("MOVE 0 0 5").find("ERR NOT_HOMED") == 0); TEST_ASSERT_EQUAL_STRING("OK HOME AXIS=BOTH", engine.handle("HOME BOTH").c_str()); - TEST_ASSERT_TRUE(engine.handle("SCAN_STEP 359 -90 5").find("OK SCAN_STEP") == 0); - TEST_ASSERT_TRUE(engine.handle("MOVE 0 91 5").find("ERR LIMIT_REACHED") == 0); + TEST_ASSERT_TRUE(engine.handle("SCAN_STEP 90 -45 5").find("OK SCAN_STEP") == 0); + TEST_ASSERT_TRUE(engine.handle("MOVE 0 46 5").find("ERR LIMIT_REACHED") == 0); } void test_stop_invalidates_position_and_requires_rehoming() { diff --git a/firmware/controller/test/test_physical/test_main.cpp b/firmware/controller/test/test_physical/test_main.cpp index d601c33..8f01e01 100644 --- a/firmware/controller/test/test_physical/test_main.cpp +++ b/firmware/controller/test/test_physical/test_main.cpp @@ -193,7 +193,7 @@ void test_compiled_defaults_are_generated_from_the_hardware_profile() { radiance3d::generated_profile::kElevation.commissioning_current_ma, config.elevation.axis.motion.motor_rms_current_ma); TEST_ASSERT_EQUAL_UINT8(30, config.azimuth.axis.motion.hold_current_percent); - TEST_ASSERT_EQUAL_UINT8(40, config.elevation.axis.motion.hold_current_percent); + TEST_ASSERT_EQUAL_UINT8(30, config.elevation.axis.motion.hold_current_percent); TEST_ASSERT_EQUAL_UINT16( radiance3d::generated_profile::kAzimuth.maximum_rms_current_ma, config.azimuth.driver.maximum_rms_current_ma); @@ -201,8 +201,8 @@ void test_compiled_defaults_are_generated_from_the_hardware_profile() { config.azimuth.driver.uart_baud); TEST_ASSERT_EQUAL_UINT32(radiance3d::generated_profile::kTmcUartTimeoutMs, config.elevation.driver.uart_timeout_ms); - TEST_ASSERT_TRUE(config.azimuth.driver.uart_single_wire); - TEST_ASSERT_TRUE(config.azimuth.driver.write_echo_expected); + TEST_ASSERT_FALSE(config.azimuth.driver.uart_single_wire); + TEST_ASSERT_FALSE(config.azimuth.driver.write_echo_expected); TEST_ASSERT_EQUAL_INT(1, config.azimuth.axis.motion.gear_ratio.numerator); TEST_ASSERT_EQUAL_INT(1, config.azimuth.axis.motion.gear_ratio.denominator); } diff --git a/scripts/check_repository.py b/scripts/check_repository.py index 45220f9..b223e8c 100755 --- a/scripts/check_repository.py +++ b/scripts/check_repository.py @@ -67,7 +67,7 @@ def check_json() -> list[str]: def check_hardware_profile() -> list[str]: generator = ROOT / "scripts" / "generate_hardware_profile_header.py" - profile = ROOT / "firmware" / "config" / "provisional-esp32dev-v1.json" + profile = ROOT / "firmware" / "config" / "radiance3d-owned-hardware.json" try: result = subprocess.run( [sys.executable, str(generator), "--profile", str(profile), "--validate-only"], @@ -151,6 +151,92 @@ def check_empty_files() -> list[str]: return errors +def check_owned_hardware_contract() -> list[str]: + """Reject production documentation/configuration that drifts from owned parts.""" + errors: list[str] = [] + forbidden = re.compile(r"\b(A4988|DRV8825|TMC2208|AD8318|RX5808|Raspberry\s*Pi|Arduino|slip\s*ring)\b", re.I) + for path in ROOT.rglob("*"): + if not path.is_file() or path.suffix not in {".md", ".json", ".cpp", ".hpp", ".py"}: + continue + if "Measurements" in path.parts or path == Path(__file__): + continue + text = path.read_text(encoding="utf-8") + # Future-expansion examples are explicitly non-production and may mention + # hardware that is intentionally excluded from Revision 1. + if path == ROOT / "docs" / "hardware" / "reference-architecture.md": + text = text.split("## Future Expansion", 1)[0] + match = forbidden.search(text) + if match: + errors.append(f"{path.relative_to(ROOT)}: unsupported production hardware {match.group(0)!r}") + profile_path = ROOT / "firmware/config/radiance3d-owned-hardware.json" + try: + profile = json.loads(profile_path.read_text(encoding="utf-8")) + power = profile["power"] + detector = profile["detector"] + axes = profile["axes"] + except (OSError, KeyError, TypeError, json.JSONDecodeError) as exc: + return [f"owned hardware profile cannot be checked: {exc}"] + if power.get("outputs_paralleled") is not False: + errors.append("owned hardware profile: buck outputs are paralleled") + external_input = power.get("external_input", {}) + if external_input.get("conductors") != ["+12V IN", "GND IN"]: + errors.append("owned hardware profile: external input must be +12V IN / GND IN") + if "strain-relieved" not in external_input.get("assembly_entry_requirement", ""): + errors.append("owned hardware profile: external input requires strain relief") + architecture = profile.get("mechanical_architecture", {}) + if architecture.get("enclosure_designed") is not False: + errors.append("owned hardware profile: enclosure must remain undesigned") + if "one controlled 360 degree rotation" not in architecture.get("pan_rotation", ""): + errors.append("owned hardware profile: pan must specify one controlled 360 degree rotation") + if "tilt motor" not in architecture.get("rotating_platform", []) or "AD8317 detector" not in architecture.get("rotating_platform", []): + errors.append("owned hardware profile: rotating platform must contain tilt motor and AD8317") + if "no RG316 jumper" not in architecture.get("rf_measurement", ""): + errors.append("owned hardware profile: direct-SMA detector mounting is required") + if power.get("digital_control_voltage_v") != 5.0: + errors.append("owned hardware profile: ESP32 branch must be regulated 5.0 V") + if power.get("rf_analog_voltage_v") != 5.0: + errors.append("owned hardware profile: AD8317 branch must be regulated 5.0 V") + if detector.get("adc_unit") != "ADC1": + errors.append("owned hardware profile: detector must use ADC1") + for name, axis in axes.items(): + if axis.get("maximum_rms_current_ma", 0) > 800: + errors.append(f"owned hardware profile: {name} current exceeds 800 mA RMS ceiling") + if axis.get("home_switch_pin") != -1: + errors.append(f"owned hardware profile: {name} assumes an unowned home switch") + harness = (ROOT / "docs/hardware/wire-standard.md").read_text(encoding="utf-8") + for required in ("18 AWG", "22 AWG", "26 AWG", "PWR-000", "PWR-001", "SIG-001", "MTR-001", + "A1/A+ → Black", "A2/A− → Yellow", "B1/B+ → Red", "B2/B− → Blue", + "labels at both ends"): + if required not in harness: + errors.append(f"harness contract missing {required!r}") + for required in ("RG316 50 Ω coax", "RF-001", "centre conductor", "shield", + "no baseline\nRG316 jumper"): + if required not in harness: + errors.append(f"RF cabling contract missing {required!r}") + if "| RF-002 |" in harness: + errors.append("RF cabling contract contains prohibited antenna-to-detector jumper") + if "H-" in harness: + errors.append("harness contract contains retired mixed-purpose H- identifier") + routing = (ROOT / "docs/hardware/assembly-order.md").read_text(encoding="utf-8") + if "Unlimited continuous pan rotation is not allowed." not in routing: + errors.append("cable-routing contract permits unlimited pan rotation") + if "one controlled 360° turn" not in routing: + errors.append("cable-routing contract lacks controlled 360 degree pan rotation") + power_tree = (ROOT / "docs/hardware/power-tree.md").read_text(encoding="utf-8") + for required in ("Off-board 12 V bench power source", "+12V IN", "GND IN", "no battery compartment"): + if required not in power_tree: + errors.append(f"external-power contract missing {required!r}") + reference = (ROOT / "docs/hardware/reference-architecture.md").read_text(encoding="utf-8") + for required in ("DESIGN INTENT: STATIONARY BASE", "DESIGN INTENT: ROTATING PLATFORM", + "external stationary 5.8 GHz VTX", "AUT directly threads onto the AD8317 SMA", + "no detector-to-antenna RG316 jumper", "Conceptual architecture only.", + "Final placement determined during CAD.", + "Firmware-defined return-to-home strategy to prevent cumulative cable twist"): + if required not in reference: + errors.append(f"reference architecture missing {required!r}") + return errors + + def main() -> int: errors = [ *check_required_paths(), @@ -159,6 +245,7 @@ def main() -> int: *check_physical_firmware_has_no_arduino_dependency(), *check_internal_links(), *check_empty_files(), + *check_owned_hardware_contract(), ] if errors: for error in errors: diff --git a/scripts/generate_hardware_profile_header.py b/scripts/generate_hardware_profile_header.py index 5bcdfae..f2d7715 100644 --- a/scripts/generate_hardware_profile_header.py +++ b/scripts/generate_hardware_profile_header.py @@ -132,7 +132,8 @@ def _axis(profile: dict[str, Any], name: str) -> dict[str, Any]: ("maximum_bench_test_steps", 2**31 - 1), ("motion_timeout_ms", 2**31 - 1), ): - _int(axis.get(key), f"{prefix}.{key}", minimum=1 if key not in {"uart_address"} else 0, maximum=upper) + minimum = -1 if key == "home_switch_pin" else (0 if key == "uart_address" else 1) + _int(axis.get(key), f"{prefix}.{key}", minimum=minimum, maximum=upper) if axis["commissioning_current_ma"] > axis["maximum_rms_current_ma"]: raise ProfileError(f"{prefix} commissioning current exceeds its safe ceiling") _string(axis.get("driver"), f"{prefix}.driver") @@ -190,11 +191,21 @@ def validate_profile(profile: dict[str, Any]) -> tuple[dict[str, Any], dict[str, for key in ("uart_tx_pin", "uart_rx_pin", "step_pin", "direction_pin", "enable_pin", "home_switch_pin") ], ] + pins = [pin for pin in pins if pin >= 0] if len(pins) != len(set(pins)): raise ProfileError("controller and axis GPIO assignments must be unique") for axis in (azimuth, elevation): if axis["step_pin"] >= 34 or axis["direction_pin"] >= 34 or axis["enable_pin"] >= 34 or axis["uart_tx_pin"] >= 34: raise ProfileError("STEP/DIR/ENABLE/TX cannot use input-only ESP32 GPIOs") + detector = _mapping(profile.get("detector"), "detector") + adc_pin = _int(detector.get("adc_pin"), "detector.adc_pin", minimum=0, maximum=39) + if adc_pin not in {32, 33, 34, 35, 36, 37, 38, 39}: + raise ProfileError("detector.adc_pin must be an ADC1-capable ESP32 GPIO") + if adc_pin in pins: + raise ProfileError("detector ADC pin must not share a controller GPIO") + power = _mapping(profile.get("power"), "power") + if power.get("outputs_paralleled") is not False: + raise ProfileError("buck converter outputs must not be paralleled") return controller, drivers, {"azimuth": azimuth, "elevation": elevation}