From 85774ac2f3274e3d75cf39bf32911426d989ddab Mon Sep 17 00:00:00 2001 From: Aurelien Labrosse Date: Sat, 14 Nov 2026 00:57:27 +0100 Subject: [PATCH 1/5] docs: tinySensor connectors and off-board wiring guide with SVG diagram Connector pinout from the v3.0 netlist, jumper settings, on-board parts, and wiring of each off-board module (battery, 3.3 V module, radio, BME280, DS18B20, analog sensor) with the project colour convention. Kept apart from the receiver guide in hardware/receiver/. Co-Authored-By: Claude Opus 5.5 --- hardware/tinysensor/WIRING.md | 121 +++++++++++++++++++++++++ hardware/tinysensor/wiring.svg | 156 +++++++++++++++++++++++++++++++++ 2 files changed, 277 insertions(+) create mode 100644 hardware/tinysensor/WIRING.md create mode 100644 hardware/tinysensor/wiring.svg diff --git a/hardware/tinysensor/WIRING.md b/hardware/tinysensor/WIRING.md new file mode 100644 index 0000000..40a1c9a --- /dev/null +++ b/hardware/tinysensor/WIRING.md @@ -0,0 +1,121 @@ +# tinySensor board: connectors and off-board wiring + +This is the **tinySensor emitter** board (ATtiny84A, `hardware/tinysensor/`). The 433 MHz receiver with OLED is documented separately in [`hardware/receiver/WIRING.md`](../receiver/WIRING.md). + +Pin numbers come from the v3.0 schematic (`tinySensorv1.kicad_sch`, netlist exported with `kicad-cli`). v2.2 boards share the same connectors, with the defects and the fixes listed in [`REWORK_v2.2.md`](REWORK_v2.2.md). Build flags per sensor unit: [`docs/environments.md`](../../docs/environments.md). + +![tinySensor connectors and modules](wiring.svg) + +## Connectors + +| Ref | Name | Pin 1 | Pin 2 | Pin 3 | Pins 4–6 | +|---|---|---|---|---|---| +| J13 | RAW IN (screw terminal) | +RAW (square pad) | GND | | | +| J2 | 3.3 V module (1×03 header) | +BATT | GND | VCC | | +| J1 | RADIO | GND | VCC | DATA (PB0) | | +| J3 | I2C 1 (BME280) | SENSOR_VCC | GND | SCL (PA4) | 4 SDA (PA6), 5 n.c., 6 SDO (JP6) | +| J14 | I2C 2 | same as J3 | | | | +| J5 | OneWire | SENSOR_VCC | GND | DQ (PA3) | | +| J7 | Analog sensor | SENSOR_VCC | AIN (PA0, through R4 75 Ω) | GND with JP4 2-3 | | +| J8 | EXT. INT0 | PB2 | GND | | | +| J4 | Digital out | PA7 | GND | | | +| J6 | ISP | MISO (PA5) | VCC | SCK (PA4, = SCL) | 4 MOSI (PA6, = SDA), 5 RESET, 6 GND | +| J10 | CR2032 socket | +BATT | GND | | | +| BT1 | 18650 holder | + on RAW_IN | − on GND | | | + +SENSOR_VCC is PA2: J3, J14, J5 and J7 are powered only while the firmware measures. + +## Jumpers + +| Jumper | Setting | Effect | +|---|---|---| +| JP2 DIRECT Vcc | closed **only when J2 is empty** | VCC = battery (2×AA/AAA straight on the MCU, build with `BATTERY_IS_VCC`). Closed with a module on J2, it shorts the module input to its output. | +| JP6 SDO | 1-2 (default) | J3/J14 pin 6 = GND: BME280 in I2C at 0x76, the firmware address | +| | 2-3 | pin 6 = MISO, for a sensor in SPI | +| JP4 Analog ref | 2-3 | J7 pin 3 = GND (normal use) | +| JP3 | closed | 1 µF from PA0 to GND: RC filter with R4 on the analog input | +| JP1 LED | closed | LED D1 on PB1. Leave open on battery to save power. | + +## On-board parts (no extra part needed) + +- I2C pull-ups R2/R3 4.7 kΩ and 1-Wire pull-up R6 4.7 kΩ, all to SENSOR_VCC: no resistor in the harness. +- Battery sense: R7/R8 1 MΩ divider with C7 100 nF, PA1 reads Vbatt / 2 (firmware scaling: BACKLOG F1). +- Q1 AO3401A: reverse-polarity protection on J13 and BT1. **Not on J10** (CR2032). +- R9 10 kΩ RESET pull-up, SW1 reset button, C8 100 nF at the radio. + +## Off-board modules + +Fit only what your build uses. Wire colours follow the project convention (see `CLAUDE.md`, *Hardware Conventions*): battery pair red/black, regulated pair white/blue (blue is power only), data green, clock yellow, select purple, enable/analog brown. Lengths depend on the enclosure: give them per build; the mushroom harness has exact lengths ([`openscad/faisceau_champignon.pdf`](../../openscad/faisceau_champignon.pdf), generated by `openscad/harness.py`). + +### Battery (one source only) + +| Source | Where | Wires | +|---|---|---| +| 2×AA or 2×AAA | J13 (1 = +, 2 = GND) | red / black, screw terminal at the board end | +| 18650 | through a TP4056 module, OUT+ / OUT− to J13 (mushroom: W5/W6 of the harness), or in BT1 | red / black | +| CR2032 | J10 | no wire, no reverse protection | + +Never fit two sources together: they are in parallel and charge each other. + +### 3.3 V module on J2 + +| Module pin | J2 pin | Colour | +|---|---|---| +| VIN | 1 +BATT | red | +| GND | 2 GND | black | +| VOUT | 3 VCC | white | + +- 18650 (up to 4.2 V) with a BME280 (3.6 V max): **TPS63020 buck-boost**. Tie EN to VIN and PS/SYNC to GND (power-save mode, otherwise it idles at a few mA). Check: 3.20–3.40 V on VCC at 3.7 V and 4.2 V in, idle current < 0.1 mA. +- 2×AA/AAA: a 3.3 V boost module, or no module and JP2 closed. +- A boost does not regulate when Vin > 3.3 V: with an 18650 it lets about 3.9 V through. Fine for a DS18B20 or an analog sensor (5.5 V max), not for a BME280. + +### Radio on J1 + +| Module pin | J1 pin | Colour | +|---|---|---| +| GND | 1 | blue | +| VCC | 2 | white | +| DATA | 3 (PB0) | green | + +Module pin orders differ (an FS1000A is DATA/VCC/GND): match the pins by name, not by position. Antenna: 17.3 cm straight wire (λ/4 at 433.92 MHz) on the module ANT pad. + +### BME280 / BMP280 on J3 (or J14) + +| Breakout pin | J3 pin | Colour | +|---|---|---| +| VIN | 1 SENSOR_VCC | white | +| GND | 2 GND | blue | +| SCL | 3 SCL | yellow | +| SDA | 4 SDA | green | +| CSB | 5 n.c. | — | +| SDO | 6 SDO (JP6) | green, 1 ring | + +- Mushroom: no wire, the breakout's 6-pin male header plugs into a 6-way 2.54 mm female header soldered under the board in J3. +- CSB is not connected on the board: the breakout must pull it up (≈ 10 kΩ to VIN), otherwise the sensor starts in SPI. Check with a meter; if it is missing, add 10 kΩ from CSB to VIN on the breakout. +- The firmware enables the PA5 pull-up at init: SDO wired to MISO (JP6 2-3) in I2C mode would give address 0x77. + +### DS18B20 on J5 + +| DS18B20 pin (TO-92, flat face, legs down, left to right) | J5 pin | Colour | +|---|---|---| +| GND | 2 GND | blue | +| DQ | 3 DQ (PA3) | green | +| VDD | 1 SENSOR_VCC | white | + +The sensor order (GND, DQ, VDD) is not the J5 order (VCC, GND, DQ): cross the wires. Pull-up R6 is on the board. + +### Analog sensor on J7 + +| Sensor pin | J7 pin | Colour | +|---|---|---| +| +Vs | 1 SENSOR_VCC | white | +| Vout | 2 AIN (PA0) | brown | +| GND | 3 (JP4 2-3) | blue | + +A TMP36 runs from SENSOR_VCC. An LM35 needs 4 V minimum: on 2×AA/AAA, power it from a 5 V boost switched by PB2 on J8, see [`VARIANTE_LM35.md`](VARIANTE_LM35.md) (`USE_SWITCHED_5V`). + +## Checks before closing + +1. No battery: J13 pin 1 and pin 2 must not beep together. +2. Battery in, no MCU: VCC (C2) between 3.0 and 3.4 V with a module on J2, or the battery voltage with JP2 closed. +3. Flash, then watch the frames on the receiver (`hardware/receiver/`) or in Home Assistant through RFLink. diff --git a/hardware/tinysensor/wiring.svg b/hardware/tinysensor/wiring.svg new file mode 100644 index 0000000..74bccbc --- /dev/null +++ b/hardware/tinysensor/wiring.svg @@ -0,0 +1,156 @@ + + tinySensor v3.0 board: connectors and off-board modules + + tinySensor v3.0 (emitter): connectors and off-board modules + Pin numbers from the v3.0 schematic. Fit only the modules your build uses. Wire colours follow the project convention. + + + + tinySensor v3.0 — ATtiny84A + + + + J13 RAW IN + 1 +RAW (□)2 GND + J2 3.3 V module + 1 +BATT2 GND3 VCC + J8 EXT. INT0 + 1 PB22 GND + J4 Digital out + 1 PA7 · 2 GND + + + + + + + + + + J1 RADIO + 1 GND2 VCC3 DATA PB0 + J3 I2C 1 (or J14 I2C 2) + 1 SENSOR_VCC2 GND3 SCL PA4 + 4 SDA PA65 n.c.6 SDO (JP6) + J5 OneWire + 1 SENSOR_VCC2 GND3 DQ PA3 + J7 Analog + 1 SENSOR_VCC2 AIN PA03 GND (JP4 2-3) + + + + + + + + + + + + Battery (one source) + 2×AA / 2×AAA, or 18650 + through a TP4056 module (OUT±) + + + + red− black + + + + 3.3 V module + TPS63020 (18650 + BME280) + or boost (2×AA/AAA) + VIN · GND · VOUT + + + + VIN redGND blackVOUT white + TPS63020: EN to VIN, PS/SYNC to GND. No module: JP2 closed. + + + + LM35 variant only + BC327 switch of a 5 V boost + see VARIANTE_LM35.md + + + PB2 brown + + + + + 433 MHz OOK emitter + STX882, FS1000A… pin order + differs: match by name + antenna 17.3 cm (λ/4) + + + + GND blueVCC whiteDATA green + + + + + BME280 / BMP280 breakout + VIN GND SCL SDA CSB SDO + Mushroom: plugs straight into J3 + (6-way female header), no wire + CSB must be pulled up on the + breakout (10 kΩ to VIN): check + max 3.6 V on VIN + + + + + + + whiteblueSCL yellow + SDA greenCSB n.c.SDO green, 1 ring + + + + DS18B20 (TO-92) + sensor pins GND, DQ, VDD: + not J5 order, cross the wires + + + + VDD whiteGND blueDQ green + + + + Analog sensor (3 pins) + TMP36 / LM35 on SENSOR_VCC; + LM35 on 5 V: see VARIANTE_LM35 + + + + +Vs whiteVout brownGND blue + + + + Wire colours + battery + (red) + battery − (black) + regulated + (white) + regulated − (blue, power only) + data (green) + clock (yellow) + select (purple) + enable / analog (brown) + + + + + On the board (no extra part needed) + • SENSOR_VCC = PA2: J3, J5, J7, J14 are powered only while measuring + • I2C pull-ups R2/R3 4.7 kΩ and 1-Wire pull-up R6 4.7 kΩ, to SENSOR_VCC + • Battery sense R7/R8 1 MΩ divider: PA1 reads Vbatt / 2 + • Q1 AO3401A: reverse-polarity protection on J13 and BT1 (not on J10 CR2032) + • BT1 18650 holder, J10 CR2032, J13: one battery source only + • JP2 DIRECT Vcc: VCC = battery; close only when J2 is empty + • JP6 SDO (J3/J14 pin 6): 1-2 GND = I2C 0x76 (default), 2-3 MISO = SPI + • JP4 2-3: J7 pin 3 = GND; JP3 closed: 1 µF filter on PA0 + • JP1: LED on PB1 (leave open to save power) + • ISP J6: MISO PA5, VCC, SCK PA4, MOSI PA6, RST, GND (shared with I2C) + + From 47dadc05460e7d82fe07d4a317fec0e901da84b8 Mon Sep 17 00:00:00 2001 From: Aurelien Labrosse Date: Sat, 14 Nov 2026 00:59:20 +0100 Subject: [PATCH 2/5] docs: hardware and debugging docs in English REWORK_v2.2, VARIANTE_LM35 (file name kept: code, requirements and CLAUDE.md point to it), BACKLOG, the 2026-11-06 PCB review header and docs/debugging.md translated from French. Co-Authored-By: Claude Opus 5.5 --- docs/debugging.md | 66 +++--- hardware/tinysensor/BACKLOG.md | 209 +++++++++--------- hardware/tinysensor/REWORK_v2.2.md | 102 ++++----- hardware/tinysensor/VARIANTE_LM35.md | 64 +++--- .../review/2026-11-06/pcb_review.md | 6 +- 5 files changed, 223 insertions(+), 224 deletions(-) diff --git a/docs/debugging.md b/docs/debugging.md index 04d3bd0..1aab12a 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -1,27 +1,27 @@ -# Debug pas-à-pas (debugWIRE) +# Step debugging (debugWIRE) -L'ancien projet Atmel Studio (`TinySensor/`) a été supprimé. Le debug passe désormais par PlatformIO + AVaRICE + AVR Dragon, en debugWIRE. +The old Atmel Studio project (`TinySensor/`) was removed. Debugging now goes through PlatformIO + AVaRICE + AVR Dragon, over debugWIRE. -> Statut : **non validé sur banc**. Procédure à confirmer au premier essai. +> Status: **not validated on the bench**. Procedure to confirm on the first try. -## Chaîne d'outils +## Toolchain -| Élément | Rôle | +| Item | Role | |---|---| -| AVR Dragon | Sonde (USB) — debugWIRE, ISP, HVSP | -| [AVaRICE](https://avarice.sourceforge.io/) | Serveur GDB pour le Dragon (`--dragon --debugwire`) | -| PlatformIO | `debug_tool = custom`, se connecte au serveur GDB | -| avrdude | Fusibles (activation/désactivation DWEN) | +| AVR Dragon | Probe (USB) — debugWIRE, ISP, HVSP | +| [AVaRICE](https://avarice.sourceforge.io/) | GDB server for the Dragon (`--dragon --debugwire`) | +| PlatformIO | `debug_tool = custom`, connects to the GDB server | +| avrdude | Fuses (enable/disable DWEN) | -Bloom n'est pas une option : il cible les sondes EDBG (Atmel-ICE, Snap, PICkit 4), pas le Dragon. `avr-stub` (débogueur natif PlatformIO) ne supporte que les ATmega. +Bloom is not an option: it targets EDBG probes (Atmel-ICE, Snap, PICkit 4), not the Dragon. `avr-stub` (PlatformIO native debugger) only supports ATmega parts. -À vérifier : présence de l'ATtiny84 dans la liste des cibles debugWIRE d'AVaRICE (`avarice --known-devices`, marqueur `+`). +To check: the ATtiny84 is in AVaRICE's debugWIRE target list (`avarice --known-devices`, `+` marker). -## Environnement PlatformIO (proposition) +## PlatformIO environment (proposal) ```ini [env:debug] -extends = env:devmodule ; env de base à choisir +extends = env:devmodule ; base env to choose build_type = debug debug_tool = custom debug_port = :4242 @@ -32,33 +32,33 @@ debug_init_cmds = load ``` -Lancer AVaRICE manuellement si besoin : `avarice --dragon --debugwire --part attiny84 :4242`. +Start AVaRICE by hand if needed: `avarice --dragon --debugwire --part attiny84 :4242`. -## Fusible DWEN +## DWEN fuse -### Activer debugWIRE -Programmer DWEN (= 0) dans le hfuse via ISP (`-c dragon_isp`). Une fois actif, **/RESET n'est plus une broche de reset et l'ISP ne fonctionne plus**. +### Enable debugWIRE +Program DWEN (= 0) in the hfuse over ISP (`-c dragon_isp`). Once active, **/RESET is no longer a reset pin and ISP no longer works**. -### Revenir en ISP (sans haute tension) -1. `avrdude -c dragon_dw -P usb -p t84` — avrdude se connecte en debugWIRE et bascule la cible en mode ISP, **sans coupure d'alimentation**. -2. `avrdude -c dragon_isp -P usb -p t84 -U hfuse:w:0xXX:m` — réécrire le hfuse avec DWEN = 1 (partir de la valeur actuelle). +### Back to ISP (no high voltage) +1. `avrdude -c dragon_dw -P usb -p t84` — avrdude connects over debugWIRE and switches the target to ISP mode, **without a power cycle**. +2. `avrdude -c dragon_isp -P usb -p t84 -U hfuse:w:0xXX:m` — rewrite the hfuse with DWEN = 1 (start from the current value). -AVaRICE ne sait pas désactiver DWEN lui-même. Séquence avrdude à valider selon la version. +AVaRICE cannot disable DWEN itself. avrdude sequence to validate depending on the version. -### Recours : haute tension (HVSP) -Si debugWIRE ne répond plus : HVSP à 12 V (tolérance ~11,5–12,5 V), supporté par le Dragon, câblage dédié. +### Fallback: high voltage (HVSP) +If debugWIRE no longer answers: HVSP at 12 V (tolerance ~11.5–12.5 V), supported by the Dragon, dedicated wiring. -## Limites debugWIRE +## debugWIRE limits -- Pas de breakpoint matériel : breakpoints logiciels par réécriture de pages flash (usure). -- Fusibles et lock bits inaccessibles pendant la session. -- Effacement puce (`-e`) impossible en mode debugWIRE. +- No hardware breakpoint: software breakpoints by rewriting flash pages (wear). +- Fuses and lock bits unreachable during the session. +- Chip erase (`-e`) impossible in debugWIRE mode. -## Contraintes matérielles — carte de debug dédiée +## Hardware constraints — dedicated debug board -Les cartes de production ne sont pas prévues pour debugWIRE. Une carte de debug dédiée est à prévoir avec : +Production boards are not designed for debugWIRE. A dedicated debug board is needed with: -- **/RESET sans condensateur** (la ligne porte le signal debugWIRE). -- **Pull-up /RESET ≥ 10 kΩ** (ou aucune résistance externe ; ne pas utiliser de valeur plus faible). -- Connecteur ISP 6 broches accessible (activation/désactivation DWEN). -- Idéalement : accès au câblage HVSP pour récupération. +- **/RESET without a capacitor** (the line carries the debugWIRE signal). +- **/RESET pull-up ≥ 10 kΩ** (or no external resistor; do not use a lower value). +- Reachable 6-pin ISP connector (enable/disable DWEN). +- Ideally: access to the HVSP wiring for recovery. diff --git a/hardware/tinysensor/BACKLOG.md b/hardware/tinysensor/BACKLOG.md index 8a1950d..2680080 100644 --- a/hardware/tinysensor/BACKLOG.md +++ b/hardware/tinysensor/BACKLOG.md @@ -1,117 +1,116 @@ -# Backlog de mise à niveau — carte tinySensor +# Upgrade backlog — tinySensor board -Revue du 2026-11-06 sur la branche `hw/schematic-upgrade`, faite avec KiCad 10.0.7 (`kicad-cli`) : ERC, netlist, DRC avec parité schéma, et comparaison avec le firmware (`lib/hal/Attiny84aHal*`). -Sévérités : **Critique** / **Majeur** / **Mineur** / **Question** / **Évolution**. Effort : S (≈ 1 h), M (une demi-journée), L (une refonte). +Review of 2026-11-06 on branch `hw/schematic-upgrade`, done with KiCad 10.0.7 (`kicad-cli`): ERC, netlist, DRC with schematic parity, and comparison with the firmware (`lib/hal/Attiny84aHal*`). +Severities: **Critical** / **Major** / **Minor** / **Question** / **Evolution**. Effort: S (≈ 1 h), M (half a day), L (a redesign). -État à l'ERC : 0 erreur et 47 avertissements. 39 sont des symboles embarqués périmés. Deux seulement signalent un vrai défaut, et ce défaut est R1. -La parité schéma ↔ PCB remonte 39 écarts (voir R6). +ERC state: 0 errors and 47 warnings. 39 are outdated embedded symbols. Only two point at a real defect, and that defect is R1. +Schematic ↔ PCB parity reports 39 mismatches (see R6). -Rapports complets, ERC/DRC et images : `review/2026-11-06/`. Reprise des cartes v2.2 déjà fabriquées : `REWORK_v2.2.md`. +Full reports, ERC/DRC and images: `review/2026-11-06/`. Rework of the already-built v2.2 boards: `REWORK_v2.2.md`. Connectors and off-board wiring: `WIRING.md`. -## Schéma +## Schematic -| ID | Sév. | Zone | Constat | Correctif | Effort | +| ID | Sev. | Area | Finding | Fix | Effort | |---|---|---|---|---|---| -| R1 | ✅ v3.0 (Critique (v2.2 : reprise)) | Alim / MCU | PA1 (U1.12, mesure batterie) est relié directement à +BATT, sans résistance ni pont diviseur. Avec une batterie au-dessus de VCC + 0,5 V (Li-ion, 18650, J13 RAW IN au-dessus de 3,8 V), la diode de clamp conduit sans limite de courant : le MCU peut être détruit, ou VCC alimenté par PA1. La lecture sature aussi à 1023. ERC : `pin_to_pin` et `multiple_net_names` +BATT/Battery_Checker. | Résistance série de 10–100 k et 100 nF vers GND. Si la batterie peut dépasser VCC, un pont diviseur, éventuellement commuté par PA2, et adapter `ConvertAnalogValueToMv`. | S | -| R2 | ◐ v3.0 (Majeur) | Alim | Aucune protection contre l'inversion de polarité. J10 (CR2032), J11 (AAA), J12 (AA), J13 (RAW IN) et le support 18650 du PCB sont tous en parallèle. Un bornier inversé met une tension négative sur le DC-DC et le MCU. Deux piles posées ensemble se rechargent l'une l'autre, ce qui est dangereux pour la CR2032. | P-MOSFET en idéal-diode, ou Schottky sur RAW IN. Marquer les supports comme exclusifs (DNP, mention sérigraphiée). | M | -| R3 | ✅ Corrigé | Alim / firmware | En hibernation, `sleep()` passe PA2 en entrée flottante (`DDRA = 0`, `Attiny84aHal.cpp:85`). La même fonction active les pull-ups internes de PA4/PA5/PA6 (l.89), et PA3 garde le sien depuis l'init (l.42). SENSOR_VCC est alors alimenté à travers R2/R3/R6 : les capteurs ne sont jamais vraiment coupés, et le BME280 n'a jamais de reset à la mise sous tension propre. | Firmware : en sommeil, PA2 en sortie à 0, PA3/PA4/PA6 en entrée sans pull-up. | S | -| R4 | ✅ Corrigé | Radio | Le module radio est alimenté en permanence (J1.2 sur VCC), et sa broche DATA (PB0) flotte en sommeil (`DDRB = 0`). Un émetteur OOK peut s'activer tout seul. Le commentaire `Attiny84aHal.h:9` dit le contraire du schéma. | Firmware : PB0 en sortie à 0 en sommeil. Option : un pull-down de 100 k sur TX_RADIO. Corriger le commentaire. | S | -| R5 | ✅ v3.0 (Majeur) | LED | D1 est montée à l'envers (anode sur GND, cathode vers R1/PB1), et l'empreinte PCB est cohérente avec cette erreur. Le firmware met PB1 à 1 pour l'allumer, donc la LED ne s'allume jamais. | Inverser D1 au schéma et au PCB. | S | -| R6 | ✅ v3.0 (Majeur) | Synchro | Le schéma n'est plus la référence. Le commit c8df224 a ajouté I2C et I2C_SPI (JST-EH 6 broches), JP5, JP6 et le support BH-18650 dans le PCB seulement. J3, J11, J12 et MH3 manquent au PCB, et MH1 y est en double. Il reste deux pastilles non connectées : JP6 vers MISO, et I2C.6 vers la zone GND. Le pôle − du 18650 semble sans net. | Reporter ces éléments dans le schéma, puis lancer « Update PCB from Schematic » jusqu'à une parité propre. Vérifier le 18650. | M | -| R7 | ✅ v3.0 (Mineur) | MCU | RESET n'a pas de pull-up externe : seul le pull-up interne, faible, et un bouton. | 10 k vers VCC, sans condensateur, pour rester compatible debugWIRE. | S | -| R8 | Mineur | Alim | Le découplage global (2 × 10 µF) est faible pour une CR2032, dont la résistance interne est de 10–20 Ω, face au pic de courant de la radio. | 100–470 µF à faible fuite pour les versions CR2032. | S | -| R9 | ✅ v3.0 (Mineur) | Alim | JP2 « DIRECT Vcc » court-circuite l'entrée et la sortie du DC-DC si J2 est monté. | Mention « JP2 fermé ⇒ J2 non monté ». Le firmware doit alors utiliser `BATTERY_IS_VCC`. | S | -| R10 | Question | Alim | Le module DC-DC J2 n'a pas de broche d'activation. Son courant de repos (souvent 10–100 µA) dépasse probablement celui de l'ATtiny en sommeil. | **Décidé (2026-11-07) :** module TPS63020 abaisseur-élévateur (stock Tars), relié par fils aux pastilles de J2 comme dans `REWORK_v2.2.md`. ◐ v3.0 : J2 devient une barrette 1×03 (1 +BATT, 2 GND, 3 VCC) aux mêmes pastilles ; choix v3.0 (2026-11-07) : 2 piles AA ou AAA sur J13 et un module élévateur 3,3 V (en stock) sur J2. Un élévateur ne régule pas si Vin > 3,3 V : avec une 18650 (BT1), il faut le TPS63020 abaisseur-élévateur. Sans BME280 (DS18B20 sur J5 ou capteur analogique 3 pattes sur J7, tous deux jusqu'à 5,5 V), une 18650 avec élévateur convient. Sérigraphie v3.0 : « 18650 : TPS63020, ou élévateur sans BME280 ». Mesurer son courant à vide (< 0,1 mA). | M | -| R11 | ✅ v3.0 (Question) | I2C | La broche CSB du BME280 (J3.5) est en l'air ; si elle est vue à 0, le capteur passe en SPI. SDO est à GND, d'où l'adresse 0x76, conforme au firmware. | Relier CSB à SENSOR_VCC, ou vérifier que le breakout la tire vers le haut. SDO sert à choisir l'adresse en I2C (GND = 0x76, celle du firmware), et c'est la sortie de données en SPI : décidé, c'est un choix fait au montage via JP6 (voir P4), avec une note au schéma. Attention : à l'init, le firmware active le pull-up de PA5, donc SDO câblé sur MISO en I2C donnerait 0x77. | S | -| R12 | Question | I2C | Le BH1750 (`USE_BH1750`, 0x23, env SOLAR_TEST) n'apparaît pas au schéma. | Le documenter sur le second connecteur I2C une fois R6 fait, avec ADDR à 0. | S | -| R13 | Question | Alim | `USE_CHARGE_PUMP` (`platformio.ini:66`, `:81`) n'est utilisé par aucun code et ne correspond à aucun circuit. Le chargeur TP4056 n'est pas au schéma. | Supprimer le flag ou documenter le matériel concerné. | S | -| R14 | Question | MCU | Les fusibles ne sont pas consignés, en particulier BODLEVEL. Le firmware coupe le BOD en sommeil. | Consigner les fusibles voulus dans le dépôt (BOD 1,8 V à 1 MHz). | S | -| R15 | ◐ v3.0 (Mineur) | Bibliothèques | `fp-lib-table` pointe vers `battery.pretty` et `Chinese breakouts.pretty`, qui sont absents. `sym-lib-table` déclare une bibliothèque Legacy qui n'existe pas. J13 utilise une empreinte retirée de KiCad, et J1/J3/J4 des symboles `Conn_*_Male/Female` retirés. U1 et J6 viennent de `-rescue.lib`. Le fichier est au format KiCad 7. | Nettoyer les tables. Utiliser les symboles et empreintes standards (`ATtiny84A-P`, `AVR-ISP-6`, `Conn_01x0N_Pin/Socket`, `TerminalBlock_Phoenix` 5,08), puis « Update Symbols from Library ». | S–M | -| R16 | ◐ v3.0 (Mineur) | Connecteurs | J1 est câblé GND/VCC/DATA, alors qu'un FS1000A est DATA/VCC/GND. J5 est câblé VCC/GND/DQ, alors qu'un DS18B20 TO-92 est GND/DQ/VDD. La pastille 1 de l'ISP est ronde, donc rien ne la repère. | Fonctions des broches en sérigraphie, et pastille 1 carrée sur l'ISP. | S | -| R17 | ◐ v3.0 (Mineur) | Hygiène | Valeurs « 4,7k » et « 4k7 » mélangées. JP3 a la valeur « ~ ». C5, une céramique de 1 nF, utilise une empreinte tantale, et C6 (0,1 µF) un symbole polarisé. La note de R4 (75R ou 18k) n'est pas claire. Un fil pendouille en (54,61 ; 120,65). Le cartouche indique Rev 2.3, la carte v2.2. | Normaliser les valeurs, passer C5 en `C_Disc`, supprimer le fil. | S | -| R18 | Évolution | v2.3 | Pistes pour une v2.3 : ATtiny84A-SSU ou ATtiny1614 (UPDI, TWI matériel, RTC basse conso), émetteur OOK pour 1,8–3,6 V (STX882, SYN115), boost nano-power avec enable, protection idéal-diode, pont diviseur commuté. | — | L | - -Points non vérifiés : les pull-ups internes aux breakouts, le courant de repos réel du DC-DC, les valeurs des fusibles. +| R1 | ✅ v3.0 (Critical (v2.2: rework)) | Power / MCU | PA1 (U1.12, battery sense) is tied straight to +BATT, with no resistor or divider. With a battery above VCC + 0.5 V (Li-ion, 18650, J13 RAW IN above 3.8 V), the clamp diode conducts with no current limit: the MCU can be destroyed, or VCC fed through PA1. The reading also saturates at 1023. ERC: `pin_to_pin` and `multiple_net_names` +BATT/Battery_Checker. | 10–100 k series resistor and 100 nF to GND. If the battery can exceed VCC, a divider, possibly switched by PA2, and adapt `ConvertAnalogValueToMv`. | S | +| R2 | ◐ v3.0 (Major) | Power | No reverse-polarity protection. J10 (CR2032), J11 (AAA), J12 (AA), J13 (RAW IN) and the PCB 18650 holder are all in parallel. A reversed terminal block puts a negative voltage on the DC-DC and the MCU. Two cells fitted together charge each other, which is dangerous for the CR2032. | P-MOSFET ideal diode, or Schottky on RAW IN. Mark the holders as exclusive (DNP, silkscreen note). | M | +| R3 | ✅ Fixed | Power / firmware | In hibernation, `sleep()` makes PA2 a floating input (`DDRA = 0`, `Attiny84aHal.cpp:85`). The same function enables the internal pull-ups of PA4/PA5/PA6 (l.89), and PA3 keeps its own since init (l.42). SENSOR_VCC is then fed through R2/R3/R6: the sensors are never really off, and the BME280 never gets a clean power-on reset. | Firmware: during sleep, PA2 output low, PA3/PA4/PA6 inputs without pull-up. | S | +| R4 | ✅ Fixed | Radio | The radio module is always powered (J1.2 on VCC), and its DATA pin (PB0) floats during sleep (`DDRB = 0`). An OOK emitter can key itself. The comment `Attiny84aHal.h:9` says the opposite of the schematic. | Firmware: PB0 output low during sleep. Option: a 100 k pull-down on TX_RADIO. Fix the comment. | S | +| R5 | ✅ v3.0 (Major) | LED | D1 is fitted backwards (anode on GND, cathode towards R1/PB1), and the PCB footprint matches that mistake. The firmware drives PB1 high to light it, so the LED never lights. | Flip D1 in the schematic and the PCB. | S | +| R6 | ✅ v3.0 (Major) | Sync | The schematic is no longer the reference. Commit c8df224 added I2C and I2C_SPI (6-pin JST-EH), JP5, JP6 and the BH-18650 holder in the PCB only. J3, J11, J12 and MH3 are missing from the PCB, and MH1 is duplicated there. Two unconnected pads remain: JP6 to MISO, and I2C.6 to the GND zone. The 18650 − pole seems to have no net. | Carry these items into the schematic, then run "Update PCB from Schematic" until parity is clean. Check the 18650. | M | +| R7 | ✅ v3.0 (Minor) | MCU | RESET has no external pull-up: only the weak internal one and a button. | 10 k to VCC, no capacitor, to stay debugWIRE compatible. | S | +| R8 | Minor | Power | The global decoupling (2 × 10 µF) is weak for a CR2032, whose internal resistance is 10–20 Ω, against the radio current peak. | 100–470 µF low-leakage for the CR2032 versions. | S | +| R9 | ✅ v3.0 (Minor) | Power | JP2 "DIRECT Vcc" shorts the DC-DC input to its output if J2 is fitted. | Note "JP2 closed ⇒ J2 not fitted". The firmware must then use `BATTERY_IS_VCC`. | S | +| R10 | Question | Power | The J2 DC-DC module has no enable pin. Its quiescent current (often 10–100 µA) probably exceeds the sleeping ATtiny's. | **Decided (2026-11-07):** TPS63020 buck-boost module (Tars stock), wired to the J2 pads as in `REWORK_v2.2.md`. ◐ v3.0: J2 becomes a 1×03 header (1 +BATT, 2 GND, 3 VCC) on the same pads; v3.0 choice (2026-11-07): 2 AA or AAA cells on J13 and a 3.3 V boost module (in stock) on J2. A boost does not regulate if Vin > 3.3 V: with an 18650 (BT1), the TPS63020 buck-boost is needed. Without BME280 (DS18B20 on J5 or 3-pin analog sensor on J7, both up to 5.5 V), an 18650 with a boost is fine. v3.0 silkscreen: "18650: TPS63020, or boost without BME280". Measure its idle current (< 0.1 mA). | M | +| R11 | ✅ v3.0 (Question) | I2C | The BME280 CSB pin (J3.5) is floating; if seen low, the sensor goes into SPI. SDO is on GND, hence address 0x76, matching the firmware. | Tie CSB to SENSOR_VCC, or check that the breakout pulls it up. SDO selects the address in I2C (GND = 0x76, the firmware's), and is the data output in SPI: decided, it is an assembly-time choice through JP6 (see P4), with a note on the schematic. Beware: at init the firmware enables the PA5 pull-up, so SDO wired to MISO in I2C would give 0x77. | S | +| R12 | Question | I2C | The BH1750 (`USE_BH1750`, 0x23, env SOLAR_TEST) does not appear in the schematic. | Document it on the second I2C connector once R6 is done, with ADDR at 0. | S | +| R13 | Question | Power | `USE_CHARGE_PUMP` (`platformio.ini:66`, `:81`) is used by no code and matches no circuit. The TP4056 charger is not in the schematic. | Drop the flag or document the hardware concerned. | S | +| R14 | Question | MCU | The fuses are not recorded, BODLEVEL in particular. The firmware turns the BOD off during sleep. | Record the intended fuses in the repository (BOD 1.8 V at 1 MHz). | S | +| R15 | ◐ v3.0 (Minor) | Libraries | `fp-lib-table` points to `battery.pretty` and `Chinese breakouts.pretty`, which are missing. `sym-lib-table` declares a Legacy library that does not exist. J13 uses a footprint removed from KiCad, and J1/J3/J4 removed `Conn_*_Male/Female` symbols. U1 and J6 come from `-rescue.lib`. The file is in KiCad 7 format. | Clean the tables. Use standard symbols and footprints (`ATtiny84A-P`, `AVR-ISP-6`, `Conn_01x0N_Pin/Socket`, `TerminalBlock_Phoenix` 5.08), then "Update Symbols from Library". | S–M | +| R16 | ◐ v3.0 (Minor) | Connectors | J1 is wired GND/VCC/DATA, while an FS1000A is DATA/VCC/GND. J5 is wired VCC/GND/DQ, while a TO-92 DS18B20 is GND/DQ/VDD. ISP pad 1 is round, so nothing marks it. | Pin functions on the silkscreen, and a square pad 1 on the ISP. | S | +| R17 | ◐ v3.0 (Minor) | Hygiene | Values "4,7k" and "4k7" mixed. JP3 has value "~". C5, a 1 nF ceramic, uses a tantalum footprint, and C6 (0.1 µF) a polarised symbol. The R4 note (75R or 18k) is unclear. A wire dangles at (54.61 ; 120.65). The title block says Rev 2.3, the board v2.2. | Normalise values, switch C5 to `C_Disc`, delete the wire. | S | +| R18 | Evolution | v2.3 | Ideas for a v2.3: ATtiny84A-SSU or ATtiny1614 (UPDI, hardware TWI, low-power RTC), OOK emitter for 1.8–3.6 V (STX882, SYN115), nano-power boost with enable, ideal-diode protection, switched divider. | — | L | + +Not checked: the breakouts' internal pull-ups, the real DC-DC quiescent current, the fuse values. ## PCB -Le circuit fait 2 couches, 1,6 mm, 93,98 × 63,75 mm, avec un plan GND en B.Cu. Tous les composants sont traversants, sauf JP2–JP6 (CMS en B.Cu). -La DRC, avec parité et re-remplissage des zones, remonte 42 violations, 2 non-connexions et 39 écarts de parité. Environ 70 de ces messages viennent des bibliothèques ou des champs. -La variante découpée pour le champignon a été vérifiée sur une copie découpée en x 147 / y 87 : la masse reste continue, et MH1, MH5, J1, I2C et J13 tombent exactement aux cotes de `pcb.scad`. -Le TP4056 est un module externe relié à J13 : le courant de charge ne passe pas par la carte. +The board is 2 layers, 1.6 mm, 93.98 × 63.75 mm, with a GND plane on B.Cu. All parts are through-hole, except JP2–JP6 (SMD on B.Cu). +DRC, with parity and zone refill, reports 42 violations, 2 unconnected items and 39 parity mismatches. About 70 of these messages come from libraries or fields. +The cropped variant for the mushroom was checked on a copy cut at x 147 / y 87: ground stays continuous, and MH1, MH5, J1, I2C and J13 fall exactly on the `pcb.scad` dimensions. +The TP4056 is an external module wired to J13: the charge current does not cross the board. -| ID | Sév. | Zone | Constat | Correctif | Effort | +| ID | Sev. | Area | Finding | Fix | Effort | |---|---|---|---|---|---| -| P1 | ✅ v3.0 (Critique) | Masse | Les pastilles GND du connecteur « I2C » (pads 2 et 6) et de l'ISP J6 (pad 6) sont sur un îlot de cuivre B.Cu isolé, entouré par les pistes VCC, SENSOR_VCC et MISO et par le texte cuivre. DRC : `unconnected_items` I2C.6 ↔ zone GND, `starved_thermal` I2C.2. Le BME280 du champignon se branche justement là : **il n'a pas de masse**. | Piste GND en F.Cu de I2C.6 à J6.6 puis vers le plan, ou un via GND dans l'îlot. Relancer la DRC jusqu'à 0 non-connexion. | S | -| P2 | ✅ v3.0 (Critique) | Alim | Le pôle − du support 18650 (`REF**` BH-18650, pad 2) n'a pas de net, et le support n'existe pas au schéma. La carte entière ne peut pas fonctionner sur 18650. | L'ajouter au schéma (BT1, − sur GND), mettre le PCB à jour, sérigraphier +/−. | S | -| P3 | ✅ v3.0 (Majeur) | Synchro | Mêmes écarts que R6. MH1 est en double, en (203 ; 52) et en (203,75 ; 105,51), ce qui rend ambiguë la désignation des trous utilisée par le boîtier. | Traité avec R6, en ré-annotant aussi les trous de fixation. | M | -| P4 | ✅ v3.0 (Majeur) | Netlist | JP6 relie MISO à MISO, et JP5 GND à GND : ces cavaliers ne servent à rien. La pastille 5 de I2C n'a pas de net. I2C_SPI met MISO en broche 5, alors que le brochage d'un breakout BMx280 est VCC/GND/SCL/SDA/CSB/SDO. | Décidé : JP6 devient un sélecteur à 3 pastilles pour la broche 5 du connecteur capteur (SDO), côté GND pour l'I2C à 0x76, côté MISO pour le SPI. Ajouter une note au schéma pour la personne qui monte la carte. JP5 (GND–GND) est à supprimer ou à justifier. Lié à R11. | S | -| P5 | ◐ v3.0 (Majeur) | Découpe | Les lignes de découpe traversent du cuivre : le plan GND, et deux pistes +BATT en F.Cu qui coupent x = 147 (vers y 59,3) et y = 87. Un bord scié met +BATT et GND à nu, et une bavure peut les court-circuiter en extérieur. | Zones d'exclusion cuivre de 1,5 mm sur les deux couches le long des deux lignes. Faire passer +BATT par un pont ou un cavalier hors de la partie conservée. | S | -| P6 | ✅ v3.0 (Majeur) | Fab | `min_copper_edge_clearance` est à 0,05 mm, et le plan GND arrive à 0,05 mm du bord. Les fabricants demandent au moins 0,2–0,3 mm, et le cuivre à nu s'oxyde dehors. | Régler le dégagement à 0,5 mm, re-remplir, relancer la DRC. | S | -| P7 | Majeur | Méca | MH5 (150,89 ; 51,84) est sous le module DC-DC J2, alors que le champignon visse la carte par MH5. Une fois J2 soudé, le tournevis n'y accède plus. | Déplacer MH5 hors de J2 (et mettre `pcb.scad` à jour) ou déplacer J2. À vérifier : le module a-t-il un trou aligné ? ✅ v3.0 : J2 n'est plus qu'une barrette 1×03, la vis de MH5 reste accessible, `pcb.scad` inchangé. | M | -| P8 | ✅ v3.0 (Majeur) | Méca | C6 (tantale traversant) est en B.Cu, alors que `pcb.scad` le modélise sur le dessus. Debout, il touche le plancher (`pcb_gap` = 5 mm). | Passer C6 en F.Cu, le coucher, ou corriger le modèle. | S | -| P9 | ◐ v3.0 (Majeur) | Montage | Rien n'indique les broches en sérigraphie : pas de +/− sur J13 RAW IN, ni de fonction des broches sur J1, J5 ou J7. Tous les textes sont en cuivre, pas en sérigraphie. | Textes en F.Silkscreen (au moins 1 mm). +/− sur J13, G/V/D sur J1, V/G/Q sur J5. Recoupe R16. | S | -| P10 | ✅ v3.0 (Majeur) | LED | Même constat que R5 : l'anode de D1 est sur GND. | Voir R5. | — | -| P11 | ✅ v3.0 (Mineur) | Méca | Les trous de fixation font 2,5 mm de diamètre, trop justes pour du M2,5. `pcb.scad` les modélise en Ø2,7. | `MountingHole_2.7mm_M2.5`. | S | -| P12 | Mineur | Découpe | J2 (x 147,57) et J13 (≈ 147,7) frôlent la ligne x = 147 : un trait de scie de 1 mm peut les entamer. | Ligne en x ≈ 145,5 (le support CR2032 s'arrête vers 141) et mettre `k_crop` à jour, ou décaler J2/J13. À vérifier : les fils de J13 entrent-ils côté découpe ? | S | -| P13 | ✅ v3.0 (Mineur) | Alim | Le découplage de la radio est loin : C4 (10 µF) est à environ 11 mm de J1, et il n'y a pas de 100 nF. | 100 nF entre J1.1 et J1.2, et C4 à moins de 5 mm. | S | -| P14 | ✅ v3.0 (Mineur) | Masse | La broche 14 de U1 n'est pas reliée au plan (`zone_connect 0`). Sa masse passe seulement par C2 et JP5. | Connexion thermique au plan. | S | -| P15 | Mineur | Alim | Les pistes +BATT font 0,25–0,3 mm, soit environ 0,09 Ω sur 45 mm. Ça suffit pour environ 100 mA. | Les élargir à 0,5–0,8 mm, pour la marge. | S | -| P16 | ✅ v3.0 (Mineur) | Montage | Une barrette femelle au pas de 2,54 dans une empreinte JST-EH au pas de 2,50 accumule 0,2 mm d'erreur sur 6 broches, avec des perçages de 0,95 mm. | Perçages de 1,0–1,1 mm, ou une empreinte double pas. | S | -| P17 | ◐ v3.0 (Mineur) | Hygiène | Références masquées : U1, C2, C3, C6, D2, D3, R6, J2, J7, I2C, I2C_SPI. C5 (1 nF) est sur une empreinte tantale marquée « + ». Le masque rogne la sérigraphie de SW1 et J10, et `min_silk_clearance` est à 0. Il reste des bouts de piste orphelins : GND (202,8 ; 97,0) et VCC (162,7–163,1 ; 61,7). Les bibliothèques sont cassées (voir R15). | Nettoyer. | S | -| P18 | Question | Radio | Quel module va dans J1, et dans quel sens ? Un module de 19 mm debout peut gêner J5. Il n'y a pas de piste RF, donc le plan de masse n'est pas en cause. | — | — | -| P19 | Question | Capteur | Dans la variante boîtier, le connecteur capteur est à 7 mm de U1, au-dessus du plan GND. C'est négligeable vu le rapport cyclique. Dans le champignon, le BME280 pend sous la carte : c'est bien. | — | — | -| P20 | Évolution | v2.3 | Pistes : vraies languettes ou mouse-bites avec exclusion cuivre, trous de fixation hors des modules, une seule entrée batterie protégée (voir R2 : CR2032, 18650 à 4,2 V et entrée boost sont en parallèle), empreinte BH1750, fente d'isolation thermique autour du capteur. | — | L | - -Points non vérifiés : rien n'a été ouvert dans l'interface graphique, les Gerbers et le fichier de perçage n'ont pas été exportés, et les collisions 3D entre modules et barrettes n'ont pas été contrôlées. Les dimensions du module radio et le côté d'entrée des fils de J13 sont supposés. - -## Ordre suggéré, schéma et PCB ensemble - -1. **R3, R4 et R5/P10**, le firmware et la LED : la règle PLAT-POWER-003 est à relire. -2. **P1** : sans masse, le BME280 du champignon ne marche pas. C'est à corriger avant toute fabrication. -3. **R1 et R2/P2**, la sécurité de l'alimentation. -4. **R6/P3/P4** : remettre le schéma en référence avant toute autre modification. -5. **P5, P6, P7, P8, P12**, les points de fabrication et de mécanique du champignon. -6. Le reste. - -## Firmware, suite de la revue - -| ID | Sév. | Constat | Correctif | Effort | +| P1 | ✅ v3.0 (Critical) | Ground | The GND pads of the "I2C" connector (pads 2 and 6) and of ISP J6 (pad 6) sit on an isolated B.Cu copper island, surrounded by the VCC, SENSOR_VCC and MISO tracks and the copper text. DRC: `unconnected_items` I2C.6 ↔ GND zone, `starved_thermal` I2C.2. The mushroom BME280 plugs in right there: **it has no ground**. | GND track on F.Cu from I2C.6 to J6.6 then to the plane, or a GND via in the island. Rerun DRC until 0 unconnected. | S | +| P2 | ✅ v3.0 (Critical) | Power | The − pole of the 18650 holder (`REF**` BH-18650, pad 2) has no net, and the holder does not exist in the schematic. The whole board cannot run on an 18650. | Add it to the schematic (BT1, − on GND), update the PCB, silkscreen +/−. | S | +| P3 | ✅ v3.0 (Major) | Sync | Same mismatches as R6. MH1 is duplicated, at (203 ; 52) and at (203.75 ; 105.51), which makes the hole naming used by the enclosure ambiguous. | Handled with R6, re-annotating the mounting holes too. | M | +| P4 | ✅ v3.0 (Major) | Netlist | JP6 ties MISO to MISO, and JP5 GND to GND: these jumpers are useless. I2C pad 5 has no net. I2C_SPI puts MISO on pin 5, while a BMx280 breakout pinout is VCC/GND/SCL/SDA/CSB/SDO. | Decided: JP6 becomes a 3-pad selector for sensor-connector pin 5 (SDO), GND side for I2C at 0x76, MISO side for SPI. Add a note on the schematic for whoever builds the board. JP5 (GND–GND) to delete or justify. Linked to R11. | S | +| P5 | ◐ v3.0 (Major) | Cut | The cut lines cross copper: the GND plane, and two F.Cu +BATT tracks crossing x = 147 (near y 59.3) and y = 87. A sawn edge bares +BATT and GND, and a burr can short them outdoors. | 1.5 mm copper keep-outs on both layers along both lines. Route +BATT through a bridge or a jumper outside the kept part. | S | +| P6 | ✅ v3.0 (Major) | Fab | `min_copper_edge_clearance` is 0.05 mm, and the GND plane comes 0.05 mm from the edge. Fabs ask for at least 0.2–0.3 mm, and bare copper oxidises outdoors. | Set the clearance to 0.5 mm, refill, rerun DRC. | S | +| P7 | Major | Mech | MH5 (150.89 ; 51.84) is under the J2 DC-DC module, while the mushroom screws the board through MH5. Once J2 is soldered, the screwdriver cannot reach it. | Move MH5 away from J2 (and update `pcb.scad`) or move J2. To check: does the module have an aligned hole? ✅ v3.0: J2 is now only a 1×03 header, the MH5 screw stays reachable, `pcb.scad` unchanged. | M | +| P8 | ✅ v3.0 (Major) | Mech | C6 (through-hole tantalum) is on B.Cu, while `pcb.scad` models it on top. Standing, it touches the floor (`pcb_gap` = 5 mm). | Move C6 to F.Cu, lay it down, or fix the model. | S | +| P9 | ◐ v3.0 (Major) | Assembly | Nothing marks the pins on the silkscreen: no +/− on J13 RAW IN, no pin functions on J1, J5 or J7. All texts are on copper, not silkscreen. | F.Silkscreen texts (at least 1 mm). +/− on J13, G/V/D on J1, V/G/Q on J5. Overlaps R16. | S | +| P10 | ✅ v3.0 (Major) | LED | Same finding as R5: the D1 anode is on GND. | See R5. | — | +| P11 | ✅ v3.0 (Minor) | Mech | Mounting holes are 2.5 mm in diameter, too tight for M2.5. `pcb.scad` models them at Ø2.7. | `MountingHole_2.7mm_M2.5`. | S | +| P12 | Minor | Cut | J2 (x 147.57) and J13 (≈ 147.7) graze the x = 147 line: a 1 mm saw kerf can bite into them. | Line at x ≈ 145.5 (the CR2032 holder stops near 141) and update `k_crop`, or move J2/J13. To check: do the J13 wires enter from the cut side? | S | +| P13 | ✅ v3.0 (Minor) | Power | The radio decoupling is far: C4 (10 µF) is about 11 mm from J1, and there is no 100 nF. | 100 nF between J1.1 and J1.2, and C4 within 5 mm. | S | +| P14 | ✅ v3.0 (Minor) | Ground | U1 pin 14 is not tied to the plane (`zone_connect 0`). Its ground goes only through C2 and JP5. | Thermal connection to the plane. | S | +| P15 | Minor | Power | The +BATT tracks are 0.25–0.3 mm, about 0.09 Ω over 45 mm. Enough for about 100 mA. | Widen them to 0.5–0.8 mm, for margin. | S | +| P16 | ✅ v3.0 (Minor) | Assembly | A 2.54 mm female header in a 2.50 mm JST-EH footprint accumulates 0.2 mm of error over 6 pins, with 0.95 mm drills. | 1.0–1.1 mm drills, or a dual-pitch footprint. | S | +| P17 | ◐ v3.0 (Minor) | Hygiene | Hidden references: U1, C2, C3, C6, D2, D3, R6, J2, J7, I2C, I2C_SPI. C5 (1 nF) is on a tantalum footprint marked "+". The mask clips the SW1 and J10 silkscreen, and `min_silk_clearance` is 0. Orphan track stubs remain: GND (202.8 ; 97.0) and VCC (162.7–163.1 ; 61.7). Libraries are broken (see R15). | Clean up. | S | +| P18 | Question | Radio | Which module goes into J1, and which way round? A 19 mm module standing up may get in J5's way. There is no RF track, so the ground plane is not at stake. | — | — | +| P19 | Question | Sensor | In the box variant, the sensor connector is 7 mm from U1, over the GND plane. Negligible given the duty cycle. In the mushroom, the BME280 hangs under the board: good. | — | — | +| P20 | Evolution | v2.3 | Ideas: real tabs or mouse-bites with copper keep-out, mounting holes away from modules, a single protected battery input (see R2: CR2032, 18650 at 4.2 V and boost input are in parallel), BH1750 footprint, thermal isolation slot around the sensor. | — | L | + +Not checked: nothing was opened in the GUI, Gerbers and the drill file were not exported, and 3D collisions between modules and headers were not checked. The radio module dimensions and the J13 wire entry side are assumed. + +## Suggested order, schematic and PCB together + +1. **R3, R4 and R5/P10**, the firmware and the LED: rule PLAT-POWER-003 to reread. +2. **P1**: without ground, the mushroom BME280 does not work. Fix before any manufacturing. +3. **R1 and R2/P2**, power safety. +4. **R6/P3/P4**: make the schematic the reference again before any other change. +5. **P5, P6, P7, P8, P12**, the mushroom manufacturing and mechanical items. +6. The rest. + +## Firmware, follow-up of the review + +| ID | Sev. | Finding | Fix | Effort | |---|---|---|---|---| -| F1 | Majeur | Avec le pont diviseur ajouté par la reprise v2.2 (R1), et dans la v3.0, PA1 ne reçoit plus que la moitié de la tension batterie. | Ajouter un réglage de rapport de pont (par exemple `BATTERY_DIVIDER`) dans `main.cpp`, ou dans `ConvertAnalogValueToMv`, via le workflow RBD. | S | - -## Révision 3.0 (2026-11-06) - -Le schéma et le PCB sont estampillés **Rev 3.0** (cartouche et sérigraphie « TinySensor v3.0 »). Ils ont été générés à partir de la v2.2 par `scripts/make_v3_pcb.py`, `scripts/make_v3_sch.py` et `scripts/sync_paths.py` (KiCad 10, `pcbnew` en Python). -DRC avec parité schéma : 0 erreur d'empreinte, 0 pastille non connectée. La seule erreur restante est la zone d'occupation de J10, mal formée dans la bibliothèque. ERC : aucune erreur, seulement des avertissements de bibliothèque hérités. - -**Corrigé :** -- **R5/P10 :** D1 tournée. -- **R1 :** PA1 passe par le pont R7/R8 de 1 MΩ, avec C7 de 100 nF ; la broche lit Vbatt / 2 (voir F1). -- **R2 :** Q1 (AO3401A) protège contre l'inversion sur J13 et BT1. -- **P1 :** la masse des connecteurs capteurs est reliée au plan. -- **P2 :** BT1 18650 est câblé, avec son pôle − à GND. -- **R6/P3 :** J3 et J14 remplacent les connecteurs « I2C » et « I2C_SPI » du PCB. J11, J12 et JP5 sont supprimés, et le MH1 en double devient MH3. -- **R11/P4 :** JP6 sélectionne SDO (broche 6), avec 1-2 = GND par défaut et 2-3 = MISO. -- **R7 :** R9 de 10 kΩ en pull-up sur RESET. -- **P13 :** C8 de 100 nF à la radio. -- **P8 :** C6 passe en 0805 sur le dessus. -- **P11 :** trous M2,5 de 2,7 mm. -- **P16 :** perçages de 1,05 mm. -- **P6 :** 0,5 mm entre le cuivre et le bord. -- **P5 (en partie) :** pas de plan de masse le long des traits de découpe. Deux ponts GND traversent ces bandes, et deux pistes +BATT y passent encore. -- **P9 (en partie) :** les textes sont passés en sérigraphie, avec « + » sur J13, G/V/D sur J1, V/G/Q sur J5 et la mention « 1 seule source batterie ». -- **R15 (en partie) :** les tables de bibliothèques sont nettoyées, et les empreintes de J13 et BT1 sont copiées dans `library/`. - -**Reste ouvert pour la v3.0 :** -- **P7 :** ✅ J2 est une barrette 1×03 (`scripts/v3_j2_header.py`), MH5 reste accessible. -- **P12 :** J2 et J13 restent près de x = 147. -- **P15 :** pistes +BATT à élargir. -- **R8, R12, R13, R14, R18/P20 :** non traités. -- **R16 :** pastille 1 carrée sur l'ISP. -- **R17 :** note de R4 et fil pendant. -- **Hygiène :** 28 empreintes ne correspondent plus à leur version en bibliothèque, et les symboles hérités (rescue) sont à remplacer. -- **Fabrication :** relecture dans l'interface graphique, export des Gerbers et du fichier de perçage. Aucun des deux n'a été fait. -- **Schéma :** quelques symboles d'alimentation recouvrent des textes (lisibilité seulement). -- **F1 :** le firmware doit multiplier la lecture de PA1 par 2. - +| F1 | Major | With the divider added by the v2.2 rework (R1), and in v3.0, PA1 only gets half the battery voltage. | Add a divider-ratio setting (for example `BATTERY_DIVIDER`) in `main.cpp`, or in `ConvertAnalogValueToMv`, through the RBD workflow. | S | + +## Revision 3.0 (2026-11-06) + +Schematic and PCB are stamped **Rev 3.0** (title block and "TinySensor v3.0" silkscreen). They were generated from v2.2 by `scripts/make_v3_pcb.py`, `scripts/make_v3_sch.py` and `scripts/sync_paths.py` (KiCad 10, `pcbnew` in Python). +DRC with schematic parity: 0 footprint errors, 0 unconnected pads. The only remaining error is the J10 courtyard, malformed in the library. ERC: no errors, only inherited library warnings. + +**Fixed:** +- **R5/P10:** D1 rotated. +- **R1:** PA1 goes through the R7/R8 1 MΩ divider, with C7 100 nF; the pin reads Vbatt / 2 (see F1). +- **R2:** Q1 (AO3401A) protects against reversal on J13 and BT1. +- **P1:** the sensor connectors' ground is tied to the plane. +- **P2:** BT1 18650 is wired, its − pole on GND. +- **R6/P3:** J3 and J14 replace the PCB "I2C" and "I2C_SPI" connectors. J11, J12 and JP5 are deleted, and the duplicate MH1 becomes MH3. +- **R11/P4:** JP6 selects SDO (pin 6), with 1-2 = GND by default and 2-3 = MISO. +- **R7:** R9 10 kΩ pull-up on RESET. +- **P13:** C8 100 nF at the radio. +- **P8:** C6 becomes an 0805 on top. +- **P11:** 2.7 mm M2.5 holes. +- **P16:** 1.05 mm drills. +- **P6:** 0.5 mm between copper and edge. +- **P5 (partly):** no ground plane along the cut lines. Two GND bridges cross these strips, and two +BATT tracks still run through them. +- **P9 (partly):** texts moved to silkscreen, with "+" on J13, G/V/D on J1, V/G/Q on J5 and the note "1 battery source only". +- **R15 (partly):** library tables cleaned, and the J13 and BT1 footprints copied into `library/`. + +**Still open for v3.0:** +- **P7:** ✅ J2 is a 1×03 header (`scripts/v3_j2_header.py`), MH5 stays reachable. +- **P12:** J2 and J13 stay close to x = 147. +- **P15:** +BATT tracks to widen. +- **R8, R12, R13, R14, R18/P20:** not handled. +- **R16:** square pad 1 on the ISP. +- **R17:** R4 note and dangling wire. +- **Hygiene:** 28 footprints no longer match their library version, and the inherited (rescue) symbols are to replace. +- **Manufacturing:** GUI review, Gerber and drill file export. Neither has been done. +- **Schematic:** a few power symbols overlap texts (readability only). +- **F1:** the firmware must multiply the PA1 reading by 2. diff --git a/hardware/tinysensor/REWORK_v2.2.md b/hardware/tinysensor/REWORK_v2.2.md index 9666982..cda507d 100644 --- a/hardware/tinysensor/REWORK_v2.2.md +++ b/hardware/tinysensor/REWORK_v2.2.md @@ -1,76 +1,76 @@ -# Reprise des cartes v2.2 déjà fabriquées — montage champignon +# Rework of the already-built v2.2 boards — mushroom build -Les cartes v2.2 sont déjà fabriquées. La version 3.0 corrigera les défauts à la source (voir `BACKLOG.md`). -Cette fiche dit comment rendre une v2.2 utilisable dans le champignon. Il s'agit de la carte découpée en x = 147 / y = 87, alimentée par une 18650 à travers un TP4056 relié à J13, avec le BME280 enfiché dans l'empreinte « I2C ». -Les coordonnées sont celles de KiCad, en mm, carte non tournée, vue de dessus (côté composants). +The v2.2 boards are already manufactured. Version 3.0 fixes the defects at the source (see `BACKLOG.md`). +This sheet says how to make a v2.2 usable in the mushroom. It covers the board cut at x = 147 / y = 87, powered by an 18650 through a TP4056 wired to J13, with the BME280 plugged into the "I2C" footprint. +Coordinates are KiCad's, in mm, board not rotated, seen from the top (component side). -## À faire absolument +## Must do -### 1. Masse du BME280 (P1) : un strap +### 1. BME280 ground (P1): a strap -Les pastilles GND de l'empreinte « I2C » (pads 2 et 6) et la broche GND de l'ISP (J6.6) sont sur un îlot de cuivre isolé. -Sans correction, le BME280 n'a pas de masse. Sa broche SDO, câblée au GND, flotte elle aussi, et l'adresse I2C du capteur n'est plus fixée. +The GND pads of the "I2C" footprint (pads 2 and 6) and the ISP GND pin (J6.6) sit on an isolated copper island. +Without a fix, the BME280 has no ground. Its SDO pin, wired to GND, floats too, and the sensor I2C address is no longer fixed. -- Le strap : un fil de 7 mm entre **I2C pad 6** (186,85 ; 77,11) et **I2C_SPI pad 6** (186,85 ; 83,91), juste en dessous, côté dessus de la carte. Les trous de l'I2C_SPI sont libres puisqu'on ne monte pas son connecteur, et son pad 6 est relié au plan de masse. -- Ensuite, au multimètre : bip entre I2C pad 2, I2C pad 6, J6 pad 6 et J13 borne 2. +- The strap: a 7 mm wire between **I2C pad 6** (186.85 ; 77.11) and **I2C_SPI pad 6** (186.85 ; 83.91), just below, on the top side of the board. The I2C_SPI holes are free since its connector is not fitted, and its pad 6 is tied to the ground plane. +- Then, with a meter: beep between I2C pad 2, I2C pad 6, J6 pad 6 and J13 terminal 2. -### 2. LED D1 (R5) : la monter à l'envers de la sérigraphie +### 2. LED D1 (R5): fit it against the silkscreen -L'empreinte est inversée. Il faut mettre l'**anode (patte longue) dans la pastille carrée** (pad 1, côté R1) et la cathode dans la ronde (pad 2, GND). Aucune coupe n'est nécessaire. +The footprint is reversed. Put the **anode (long leg) in the square pad** (pad 1, R1 side) and the cathode in the round one (pad 2, GND). No cut needed. -### 3. Mesure batterie sur PA1 (R1) : une coupe et un pont diviseur +### 3. Battery sense on PA1 (R1): one cut and a divider -PA1 (U1 broche 12) est relié directement à +BATT. Avec une 18650 pleine (4,2 V), la broche reçoit plus que VCC : la mesure sature à 1023, et la diode de protection du MCU risque de conduire. +PA1 (U1 pin 12) is tied straight to +BATT. With a full 18650 (4.2 V), the pin gets more than VCC: the reading saturates at 1023, and the MCU clamp diode may conduct. -1. **Couper** la piste B.Cu de 0,3 mm qui part en diagonale de la broche 12 de U1 (174,00 ; 62,50) vers (170,19 ; 66,31), à 1–2 mm de la broche. Faire deux traits de cutter et retirer le cuivre entre les deux. Vérifier qu'il n'y a plus de continuité entre U1.12 et J13 borne 1. -2. **Gratter** le vernis de la piste sur 2 mm, juste après la coupe côté +BATT. -3. **R haute, 1 MΩ :** de U1.12 au cuivre gratté, par-dessus la coupe, côté dessous. -4. **R basse, 1 MΩ, et 100 nF :** de U1.12 à U1.14 (GND), côté dessous. Les pattes enjambent la broche 13 (PA0) : les gainer. -5. Le pont consomme environ 2 µA en continu. La valeur lue est la moitié de la tension batterie, et le firmware doit la multiplier par 2. Ce réglage n'existe pas encore : je l'ajoute au backlog firmware. En attendant, ne pas activer `BATTERY_VOLTAGE_X10_ID`, ou bien compiler avec `BATTERY_IS_VCC` (qui ne mesure que VCC). +1. **Cut** the 0.3 mm B.Cu track leaving U1 pin 12 (174.00 ; 62.50) diagonally towards (170.19 ; 66.31), 1–2 mm from the pin. Make two cutter strokes and lift the copper between them. Check there is no continuity left between U1.12 and J13 terminal 1. +2. **Scrape** the track solder mask over 2 mm, just after the cut on the +BATT side. +3. **High resistor, 1 MΩ:** from U1.12 to the scraped copper, across the cut, bottom side. +4. **Low resistor, 1 MΩ, and 100 nF:** from U1.12 to U1.14 (GND), bottom side. The legs straddle pin 13 (PA0): sleeve them. +5. The divider draws about 2 µA all the time. The reading is half the battery voltage, and the firmware must multiply it by 2. That setting does not exist yet (firmware backlog F1). Until then, do not enable `BATTERY_VOLTAGE_X10_ID`, or build with `BATTERY_IS_VCC` (which measures VCC only). -### 4. Découpe de la carte (P5) : ne pas laisser +BATT contre la masse +### 4. Board cut (P5): do not leave +BATT against ground -Les traits x = 147 et y = 87 coupent le plan de masse (B.Cu) et deux pistes +BATT (F.Cu) : la première vers y ≈ 59,3, la seconde vers x = 148,35. +The x = 147 and y = 87 lines cut the ground plane (B.Cu) and two +BATT tracks (F.Cu): the first near y ≈ 59.3, the second near x = 148.35. -- Scier un peu à l'extérieur du trait, puis limer jusqu'au trait. -- Sur les deux bouts de piste +BATT, enlever au cutter le dernier millimètre de cuivre côté bord. -- Chanfreiner légèrement le bord côté B.Cu à la lime pour casser la bavure du plan de masse. -- Contrôle : J13 borne 1 et J13 borne 2 ne doivent **pas** biper ensemble. -- Passer un vernis sur les bords (vernis à ongles ou vernis de tropicalisation) : la carte vit dehors. +- Saw slightly outside the line, then file down to the line. +- On both +BATT track ends, remove the last millimetre of copper at the edge with a cutter. +- Lightly chamfer the B.Cu side of the edge with the file to break the ground-plane burr. +- Check: J13 terminal 1 and J13 terminal 2 must **not** beep together. +- Varnish the edges (nail varnish or conformal coating): the board lives outdoors. -### 5. Alimentation 3,3 V : un TPS63020 à la place du module J2 +### 5. 3.3 V supply: a TPS63020 instead of the J2 module -Le module prévu pour J2 (« 0,9–3,3 V vers 3,3 V ») est un élévateur. Avec une 18650 à 4,2 V, ces modules laissent en général passer la tension d'entrée (≈ 3,9 V), alors que le BME280 est donné pour 3,6 V maximum. -On le remplace par le module **TPS63020** abaisseur-élévateur 3,3 V disponible du projet Tars (26,7 × 17,7 × 1,6 mm, trop grand pour l'emplacement J2). Il est relié par fils : +The module planned for J2 ("0.9–3.3 V to 3.3 V") is a boost. With an 18650 at 4.2 V, these modules usually let the input voltage through (≈ 3.9 V), while the BME280 is rated 3.6 V max. +It is replaced by the **TPS63020** 3.3 V buck-boost module in stock from the Tars project (26.7 × 17.7 × 1.6 mm, too big for the J2 spot). It is wired: -| Fil | Couleur | Module TPS63020 | Carte | Longueur | +| Wire | Colour | TPS63020 module | Board | Length | |---|---|---|---|---| -| W7 | rouge | VIN | J2 pad 1, +BATT (149,53 ; 57,25) | ~40 mm | -| W8 | noir | GND | J2 pad 2, GND (152,07 ; 57,25) | ~40 mm | -| W9 | blanc | VOUT 3,3 V | J2 pad 3, VCC (154,61 ; 57,25) | ~40 mm | +| W7 | red | VIN | J2 pad 1, +BATT (149.53 ; 57.25) | ~40 mm | +| W8 | black | GND | J2 pad 2, GND (152.07 ; 57.25) | ~40 mm | +| W9 | white | VOUT 3.3 V | J2 pad 3, VCC (154.61 ; 57.25) | ~40 mm | -- Sur le module : **EN relié à VIN**, et **PS/SYNC relié à GND** (mode économie). Sinon il reste en PWM et consomme quelques mA. -- Le module est posé à plat sur le dessus de la carte, sur de l'adhésif mousse double face, en vérifiant qu'aucune soudure ne passe dessous. Le volume sous le chapeau est largement suffisant. Le modèle du champignon ne prévoit pas encore son emplacement : c'est à ajouter. -- Avantage : la vis de MH5 n'est plus masquée (P7). -- Contrôle, sans BME280 ni ATtiny, en alimentant J13 par une alim de labo limitée à 100 mA : - - à 3,7 V et à 4,2 V, VCC (C2 pad 2) doit être entre **3,20 et 3,40 V** ; - - le courant à vide doit être **inférieur à 0,1 mA**. - Noter le résultat aussi dans le banc de test Tars `tests/bench/PLAT-POWER-REG-001.md`, qui n'a pas encore été passé. -- Consommation : l'ATtiny en sommeil consomme quelques µA ; le module en ajoute environ 25–50 µA à vide. Cela reste plusieurs années sur une 18650 de 2500 mAh. +- On the module: **EN tied to VIN**, and **PS/SYNC tied to GND** (power-save mode). Otherwise it stays in PWM and draws a few mA. +- The module lies flat on top of the board, on double-sided foam tape, checking that no solder joint runs under it. There is plenty of room under the cap. The mushroom model does not plan its spot yet: to add. +- Bonus: the MH5 screw is no longer hidden (P7). +- Check, without BME280 or ATtiny, feeding J13 from a bench supply limited to 100 mA: + - at 3.7 V and at 4.2 V, VCC (C2 pad 2) must be between **3.20 and 3.40 V**; + - the idle current must be **below 0.1 mA**. + Also log the result in the Tars test bench `tests/bench/PLAT-POWER-REG-001.md`, which has not been run yet. +- Consumption: the sleeping ATtiny draws a few µA; the module adds about 25–50 µA idle. That still means several years on a 2500 mAh 18650. -## Montage adapté +## Adapted assembly -- **J2 (P7) :** ne rien monter dans l'empreinte, seulement les fils W7–W9 (voir point 5). -- **C6 (P8) :** ne pas le monter. Il ne sert qu'au capteur analogique (J7 / JP4), absent du champignon. -- **JP2 « DIRECT Vcc » (R9) :** le laisser ouvert : sinon il court-circuite l'entrée et la sortie du TPS63020. -- **CSB du BME280 (R11) :** la position 5 n'est pas connectée. Vérifier au multimètre que le breakout tire bien CSB vers VIN (≈ 10 kΩ) : sinon le capteur passe en SPI. -- **Polarité de J13 (R2, P9) :** pas de protection contre l'inversion. Respecter le code couleur W5 rouge = borne 1 (+, pastille carrée) et W6 noir = borne 2. Marquer « + » au feutre à côté de J13. +- **J2 (P7):** fit nothing in the footprint, only wires W7–W9 (see item 5). +- **C6 (P8):** do not fit. It only serves the analog sensor (J7 / JP4), absent from the mushroom. +- **JP2 "DIRECT Vcc" (R9):** leave it open: otherwise it shorts the TPS63020 input to its output. +- **BME280 CSB (R11):** position 5 is not connected. Check with a meter that the breakout pulls CSB up to VIN (≈ 10 kΩ): otherwise the sensor goes into SPI mode. +- **J13 polarity (R2, P9):** no reverse protection. Follow the colour code W5 red = terminal 1 (+, square pad) and W6 black = terminal 2. Mark "+" with a felt pen next to J13. -## Recommandé, peu coûteux +## Recommended, cheap -- **P13 :** un condensateur de 100 nF côté dessous entre J1 broche 1 (GND, 193,00 ; 56,86) et J1 broche 2 (VCC), au plus près du module radio. -- **R7 :** une résistance de 10 kΩ entre J6 broche 5 (RESET) et J6 broche 2 (VCC), côté dessous. Ne pas ajouter de condensateur, pour rester compatible ISP et debugWIRE. +- **P13:** a 100 nF capacitor, bottom side, between J1 pin 1 (GND, 193.00 ; 56.86) and J1 pin 2 (VCC), as close as possible to the radio module. +- **R7:** a 10 kΩ resistor between J6 pin 5 (RESET) and J6 pin 2 (VCC), bottom side. No capacitor, to stay ISP and debugWIRE compatible. ## Firmware -Le correctif du sommeil (R3, R4) est sur la branche `hw/schematic-upgrade` : flasher cette version. +The sleep fix (R3, R4) is on branch `hw/schematic-upgrade`: flash that version. diff --git a/hardware/tinysensor/VARIANTE_LM35.md b/hardware/tinysensor/VARIANTE_LM35.md index 952f4be..ac10f24 100644 --- a/hardware/tinysensor/VARIANTE_LM35.md +++ b/hardware/tinysensor/VARIANTE_LM35.md @@ -1,59 +1,59 @@ -# Variante LM35 : capteur analogique alimenté en 5 V seulement pendant les mesures +# LM35 variant: analog sensor powered at 5 V only while measuring -Valable pour la v2.2 et la v3.0. L'ATtiny tourne directement sur 2 piles AA ou AAA. Le LM35 (4 V minimum) est alimenté par un module élévateur 5 V sans broche EN. Ce module n'est mis sous tension que pendant les mesures, par un PNP BC327 commandé par PB2 (J8 « EXT. INT0 ») : sortie à l'état bas pendant les mesures, entrée haute impédance le reste du temps (option de compilation `USE_SWITCHED_5V`). +Valid for v2.2 and v3.0. The ATtiny runs straight from 2 AA or AAA cells. The LM35 (4 V minimum) is powered by a 5 V boost module without an EN pin. That module is powered only while measuring, through a BC327 PNP driven by PB2 (J8 "EXT. INT0"): output low while measuring, high-impedance input the rest of the time (build option `USE_SWITCHED_5V`). -## Alimentation de la carte +## Board supply -- Piles 2×AA ou 2×AAA sur J13 (borne 1 = +, carrée ; borne 2 = GND). -- **JP2 « DIRECT Vcc » fermé**, rien sur J2 : VCC = tension des piles (2,0–3,2 V). L'ATtiny84A à 1 MHz fonctionne dès 1,8 V. -- Pas de BME280 (3,6 V maximum, mais surtout inutile ici). La portée radio baisse sous 3 V. +- 2×AA or 2×AAA cells on J13 (terminal 1 = +, square; terminal 2 = GND). +- **JP2 "DIRECT Vcc" closed**, nothing on J2: VCC = cell voltage (2.0–3.2 V). The ATtiny84A at 1 MHz runs from 1.8 V. +- No BME280 (3.6 V max, but mostly useless here). Radio range drops below 3 V. -## Commutation du module 5 V (câblage en l'air) +## Switching the 5 V module (point-to-point wiring) ``` +BATT ──┬────────── E BC327 (PNP) - 10k C ──── VIN module 5 V + 10k C ──── 5 V module VIN │ B └──── 1k ──────┤ │ - J8 broche 1 (PB2) + J8 pin 1 (PB2) ``` -| Pièce | Rôle | +| Part | Role | |---|---| -| BC327 | interrupteur côté +BATT du module 5 V | -| 10 kΩ base–émetteur | bloque le BC327 quand PB2 est en haute impédance (repos, sommeil, démarrage) | -| 1 kΩ vers PB2 | ≈ 2 mA de base pendant les mesures : le BC327 laisse passer ≈ 40 mA, le module démarre un peu plus lentement, sans conséquence | +| BC327 | switch on the +BATT side of the 5 V module | +| 10 kΩ base–emitter | keeps the BC327 off while PB2 is high impedance (idle, sleep, start-up) | +| 1 kΩ to PB2 | ≈ 2 mA of base current while measuring: the BC327 passes ≈ 40 mA, the module starts a bit slower, no consequence | -PB2 n'est jamais forcée à l'état haut. +BATT ne doit pas dépasser VCC, sinon un courant de fuite passe par la diode de protection de PB2 : c'est garanti avec JP2 fermé (VCC = piles). +PB2 is never driven high. +BATT must not exceed VCC, otherwise a leakage current flows through the PB2 clamp diode: guaranteed with JP2 closed (VCC = cells). -Brochage TO-92, face plate vers soi, pattes en bas, de gauche à droite : -- BC327 : C, B, E ; -- LM35 : +Vs, Vout, GND. +TO-92 pinout, flat face towards you, legs down, left to right: +- BC327: C, B, E; +- LM35: +Vs, Vout, GND. -Le vérifier sur la fiche du fabricant. +Check it on the maker's datasheet. ## LM35 -| LM35 | Relié à | +| LM35 | Wired to | |---|---| -| +Vs | VOUT du module 5 V | -| Vout | J7 broche 2 (entrée analogique, PA0 à travers R4 75 Ω) | -| GND | GND de la carte (J2 broche 2, ou J7 broche 3 si JP4 est soudé 2-3) | +| +Vs | 5 V module VOUT | +| Vout | J7 pin 2 (analog input, PA0 through R4 75 Ω) | +| GND | board GND (J2 pin 2, or J7 pin 3 if JP4 is soldered 2-3) | -GND du module 5 V : GND de la carte. +5 V module GND: board GND. -Pourquoi couper le +BATT et pas la masse : avec la masse coupée, celle du LM35 le serait aussi. La tension de saturation du transistor (10–30 mV) s'ajouterait à la mesure, soit 1 à 3 °C d'erreur. Le LM35 doit rester sur la vraie masse. Mais le module laisse passer VIN vers VOUT quand sa masse est coupée, et le LM35 resterait alors alimenté en sommeil. +Why switch +BATT and not ground: with the ground switched, the LM35 ground would be switched too. The transistor saturation voltage (10–30 mV) would add to the reading, i.e. 1 to 3 °C of error. The LM35 must stay on the true ground. But the module passes VIN to VOUT when its ground is cut, and the LM35 would then stay powered during sleep. ## Firmware -- Construire avec `BATTERY_IS_VCC`, `ANALOG1_X10_ID` et `USE_SWITCHED_5V`. -- Le firmware envoie des mV : la température vaut **mV ÷ 10** (°C), à convertir côté réception. -- Entre la mise sous tension des capteurs et la lecture analogique, il s'écoule au moins 30 ms. C'est assez pour que le module démarre (quelques ms) et que le LM35 se stabilise : à confirmer à l'oscilloscope sur VOUT. -- La mesure se fait par rapport à VCC, corrigée par la VCC mesurée : la résolution est d'environ 0,3 °C à 3 V. Pour faire mieux, il faudrait passer à la référence interne de 1,1 V. C'est une évolution firmware, qui passe par une exigence RBD. +- Build with `BATTERY_IS_VCC`, `ANALOG1_X10_ID` and `USE_SWITCHED_5V`. +- The firmware sends mV: the temperature is **mV ÷ 10** (°C), to convert on the receiving side. +- At least 30 ms elapse between sensor power-on and the analog reading. Enough for the module to start (a few ms) and for the LM35 to settle: to confirm with a scope on VOUT. +- The reading is relative to VCC, corrected by the measured VCC: resolution is about 0.3 °C at 3 V. Doing better needs the internal 1.1 V reference. That is a firmware change, through an RBD requirement. -## Contrôles +## Checks -1. Sans LM35, piles en place : en sommeil, VIN du module à 0 V et le courant total de quelques µA. -2. Pendant une mesure, VOUT du module à 5 V. Au multimètre, ça se voit difficilement : pour le test, relier J8 broche 1 à GND avec un fil. -3. Avec le LM35 : comparer à un thermomètre. Une erreur constante de 1 à 2 °C se corrige côté réception. +1. Without LM35, cells in: during sleep, module VIN at 0 V and a total current of a few µA. +2. During a measurement, module VOUT at 5 V. Hard to see with a meter: for the test, tie J8 pin 1 to GND with a wire. +3. With the LM35: compare with a thermometer. A constant 1 to 2 °C error is corrected on the receiving side. diff --git a/hardware/tinysensor/review/2026-11-06/pcb_review.md b/hardware/tinysensor/review/2026-11-06/pcb_review.md index 648038d..41efdac 100644 --- a/hardware/tinysensor/review/2026-11-06/pcb_review.md +++ b/hardware/tinysensor/review/2026-11-06/pcb_review.md @@ -1,6 +1,6 @@ -# Revue du PCB — tinySensor v2.2, 2026-11-06 +# PCB review — tinySensor v2.2, 2026-11-06 -Rapport brut de l'agent de revue (KiCad 10.0.7). Les chemins `scratchpad/` renvoient à des fichiers temporaires ; les rapports utiles sont copiés à côté. +Raw report from the review agent (KiCad 10.0.7). The `scratchpad/` paths point to temporary files; the useful reports are copied alongside. ### 1. Summary @@ -163,6 +163,6 @@ All under ``: - `F.Cu-1.png`, `B.Cu-1.png`, `F.Silkscreen-1.png` - `B.Cu_i2c_zoom-1.png` (shows the isolated island) - `.svg` and `.pdf` versions of the same layers -- Scripts: `(outils non conservés) ` (`sx.py`, `fps.py`, `trk.py`, `gr.py`, `sch.py`, `crop.py`) +- Scripts: (tools not kept) (`sx.py`, `fps.py`, `trk.py`, `gr.py`, `sch.py`, `crop.py`) **Not checked:** I did not open the GUI or export Gerbers or a drill file. I did not look at the 3D clash between real module models and the headers. The radio module's dimensions and the J13 wire-entry side are inferred, not verified. From 0f05a739da71abe3a849bcdd626e367d189a88bc Mon Sep 17 00:00:00 2001 From: Aurelien Labrosse Date: Sat, 14 Nov 2026 01:00:57 +0100 Subject: [PATCH 3/5] docs: openscad README in English Orca plate and object names stay as they are (identifiers used by update_3mf.py). Adds receiver.scad to the file list, the receiver screws and links to the board wiring guide. Co-Authored-By: Claude Opus 5.5 --- openscad/README.md | 340 +++++++++++++++++++++++---------------------- 1 file changed, 173 insertions(+), 167 deletions(-) diff --git a/openscad/README.md b/openscad/README.md index 430e285..65249af 100644 --- a/openscad/README.md +++ b/openscad/README.md @@ -1,234 +1,240 @@ -# Boîtiers imprimés 3D — TinySensor v2.2 +# 3D-printed enclosures — TinySensor v2.2 -Modèles OpenSCAD paramétriques (testés avec OpenSCAD 2021.01). Matière visée : **PETG**. +Parametric OpenSCAD models (tested with OpenSCAD 2021.01). Target material: **PETG**. -| Au mur | Sur pied | +| On the wall | On a stand | |---|---| -| ![Vue d'ensemble au mur](rendu.png) | ![Vue d'ensemble sur pied](rendu_pied.png) | +| ![Overview, on the wall](rendu.png) | ![Overview, on a stand](rendu_pied.png) | -![Boîtier ouvert : carte et accu 18650](rendu_ouvert.png) +![Open box: board and 18650 cell](rendu_ouvert.png) -Pièce de 2 € (Ø25,75 mm) dans les rendus pour l'échelle. +A €2 coin (Ø25.75 mm) appears in the renders for scale. -| Fichier | Contenu | +| File | Content | |---|---| -| `tinysensor_box.scad` | Boîtier mural principal (base + couvercle) | -| `sensor_shield.scad` | Abri de sonde extérieur pour breakout BMx280 + clip optionnel | -| `stand.scad` | Pied de table : mât + semelle, pour poser boîtier + abri debout | -| `pcb.scad` | Représentation simplifiée de la carte v2.2 et de l'accu 18650 (rendus uniquement) | -| `euro2.scad` | Pièce de 2 € pour l'échelle des rendus | -| `mushroom.scad` | Boîtier « champignon » décoratif (en plus du boîtier mural) | -| `harness.py` | Générateur du plan de câblage imprimable du champignon (`python3 harness.py` → `faisceau_champignon.pdf`, A4) | -| `assembly.scad` | Vue d'ensemble (boîtier + abri clipsé, au mur ou sur pied), sert à générer `rendu.png` et `rendu_pied.png` | - -> Statut : **non imprimé, non validé**. Jeux et serrages à confirmer au premier tirage. +| `tinysensor_box.scad` | Main wall box (base + lid) | +| `sensor_shield.scad` | Outdoor radiation shield for a BMx280 breakout + optional clip | +| `stand.scad` | Table stand: mast + foot, to stand the box + shield upright | +| `pcb.scad` | Simplified v2.2 board and 18650 cell (renders only) | +| `euro2.scad` | €2 coin for render scale | +| `mushroom.scad` | Decorative "mushroom" enclosure (alongside the wall box) | +| `receiver.scad` | Desk enclosure of the tinyDecoders receiver (Pro Micro, RXB6, OLED) | +| `harness.py` | Printable mushroom wiring plan generator (`python3 harness.py` → `faisceau_champignon.pdf`, A4) | +| `assembly.scad` | Overview (box + clipped shield, on the wall or on the stand), generates `rendu.png` and `rendu_pied.png` | + +> Status: **not printed, not validated** (wall box, shield, stand). Clearances and fits to confirm on the first print. ## Export -Les pièces prêtes à trancher sont dans `print/`, **un projet OrcaSlicer par modèle**, une plaque par usage et chaque objet nommé : +Ready-to-slice parts are in `print/`, **one OrcaSlicer project per model**, one plate per use and every object named: -| Projet | Plaques | Objets | +| Project | Plates | Objects | |---|---|---| -| `box.3mf` | Fond extérieur, Fond intérieur, Couvercle | n'imprimer qu'un des deux fonds : `ext` (presse-étoupe, extérieur) ou `int` (aérations, intérieur) | -| `shield.3mf` | Abri, Assiettes | Chapeau, Fond, Clip (optionnel) ; 4 assiettes | -| `stand.3mf` | Mât, Pied | pied de table, optionnel | -| `receiver.3mf` | Pupitre | Coque, Fond : pupitre du récepteur tinyDecoders (Pro Micro, RXB6, OLED), vis M3×6 à tête cylindrique | +| `box.3mf` | Fond extérieur, Fond intérieur, Couvercle | print only one of the two bases: `ext` (cable gland, outdoor) or `int` (vents, indoor) | +| `shield.3mf` | Abri, Assiettes | Chapeau, Fond, Clip (optional); 4 plates | +| `stand.3mf` | Mât, Pied | table stand, optional | +| `receiver.3mf` | Pupitre | Coque, Fond: tinyDecoders receiver desk enclosure (Pro Micro, RXB6, OLED), M3×6 cylinder-head screws; wiring: [`hardware/receiver/WIRING.md`](../hardware/receiver/WIRING.md) | | `mushroom.3mf` | Ailettes, Hat, White buttons, Ground, Divers, Foot, Support | Chapeau, Lamelles, Châssis, Plancher, Pied, Sol, Berceau, Bouchon, Pois Ø6 / Ø8 / Ø10 | -`mushroom_cap.3mf` : chapeau du champignon tel qu'imprimé (réglages Orca de l'impression). Snapshot figé, hors de `export.sh`. +Plate and object names are the ones in the Orca projects and in `update_3mf.py` (French, kept as identifiers). + +`mushroom_cap.3mf`: mushroom cap as printed (Orca settings of that print). Frozen snapshot, outside `export.sh`. -Après une modification d'un `.scad` (environ 30 s, fermer les projets dans Orca avant) : +After changing a `.scad` (about 30 s, close the projects in Orca first): ```bash -./export.sh # tous les projets -./export.sh print/box.3mf # ou un seul +./export.sh # every project +./export.sh print/box.3mf # or just one ``` -`export.sh` lance `update_3mf.py`, qui réexporte chaque pièce et remplace son maillage dans le projet : plaques, positions, rotations et réglages Orca sont gardés, l'ancien projet reste en `print/.3mf.bak`. Un projet absent est recréé, avec les réglages d'imprimante et de filament de `mushroom.3mf` (Ender 3). Les pièces ne doivent être tournées qu'autour de Z dans Orca, sinon le script refuse de les mettre à jour ; une pièce supprimée dans Orca est signalée, pas remise. Réglages par objet posés s'ils manquent : chapeau du champignon brim extérieur 10 mm, remplissage 10 % ; pied du champignon 6 parois. Les nombres de pois sont ceux du projet : l'écho « Pois » de `mushroom.scad` donne le compte à placer si la graine ou `dot_gap` changent. +`export.sh` runs `update_3mf.py`, which re-exports each part and replaces its mesh in the project: plates, positions, rotations and Orca settings are kept, the previous project stays as `print/.3mf.bak`. A missing project is recreated, with the printer and filament settings of `mushroom.3mf` (Ender 3). Parts may only be rotated about Z in Orca, otherwise the script refuses to update them; a part deleted in Orca is reported, not put back. Per-object settings set when missing: mushroom cap outer brim 10 mm, 10 % infill; mushroom stem 6 walls. The dot counts are the project's: the "Pois" echo of `mushroom.scad` gives the count to place if the seed or `dot_gap` change. + +Exporting a single part outside a project (STL, another format): `openscad -D 'part="lid"' -o out/lid.3mf tinysensor_box.scad` (see each `.scad` header). -Export d'une pièce hors projet (STL, autre format) : `openscad -D 'part="lid"' -o out/lid.3mf tinysensor_box.scad` (voir l'en-tête de chaque `.scad`). +Open-box render: `openscad --preview -D 'part="open"' --camera=110,-230,420,110,30,0 --imgsize=1400,1000 -o rendu_ouvert.png tinysensor_box.scad`. -Rendu boîtier ouvert : `openscad --preview -D 'part="open"' --camera=110,-230,420,110,30,0 --imgsize=1400,1000 -o rendu_ouvert.png tinysensor_box.scad`. +Every part is exported in its print orientation and prints **without support**, the mushroom dome included. `out/` is ignored by git, `print/` is not. -Toutes les pièces sont exportées dans leur orientation d'impression et s'impriment **sans support**, y compris le dôme du champignon. `out/` est ignoré par git, pas `print/`. +## Main box (`tinysensor_box.scad`) -## Boîtier principal (`tinysensor_box.scad`) +- Fitted to the v2.2 PCB (`hardware/tinysensor/tinySensorv1.kicad_pcb`): 93.98 × 63.75 mm, held by the 4 corner M2.5 holes on 5 mm standoffs. +- Clearance above the PCB: 25 mm (large capacitors), parameter `comp_h`. +- Free 14 mm bay on the bottom side (18650 holder side) for cables and antenna. +- Overall size: ~117 × 101 × 39 mm (flange and lid included). +- **Closure**: 6 M3 self-tapping screws, outside the seal area. +- **Sealing**: 2.2 × 1.5 mm groove for a Ø2 cord (printed TPU or EPDM), squeezed by a lid rib. +- **Wall mounting**: 2 tabs, Ø4 countersunk screws. The back stays solid. +- Variants (`usage`): + - `ext`: Ø12.5 hole for a **PG7** cable gland (remote probe). By default in the **left side wall**, level with the bay, under the flange chamfer that keeps the rain off; `gland_side` = `left` / `right` / `bottom` (former position, bottom wall). The cable runs down along the box then up into the shield through its base: allow ~15 cm more and a drip loop. + - `int`: top and bottom vent slots (convection). +- The bottom tab has two side notches for the shield clip (`shield_notch`). -- Calé sur le PCB v2.2 (`hardware/tinysensor/tinySensorv1.kicad_pcb`) : 93,98 × 63,75 mm, fixation par les 4 trous M2.5 des coins sur entretoises de 5 mm. -- Dégagement au-dessus du PCB : 25 mm (gros condensateurs), paramètre `comp_h`. -- Baie libre de 14 mm côté bas (côté support 18650) pour câbles et antenne. -- Encombrement : ~117 × 101 × 39 mm (rebord compris, couvercle inclus). -- **Fermeture** : 6 vis M3 auto-taraudeuses, hors de la zone joint. -- **Étanchéité** : gorge 2,2 × 1,5 mm pour cordon Ø2 (TPU imprimé ou EPDM), écrasé par une nervure du couvercle. -- **Fixation murale** : 2 pattes, vis Ø4 tête fraisée. Le fond reste plein. -- Variantes (`usage`) : - - `ext` : trou Ø12,5 pour presse-étoupe **PG7** (sonde déportée). Par défaut en **paroi latérale gauche**, au niveau de la baie, sous le chanfrein du rebord qui l'abrite de la pluie ; `gland_side` = `left` / `right` / `bottom` (ancienne position, paroi basse). Le câble redescend le long du boîtier puis remonte dans l'abri par le fond : prévoir ~15 cm de plus et une boucle d'égouttement. - - `int` : fentes d'aération haut et bas (convection). -- La patte basse porte deux encoches latérales pour le clip de l'abri (`shield_notch`). +## Radiation shield (`sensor_shield.scad`) -## Abri de sonde (`sensor_shield.scad`) +Stacked-plate shield (Stevenson type): outdoors, a BMx280 shut inside the sealed box would measure the box air, not the ambient air. -Abri à assiettes (type Stevenson) : en extérieur, un BMx280 enfermé dans le boîtier étanche mesurerait l'air du boîtier, pas l'air ambiant. +- Solid cap + 4 louvred plates + base, Ø56 mm, 10 mm pitch, ~52 mm high. +- Assembly: 3 **M3** threaded rods through the spacers, nuts under the base. +- Base: vertical slide for the **15 × 11 mm, 6-pin** breakout (`BMx280_breakout` footprint), Ø6 cable pass underneath, 6 drain holes. +- Print in **white** (limits solar heating). Place in the shade if possible. -- Chapeau plein + 4 assiettes ajourées + fond, Ø56 mm, pas de 10 mm, hauteur ~52 mm. -- Assemblage : 3 tiges filetées **M3** traversant les entretoises, écrous sous le fond. -- Fond : glissière verticale pour le breakout **15 × 11 mm, 6 broches** (empreinte `BMx280_breakout`), passe-câble Ø6 dessous, 6 trous de drainage. -- Imprimer en **blanc** (limite l'échauffement solaire). Poser à l'ombre si possible. +### Mounting (pick one) -### Fixation (au choix) +1. **Wall**: plate with 2 Ø4 countersunk screws on the probe side. Screw the cap to the wall **before** stacking the plates. +2. **Clip under the box** (`part="clip"`): adapter bolted behind the plate (2 M4×8 countersunk screws + nuts sunk on the wall side). Its two prongs grip the bottom wall tab of the box (45° lips) and a bump snaps into each notch. Fitting: screw the box, slide the shield up until it clicks. + - Adjustable fit: `bump_r` (grip), `prong_w` (stiffness). -1. **Murale** : platine à 2 vis Ø4 fraisées côté sonde. Visser le chapeau au mur **avant** d'empiler les assiettes. -2. **Clip sous le boîtier** (`part="clip"`) : adaptateur boulonné derrière la platine (2 vis M4×8 tête fraisée + écrous noyés côté mur). Ses deux branches enserrent la patte murale basse du boîtier (lèvres à 45°) et un bossage s'enclenche dans chaque encoche. Pose : visser le boîtier, glisser l'abri vers le haut jusqu'au clic. - - Serrage réglable : `bump_r` (accroche), `prong_w` (souplesse). +3. **On the stand** (`stand.scad`): see below. -3. **Sur pied** (`stand.scad`) : voir ci-dessous. +## Table stand (`stand.scad`) -## Pied de table (`stand.scad`) +To stand the box + shield upright on a table, with no wall. -Pour poser l'ensemble boîtier + abri debout sur une table, sans mur. +- **Mast** 30 × 6 × 212 mm: it stands in for the wall. The box screws onto it by its two wall tabs (M4 countersunk, nuts sunk in the back of the mast); the shield stays clipped under the bottom tab, as on the wall. +- **Foot** 130 × 136 mm with a 25 mm socket and gussets: the mast fits in and is locked by a through M4 screw. It reaches 90 mm forward to balance the shield, which sticks out to ~72 mm from the mast. +- 30 mm of clearance under the shield (`under_clr`) for the loop of the cable that comes up through the base. +- The mast (212 mm) fits a 220 × 220 bed. +- The box and shield dimensions are copied at the top of the file: realign them if the other models change. -- **Mât** 30 × 6 × 212 mm : il tient lieu de mur. Le boîtier s'y visse par ses deux pattes murales (M4 tête fraisée, écrous noyés dans le dos du mât) ; l'abri reste clipsé sous la patte basse, comme au mur. -- **Semelle** 130 × 136 mm avec douille de 25 mm et goussets : le mât s'y emboîte et se bloque par une vis M4 traversante. Elle déborde de 90 mm vers l'avant pour équilibrer l'abri, qui avance jusqu'à ~72 mm du mât. -- 30 mm de dégagement sous l'abri (`under_clr`) pour la boucle du câble qui remonte par le fond. -- Le mât (212 mm) tient sur un plateau 220 × 220. -- Les cotes du boîtier et de l'abri sont recopiées en tête de fichier : à réaligner si on modifie les autres modèles. +### Wiring -### Câblage +- I2C link < 1 m: 4.7 kΩ pull-ups, twisted cable (SDA/GND, SCL/GND). +- The cable leaves the cable gland and runs up into the shield through its base. -- Liaison I2C < 1 m : pull-ups 4,7 kΩ, câble torsadé (SDA/GND, SCL/GND). -- Le câble sort du presse-étoupe et remonte dans l'abri par le fond. +Board connectors and module wiring: [`hardware/tinysensor/WIRING.md`](../hardware/tinysensor/WIRING.md). -## Champignon (`mushroom.scad`) +## Mushroom (`mushroom.scad`) -Variante décorative, type amanite, qui ne remplace pas le boîtier mural. Le chapeau est un tout (dôme + lamelles) posé sur le pied avec une légère inclinaison ; toute l'électronique est dans le chapeau, le pied ne porte que l'accu. +Decorative fly-agaric variant, which does not replace the wall box. The cap is one piece (dome + gills) sitting on the stem with a slight tilt; all the electronics are in the cap, the stem only carries the cell. -| Vue d'ensemble | Pied | Dessous | Coupe | +| Overview | Stem | Underside | Section | |---|---|---|---| -| ![Champignon](rendu_champignon.png) | ![Champignon, pied](rendu_champignon_pied.png) | ![Champignon, dessous](rendu_champignon_dessous.png) | ![Champignon, coupe](rendu_champignon_coupe.png) | +| ![Mushroom](rendu_champignon.png) | ![Mushroom, stem](rendu_champignon_pied.png) | ![Mushroom, underside](rendu_champignon_dessous.png) | ![Mushroom, section](rendu_champignon_coupe.png) | -Pièces : +Parts: -- **Dôme** (`cap`) ~147 × 147 × 190 mm (rayon maxi moyen 72) : **gourde** d'après croquis, méridien `cap_shape` (bas large, taille Ø~55, haut Ø~72), **contour lobé** issu d'un générateur (voir ci-dessous). À partir de la zone qui se resserre (`cap_neck`), le haut penche de 8° vers −X (`cap_lean`, contre l'inclinaison du chapeau), pour ramener la masse vers l'axe du pied : sommet à ~6 mm de l'axe du pied côté +X. La carte est tournée de 180° dans le châssis pour que le sommet reste à l'aplomb de **J1 « RADIO »** (module 433 MHz), selon l'axe de la carte (`cap_top` est calculé), à 126 mm au-dessus de la carte. **Évent** Ø5 (`vent_d`) : de la pointe du plafond, il descend de ~15° vers le flanc arrière (+Y), sans pois autour ; la pluie n'entre pas et l'air chaud sort, à l'opposé de J1. Le gros du bas part à l'opposé, côté évasement du pied. Plafond intérieur conique à 45°, sans support : le haut est plein sous le sommet. Son bord descend de 4 mm sous le disque à lamelles (cache sa tranche) et s'épaissit vers l'intérieur à **6 mm de large** (`rim_w`, raccord à 45°) : ~2,4 fois plus de surface sur le plateau pour une pièce de 190 mm de haut, silhouette inchangée. Fixation : 3 **consoles** sur la paroi intérieure, juste au-dessus du disque (`corbel_*` : 12 mm de large, face droite de 10 mm puis chanfrein à 45° jusqu'à la paroi, trou pilote Ø2,5 × 8) ; le disque monte dans le dôme et y est plaqué par 3 vis M3 × 10 auto-taraudeuses par en dessous, têtes sous le disque entre les lamelles. ![Coupe par une console](rendu_champignon_console.png) -- **Lamelles** (`gills`) : disque qui ferme le dessous du chapeau, au contour du plus étroit de la jupe du dôme moins 0,4 mm (`disc_fit`) pour passer les creux des lobes, 48 lames radiales plus profondes près du pied (`gill_depth`), fentes de ventilation entre les lames sous la chambre du capteur (y compris côté +X, à 0° et ±7,5°, au ras du bord +X du BME280), ouverture de la prise USB, fourreau central qui reçoit le tenon du pied. S'imprime lames vers le haut. -- **Châssis** (`chassis`) vissé sur le disque (4 vis M3 par en dessous), imprimé debout : - - chambre du bas (48 mm, `chamber_h`), ventilée : **BME280 enfiché sous la carte**, sans fil ni glissière (voir *Câblage*) ; le breakout pend parallèle à la carte, capteur vers le disque, à 42 mm au-dessus du disque ; fenêtres du châssis en haut de la chambre (`win_h`), pour que l'air passe devant le capteur. Le haut du berceau traverse le disque de 30 mm (trou Ø37, `disc_bore`) et finit 12 mm sous le breakout, avec la paire JST-XH ; - - **plancher percé** (`deck`) sur des appuis, avec une fenêtre de 17,2 × 4,5 mm pour la barrette femelle ; - - **carte v2.2 découpée** (60 × 39 mm, le long des pointillés de sérigraphie x = 147 / y = 87, sans les supports 18650 et CR2032) : tournée de 180° (J1 côté −X), 2 vis M2.5 (MH5, MH1 du haut), le bord bas repose sur un appui. L'accu arrive sur **J13 « RAW IN »** (`+BATT`/`GND`) ; - - glissière du **module de charge** TP4056, vertical sur le côté, prise **micro-USB vers le bas** entre deux lamelles. -- **Pied** (`stem`) en **poire d'amanite** d'après croquis : base bulbeuse (~106 × 96 mm au ventre, Ø70 à Ø85 sur la table), col Ø44 à 50 à l'anneau, Ø45 sous le fourreau, creux (Ø29,4) jusqu'en haut : reçoit le berceau de l'accu, inséré par le haut, posé à 72 mm du sol. Dessous, une **chambre à lest** fermée qui épouse la forme du pied (paroi de 2,4 mm, `ballast_wall`, bulbe compris), plafond conique à 45° jusqu'à 70 mm (`ballast_h`) : ~220 cm³, soit ~1 kg de billes d'acier de 1 mm si on la remplit. Remplissage par un trou de Ø15 dans le fond, fermé par un **bouchon** (`plug`, Ø21 × 1,2 mm) collé dans un lamage, à fleur du dessous. Même esprit que le chapeau : - - fût lobé (`stem_amp`, lobes qui tournent le long du fût, `stem_twist`) ; la déformation n'ajoute que de la matière, la paroi autour de l'accu ne s'amincit pas, et s'annule sous le fourreau ; - - base bulbeuse dissymétrique (`bulb_r`, `bulb_z`, `bulb_up`, `bulb_dn`, `foot_dir`) : épaule plus marquée d'un côté, dessous arrondi à 44° au plus ; - - anneau en **coupelle** sous le chapeau (`ring_z`, `ring_ext`, `ring_rise`), bord relevé et ondulé (`ring_amp`), 45° au plus : sans support. Les ondulations sont calées (`ring_rot`) pour que la coupelle soit en creux sous la prise USB : son bord reste à 2 mm en deçà de la fenêtre sous la fiche (vérifié par un `assert`), le câble se branche par en dessous. - - En haut, un tenon incliné de 6° (`cap_tilt`) s'emboîte dans le fourreau, 1 vis M3 radiale côté +X, sous la prise USB (`collar_screw_a`). -- **Berceau** (`cradle`) 74,8 × Ø27,8 mm : gouttière amovible pour une **18650 nue** (65 mm, téton plat ; `cell_l` pour une autre longueur), sans soudure sur l'accu. Deux **butées à languette** (plaque 13 × 11 mm, languette 3 × 7 percée) glissées dans des rainures : ressort côté « − » en bas, bossage côté « + » en haut (polarité gravée sur les bouts). Languettes vers le haut : chaque fil soudé dessus sort par l'encoche voisine (`wire_notch`) et longe l'accu dans la fente du dessus jusqu'au fourreau (W1 rouge 40 mm, W2 noir 110 mm, soudé → JST-XH F, voir *Câblage*) ; on soude les fils sur les languettes, pas sur l'accu. L'accu entre par la fente du dessus en comprimant le ressort (≈ 4 mm, `spring_c`). Trou de tirage Ø2,5 en haut (ficelle, nœud à l'intérieur). Flancs allégés par 3 fenêtres de chaque côté, sous les lèvres qui retiennent l'accu (`cr_win`, 15,5 g au lieu de 21 g en plein). Imprimé à plat, méplat sur le plateau, sans support. ![Berceau : monté et nu](rendu_champignon_berceau.png) -- **Sol** (`ground`) : disque de 1 mm au contour lobé (Ø~145, `ground_R`), frange d'herbe en zigzag autour du pied, touffes de grands brins et brins isolés (`grass_*`, `fringe_h`). Pièce séparée à imprimer en vert ; le pied se colle dessus. Élargit l'appui au sol. -- **Pois** (`dot`) rapportés, collés dans des logements de 0,8 mm, répartis par tirage sur la surface réelle (écart mini `dot_gap`) : 99 (Ø6 × 45, Ø8 × 26, Ø10 × 28), l'écho OpenSCAD donne le nombre par taille. +- **Dome** (`cap`) ~147 × 147 × 190 mm (average max radius 72): **gourd** after a sketch, meridian `cap_shape` (wide bottom, waist Ø~55, top Ø~72), **lobed outline** from a generator (see below). From the narrowing zone (`cap_neck`) up, the top leans 8° towards −X (`cap_lean`, against the cap tilt), to bring the mass back over the stem axis: top ~6 mm from the stem axis on the +X side. The board is turned 180° in the chassis so that the top stays right above **J1 "RADIO"** (433 MHz module), along the board axis (`cap_top` is computed), 126 mm above the board. **Vent** Ø5 (`vent_d`): from the ceiling tip, it runs ~15° down towards the rear flank (+Y), with no dots around it; rain stays out and warm air leaves, opposite J1. The bulk of the bottom goes the other way, towards the flare of the stem. Inner ceiling conical at 45°, no support: the top is solid under the tip. Its edge drops 4 mm below the gill disc (hides its edge) and thickens inwards to **6 mm wide** (`rim_w`, 45° blend): ~2.4 times more bed contact for a 190 mm high part, same silhouette. Fixing: 3 **corbels** on the inner wall, just above the disc (`corbel_*`: 12 mm wide, 10 mm straight face then 45° chamfer to the wall, Ø2.5 × 8 pilot hole); the disc rises into the dome and is pulled against them by 3 M3 × 10 self-tapping screws from below, heads under the disc between the gills. ![Section through a corbel](rendu_champignon_console.png) +- **Gills** (`gills`): disc closing the underside of the cap, following the narrowest outline of the dome skirt minus 0.4 mm (`disc_fit`) to clear the lobe hollows, 48 radial blades deeper near the stem (`gill_depth`), vent slots between the blades under the sensor chamber (including on the +X side, at 0° and ±7.5°, flush with the +X edge of the BME280), USB socket opening, central sleeve taking the stem tenon. Prints blades up. +- **Chassis** (`chassis`) screwed onto the disc (4 M3 screws from below), printed upright: + - lower chamber (48 mm, `chamber_h`), ventilated: **BME280 plugged under the board**, no wire or slide (see *Wiring*); the breakout hangs parallel to the board, sensor towards the disc, 42 mm above the disc; chassis windows at the top of the chamber (`win_h`), so that air flows past the sensor. The top of the cradle goes 30 mm through the disc (Ø37 hole, `disc_bore`) and ends 12 mm below the breakout, with the JST-XH pair; + - **perforated deck** (`deck`) on supports, with a 17.2 × 4.5 mm window for the female header; + - **cut-down v2.2 board** (60 × 39 mm, along the silkscreen dotted lines x = 147 / y = 87, without the 18650 and CR2032 holders): turned 180° (J1 on −X), 2 M2.5 screws (MH5, top MH1), the bottom edge rests on a ledge. The cell arrives on **J13 "RAW IN"** (`+BATT`/`GND`); + - slide for the TP4056 **charge module**, upright on the side, **micro-USB facing down** between two gills. +- **Stem** (`stem`) as a **fly-agaric pear** after a sketch: bulbous base (~106 × 96 mm at the belly, Ø70 to Ø85 on the table), neck Ø44 to 50 at the ring, Ø45 under the sleeve, hollow (Ø29.4) to the top: takes the cell cradle, inserted from the top, sitting 72 mm above the ground. Underneath, a closed **ballast chamber** following the stem shape (2.4 mm wall, `ballast_wall`, bulb included), conical ceiling at 45° up to 70 mm (`ballast_h`): ~220 cm³, i.e. ~1 kg of 1 mm steel shot if filled. Filled through a Ø15 hole in the bottom, closed by a **plug** (`plug`, Ø21 × 1.2 mm) glued into a counterbore, flush with the underside. Same spirit as the cap: + - lobed shaft (`stem_amp`, lobes twisting along the shaft, `stem_twist`); the deformation only adds material, the wall around the cell never gets thinner, and it fades out under the sleeve; + - asymmetric bulbous base (`bulb_r`, `bulb_z`, `bulb_up`, `bulb_dn`, `foot_dir`): stronger shoulder on one side, underside rounded at 44° at most; + - **cup** ring under the cap (`ring_z`, `ring_ext`, `ring_rise`), raised wavy edge (`ring_amp`), 45° at most: no support. The waves are phased (`ring_rot`) so that the cup dips under the USB socket: its edge stays 2 mm short of the window under the plug (checked by an `assert`), the cable plugs in from below. + - At the top, a tenon tilted 6° (`cap_tilt`) fits into the sleeve, 1 radial M3 screw on the +X side, under the USB socket (`collar_screw_a`). +- **Cradle** (`cradle`) 74.8 × Ø27.8 mm: removable trough for a **bare 18650** (65 mm, flat top; `cell_l` for another length), no solder on the cell. Two **tab contacts** (13 × 11 mm plate, drilled 3 × 7 tab) slid into grooves: spring on the "−" side at the bottom, bump on the "+" side at the top (polarity engraved on the ends). Tabs up: each wire soldered on them leaves through the nearby notch (`wire_notch`) and runs along the cell in the top slot up to the sleeve (W1 red 40 mm, W2 black 110 mm, soldered → JST-XH F, see *Wiring*); the wires are soldered to the tabs, not to the cell. The cell goes in through the top slot, compressing the spring (≈ 4 mm, `spring_c`). Ø2.5 pull hole at the top (string, knot inside). Flanks lightened by 3 windows on each side, under the lips that hold the cell (`cr_win`, 15.5 g instead of 21 g solid). Printed flat, flat face on the bed, no support. ![Cradle: fitted and bare](rendu_champignon_berceau.png) +- **Ground** (`ground`): 1 mm disc with a lobed outline (Ø~145, `ground_R`), zigzag grass fringe around the stem, tufts of tall blades and single blades (`grass_*`, `fringe_h`). Separate part to print in green; the stem is glued on it. Widens the footprint. +- **Dots** (`dot`) added afterwards, glued into 0.8 mm recesses, spread by random draw over the real surface (minimum gap `dot_gap`): 99 (Ø6 × 45, Ø8 × 26, Ø10 × 28), the OpenSCAD echo gives the count per size. -Hauteur totale ~300 mm. +Total height ~300 mm. -### Contour du chapeau +### Cap outline -Le contour vu de dessus est une somme d'harmoniques (2 à 7) à phases aléatoires, pondérées autour de `lobe_n` lobes et normalisées à `lobe_amp` (écart maxi au rayon moyen, ±22 % par défaut). La déformation est pleine au bord et s'atténue vers le sommet (`lobe_top` = part qui reste en haut). Le bord reste plat (impression bord sur le plateau). +The top-view outline is a sum of harmonics (2 to 7) with random phases, weighted around `lobe_n` lobes and normalised to `lobe_amp` (maximum deviation from the mean radius, ±22 % by default). The deformation is full at the edge and fades towards the top (`lobe_top` = share left at the top). The edge stays flat (printed edge on the bed). -- `lobe_seed` change la forme. Planche des graines 1 à 8 (vue de dessus) : ![Graines](champignon_graines.png) -- `lobe_rot` : orientation du contour (165° pour la graine 6) ; `-1` : OpenSCAD choisit l'orientation qui laisse le plus de jeu autour de l'électronique (creux face aux petits côtés du châssis, lobe sur le chargeur). Les 3 vis du dôme se placent d'elles-mêmes dans les lobes. -- La console affiche l'**indice de jeu** : ≤ 1 = châssis, carte (composants ≤ 14 mm) et module de charge à ≥ 3 mm du dôme partout ; sinon un avertissement (augmenter `cap_R` / `cap_H` ou changer de graine). La console affiche aussi la marge des fils au-dessus du berceau ; `stem_h` et `ballast_h` ne descendent pas sous 5 mm de marge (assert). +- `lobe_seed` changes the shape. Seeds 1 to 8 (top view): ![Seeds](champignon_graines.png) +- `lobe_rot`: outline orientation (165° for seed 6); `-1`: OpenSCAD picks the orientation that leaves the most clearance around the electronics (hollows facing the short sides of the chassis, a lobe on the charger). The 3 dome screws place themselves in the lobes. +- The console shows the **clearance index**: ≤ 1 = chassis, board (components ≤ 14 mm) and charge module ≥ 3 mm from the dome everywhere; otherwise a warning (raise `cap_R` / `cap_H` or change seed). The console also shows the wire margin above the cradle; `stem_h` and `ballast_h` do not go below 5 mm of margin (assert). -### Impression +### Printing -- Toutes les pièces sortent dans leur sens d'impression. Le **dôme** (190 mm de haut, tient sur Ender 3) s'imprime bord sur le plateau, **sans support** : imprimé en PETG (~20 h), les faces d'appui des consoles, à 7 mm du plateau, passent en pont sur plus de 20 mm avec deux angles droits. Remplissage faible (~10 %) : le haut est plein dans le modèle (~60 cm³, 76 g à 100 %, ~8 g à 10 %), juste pour se passer de supports derrière le col, et il pèse au mauvais endroit. Les autres pièces s'impriment sans support (pied : 113 mm de haut). -- Pied : **6 parois** (buse 0,4) : la paroi de la chambre à lest fait 2,4 mm (`ballast_wall`), elle est ainsi pleine sans remplissage d'interstices. 4 parois suffisent pour les autres pièces. -- Dôme en rouge (ou brun : panthère), pois en blanc, sol en vert, le reste en blanc / ivoire. -- Sol : brins et frange de 0,9 mm d'épaisseur, pointes à 0,6 mm (buse 0,4) ; le slicer peut tronquer les pointes les plus fines. +- Every part comes out in its print orientation. The **dome** (190 mm high, fits an Ender 3) prints edge on the bed, **without support**: printed in PETG (~20 h), the corbel bearing faces, 7 mm above the bed, bridge over more than 20 mm with two right angles. Low infill (~10 %): the top is solid in the model (~60 cm³, 76 g at 100 %, ~8 g at 10 %), only to avoid supports behind the neck, and it weighs in the wrong place. The other parts print without support (stem: 113 mm high). +- Stem: **6 walls** (0.4 nozzle): the ballast chamber wall is 2.4 mm (`ballast_wall`), so it is solid without gap infill. 4 walls are enough for the other parts. +- Dome in red (or brown: panther cap), dots in white, ground in green, the rest in white / ivory. +- Ground: blades and fringe 0.9 mm thick, tips at 0.6 mm (0.4 nozzle); the slicer may truncate the finest tips. -### Montage, accu +### Assembly, cell -![Vue éclatée : sol et pied, accu, disque à lamelles, châssis et chargeur, plancher percé, carte + BME280, dôme](rendu_champignon_eclate.png) +![Exploded view: ground and stem, cell, gill disc, chassis and charger, perforated deck, board + BME280, dome](rendu_champignon_eclate.png) -0. Pied à l'envers : grenaille par le trou du fond (quelques gouttes de colle pour la figer et éviter qu'elle sonne), bouchon collé dans son lamage (époxy), puis pied collé sur le sol. -1. Carte : souder une **barrette femelle 2,54 1×6** sur le **dessous** de la carte, dans l'empreinte « I2C » ; les JST « I2C » et « I2C_SPI » ne sont pas posés (voir *Câblage*). Côté breakout, souder sa barrette mâle de façon que la puce regarde le disque une fois enfiché (corps du breakout vers l'intérieur du châssis, à vérifier selon le modèle de breakout). -2. Module de charge : souder W5 / W6 (rouge / noir, 70 mm, OUT+ / OUT− soudé → bornier à vis J13). W3 / W4 se soudent à l'étape 3 : le boîtier JST-XH (~7,4 × 5,8 mm) passe de justesse dans la fenêtre de la paroi +Y (8 mm de large), on passe donc l'extrémité nue. -3. Plancher posé dans le châssis, carte vissée par-dessus (la barrette femelle traverse la fenêtre du plancher) ; châssis retourné, BME280 enfiché par le dessous ouvert du châssis. Module de charge dans sa glissière ; W5 / W6 (rouge / noir, 70 mm, TP4056 soudé → bornier à vis) vissés sur J13 ; W3 / W4 (rouge / noir, 100 mm, TP4056 soudé → JST-XH M) : JST-XH M déjà serti, côté chambre ; pousser l'autre extrémité, nue, de l'intérieur de la chambre vers l'extérieur par la fenêtre de la paroi +Y (x = 22), puis la souder sur B+ / B− du TP4056 ; le côté JST reste dans la chambre, au-dessus du berceau. Enfin châssis sur le disque à lamelles (4 vis par en dessous). -4. Disque à lamelles poussé dans le dôme jusqu'aux 3 consoles, 3 vis M3 × 10 auto-taraudeuses par en dessous (têtes entre les lamelles). L'antenne du module 433 MHz monte droit de J1 dans le haut de la gourde. -5. Berceau : butées dans les rainures, W1 rouge 40 mm / W2 noir 110 mm soudés en haut des languettes, sortis par les encoches des bouts (W2 longe l'accu dans la fente du dessus) → JST-XH F ; accu clipsé ; berceau dans le pied par le haut (ressort en bas). -6. Chapeau tenu ~50 mm au-dessus du pied, brancher la paire JST-XH, la loger dans la chambre juste au-dessus du berceau (trou central du disque), chapeau emboîté sur le tenon, vis radiale. -7. Changer l'accu : retirer la vis radiale, soulever le chapeau (~50 mm) : la paire JST-XH apparaît, la débrancher, sortir le berceau par la ficelle. +0. Stem upside down: shot through the bottom hole (a few drops of glue to freeze it and stop it rattling), plug glued into its counterbore (epoxy), then stem glued onto the ground. +1. Board: solder a **2.54 mm 1×6 female header** on the **underside** of the board, in the "I2C" footprint; the "I2C" and "I2C_SPI" JSTs are not fitted (see *Wiring*). On the breakout, solder its male header so that the chip faces the disc once plugged in (breakout body towards the inside of the chassis, to check for your breakout model). +2. Charge module: solder W5 / W6 (red / black, 70 mm, OUT+ / OUT− soldered → J13 screw terminal). W3 / W4 are soldered in step 3: the JST-XH housing (~7.4 × 5.8 mm) barely fits through the window of the +Y wall (8 mm wide), so the bare end is passed through. +3. Deck placed in the chassis, board screwed on top (the female header goes through the deck window); chassis upside down, BME280 plugged in from the open underside of the chassis. Charge module in its slide; W5 / W6 (red / black, 70 mm, TP4056 soldered → screw terminal) screwed into J13; W3 / W4 (red / black, 100 mm, TP4056 soldered → JST-XH M): JST-XH M already crimped, chamber side; push the other, bare end from inside the chamber outwards through the +Y wall window (x = 22), then solder it to B+ / B− of the TP4056; the JST side stays in the chamber, above the cradle. Finally chassis onto the gill disc (4 screws from below). +4. Gill disc pushed into the dome up to the 3 corbels, 3 M3 × 10 self-tapping screws from below (heads between the gills). The 433 MHz module antenna goes straight up from J1 into the top of the gourd. +5. Cradle: contacts in the grooves, W1 red 40 mm / W2 black 110 mm soldered at the top of the tabs, out through the end notches (W2 runs along the cell in the top slot) → JST-XH F; cell clipped in; cradle into the stem from the top (spring at the bottom). +6. Cap held ~50 mm above the stem, plug the JST-XH pair, tuck it into the chamber just above the cradle (central hole of the disc), cap fitted on the tenon, radial screw. +7. Changing the cell: remove the radial screw, lift the cap (~50 mm): the JST-XH pair shows, unplug it, pull the cradle out by the string. -### Câblage (champignon) +### Wiring (mushroom) -Plan imprimable A4 : [`faisceau_champignon.pdf`](faisceau_champignon.pdf) (généré par `python3 harness.py`). +Printable A4 plan: [`faisceau_champignon.pdf`](faisceau_champignon.pdf) (generated by `python3 harness.py`). Board connectors: [`hardware/tinysensor/WIRING.md`](../hardware/tinysensor/WIRING.md). -**Capteur, sans fil.** Le breakout BME280 (6 broches VIN GND SCL SDA CSB SDO, ~15 × 11 mm) s'enfiche directement par sa barrette mâle dans une **barrette femelle 2,54 1×6** soudée sous la carte, dans l'empreinte « I2C » (JST-EH 6, pas 2,50 : décalage cumulé de 0,2 mm sur 6 positions, absorbé par le jeu des trous Ø0,95). +**Sensor, no wire.** The BME280 breakout (6 pins VIN GND SCL SDA CSB SDO, ~15 × 11 mm) plugs its male header straight into a **2.54 mm 1×6 female header** soldered under the board, in the "I2C" footprint (6-pin JST-EH, 2.50 pitch: 0.2 mm cumulated offset over 6 positions, absorbed by the play of the Ø0.95 holes). - Positions 1-4 = SENSOR_VCC, GND, SCL, SDA = VIN, GND, SCL, SDA. -- Position 5 non connectée = CSB : le tirage du breakout garde le mode I2C. -- Position 6 = GND = SDO : adresse **0x76**, celle qu'attend le firmware. -- Pull-ups I2C R2 / R3 4,7 kΩ sur la carte (côté carte) ; le breakout porte aussi les siennes. -- Hauteur : femelle 8,5 mm + plastique mâle 2,5 mm ; le breakout est au-dessus du berceau, à 42 mm du disque ; l'air monte par les fentes du disque, passe devant le capteur, sort par les fenêtres hautes du châssis et l'évent du sommet. +- Position 5 not connected = CSB: the breakout pull-up keeps the I2C mode. +- Position 6 = GND = SDO: address **0x76**, the one the firmware expects. +- I2C pull-ups R2 / R3 4.7 kΩ on the board (board end); the breakout carries its own too. +- Height: 8.5 mm female + 2.5 mm male plastic; the breakout is above the cradle, 42 mm from the disc; air rises through the disc slots, flows past the sensor, leaves through the high chassis windows and the top vent. -![BME280 enfiché sous la carte](rendu_champignon_capteur.png) +![BME280 plugged under the board](rendu_champignon_capteur.png) -**Accu** (fils 22–24 AWG : plus petit numéro = plus gros fil, W1-W4 portent le courant de charge de 1 A ; longueurs avec 15 à 25 mm de marge, à ajuster au montage) : +**Cell** (22–24 AWG wire: lower number = thicker wire, W1-W4 carry the 1 A charge current; lengths with 15 to 25 mm of slack, adjust on assembly): -| # | Couleur | Signal | De (extrémité) | Vers (extrémité) | Longueur | Section | +| # | Colour | Signal | From (end) | To (end) | Length | Gauge | |---|---|---|---|---|---|---| -| W1 | rouge | B+ | languette « + » du berceau (haut) — soudé | JST-XH 2p, broche 1 — JST-XH F (côté berceau) | 40 mm | 22–24 AWG | -| W2 | noir | B− | languette « − » du berceau (bas, longe l'accu dans la fente du dessus) — soudé | JST-XH 2p, broche 2 — JST-XH F | 110 mm | 22–24 AWG | -| W3 | rouge | B+ | JST-XH 2p broche 1 — JST-XH M (côté chapeau) | TP4056 B+ — soudé | 100 mm | 22–24 AWG | -| W4 | noir | B− | JST-XH 2p broche 2 — JST-XH M | TP4056 B− — soudé | 100 mm | 22–24 AWG | -| W5 | rouge | +BATT | TP4056 OUT+ — soudé | J13 « RAW IN » borne 1 (+BATT) — bornier à vis | 70 mm | 22–24 AWG | -| W6 | noir | GND | TP4056 OUT− — soudé | J13 borne 2 (GND) — bornier à vis | 70 mm | 22–24 AWG | +| W1 | red | B+ | cradle "+" tab (top) — soldered | JST-XH 2p, pin 1 — JST-XH F (cradle side) | 40 mm | 22–24 AWG | +| W2 | black | B− | cradle "−" tab (bottom, runs along the cell in the top slot) — soldered | JST-XH 2p, pin 2 — JST-XH F | 110 mm | 22–24 AWG | +| W3 | red | B+ | JST-XH 2p pin 1 — JST-XH M (cap side) | TP4056 B+ — soldered | 100 mm | 22–24 AWG | +| W4 | black | B− | JST-XH 2p pin 2 — JST-XH M | TP4056 B− — soldered | 100 mm | 22–24 AWG | +| W5 | red | +BATT | TP4056 OUT+ — soldered | J13 "RAW IN" terminal 1 (+BATT) — screw terminal | 70 mm | 22–24 AWG | +| W6 | black | GND | TP4056 OUT− — soldered | J13 terminal 2 (GND) — screw terminal | 70 mm | 22–24 AWG | -Cheminement : +Routing: -- Paire JST-XH (W1 rouge 40 mm / W2 noir 110 mm, languettes soudé → JST-XH F ; W3 / W4 rouge / noir 100 mm, JST-XH M → TP4056 soudé) dans la chambre, juste au-dessus du berceau : soulever le chapeau d'environ 50 mm suffit pour la débrancher par le trou central du disque. -- W3 / W4 (rouge / noir, 100 mm, JST-XH M → TP4056 soudé) sortent de la chambre du capteur par la fenêtre de la paroi +Y (x = 22) et rejoignent le TP4056 (module **avec protection** : pastilles B+ / B− / OUT+ / OUT−, micro-USB vers le bas). -- W5 / W6 (rouge / noir, 70 mm, soudé → bornier à vis) passent par-dessus la paroi +X du châssis jusqu'à J13, tout proche. -- Repérage : W3 / W4 et W5 / W6 sont deux paires rouge / noir soudées côte à côte sur le TP4056. **1 bague** de gaine thermo à chaque bout de W3 / W4, **2 bagues** sur W5 / W6 : une inversion B± / OUT± contourne la protection DW01. -- JST-XH pré-sertis : vérifier rouge = broche 1 des deux côtés (M et F) avant de souder. -- Antenne 433 MHz : droite, de J1 « RADIO » dans le haut de la gourde (sommet à l'aplomb de J1). +- JST-XH pair (W1 red 40 mm / W2 black 110 mm, tabs soldered → JST-XH F; W3 / W4 red / black 100 mm, JST-XH M → TP4056 soldered) in the chamber, just above the cradle: lifting the cap by about 50 mm is enough to unplug it through the central hole of the disc. +- W3 / W4 (red / black, 100 mm, JST-XH M → TP4056 soldered) leave the sensor chamber through the +Y wall window (x = 22) and reach the TP4056 (module **with protection**: B+ / B− / OUT+ / OUT− pads, micro-USB facing down). +- W5 / W6 (red / black, 70 mm, soldered → screw terminal) run over the +X wall of the chassis to J13, close by. +- Marking: W3 / W4 and W5 / W6 are two red / black pairs soldered side by side on the TP4056. **1 ring** of heat-shrink at each end of W3 / W4, **2 rings** on W5 / W6: swapping B± / OUT± bypasses the DW01 protection. +- Pre-crimped JST-XH: check red = pin 1 on both sides (M and F) before soldering. +- 433 MHz antenna: straight, from J1 "RADIO" into the top of the gourd (top right above J1). -### Points ouverts +### Open points -- Berceau (1er tirage) : entrait tout juste dans le pied et le fil « − » ne passait pas (encoche coupée à la pince). Corrigé pour le prochain tirage : jeu `cr_fit` 0,8, rainures des butées plus profondes (`bt_sink`), languettes vers le haut, encoche du fil à chaque bout (`wire_notch`). -- Cotes à mesurer : butées (épaisseur de tôle `bt_t`, ressort comprimé `spring_c`), position de la prise du module de charge (`usb_cut`), breakout BME (`bme_w/d`, `bme_edge`, hauteur de la barrette femelle `fh_h`). -- Pas de lame d'air entre le dôme et la carte : le capteur est sous le plancher, ventilé par en dessous ; échauffement au soleil à vérifier par mesure. -- Stabilité : réglée par le lest, à vérifier une fois imprimé. Estimation (coque PETG pleine, appui r ~38 mm) : basculement vers ~15° sans lest, ~21° avec 350 g, ~28° avec la chambre pleine (~1 kg, ~1,7 kg au total, centre de gravité à ~69 mm). Un remplissage partiel suffit : à figer à la colle, pied à l'endroit. +- Cradle (1st print): barely fitted into the stem and the "−" wire did not pass (notch cut with pliers). Fixed for the next print: `cr_fit` clearance 0.8, deeper contact grooves (`bt_sink`), tabs up, wire notch at each end (`wire_notch`). +- Dimensions to measure: contacts (sheet thickness `bt_t`, compressed spring `spring_c`), position of the charge module socket (`usb_cut`), BME breakout (`bme_w/d`, `bme_edge`, female header height `fh_h`). +- No air gap between the dome and the board: the sensor is under the deck, ventilated from below; solar heating to check by measurement. +- Stability: set by the ballast, to check once printed. Estimate (solid PETG shell, support radius ~38 mm): tips over at ~15° without ballast, ~21° with 350 g, ~28° with the chamber full (~1 kg, ~1.7 kg in total, centre of gravity at ~69 mm). Partial filling is enough: freeze it with glue, stem upright. -## Visserie +## Screws and fixings -| Usage | Qté | Référence | +| Use | Qty | Reference | |---|---|---| -| PCB → entretoises | 4 | M2.5 × 6 auto-taraudeuse | -| Couvercle | 6 | M3 × 8 auto-taraudeuse | -| Boîtier → mur | 2 | Ø4 tête fraisée + chevilles | -| Presse-étoupe | 1 | PG7 | -| Joint | ~45 cm | cordon Ø2 TPU/EPDM | -| Abri : assemblage | 3 | tige filetée M3 ~60 mm + 6 écrous | -| Abri → mur (option 1) | 2 | Ø4 tête fraisée | -| Clip → abri (option 2) | 2 | M4 × 8 tête fraisée + écrous | -| Boîtier → mât (pied) | 2 | M4 × 10 tête fraisée + écrous | -| Mât → semelle (pied) | 1 | M4 × 20 + écrou | -| Champignon : carte → châssis | 2 | M2.5 × 6 auto-taraudeuse | -| Champignon : châssis → disque | 4 | M3 × 8 auto-taraudeuse | -| Champignon : dôme → disque | 3 | M3 × 10 auto-taraudeuse (dans les consoles) | -| Champignon : chapeau → pied | 1 | M3 × 8 auto-taraudeuse (radiale) | - -## Connectique et électronique (champignon) - -| Usage | Qté | Référence | +| PCB → standoffs | 4 | M2.5 × 6 self-tapping | +| Lid | 6 | M3 × 8 self-tapping | +| Box → wall | 2 | Ø4 countersunk + wall plugs | +| Cable gland | 1 | PG7 | +| Seal | ~45 cm | Ø2 TPU/EPDM cord | +| Shield: assembly | 3 | M3 threaded rod ~60 mm + 6 nuts | +| Shield → wall (option 1) | 2 | Ø4 countersunk | +| Clip → shield (option 2) | 2 | M4 × 8 countersunk + nuts | +| Box → mast (stand) | 2 | M4 × 10 countersunk + nuts | +| Mast → foot (stand) | 1 | M4 × 20 + nut | +| Mushroom: board → chassis | 2 | M2.5 × 6 self-tapping | +| Mushroom: chassis → disc | 4 | M3 × 8 self-tapping | +| Mushroom: dome → disc | 3 | M3 × 10 self-tapping (in the corbels) | +| Mushroom: cap → stem | 1 | M3 × 8 self-tapping (radial) | +| Receiver: plate → shell | 4 | M3 × 6 cylinder-head machine screw | + +## Connectors and electronics (mushroom) + +| Use | Qty | Reference | |---|---|---| -| Capteur → carte (dessous du PCB, empreinte I2C) | 1 | barrette femelle 2,54 1×6 | -| Accu ↔ chapeau | 1 | paire JST-XH 2 broches mâle / femelle (fils pré-sertis ou à sertir) | -| Charge | 1 | module TP4056 micro-USB avec protection (DW01), 28,7 × 17,15 × 0,9 mm | -| Berceau | 1 jeu | butées à languette (ressort « − » + bossage « + », plaque 13 × 11 mm) | -| Faisceau W1-W6 | ~0,5 m | fil 22–24 AWG rouge et noir | -| Soudures | — | gaine thermo | -| Accu | 1 | 18650 nu, téton plat, 65 mm | -| Capteur | 1 | breakout BME280 6 broches (adresse 0x76) | - -## Alimentation (à trancher) - -- Support 18650 sur le PCB. -- Boîtiers AA/AAA disponibles : à intégrer dans la baie ou dans un compartiment séparé (dimensions à fournir). -- Solaire : le TP4056 (chargeur linéaire, sans MPPT) est peu adapté ; préférer un CN3065 ou un CN3791. +| Sensor → board (PCB underside, I2C footprint) | 1 | 2.54 mm 1×6 female header | +| Cell ↔ cap | 1 | 2-pin JST-XH male / female pair (pre-crimped or to crimp) | +| Charging | 1 | micro-USB TP4056 module with protection (DW01), 28.7 × 17.15 × 0.9 mm | +| Cradle | 1 set | tab contacts ("−" spring + "+" bump, 13 × 11 mm plate) | +| Harness W1-W6 | ~0.5 m | 22–24 AWG red and black wire | +| Joints | — | heat-shrink | +| Cell | 1 | bare 18650, flat top, 65 mm | +| Sensor | 1 | 6-pin BME280 breakout (address 0x76) | + +## Power (to decide) + +- 18650 holder on the PCB. +- Available AA/AAA holders: to fit in the bay or in a separate compartment (dimensions to provide). +- Solar: the TP4056 (linear charger, no MPPT) is a poor fit; prefer a CN3065 or a CN3791. From 41a60f5e4dfdf35a8446be000f4fe24479da8b1a Mon Sep 17 00:00:00 2001 From: Aurelien Labrosse Date: Sat, 14 Nov 2026 01:03:39 +0100 Subject: [PATCH 4/5] docs: technical debt audit in English Co-Authored-By: Claude Opus 5.5 --- AUDIT_TECH_DEBT.md | 576 ++++++++++++++++++++++----------------------- 1 file changed, 288 insertions(+), 288 deletions(-) diff --git a/AUDIT_TECH_DEBT.md b/AUDIT_TECH_DEBT.md index 705e507..b680569 100644 --- a/AUDIT_TECH_DEBT.md +++ b/AUDIT_TECH_DEBT.md @@ -1,121 +1,121 @@ -# Audit de dette technique — TinySensor +# Technical debt audit — TinySensor -**Projet** : [arcadien/tinySensor](https://github.com/arcadien/tinySensor) — firmware AVR bas-consommation pour capteurs d'environnement sans fil (ATtiny84a, 433 MHz, protocoles Oregon v3 / Lacrosse WS7000 / X10). +**Project**: [arcadien/tinySensor](https://github.com/arcadien/tinySensor) — low-power AVR firmware for wireless environment sensors (ATtiny84a, 433 MHz, Oregon v3 / Lacrosse WS7000 / X10 protocols). -**Auditeur** : Claude (modèle opus 4.7) — revue statique +**Auditor**: Claude (opus 4.7 model) — static review -**Date** : 19 avril 2026 — mise à jour 19 juin 2026 (voir §1bis) +**Date**: 19 April 2026 — updated 19 June 2026 (see §1bis) -**Périmètre** : ensemble du dépôt au commit `aa53a83` (branche `master`) — code firmware (`src/`, `lib/`, `include/`), tests (`test/`), projet Atmel Studio legacy (`TinySensor/`), outillage (`scripts/`, `shared/`), web builder (`web/`), hardware (`hardware/`), CI (`.github/workflows/`), Docker/devcontainer. +**Scope**: whole repository at commit `aa53a83` (branch `master`) — firmware code (`src/`, `lib/`, `include/`), tests (`test/`), legacy Atmel Studio project (`TinySensor/`), tooling (`scripts/`, `shared/`), web builder (`web/`), hardware (`hardware/`), CI (`.github/workflows/`), Docker/devcontainer. --- -## 1. Synthèse exécutive +## 1. Executive summary -TinySensor est un projet firmware embarqué mature sur le plan fonctionnel (protocoles radio testés, HAL propre, tests unitaires natifs avec Unity, portage PlatformIO). Le code "cœur" (`lib/oregon`, `lib/LacrosseWS7000`, `lib/x10`, HAL) est propre, bien abstrait, testé. La dette se concentre dans trois zones qui pèsent sur la maintenabilité et le risque opérationnel : +TinySensor is a functionally mature embedded firmware project (tested radio protocols, clean HAL, native unit tests with Unity, PlatformIO port). The "core" code (`lib/oregon`, `lib/LacrosseWS7000`, `lib/x10`, HAL) is clean, well abstracted and tested. The debt is concentrated in three areas that weigh on maintainability and operational risk: -D'abord, **un secret Coveralls est committé en clair** dans `.coveralls.yml` — il doit être révoqué immédiatement. Ensuite, **la CI ne compile aucun firmware AVR** (seul l'environnement natif est testé), donc un refactor peut casser silencieusement les builds de production `S_02`..`S_05`. Enfin, **un second arbre source** (`TinySensor/`, projet Atmel Studio legacy) duplique le code firmware et contient des binaires de build versionnés ; il est une source de divergence certaine et gonfle l'historique git. +First, **a Coveralls secret is committed in clear text** in `.coveralls.yml` — it must be revoked immediately. Second, **the CI builds no AVR firmware** (only the native environment is tested), so a refactoring can silently break the production builds `S_02`..`S_05`. Third, **a second source tree** (`TinySensor/`, legacy Atmel Studio project) duplicates the firmware code and holds versioned build binaries; it is a certain source of divergence and bloats the git history. -Au-delà de ces trois points, la configuration per-device vit sous forme de blocs presque identiques dans `platformio.ini` (7 environnements avec copier-coller), les dépendances tirent toutes vers des forks personnels non épinglés, l'image Docker `php:7.4-apache` est EOL (novembre 2022), et plusieurs scripts d'outillage contiennent des chemins absolus spécifiques à l'auteur. L'ensemble est traitable en ~3 sprints d'un développeur à temps partiel, avec les gains de risque concentrés sur les deux premières semaines. +Beyond these three points, the per-device configuration lives as near-identical blocks in `platformio.ini` (7 copy-pasted environments), the dependencies all pull from unpinned personal forks, the `php:7.4-apache` Docker image is EOL (November 2022), and several tooling scripts contain author-specific absolute paths. The whole is manageable in ~3 sprints of a part-time developer, with the risk gains concentrated in the first two weeks. -**Chiffres clés** : 3 779 lignes de code+tests sur ~20 fichiers. 8 suites de tests Unity natives. 1 workflow GitHub Actions (build + test native uniquement). 6 envs PlatformIO pour cibles physiques distinctes. 1 secret exposé. 2 dépendances critiques EOL. +**Key figures**: 3,779 lines of code+tests over ~20 files. 8 native Unity test suites. 1 GitHub Actions workflow (native build + test only). 6 PlatformIO envs for distinct physical targets. 1 exposed secret. 2 critical EOL dependencies. --- -## 1bis. Mises à jour de l'audit +## 1bis. Audit updates -Cette section suit l'évolution de la dette au fil des corrections apportées. Chaque entrée indique l'item résolu, la date, le(s) fichier(s) touché(s) et l'impact sur le score global. +This section tracks the debt as fixes land. Each entry gives the resolved item, the date, the file(s) touched and the impact on the overall score. -### 2026-04-19 — Workflow GitHub Actions refondé +### 2026-04-19 — GitHub Actions workflow rebuilt -Fichier modifié : `.github/workflows/main.yml` +File changed: `.github/workflows/main.yml` -Trois items du top-20 traités en une seule PR : +Three top-20 items handled in a single PR: -- **T1 (36) — CI compile maintenant toutes les cibles AVR**. Le workflow a été scindé en deux jobs : `test-native` (inchangé) et `build-avr` avec matrice `[S_02, S_03, S_04, S_05, robot, devmodule, SOLAR_TEST]`. `fail-fast: false` pour isoler les pannes par env. → **Résolu**. -- **D2 (30) — Actions et Python mis à jour**. `checkout@v4`, `cache@v4`, `setup-python@v5`, Python `3.12` (remplace 3.9 EOL). Clé de cache désormais invalidée sur `hashFiles('platformio.ini')`. → **Résolu**. -- **I4 (20) — Artefacts firmware publiés**. Chaque build AVR upload `firmware.hex` + `firmware.elf` via `actions/upload-artifact@v4`, rétention 30 j. Addresse la partie "artefact téléchargeable" d'I4 ; la partie "release automation sur tag" reste à faire. → **Partiellement résolu**. +- **T1 (36) — CI now builds every AVR target**. The workflow was split into two jobs: `test-native` (unchanged) and `build-avr` with the matrix `[S_02, S_03, S_04, S_05, robot, devmodule, SOLAR_TEST]`. `fail-fast: false` to isolate failures per env. → **Resolved**. +- **D2 (30) — Actions and Python updated**. `checkout@v4`, `cache@v4`, `setup-python@v5`, Python `3.12` (replaces EOL 3.9). Cache key now invalidated on `hashFiles('platformio.ini')`. → **Resolved**. +- **I4 (20) — Firmware artefacts published**. Each AVR build uploads `firmware.hex` + `firmware.elf` through `actions/upload-artifact@v4`, 30-day retention. Covers the "downloadable artefact" part of I4; the "release automation on tag" part remains. → **Partly resolved**. -Également : déclenchement étendu aux pull requests (auparavant : `on: push` seul), et cache configuré avec fallback via `restore-keys`. +Also: triggering extended to pull requests (previously `on: push` only), and cache configured with a `restore-keys` fallback. -Impact global : **score cumulé réduit de 86 points** (36 + 30 + 20). Top-20 recomposé au §4. +Overall impact: **cumulated score reduced by 86 points** (36 + 30 + 20). Top 20 rebuilt in §4. -### 2026-04-19 — `.gitignore` nettoyé et patrons d'artefacts ajoutés +### 2026-04-19 — `.gitignore` cleaned and artefact patterns added -Fichier modifié : `.gitignore` +File changed: `.gitignore` -- **I7 (20)** — Ligne 1 buggée `Debug/ Release/` (un seul pattern littéral avec espace) supprimée. Fichier réorganisé en blocs commentés. Ajout des patrons `*.o`, `*.d`, `*.hex`, `*.eep`, `*.lss`, `*.map`, `*.srec` (`*.elf` était déjà là). Préservation des fins de ligne CRLF. → **Résolu**. -- **I1 (25)** — Audit-revisité. `git ls-files TinySensor/Debug/` retourne vide : **aucun artefact n'était réellement tracké** (les `.o`/`.hex`/`.elf` sur disque proviennent d'une compilation locale Atmel Studio non committée). Le `git rm --cached` prévu par le plan est un no-op. Le nouveau `.gitignore` empêche toute régression : `git check-ignore` confirme que chaque fichier de `TinySensor/Debug/` match la règle `Debug/`. → **Résolu (par prévention ; l'audit initial surestimait l'item).** +- **I7 (20)** — Buggy line 1 `Debug/ Release/` (a single literal pattern with a space) removed. File reorganised into commented blocks. Added the patterns `*.o`, `*.d`, `*.hex`, `*.eep`, `*.lss`, `*.map`, `*.srec` (`*.elf` was already there). CRLF line endings preserved. → **Resolved**. +- **I1 (25)** — Audit revisited. `git ls-files TinySensor/Debug/` returns nothing: **no artefact was actually tracked** (the `.o`/`.hex`/`.elf` on disk come from an uncommitted local Atmel Studio build). The `git rm --cached` planned is a no-op. The new `.gitignore` prevents any regression: `git check-ignore` confirms every file of `TinySensor/Debug/` matches the `Debug/` rule. → **Resolved (by prevention; the initial audit overestimated the item).** -Impact cumulé 2026-04-19 (toutes entrées confondues) : **131 points** (86 + 25 + 20). +Cumulated impact 2026-04-19 (all entries): **131 points** (86 + 25 + 20). -### 2026-06-16 — Revue MAGI + nettoyage CI/outillage (PR #24) +### 2026-06-16 — MAGI review + CI/tooling clean-up (PR #24) -Branche : `fix/magi-review-findings` — revue indépendante par trois agents MAGI (Gaspard, Melchior, Balthazar) ayant voté REJECT 2/3 sur des bugs firmware, suivie d'une session de corrections. +Branch: `fix/magi-review-findings` — independent review by three MAGI agents (Gaspard, Melchior, Balthazar) who voted REJECT 2/3 on firmware bugs, followed by a fixing session. -**Nouveaux bugs firmware identifiés par MAGI (non présents dans l'audit initial) :** +**New firmware bugs found by MAGI (not in the initial audit):** -- **M1 — `PRR &= ~_BV(PRTIM1/PRADC)` logique inversée dans `UseLessPowerAsPossible()`** (`lib/hal/Attiny84aHal.cpp`). Dans le registre PRR, mettre un bit à 1 éteint le périphérique ; `&= ~` l'allumait (Timer1 et ADC restaient actifs en permanence). Corrigé en `|=`. Score rétroactif : (3+3)×5 = **30**. → **Résolu**. -- **M2 — `SetChannel()` sans validation de bornes** (`lib/oregon/Oregon_v3.cpp`). Channel > 3 décalait des bits dans les nibbles de l'ID capteur, corrompant silencieusement `message[2]`. Guard ajouté. Score rétroactif : (3+3)×5 = **30**. → **Résolu**. -- **M3 — `message[]` variable globale de fichier, non membre de classe** (`lib/oregon/Oregon_v3.cpp`). Singleton accidentel : deux instances `OregonV3` auraient partagé le même buffer. Déplacé en membre privé. Score rétroactif : (2+2)×4 = **16**. → **Résolu**. +- **M1 — `PRR &= ~_BV(PRTIM1/PRADC)` inverted logic in `UseLessPowerAsPossible()`** (`lib/hal/Attiny84aHal.cpp`). In the PRR register, setting a bit turns the peripheral off; `&= ~` turned it on (Timer1 and ADC stayed on all the time). Fixed to `|=`. Retroactive score: (3+3)×5 = **30**. → **Resolved**. +- **M2 — `SetChannel()` with no bounds check** (`lib/oregon/Oregon_v3.cpp`). Channel > 3 shifted bits into the sensor ID nibbles, silently corrupting `message[2]`. Guard added. Retroactive score: (3+3)×5 = **30**. → **Resolved**. +- **M3 — `message[]` file-scope global, not a class member** (`lib/oregon/Oregon_v3.cpp`). Accidental singleton: two `OregonV3` instances would have shared the same buffer. Moved to a private member. Retroactive score: (2+2)×4 = **16**. → **Resolved**. -**Items de l'audit résolus dans cette PR :** +**Audit items resolved in this PR:** -- **C4 (20) — Dead-code `if (int(count) != count)` dans `OregonV3::Sum`**. Héritage d'une implémentation internet où `count` était `float` ; avec `uint8_t`, la branche est toujours fausse. Supprimée. → **Résolu**. -- **C7 (18) + C10 (15) — Duplication massive `platformio.ini`**. Introduction de `[env:avr_base]` + `extends` ; suppression du `USE_BMP280=1` en double dans `env:S_03`. → **Résolu**. -- **D9 (25) — Pas de Dependabot**. Fichier `.github/dependabot.yml` ajouté : mises à jour hebdomadaires pour `github-actions`, mensuelles pour `docker` et `pip`. → **Résolu**. -- **I4 (12) — Release automation sur tag** (partie restante). Job `release` ajouté au workflow : se déclenche sur `v*`, télécharge tous les artefacts firmware et publie une GitHub Release avec notes auto-générées. → **Résolu (complètement)**. -- **D8/Doc7 (10) — Badge Travis mort**. Remplacé par le badge GitHub Actions dans `README.md`. → **Résolu**. +- **C4 (20) — Dead code `if (int(count) != count)` in `OregonV3::Sum`**. Inherited from an internet implementation where `count` was a `float`; with `uint8_t`, the branch is always false. Removed. → **Resolved**. +- **C7 (18) + C10 (15) — Massive `platformio.ini` duplication**. Introduced `[env:avr_base]` + `extends`; removed the duplicate `USE_BMP280=1` in `env:S_03`. → **Resolved**. +- **D9 (25) — No Dependabot**. Added `.github/dependabot.yml`: weekly updates for `github-actions`, monthly for `docker` and `pip`. → **Resolved**. +- **I4 (12) — Release automation on tag** (remaining part). `release` job added to the workflow: triggers on `v*`, downloads every firmware artefact and publishes a GitHub Release with auto-generated notes. → **Resolved (completely)**. +- **D8/Doc7 (10) — Dead Travis badge**. Replaced by the GitHub Actions badge in `README.md`. → **Resolved**. -Impact 2026-06-16 : **176 points** résolus (M1:30 + M2:30 + M3:16 + C4:20 + C7:18 + C10:15 + D9:25 + I4:12 + D8:10). +Impact 2026-06-16: **176 points** resolved (M1:30 + M2:30 + M3:16 + C4:20 + C7:18 + C10:15 + D9:25 + I4:12 + D8:10). -Impact cumulé toutes dates : **307 points** (131 + 176). +Cumulated impact, all dates: **307 points** (131 + 176). -### 2026-06-18 — Révocation du token Coveralls + suppression de `web/` + requalification A1 +### 2026-06-18 — Coveralls token revoked + `web/` removed + A1 requalified -- **C1 (35) — Token Coveralls révoqué et sorti du repo**. Le token `K1UMmvKnmugffPWriB48kR274KXcAuvuK` présent dans `.coveralls.yml` a été révoqué côté coveralls.io et remplacé par un secret GitHub Actions `COVERALLS_REPO_TOKEN`. → **Résolu**. -- **D1 (36) + A4 (15) + D6 (12) — `web/` supprimé**. Décision : le firmware builder web n'a jamais été finalisé ni déployé. Suppression des deux fichiers (`web/build.php`, `web/index.html`). Élimine d'un coup l'image Docker PHP 7.4 EOL, la vulnérabilité `shell_exec`, et jQuery 1.12.4. → **Résolu (D1, A4, D6)**. -- **A1 — Requalifié (risque accepté)**. `TinySensor/` n'est pas du code mort : il est conservé intentionnellement pour le workflow de debug/flash Atmel Studio (debugWIRE/JTAG), non remplaçable par PlatformIO. Score abaissé de 32 → 20, catégorie "risque de divergence à surveiller". Mitigations recommandées : commentaires croisés dans les deux `main.cpp` + step CI `diff` en warn-only. → **Requalifié, non supprimé**. -- **C2 (30) — Macro `OREGON_DELAY_US` morte supprimée**. La macro n'était jamais appelée (Oregon utilise `_hal->Delay512Us()/Delay1024Us()` directement) et référençait une méthode inexistante. Supprimée de `TestHal.h`. → **Résolu**. -- **C15 (15) — `override` ajouté sur les stubs de délai de `TestHal`**. `Delay400Us`, `Delay1024Us`, `Delay512Us` n'avaient pas `override` ; dérive de signature désormais détectable à la compilation. → **Résolu**. +- **C1 (35) — Coveralls token revoked and out of the repo**. The token `K1UMmvKnmugffPWriB48kR274KXcAuvuK` in `.coveralls.yml` was revoked on coveralls.io and replaced by a GitHub Actions secret `COVERALLS_REPO_TOKEN`. → **Resolved**. +- **D1 (36) + A4 (15) + D6 (12) — `web/` removed**. Decision: the web firmware builder was never finished or deployed. Both files removed (`web/build.php`, `web/index.html`). Removes at once the EOL PHP 7.4 Docker image, the `shell_exec` vulnerability, and jQuery 1.12.4. → **Resolved (D1, A4, D6)**. +- **A1 — Requalified (accepted risk)**. `TinySensor/` is not dead code: it is kept on purpose for the Atmel Studio debug/flash workflow (debugWIRE/JTAG), which PlatformIO cannot replace. Score lowered from 32 → 20, category "divergence risk to watch". Recommended mitigations: cross comments in both `main.cpp` + a warn-only CI `diff` step. → **Requalified, not removed**. +- **C2 (30) — Dead `OREGON_DELAY_US` macro removed**. The macro was never called (Oregon uses `_hal->Delay512Us()/Delay1024Us()` directly) and referred to a non-existent method. Removed from `TestHal.h`. → **Resolved**. +- **C15 (15) — `override` added to the `TestHal` delay stubs**. `Delay400Us`, `Delay1024Us`, `Delay512Us` lacked `override`; signature drift is now caught at compile time. → **Resolved**. -### 2026-10-25 — Suppression de l'arbre legacy Atmel Studio +### 2026-10-25 — Legacy Atmel Studio tree removed -- **A1 (20) + D7 (16) — `TinySensor/` supprimé**. Décision : le debug pas-à-pas passe par PlatformIO + AVaRICE + AVR Dragon (debugWIRE), documenté dans `docs/debugging.md` (à valider sur banc ; carte de debug dédiée à prévoir). Les copies tierces vendored disparaissent avec le dossier. → **Résolu**. +- **A1 (20) + D7 (16) — `TinySensor/` removed**. Decision: step debugging goes through PlatformIO + AVaRICE + AVR Dragon (debugWIRE), documented in `docs/debugging.md` (to validate on the bench; a dedicated debug board is needed). The vendored third-party copies go with the folder. → **Resolved**. -### 2026-10-25 — Abandon de Coveralls +### 2026-10-25 — Coveralls dropped -- **T2 (12) — Abandonné**. Décision : pas de rapport de couverture via Coveralls. `.coveralls.yml` était déjà retiré et aucune étape CI n'y faisait appel. Reste à supprimer manuellement le secret GitHub Actions `COVERALLS_REPO_TOKEN` (Settings → Secrets and variables → Actions). → **Clos sans action**. +- **T2 (12) — Dropped**. Decision: no coverage report through Coveralls. `.coveralls.yml` was already gone and no CI step used it. The GitHub Actions secret `COVERALLS_REPO_TOKEN` remains to delete by hand (Settings → Secrets and variables → Actions). → **Closed without action**. -- **C3 (20) — `Init()` orphelin supprimé**. Fonction libre `void Init(void)` en bas de `Attiny84aHal.cpp`, hors de la classe, jamais appelée — shadow silencieux de `Attiny84aHal::Init`. Supprimée. → **Résolu**. -- **C11 (15) — Branche morte `SetPressure` supprimée**. `if (pressure < 200) pressure = 200;` était immédiatement écrasé par le clamp à 850 ; supprimé. → **Résolu**. -- **D5 (20) — `avrdude.conf` sorti du repo**. Le fichier (534 KB) provient du toolchain PlatformIO (`~/.platformio/packages/tool-avrdude/`) ; supprimé avec `git rm`. → **Résolu**. -- **I3 — Requalifié**. Audit revisité : le ZIP KiCAD n'était pas tracké git (sur disque uniquement) ; `*.zip` déjà couvert par `.gitignore`. Non-issue, comme I1. → **Résolu (par prévention)**. -- **I2 (25) — Chemins absolus paramétrés**. `Makefile` : `PIO ?= $(HOME)/.platformio/penv/bin/pio`, `TARGET ?= S_03`, `cp` remplace `copy`. `scripts/flash_tinySensor` : `PIO_HOME` et `PROJECT_DIR` avec défauts portables (`${HOME}/.platformio` et `$(dirname $0)/..`). → **Résolu**. -- **D3 (24) + D4 (24) — Dépendances PlatformIO épinglées sur SHA**. Les 5 dépendances (BH1750, tiny-i2c, Unity, avr-ds18b20, SparkFun_BME280) épinglées sur leur commit HEAD courant dans `platformio.ini`. Reproductibilité garantie, risque supply-chain éliminé. → **Résolu**. -- **Doc1 (20) — `docs/environments.md` créé**. Convention `S_*` = numéro sur le PCB, guide de calibration `INTERNAL_1v1` par carte, table boards physiques + envs spéciaux (`robot`, `devmodule`, `SOLAR_TEST`), seuils batterie par chimie, procédure d'ajout d'une nouvelle carte. → **Résolu**. +- **C3 (20) — Orphan `Init()` removed**. Free function `void Init(void)` at the bottom of `Attiny84aHal.cpp`, outside the class, never called — a silent shadow of `Attiny84aHal::Init`. Removed. → **Resolved**. +- **C11 (15) — Dead `SetPressure` branch removed**. `if (pressure < 200) pressure = 200;` was immediately overwritten by the clamp at 850; removed. → **Resolved**. +- **D5 (20) — `avrdude.conf` out of the repo**. The file (534 KB) comes from the PlatformIO toolchain (`~/.platformio/packages/tool-avrdude/`); removed with `git rm`. → **Resolved**. +- **I3 — Requalified**. Audit revisited: the KiCAD ZIP was not tracked by git (on disk only); `*.zip` already covered by `.gitignore`. Non-issue, like I1. → **Resolved (by prevention)**. +- **I2 (25) — Absolute paths parameterised**. `Makefile`: `PIO ?= $(HOME)/.platformio/penv/bin/pio`, `TARGET ?= S_03`, `cp` replaces `copy`. `scripts/flash_tinySensor`: `PIO_HOME` and `PROJECT_DIR` with portable defaults (`${HOME}/.platformio` and `$(dirname $0)/..`). → **Resolved**. +- **D3 (24) + D4 (24) — PlatformIO dependencies pinned to a SHA**. The 5 dependencies (BH1750, tiny-i2c, Unity, avr-ds18b20, SparkFun_BME280) pinned to their current HEAD commit in `platformio.ini`. Reproducibility guaranteed, supply-chain risk removed. → **Resolved**. +- **Doc1 (20) — `docs/environments.md` created**. Convention `S_*` = number on the PCB, `INTERNAL_1v1` calibration guide per board, table of physical boards + special envs (`robot`, `devmodule`, `SOLAR_TEST`), battery thresholds per chemistry, procedure to add a new board. → **Resolved**. -Impact 2026-06-18 : **291 points** résolus (C1:35 + D1:36 + A4:15 + D6:12 + C2:30 + C15:15 + C3:20 + C11:15 + D5:20 + I2:25 + D3:24 + D4:24 + Doc1:20). +Impact 2026-06-18: **291 points** resolved (C1:35 + D1:36 + A4:15 + D6:12 + C2:30 + C15:15 + C3:20 + C11:15 + D5:20 + I2:25 + D3:24 + D4:24 + Doc1:20). -Impact cumulé toutes dates : **598 points** (307 + 291). +Cumulated impact, all dates: **598 points** (307 + 291). ### 2026-06-19 — RBD setup, requirements reverse-engineering, architectural violations V1-V4 -#### Infrastructure RBD +#### RBD infrastructure -Le projet est passé sous **Requirement-Based Development (RBD)** via le plugin Claude Code `arcadien/rbd@0.12.0`. Cela comprend : +The project moved to **Requirement-Based Development (RBD)** through the Claude Code plugin `arcadien/rbd@0.12.0`. This includes: -- `.rbd/config.yml` — ID format `FUNC/TECH/CONF/PLAT-DOMAIN-NNN`, framework Unity/C++, linter `clang-format` -- `.clang-format` — style Google, 2 espaces, 80 colonnes, `SortIncludes: false` -- Baseline clang-format appliquée sur tous les `.cpp`/`.h` existants (37 fichiers) -- `CLAUDE.md` mis à jour avec la section RBD (commit prefix obligatoire, `// @req {id}`, etc.) -- Pre-push hook `~/.claude/hooks/rbd-pre-push-check.sh` installé +- `.rbd/config.yml` — ID format `FUNC/TECH/CONF/PLAT-DOMAIN-NNN`, Unity/C++ framework, `clang-format` linter +- `.clang-format` — Google style, 2 spaces, 80 columns, `SortIncludes: false` +- clang-format baseline applied to every existing `.cpp`/`.h` (37 files) +- `CLAUDE.md` updated with the RBD section (mandatory commit prefix, `// @req {id}`, etc.) +- Pre-push hook `~/.claude/hooks/rbd-pre-push-check.sh` installed -#### Rétro-ingénierie des exigences +#### Requirements reverse-engineering -Analyse architecturale complète (`docs/analysis-2026-06-18.md`) suivie de 8 vagues de `requirement-analyst` agents en parallèle. **67 exigences validées et committées** : +Full architectural analysis (`docs/analysis-2026-06-18.md`) followed by 8 waves of parallel `requirement-analyst` agents. **67 requirements validated and committed**: -| Fichier | Domaines | Nb exigences | +| File | Domains | Requirements | |---------|----------|:------------:| | `requirements/functional.md` | FUNC-OREGON, LACROSSE, X10, BATTERY, SENSOR, ANALOG | 37 | | `requirements/technical.md` | TECH-HAL, FILTER, CONVERT, SERIAL, X10 | 14 | @@ -123,325 +123,325 @@ Analyse architecturale complète (`docs/analysis-2026-06-18.md`) suivie de 8 vag | `requirements/platform.md` | PLAT-POWER | 5 | | **Total** | | **67** | -`docs/architecture.md` enrichi d'une table de traçabilité complète (67 lignes Requirement → Component). +`docs/architecture.md` extended with a full traceability table (67 Requirement → Component rows). -#### Violations architecturales V1-V4 résolues +#### Architectural violations V1-V4 resolved -- **V1 — `x10rf` contournait le HAL pour tous les timings** : 5 fonctions statiques locales (`x10_preamble_high`, etc.) appelaient `_delay_us()` directement, violant `TECH-HAL-001`. Cycle RBD complet : TECH-X10-001 (5 nouvelles méthodes `Hal` : `DelayX10PreambleHigh` 8960 µs, `DelayX10PreambleLow` 4500 µs, `DelayX10BitShort` 560 µs, `DelayX10BitLong` 1120 µs, `DelayX10Gap` 40 000 µs) + TECH-X10-002 (x10rf remplacé — bloc `#if defined(AVR) _delay_us` supprimé, 5 appels `_hal->DelayX10*()` substitués). 14 nouveaux tests Unity. → **Résolu**. Note : **C6 (SoftSerial) reste ouvert** — même pattern, traitement séparé. +- **V1 — `x10rf` bypassed the HAL for every timing**: 5 local static functions (`x10_preamble_high`, etc.) called `_delay_us()` directly, violating `TECH-HAL-001`. Full RBD cycle: TECH-X10-001 (5 new `Hal` methods: `DelayX10PreambleHigh` 8960 µs, `DelayX10PreambleLow` 4500 µs, `DelayX10BitShort` 560 µs, `DelayX10BitLong` 1120 µs, `DelayX10Gap` 40,000 µs) + TECH-X10-002 (x10rf replaced — `#if defined(AVR) _delay_us` block removed, 5 `_hal->DelayX10*()` calls substituted). 14 new Unity tests. → **Resolved**. Note: **C6 (SoftSerial) stays open** — same pattern, handled separately. -- **V2 — `#include ` mort dans `Attiny84aHal.cpp`** : inclus ligne 2, jamais référencé — `AdcRead` utilisait des variables locales. Supprimé. → **Résolu**. +- **V2 — Dead `#include ` in `Attiny84aHal.cpp`**: included on line 2, never referenced — `AdcRead` used local variables. Removed. → **Resolved**. -- **V3 — `BH1750` absent de `docs/architecture.md`** : ajouté au diagramme Mermaid et à la table des responsabilités. → **Résolu**. +- **V3 — `BH1750` missing from `docs/architecture.md`**: added to the Mermaid diagram and the responsibility table. → **Resolved**. -- **V4 — `ds18b20` absent de `docs/architecture.md`** : idem. → **Résolu**. +- **V4 — `ds18b20` missing from `docs/architecture.md`**: same. → **Resolved**. #### Devcontainer -- **DevContainer** : Node.js 20 LTS + `@anthropic-ai/claude-code` ajoutés à `.devcontainer/Dockerfile` (installation système via NodeSource). Claude Code désormais disponible après `Rebuild Container`. +- **DevContainer**: Node.js 20 LTS + `@anthropic-ai/claude-code` added to `.devcontainer/Dockerfile` (system install through NodeSource). Claude Code now available after `Rebuild Container`. -#### C5 — Accès DS18B20 encapsulé dans la classe `Ds18b20` +#### C5 — DS18B20 access wrapped in the `Ds18b20` class -- **C5 (20) — `main.cpp` accédait directement aux registres AVR pour DS18B20** (`ds18b20convert(&PORTA, &DDRA, &PINA, …)`). Cycle RBD complet : TECH-SENSOR-001 validé, 4 tests Unity natifs (`test/test_ds18b20/`), classe `include/Ds18b20.h` créée suivant exactement le patron `BMx280` (sections `#if defined(AVR)` / `#else` stub, constructeur `Hal*`). `main.cpp` réduit à `ds18b20sensor.Convert()` + `ds18b20sensor.Read()` — aucune référence résiduelle à `PORTA`/`DDRA`/`PINA`. → **Résolu**. +- **C5 (20) — `main.cpp` accessed the AVR registers directly for the DS18B20** (`ds18b20convert(&PORTA, &DDRA, &PINA, …)`). Full RBD cycle: TECH-SENSOR-001 validated, 4 native Unity tests (`test/test_ds18b20/`), class `include/Ds18b20.h` created following exactly the `BMx280` pattern (`#if defined(AVR)` / `#else` stub sections, `Hal*` constructor). `main.cpp` reduced to `ds18b20sensor.Convert()` + `ds18b20sensor.Read()` — no remaining reference to `PORTA`/`DDRA`/`PINA`. → **Resolved**. -Impact 2026-06-19 : **20 points** résolus (C5:20). Score cumulé : **618 points** (598 + 20). +Impact 2026-06-19: **20 points** resolved (C5:20). Cumulated score: **618 points** (598 + 20). -### 2026-10-25 — SoftSerial via HAL, bit macros, saturation BCD, outillage format +### 2026-10-25 — SoftSerial through the HAL, bit macros, BCD saturation, format tooling -- **C6 (20) + T4 (16) — `SoftSerial` passe par le HAL**. `SerialGoHigh/Low` remplacés par `Hal::SerialWriteBit(level)`, qui fixe le niveau et tient une période de bit : un seul appel virtuel par bit comme avant, timing AVR conservé (à 1–2 cycles près, 40 octets de flash en moins). La période (`SERIAL_BIT_PERIOD_US`) vit dans `Attiny84aHal`. Nouvelle suite native `test_softSerial` (6 tests : trame, chaînes, nombres, bases, `println`). TECH-SERIAL-001, `docs/architecture.md` et CLAUDE.md mis à jour. → **Résolu**. -- **C9 (16) — `dec16ToHex` sature à `0x9999`** au-delà de 9999 au lieu de déborder. TECH-CONVERT-001 et test mis à jour. → **Résolu**. -- **C12 (12) — Macros de bits centralisées** dans `lib/utils/include/bitOps.h` ; copies supprimées (Oregon, Lacrosse, x10rf, `x10rf.h` public, test X10). Code AVR des encodeurs identique (taille `.text` inchangée). → **Résolu**. -- **C8 (16) — Constantes** : mortes supprimées (`HALF_DELAY_US`, `DELAY_US`, `MAX_ROLLING_CODE_VALUE`, `SYNC_NIBBLE`, timings X10 de `x10rf.cpp`), `PRESSURE_SCALING_VALUE` documentée, timings X10 nommés et documentés dans le HAL. → **Résolu**. -- **C14 (12) — TODO X10** remplacé par une note de périmètre (seul le type de paquet normal est utilisé). → **Résolu**. -- **I6 (12) — Outillage format** : `.editorconfig`, hook pre-commit clang-format **18.1.8**, job CI `lint` à la même version ; reformatage ponctuel (espaces uniquement) de `BMx280.h`, `Ds18b20.h`, `x10rf.cpp`. → **Résolu**. +- **C6 (20) + T4 (16) — `SoftSerial` goes through the HAL**. `SerialGoHigh/Low` replaced by `Hal::SerialWriteBit(level)`, which sets the level and holds one bit period: a single virtual call per bit as before, AVR timing preserved (within 1–2 cycles, 40 bytes of flash less). The period (`SERIAL_BIT_PERIOD_US`) lives in `Attiny84aHal`. New native suite `test_softSerial` (6 tests: frame, strings, numbers, bases, `println`). TECH-SERIAL-001, `docs/architecture.md` and CLAUDE.md updated. → **Resolved**. +- **C9 (16) — `dec16ToHex` saturates at `0x9999`** beyond 9999 instead of overflowing. TECH-CONVERT-001 and test updated. → **Resolved**. +- **C12 (12) — Bit macros centralised** in `lib/utils/include/bitOps.h`; copies removed (Oregon, Lacrosse, x10rf, public `x10rf.h`, X10 test). Encoder AVR code identical (`.text` size unchanged). → **Resolved**. +- **C8 (16) — Constants**: dead ones removed (`HALF_DELAY_US`, `DELAY_US`, `MAX_ROLLING_CODE_VALUE`, `SYNC_NIBBLE`, X10 timings of `x10rf.cpp`), `PRESSURE_SCALING_VALUE` documented, X10 timings named and documented in the HAL. → **Resolved**. +- **C14 (12) — X10 TODO** replaced by a scope note (only the normal packet type is used). → **Resolved**. +- **I6 (12) — Format tooling**: `.editorconfig`, clang-format **18.1.8** pre-commit hook, CI `lint` job at the same version; one-off reformatting (whitespace only) of `BMx280.h`, `Ds18b20.h`, `x10rf.cpp`. → **Resolved**. -Impact 2026-10-25 : **104 points** résolus (C6:20 + T4:16 + C9:16 + C8:16 + C12:12 + C14:12 + I6:12). Score cumulé : **722 points** (618 + 104), hors PR #35 (T2) et #36 (A1, D7). +Impact 2026-10-25: **104 points** resolved (C6:20 + T4:16 + C9:16 + C8:16 + C12:12 + C14:12 + I6:12). Cumulated score: **722 points** (618 + 104), excluding PR #35 (T2) and #36 (A1, D7). -### 2026-10-25 — Restructuration de `main.cpp` +### 2026-10-25 — `main.cpp` restructuring -- **A2 (15) — Interface commune des encodeurs d'environnement**. `include/EnvironmentEncoder.h` : alias `EnvironmentEncoder` choisi à la compilation (Oregon ou Lacrosse), `ConfigureEncoder()` et une surcharge `ApplyMeasurements()` par encodeur. Choix statique plutôt que virtuel (pas de vtable sur 8 Ko de flash). Les `#if USE_OREGON/USE_LACROSSE` répétés disparaissent de la boucle. → **Résolu**. -- **A3 (20) — BH1750 sorti de la boucle**. Wrapper `include/LightSensor.h` (modèle `BMx280`/`Ds18b20`, stub natif) ; capteur et encodeur Lacrosse lumière créés une seule fois. La luminosité reste émise en Lacrosse (Oregon n'a pas de trame lumière), 2 fois (`LIGHT_FRAME_REPEAT`). → **Résolu**. -- `include/Measurements.h` remplace les flottants sentinelles `NOT_SET = -1000` par des champs à drapeau de présence. -- Défauts corrigés au passage : `_delay_ms(150)` direct dans `main.cpp` (passe désormais par `Hal::Delay30ms`) ; `vccMv` utilisé par le bloc `ANALOG1_X10_ID` mais déclaré seulement dans le bloc batterie ; stub natif `BMx280::Shutdown()` manquant. -- Nouvelle suite `test_environment` (7 tests) : équivalence des ordres HAL entre `ApplyMeasurements()` et les appels directs d'avant. Structure documentée dans `docs/architecture.md` (*Main loop structure*). +- **A2 (15) — Common interface for the environment encoders**. `include/EnvironmentEncoder.h`: `EnvironmentEncoder` alias chosen at build time (Oregon or Lacrosse), `ConfigureEncoder()` and one `ApplyMeasurements()` overload per encoder. Static rather than virtual choice (no vtable on 8 KB of flash). The repeated `#if USE_OREGON/USE_LACROSSE` disappear from the loop. → **Resolved**. +- **A3 (20) — BH1750 out of the loop**. Wrapper `include/LightSensor.h` (`BMx280`/`Ds18b20` model, native stub); sensor and Lacrosse light encoder created once. Luminosity still goes out as Lacrosse (Oregon has no light frame), twice (`LIGHT_FRAME_REPEAT`). → **Resolved**. +- `include/Measurements.h` replaces the `NOT_SET = -1000` sentinel floats with presence-flag fields. +- Defects fixed along the way: direct `_delay_ms(150)` in `main.cpp` (now goes through `Hal::Delay30ms`); `vccMv` used by the `ANALOG1_X10_ID` block but declared only in the battery block; missing native `BMx280::Shutdown()` stub. +- New suite `test_environment` (7 tests): equivalence of the HAL orders between `ApplyMeasurements()` and the former direct calls. Structure documented in `docs/architecture.md` (*Main loop structure*). -Impact 2026-10-25 (b) : **35 points** résolus (A2:15 + A3:20). Score cumulé : **757 points** (722 + 35). +Impact 2026-10-25 (b): **35 points** resolved (A2:15 + A3:20). Cumulated score: **757 points** (722 + 35). --- -## 2. Méthodologie +## 2. Methodology -Chaque item identifié est scoré selon la formule du framework tech-debt : +Each item is scored with the tech-debt framework formula: -**Priorité = (Impact + Risque) × (6 − Effort)** +**Priority = (Impact + Risk) × (6 − Effort)** -- **Impact** (1–5) : dans quelle mesure cela ralentit l'équipe ou dégrade la qualité ? -- **Risque** (1–5) : que se passe-t-il si on ne corrige pas ? -- **Effort** (1–5) : coût de la correction (1 = trivial, 5 = gros chantier). +- **Impact** (1–5): how much does it slow the team down or degrade quality? +- **Risk** (1–5): what happens if it is not fixed? +- **Effort** (1–5): cost of the fix (1 = trivial, 5 = big job). -Le plus haut score théorique est 50 ((5+5)×(6−1)). Je classe les items en quatre bandes : +The highest theoretical score is 50 ((5+5)×(6−1)). Items fall into four bands: -| Bande | Score | Traitement | +| Band | Score | Handling | |---|---|---| -| P0 — Critique | ≥ 30 | À traiter sous 1–2 semaines | -| P1 — Élevé | 20–29 | Sprint courant ou suivant | -| P2 — Moyen | 12–19 | Backlog qualité à dérouler sur un trimestre | -| P3 — Faible | < 12 | Opportuniste, quand on touche la zone | +| P0 — Critical | ≥ 30 | Within 1–2 weeks | +| P1 — High | 20–29 | Current or next sprint | +| P2 — Medium | 12–19 | Quality backlog over a quarter | +| P3 — Low | < 12 | Opportunistic, when touching the area | --- -## 3. Dette par catégorie +## 3. Debt by category -### 3.1 Dette de code +### 3.1 Code debt -| ID | Item | Localisation | Impact | Risque | Effort | Priorité | +| ID | Item | Location | Impact | Risk | Effort | Priority | |----|------|--------------|:------:|:------:|:------:|:--------:| -| ~~C1~~ | ~~Token Coveralls commité en clair (`repo_token: K1UMmvKnmugffPWriB48kR274KXcAuvuK`)~~ — ✅ **Résolu 2026-06-18** (token révoqué, déplacé en secret GitHub Actions `COVERALLS_REPO_TOKEN`) | `.coveralls.yml` | 2 | 5 | 1 | ~~35~~ | -| ~~C2~~ | ~~`TestHal` référence un `OREGON_DELAY_US(x) TestHal.Delay(x)` qui n'existe pas.~~ — ✅ **Résolu 2026-06-18** (macro jamais appelée — Oregon utilise `_hal->Delay512Us()/Delay1024Us()` directement ; macro morte supprimée) | `lib/hal/TestHal.h:74` | 3 | 3 | 1 | ~~30~~ | -| ~~C3~~ | ~~Fonction libre `void Init(void)` orpheline en bas de `Attiny84aHal.cpp`, hors de la classe, jamais appelée.~~ — ✅ **Résolu 2026-06-18** (fonction supprimée) | `lib/hal/Attiny84aHal.cpp:199-203` | 2 | 2 | 1 | ~~20~~ | -| ~~C4~~ | ~~Dead-code dans `OregonV3::Sum` : `if (int(count) != count)` est toujours faux pour un `uint8_t`.~~ — ✅ **Résolu 2026-06-16** (branche morte supprimée ; héritage d'une impl. internet avec `float count`) | `lib/oregon/Oregon_v3.cpp:82` | 2 | 2 | 1 | ~~20~~ | -| ~~C5~~ | ~~`main.cpp` fait des accès registres directs (`ds18b20convert(&PORTA, &DDRA, &PINA, (1 << 3), nullptr)`), contournant le HAL.~~ — ✅ **Résolu 2026-06-19** (classe `Ds18b20` créée dans `include/Ds18b20.h` — même patron que `BMx280` ; `main.cpp` appelle `ds18b20sensor.Convert()` / `ds18b20sensor.Read()`, plus aucune référence à `PORTA`/`DDRA`/`PINA` ; TECH-SENSOR-001 + 4 tests Unity) | `src/main.cpp:116,120` | 3 | 2 | 2 | ~~20~~ | -| ~~C6~~ | ~~`SoftSerial` utilise `_delay_us()` directement au lieu du HAL → impossible à tester en natif et rompt le contrat d'abstraction.~~ — ✅ **Résolu 2026-10-25** (`SerialWriteBit` HAL, plus de `_delay_us` dans SoftSerial) | `lib/softSerial/SoftSerial.cpp:3-6,16,24,32,36` | 3 | 2 | 2 | ~~20~~ | -| ~~C7~~ | ~~Config per-env avec duplication massive dans `platformio.ini` : S_02, S_03, S_04, S_05, robot, devmodule, SOLAR_TEST partagent 80 % de leurs `build_flags`.~~ — ✅ **Résolu 2026-06-16** (`[env:avr_base]` + `extends` ; `default_envs` pour exclure la base des builds) | `platformio.ini` | 4 | 2 | 3 | ~~18~~ | -| ~~C8~~ | ~~Magic numbers non documentés : `PRESSURE_SCALING_VALUE = 795`, `MAX_ROLLING_CODE_VALUE = 0xA5` (non utilisée), `SYNC_NIBBLE = 0x0A` (non utilisée), timings X10 (`PREAMBLE_LOW = 4500`, etc.) sans référence datasheet.~~ — ✅ **Résolu 2026-10-25** (constantes mortes supprimées, `PRESSURE_SCALING_VALUE` et timings X10 documentés) | `lib/oregon/Oregon_v3.cpp`, `lib/x10/x10rf.cpp` | 2 | 2 | 2 | ~~16~~ | -| ~~C9~~ | ~~`dec16ToHex` déborde silencieusement pour `input > 9999` (le test `conversionToolsTest.cpp` le prouve avec `10000 → 0xA000` marqué en commentaire "overflow"). Pas de `assert` ni de clamp.~~ — ✅ **Résolu 2026-10-25** (saturation à `0x9999`, TECH-CONVERT-001 mis à jour) | `lib/utils/src/conversionTools.cpp:5-22` | 2 | 2 | 2 | ~~16~~ | -| ~~C10~~ | ~~`USE_BMP280=1` déclaré **deux fois** dans `env:S_03` (lignes 88 et 90) — copier-coller résiduel.~~ — ✅ **Résolu 2026-06-16** (subsumed par C7 ; la refacto `extends` a éliminé le doublon) | `platformio.ini:88,90` | 2 | 1 | 1 | ~~15~~ | -| ~~C11~~ | ~~Branche morte dans `LacrosseWS7000::SetPressure` : `if (pressure < 200) pressure = 200;` est immédiatement écrasé par `if (pressure < 850) pressure = 850;`.~~ — ✅ **Résolu 2026-06-18** (branche morte supprimée) | `lib/LacrosseWS7000/LacrosseWS7000.cpp:24-25` | 1 | 2 | 1 | ~~15~~ | -| ~~C12~~ | ~~Macro `BITREAD` redéfinie dans 4 fichiers (`Oregon_v3.cpp`, `LacrosseWS7000.cpp`, `x10rf.cpp`, `x10rf.h`) avec des variantes (`bitRead`, `BITREAD`).~~ — ✅ **Résolu 2026-10-25** (`lib/utils/include/bitOps.h`) | (multiple) | 2 | 1 | 2 | ~~12~~ | -| C13 | HAL "leaky" : méthodes nommées `RadioGoHigh/Low`, `SerialGoHigh/Low` — pilotage sémantique d'IO, pas une vraie abstraction de bus radio. Un port sur un MCU sans GPIO configurable de la même façon obligera à retoucher le HAL. | `lib/hal/Hal.h` | 3 | 2 | 4 | **10** | -| ~~C14~~ | ~~TODO isolé : `// TODO: tests for other modes`~~ — ✅ **Résolu 2026-10-25** (TODO remplacé par une note de périmètre) | `test/test_x10/x10LibTest.cpp:130` | 2 | 2 | 3 | ~~12~~ | -| ~~C15~~ | ~~`TestHal::Delay400Us/Delay1024Us/Delay512Us` manquent `override`.~~ — ✅ **Résolu 2026-06-18** (`override` ajouté sur les trois méthodes) | `lib/hal/TestHal.h:41-44` | 2 | 1 | 1 | ~~15~~ | -| ~~M1~~ | ~~`PRR &= ~_BV(PRTIM1/PRADC)` logique inversée dans `UseLessPowerAsPossible()` : les périphériques restaient actifs (consommation inutile sur batterie).~~ — ✅ **Résolu 2026-06-16** (identifié par revue MAGI, corrigé en `\|=`) | `lib/hal/Attiny84aHal.cpp` | 3 | 3 | 1 | ~~30~~ | -| ~~M2~~ | ~~`OregonV3::SetChannel()` sans validation de bornes : channel > 3 corrompt silencieusement `message[2]` en écrasant les nibbles ID capteur.~~ — ✅ **Résolu 2026-06-16** (guard `if (channel < 1 \|\| channel > 3) return;` ajouté) | `lib/oregon/Oregon_v3.cpp` | 3 | 3 | 1 | ~~30~~ | -| ~~M3~~ | ~~`message[]` variable globale de fichier dans `Oregon_v3.cpp` — singleton accidentel : deux instances `OregonV3` auraient partagé le même buffer.~~ — ✅ **Résolu 2026-06-16** (déplacé en membre privé `uint8_t message[MESSAGE_SIZE_IN_BYTES]`) | `lib/oregon/Oregon_v3.cpp/.h` | 2 | 2 | 2 | ~~16~~ | - -### 3.2 Dette d'architecture - -| ID | Item | Impact | Risque | Effort | Priorité | +| ~~C1~~ | ~~Coveralls token committed in clear (`repo_token: K1UMmvKnmugffPWriB48kR274KXcAuvuK`)~~ — ✅ **Resolved 2026-06-18** (token revoked, moved to the GitHub Actions secret `COVERALLS_REPO_TOKEN`) | `.coveralls.yml` | 2 | 5 | 1 | ~~35~~ | +| ~~C2~~ | ~~`TestHal` refers to an `OREGON_DELAY_US(x) TestHal.Delay(x)` that does not exist.~~ — ✅ **Resolved 2026-06-18** (macro never called — Oregon uses `_hal->Delay512Us()/Delay1024Us()` directly; dead macro removed) | `lib/hal/TestHal.h:74` | 3 | 3 | 1 | ~~30~~ | +| ~~C3~~ | ~~Orphan free function `void Init(void)` at the bottom of `Attiny84aHal.cpp`, outside the class, never called.~~ — ✅ **Resolved 2026-06-18** (function removed) | `lib/hal/Attiny84aHal.cpp:199-203` | 2 | 2 | 1 | ~~20~~ | +| ~~C4~~ | ~~Dead code in `OregonV3::Sum`: `if (int(count) != count)` is always false for a `uint8_t`.~~ — ✅ **Resolved 2026-06-16** (dead branch removed; inherited from an internet impl. with `float count`) | `lib/oregon/Oregon_v3.cpp:82` | 2 | 2 | 1 | ~~20~~ | +| ~~C5~~ | ~~`main.cpp` accesses registers directly (`ds18b20convert(&PORTA, &DDRA, &PINA, (1 << 3), nullptr)`), bypassing the HAL.~~ — ✅ **Resolved 2026-06-19** (class `Ds18b20` created in `include/Ds18b20.h` — same pattern as `BMx280`; `main.cpp` calls `ds18b20sensor.Convert()` / `ds18b20sensor.Read()`, no more reference to `PORTA`/`DDRA`/`PINA`; TECH-SENSOR-001 + 4 Unity tests) | `src/main.cpp:116,120` | 3 | 2 | 2 | ~~20~~ | +| ~~C6~~ | ~~`SoftSerial` uses `_delay_us()` directly instead of the HAL → impossible to test natively and breaks the abstraction contract.~~ — ✅ **Resolved 2026-10-25** (`SerialWriteBit` HAL, no more `_delay_us` in SoftSerial) | `lib/softSerial/SoftSerial.cpp:3-6,16,24,32,36` | 3 | 2 | 2 | ~~20~~ | +| ~~C7~~ | ~~Per-env config with massive duplication in `platformio.ini`: S_02, S_03, S_04, S_05, robot, devmodule, SOLAR_TEST share 80 % of their `build_flags`.~~ — ✅ **Resolved 2026-06-16** (`[env:avr_base]` + `extends`; `default_envs` to keep the base out of the builds) | `platformio.ini` | 4 | 2 | 3 | ~~18~~ | +| ~~C8~~ | ~~Undocumented magic numbers: `PRESSURE_SCALING_VALUE = 795`, `MAX_ROLLING_CODE_VALUE = 0xA5` (unused), `SYNC_NIBBLE = 0x0A` (unused), X10 timings (`PREAMBLE_LOW = 4500`, etc.) with no datasheet reference.~~ — ✅ **Resolved 2026-10-25** (dead constants removed, `PRESSURE_SCALING_VALUE` and X10 timings documented) | `lib/oregon/Oregon_v3.cpp`, `lib/x10/x10rf.cpp` | 2 | 2 | 2 | ~~16~~ | +| ~~C9~~ | ~~`dec16ToHex` silently overflows for `input > 9999` (the test `conversionToolsTest.cpp` shows it with `10000 → 0xA000` commented "overflow"). No `assert` or clamp.~~ — ✅ **Resolved 2026-10-25** (saturates at `0x9999`, TECH-CONVERT-001 updated) | `lib/utils/src/conversionTools.cpp:5-22` | 2 | 2 | 2 | ~~16~~ | +| ~~C10~~ | ~~`USE_BMP280=1` declared **twice** in `env:S_03` (lines 88 and 90) — leftover copy-paste.~~ — ✅ **Resolved 2026-06-16** (subsumed by C7; the `extends` refactoring removed the duplicate) | `platformio.ini:88,90` | 2 | 1 | 1 | ~~15~~ | +| ~~C11~~ | ~~Dead branch in `LacrosseWS7000::SetPressure`: `if (pressure < 200) pressure = 200;` is immediately overwritten by `if (pressure < 850) pressure = 850;`.~~ — ✅ **Resolved 2026-06-18** (dead branch removed) | `lib/LacrosseWS7000/LacrosseWS7000.cpp:24-25` | 1 | 2 | 1 | ~~15~~ | +| ~~C12~~ | ~~`BITREAD` macro redefined in 4 files (`Oregon_v3.cpp`, `LacrosseWS7000.cpp`, `x10rf.cpp`, `x10rf.h`) with variants (`bitRead`, `BITREAD`).~~ — ✅ **Resolved 2026-10-25** (`lib/utils/include/bitOps.h`) | (multiple) | 2 | 1 | 2 | ~~12~~ | +| C13 | "Leaky" HAL: methods named `RadioGoHigh/Low`, `SerialGoHigh/Low` — semantic IO driving, not a real radio bus abstraction. A port to an MCU without similarly configurable GPIO will require touching the HAL. | `lib/hal/Hal.h` | 3 | 2 | 4 | **10** | +| ~~C14~~ | ~~Lone TODO: `// TODO: tests for other modes`~~ — ✅ **Resolved 2026-10-25** (TODO replaced by a scope note) | `test/test_x10/x10LibTest.cpp:130` | 2 | 2 | 3 | ~~12~~ | +| ~~C15~~ | ~~`TestHal::Delay400Us/Delay1024Us/Delay512Us` lack `override`.~~ — ✅ **Resolved 2026-06-18** (`override` added on the three methods) | `lib/hal/TestHal.h:41-44` | 2 | 1 | 1 | ~~15~~ | +| ~~M1~~ | ~~`PRR &= ~_BV(PRTIM1/PRADC)` inverted logic in `UseLessPowerAsPossible()`: the peripherals stayed on (wasted battery power).~~ — ✅ **Resolved 2026-06-16** (found by the MAGI review, fixed to `\|=`) | `lib/hal/Attiny84aHal.cpp` | 3 | 3 | 1 | ~~30~~ | +| ~~M2~~ | ~~`OregonV3::SetChannel()` with no bounds check: channel > 3 silently corrupts `message[2]` by overwriting the sensor ID nibbles.~~ — ✅ **Resolved 2026-06-16** (guard `if (channel < 1 \|\| channel > 3) return;` added) | `lib/oregon/Oregon_v3.cpp` | 3 | 3 | 1 | ~~30~~ | +| ~~M3~~ | ~~`message[]` file-scope global in `Oregon_v3.cpp` — accidental singleton: two `OregonV3` instances would have shared the same buffer.~~ — ✅ **Resolved 2026-06-16** (moved to the private member `uint8_t message[MESSAGE_SIZE_IN_BYTES]`) | `lib/oregon/Oregon_v3.cpp/.h` | 2 | 2 | 2 | ~~16~~ | + +### 3.2 Architecture debt + +| ID | Item | Impact | Risk | Effort | Priority | |----|------|:------:|:------:|:------:|:--------:| -| ~~A1~~ | ~~Arbre source dupliqué `TinySensor/` (projet Atmel Studio)~~ — ✅ **Résolu 2026-10-25** (dossier supprimé, debug migré vers PlatformIO + AVaRICE, cf. `docs/debugging.md`) | 2 | 3 | 2 | ~~20~~ | -| ~~A2~~ | ~~Pas d'interface commune `EnvironmentEncoder` : `main.cpp` dispatche Oregon vs Lacrosse avec des `#if defined(USE_OREGON)/USE_LACROSSE` répétés 4× dans la même fonction. Ajouter un 3e protocole coûtera un passage dans toute la `main()`.~~ — ✅ **Résolu 2026-10-25** (`EnvironmentEncoder` alias + `ApplyMeasurements()`, sans virtuel) | 3 | 2 | 3 | ~~15~~ | -| ~~A3~~ | ~~Le bloc BH1750 dans `main.cpp:155-168` instancie en ligne un `LacrosseWS7000 lightEncoder(&hal)` — logique capteur mélangée avec logique de transmission.~~ — ✅ **Résolu 2026-10-25** (`LightSensor` wrapper, encodeur lumière créé une fois) | 3 | 2 | 2 | ~~20~~ | -| ~~A4~~ | ~~Web firmware builder (`web/build.php`) : `shell_exec` sans sanitisation des inputs.~~ — ✅ **Résolu 2026-06-18** (décision : supprimer `web/` ; builder jamais finalisé, jamais utilisé en production — `web/build.php` + `web/index.html` supprimés) | 2 | 3 | 3 | ~~15~~ | -| A5 | Configuration 100 % build-time : chaque capteur physique = un env PIO + une recompilation. Pas de config runtime, donc aucune mise à jour terrain sans reflashage manuel. Architecture acceptable pour un hobby / production très petite série ; limitante si on veut scaler. | 3 | 2 | 4 | 10 | -| A6 | HAL ATmega328p absent malgré mention README. Branche `feat/328p_hal` existe mais non mergée — code parallèle qui va dériver. | 2 | 1 | 4 | 6 | +| ~~A1~~ | ~~Duplicated `TinySensor/` source tree (Atmel Studio project)~~ — ✅ **Resolved 2026-10-25** (folder removed, debugging moved to PlatformIO + AVaRICE, see `docs/debugging.md`) | 2 | 3 | 2 | ~~20~~ | +| ~~A2~~ | ~~No common `EnvironmentEncoder` interface: `main.cpp` dispatches Oregon vs Lacrosse with `#if defined(USE_OREGON)/USE_LACROSSE` repeated 4× in the same function. Adding a 3rd protocol will mean a pass through the whole `main()`.~~ — ✅ **Resolved 2026-10-25** (`EnvironmentEncoder` alias + `ApplyMeasurements()`, no virtual) | 3 | 2 | 3 | ~~15~~ | +| ~~A3~~ | ~~The BH1750 block in `main.cpp:155-168` instantiates a `LacrosseWS7000 lightEncoder(&hal)` inline — sensor logic mixed with transmission logic.~~ — ✅ **Resolved 2026-10-25** (`LightSensor` wrapper, light encoder created once) | 3 | 2 | 2 | ~~20~~ | +| ~~A4~~ | ~~Web firmware builder (`web/build.php`): `shell_exec` with no input sanitisation.~~ — ✅ **Resolved 2026-06-18** (decision: remove `web/`; builder never finished, never used in production — `web/build.php` + `web/index.html` removed) | 2 | 3 | 3 | ~~15~~ | +| A5 | 100 % build-time configuration: each physical sensor = one PIO env + one rebuild. No runtime config, so no field update without manual reflashing. Acceptable architecture for a hobby / very small production run; limiting if it has to scale. | 3 | 2 | 4 | 10 | +| A6 | ATmega328p HAL missing despite the README mention. Branch `feat/328p_hal` exists but is not merged — parallel code that will drift. | 2 | 1 | 4 | 6 | -### 3.3 Dette de tests +### 3.3 Test debt -| ID | Item | Impact | Risque | Effort | Priorité | +| ID | Item | Impact | Risk | Effort | Priority | |----|------|:------:|:------:|:------:|:--------:| -| ~~T1~~ | ~~**La CI ne compile aucune cible AVR**. Le workflow ne lance que `pio test -e native_debug`. Un refactor peut casser `S_02`..`SOLAR_TEST` sans alerte. Le projet "prod" n'est jamais vérifié.~~ — ✅ **Résolu 2026-04-19** (workflow à deux jobs, matrice AVR) | 5 | 4 | 2 | ~~36~~ | -| ~~T2~~ | ~~Pas de rapport de couverture actif. `.coveralls.yml` existe (et est compromis, cf. C1), mais aucune étape CI ne génère/upload la couverture.~~ — ⛔ **Abandonné 2026-10-25** (Coveralls retiré du projet) | 2 | 1 | 2 | ~~12~~ | -| T3 | Pas de test pour le wrapper `BMx280` (le fichier d'en-tête expose `GetTemperature/GetHumidity/GetPressure`, aucun test ne vérifie les passerelles). | 3 | 2 | 3 | 15 | -| ~~T4~~ | ~~Pas de test pour `SoftSerial` — blocage lié à C6 (dépendance à `_delay_us`).~~ — ✅ **Résolu 2026-10-25** (suite `test_softSerial`, 6 tests) | 2 | 2 | 2 | ~~16~~ | -| T5 | Pas de test HW-in-the-loop ni Simavr-based exécuté en CI, malgré la présence de `[env:SOLAR_TEST]` configuré pour simavr. Infrastructure prête mais non exploitée. | 3 | 3 | 4 | 12 | -| T6 | Les tests de timings Oregon/Lacrosse valident une *chaîne de caractères d'ordres* (`"LDH PLD"` = 1, `"HDL PHD"` = 0) via `TestHal.Orders`. C'est élégant mais ne vérifie pas les timings réels ni l'interop avec RTL_433/RFLink que le README cite comme référence. | 3 | 3 | 4 | 12 | -| T7 | `test_busses/bussesTest.cpp` ne teste qu'un booléen `I2CIsConfigured` après `Init()` — valeur de régression nulle. | 2 | 1 | 2 | 12 | +| ~~T1~~ | ~~**The CI builds no AVR target**. The workflow only runs `pio test -e native_debug`. A refactoring can break `S_02`..`SOLAR_TEST` without warning. The "prod" project is never checked.~~ — ✅ **Resolved 2026-04-19** (two-job workflow, AVR matrix) | 5 | 4 | 2 | ~~36~~ | +| ~~T2~~ | ~~No active coverage report. `.coveralls.yml` exists (and is compromised, see C1), but no CI step generates/uploads coverage.~~ — ⛔ **Dropped 2026-10-25** (Coveralls removed from the project) | 2 | 1 | 2 | ~~12~~ | +| T3 | No test for the `BMx280` wrapper (the header exposes `GetTemperature/GetHumidity/GetPressure`, no test checks the bridges). | 3 | 2 | 3 | 15 | +| ~~T4~~ | ~~No test for `SoftSerial` — blocked by C6 (dependency on `_delay_us`).~~ — ✅ **Resolved 2026-10-25** (`test_softSerial` suite, 6 tests) | 2 | 2 | 2 | ~~16~~ | +| T5 | No HW-in-the-loop or simavr-based test run in CI, although `[env:SOLAR_TEST]` is configured for simavr. Infrastructure ready but unused. | 3 | 3 | 4 | 12 | +| T6 | The Oregon/Lacrosse timing tests check a *string of orders* (`"LDH PLD"` = 1, `"HDL PHD"` = 0) through `TestHal.Orders`. Elegant, but it checks neither the real timings nor the interop with RTL_433/RFLink that the README cites as a reference. | 3 | 3 | 4 | 12 | +| T7 | `test_busses/bussesTest.cpp` only tests an `I2CIsConfigured` boolean after `Init()` — no regression value. | 2 | 1 | 2 | 12 | -### 3.4 Dette de dépendances +### 3.4 Dependency debt -| ID | Item | Impact | Risque | Effort | Priorité | +| ID | Item | Impact | Risk | Effort | Priority | |----|------|:------:|:------:|:------:|:--------:| -| ~~D1~~ | ~~**Image Docker `php:7.4-apache` EOL depuis novembre 2022.** Utilisée comme base du firmware builder web.~~ — ✅ **Résolu 2026-06-18** (supprimé avec `web/` — plus de Dockerfile PHP dans le repo) | 4 | 5 | 2 | ~~36~~ | -| ~~D2~~ | ~~GitHub Actions obsolètes : `actions/checkout@v3`, `actions/cache@v3`, `actions/setup-python@v4`. Python 3.9 utilisé — EOL octobre 2025 (nous sommes en avril 2026).~~ — ✅ **Résolu 2026-04-19** (v4/v4/v5, Python 3.12) | 3 | 3 | 1 | ~~30~~ | -| ~~D3~~ | ~~Dépendances PlatformIO tirées de forks personnels sans épinglage de commit.~~ — ✅ **Résolu 2026-06-18** (toutes les 5 dépendances épinglées sur leur SHA HEAD courant dans `platformio.ini`) | 4 | 4 | 3 | ~~24~~ | -| ~~D4~~ | ~~`bh1750` épinglé sur la branche `feat/attiny_support` au lieu d'un SHA.~~ — ✅ **Résolu 2026-06-18** (subsumed par D3 — épinglé sur `6d06b87`) | 3 | 3 | 2 | ~~24~~ | -| ~~D5~~ | ~~`avrdude.conf` (534 KB) commité dans le repo.~~ — ✅ **Résolu 2026-06-18** (`git rm avrdude.conf` ; vient du toolchain PlatformIO, pas de la source) | 2 | 2 | 1 | ~~20~~ | -| ~~D6~~ | ~~`web/build.php` charge jQuery 1.12.4 depuis CDN (sortie 2016, EOL).~~ — ✅ **Résolu 2026-06-18** (supprimé avec `web/`) | 1 | 2 | 2 | ~~12~~ | -| ~~D7~~ | ~~Copies tierces vendored dans `TinySensor/third_party/` (BMX, I2C, X10) figées, risque de divergence vs. upstream.~~ — ✅ **Résolu 2026-10-25** (supprimées avec `TinySensor/`) | 2 | 2 | 2 | ~~16~~ | -| ~~D8~~ | ~~Badge README pointe vers **Travis CI** (`api.travis-ci.org`) — service fermé aux OSS non payants depuis 2020. Badge cassé = signal négatif pour les contributeurs.~~ — ✅ **Résolu 2026-06-16** (badge GitHub Actions dans `README.md`) | 1 | 1 | 1 | ~~10~~ | -| ~~D9~~ | ~~Pas de configuration Dependabot/Renovate. Dérive des versions non notifiée.~~ — ✅ **Résolu 2026-06-16** (`.github/dependabot.yml` : hebdomadaire pour `github-actions`, mensuel pour `docker` + `pip`) | 2 | 3 | 1 | ~~25~~ | - -### 3.5 Dette de documentation - -| ID | Item | Impact | Risque | Effort | Priorité | +| ~~D1~~ | ~~**Docker image `php:7.4-apache` EOL since November 2022.** Used as the base of the web firmware builder.~~ — ✅ **Resolved 2026-06-18** (removed with `web/` — no more PHP Dockerfile in the repo) | 4 | 5 | 2 | ~~36~~ | +| ~~D2~~ | ~~Outdated GitHub Actions: `actions/checkout@v3`, `actions/cache@v3`, `actions/setup-python@v4`. Python 3.9 used — EOL October 2025 (we are in April 2026).~~ — ✅ **Resolved 2026-04-19** (v4/v4/v5, Python 3.12) | 3 | 3 | 1 | ~~30~~ | +| ~~D3~~ | ~~PlatformIO dependencies pulled from personal forks with no commit pinning.~~ — ✅ **Resolved 2026-06-18** (all 5 dependencies pinned to their current HEAD SHA in `platformio.ini`) | 4 | 4 | 3 | ~~24~~ | +| ~~D4~~ | ~~`bh1750` pinned to branch `feat/attiny_support` instead of a SHA.~~ — ✅ **Resolved 2026-06-18** (subsumed by D3 — pinned to `6d06b87`) | 3 | 3 | 2 | ~~24~~ | +| ~~D5~~ | ~~`avrdude.conf` (534 KB) committed in the repo.~~ — ✅ **Resolved 2026-06-18** (`git rm avrdude.conf`; it comes from the PlatformIO toolchain, not from the source) | 2 | 2 | 1 | ~~20~~ | +| ~~D6~~ | ~~`web/build.php` loads jQuery 1.12.4 from a CDN (released 2016, EOL).~~ — ✅ **Resolved 2026-06-18** (removed with `web/`) | 1 | 2 | 2 | ~~12~~ | +| ~~D7~~ | ~~Vendored third-party copies in `TinySensor/third_party/` (BMX, I2C, X10) frozen, divergence risk vs. upstream.~~ — ✅ **Resolved 2026-10-25** (removed with `TinySensor/`) | 2 | 2 | 2 | ~~16~~ | +| ~~D8~~ | ~~README badge points to **Travis CI** (`api.travis-ci.org`) — closed to non-paying OSS since 2020. Broken badge = negative signal for contributors.~~ — ✅ **Resolved 2026-06-16** (GitHub Actions badge in `README.md`) | 1 | 1 | 1 | ~~10~~ | +| ~~D9~~ | ~~No Dependabot/Renovate configuration. Version drift goes unnoticed.~~ — ✅ **Resolved 2026-06-16** (`.github/dependabot.yml`: weekly for `github-actions`, monthly for `docker` + `pip`) | 2 | 3 | 1 | ~~25~~ | + +### 3.5 Documentation debt + +| ID | Item | Impact | Risk | Effort | Priority | |----|------|:------:|:------:|:------:|:--------:| -| ~~Doc1~~ | ~~Sémantique des environnements `S_02`..`S_05` non documentée.~~ — ✅ **Résolu 2026-06-18** (`docs/environments.md` créé : convention de nommage PCB, guide de calibration `INTERNAL_1v1`, table des boards physiques et envs spéciaux, seuils batterie) | 3 | 2 | 2 | ~~20~~ | -| Doc2 | Pas de guide d'onboarding (stack requise, setup local, premier build). Le README est orienté utilisateur/hardware, pas contributeur. | 3 | 2 | 2 | **20** | -| Doc3 | Workflow de flashage non documenté : `scripts/flash_tinySensor` + `waitForUpload` supposent une organisation NAS spécifique (`/var/services/homes/aurelien/…`). | 2 | 2 | 2 | 16 | -| Doc4 | Spécifications Oregon et Lacrosse référencées en commentaires (*"A copy of the document should be provided along the implementation"*) mais les docs PDF ne sont pas au repo. Lien externe ou fichier embarqué requis. | 2 | 2 | 2 | 16 | -| Doc5 | Absent : `CONTRIBUTING.md`, `SECURITY.md` (surtout pertinent vu C1), `CODE_OF_CONDUCT.md`, `CHANGELOG.md`. | 2 | 1 | 2 | 12 | -| Doc6 | Hardware : pas de BOM consolidé, pas de guide d'assemblage dans `doc/` (seulement 3 images : `boardv2.png`, `boardv2_copper.png`, `schematic.png`). | 2 | 1 | 3 | 9 | -| ~~Doc7~~ | ~~Badge Travis cassé dans README (doublon avec D8, traité côté deps).~~ — ✅ **Résolu 2026-06-16** (badge remplacé par GitHub Actions dans `README.md`) | 1 | 1 | 1 | ~~10~~ | +| ~~Doc1~~ | ~~Semantics of the `S_02`..`S_05` environments undocumented.~~ — ✅ **Resolved 2026-06-18** (`docs/environments.md` created: PCB naming convention, `INTERNAL_1v1` calibration guide, table of physical boards and special envs, battery thresholds) | 3 | 2 | 2 | ~~20~~ | +| Doc2 | No onboarding guide (required stack, local setup, first build). The README targets users/hardware, not contributors. | 3 | 2 | 2 | **20** | +| Doc3 | Undocumented flashing workflow: `scripts/flash_tinySensor` + `waitForUpload` assume a specific NAS layout (`/var/services/homes/aurelien/…`). | 2 | 2 | 2 | 16 | +| Doc4 | Oregon and Lacrosse specifications referenced in comments (*"A copy of the document should be provided along the implementation"*) but the PDF docs are not in the repo. External link or embedded file needed. | 2 | 2 | 2 | 16 | +| Doc5 | Missing: `CONTRIBUTING.md`, `SECURITY.md` (especially relevant given C1), `CODE_OF_CONDUCT.md`, `CHANGELOG.md`. | 2 | 1 | 2 | 12 | +| Doc6 | Hardware: no consolidated BOM, no assembly guide in `doc/` (only 3 images: `boardv2.png`, `boardv2_copper.png`, `schematic.png`). Partly addressed 2026-11-14: `hardware/tinysensor/WIRING.md` (connectors, jumpers, module wiring). | 2 | 1 | 3 | 9 | +| ~~Doc7~~ | ~~Broken Travis badge in the README (duplicate of D8, handled on the deps side).~~ — ✅ **Resolved 2026-06-16** (badge replaced by GitHub Actions in `README.md`) | 1 | 1 | 1 | ~~10~~ | -### 3.6 Dette d'infrastructure +### 3.6 Infrastructure debt -| ID | Item | Impact | Risque | Effort | Priorité | +| ID | Item | Impact | Risk | Effort | Priority | |----|------|:------:|:------:|:------:|:--------:| -| ~~I1~~ | ~~**Artefacts de build commités** dans `TinySensor/Debug/` : `.o`, `.d`, `.elf`, `.hex`, `.lss`, `.map`, `.srec`.~~ — ✅ **Résolu 2026-04-19** (audit revisité : aucun artefact n'était réellement tracké ; `TinySensor/Debug/` désormais couvert par `.gitignore`). | 3 | 2 | 1 | ~~25~~ | -| ~~I2~~ | ~~Chemins absolus spécifiques à l'auteur dans `Makefile` et `scripts/flash_tinySensor`.~~ — ✅ **Résolu 2026-06-18** (`PIO ?= $(HOME)/.platformio/penv/bin/pio`, `TARGET ?= S_03` dans `Makefile` ; `PIO_HOME` et `PROJECT_DIR` avec défauts `${HOME}/.platformio` et `$(dirname $0)/..` dans le script) | 3 | 2 | 1 | ~~25~~ | -| ~~I3~~ | ~~Archive de backup KiCAD commitée (`tinySensorv1-2025-05-01_183922.zip`).~~ — ✅ **Résolu 2026-06-18** (audit revisité : le ZIP n'était pas tracké par git — sur disque uniquement ; `*.zip` déjà couvert par `.gitignore`) | 2 | 2 | 1 | ~~20~~ | -| ~~I4~~ | ~~La CI ne produit aucun artefact (firmware `.hex` téléchargeable).~~ — ✅ **Résolu 2026-06-16** (job `release` ajouté sur `v*` tags : télécharge tous les artefacts et publie une GitHub Release avec notes auto-générées — complète la résolution partielle du 2026-04-19) | 2 | 1 | 2 | ~~12~~ | -| I5 | CI single-runner, single-python, single-env. Pas de matrice pour tester différentes versions d'outillage. | 3 | 2 | 2 | **20** | -| ~~I6~~ | ~~Pas de pre-commit ni de `.editorconfig`. `clang-format` est installé dans le devcontainer mais non appliqué automatiquement (le commit `68cefb8 chore: clang-format` laisse penser à une passe manuelle ponctuelle).~~ — ✅ **Résolu 2026-10-25** (`.editorconfig`, pre-commit clang-format 18.1.8, job CI `lint`) | 2 | 1 | 2 | ~~12~~ | -| ~~I7~~ | ~~`.gitignore` minimaliste (12 lignes) : n'exclut pas `Debug/` (d'où I1), pas `*.o`, `*.d`, `*.hex` au root.~~ — ✅ **Résolu 2026-04-19** (ligne 1 buggée supprimée, patrons d'artefacts ajoutés, structure commentée). | 2 | 2 | 1 | ~~20~~ | -| I8 | Pas de release / tag git visible sur les 30 derniers commits. Versioning absent. | 2 | 1 | 2 | 12 | +| ~~I1~~ | ~~**Build artefacts committed** in `TinySensor/Debug/`: `.o`, `.d`, `.elf`, `.hex`, `.lss`, `.map`, `.srec`.~~ — ✅ **Resolved 2026-04-19** (audit revisited: no artefact was actually tracked; `TinySensor/Debug/` now covered by `.gitignore`). | 3 | 2 | 1 | ~~25~~ | +| ~~I2~~ | ~~Author-specific absolute paths in `Makefile` and `scripts/flash_tinySensor`.~~ — ✅ **Resolved 2026-06-18** (`PIO ?= $(HOME)/.platformio/penv/bin/pio`, `TARGET ?= S_03` in `Makefile`; `PIO_HOME` and `PROJECT_DIR` with defaults `${HOME}/.platformio` and `$(dirname $0)/..` in the script) | 3 | 2 | 1 | ~~25~~ | +| ~~I3~~ | ~~KiCAD backup archive committed (`tinySensorv1-2025-05-01_183922.zip`).~~ — ✅ **Resolved 2026-06-18** (audit revisited: the ZIP was not tracked by git — on disk only; `*.zip` already covered by `.gitignore`) | 2 | 2 | 1 | ~~20~~ | +| ~~I4~~ | ~~The CI produces no artefact (downloadable `.hex` firmware).~~ — ✅ **Resolved 2026-06-16** (`release` job added on `v*` tags: downloads every artefact and publishes a GitHub Release with auto-generated notes — completes the partial resolution of 2026-04-19) | 2 | 1 | 2 | ~~12~~ | +| I5 | Single-runner, single-python, single-env CI. No matrix to test different tooling versions. | 3 | 2 | 2 | **20** | +| ~~I6~~ | ~~No pre-commit or `.editorconfig`. `clang-format` is installed in the devcontainer but not applied automatically (commit `68cefb8 chore: clang-format` suggests a one-off manual pass).~~ — ✅ **Resolved 2026-10-25** (`.editorconfig`, clang-format 18.1.8 pre-commit, CI `lint` job) | 2 | 1 | 2 | ~~12~~ | +| ~~I7~~ | ~~Minimal `.gitignore` (12 lines): does not exclude `Debug/` (hence I1), nor `*.o`, `*.d`, `*.hex` at the root.~~ — ✅ **Resolved 2026-04-19** (buggy line 1 removed, artefact patterns added, commented structure). | 2 | 2 | 1 | ~~20~~ | +| I8 | No release / git tag visible over the last 30 commits. No versioning. | 2 | 1 | 2 | 12 | --- -## 4. Classement prioritaire (top 20) +## 4. Priority ranking (top 20) -> Les items barrés ont été traités depuis la publication initiale (cf. §1bis). Ils restent listés pour traçabilité de l'effort consenti. +> Struck-out items were handled after the initial publication (see §1bis). They stay listed to trace the effort spent. -| Rang | ID | Item | Cat. | Score | +| Rank | ID | Item | Cat. | Score | |:----:|----|------|:----:|:-----:| -| — | ~~T1~~ | ~~CI ne compile aucune cible AVR~~ ✅ | Test | ~~36~~ | -| — | ~~D1~~ | ~~Image Docker PHP 7.4 EOL~~ ✅ | Dep | ~~36~~ | -| — | ~~C1~~ | ~~Token Coveralls en clair dans le repo~~ ✅ | Code | ~~35~~ | -| — | ~~A1~~ | ~~Arbre source dupliqué `TinySensor/`~~ → requalifié, risque accepté (score 20, voir §3.2) | Archi | ~~32~~ | -| — | ~~D2~~ | ~~GitHub Actions obsolètes + Python 3.9 EOL~~ ✅ | Dep | ~~30~~ | -| — | ~~C2~~ | ~~Macro `OREGON_DELAY_US` réfère une méthode inexistante~~ ✅ | Code | ~~30~~ | -| — | ~~M1~~ | ~~PRR bit inversé dans `UseLessPowerAsPossible()`~~ ✅ | Code | ~~30~~ | -| — | ~~M2~~ | ~~`SetChannel()` sans validation de bornes~~ ✅ | Code | ~~30~~ | -| — | ~~D9~~ | ~~Pas de Dependabot/Renovate~~ ✅ | Dep | ~~25~~ | -| — | ~~I1~~ | ~~Artefacts de build commités (`Debug/`)~~ ✅ | Infra | ~~25~~ | -| — | ~~I2~~ | ~~Chemins absolus auteur dans Makefile/scripts~~ ✅ | Infra | ~~25~~ | -| — | ~~D3~~ | ~~Deps pointent vers forks personnels non épinglés~~ ✅ | Dep | ~~24~~ | -| — | ~~D4~~ | ~~`bh1750` épinglé sur une branche au lieu d'un tag/SHA~~ ✅ | Dep | ~~24~~ | -| — | ~~C3~~ | ~~`Init()` orphelin dans `Attiny84aHal.cpp`~~ ✅ | Code | ~~20~~ | -| — | ~~C4~~ | ~~Dead-code `if (int(count) != count)` dans `Sum`~~ ✅ | Code | ~~20~~ | -| 8 | C5 | `main.cpp` accède aux registres sans passer par le HAL | Code | 20 | -| 8 | C6 | `SoftSerial` utilise `_delay_us` directement (non testable) | Code | 20 | -| 8 | A3 | Logique BH1750 mélangée dans `main.cpp` | Archi | 20 | -| — | ~~D5~~ | ~~`avrdude.conf` (534 KB) commité~~ ✅ | Dep | ~~20~~ | -| — | ~~Doc1~~ | ~~Envs `S_0x` non documentés (tribal knowledge)~~ ✅ | Doc | ~~20~~ | -| 8 | Doc2 | Pas de guide d'onboarding | Doc | 20 | -| — | ~~I3~~ | ~~Backup ZIP KiCAD commité~~ → non tracké git ✅ | Infra | ~~20~~ | -| 8 | I5 | Matrice CI inexistante | Infra | 20 | -| — | ~~I7~~ | ~~`.gitignore` minimaliste~~ ✅ | Infra | ~~20~~ | -| — | ~~M3~~ | ~~`message[]` variable globale partagée entre instances~~ ✅ | Code | ~~16~~ | -| — | ~~C7~~ | ~~Duplication massive `platformio.ini`~~ ✅ | Code | ~~18~~ | -| — | ~~C10~~ | ~~`USE_BMP280=1` en double dans `env:S_03`~~ ✅ | Code | ~~15~~ | -| — | ~~I4~~ | ~~Pas de release automation sur tag~~ ✅ | Infra | ~~12~~ | -| — | ~~D8~~ | ~~Badge Travis mort~~ ✅ | Dep | ~~10~~ | +| — | ~~T1~~ | ~~CI builds no AVR target~~ ✅ | Test | ~~36~~ | +| — | ~~D1~~ | ~~EOL PHP 7.4 Docker image~~ ✅ | Dep | ~~36~~ | +| — | ~~C1~~ | ~~Coveralls token in clear in the repo~~ ✅ | Code | ~~35~~ | +| — | ~~A1~~ | ~~Duplicated `TinySensor/` source tree~~ → requalified, accepted risk (score 20, see §3.2) | Archi | ~~32~~ | +| — | ~~D2~~ | ~~Outdated GitHub Actions + EOL Python 3.9~~ ✅ | Dep | ~~30~~ | +| — | ~~C2~~ | ~~`OREGON_DELAY_US` macro refers to a non-existent method~~ ✅ | Code | ~~30~~ | +| — | ~~M1~~ | ~~Inverted PRR bit in `UseLessPowerAsPossible()`~~ ✅ | Code | ~~30~~ | +| — | ~~M2~~ | ~~`SetChannel()` with no bounds check~~ ✅ | Code | ~~30~~ | +| — | ~~D9~~ | ~~No Dependabot/Renovate~~ ✅ | Dep | ~~25~~ | +| — | ~~I1~~ | ~~Build artefacts committed (`Debug/`)~~ ✅ | Infra | ~~25~~ | +| — | ~~I2~~ | ~~Author absolute paths in Makefile/scripts~~ ✅ | Infra | ~~25~~ | +| — | ~~D3~~ | ~~Deps point to unpinned personal forks~~ ✅ | Dep | ~~24~~ | +| — | ~~D4~~ | ~~`bh1750` pinned to a branch instead of a tag/SHA~~ ✅ | Dep | ~~24~~ | +| — | ~~C3~~ | ~~Orphan `Init()` in `Attiny84aHal.cpp`~~ ✅ | Code | ~~20~~ | +| — | ~~C4~~ | ~~Dead code `if (int(count) != count)` in `Sum`~~ ✅ | Code | ~~20~~ | +| 8 | C5 | `main.cpp` accesses registers without going through the HAL | Code | 20 | +| 8 | C6 | `SoftSerial` uses `_delay_us` directly (untestable) | Code | 20 | +| 8 | A3 | BH1750 logic mixed into `main.cpp` | Archi | 20 | +| — | ~~D5~~ | ~~`avrdude.conf` (534 KB) committed~~ ✅ | Dep | ~~20~~ | +| — | ~~Doc1~~ | ~~`S_0x` envs undocumented (tribal knowledge)~~ ✅ | Doc | ~~20~~ | +| 8 | Doc2 | No onboarding guide | Doc | 20 | +| — | ~~I3~~ | ~~KiCAD backup ZIP committed~~ → not tracked by git ✅ | Infra | ~~20~~ | +| 8 | I5 | No CI matrix | Infra | 20 | +| — | ~~I7~~ | ~~Minimal `.gitignore`~~ ✅ | Infra | ~~20~~ | +| — | ~~M3~~ | ~~`message[]` global shared between instances~~ ✅ | Code | ~~16~~ | +| — | ~~C7~~ | ~~Massive `platformio.ini` duplication~~ ✅ | Code | ~~18~~ | +| — | ~~C10~~ | ~~`USE_BMP280=1` duplicated in `env:S_03`~~ ✅ | Code | ~~15~~ | +| — | ~~I4~~ | ~~No release automation on tag~~ ✅ | Infra | ~~12~~ | +| — | ~~D8~~ | ~~Dead Travis badge~~ ✅ | Dep | ~~10~~ | --- -## 5. Plan de remédiation phasé +## 5. Phased remediation plan -Plan dimensionné pour 1 développeur à ~20 % de son temps. Chaque phase s'intercale avec du travail feature ; aucune ne bloque les autres. +Plan sized for 1 developer at ~20 % of their time. Each phase interleaves with feature work; none blocks the others. -### Phase 0 — Sécurité immédiate (jour 0–1, < 2 h) +### Phase 0 — Immediate security (day 0–1, < 2 h) -Un seul item, non négociable : **révoquer le token Coveralls** (`C1`). Révoquer côté coveralls.io, supprimer le token du fichier, le déplacer dans un secret GitHub Actions `COVERALLS_REPO_TOKEN`, et adapter le workflow. Même si le repo est public et que l'expo a déjà eu lieu, la rotation est obligatoire. +A single, non-negotiable item: **revoke the Coveralls token** (`C1`). Revoke it on coveralls.io, remove the token from the file, move it to a GitHub Actions secret `COVERALLS_REPO_TOKEN`, and adapt the workflow. Even though the repo is public and the exposure already happened, rotation is mandatory. -### Phase 1 — Stabilisation CI & outillage (semaine 1–2, ~1 jour) — **partiellement faite 2026-04-19** +### Phase 1 — CI & tooling stabilisation (week 1–2, ~1 day) — **partly done 2026-04-19** -Objectif : ne plus pouvoir casser le firmware de prod sans le voir dans la CI, et remettre l'outillage au goût du jour. +Goal: no longer be able to break the production firmware without the CI showing it, and bring the tooling up to date. -Actions, par ordre de valeur : +Actions, by value: -1. ~~**T1 — Ajouter la compilation AVR à la CI**. Étendre `.github/workflows/main.yml` avec une matrice qui fait `pio run -e S_02 -e S_03 -e S_04 -e S_05 -e robot -e devmodule` en plus du `pio test -e native_debug`. Coût : 30 min.~~ ✅ **Fait 2026-04-19** (matrice sur 7 envs, `fail-fast: false`). -2. ~~**D2 — Mettre à jour les Actions** : `checkout@v4`, `cache@v4`, `setup-python@v5`, Python `3.12`. Coût : 15 min.~~ ✅ **Fait 2026-04-19**. -3. ~~**I1, I7 — Nettoyer les artefacts versionnés**. Ajouter à `.gitignore` : `Debug/`, `Release/`, `*.o`, `*.d`, `*.elf`, `*.hex` (root seulement), `*.lss`, `*.map`, `*.srec`, `TinySensor/Debug/`. Puis `git rm -r --cached TinySensor/Debug/` et commit.~~ ✅ **Fait 2026-04-19** (cf. §1bis ; `git rm --cached` s'est révélé no-op, aucun artefact n'était réellement tracké). -4. **I2 — Paramétrer les scripts**. Remplacer les chemins absolus du `Makefile` et de `scripts/flash_tinySensor` par des variables d'environnement (`PIO_HOME`, `PROJECT_DIR`). Documenter dans le README. Coût : 1 h. -5. **D9 — Activer Dependabot** (`.github/dependabot.yml`) pour `github-actions`, `docker`, `pip` (le python utilisé dans les scripts). Coût : 10 min. -6. **D8 + Doc7 — Retirer le badge Travis**, ajouter le badge GitHub Actions. Coût : 5 min. -7. **I4 (partie restante) — Release automation sur tag** : workflow `on: push: tags: ['v*']` qui rejoue la matrice et publie les `.hex` / `.elf` en GitHub Release. Coût : 1 h. *Déplacé ici depuis la phase 4 car le socle artefacts est désormais en place.* +1. ~~**T1 — Add the AVR build to the CI**. Extend `.github/workflows/main.yml` with a matrix running `pio run -e S_02 -e S_03 -e S_04 -e S_05 -e robot -e devmodule` alongside `pio test -e native_debug`. Cost: 30 min.~~ ✅ **Done 2026-04-19** (matrix over 7 envs, `fail-fast: false`). +2. ~~**D2 — Update the Actions**: `checkout@v4`, `cache@v4`, `setup-python@v5`, Python `3.12`. Cost: 15 min.~~ ✅ **Done 2026-04-19**. +3. ~~**I1, I7 — Clean the versioned artefacts**. Add to `.gitignore`: `Debug/`, `Release/`, `*.o`, `*.d`, `*.elf`, `*.hex` (root only), `*.lss`, `*.map`, `*.srec`, `TinySensor/Debug/`. Then `git rm -r --cached TinySensor/Debug/` and commit.~~ ✅ **Done 2026-04-19** (see §1bis; `git rm --cached` turned out a no-op, no artefact was actually tracked). +4. **I2 — Parameterise the scripts**. Replace the absolute paths of the `Makefile` and `scripts/flash_tinySensor` with environment variables (`PIO_HOME`, `PROJECT_DIR`). Document in the README. Cost: 1 h. +5. **D9 — Enable Dependabot** (`.github/dependabot.yml`) for `github-actions`, `docker`, `pip` (the Python used in the scripts). Cost: 10 min. +6. **D8 + Doc7 — Remove the Travis badge**, add the GitHub Actions badge. Cost: 5 min. +7. **I4 (remaining part) — Release automation on tag**: an `on: push: tags: ['v*']` workflow that replays the matrix and publishes the `.hex` / `.elf` as a GitHub Release. Cost: 1 h. *Moved here from phase 4 since the artefact base is now in place.* -Livrable de fin de phase : CI verte sur **toutes les cibles AVR + natif** (atteint), plus aucune modification ne peut casser silencieusement un firmware de production (atteint), artefacts téléchargeables par build (atteint), et releases taguées automatiques (à faire). +End-of-phase deliverable: green CI on **every AVR target + native** (reached), no change can silently break a production firmware any more (reached), downloadable artefacts per build (reached), and automatic tagged releases (to do). -### Phase 2 — Consolidation de l'arbre source (semaine 3–5, ~3 jours cumulés) +### Phase 2 — Source tree consolidation (week 3–5, ~3 days in total) -Objectif : une seule source de vérité pour chaque fichier, dépendances reproductibles. +Goal: a single source of truth for each file, reproducible dependencies. -1. **A1 — Décision sur `TinySensor/` (legacy Atmel Studio)**. Deux options : - - (a) Supprimer intégralement le dossier. C'est ce que je recommande : le projet PlatformIO couvre tout l'usage, Atmel Studio n'est plus maintenu (Microchip Studio 7.0 également en fin de support). Coût : 10 min + audit de ce qui serait perdu. - - (b) Le conserver en tant que branche ou submodule séparé, hors de `master`. - Action : ouvrir une PR "remove legacy Atmel Studio tree" avec une fenêtre de confirmation d'une semaine. -2. **D3, D4 — Épingler les dépendances PlatformIO**. Passer de `url.git#branch` à `url.git#` (ou mieux, créer des tags `v1.0.0` sur chaque fork et utiliser `#v1.0.0`). Coût : 1 h. -3. **D5 — Sortir `avrdude.conf`** du repo. La config vient du paquet `tool-avrdude` de PlatformIO ; documenter dans le README et ajouter au `.gitignore`. Coût : 15 min. -4. **I3 — Supprimer le backup ZIP KiCAD**. L'historique git suffit. Coût : 5 min. -5. **D1 — Migrer le Dockerfile** de `php:7.4-apache` vers `php:8.3-apache` (ou plus récent). Tester le build. Coût : 2 h + test. +1. **A1 — Decision on `TinySensor/` (legacy Atmel Studio)**. Two options: + - (a) Remove the folder entirely. This is the recommendation: the PlatformIO project covers every use, Atmel Studio is no longer maintained (Microchip Studio 7.0 is also at end of support). Cost: 10 min + an audit of what would be lost. + - (b) Keep it as a separate branch or submodule, outside `master`. + Action: open a "remove legacy Atmel Studio tree" PR with a one-week confirmation window. +2. **D3, D4 — Pin the PlatformIO dependencies**. Move from `url.git#branch` to `url.git#` (or better, create `v1.0.0` tags on each fork and use `#v1.0.0`). Cost: 1 h. +3. **D5 — Take `avrdude.conf` out** of the repo. The config comes from PlatformIO's `tool-avrdude` package; document it in the README and add it to `.gitignore`. Cost: 15 min. +4. **I3 — Delete the KiCAD backup ZIP**. The git history is enough. Cost: 5 min. +5. **D1 — Migrate the Dockerfile** from `php:7.4-apache` to `php:8.3-apache` (or newer). Test the build. Cost: 2 h + test. -Livrable : un seul arbre source, deps reproductibles, image Docker à jour. +Deliverable: a single source tree, reproducible deps, up-to-date Docker image. -### Phase 3 — Qualité du code firmware (semaine 6–9, ~3 jours cumulés) +### Phase 3 — Firmware code quality (week 6–9, ~3 days in total) -Objectif : éliminer la dette de code identifiée, renforcer les tests. +Goal: remove the identified code debt, strengthen the tests. -Quick wins (1 h chacun) : +Quick wins (1 h each): -1. **C2** — Corriger la macro `OREGON_DELAY_US` dans `TestHal.h`. -2. **C3** — Supprimer la fonction libre `Init()` orpheline. -3. **C4** — Supprimer le `if` mort dans `OregonV3::Sum`. -4. **C10** — Dédupliquer `USE_BMP280=1` dans `env:S_03`. -5. **C11** — Retirer le `if (pressure < 200)` mort. -6. **C15** — Ajouter `override` sur toutes les méthodes virtuelles de `TestHal`. +1. **C2** — Fix the `OREGON_DELAY_US` macro in `TestHal.h`. +2. **C3** — Remove the orphan free function `Init()`. +3. **C4** — Remove the dead `if` in `OregonV3::Sum`. +4. **C10** — Deduplicate `USE_BMP280=1` in `env:S_03`. +5. **C11** — Remove the dead `if (pressure < 200)`. +6. **C15** — Add `override` on every virtual method of `TestHal`. -Refactorings moyens (1 jour chacun) : +Medium refactorings (1 day each): -7. **C5 + A3** — Encapsuler les accès DS18B20 et BH1750 derrière des classes capteurs (`Ds18b20Sensor`, `Bh1750Sensor`) utilisant le HAL. Alignerait sur le pattern `BMx280`. -8. **C6 + T4** — Refactorer `SoftSerial` pour passer toutes les temporisations par le HAL (`hal->DelayBitPeriod()` ou similaire). Ajouter un test natif de `SoftSerial` qui vérifie la séquence de `SerialGoHigh/Low` pour un caractère connu. -9. **C7 — Dédupliquer `platformio.ini`** : utiliser `[env:base_avr]` héritée par `[env:S_02]`, etc. (feature native de PlatformIO via `extends = env:base_avr`). Coût : 2 h + test. -10. **C12** — Centraliser `BITREAD` dans `lib/utils/include/bitops.h` et supprimer les redéfinitions. -11. **T3** — Ajouter un test unitaire pour `BMx280` en mode non-AVR (vérifie que les stubs renvoient 0 sans crash). -12. **C8** — Documenter les magic numbers (commentaire avec référence datasheet + lien). +7. **C5 + A3** — Wrap the DS18B20 and BH1750 accesses behind sensor classes (`Ds18b20Sensor`, `Bh1750Sensor`) using the HAL. Would align with the `BMx280` pattern. +8. **C6 + T4** — Refactor `SoftSerial` so that every delay goes through the HAL (`hal->DelayBitPeriod()` or similar). Add a native `SoftSerial` test checking the `SerialGoHigh/Low` sequence for a known character. +9. **C7 — Deduplicate `platformio.ini`**: use an `[env:base_avr]` inherited by `[env:S_02]`, etc. (native PlatformIO feature through `extends = env:base_avr`). Cost: 2 h + test. +10. **C12** — Centralise `BITREAD` in `lib/utils/include/bitops.h` and remove the redefinitions. +11. **T3** — Add a unit test for `BMx280` in non-AVR mode (checks that the stubs return 0 without crashing). +12. **C8** — Document the magic numbers (comment with datasheet reference + link). -### Phase 4 — Architecture & documentation (trimestre Q2/Q3, au fil de l'eau) +### Phase 4 — Architecture & documentation (Q2/Q3, as it comes) -1. **A2** — Introduire une interface `IEnvironmentEncoder` partagée par `OregonV3` et `LacrosseWS7000`, injectée dans `main()`. Simplifie l'ajout de futurs protocoles. -2. **A4** — Statuer sur le sort de `web/` : (a) le finir (formulaire + sanitisation) ou (b) le supprimer et documenter un builder local. Je recommande (b) vu l'état. -3. **Doc1, Doc2** — Écrire `docs/environments.md` (quel env pour quelle carte physique) et `docs/CONTRIBUTING.md` (stack, premier build, architecture HAL/encoders). -4. **Doc5** — Créer `SECURITY.md` (processus de report) et `CHANGELOG.md` (tenir à jour sur les prochaines releases). -5. **I5 — Matrice d'outillage** : étendre la matrice existante à plusieurs Pythons (`3.11`, `3.12`, `3.13`) et/ou plusieurs versions de PlatformIO Core. (La partie release automation d'I4 a été remontée en Phase 1.) -6. **T5, T6 — Élever la barre des tests** : intégrer `simavr` dans la CI pour au moins un env (`SOLAR_TEST` est prêt), et ajouter un test d'intégration qui fait passer la trame Oregon au décodeur RTL_433 (binaire installable en CI) pour valider l'interop réelle. -7. **A5 — Évaluation** : runtime config via EEPROM ? Pertinent seulement si on vise plus de 5 unités en production. Ne pas faire avant d'en avoir le besoin. +1. **A2** — Introduce an `IEnvironmentEncoder` interface shared by `OregonV3` and `LacrosseWS7000`, injected into `main()`. Simplifies adding future protocols. +2. **A4** — Decide on `web/`: (a) finish it (form + sanitisation) or (b) remove it and document a local builder. (b) is recommended given its state. +3. **Doc1, Doc2** — Write `docs/environments.md` (which env for which physical board) and `docs/CONTRIBUTING.md` (stack, first build, HAL/encoder architecture). +4. **Doc5** — Create `SECURITY.md` (reporting process) and `CHANGELOG.md` (kept up to date over the next releases). +5. **I5 — Tooling matrix**: extend the existing matrix to several Pythons (`3.11`, `3.12`, `3.13`) and/or several PlatformIO Core versions. (The release automation part of I4 moved up to Phase 1.) +6. **T5, T6 — Raise the test bar**: integrate `simavr` in the CI for at least one env (`SOLAR_TEST` is ready), and add an integration test that feeds the Oregon frame to the RTL_433 decoder (binary installable in CI) to validate real interop. +7. **A5 — Assessment**: runtime config through EEPROM? Relevant only when aiming at more than 5 units in production. Do not do it before the need arises. --- -## 6. Ce qui va bien (à préserver) +## 6. What works well (to preserve) -Pour équilibrer, voici les forces du projet à ne pas casser lors des refactorings : +For balance, here are the project's strengths not to break during refactorings: -**Le HAL est bien pensé** (`Hal.h` → `Attiny84aHal` / `TestHal_`) : abstraction claire, méthode template (`ComputeVccMv`, `ConvertAnalogValueToMv`) dans la classe de base, substituable pour les tests. C'est le patron à généraliser. +**The HAL is well designed** (`Hal.h` → `Attiny84aHal` / `TestHal_`): clear abstraction, template methods (`ComputeVccMv`, `ConvertAnalogValueToMv`) in the base class, substitutable for tests. It is the pattern to generalise. -**Les encodeurs protocolaires sont testés et déterministes** : `OregonV3` et `LacrosseWS7000` sont vérifiés via une trace d'ordres HAL (`Orders.push_back('H')`), ce qui permet d'asserter la séquence de bits sans matériel. Approche élégante. +**The protocol encoders are tested and deterministic**: `OregonV3` and `LacrosseWS7000` are checked through a trace of HAL orders (`Orders.push_back('H')`), which lets the bit sequence be asserted without hardware. Elegant approach. -**Les commentaires inline** dans les encodeurs documentent le format de trame (structure des nibbles, bits de battery status, checksum) — précieux pour un domaine où les specs sont rares. +**The inline comments** in the encoders document the frame format (nibble layout, battery status bits, checksum) — precious in a field where specs are rare. -**Le `Dockerfile` devcontainer** est complet et autosuffisant pour onboarder un nouveau développeur — le point d'entrée technique est clair. +**The devcontainer `Dockerfile`** is complete and self-sufficient to onboard a new developer — the technical entry point is clear. -**Les commits** suivent une convention (`feat:`, `fix:`, `chore:`, `hardware:`, `optim:`) — base pour semver automatique quand les releases seront outillées (phase 4). +**The commits** follow a convention (`feat:`, `fix:`, `chore:`, `hardware:`, `optim:`) — a base for automatic semver once releases are tooled (phase 4). --- -## 7. Annexes +## 7. Appendices -### 7.1 Métriques +### 7.1 Metrics -| Métrique | Valeur | +| Metric | Value | |---|---| -| Lignes de code (lib + src + include) | 1 543 | -| Lignes de tests | 1 835 | -| Ratio tests/code | 1,19 | -| Fichiers `.cpp`/`.h` audités (hors legacy) | 19 | -| TODO/FIXME/HACK restants | 1 | -| Environnements PlatformIO | 8 (1 natif + 7 AVR) | -| Dépendances externes | 5 (toutes forks perso) | -| Workflows CI | 1 (2 jobs : native + matrice AVR 7 envs, maj 2026-04-19) | -| Secrets exposés | 0 (C1 révoqué) | +| Lines of code (lib + src + include) | 1,543 | +| Lines of tests | 1,835 | +| Test/code ratio | 1.19 | +| `.cpp`/`.h` files audited (excluding legacy) | 19 | +| Remaining TODO/FIXME/HACK | 1 | +| PlatformIO environments | 8 (1 native + 7 AVR) | +| External dependencies | 5 (all personal forks) | +| CI workflows | 1 (2 jobs: native + 7-env AVR matrix, updated 2026-04-19) | +| Exposed secrets | 0 (C1 revoked) | -### 7.2 Items non retenus (hors scope) +### 7.2 Items not retained (out of scope) -Les éléments suivants ont été vus mais ne justifient pas d'action immédiate : +The following were seen but do not justify immediate action: -- *Qualité du KiCAD* (hardware) : hors compétence d'une revue de code. À confier à un reviewer hardware. -- *Choix du langage* (C++ sur ATtiny) : adapté au contexte mémoire/ressources. Pas de dette. -- *Format `float` pour la température* (`Oregon_v3.cpp`) : acceptable vu la place disponible ; à reconsidérer seulement si la taille flash devient critique. +- *KiCAD quality* (hardware): beyond the reach of a code review. To hand to a hardware reviewer. +- *Language choice* (C++ on ATtiny): suited to the memory/resource context. No debt. +- *`float` format for the temperature* (`Oregon_v3.cpp`): acceptable given the available room; to reconsider only if flash size becomes critical. -### 7.3 Commandes utiles pour la remédiation +### 7.3 Useful commands for the remediation ```bash -# Phase 1 — vérifier que la CI couvre toutes les cibles AVR +# Phase 1 — check that the CI covers every AVR target pio run -e S_02 -e S_03 -e S_04 -e S_05 -e robot -e devmodule -e SOLAR_TEST -# Phase 2 — purger l'arbre legacy (après décision) +# Phase 2 — purge the legacy tree (after decision) git rm -r TinySensor/ git rm avrdude.conf git rm hardware/tinysensor/tinySensorv1-backups/ -# Phase 3 — dédupliquer platformio.ini avec extends +# Phase 3 — deduplicate platformio.ini with extends # [env:base_avr] # platform = atmelavr # board = attiny84 @@ -456,4 +456,4 @@ git rm hardware/tinysensor/tinySensorv1-backups/ --- -*Fin du rapport. Auteur : Claude. Date : 19 avril 2026. Ce rapport reflète l'état du dépôt au commit `aa53a83`. Les scores sont indicatifs et doivent être recalibrés selon la capacité équipe et les priorités produit.* +*End of report. Author: Claude. Date: 19 April 2026. This report reflects the repository state at commit `aa53a83`. Scores are indicative and must be recalibrated to team capacity and product priorities.* From 386af8bc68c9d7f6fbf3353be24fa3d0d4d68d8b Mon Sep 17 00:00:00 2001 From: Aurelien Labrosse Date: Sat, 14 Nov 2026 02:19:03 +0100 Subject: [PATCH 5/5] chore: OpenSCAD and Python comments in English Comments, docstrings, customizer section names, echo and assert messages of every openscad/ model and of update_3mf.py / harness.py / export.sh translated from French. Geometry unchanged: the CSG trees of all 39 part/usage combinations are byte-identical before and after. Kept in French: Orca plate and object names (identifiers in the 3MF projects), the printed texts of harness.py (WIRES, NOTES, labels of the bench PDF) and the KiCad generator strings in hardware/tinysensor/scripts (they are the v3.0 board and schematic texts). Co-Authored-By: Claude Opus 5.5 --- openscad/README.md | 2 +- openscad/assembly.scad | 20 +- openscad/euro2.scad | 2 +- openscad/export.sh | 6 +- openscad/harness.py | 39 +-- openscad/mushroom.scad | 524 +++++++++++++++++------------------ openscad/pcb.scad | 38 +-- openscad/receiver.scad | 252 ++++++++--------- openscad/sensor_shield.scad | 120 ++++---- openscad/stand.scad | 78 +++--- openscad/tinysensor_box.scad | 116 ++++---- openscad/update_3mf.py | 70 ++--- 12 files changed, 634 insertions(+), 633 deletions(-) diff --git a/openscad/README.md b/openscad/README.md index 65249af..b4ba22e 100644 --- a/openscad/README.md +++ b/openscad/README.md @@ -47,7 +47,7 @@ After changing a `.scad` (about 30 s, close the projects in Orca first): ./export.sh print/box.3mf # or just one ``` -`export.sh` runs `update_3mf.py`, which re-exports each part and replaces its mesh in the project: plates, positions, rotations and Orca settings are kept, the previous project stays as `print/.3mf.bak`. A missing project is recreated, with the printer and filament settings of `mushroom.3mf` (Ender 3). Parts may only be rotated about Z in Orca, otherwise the script refuses to update them; a part deleted in Orca is reported, not put back. Per-object settings set when missing: mushroom cap outer brim 10 mm, 10 % infill; mushroom stem 6 walls. The dot counts are the project's: the "Pois" echo of `mushroom.scad` gives the count to place if the seed or `dot_gap` change. +`export.sh` runs `update_3mf.py`, which re-exports each part and replaces its mesh in the project: plates, positions, rotations and Orca settings are kept, the previous project stays as `print/.3mf.bak`. A missing project is recreated, with the printer and filament settings of `mushroom.3mf` (Ender 3). Parts may only be rotated about Z in Orca, otherwise the script refuses to update them; a part deleted in Orca is reported, not put back. Per-object settings set when missing: mushroom cap outer brim 10 mm, 10 % infill; mushroom stem 6 walls. The dot counts are the project's: the "Dots" echo of `mushroom.scad` gives the count to place if the seed or `dot_gap` change. Exporting a single part outside a project (STL, another format): `openscad -D 'part="lid"' -o out/lid.3mf tinysensor_box.scad` (see each `.scad` header). diff --git a/openscad/assembly.scad b/openscad/assembly.scad index e5540d5..3b1d52d 100644 --- a/openscad/assembly.scad +++ b/openscad/assembly.scad @@ -1,7 +1,7 @@ -// Vue d'ensemble : boîtier version "ext" + abri de sonde clipsé dessous, -// au mur (mount = "wall") ou sur le pied de table (mount = "stand", stand.scad). -// Les paramètres ci-dessous doivent rester alignés sur les modèles. -// Rendu : openscad --preview --camera=0,0,0,-20,-25,0,0 --viewall --autocenter --imgsize=1000,1200 -o rendu.png assembly.scad +// Overview: "ext" box + radiation shield clipped underneath, +// on the wall (mount = "wall") or on the table stand (mount = "stand", stand.scad). +// The parameters below must stay aligned with the models. +// Render: openscad --preview --camera=0,0,0,-20,-25,0,0 --viewall --autocenter --imgsize=1000,1200 -o rendu.png assembly.scad // openscad --preview -D 'mount="stand"' --camera=0,0,0,-20,-35,0,0 --viewall --autocenter --imgsize=1000,1200 -o rendu_pied.png assembly.scad mount = "wall"; // [wall, stand] @@ -10,17 +10,17 @@ use use use use -// paramètres repris des deux fichiers +// parameters copied from both files cav_w = 95.98; reach = 22.4; base_h = 2.4 + 5 + 1.6 + 25 + 2; gap = 10; plate_t = 1.6; n = 4; stack_h = (n + 1) * gap + plate_t; wall_y = 40; clip_t = 4; clip_gap = 6; cx = cav_w / 2; oy = -reach - (stack_h + clip_gap); W = wall_y + clip_t; -// mur +// wall -// boîtier +// box color("DimGray") base(usage = "ext"); color("Gray") translate([0, 0, base_h]) lid(); -// abri (coords d'usage de l'abri -> coords boîtier) +// shield (shield use coordinates -> box coordinates) multmatrix([[1, 0, 0, cx], [0, 0, 1, oy], [0, -1, 0, W], [0, 0, 0, 1]]) { color("White") base_print(); for (i = [1 : n]) color("WhiteSmoke") translate([0, 0, i * gap + plate_t]) mirror([0, 0, 1]) plate_print(); @@ -32,7 +32,7 @@ if (mount == "stand") { color("Sienna") foot_use(); } -// pièce de 2 € pour l'échelle (euro2.scad) -// au mur : à plat contre le mur, à droite de l'abri ; sur pied : posée sur la table devant la semelle +// €2 coin for scale (euro2.scad) +// on the wall: flat against the wall, right of the shield; on the stand: lying on the table in front of the foot if (mount == "stand") translate([cx + 45, -114, 115]) rotate([-90, 0, 0]) euro2(); else translate([cav_w + 35, -60, 0]) euro2(); diff --git a/openscad/euro2.scad b/openscad/euro2.scad index 2002fc5..67f65f0 100644 --- a/openscad/euro2.scad +++ b/openscad/euro2.scad @@ -1,4 +1,4 @@ -// Pièce de 2 € (Ø25,75 × 2,2 mm, cœur Ø18,75), posée à plat sur Z = 0 : sert d'échelle dans les rendus. +// €2 coin (Ø25.75 × 2.2 mm, centre Ø18.75), lying flat on Z = 0: scale reference in the renders. module euro2() { color("Silver") difference() { cylinder(d = 25.75, h = 2.2, $fn = 96); translate([0, 0, -0.1]) cylinder(d = 18.75, h = 2.4, $fn = 96); } color("Gold") cylinder(d = 18.75, h = 2.2, $fn = 96); diff --git a/openscad/export.sh b/openscad/export.sh index 8157a67..c2f0cc0 100755 --- a/openscad/export.sh +++ b/openscad/export.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash -# Met à jour les projets Orca de print/ (un par modèle) depuis les .scad : plaques, positions et réglages -# gardés, projet créé s'il manque. Détail : update_3mf.py. -# Usage : ./export.sh [projet.3mf ...] (par défaut tous : box, shield, stand, mushroom, receiver) +# Updates the Orca projects of print/ (one per model) from the .scad files: plates, positions and settings +# kept, project created when missing. Details: update_3mf.py. +# Usage: ./export.sh [project.3mf ...] (default: all of them: box, shield, stand, mushroom, receiver) set -euo pipefail cd "$(dirname "$0")" python3 update_3mf.py "$@" diff --git a/openscad/harness.py b/openscad/harness.py index 437243a..f9ef6ac 100644 --- a/openscad/harness.py +++ b/openscad/harness.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 -"""Faisceau accu du champignon TinySensor -> faisceau_champignon.pdf (A4 portrait, 300 dpi). +"""TinySensor mushroom cell harness -> faisceau_champignon.pdf (A4 portrait, 300 dpi). -Usage : python3 harness.py [sortie.pdf] -Dépendance : Pillow seulement. Police : DejaVu Sans (fc-list | grep DejaVuSans). -Les données sont dans WIRES (une ligne par fil) : modifier là, puis relancer. +Usage: python3 harness.py [output.pdf] +Dependency: Pillow only. Font: DejaVu Sans (fc-list | grep DejaVuSans). +The data is in WIRES (one line per wire): edit it there, then rerun. +The printed texts (WIRES, NOTES, labels) are in French: the plan is printed for the bench. """ import sys from pathlib import Path @@ -12,9 +13,9 @@ HERE = Path(__file__).resolve().parent OUT = Path(sys.argv[1]) if len(sys.argv) > 1 else HERE / "faisceau_champignon.pdf" -# --- Données ----------------------------------------------------------------------------- -# id, couleur, signal, de (repère schéma, texte), extrémité de départ, vers (repère, texte), -# extrémité d'arrivée, longueur (mm), section +# --- Data -------------------------------------------------------------------------------- +# id, colour, signal, from (schematic mark, text), start end type, to (mark, text), +# arrival end type, length (mm), gauge WIRES = [ ("W1", "rouge", "B+", "bat+", "languette « + » du berceau (haut)", "soudé", "jstF1", "JST-XH 2p, broche 1", "JST-XH F (côté berceau)", 40, "22–24 AWG"), @@ -51,10 +52,10 @@ "Longueurs : calculées sur le modèle, chapeau soulevé de 50 mm, avec 15 à 25 mm de marge ; à ajuster au montage.", ] -# --- Mise en page ------------------------------------------------------------------------ +# --- Layout ------------------------------------------------------------------------------ DPI = 300 -W, H = 2480, 3508 # A4 à 300 dpi -M = 150 # marge +W, H = 2480, 3508 # A4 at 300 dpi +M = 150 # margin FONT_DIR = next((d for d in (Path("/usr/share/fonts/truetype/dejavu"), Path("C:/Windows/Fonts")) if (d / "DejaVuSans.ttf").exists()), Path("/usr/share/fonts/truetype/dejavu")) @@ -64,7 +65,7 @@ def font(size, bold=False): F_TITLE, F_H2, F_TXT, F_SMALL, F_BOLD = font(64, True), font(44, True), font(32), font(30), font(32, True) -# 30 px à 300 dpi = 7,2 pt : plus petite police utilisée +# 30 px at 300 dpi = 7.2 pt: smallest font used def wrap(d, text, f, width): @@ -105,8 +106,8 @@ def need(self, h): def diagram(doc): d = doc.d y0 = doc.y - P1, P2 = y0 + 360, y0 + 650 # lignes des fils + et − - # zones pied / chapeau + P1, P2 = y0 + 360, y0 + 650 # lines of the + and − wires + # stem / cap areas xs = 1080 d.rectangle([M, y0, xs - 20, y0 + 1000], outline=(120, 120, 120), width=3) d.rectangle([xs + 20, y0, W - M, y0 + 1000], outline=(120, 120, 120), width=3) @@ -114,7 +115,7 @@ def diagram(doc): d.text((xs + 40, y0 + 15), "CHAPEAU (au-dessus du disque à lamelles)", font=F_BOLD, fill=(90, 90, 90)) pts = {} - # berceau 18650 (vertical, + en haut) + # 18650 cradle (upright, + at the top) cx0, cx1, cy0, cy1 = 230, 390, P1 - 90, P2 + 150 d.rounded_rectangle([cx0, cy0, cx1, cy1], radius=30, outline="black", width=5, fill=(235, 235, 225)) d.text(((cx0 + cx1) / 2, (cy0 + cy1) / 2), "18650", font=F_BOLD, fill="black", anchor="mm") @@ -125,7 +126,7 @@ def diagram(doc): d.text(((cx0 + cx1) / 2, cy0 - 35), "(languettes)", font=F_SMALL, fill="black", anchor="mb") pts["bat+"], pts["bat-"] = (cx1 + 40, P1), (cx1 + 40, P2 + 60) - # paire JST-XH 2 broches : F (côté berceau) | M (côté chapeau) + # 2-pin JST-XH pair: F (cradle side) | M (cap side) jx = 880 d.rectangle([jx - 70, P1 - 50, jx, P2 + 50], outline="black", width=4, fill=(250, 250, 250)) d.rectangle([jx, P1 - 50, jx + 70, P2 + 50], outline="black", width=4, fill=(220, 220, 220)) @@ -150,7 +151,7 @@ def diagram(doc): d.text((x + (22 if anc == "lm" else -22), y), name, font=F_SMALL, fill="white", anchor=anc) pts[key] = (x, y) - # carte v2.2 et bornier J13 + # v2.2 board and J13 terminal block bx0, bx1 = 2020, W - M - 30 d.rectangle([bx0, P1 - 120, bx1, P2 + 160], outline="black", width=4, fill=(30, 110, 60)) d.text(((bx0 + bx1) / 2, P2 + 140), "carte v2.2", font=F_BOLD, fill="white", anchor="mb") @@ -164,14 +165,14 @@ def diagram(doc): d.text(((bx0 + bx1) / 2, P1 - 70), "J13 RAW IN", font=F_BOLD, fill="white", anchor="mb") d.text(((bx0 + bx1) / 2, P2 + 175), "J13 : bornier à vis 5,08", font=F_SMALL, fill="black", anchor="mt") - # fils + étiquettes + # wires + labels for wid, col, sig, a, _, a_end, b, _, b_end, length, awg in WIRES: (xa, ya), (xb, yb) = pts[a], pts[b] rgb = COLORS[col] if ya == yb: d.line([xa, ya, xb, yb], fill=rgb, width=10) xm, ym = (xa + xb) / 2, ya - else: # W2 : horizontal en bas, puis remonte à la broche + else: # W2: horizontal at the bottom, then up to the pin xk = xb - 60 d.line([(xa, ya), (xk, ya), (xk, yb), (xb, yb)], fill=rgb, width=10, joint="curve") xm, ym = (xa + xk) / 2, ya @@ -246,7 +247,7 @@ def main(): notes(doc) doc.pages[0].save(OUT, "PDF", resolution=DPI, save_all=True, append_images=doc.pages[1:], title="Faisceau accu — TinySensor champignon", - creationDate=None, modDate=None) # pas de date : sortie reproductible + creationDate=None, modDate=None) # no date: reproducible output print("%s : %d page(s)" % (OUT, len(doc.pages))) diff --git a/openscad/mushroom.scad b/openscad/mushroom.scad index ad27eb3..e559ca1 100644 --- a/openscad/mushroom.scad +++ b/openscad/mushroom.scad @@ -1,32 +1,32 @@ -// TinySensor « champignon » — boîtier décoratif (amanite), en plus du boîtier mural. -// Unités : mm. Matière : PETG. +// TinySensor "mushroom" — decorative enclosure (fly agaric), alongside the wall box. +// Units: mm. Material: PETG. // -// Le chapeau est un tout (dôme + lamelles) posé sur le pied avec une légère inclinaison ; -// toute l'électronique est dans le chapeau, le pied ne porte que l'accu. +// The cap is one piece (dome + gills) sitting on the stem with a slight tilt; +// all the electronics are in the cap, the stem only carries the cell. // -// cap : dôme en gourde (croquis), bord ondulé ; 3 consoles intérieures au-dessus du disque, vis M3 par en dessous (autotaraudées) -// gills : disque à lamelles = dessous du chapeau ; fentes de ventilation, prise USB, fourreau sur le pied -// chassis : cadre imprimé debout, vissé sur le disque : chambre du bas ventilée (BME280 enfiché sous la carte), -// plancher, carte v2.2 découpée, glissière du module de charge (prise vers le bas, entre deux lamelles) -// deck : plancher percé entre la chambre et la carte, fenêtre du connecteur I2C (barrette femelle traversante) -// stem : pied creux (berceau 18650 vertical), lobé, base bulbeuse, anneau en coupelle, tenon incliné en haut -// ground : disque de 1 mm sous le pied, avec des touffes d'herbe (stabilité + décor) -// cradle : berceau de l'accu, amovible, glissé dans le pied ; deux butées à languette (accu non soudé) -// plug : bouchon de la chambre à lest (sous le berceau), collé dans le lamage du fond -// dot : pois blancs rapportés, collés dans les logements du dôme +// cap : gourd-shaped dome (sketch), wavy edge; 3 inner corbels above the disc, M3 screws from below (self-tapping) +// gills : gill disc = underside of the cap; vent slots, USB socket, sleeve on the stem +// chassis : frame printed upright, screwed onto the disc: ventilated lower chamber (BME280 plugged under the board), +// deck, cut-down v2.2 board, charge module slide (socket facing down, between two gills) +// deck : perforated deck between the chamber and the board, I2C connector window (female header through it) +// stem : hollow stem (upright 18650 cradle), lobed, bulbous base, cup ring, tilted tenon at the top +// ground : 1 mm disc under the stem, with grass tufts (stability + decoration) +// cradle : removable cell cradle, slid into the stem; two tab contacts (cell not soldered) +// plug : plug of the ballast chamber (under the cradle), glued into the counterbore of the bottom +// dot : white dots added afterwards, glued into the dome recesses // -// Export : python3 update_3mf.py met à jour les pièces dans le projet Orca print/mushroom.3mf (plaques gardées). -// À la main (toutes les pièces dans leur sens d'impression) : -// openscad -D 'part="cap"' -o mushroom_cap.3mf mushroom.scad # bord sur le plateau, sans support -// openscad -D 'part="gills"' -o mushroom_gills.3mf mushroom.scad # lamelles vers le haut +// Export: python3 update_3mf.py updates the parts in the Orca project print/mushroom.3mf (plates kept). +// By hand (every part in its print orientation): +// openscad -D 'part="cap"' -o mushroom_cap.3mf mushroom.scad # edge on the bed, no support +// openscad -D 'part="gills"' -o mushroom_gills.3mf mushroom.scad # gills up // openscad -D 'part="chassis"' -o mushroom_chassis.3mf mushroom.scad // openscad -D 'part="deck"' -o mushroom_deck.3mf mushroom.scad // openscad -D 'part="stem"' -o mushroom_stem.3mf mushroom.scad -// openscad -D 'part="ground"' -o mushroom_ground.3mf mushroom.scad # sol + herbe, en vert ; pied collé dessus -// openscad -D 'part="cradle"' -o mushroom_cradle.3mf mushroom.scad # à plat, méplat sur le plateau +// openscad -D 'part="ground"' -o mushroom_ground.3mf mushroom.scad # ground + grass, in green; stem glued on it +// openscad -D 'part="cradle"' -o mushroom_cradle.3mf mushroom.scad # flat, flat face on the bed // openscad -D 'part="plug"' -o mushroom_plug.3mf mushroom.scad -// openscad -D 'part="dot"' -D 'dot_d=8' -o mushroom_dot8.3mf mushroom.scad # x n, voir l'écho -// Rendus : +// openscad -D 'part="dot"' -D 'dot_d=8' -o mushroom_dot8.3mf mushroom.scad # x n, see the echo +// Renders: // openscad --preview -D 'part="assembly"' --camera=10,0,145,75,0,25,950 --imgsize=1000,1300 -o rendu_champignon.png mushroom.scad // openscad --preview -D 'part="assembly"' --camera=0,0,90,112,0,200,720 --imgsize=1000,1000 -o rendu_champignon_dessous.png mushroom.scad // openscad --preview -D 'part="assembly"' --camera=0,0,30,65,0,40,260 --imgsize=1000,800 -o rendu_champignon_pied.png mushroom.scad @@ -36,152 +36,152 @@ // openscad --preview -D 'part="sensor"' --camera=-18,5,166,96,0,0,150 --imgsize=1200,800 -o rendu_champignon_capteur.png mushroom.scad // openscad --preview -D 'part="console"' --camera=56,0,5,75,0,-20,95 --imgsize=1200,800 -o rendu_champignon_console.png mushroom.scad # ~4 min // -// Repères : monde = Z vers le haut, axe du pied en X = Y = 0, Z = 0 sur la table. -// Chapeau (cap_xf) : Z = 0 sous le disque à lamelles, axe incliné de cap_tilt vers +X. -// Dôme : Z = 0 à son bord, cap_skirt sous le disque. Le haut de la gourde est centré sur J1. +// Frames: world = Z up, stem axis at X = Y = 0, Z = 0 on the table. +// Cap (cap_xf): Z = 0 under the gill disc, axis tilted by cap_tilt towards +X. +// Dome: Z = 0 at its edge, cap_skirt below the disc. The top of the gourd is centred on J1. use use -/* [Rendu] */ +/* [Render] */ part = "assembly"; // [assembly, exploded, cradle_view, section, sensor, console, cap, gills, chassis, deck, stem, ground, cradle, plug, dot] -dot_d = 8; // diamètre du pois exporté (part = "dot") -explode = 40; // vue éclatée : écart entre étages (part = "exploded") -cap_color = "#c0392b"; // rendu uniquement (tue-mouches ; panthère : "#8b5a2b") +dot_d = 8; // diameter of the exported dot (part = "dot") +explode = 40; // exploded view: gap between levels (part = "exploded") +cap_color = "#c0392b"; // render only (fly agaric; panther cap: "#8b5a2b") -/* [Carte v2.2 découpée — voir pcb.scad] */ +/* [Cut-down v2.2 board — see pcb.scad] */ crop_w = 60.02; crop_h = 38.974; pcb_t = 1.6; -pcb_holes = [[26.1243, -15.6770], [-25.9902, -15.5130]]; // MH5, MH1 du haut, carte tournée de 180° -k_crop_c = [177.01008, 67.51316]; // centre de la découpe (KiCad) : local = [x - cx, cy - y] -i2c_k = [199.35016, 77.11032]; // empreinte « I2C » (JST-EH 6, pas 2.50), broche 1 ; broches vers +X une fois la carte tournée -radio_k = [192.99936, 56.86044]; // J1 « RADIO » (1x3, module 433 MHz debout) : le sommet du dôme est à son aplomb - -/* [Chapeau] */ -cap_tilt = 6; // inclinaison du chapeau sur le pied -cap_R = 72; // rayon maxi moyen du chapeau (bas de la gourde) -cap_H = 190; // hauteur du dôme au-dessus de son bord -// méridien de la gourde, d'après le croquis : [rayon / cap_R, hauteur / cap_H], du bord au sommet (hauteurs croissantes) +pcb_holes = [[26.1243, -15.6770], [-25.9902, -15.5130]]; // MH5, top MH1, board turned 180° +k_crop_c = [177.01008, 67.51316]; // centre of the cut (KiCad): local = [x - cx, cy - y] +i2c_k = [199.35016, 77.11032]; // "I2C" footprint (JST-EH 6, 2.50 pitch), pin 1; pins towards +X once the board is turned +radio_k = [192.99936, 56.86044]; // J1 "RADIO" (1x3, 433 MHz module standing): the dome top is right above it + +/* [Cap] */ +cap_tilt = 6; // tilt of the cap on the stem +cap_R = 72; // average max radius of the cap (bottom of the gourd) +cap_H = 190; // dome height above its edge +// gourd meridian, after the sketch: [radius / cap_R, height / cap_H], from edge to top (increasing heights) cap_shape = [[0.86, 0], [0.97, 0.08], [1, 0.16], [0.93, 0.29], [0.68, 0.41], [0.45, 0.5], [0.38, 0.58], [0.43, 0.71], [0.5, 0.84], [0.45, 0.94], [0.25, 0.99], [0, 1]]; -cap_lean = 8; // le haut penche vers -X (contre cap_tilt), en pivotant là où le chapeau se resserre -cap_neck = [0.4, 0.62]; // col : déport (calculé pour le sommet sur J1) et penchement s'installent entre ces deux hauteurs (/ cap_H) +cap_lean = 8; // the top leans towards -X (against cap_tilt), pivoting where the cap narrows +cap_neck = [0.4, 0.62]; // neck: offset (computed to put the top over J1) and lean set in between these two heights (/ cap_H) cap_t = 2.4; -vent_d = 5; // évent : du haut du plafond vers le flanc, en descendant (la pluie n'entre pas, l'air chaud sort) -vent_at = [90, 72]; // sortie de l'évent sur la surface : (phi, t) de cap_pt, côté arrière -vent_clear = 14; // pas de pois à moins de cette distance de la sortie -win_h = 22; // fenêtres du châssis, en haut de la chambre : l'air passe devant le BME280 -rim_w = 6; // largeur du bord sur le plateau : le bas de la paroi s'épaissit vers l'intérieur (accroche, chapeau très haut) -cap_skirt = 4; // le bord du dôme descend sous le disque (cache sa tranche ; le disque doit y passer) -disc_fit = 0.4; // jeu au rayon entre le disque et le plus étroit de la jupe du dôme -corbel_w = 12; // consoles du dôme, au-dessus du disque : largeur (autour de la vis) -corbel_h = 10; // hauteur de la face droite, chanfrein à 45° au-dessus jusqu'à la paroi -corbel_in = 5; // débord de la console au-delà de l'axe de la vis, vers l'intérieur -corbel_pilot = 8; // profondeur du trou pilote (M3x10 autotaraudeuse : 3 de disque + 7 en prise) -tab_base = [90, 210, 330]; // vis du dôme (consoles) : chacune glisse (±40°) vers le lobe le plus proche +vent_d = 5; // vent: from the top of the ceiling to the flank, going down (rain stays out, warm air leaves) +vent_at = [90, 72]; // vent exit on the surface: (phi, t) of cap_pt, rear side +vent_clear = 14; // no dot closer than this to the exit +win_h = 22; // chassis windows, at the top of the chamber: air flows past the BME280 +rim_w = 6; // edge width on the bed: the bottom of the wall thickens inwards (adhesion, very tall cap) +cap_skirt = 4; // the dome edge drops below the disc (hides its edge; the disc must get through) +disc_fit = 0.4; // radial clearance between the disc and the narrowest part of the dome skirt +corbel_w = 12; // dome corbels, above the disc: width (around the screw) +corbel_h = 10; // height of the straight face, 45° chamfer above up to the wall +corbel_in = 5; // corbel overhang beyond the screw axis, inwards +corbel_pilot = 8; // pilot hole depth (M3x10 self-tapping: 3 of disc + 7 engaged) +tab_base = [90, 210, 330]; // dome screws (corbels): each slides (±40°) towards the nearest lobe dot_sizes = [6, 8, 10]; -dot_bag = [6, 6, 6, 8, 8, 8, 10, 10, 10, 10, 10]; // tirage de la taille des pois : gros favorisés +dot_bag = [6, 6, 6, 8, 8, 8, 10, 10, 10, 10, 10]; // dot size draw: large ones favoured dot_pocket = 0.8; -dot_gap = 12; // espace mini entre deux pois +dot_gap = 12; // minimum gap between two dots -/* [Contour du chapeau : générateur de lobes] */ -lobe_seed = 6; // graine : changer pour une autre forme -lobe_n = 4.5; // nombre de lobes dominant -lobe_amp = 0.22; // écart maxi au rayon moyen, au bord (0.22 = ±22 %) -lobe_top = 0.3; // part de la déformation qui reste au sommet (1 = bosselé jusqu'en haut) -lobe_rot = 165; // orientation du contour en degrés ; -1 = automatique (meilleur jeu autour de l'électronique) -clear_m = 3; // jeu mini visé entre l'électronique et le dôme (indice de jeu) +/* [Cap outline: lobe generator] */ +lobe_seed = 6; // seed: change for another shape +lobe_n = 4.5; // dominant lobe count +lobe_amp = 0.22; // max deviation from the mean radius, at the edge (0.22 = ±22 %) +lobe_top = 0.3; // share of the deformation left at the top (1 = bumpy all the way up) +lobe_rot = 165; // outline orientation in degrees; -1 = automatic (best clearance around the electronics) +clear_m = 3; // target minimum clearance between the electronics and the dome (clearance index) -/* [Lamelles] */ +/* [Gills] */ disc_t = 3; -collar_h = 20; // fourreau sous le disque, reçoit le tenon du pied -collar_r = 22.5; // rayon extérieur du fourreau (= haut du pied) -collar_screw_a = 0; // vis radiale du fourreau : sous la prise USB (+X), là où la coupelle est en creux +collar_h = 20; // sleeve under the disc, takes the stem tenon +collar_r = 22.5; // outer radius of the sleeve (= top of the stem) +collar_screw_a = 0; // radial sleeve screw: under the USB socket (+X), where the cup dips tenon_r = 19.5; -disc_bore = 18.5; // trou central du disque : le haut du berceau y passe, en biais de cap_tilt +disc_bore = 18.5; // central hole of the disc: the top of the cradle goes through, slanted by cap_tilt n_gills = 48; gill_t = 1.2; -gill_depth = [17, 3]; // profondeur des lamelles près du pied / au bord +gill_depth = [17, 3]; // gill depth near the stem / at the edge -/* [Châssis] */ -chamber_h = 48; // chambre du bas : BME280 suspendu sous la carte, haut du berceau et paire JST-XH +/* [Chassis] */ +chamber_h = 48; // lower chamber: BME280 hanging under the board, top of the cradle and JST-XH pair deck_t = 1.6; -pcb_gap = 5; // plancher -> dessous de la carte (broches traversantes) +pcb_gap = 5; // deck -> underside of the board (through-hole pins) ch_wall = 2; -bme_w = 15; // breakout BMx280 15 x 11, 6 broches VIN GND SCL SDA CSB SDO centrées sur la largeur +bme_w = 15; // BMx280 breakout 15 x 11, 6 pins VIN GND SCL SDA CSB SDO centred on the width bme_d = 11; bme_t = 1.6; bme_pins = 6; -bme_edge = 1.3; // rangée de broches -> bord du breakout (corps vers +Y) -hdr_p = 2.54; // barrettes au pas de 2.54 -fh_n = 6; // barrette femelle sous la carte : 1 SENSOR_VCC, 2 GND, 3 SCL, 4 SDA, 5 NC (CSB), 6 GND (SDO -> 0x76) -fh_h = 8.5; // hauteur de la femelle sous la carte -mh_h = 2.5; // plastique de la barrette mâle du breakout -mh_pin = 6; // broches mâles au-dessus du plastique (dessinées si fh_n < bme_pins) -fh_clr = 1; // jeu de la fenêtre du plancher autour des barrettes -chg_w = 17.15; // module de charge TP4056 protégé, mesuré (Tars) : 28.7 prise et ergots de découpe compris +bme_edge = 1.3; // pin row -> breakout edge (body towards +Y) +hdr_p = 2.54; // 2.54 pitch headers +fh_n = 6; // female header under the board: 1 SENSOR_VCC, 2 GND, 3 SCL, 4 SDA, 5 NC (CSB), 6 GND (SDO -> 0x76) +fh_h = 8.5; // height of the female header under the board +mh_h = 2.5; // plastic of the breakout's male header +mh_pin = 6; // male pins above the plastic (drawn if fh_n < bme_pins) +fh_clr = 1; // clearance of the deck window around the headers +chg_w = 17.15; // protected TP4056 charge module, measured (Tars): 28.7 including socket and break-off tabs chg_l = 28.7; chg_t = 0.9; -chg_x = 40; // plan de la carte du module +chg_x = 40; // plane of the module board usb_cut = [8.5, 12]; -/* [Pied] */ -stem_h = 95; // centre du haut du pied (sous le fourreau) -bore_r = 14.7; // logement de l'accu (Ø29.4, reçoit le berceau) -foot_t = 3; // fond du pied -ballast_h = 52; // chambre à lest sous le berceau : hauteur au rayon du logement, plafond conique à 45° -ballast_wall = 2.4; // paroi entre la chambre et l'extérieur du pied -fill_d = 15; // trou de remplissage dans le fond -plug_d = 21; // lamage du bouchon collé -plug_t = 1.2; // = profondeur du lamage (6 couches de 0.2) -stem_r = 19; // rayon du fût en bas, il s'élargit régulièrement jusqu'au fourreau (collar_r) -stem_amp = 0.15; // lobes du fût (n'ajoutent que de la matière : la paroi autour de l'accu ne s'amincit pas) -stem_twist = 1.2; // rotation des lobes le long du fût (°/mm) -bulb_r = [22, 34]; // base bulbeuse (croquis) : saillie, petit côté puis grand côté -bulb_z = 22; // hauteur du ventre -bulb_up = [21, 27]; // étalement du bulbe vers le haut : petit côté, grand côté (épaule) -bulb_dn = 34; // étalement vers le bas : base arrondie, pente < 45° pour l'impression -foot_dir = 200; // direction du grand côté (°) -ring_z = 72; // anneau en coupelle : attache sur le col, bord relevé sous le chapeau -ring_ext = 20; // débord moyen de la coupelle -ring_rise = 0.75; // montée du bord / débord (< 1 : 45° au plus, imprimable sans support) -ring_amp = 0.35; // ondulation de la coupelle (débord et montée) -ring_rot = 131; // phase des ondulations de la coupelle (°) : son creux le plus large sous la prise USB (+X), pour brancher le câble - -/* [Berceau de l'accu] */ -cell_d = 18.6; // 18650 nue, téton plat (protégée / à bouton : jusqu'à ~70) +/* [Stem] */ +stem_h = 95; // centre of the top of the stem (under the sleeve) +bore_r = 14.7; // cell bore (Ø29.4, takes the cradle) +foot_t = 3; // stem bottom +ballast_h = 52; // ballast chamber under the cradle: height at the bore radius, conical ceiling at 45° +ballast_wall = 2.4; // wall between the chamber and the outside of the stem +fill_d = 15; // filling hole in the bottom +plug_d = 21; // counterbore of the glued plug +plug_t = 1.2; // = counterbore depth (6 layers of 0.2) +stem_r = 19; // shaft radius at the bottom, it widens steadily up to the sleeve (collar_r) +stem_amp = 0.15; // shaft lobes (only add material: the wall around the cell never gets thinner) +stem_twist = 1.2; // lobe rotation along the shaft (°/mm) +bulb_r = [22, 34]; // bulbous base (sketch): bulge, short side then long side +bulb_z = 22; // belly height +bulb_up = [21, 27]; // upward spread of the bulb: short side, long side (shoulder) +bulb_dn = 34; // downward spread: rounded base, slope < 45° for printing +foot_dir = 200; // direction of the long side (°) +ring_z = 72; // cup ring: attached to the neck, raised edge under the cap +ring_ext = 20; // average cup overhang +ring_rise = 0.75; // edge rise / overhang (< 1: 45° at most, printable without support) +ring_amp = 0.35; // cup waviness (overhang and rise) +ring_rot = 131; // phase of the cup waves (°): its widest dip under the USB socket (+X), to plug the cable + +/* [Cell cradle] */ +cell_d = 18.6; // bare 18650, flat top (protected / button top: up to ~70) cell_l = 65; -cr_fit = 0.8; // jeu berceau / logement (au rayon ; 0.4 : 1er tirage entré tout juste) -cell_up = 2; // déport de l'accu vers le dessus ouvert (épaisseur du fond sous l'accu) -bt_w = 13; // butées à languette : plaque 13 x 11, languette 3 x 7 (percée) +cr_fit = 0.8; // cradle / bore clearance (radial; 0.4: 1st print barely went in) +cell_up = 2; // cell offset towards the open top (thickness of the floor under the cell) +bt_w = 13; // tab contacts: 13 x 11 plate, 3 x 7 tab (drilled) bt_h = 11; -bt_t = 0.4; // épaisseur de la tôle (à mesurer) -bt_tab = [3, 7]; // languette vers le haut (côté ouvert), fil soudé dessus -cr_win = [3, 15, 4, 14]; // allègement : fenêtres par flanc (nombre, longueur, bas, haut au-dessus du méplat) ; fond et rails gardés -wire_notch = [4.5, 3]; // passage du fil au-dessus de chaque bout : largeur, profondeur sous le dessus de l'accu -bt_dimple = 1.8; // bossage de la butée « + » -spring_c = 4; // ressort de la butée « − » comprimé (10.7 libre) -bt_slot = 0.8; // rainure de la plaque -bt_sink = 1.5; // rainure plus profonde que la plaque (1er tirage : languette 1 mm au-dessus du bord ; +0.4 pour cr_fit 0.8) -bt_win = 10.5; // fenêtre devant la plaque (passage du ressort / du bossage) - -/* [Sol] */ -ground_t = 1; // disque sous le pied (pièce séparée, à coller) -ground_R = 72; // rayon moyen +bt_t = 0.4; // sheet thickness (to measure) +bt_tab = [3, 7]; // tab up (open side), wire soldered on it +cr_win = [3, 15, 4, 14]; // lightening: windows per flank (count, length, bottom, top above the flat); floor and rails kept +wire_notch = [4.5, 3]; // wire pass above each end: width, depth below the top of the cell +bt_dimple = 1.8; // bump of the "+" contact +spring_c = 4; // spring of the "−" contact compressed (10.7 free) +bt_slot = 0.8; // plate groove +bt_sink = 1.5; // groove deeper than the plate (1st print: tab 1 mm above the edge; +0.4 for cr_fit 0.8) +bt_win = 10.5; // window in front of the plate (room for the spring / the bump) + +/* [Ground] */ +ground_t = 1; // disc under the stem (separate part, to glue) +ground_R = 72; // mean radius ground_amp = 0.15; grass_seed = 11; -grass_tufts = 4; // touffes de grands brins autour du pied -grass_single = 30; // brins isolés -fringe_h = [3, 9]; // frange d'herbe en zigzag autour du pied : hauteur des dents -fringe_n = 64; // nombre de dents de la frange +grass_tufts = 4; // tufts of tall blades around the stem +grass_single = 30; // single blades +fringe_h = [3, 9]; // zigzag grass fringe around the stem: tooth height +fringe_n = 64; // number of fringe teeth -/* [Résolution des maillages] */ -na = 90; // dôme : pas angulaire -nt = 120; // dôme : pas le long du méridien -ns = 72; // pied, corolle : pas angulaire +/* [Mesh resolution] */ +na = 90; // dome: angular step +nt = 120; // dome: step along the meridian +ns = 72; // stem, ring: angular step -/* [Visserie] */ +/* [Screws] */ m3_pilot = 2.5; m3_clear = 3.4; m25_pilot = 2.1; @@ -189,60 +189,60 @@ m25_pilot = 2.1; $fn = 64; eps = 0.01; -// ---------- dérivées ---------- -bore_z = foot_t + ballast_h + bore_r + 2; // fond du logement du berceau, au-dessus du cône -ballast_top = foot_t + ballast_h + bore_r; // sommet du plafond -ix = crop_w / 2 + 0.4; // demi-dimensions intérieures du châssis +// ---------- derived ---------- +bore_z = foot_t + ballast_h + bore_r + 2; // bottom of the cradle bore, above the cone +ballast_top = foot_t + ballast_h + bore_r; // top of the ceiling +ix = crop_w / 2 + 0.4; // inner half-dimensions of the chassis iy = crop_h / 2 + 0.4; -ch_z = disc_t; // bas du châssis -deck_z = ch_z + chamber_h; // dessus des appuis du plancher -pcb_z = deck_z + deck_t + pcb_gap; // dessous de la carte +ch_z = disc_t; // bottom of the chassis +deck_z = ch_z + chamber_h; // top of the deck ledges +pcb_z = deck_z + deck_t + pcb_gap; // underside of the board ch_h = pcb_z - ch_z; -// carte tournée de 180° dans le châssis : J1 passe du côté -X, sous le haut penché du chapeau -i2c_p1 = [k_crop_c[0] - i2c_k[0], i2c_k[1] - k_crop_c[1]]; // broche 1 dans le repère du chapeau -radio_p = [k_crop_c[0] - radio_k[0], radio_k[1] - k_crop_c[1]]; // J1, idem (= repère du dôme en XY) -cap_top = [(radio_p[0] + (1 - cap_neck[0]) * cap_H * sin(cap_lean)) / cos(cap_lean), radio_p[1]]; // déport qui met le sommet penché sur J1 -bme_z = pcb_z - fh_h - mh_h; // dessus du breakout +// board turned 180° in the chassis: J1 moves to the -X side, under the leaning top of the cap +i2c_p1 = [k_crop_c[0] - i2c_k[0], i2c_k[1] - k_crop_c[1]]; // pin 1 in the cap frame +radio_p = [k_crop_c[0] - radio_k[0], radio_k[1] - k_crop_c[1]]; // J1, same (= dome frame in XY) +cap_top = [(radio_p[0] + (1 - cap_neck[0]) * cap_H * sin(cap_lean)) / cos(cap_lean), radio_p[1]]; // offset putting the leaning top over J1 +bme_z = pcb_z - fh_h - mh_h; // top of the breakout module cap_xf() { translate([0, 0, stem_h]) rotate([0, cap_tilt, 0]) translate([0, 0, collar_h]) children(); } -// ---------- dôme ---------- -// méridien : Catmull-Rom par cap_shape ; points fantômes miroirs = tangente verticale au bord, horizontale au sommet +// ---------- dome ---------- +// meridian: Catmull-Rom through cap_shape; mirrored ghost points = vertical tangent at the edge, horizontal at the top cap_prof = [for (p = cap_shape) [p[0] * cap_R, p[1] * cap_H]]; prof_n = len(cap_prof) - 1; function ctrl(i) = i < 0 ? [cap_prof[1][0], -cap_prof[1][1]] : i > prof_n ? [-cap_prof[prof_n - 1][0], cap_prof[prof_n - 1][1]] : cap_prof[i]; -// [point, dérivée] du méridien en u, de 0 (bord) à 1 (sommet) +// [point, derivative] of the meridian at u, from 0 (edge) to 1 (top) function prof(u) = let(x = u * prof_n, i = min(prof_n - 1, floor(x)), v = x - i, a = ctrl(i - 1), b = ctrl(i), c = ctrl(i + 1), d = ctrl(i + 2), k1 = c - a, k2 = 2 * a - 5 * b + 4 * c - d, k3 = 3 * b - a - 3 * c + d) [b + (k1 + (k2 + k3 * v) * v) * v / 2, (k1 + (2 * k2 + 3 * k3 * v) * v) / 2]; -prof_zr = [for (i = [0 : 240]) let(p = prof(i / 240)[0]) [p[1], p[0]]]; // table hauteur -> rayon -assert(min([for (i = [1 : 240]) prof_zr[i][0] - prof_zr[i - 1][0]]) > 0, "cap_shape : hauteurs non croissantes"); -// axe du dôme à la hauteur z : part de l'axe du disque, finit en cap_top +prof_zr = [for (i = [0 : 240]) let(p = prof(i / 240)[0]) [p[1], p[0]]]; // height -> radius table +assert(min([for (i = [1 : 240]) prof_zr[i][0] - prof_zr[i - 1][0]]) > 0, "cap_shape: heights not increasing"); +// dome axis at height z: starts on the disc axis, ends at cap_top function neck_k(z) = let(k = max(0, min(1, (z / cap_H - cap_neck[0]) / (cap_neck[1] - cap_neck[0])))) k * k * (3 - 2 * k); function cap_shift(z) = cap_top * neck_k(z); -// penchement : rotation autour de la base du col, nulle sous le col (électronique, consoles et disque non concernés) +// lean: rotation about the base of the neck, zero below the neck (electronics, corbels and disc unaffected) function lean_pt(p) = let(a = cap_lean * neck_k(p[2]), dz = p[2] - cap_neck[0] * cap_H) [p[0] * cos(a) - dz * sin(a), p[1], cap_neck[0] * cap_H + p[0] * sin(a) + dz * cos(a)]; -// contour : harmoniques 2 à 7 à phases aléatoires, pondérées autour de lobe_n, normalisées à lobe_amp +// outline: harmonics 2 to 7 with random phases, weighted around lobe_n, normalised to lobe_amp function vsum(v, i = 0) = i < len(v) ? v[i] + vsum(v, i + 1) : 0; lobe_terms = [for (k = [2 : 7]) let(q = rands(0, 1, 2, lobe_seed * 17 + k)) [k, (0.5 + 0.5 * q[0]) * exp(-pow((k - lobe_n) / 1.5, 2)), 360 * q[1]]]; function lobe_raw(phi) = vsum([for (h = lobe_terms) h[1] * sin(h[0] * phi + h[2])]); lobe_peak = max([for (i = [0 : 179]) abs(lobe_raw(2 * i))]); function lobe_at(phi, rot) = lobe_amp * lobe_raw(phi - rot) / lobe_peak; -// déformation pleine au bord (s = 0), réduite à lobe_top au sommet (s = 1) +// full deformation at the edge (s = 0), reduced to lobe_top at the top (s = 1) function wav_r(phi, s, rot) = 1 + lobe_at(phi, rot) * (lobe_top + (1 - lobe_top) * (1 - s)); -// surépaisseur du bord : pleine sous le disque, puis raccord à 45° (sans support) +// edge thickening: full under the disc, then 45° blend (no support) function rim_extra(z) = let(e = rim_w - cap_t) max(0, min(e, e - (z - cap_skirt))); function inner_rr(phi, zz, rot) = lookup(zz, prof_zr) * wav_r(phi, min(1, zz / cap_H), rot) - cap_t - rim_extra(zz); -// jeu : points de l'électronique (repère du chapeau) qui doivent rester à >= 3 mm sous le dôme +// clearance: electronics points (cap frame) that must stay >= 3 mm under the dome clear_pts = concat( - [for (sx = [-1, 1], sy = [-1, 1]) [sx * (crop_w / 2 + 2.4), sy * (crop_h / 2 + 2.4), disc_t + chamber_h + 9]], // châssis - [for (sx = [-1, 1], sy = [-1, 1]) [sx * 30.5, sy * 20, disc_t + chamber_h + 23]], // composants - [for (x = [38, 45], sy = [-1, 1]) [x, sy * 10.5, disc_t + chg_l]]); // chargeur -// v <= 1 : le point est sous le dôme réduit de clear_m + [for (sx = [-1, 1], sy = [-1, 1]) [sx * (crop_w / 2 + 2.4), sy * (crop_h / 2 + 2.4), disc_t + chamber_h + 9]], // chassis + [for (sx = [-1, 1], sy = [-1, 1]) [sx * 30.5, sy * 20, disc_t + chamber_h + 23]], // components + [for (x = [38, 45], sy = [-1, 1]) [x, sy * 10.5, disc_t + chg_l]]); // charger +// v <= 1: the point is under the dome shrunk by clear_m function v_in(p, rot) = let(zz = p[2] + cap_skirt, d = [p[0], p[1]] - cap_shift(zz)) norm(d) / (inner_rr(atan2(d[1], d[0]), zz, rot) - clear_m); function worst(rot) = max([for (p = clear_pts) v_in(p, rot)]); @@ -250,29 +250,29 @@ rots = [for (a = [0 : 5 : 355]) worst(a)]; best_rot = search(min(rots), rots)[0] * 5; rot = lobe_rot >= 0 ? lobe_rot : best_rot; function wav(phi, s = 0) = wav_r(phi, s, rot); -// point de la surface : t de 0 (bord) à 90 (sommet) le long du méridien, off = retrait selon la normale +// surface point: t from 0 (edge) to 90 (top) along the meridian, off = inset along the normal function cap_pt(phi, t, off = 0) = let(m = prof(t / 90), n = [m[1][1], -m[1][0]] / norm(m[1]), s = m[0][1] / cap_H, e = off > 0 ? rim_extra(m[0][1]) : 0, r = max(0, m[0][0] * wav(phi, s) - off * n[0] - e), z = m[0][1] - off * n[1], c = cap_shift(z)) lean_pt([r * cos(phi) + c[0], r * sin(phi) + c[1], z]); -// rayon intérieur du dôme à la hauteur zz (repère du dôme) +// inner dome radius at height zz (dome frame) function inner_r(phi, zz) = inner_rr(phi, zz, rot); -apex = cap_pt(0, 90); // repère du dôme -vent_in = apex - [0, 0, cap_t + 3]; // sous la pointe du plafond conique +apex = cap_pt(0, 90); // dome frame +vent_in = apex - [0, 0, cap_t + 3]; // under the tip of the conical ceiling vent_out = cap_pt(vent_at[0], vent_at[1]); -assert(vent_out[2] < vent_in[2], "L'évent doit descendre vers l'extérieur"); +assert(vent_out[2] < vent_in[2], "The vent must slope down towards the outside"); function ring(j, off) = [for (i = [0 : na - 1]) cap_pt(i * 360 / na, j * 90 / nt, off)]; function surf(off) = concat([for (j = [0 : nt - 1]) each ring(j, off)], [cap_pt(0, 90, off)]); function o(i, j) = j * na + (i % na); -module dome_solid(off = 0) { // volume plein sous la surface, fermé par le plan du bord +module dome_solid(off = 0) { // solid volume under the surface, closed by the edge plane polyhedron(convexity = 6, points = surf(off), faces = concat( [for (j = [0 : nt - 2], i = [0 : na - 1]) [o(i, j), o(i, j + 1), o(i + 1, j + 1), o(i + 1, j)]], [for (i = [0 : na - 1]) [o(i, nt - 1), nt * na, o(i + 1, nt - 1)]], [[for (i = [0 : na - 1]) o(i, 0)]])); } -// pois : tirage « disque de Poisson » sur la surface réelle : candidats sur une grille -// (phi, t) dense et bruitée, parcourus dans un ordre pseudo-aléatoire, gardés s'ils sont assez loin des autres +// dots: "Poisson disc" draw on the real surface: candidates on a dense, noisy (phi, t) +// grid, visited in a pseudo-random order, kept if far enough from the others dot_cand = [for (t = [4 : 2 : 88], phi = [0 : 3 : 357]) let(q = rands(0, 1, 3, t * 1000 + phi + lobe_seed)) let(tt = t + q[0] * 2, pp = phi + q[1] * 3, p = cap_pt(pp, tt)) if (p[2] > 9) [pp, tt, dot_bag[floor(q[2] * len(dot_bag) * 0.999)], p]]; @@ -280,7 +280,7 @@ function dot_far(c, acc, i = 0) = i >= len(acc) ? true : norm(c[3] - acc[i][3]) < (c[2] + acc[i][2]) / 2 + dot_gap ? false : dot_far(c, acc, i + 1); function dot_pick(i, acc) = i >= len(dot_cand) ? acc : let(c = dot_cand[(i * 7919) % len(dot_cand)]) dot_pick(i + 1, dot_far(c, acc) ? concat(acc, [c]) : acc); -dots = [for (d = dot_pick(0, [])) if (norm(d[3] - vent_out) > vent_clear) d]; // filtrés après le tirage : la répartition des tailles ne bouge pas +dots = [for (d = dot_pick(0, [])) if (norm(d[3] - vent_out) > vent_clear) d]; // filtered after the draw: the size distribution does not change function unit(v) = v / norm(v); function cap_normal(phi, t) = unit(cross(cap_pt(phi + 0.5, t) - cap_pt(phi - 0.5, t), cap_pt(phi, t + 0.5) - cap_pt(phi, t - 0.5))); module at_dot(d) { @@ -288,17 +288,17 @@ module at_dot(d) { translate(cap_pt(d[0], d[1])) rotate([0, acos(n[2]), atan2(n[1], n[0])]) children(); } -// vis du dôme : axe à 5 mm de la paroi au ras du bord (loin de l'électronique), dans les consoles +// dome screws: axis 5 mm from the wall at the edge (away from the electronics), in the corbels function tab_r(a) = inner_r(a, 2) - 5; -corbel_z = cap_skirt + disc_t; // face d'appui des consoles = dessus du disque (repère du dôme) +corbel_z = cap_skirt + disc_t; // corbel bearing face = top of the disc (dome frame) function argmax_a(a0) = let(c = [for (d = [-40 : 2 : 40]) lobe_at(a0 + d, rot)]) a0 - 40 + 2 * search(max(c), c)[0]; tab_a = [for (a = tab_base) argmax_a(a)]; -// consoles sur la paroi, juste au-dessus du disque : le disque monte dans le dôme et y est tiré par les vis ; -// face droite côté intérieur, chanfrein à 45° jusqu'à la paroi au-dessus ; seule face vers le plateau : -// l'appui (corbel_z, ~7 mm au-dessus du plateau), en pont sans support (vérifié en PETG) +// corbels on the wall, just above the disc: the disc rises into the dome and is pulled up by the screws; +// straight face on the inside, 45° chamfer up to the wall above; the only face towards the bed: +// the bearing face (corbel_z, ~7 mm above the bed), bridged without support (checked in PETG) module corbels() { intersection() { - for (a = tab_a) let(x0 = tab_r(a) - corbel_in, x1 = 1.5 * cap_R) // coupée par la paroi + for (a = tab_a) let(x0 = tab_r(a) - corbel_in, x1 = 1.5 * cap_R) // cut by the wall rotate([0, 0, a]) translate([0, corbel_w / 2, 0]) rotate([90, 0, 0]) linear_extrude(corbel_w) polygon([[x0, corbel_z], [x1, corbel_z], [x1, corbel_z + corbel_h + x1 - x0], [x0, corbel_z + corbel_h]]); dome_solid(); @@ -309,7 +309,7 @@ module cap() { union() { difference() { dome_solid(); - // plafond conique à 45° au sommet : le col étroit empêcherait de retirer des supports + // 45° conical ceiling at the top: the narrow neck would prevent removing supports intersection() { translate([0, 0, -eps]) dome_solid(cap_t); translate([apex[0], apex[1], 0]) cylinder(r1 = apex[2] - cap_t, r2 = 0, h = apex[2] - cap_t); @@ -324,35 +324,35 @@ module cap() { } module dot(d) { - cylinder(d = d, h = dot_pocket + eps, $fn = 32); // recouvrement : un seul solide avec la lentille + cylinder(d = d, h = dot_pocket + eps, $fn = 32); // overlap: a single solid with the lens translate([0, 0, dot_pocket]) intersection() { scale([1, 1, 0.45]) sphere(d = d, $fn = 32); translate([-d, -d, 0]) cube([2 * d, 2 * d, d]); } } -// ---------- disque à lamelles (repère du chapeau) ---------- -// rayon du disque : le plus étroit de la paroi entre le bord et le dessus du disque (aux creux des lobes, -// la paroi rentre en descendant), pour qu'il monte dans le dôme fermé jusqu'aux consoles +// ---------- gill disc (cap frame) ---------- +// disc radius: the narrowest part of the wall between the edge and the top of the disc (in the lobe hollows, +// the wall moves inwards going down), so that it rises into the closed dome up to the corbels function inner_min(a) = min([for (z = [0 : 0.5 : cap_skirt + disc_t]) inner_r(a, z)]); function disc_pts() = [for (i = [0 : na - 1]) let(a = i * 360 / na, r = inner_min(a) - disc_fit) r * [cos(a), sin(a)]]; -disc_rmax = cap_R - cap_t; // normalisation de la profondeur des lamelles (au-delà : profondeur mini) +disc_rmax = cap_R - cap_t; // gill depth normalisation (beyond: minimum depth) function gill_d(r) = gill_depth[1] + (gill_depth[0] - gill_depth[1]) * pow(max(0, (disc_rmax - r) / (disc_rmax - collar_r)), 1.3); function adiff(a, b) = abs(((a - b + 540) % 360) - 180); -// angles où une lamelle gênerait : vis du dôme, prise USB, vis du châssis, vis du fourreau +// angles where a gill would be in the way: dome screws, USB socket, chassis screws, sleeve screw ch_screws = [for (sx = [-1, 1], sy = [-1, 1]) [sx * (ix - 2.5), sy * (iy - 2.5)]]; gill_skip = concat([for (a = tab_a) [a, 7]], [[0, 12], [collar_screw_a, 7]], [for (p = ch_screws) [atan2(p[1], p[0]), 5]]); -assert(min([for (a = [-12 : 2 : 12]) inner_min(a) - disc_fit]) >= chg_x + chg_t + 1.5 + usb_cut[0] / 2 + 2, "Disque trop petit autour de la prise USB"); +assert(min([for (a = [-12 : 2 : 12]) inner_min(a) - disc_fit]) >= chg_x + chg_t + 1.5 + usb_cut[0] / 2 + 2, "Disc too small around the USB socket"); function gill_ok(a) = min([for (s = gill_skip) adiff(a, s[0]) - s[1]]) > 0; module gills() { difference() { union() { linear_extrude(disc_t) polygon(disc_pts()); - // fourreau + // sleeve translate([0, 0, -collar_h]) cylinder(r = collar_r, h = collar_h + eps); - // lamelles : lames radiales, plus profondes près du pied + // gills: radial blades, deeper near the stem intersection() { for (i = [0 : n_gills - 1]) let(a = (i + 0.5) * 360 / n_gills) if (gill_ok(a)) rotate([0, 0, a]) rotate([90, 0, 0]) linear_extrude(gill_t, center = true) @@ -360,52 +360,52 @@ module gills() { translate([0, 0, -50]) linear_extrude(50 + eps) offset(delta = -1) polygon(disc_pts()); } } - // fourreau + // sleeve translate([0, 0, -collar_h - eps]) cylinder(r = tenon_r + 0.25, h = collar_h); translate([0, 0, -1]) cylinder(r = disc_bore, h = disc_t + 2); - // vis radiale du fourreau + // radial sleeve screw rotate([0, 0, collar_screw_a]) translate([tenon_r - 1, 0, -collar_h + 8]) rotate([0, 90, 0]) cylinder(d = m3_clear, h = collar_r - tenon_r + 2); - // fentes de ventilation sous la chambre du capteur, entre les lamelles ; côté +X aussi (au ras du bord - // du BME280, le reste du breakout est au-dessus du fourreau) + // vent slots under the sensor chamber, between the gills; on the +X side too (flush with the edge + // of the BME280, the rest of the breakout is above the sleeve) for (i = [0 : n_gills - 1]) let(a = i * 360 / n_gills) rotate([0, 0, a]) translate([collar_r + 1, -0.8, -1]) cube([min(ix / max(abs(cos(a)), 0.01), iy / max(abs(sin(a)), 0.01), 31) - collar_r - 1, 1.6, disc_t + 2]); - // prise micro-USB + // micro-USB socket translate([chg_x + chg_t + 1.5 - usb_cut[0] / 2, -usb_cut[1] / 2, -1]) cube([usb_cut[0], usb_cut[1], disc_t + 2]); - // vis du dôme : passage, têtes sous le disque entre les lamelles + // dome screws: clearance, heads under the disc between the gills for (a = tab_a) rotate([0, 0, a]) translate([tab_r(a), 0, -1]) cylinder(d = m3_clear, h = disc_t + 2); - // vis du châssis, têtes sous le disque + // chassis screws, heads under the disc for (p = ch_screws) translate([p[0], p[1], -1]) cylinder(d = m3_clear, h = disc_t + 2); } } -// ---------- châssis (repère du chapeau, posé sur le disque) ---------- +// ---------- chassis (cap frame, resting on the disc) ---------- module chassis() { translate([0, 0, ch_z]) difference() { union() { difference() { translate([-ix - ch_wall, -iy - ch_wall, 0]) cube([2 * (ix + ch_wall), 2 * (iy + ch_wall), ch_h]); translate([-ix, -iy, -1]) cube([2 * ix, 2 * iy, ch_h + 2]); - // fenêtres de la chambre + // chamber windows for (x = [-20, -6, 8, 22]) translate([x - 4, -iy - ch_wall - 1, chamber_h - 1 - win_h]) cube([8, 2 * (iy + ch_wall) + 2, win_h]); for (y = [-8, 8]) translate([-ix - ch_wall - 1, y - 4, chamber_h - 1 - win_h]) cube([ch_wall + 2, 8, win_h]); } - // appuis du plancher et de la carte le long des grands côtés (chanfrein 45° dessous) + // deck and board ledges along the long sides (45° chamfer underneath) for (s = [-1, 1], z = [chamber_h, ch_h]) mirror([0, s < 0 ? 1 : 0, 0]) translate([-ix, iy, z]) rotate([0, 90, 0]) linear_extrude(2 * ix) polygon([[0, 0], [1.5, 0], [0, -1.5]]); - // coins : bossages des vis du châssis (bas) ; coins hauts pleins jusqu'à la carte (vis M2.5 de la carte) + // corners: chassis screw bosses (bottom); top corners solid up to the board (board M2.5 screws) for (p = ch_screws) translate([p[0] > 0 ? ix - 7 : -ix, p[1] > 0 ? iy - 7 : -iy, 0]) cube([7, 7, p[1] < 0 ? ch_h : chamber_h - 1]); - // glissière du module de charge, tenue par deux bras sur la paroi +X + // charge module slide, held by two arms on the +X wall translate([chg_x, 0, 0]) rails(chg_w, chg_t, 20); for (s = [-1, 1]) translate([ix + ch_wall - eps, s > 0 ? chg_w / 2 - 1 : -chg_w / 2 - 2, 0]) cube([chg_x - 1.5 - ix - ch_wall + 2 * eps, 3, 20]); } - // vis du châssis (par en dessous) et de la carte (par le dessus) + // chassis screws (from below) and board screws (from above) for (p = ch_screws) translate([p[0], p[1], -eps]) cylinder(d = m3_pilot, h = 7); for (h = pcb_holes) translate([h[0], h[1], ch_h - 6]) cylinder(d = m25_pilot, h = 6 + eps); } } -// deux rails en U face à face, carte dans le plan YZ (épaisseur selon X) +// two facing U rails, board in the YZ plane (thickness along X) module rails(w, t, h) { for (s = [-1, 1]) translate([-1.5, s > 0 ? w / 2 - 1 : -w / 2 - 2, 0]) difference() { cube([t + 3, 3, h]); @@ -413,43 +413,43 @@ module rails(w, t, h) { } } -// ---------- plancher percé ---------- +// ---------- perforated deck ---------- module deck() { difference() { translate([-ix + 0.3, -iy + 0.3, 0]) cube([2 * ix - 0.6, 2 * iy - 0.6, deck_t]); for (p = ch_screws) translate([p[0] - 4.6, p[1] - 4.6, -1]) cube([9.2, 9.2, deck_t + 2]); for (x = [-24 : 6 : 24], y = [-15 : 6 : 15]) translate([x + (round(y / 6) % 2) * 3, y, -1]) cylinder(d = 4, h = deck_t + 2, $fn = 6); - // fenêtre de la barrette femelle I2C + // I2C female header window translate([i2c_p1[0], i2c_p1[1], -1]) rotate(180) translate([-(bme_pins - 0.5) * hdr_p - fh_clr, -hdr_p / 2 - fh_clr, 0]) cube([bme_pins * hdr_p + 2 * fh_clr, hdr_p + 2 * fh_clr, deck_t + 2]); } } -// ---------- pied ---------- -// générateur générique (même principe que le contour du chapeau) : harmoniques 2..7 à phases aléatoires +// ---------- stem ---------- +// generic generator (same principle as the cap outline): harmonics 2..7 with random phases function hterms(seed, n) = [for (k = [2 : 7]) let(q = rands(0, 1, 2, seed * 17 + k)) [k, (0.5 + 0.5 * q[0]) * exp(-pow((k - n) / 1.5, 2)), 360 * q[1]]]; function hval(T, phi) = vsum([for (h = T) h[1] * sin(h[0] * phi + h[2])]); function hpeak(T) = max([for (i = [0 : 179]) abs(hval(T, 2 * i))]); -function hnorm(T, P, phi) = hval(T, phi) / P; // dans [-1, 1] +function hnorm(T, P, phi) = hval(T, phi) / P; // in [-1, 1] stem_T = hterms(lobe_seed + 101, 3); stem_P = hpeak(stem_T); ring_T = hterms(lobe_seed + 202, 4); ring_P = hpeak(ring_T); ring_T2 = hterms(lobe_seed + 303, 5); ring_P2 = hpeak(ring_T2); ground_T = hterms(grass_seed, 4); ground_P = hpeak(ground_T); function base_r(z) = stem_r + (collar_r - stem_r) * min(1, z / stem_h); -// rayon extérieur du pied : fût conique + lobes tournants (nuls sous le fourreau) + bulbe dissymétrique -function foot_w(phi) = pow((1 + cos(phi - foot_dir)) / 2, 2); // 1 côté grand évasement, 0 à l'opposé +// outer stem radius: conical shaft + twisting lobes (zero under the sleeve) + asymmetric bulb +function foot_w(phi) = pow((1 + cos(phi - foot_dir)) / 2, 2); // 1 on the wide-flare side, 0 opposite function stem_rr(phi, z) = let(w = foot_w(phi), B = bulb_r[0] + (bulb_r[1] - bulb_r[0]) * w, S = z < bulb_z ? bulb_dn : bulb_up[0] + (bulb_up[1] - bulb_up[0]) * w, fade = max(0, min(1, (stem_h - z) / 15))) base_r(z) * (1 + stem_amp * (0.5 + 0.5 * hnorm(stem_T, stem_P, phi - stem_twist * z)) * fade) + B * exp(-pow((z - bulb_z) / S, 2)); -// volume de la chambre à lest (cm³), intégré en tranches +// ballast chamber volume (cm³), integrated in slices ballast_v = let(dz = 0.5, na = 72) vsum([for (z = [foot_t : dz : ballast_top]) vsum([for (i = [0 : na - 1]) let(r = max(0, min(stem_rr(i * 360 / na, z) - ballast_wall, ballast_top - z))) 0.5 * r * r * (2 * PI / na) * dz])]) / 1000; stem_zs = [for (z = [0 : 1.5 : stem_h + 6]) z]; -module stem_body(off = 0, zs = stem_zs) { // off : retrait de la surface (intérieur de la chambre à lest) +module stem_body(off = 0, zs = stem_zs) { // off: inset from the surface (inside of the ballast chamber) J = len(zs); function v(i, j) = j * ns + (i % ns); polyhedron(convexity = 4, @@ -458,14 +458,14 @@ module stem_body(off = 0, zs = stem_zs) { // off : retrait de la surface (int [[for (i = [0 : ns - 1]) v(i, 0)]], [[for (i = [ns - 1 : -1 : 0]) v(i, J - 1)]])); } -// anneau en coupelle : part du col et remonte en s'évasant, bord ondulé ; 45° au plus, sans support +// cup ring: starts at the neck and rises while flaring, wavy edge; 45° at most, no support function ring_prof(a) = let(ri = stem_rr(a, ring_z) - 1, ext = ring_ext * (1 + ring_amp * hnorm(ring_T, ring_P, a - ring_rot)), rise = ext * ring_rise * (1 + 0.3 * ring_amp * hnorm(ring_T2, ring_P2, a - ring_rot))) [[ri, ring_z + 2.4], [ri + ext, ring_z + rise + 1.2], [ri + ext, ring_z + rise], [ri, ring_z - 1.2]]; -// prise USB vue du pied : bord intérieur de la fenêtre (rayon) ; la coupelle reste 2 mm en deçà sous la fiche (~11 mm de large : ±8°) +// USB socket seen from the stem: inner edge of the window (radius); the cup stays 2 mm short under the plug (~11 mm wide: ±8°) usb_in_r = (chg_x + chg_t + 1.5 - usb_cut[0] / 2) * cos(cap_tilt) + collar_h * sin(cap_tilt); usb_ring_r = max([for (a = [-8 : 1 : 8]) ring_prof(a)[1][0]]); -assert(usb_ring_r <= usb_in_r - 2, str("Coupelle sous la prise USB (bord à ", usb_ring_r, " mm, fenêtre à ", usb_in_r, " mm) : changer ring_rot")); +assert(usb_ring_r <= usb_in_r - 2, str("Cup under the USB socket (edge at ", usb_ring_r, " mm, window at ", usb_in_r, " mm): change ring_rot")); module ring_skirt() { function prof(a) = ring_prof(a); function v(i, k) = (i % ns) * 4 + (k % 4); @@ -477,99 +477,99 @@ module ring_skirt() { module stem() { difference() { union() { - // fût, coupé sous le fourreau (plan perpendiculaire à l'axe du chapeau) + // shaft, cut under the sleeve (plane perpendicular to the cap axis) intersection() { stem_body(); cap_xf() translate([-100, -100, -collar_h - 200]) cube([200, 200, 200]); } ring_skirt(); - // tenon incliné, emboîté dans le fourreau + // tilted tenon, fitted into the sleeve cap_xf() translate([0, 0, -collar_h - 1]) cylinder(r = tenon_r, h = 17); } - // logement de l'accu, vertical jusqu'en haut (le support passe par le tenon) + // cell bore, vertical up to the top (the cradle goes through the tenon) translate([0, 0, bore_z]) cylinder(r = bore_r, h = stem_h + 40); - // chambre à lest : suit la peau du pied (paroi ballast_wall, pentes <= 45° comme le bulbe), - // plafond conique à 45° (sans support) ; remplie par le trou du fond puis bouchée + // ballast chamber: follows the stem skin (ballast_wall wall, slopes <= 45° like the bulb), + // conical ceiling at 45° (no support); filled through the bottom hole then plugged intersection() { stem_body(ballast_wall, [for (z = [foot_t : 1.5 : ballast_top]) z]); translate([0, 0, foot_t - eps]) cylinder(r1 = ballast_top - foot_t, r2 = 0, h = ballast_top - foot_t); } translate([0, 0, -1]) cylinder(d = fill_d, h = foot_t + 2); translate([0, 0, -1]) cylinder(d = plug_d + 0.3, h = plug_t + 1); - // vis radiale du fourreau + // radial sleeve screw cap_xf() rotate([0, 0, collar_screw_a]) translate([tenon_r - 5, 0, -collar_h + 8]) rotate([0, 90, 0]) cylinder(d = m3_pilot, h = 6); } } -// ---------- berceau de l'accu ---------- -// Gouttière imprimée à plat (méplat sur le plateau, ouverte vers le haut), glissée dans le pied par le haut. -// Une butée à languette par bout, glissée dans une rainure, languette vers le haut ; le fil soudé dessus sort par -// l'encoche voisine et longe l'accu dans la fente du dessus (jusqu'au fourreau). Ressort « − » en bas, bossage « + » en haut. -// Repère d'impression : X = axe de l'accu (X = 0 en bas), méplat en Z = 0. +// ---------- cell cradle ---------- +// Trough printed flat (flat face on the bed, open upwards), slid into the stem from the top. +// One tab contact per end, slid into a groove, tab up; the wire soldered on it leaves through +// the nearby notch and runs along the cell in the top slot (up to the sleeve). "−" spring at the bottom, "+" bump at the top. +// Print frame: X = cell axis (X = 0 at the bottom), flat face at Z = 0. cr_r = bore_r - cr_fit; -cr_flat = cr_r * cos(45); // méplat : flancs à 45° au plus -cr_ax = cr_flat; // axe du logement au-dessus du plateau -cl_z = cr_ax + cell_up; // axe de l'accu -cr_end = 1.6 + bt_slot + 1; // bout : paroi ext. + rainure + paroi int. -cell_x0 = 1.6 + bt_t + spring_c; // bas de l'accu (ressort comprimé) +cr_flat = cr_r * cos(45); // flat: flanks at 45° at most +cr_ax = cr_flat; // bore axis above the bed +cl_z = cr_ax + cell_up; // cell axis +cr_end = 1.6 + bt_slot + 1; // end: outer wall + groove + inner wall +cell_x0 = 1.6 + bt_t + spring_c; // bottom of the cell (spring compressed) cr_len = cell_x0 + cell_l + bt_dimple + bt_t + 1.6; -// haut du berceau côté bas du breakout, dans le repère du chapeau (Z = 0 sous le disque) +// top of the cradle below the breakout, in the cap frame (Z = 0 under the disc) cradle_top = (bore_z + cr_len - stem_h) * cos(cap_tilt) + cr_r * sin(cap_tilt) - collar_h; -wire_gap = bme_z - bme_t - cradle_top; // place de la paire JST-XH -assert(wire_gap >= 8, str("Pas de place pour la paire JST-XH : ", wire_gap, " mm sous le BME280")); +wire_gap = bme_z - bme_t - cradle_top; // room for the JST-XH pair +assert(wire_gap >= 8, str("No room for the JST-XH pair: ", wire_gap, " mm under the BME280")); module cradle() { difference() { intersection() { rotate([0, 90, 0]) translate([-cr_ax, 0, 0]) cylinder(r = cr_r, h = cr_len, $fn = 96); translate([0, -cr_r, 0]) cube([cr_len, 2 * cr_r, 2 * cr_r]); } - // logement de l'accu, fente d'insertion au-dessus de son axe (+1 mm) + // cell bore, insertion slot above its axis (+1 mm) translate([cr_end, 0, cl_z]) rotate([0, 90, 0]) cylinder(d = cell_d + 0.6, h = cr_len - 2 * cr_end); translate([cr_end, -cell_d / 2 - 0.3, cl_z + 1]) cube([cr_len - 2 * cr_end, cell_d + 0.6, 2 * cr_r]); for (s = [0, 1]) { - xs = s == 0 ? 1.6 : cr_len - 1.6 - bt_slot; // rainure - // rainure de la plaque, ouverte en haut (languette vers le haut) + xs = s == 0 ? 1.6 : cr_len - 1.6 - bt_slot; // groove + // plate groove, open at the top (tab up) translate([xs, -(bt_w + 0.6) / 2, cl_z - bt_h / 2 - bt_sink]) cube([bt_slot, bt_w + 0.6, 2 * cr_r]); - // encoche du fil à côté de la languette, à travers les parois du bout, jusqu'au-dessus de l'accu : - // le fil longe ensuite l'accu dans la fente du dessus (≈3 mm sous le logement) + // wire notch next to the tab, through the end walls, up to above the cell: + // the wire then runs along the cell in the top slot (≈3 mm below the bore) translate([s == 0 ? -1 : cr_len - cr_end - 1, -(bt_w + 0.6) / 2 - wire_notch[0] + 2, cl_z + cell_d / 2 - wire_notch[1]]) cube([cr_end + 2, wire_notch[0], 2 * cr_r]); - // fenêtre devant la plaque + // window in front of the plate translate([s == 0 ? xs + bt_slot - eps : cr_len - cr_end - eps, -bt_win / 2, cl_z - bt_h / 2]) cube([1 + 2 * eps, bt_win, 2 * cr_r]); } - // fenêtres traversantes dans les flancs, sous les lèvres qui retiennent l'accu (dessus en pont) + // through windows in the flanks, under the lips holding the cell (top bridged) let(n = cr_win[0], l = cr_win[1], post = (cr_len - 2 * cr_end - 8 - n * l) / (n - 1)) for (i = [0 : n - 1]) translate([cr_end + 4 + i * (l + post), -cr_r - 1, cr_win[2]]) cube([l, 2 * cr_r + 2, cr_win[3] - cr_win[2]]); - // trou de tirage dans le bout du haut (ficelle, nœud à l'intérieur) + // pull hole in the top end (string, knot inside) translate([cr_len - cr_end - 1, 0, cl_z + cell_d / 2 + 0.8]) rotate([0, 90, 0]) cylinder(d = 2.5, h = cr_end + 2, $fn = 24); - // polarité, gravée sur les faces des bouts + // polarity, engraved on the end faces for (m = [[0, "-"], [1, "+"]]) translate([m[0] * cr_len, 0, cl_z]) mirror([m[0], 0, 0]) rotate([90, 0, -90]) translate([0, 0, -0.6]) linear_extrude(1) text(m[1], size = 8, halign = "center", valign = "center"); } } -// berceau monté : bas du berceau sur le fond du logement, méplat vers -X (côté ouvert vers +X) +// fitted cradle: bottom of the cradle on the floor of the bore, flat face towards -X (open side towards +X) module cradle_placed() { translate([0, 0, bore_z]) rotate([0, -90, 0]) translate([0, 0, -cr_ax]) children(); } -// ---------- sol : disque de 1 mm et végétation ---------- +// ---------- ground: 1 mm disc and vegetation ---------- function ground_r(a) = ground_R * (1 + ground_amp * hnorm(ground_T, ground_P, a)); -// brin : lame effilée (0.9 mm d'épaisseur, pointe 0.6 mm), courbée dans son plan, pente <= ~40° (imprimable) +// blade: tapered blade (0.9 mm thick, 0.6 mm tip), curved in its plane, slope <= ~40° (printable) module blade(h, w, lean) { for (k = [0 : 3]) hull() for (q = [k, k + 1]) translate([lean * pow(q / 4, 2), 0, h * q / 4]) cube([max(0.6, w * pow(1 - q / 4, 1.3)), 0.9, 0.01], center = true); } grass = let(R = rands(0, 1, 6 * (grass_tufts * 9 + grass_single), grass_seed)) concat( - // touffes : 6 à 9 grands brins en éventail, collés au pied + // tufts: 6 to 9 tall blades fanning out, against the stem [for (t = [0 : grass_tufts - 1], b = [0 : 8]) let(o = 6 * (t * 9 + b), t0 = 6 * t * 9, ta = t * 360 / grass_tufts + 40 * R[t0], tr = stem_rr(ta, 0) + 2 + 4 * R[t0 + 1], fan = -70 + 140 * b / 8 + 15 * (R[o + 2] - 0.5), h = 10 + 9 * R[o + 3]) if (b < 6 + 3 * R[t0 + 2]) [[tr * cos(ta) + 4 * (R[o + 4] - 0.5), tr * sin(ta) + 4 * (R[o + 5] - 0.5)], ta + fan, h, 3 + 1.5 * R[o + 1], h * 0.55 * (R[o] - 0.2)]], - // brins isolés + // single blades [for (i = [0 : grass_single - 1]) let(o = 6 * (grass_tufts * 9 + i), a = 360 * R[o], rr = stem_rr(a, 0) + 4 + (ground_r(a) - 8 - stem_rr(a, 0) - 4) * pow(R[o + 1], 1.5), h = 4 + 6 * R[o + 2]) [[rr * cos(a), rr * sin(a)], a + 90 * (R[o + 3] - 0.5), h, 2.5 + R[o + 4], h * 0.5 * (R[o + 5] - 0.5)]]); -// frange : dents triangulaires le long d'une courbe ondulée autour du pied (paroi continue de 0.9 mm) +// fringe: triangular teeth along a wavy curve around the stem (continuous 0.9 mm wall) fringe = let(R = rands(0, 1, 3 * fringe_n, grass_seed + 7)) [for (i = [0 : fringe_n - 1]) let(a = i * 360 / fringe_n, rr = stem_rr(a, 0) + 2.5 + 2 * R[3 * i]) [rr * cos(a), rr * sin(a), fringe_h[0] + (fringe_h[1] - fringe_h[0]) * R[3 * i + 1], 2 * (R[3 * i + 2] - 0.5)]]; @@ -588,8 +588,8 @@ module ground() { translate([0, 0, ground_t - 0.01]) fringe_wall(); } -// ---------- maquettes ---------- -module cradle_cell() { // berceau, accu, butées (repère d'impression) +// ---------- mock-ups ---------- +module cradle_cell() { // cradle, cell, contacts (print frame) color("#303030") cradle(); color("#2e6fd8") translate([cell_x0, 0, cl_z]) rotate([0, 90, 0]) cylinder(d = cell_d, h = cell_l); color("Silver") for (x = [1.6, cr_len - 1.6 - bt_t]) translate([x, -bt_w / 2, cl_z - bt_h / 2 - bt_sink]) { @@ -598,8 +598,8 @@ module cradle_cell() { // berceau, accu, butées (repère d'impression) } } module charger() { color("#1a4fa0") translate([0, -chg_w / 2, 0]) cube([chg_t, chg_w, chg_l]); color("Silver") translate([chg_t, -4, 0]) cube([3, 8, 5.5]); } -// BME280 enfiché sous la carte (repère : broche 1 de l'empreinte I2C, dessous de la carte) : -// femelle fh_n positions, plastique de la barrette mâle, breakout parallèle à la carte +// BME280 plugged under the board (frame: pin 1 of the I2C footprint, underside of the board): +// fh_n-position female header, male header plastic, breakout parallel to the board module bme() { color("#222") translate([-(fh_n - 0.5) * hdr_p, -hdr_p / 2, -fh_h]) cube([fh_n * hdr_p, hdr_p, fh_h]); color("#222") translate([-(bme_pins - 0.5) * hdr_p, -hdr_p / 2, -fh_h - mh_h]) cube([bme_pins * hdr_p, hdr_p, mh_h]); @@ -607,15 +607,15 @@ module bme() { color("Purple") translate([-(bme_pins - 1) * hdr_p / 2 - bme_w / 2, -bme_edge, -fh_h - mh_h - bme_t]) cube([bme_w, bme_d, bme_t]); } -// coupe radiale par la console d'angle a : console, trou pilote, vis M3 x 10, disque à lamelles -// (render() : faces de coupe propres, sinon z-fighting en aperçu ; ~4 min) +// radial section through the corbel at angle a: corbel, pilot hole, M3 x 10 screw, gill disc +// (render(): clean cut faces, otherwise z-fighting in preview; ~4 min) module console_cut(a) { r = tab_r(a); module cut() rotate([0, 0, a]) translate([r - 24, 0, -12]) cube([40, 30, 34]); rotate([0, 0, -a]) { color("Ivory") render() intersection() { gills(); cut(); } color(cap_color) render() intersection() { translate([0, 0, -cap_skirt]) cap(); cut(); } - color("Silver") rotate([0, 0, a]) translate([r, 0, 0]) render() intersection() { // autotaraudeuse, tête sous le disque + color("Silver") rotate([0, 0, a]) translate([r, 0, 0]) render() intersection() { // self-tapping, head under the disc union() { translate([0, 0, -2]) cylinder(d = 5.5, h = 2, $fn = 32); cylinder(d = 2.9, h = 10, $fn = 32); } translate([-5, 0, -5]) cube([10, 5, 20]); } @@ -640,8 +640,8 @@ module assembled() { } } -// vue éclatée, ordre de montage : accu sorti du pied, chapeau levé ; dans le chapeau, de bas en haut : -// disque, châssis + chargeur, plancher, carte + BME280, dôme +// exploded view, assembly order: cell out of the stem, cap lifted; in the cap, bottom to top: +// disc, chassis + charger, deck, board + BME280, dome module exploded(e = explode) { color("Ivory") stem(); color("#4a8f3c") translate([0, 0, -ground_t]) ground(); @@ -661,14 +661,14 @@ module exploded(e = explode) { } } -echo(str("Contour : graine ", lobe_seed, ", orientation ", rot, "°, indice de jeu ", round(worst(rot) * 100) / 100, worst(rot) <= 1 ? " (ok : >= 3 mm partout)" : " (ATTENTION : électronique à moins de 3 mm du dôme, augmenter cap_R / cap_H ou changer de graine)")); -echo(str("Pois : ", len(dots), " au total — ", [for (s = dot_sizes) str("Ø", s, " x", len([for (d = dots) if (d[2] == s) 1]))])); -echo(str("Sommet du dôme : ", [for (v = [apex[0], apex[1]]) round(v * 10) / 10], " (J1 « RADIO » en ", [for (v = radio_p) round(v * 10) / 10], "), ", round(apex[2] - cap_skirt - pcb_z - pcb_t), " mm au-dessus de la carte ; ", round(collar_h * sin(cap_tilt) + apex[0] * cos(cap_tilt) + (apex[2] - cap_skirt) * sin(cap_tilt)), " mm de l'axe du pied en X")); -echo(str("BME280 : broche 1 en ", [for (v = i2c_p1) round(v * 100) / 100], ", dessous du breakout à ", bme_z - bme_t - disc_t, " mm au-dessus du disque")); -echo(str("Hauteur ~", round(stem_h - apex[0] * sin(cap_tilt) + (apex[2] - cap_skirt + collar_h) * cos(cap_tilt) + ground_t), " mm ; chapeau Ø~", round(2 * cap_R), " ; logement accu Ø", 2 * bore_r, " ; berceau ", round(cr_len * 10) / 10, " mm (accu ", cell_l, " mm) ; berceau ", round(cradle_top), " mm au-dessus du dessous du disque, ", round(wire_gap), " mm sous le BME280 ; lest ", round(ballast_v), " cm³ ≈ ", round(ballast_v * 4.7), " g de grenaille acier")); +echo(str("Outline: seed ", lobe_seed, ", orientation ", rot, "°, clearance index ", round(worst(rot) * 100) / 100, worst(rot) <= 1 ? " (ok: >= 3 mm everywhere)" : " (WARNING: electronics less than 3 mm from the dome, raise cap_R / cap_H or change the seed)")); +echo(str("Dots: ", len(dots), " in total — ", [for (s = dot_sizes) str("Ø", s, " x", len([for (d = dots) if (d[2] == s) 1]))])); +echo(str("Dome top: ", [for (v = [apex[0], apex[1]]) round(v * 10) / 10], " (J1 \"RADIO\" at ", [for (v = radio_p) round(v * 10) / 10], "), ", round(apex[2] - cap_skirt - pcb_z - pcb_t), " mm above the board; ", round(collar_h * sin(cap_tilt) + apex[0] * cos(cap_tilt) + (apex[2] - cap_skirt) * sin(cap_tilt)), " mm from the stem axis in X")); +echo(str("BME280: pin 1 at ", [for (v = i2c_p1) round(v * 100) / 100], ", underside of the breakout ", bme_z - bme_t - disc_t, " mm above the disc")); +echo(str("Height ~", round(stem_h - apex[0] * sin(cap_tilt) + (apex[2] - cap_skirt + collar_h) * cos(cap_tilt) + ground_t), " mm; cap Ø~", round(2 * cap_R), "; cell bore Ø", 2 * bore_r, "; cradle ", round(cr_len * 10) / 10, " mm (cell ", cell_l, " mm); cradle ", round(cradle_top), " mm above the underside of the disc, ", round(wire_gap), " mm under the BME280; ballast ", round(ballast_v), " cm³ ≈ ", round(ballast_v * 4.7), " g of steel shot")); if (part == "cap") cap(); -else if (part == "gills") translate([0, 0, disc_t]) rotate([180, 0, 0]) gills(); // lamelles vers le haut +else if (part == "gills") translate([0, 0, disc_t]) rotate([180, 0, 0]) gills(); // gills up else if (part == "chassis") translate([0, 0, -ch_z]) chassis(); else if (part == "deck") deck(); else if (part == "stem") stem(); @@ -676,14 +676,14 @@ else if (part == "ground") ground(); else if (part == "cradle") cradle(); else if (part == "plug") cylinder(d = plug_d, h = plug_t); else if (part == "dot") dot(dot_d); -else if (part == "sensor") difference() { // gros plan : BME280 enfiché sous la carte +else if (part == "sensor") difference() { // close-up: BME280 plugged under the board assembled(); - translate([-100, -90.5, 70]) cube([200, 90, 150]); // la caméra doit rester hors du volume retiré + translate([-100, -90.5, 70]) cube([200, 90, 150]); // the camera must stay outside the removed volume translate([-100, -100, 70]) cube([200, 200, 30]); } else if (part == "console") console_cut(tab_a[0]); else if (part == "exploded") exploded(); -else if (part == "cradle_view") { cradle_cell(); translate([0, 40, 0]) color("Gold") cradle(); } // berceau monté et nu +else if (part == "cradle_view") { cradle_cell(); translate([0, 40, 0]) color("Gold") cradle(); } // cradle fitted and bare else if (part == "section") difference() { assembled(); translate([-200, -200, -1]) cube([400, 200, 400]); } else { assembled(); diff --git a/openscad/pcb.scad b/openscad/pcb.scad index f666a41..539b34a 100644 --- a/openscad/pcb.scad +++ b/openscad/pcb.scad @@ -1,28 +1,28 @@ -// Représentation simplifiée de la carte TinySensor v2.2 et de son accu 18650, pour les rendus. -// Positions relevées dans hardware/tinysensor/tinySensorv1.kicad_pcb (coordonnées KiCad absolues), -// volumes approximatifs (boîtes et cylindres), pas destinés à l'impression. +// Simplified TinySensor v2.2 board and its 18650 cell, for the renders. +// Positions read from hardware/tinysensor/tinySensorv1.kicad_pcb (absolute KiCad coordinates), +// approximate volumes (boxes and cylinders), not meant for printing. // -// Repère : coin bas-gauche du PCB, Y vers le haut (KiCad : Y vers le bas), dessous du PCB en Z = 0. +// Frame: bottom-left corner of the PCB, Y up (KiCad: Y down), PCB underside at Z = 0. pcb_w = 93.98; pcb_h = 63.754; pcb_t = 1.6; -k_origin = [113.04016, 48.02632]; // coin haut-gauche du contour Edge.Cuts +k_origin = [113.04016, 48.02632]; // top-left corner of the Edge.Cuts outline k_holes = [[203.75016, 105.51032], [118.04904, 52.81676], [117.61216, 105.43032], [203, 52], [150.88616, 51.83632]]; show_battery = true; -// découpe le long des pointillés de sérigraphie (x = 147, y = 87 en KiCad) : on ne garde que -// la partie sans support 18650 ni support CR2032 ; l'accu arrive alors sur J13 « RAW IN » +// cut along the silkscreen dotted lines (x = 147, y = 87 in KiCad): only the part without +// the 18650 and CR2032 holders is kept; the cell then arrives on J13 "RAW IN" k_crop = [[147, 48.02632], [207.02016, 87]]; crop_w = k_crop[1][0] - k_crop[0][0]; crop_h = k_crop[1][1] - k_crop[0][1]; -// trous restants sur la carte découpée (MH5 et MH1 du haut), coordonnées KiCad +// holes left on the cut board (MH5 and top MH1), KiCad coordinates k_crop_holes = [[150.88616, 51.83632], [203, 52]]; $fn = 32; -// Place une empreinte KiCad (position absolue, angle) sur le dessus du PCB. -// Les enfants sont décrits en coordonnées locales de l'empreinte, Y vers le haut (= -Y KiCad). +// Places a KiCad footprint (absolute position, angle) on top of the PCB. +// Children are given in the footprint's local coordinates, Y up (= -Y KiCad). module kfp(x, y, a = 0) { translate([x - k_origin[0], pcb_h - (y - k_origin[1]), pcb_t]) rotate([0, 0, a]) children(); } @@ -36,7 +36,7 @@ module radial(d, h, p, c = "#1b3d8f") { color(c) translate([p / 2, 0, 0]) cylind module axial(l = 3.6, d = 1.6, p = 7.62, c = "Tan") { color(c) translate([p / 2 - l / 2, 0, d / 2 + 0.3]) rotate([0, 90, 0]) cylinder(d = d, h = l); } module holder_18650() { - // MPD BH-18650-PC2 : fab -2.8..74.8 x -9.85..9.85, bornes à 0 et 72 + // MPD BH-18650-PC2: fab -2.8..74.8 x -9.85..9.85, terminals at 0 and 72 color("#202020") { translate([-2.8, -9.85, 0]) cube([77.6, 19.7, 1.5]); for (y = [-9.85, 9.85 - 1.5]) translate([-2.8, y, 0]) cube([77.6, 1.5, 7]); @@ -50,23 +50,23 @@ module holder_18650() { } } -// carte découpée, centrée sur l'origine (dessous du PCB en Z = 0) -module pcb_v22_cropped(jst_eh = true) { // jst_eh = false : JST « I2C » et « I2C_SPI » non posés (champignon) +// cut board, centred on the origin (PCB underside at Z = 0) +module pcb_v22_cropped(jst_eh = true) { // jst_eh = false: "I2C" and "I2C_SPI" JSTs not fitted (mushroom) translate([-(k_crop[0][0] - k_origin[0]) - crop_w / 2, -(pcb_h - (k_crop[1][1] - k_origin[1])) - crop_h / 2, 0]) intersection() { pcb_v22(holder = false, jst_eh = jst_eh); translate([k_crop[0][0] - k_origin[0], pcb_h - (k_crop[1][1] - k_origin[1]), -1]) cube([crop_w, crop_h, 60]); } } -// trous de la carte découpée dans le repère de pcb_v22_cropped() +// holes of the cut board in the pcb_v22_cropped() frame function crop_holes() = [for (h = k_crop_holes) [h[0] - (k_crop[0][0] + k_crop[1][0]) / 2, (k_crop[0][1] + k_crop[1][1]) / 2 - h[1]]]; -module pcb_v22(holder = true, jst_eh = true) { // holder = false : support 18650 non peuplé +module pcb_v22(holder = true, jst_eh = true) { // holder = false: 18650 holder not fitted color("#1f7a3a") difference() { cube([pcb_w, pcb_h, pcb_t]); for (h = k_holes) translate([h[0] - k_origin[0], pcb_h - (h[1] - k_origin[1]), -1]) cylinder(d = 2.7, h = pcb_t + 2); } - // alimentation : support 18650 et convertisseur DC/DC + // power: 18650 holder and DC/DC converter if (holder) kfp(124.72416, 99.08032) holder_18650(); kfp(147.56966, 58.78882) { pins([[1.96, 1.54], [4.5, 1.54], [7.04, 1.54]], 6); @@ -75,7 +75,7 @@ module pcb_v22(holder = true, jst_eh = true) { // holder = false : support 186 } // ATtiny84A (DIP-14) kfp(168.92, 70.12, 90) color("#111") translate([0.6, -16.5, 0.5]) cube([6.4, 17.8, 3.3]); - // bouton, condensateurs + // button, capacitors kfp(153.53016, 68.09232) { color("#444") translate([0.25, -5.25, 0]) cube([6, 6, 3.5]); color("#111") translate([3.25, -2.25, 0]) cylinder(d = 3.5, h = 4.3); @@ -86,12 +86,12 @@ module pcb_v22(holder = true, jst_eh = true) { // holder = false : support 186 kfp(155.92044, 61.60008, 180) radial(4.5, 8, 2.5, "Orange"); kfp(164.09416, 54.63032, -90) radial(4.5, 8, 2.5, "Orange"); kfp(165.5, 64, -90) color("#c8a000") translate([-1, -1, 0]) cube([7, 2, 6]); - // résistances, diodes, LED + // resistors, diodes, LED for (r = [[179.96444, 75.3934, -90], [182.36444, 75.3934, -90], [162.06216, 84.60232, 90], [185.78068, 57.99836, 90], [175.47336, 55.45836, 180]]) kfp(r[0], r[1], r[2]) axial(); for (d = [[182.58028, 58.07456, 90], [180.01996, 50.49012, -90]]) kfp(d[0], d[1], d[2]) axial(3.8, 1.9, 7.62, "#8a1c1c"); kfp(157.99816, 81.55932, 90) color("Red") translate([1.27, 0, 0]) cylinder(d = 1.8, h = 3); - // connecteurs + // connectors if (jst_eh) for (y = [83.91032, 77.11032]) kfp(199.35016, y, 180) { color("WhiteSmoke") translate([-2.5, -2.3, 0]) cube([17.5, 4.0, 6]); pins([for (i = [0 : 5]) [i * 2.5, 0]], 3); diff --git a/openscad/receiver.scad b/openscad/receiver.scad index 1611872..be3afad 100644 --- a/openscad/receiver.scad +++ b/openscad/receiver.scad @@ -1,139 +1,139 @@ -// Pupitre du récepteur tinyDecoders : Pro Micro, RXB6, OLED 0,91" 128x32. -// Unités : mm. Matière : PLA/PETG. +// tinyDecoders receiver desk enclosure: Pro Micro, RXB6, 0.91" OLED 128x32. +// Units: mm. Material: PLA/PETG. // -// Écran en façade inclinée vers l'utilisateur, prise micro-USB de la Pro Micro à l'arrière -// (alimentation 5 V + lignes RFLink), antenne hélicoïdale à l'intérieur, perçage SMA défonçable -// à l'arrière pour une embase et une antenne fouet. -// Pro Micro posée sur ses broches (8 mm, plastique compris) plantées dans deux rails percés : démontable. -// RXB6 retourné (composants en dessous, moignons des broches en l'air), en travers au-dessus de la -// Pro Micro, capot posé sur deux supports de part et d'autre de la Pro Micro, ergots + point de colle. -// Cotes marquées « À MESURER » : valeurs estimées. +// Screen in a face tilted towards the user, Pro Micro micro-USB at the rear +// (5 V supply + RFLink lines), helical antenna inside, knock-out SMA hole +// at the rear for a socket and a whip antenna. +// Pro Micro standing on its pins (8 mm, plastic included) planted in two drilled rails: removable. +// RXB6 flipped (components underneath, pin stubs up), across the box above the +// Pro Micro, shield resting on two supports on either side of the Pro Micro, ergots + a drop of glue. +// Dimensions marked "TO MEASURE": estimated values. // -// Pièces (imprimables sans support) : -// shell : coque, dessus sur le plateau (face inclinée à 40° de la verticale) x1 -// plate : fond vissé (4 vis), rails de la Pro Micro, supports du RXB6 x1 -// Export : +// Parts (printable without support): +// shell: shell, top on the bed (face tilted 40° from vertical) x1 +// plate: screwed bottom plate (4 screws), Pro Micro rails, RXB6 supports x1 +// Export: // openscad -D 'part="shell"' -o receiver_shell.stl receiver.scad // openscad -D 'part="plate"' -o receiver_plate.stl receiver.scad // -// Repère : X largeur (gauche -> droite), Y profondeur (façade Y = 0 -> arrière), Z haut, dessus du fond en Z = 0. +// Frame: X width (left -> right), Y depth (face Y = 0 -> rear), Z up, top of the plate at Z = 0. -/* [Rendu] */ +/* [Render] */ part = "assembly"; // [assembly, open, cutaway, shell, plate] -/* [Coque] */ -box_w = 48; // largeur : l'écran (38 mm) + parois et jeu -box_d = 52; // profondeur : Pro Micro contre la paroi arrière -box_h = 26; // hauteur à l'arrière : Pro Micro sur ses broches, RXB6 au-dessus, antenne hélicoïdale couchée -front_h = 10; // hauteur du bas de la façade -face_tilt = 40; // façade : inclinaison depuis la verticale (40° : se lit assis, imprimable à l'envers) +/* [Shell] */ +box_w = 48; // width: the screen (38 mm) + walls and clearance +box_d = 52; // depth: Pro Micro against the rear wall +box_h = 26; // height at the rear: Pro Micro on its pins, RXB6 above, helical antenna lying down +front_h = 10; // height of the bottom of the face +face_tilt = 40; // face: tilt from vertical (40°: readable seated, printable upside down) wall = 2; -corner_r = 2; // arrondi du profil +corner_r = 2; // profile rounding -/* [Fond] */ +/* [Plate] */ plate_t = 2; -plate_clr = 0.2; // jeu du fond encastré dans la coque -boss_d = 6; // 4 colonnes de vis dans les coins +plate_clr = 0.2; // clearance of the plate recessed in the shell +boss_d = 6; // 4 screw bosses in the corners boss_h = 8; -screw_pilot = 2.5; // vis M3 à métaux taraudant la colonne (repercer à 2.8 si elle force) +screw_pilot = 2.5; // M3 machine screw tapping the boss (drill to 2.8 if it binds) screw_clear = 3.4; -screw_head = [5.4, 3.1]; // tête cylindrique (diamètre, hauteur) mesurée, noyée dans le fond -screw_l = 6; // M3x6 : une M3x8 demanderait des colonnes plus hautes, qui toucheraient l'écran -pad_floor = 1.2; // matière sous la tête -pocket_clr = 0.4; // jeu autour de l'écran dans son logement +screw_head = [5.4, 3.1]; // cylinder head (diameter, height) measured, recessed in the plate +screw_l = 6; // M3x6: an M3x8 would need taller bosses, which would reach the screen +pad_floor = 1.2; // material under the head +pocket_clr = 0.4; // clearance around the screen in its pocket /* [Pro Micro] */ -pm_l = 33.4; // mesuré +pm_l = 33.4; // measured pm_w = 18.1; pm_t = 1.6; -pm_comp = 1.74; // composants au-dessus de la carte, hors prise (mesuré : 3.34 avec la carte) -pm_pin = 8; // broches sous la carte, plastique compris (mesuré) -pm_spacer = 2.5; // plastique des broches sous la carte -pm_row = 15.24; // écart des deux rangées (0,6") +pm_comp = 1.74; // components above the board, socket excluded (measured: 3.34 with the board) +pm_pin = 8; // pins under the board, plastic included (measured) +pm_spacer = 2.5; // pin plastic under the board +pm_row = 15.24; // spacing of the two rows (0.6") pm_pitch = 2.54; -pm_pins = 12; // broches par rangée -pm_pin_off = 1.5 + (12 - 1) * 2.54 / 2 - 33.4 / 2; // décalage des rangées vers l'arrière (+Y) / centre de la carte : 1re broche à 1.5 du bout avant (mesuré) -usb_port = [8, 3]; // prise micro-USB (largeur, hauteur mesurée), dessus de la carte -usb_over = 1.7; // dépassement de la prise au-delà du bord de la carte (mesuré) -usb_plug = [12, 8]; // ouverture dans la paroi pour la fiche surmoulée +pm_pins = 12; // pins per row +pm_pin_off = 1.5 + (12 - 1) * 2.54 / 2 - 33.4 / 2; // offset of the rows towards the rear (+Y) / board centre: 1st pin 1.5 from the front end (measured) +usb_port = [8, 3]; // micro-USB socket (width, measured height), on top of the board +usb_over = 1.7; // socket overhang beyond the board edge (measured) +usb_plug = [12, 8]; // opening in the wall for the overmoulded plug -/* [Rails de la Pro Micro] */ -rail_h = 4; // le plastique des broches repose sur le rail +/* [Pro Micro rails] */ +rail_h = 4; // the pin plastic rests on the rail rail_w = 3; -rail_hole = 1.2; // broches carrées 0.64 -rail_sink = 1.5; // trous prolongés dans le fond +rail_hole = 1.2; // 0.64 square pins +rail_sink = 1.5; // holes continued into the plate /* [RXB6] */ -rx_l = 43.1; // mesuré ; retourné, composants en dessous, bout DATA à gauche (X bas), bout ANT à droite +rx_l = 43.1; // measured; flipped, components underneath, DATA end on the left (low X), ANT end on the right rx_w = 11.7; rx_t = 1.0; -rx_y = 29.5; // bord avant de la carte -rx_gap = 1; // jeu entre les composants de la Pro Micro et le capot -rx_sh = [21, 10, 3.7]; // capot (mesuré) -rx_sh_y0 = 0.8; // bord avant de la carte -> capot (mesuré) -rx_sh_x0 = 11.7; // bout DATA -> capot (mesuré) -rx_q = [9, 4.45, 4.9]; // quartz couché au ras du bord arrière, bout DATA, de 2 à 11 (mesuré) -rx_q_x0 = 2; // bout DATA -> quartz -sup_bite = 1; // portée minimale d'un support sous le capot -ergot = [1, 1.5]; // ergot au-delà du bout du capot (épaisseur, hauteur au-dessus du support) -ant = [27.5, 5]; // antenne hélicoïdale (longueur, diamètre) mesurée, couchée au-dessus des moignons +rx_y = 29.5; // front edge of the board +rx_gap = 1; // clearance between the Pro Micro components and the shield +rx_sh = [21, 10, 3.7]; // shield (measured) +rx_sh_y0 = 0.8; // front edge of the board -> shield (measured) +rx_sh_x0 = 11.7; // DATA end -> shield (measured) +rx_q = [9, 4.45, 4.9]; // crystal lying flush with the rear edge, DATA end, from 2 to 11 (measured) +rx_q_x0 = 2; // DATA end -> crystal +sup_bite = 1; // minimum reach of a support under the shield +ergot = [1, 1.5]; // ergot beyond the shield end (thickness, height above the support) +ant = [27.5, 5]; // helical antenna (length, diameter) measured, lying above the stubs -/* [OLED 0,91" 128x32] */ -oled_l = 38; // carte (mesuré) +/* [OLED 0.91" 128x32] */ +oled_l = 38; // board (measured) oled_w = 12; oled_t = 1.2; -oled_glass = 1.9; // verre + film de protection au-dessus de la carte (épaisseur totale mesurée 3.1) -oled_aa = [22.38, 5.58]; // zone active (plan du fabricant) -oled_aa_off = 0.56; // décalage de la zone active vers la droite (côté sans broches) : verre mesuré à 6.27 du bord côté broches, zone active à 2.1 du bord du verre (plan) -oled_aa_off_y = 0.78; // décalage vers le haut de la façade (broches à gauche) : zone active à 2.43 du bord haut de la carte (plan) -oled_pin_band = 3.5; // bande des soudures des broches depuis le bord gauche de la carte (broches à 1.5 du bord) -oled_pin_relief = 1; // creux supplémentaire au-dessus des soudures -win_m = 1; -win_bevel = wall + 0.8; // biseau à 45° de la fenêtre : toute l'épaisseur au-dessus du verre // marge de la fenêtre autour de la zone active -oled_pad = 3; // surépaisseur de la façade autour du logement de l'écran +oled_glass = 1.9; // glass + protective film above the board (total thickness measured 3.1) +oled_aa = [22.38, 5.58]; // active area (maker drawing) +oled_aa_off = 0.56; // active area offset to the right (side without pins): glass measured 6.27 from the pin-side edge, active area 2.1 from the glass edge (drawing) +oled_aa_off_y = 0.78; // offset towards the top of the face (pins on the left): active area 2.43 from the top edge of the board (drawing) +oled_pin_band = 3.5; // band of the pin solder joints from the left edge of the board (pins 1.5 from the edge) +oled_pin_relief = 1; // extra recess above the solder joints +win_m = 1; // window margin around the active area +win_bevel = wall + 0.8; // 45° window bevel: the whole thickness above the glass +oled_pad = 3; // face thickening around the screen pocket -/* [Embase SMA] */ -sma_d = 6.5; // perçage de l'embase (méplat à 5.9) — À VÉRIFIER +/* [SMA socket] */ +sma_d = 6.5; // socket hole (flat at 5.9) — TO CHECK sma_flat = 5.9; -sma_skin = 0.4; // pellicule laissée côté extérieur : percer pour passer au fouet -sma_pos = [38, 20]; // X, Z sur la paroi arrière (au-dessus du RXB6) +sma_skin = 0.4; // skin left on the outside: drill through for the whip +sma_pos = [38, 20]; // X, Z on the rear wall (above the RXB6) $fn = 48; eps = 0.01; -// ---------- dérivées ---------- -face_dy = (box_h - front_h) * tan(face_tilt); // recul du haut de la façade -face_a = 90 - face_tilt; // angle de la façade depuis l'horizontale +// ---------- derived ---------- +face_dy = (box_h - front_h) * tan(face_tilt); // setback of the top of the face +face_a = 90 - face_tilt; // face angle from horizontal face_len = (box_h - front_h) / cos(face_tilt); -profile = [[0, -plate_t], [box_d, -plate_t], [box_d, box_h], [face_dy, box_h], [0, front_h]]; // parois prolongées sous Z = 0 : le fond s'y encastre -cb_h = screw_head[1] + 0.2; // lamage de la tête depuis le dessous du fond -pad_h = cb_h + pad_floor - plate_t; // plot du fond sous chaque colonne -pilot_top = -plate_t + cb_h + screw_l + 0.5; // bout de la vis + 0.5 -pad_d = screw_head[0] + 0.4 + 2.4; // plot du fond : lamage + 1.2 de paroi -boss_in = wall + plate_clr + pad_d / 2; // colonnes rentrées : plots du fond entiers, pas rognés par les parois +profile = [[0, -plate_t], [box_d, -plate_t], [box_d, box_h], [face_dy, box_h], [0, front_h]]; // walls extended below Z = 0: the plate recesses into them +cb_h = screw_head[1] + 0.2; // head counterbore from the underside of the plate +pad_h = cb_h + pad_floor - plate_t; // plate pad under each boss +pilot_top = -plate_t + cb_h + screw_l + 0.5; // screw tip + 0.5 +pad_d = screw_head[0] + 0.4 + 2.4; // plate pad: counterbore + 1.2 of wall +boss_in = wall + plate_clr + pad_d / 2; // bosses moved inward: whole plate pads, not clipped by the walls bosses = [for (x = [boss_in, box_w - boss_in], y = [max(boss_in, wall + boss_d / 2 + 1.5), box_d - boss_in]) [x, y]]; -rx_x = box_w / 2; // RXB6 centré entre les parois -rx_x0 = rx_x - rx_l / 2; // bout DATA -sh_x0 = rx_x0 + rx_sh_x0; // début du capot -sh_x1 = sh_x0 + rx_sh[0]; // fin du capot -pm_x = (sh_x0 + sh_x1) / 2; // Pro Micro centrée sous le capot : supports de part et d'autre -pm_y1 = box_d - wall - 0.3 - usb_over; // bord arrière de la carte, prise contre la paroi +rx_x = box_w / 2; // RXB6 centred between the walls +rx_x0 = rx_x - rx_l / 2; // DATA end +sh_x0 = rx_x0 + rx_sh_x0; // start of the shield +sh_x1 = sh_x0 + rx_sh[0]; // end of the shield +pm_x = (sh_x0 + sh_x1) / 2; // Pro Micro centred under the shield: supports on either side +pm_y1 = box_d - wall - 0.3 - usb_over; // rear edge of the board, socket against the wall pm_y = pm_y1 - pm_l / 2; -pm_z = rail_h + pm_spacer; // dessous de la carte +pm_z = rail_h + pm_spacer; // underside of the board usb_port_z = pm_z + pm_t + usb_port[1] / 2; -rx_z = pm_z + pm_t + pm_comp + rx_gap; // dessous du capot du RXB6 -rx_bz = rx_z + rx_sh[2]; // face composants de la carte RXB6 -ant_z = rx_bz + rx_t + 2.5 + ant[1] / 2; // axe de l'antenne : 2.5 de moignons + fils sous elle -sup_l = [sh_x0 - 0.3 - ergot[0], pm_x - pm_w / 2 - 0.3]; // support gauche (X) -sup_r = [pm_x + pm_w / 2 + 0.3, sh_x1 + 0.3 + ergot[0]]; // support droit (X) +rx_z = pm_z + pm_t + pm_comp + rx_gap; // underside of the RXB6 shield +rx_bz = rx_z + rx_sh[2]; // component face of the RXB6 board +ant_z = rx_bz + rx_t + 2.5 + ant[1] / 2; // antenna axis: 2.5 of stubs + wires under it +sup_l = [sh_x0 - 0.3 - ergot[0], pm_x - pm_w / 2 - 0.3]; // left support (X) +sup_r = [pm_x + pm_w / 2 + 0.3, sh_x1 + 0.3 + ergot[0]]; // right support (X) -// repère de la façade : origine au bas de la face extérieure, Y le long de la pente, Z sortant +// face frame: origin at the bottom of the outer face, Y along the slope, Z outwards module face_xf() { translate([0, 0, front_h]) rotate([face_a, 0, 0]) children(); } -// ---------- coque ---------- -module body(off = 0) { // off : retrait (intérieur) ; toutes les arêtes arrondies (profil convexe : minkowski rapide) +// ---------- shell ---------- +module body(off = 0) { // off: inset (inside); every edge rounded (convex profile: fast minkowski) r = max(corner_r - off, 0.5); - a = off + r; // noyau rentré de a, regonflé de r : retrait net off + a = off + r; // core inset by a, grown back by r: net inset off translate([a, 0, 0]) minkowski() { rotate([90, 0, 90]) linear_extrude(box_w - 2 * a) offset(delta = -a) polygon(profile); sphere(r, $fn = 24); @@ -145,20 +145,20 @@ module shell() { union() { difference() { body(); - // intérieur ouvert en bas + // interior open at the bottom union() { body(wall); translate([wall, wall, -plate_t - 1]) cube([box_w - 2 * wall, box_d - 2 * wall, front_h / 2 + plate_t + 1]); } } - // colonnes de vis - for (p = bosses) { // posée sur le plot du fond, pleine jusqu'aux deux parois de son coin (pas de colonne en l'air) + // screw bosses + for (p = bosses) { // resting on the plate pad, solid up to both walls of its corner (no free-standing boss) c = [p[0] < box_w / 2 ? wall - 0.5 : box_w - wall + 0.5, p[1] < box_d / 2 ? wall - 0.5 : box_d - wall + 0.5]; hull() { translate([p[0], p[1], pad_h]) cylinder(d = boss_d, h = boss_h - pad_h); translate([min(c[0], p[0]), min(c[1], p[1]), pad_h]) cube([abs(p[0] - c[0]), abs(p[1] - c[1]), boss_h - pad_h]); } } - // cônes de raccord : chaque colonne monte vers son coin de parois à 45°, plus de surplomb à l'impression (coque à l'envers) + // blending cones: each boss rises to its wall corner at 45°, no overhang when printing (shell upside down) for (p = bosses) { - c = [p[0] < box_w / 2 ? wall : box_w - wall, p[1] < box_d / 2 ? wall : box_d - wall]; // coin intérieur + c = [p[0] < box_w / 2 ? wall : box_w - wall, p[1] < box_d / 2 ? wall : box_d - wall]; // inner corner intersection() { body(); hull() { @@ -168,28 +168,28 @@ module shell() { } } } - // surépaisseur de façade autour du logement de l'écran + // face thickening around the screen pocket intersection() { body(); face_xf() translate([box_w / 2 - oled_l / 2 - 2, face_len / 2 - oled_w / 2 - 2, -wall - oled_pad]) cube([oled_l + 4, oled_w + 4, oled_pad + 1]); } } for (p = bosses) translate([p[0], p[1], pad_h - eps]) cylinder(d = screw_pilot, h = pilot_top - pad_h); - // fenêtre de l'écran et logement de la carte (verre affleurant sous la face) + // screen window and board pocket (glass flush under the face) face_xf() { translate([box_w / 2 + oled_aa_off - oled_aa[0] / 2 - win_m, face_len / 2 + oled_aa_off_y - oled_aa[1] / 2 - win_m, -wall - 1]) cube([oled_aa[0] + 2 * win_m, oled_aa[1] + 2 * win_m, wall + 2]); - // biseau à 45° sur les côtés et le haut ; bord bas droit : biseauté, il deviendrait un plafond à l'impression (coque à l'envers) + // 45° bevel on the sides and the top; bottom edge square: bevelled, it would become a ceiling when printing (shell upside down) hull() { translate([box_w / 2 + oled_aa_off - oled_aa[0] / 2 - win_m, face_len / 2 + oled_aa_off_y - oled_aa[1] / 2 - win_m, -wall - 0.8]) cube([oled_aa[0] + 2 * win_m, oled_aa[1] + 2 * win_m, eps]); translate([box_w / 2 + oled_aa_off - oled_aa[0] / 2 - win_m - win_bevel, face_len / 2 + oled_aa_off_y - oled_aa[1] / 2 - win_m, 0]) cube([oled_aa[0] + 2 * win_m + 2 * win_bevel, oled_aa[1] + 2 * win_m + win_bevel, 1]); } translate([box_w / 2 - oled_l / 2 - pocket_clr, face_len / 2 - oled_w / 2 - pocket_clr, -wall - oled_pad - 1]) cube([oled_l + 2 * pocket_clr, oled_w + 2 * pocket_clr, oled_pad + 1 - 0.8 + eps]); - // rainure des soudures des broches (à gauche, presque à hauteur du verre) : 1 mm de plus que le logement + // groove for the pin solder joints (on the left, almost as high as the glass): 1 mm deeper than the pocket translate([box_w / 2 - oled_l / 2 - pocket_clr, face_len / 2 - oled_w / 2 - pocket_clr, -wall - 0.8 - eps]) cube([oled_pin_band + pocket_clr, oled_w + 2 * pocket_clr, oled_pin_relief + eps]); } - // prise micro-USB + // micro-USB socket translate([pm_x - usb_plug[0] / 2, box_d - wall - 1, usb_port_z - usb_plug[1] / 2]) cube([usb_plug[0], wall + 2, usb_plug[1]]); - // embase SMA défonçable : perçage à méplat, pellicule côté extérieur + // knock-out SMA socket: hole with a flat, skin on the outside translate([sma_pos[0], box_d - wall - 1, sma_pos[1]]) rotate([-90, 0, 0]) intersection() { cylinder(d = sma_d, h = wall + 1 - sma_skin); translate([-sma_d / 2, -sma_flat / 2, 0]) cube([sma_d, sma_flat, wall + 1]); @@ -197,22 +197,22 @@ module shell() { } } -// ---------- fond ---------- +// ---------- plate ---------- module plate() { difference() { union() { - // fond encastré entre les parois, affleurant le bas de la coque + // plate recessed between the walls, flush with the bottom of the shell translate([wall + plate_clr, wall + plate_clr, -plate_t]) linear_extrude(plate_t) offset(r = 0.5) offset(delta = -0.5) square([box_w - 2 * (wall + plate_clr), box_d - 2 * (wall + plate_clr)]); - // rails : les broches de la Pro Micro s'y plantent, leur plastique repose dessus + // rails: the Pro Micro pins plug in, their plastic rests on top for (sx = [-1, 1]) translate([pm_x + sx * pm_row / 2 - rail_w / 2, pm_y + pm_pin_off - (pm_pins - 1) * pm_pitch / 2 - rail_w / 2, 0]) cube([rail_w, (pm_pins - 1) * pm_pitch + rail_w, rail_h]); - // plots sous les colonnes : la tête de vis s'y noie + // pads under the bosses: the screw head sinks into them intersection() { for (p = bosses) translate([p[0], p[1], 0]) cylinder(d = pad_d, h = pad_h); translate([wall + plate_clr, wall + plate_clr, 0]) cube([box_w - 2 * (wall + plate_clr), box_d - 2 * (wall + plate_clr), pad_h]); } - // supports du RXB6 : le capot repose dessus, un ergot au-delà de chaque bout du capot (+ point de colle) - // gauche : dans la bande sans quartz (avant de la carte) + // RXB6 supports: the shield rests on them, an ergot beyond each end of the shield (+ a drop of glue) + // left: in the crystal-free band (front of the board) translate([sup_l[0], rx_y + 0.5, 0]) cube([sup_l[1] - sup_l[0], rx_w - rx_q[1] - 1, rx_z]); translate([sup_l[0], rx_y + 0.5, 0]) cube([ergot[0], rx_w - rx_q[1] - 1, rx_z + ergot[1]]); translate([sup_r[0], rx_y + 0.5, 0]) cube([sup_r[1] - sup_r[0], rx_w - 1, rx_z]); @@ -222,12 +222,12 @@ module plate() { translate([pm_x + sx * pm_row / 2, pm_y + pm_pin_off + (i - (pm_pins - 1) / 2) * pm_pitch, -rail_sink]) cylinder(d = rail_hole, h = rail_h + rail_sink + 1, $fn = 12); for (p = bosses) translate([p[0], p[1], -plate_t - 1]) { cylinder(d = screw_clear, h = plate_t + pad_h + 2); - cylinder(d = screw_head[0] + 0.4, h = cb_h + 1); // lamage : tête noyée + cylinder(d = screw_head[0] + 0.4, h = cb_h + 1); // counterbore: recessed head } } } -// ---------- modules (gabarits) ---------- +// ---------- modules (gauges) ---------- module promicro() { translate([pm_x - pm_w / 2, pm_y1 - pm_l, pm_z]) { color("#1a5fa0") cube([pm_w, pm_l, pm_t]); @@ -240,7 +240,7 @@ module rxb6() { color("#2a7a3a") translate([rx_x0, rx_y, rx_bz]) cube([rx_l, rx_w, rx_t]); color("Silver") translate([sh_x0, rx_y + rx_sh_y0, rx_z]) cube([rx_sh[0], rx_sh[1], rx_sh[2]]); color("Silver") translate([rx_x0 + rx_q_x0, rx_y + rx_w - rx_q[1], rx_bz - rx_q[2]]) cube([rx_q[0], rx_q[1], rx_q[2]]); - color("Gold") translate([rx_x + rx_l / 2 - ant[0], rx_y + rx_w / 2, ant_z]) rotate([0, 90, 0]) cylinder(d = ant[1], h = ant[0]); // hélicoïdale couchée, côté ANT + color("Gold") translate([rx_x + rx_l / 2 - ant[0], rx_y + rx_w / 2, ant_z]) rotate([0, 90, 0]) cylinder(d = ant[1], h = ant[0]); // helical lying down, ANT side } module oled() { face_xf() translate([box_w / 2 - oled_l / 2, face_len / 2 - oled_w / 2, -wall - oled_t - oled_glass]) { @@ -255,24 +255,24 @@ module assembled() { promicro(); rxb6(); oled(); } -assert(rx_l + 0.6 <= box_w - 2 * wall, "RXB6 trop long entre les parois"); -assert(pm_pin - pm_spacer <= rail_h + rail_sink, "Trous des rails trop courts pour les broches"); -assert(sup_l[1] - sh_x0 >= sup_bite && sh_x1 - sup_r[0] >= sup_bite, "Supports du RXB6 hors du capot : Pro Micro plus large que le capot"); -assert(rx_bz - rx_q[2] > 0, "Quartz sur le fond"); -assert(ant_z + ant[1] / 2 < box_h - wall, "Antenne contre le dessus"); -assert(pilot_top <= boss_h, "Vis trop longue pour la colonne"); -assert(usb_port_z + usb_plug[1] / 2 < box_h - wall, "Prise USB trop haute"); -echo(str("Pupitre ", box_w, " x ", box_d, " x ", box_h + plate_t, " mm ; façade à ", face_a, "° de l'horizontale, ", round(face_len), " mm de long")); +assert(rx_l + 0.6 <= box_w - 2 * wall, "RXB6 too long between the walls"); +assert(pm_pin - pm_spacer <= rail_h + rail_sink, "Rail holes too short for the pins"); +assert(sup_l[1] - sh_x0 >= sup_bite && sh_x1 - sup_r[0] >= sup_bite, "RXB6 supports outside the shield: Pro Micro wider than the shield"); +assert(rx_bz - rx_q[2] > 0, "Crystal on the plate"); +assert(ant_z + ant[1] / 2 < box_h - wall, "Antenna against the top"); +assert(pilot_top <= boss_h, "Screw too long for the boss"); +assert(usb_port_z + usb_plug[1] / 2 < box_h - wall, "USB socket too high"); +echo(str("Enclosure ", box_w, " x ", box_d, " x ", box_h + plate_t, " mm; face at ", face_a, "° from horizontal, ", round(face_len), " mm long")); -if (part == "shell") translate([0, 0, box_h]) rotate([180, 0, 0]) shell(); // dessus sur le plateau +if (part == "shell") translate([0, 0, box_h]) rotate([180, 0, 0]) shell(); // top on the bed else if (part == "plate") translate([0, 0, plate_t]) plate(); -else if (part == "open") { // vue ouverte : coque soulevée au-dessus du fond et des cartes +else if (part == "open") { // open view: shell lifted above the plate and the boards translate([0, 0, 40]) color("#e8e2d0") shell(); color("#c8c0a8") plate(); promicro(); rxb6(); translate([0, 0, 40]) oled(); } -else if (part == "cutaway") { // coque coupée à mi-largeur : disposition intérieure +else if (part == "cutaway") { // shell cut at mid-width: inner layout color("#e8e2d0") difference() { shell(); translate([-1, -1, -1]) cube([box_w / 2 + 1, box_d + 2, box_h + 2]); } color("#c8c0a8") plate(); promicro(); rxb6(); oled(); diff --git a/openscad/sensor_shield.scad b/openscad/sensor_shield.scad index 8074f1d..cbc34c6 100644 --- a/openscad/sensor_shield.scad +++ b/openscad/sensor_shield.scad @@ -1,57 +1,57 @@ -// Abri de sonde extérieur (type Stevenson à assiettes) pour breakout BMx280 -// Unités : mm. Matière : PETG (blanc de préférence : moins d'échauffement solaire). +// Outdoor radiation shield (stacked-plate Stevenson type) for a BMx280 breakout +// Units: mm. Material: PETG (white preferably: less solar heating). // -// Pièces (toutes imprimables sans support, face supérieure sur le plateau) : -// cap : chapeau plein + bras + platine murale x1 -// plate : assiette ajourée x n_plates -// base : fond avec glissière capteur et passe-câble x1 -// clip : (option) adaptateur pour accrocher l'abri sous le boîtier principal x1 -// boulonné derrière la platine murale (2 vis M4x8 tête fraisée + écrous -// noyés), il se clipse sur la patte murale basse du boîtier, qui porte -// deux encoches latérales (tinysensor_box.scad, shield_notch = true). -// Export : +// Parts (all printable without support, top face on the bed): +// cap : solid cap + arm + wall plate x1 +// plate: louvred plate x n_plates +// base : base with sensor slide and cable pass x1 +// clip : (option) adapter to hang the shield under the main box x1 +// bolted behind the wall plate (2 M4x8 countersunk screws + sunk +// nuts), it clips onto the bottom wall tab of the box, which carries +// two side notches (tinysensor_box.scad, shield_notch = true). +// Export: // openscad -D 'part="cap"' -o shield_cap.3mf sensor_shield.scad // openscad -D 'part="plate"' -o shield_plate.3mf sensor_shield.scad // openscad -D 'part="base"' -o shield_base.3mf sensor_shield.scad // -// Assemblage : 3 tiges filetées M3 (ou vis M3 longues) traversant les entretoises, -// écrous sous le fond. Fixer le chapeau au mur AVANT d'empiler les assiettes. +// Assembly: 3 M3 threaded rods (or long M3 screws) through the spacers, +// nuts under the base. Fix the cap to the wall BEFORE stacking the plates. -/* [Rendu] */ +/* [Render] */ part = "assembly"; // [cap, plate, base, clip, assembly] -/* [Assiettes] */ -n_plates = 4; // assiettes ajourées entre chapeau et fond -plate_d = 56; // diamètre extérieur -hole_d = 26; // diamètre de la colonne centrale (glissière capteur ~19.4 de large) +/* [Plates] */ +n_plates = 4; // louvred plates between cap and base +plate_d = 56; // outer diameter +hole_d = 26; // diameter of the central column (sensor slide ~19.4 wide) plate_t = 1.6; -skirt_h = 5; // jupe : arrête la pluie oblique et le rayonnement +skirt_h = 5; // skirt: stops slanting rain and radiation skirt_t = 1.6; -gap = 10; // pas vertical entre deux assiettes (hauteur des entretoises) +gap = 10; // vertical pitch between two plates (spacer height) -/* [Assemblage] */ -post_r = 20; // rayon du cercle des entretoises +/* [Assembly] */ +post_r = 20; // radius of the spacer circle post_d = 7; -rod_d = 3.4; // passage M3 +rod_d = 3.4; // M3 clearance -/* [Fixation murale] */ -wall_gap = 12; // distance jupe / mur +/* [Wall mounting] */ +wall_gap = 12; // skirt / wall distance bracket_w = 40; bracket_t = 4; wall_screw_d = 4.5; -/* [Clip sous le boîtier — doit correspondre à la patte basse de tinysensor_box.scad] */ -ear_t = 4; // épaisseur de la patte (floor_t + 1.6) +/* [Clip under the box — must match the bottom tab of tinysensor_box.scad] */ +ear_t = 4; // tab thickness (floor_t + 1.6) ear_w = 20; -notch_y = 8; // encoche depuis le bout de la patte (shield_notch_y) -clip_gap = 6; // jeu vertical entre bout de patte et dessus du chapeau -clip_engage = 16; // longueur de patte prise dans le clip -clip_t = 4; // épaisseur de l'adaptateur -prong_w = 2.4; // largeur des branches (flexion) -lip = 3; // recouvrement de la face avant de la patte -bump_r = 0.8; // bossage qui s'engage dans l'encoche (r = 1) +notch_y = 8; // notch from the end of the tab (shield_notch_y) +clip_gap = 6; // vertical gap between tab end and top of the cap +clip_engage = 16; // tab length held in the clip +clip_t = 4; // adapter thickness +prong_w = 2.4; // prong width (flex) +lip = 3; // overlap on the front face of the tab +bump_r = 0.8; // bump engaging the notch (r = 1) -/* [Capteur — breakout 15 x 11 mm, PCB 1.6 mm] */ +/* [Sensor — 15 x 11 mm breakout, 1.6 mm PCB] */ sensor_w = 15; sensor_pcb_t = 1.6; rail_h = 16; @@ -60,8 +60,8 @@ cable_d = 6; $fn = 96; eps = 0.01; -stack_h = (n_plates + 1) * gap + plate_t; // du dessous du fond au dessus du chapeau -wall_y = plate_d / 2 + wall_gap; // plan du mur (dos de la platine) +stack_h = (n_plates + 1) * gap + plate_t; // from the underside of the base to the top of the cap +wall_y = plate_d / 2 + wall_gap; // wall plane (back of the wall plate) module posts(h) { for (a = [90, 210, 330]) rotate([0, 0, a]) translate([post_r, 0, 0]) cylinder(d = post_d, h = h); @@ -70,7 +70,7 @@ module rod_holes(h) { for (a = [90, 210, 330]) rotate([0, 0, a]) translate([post_r, 0, -eps]) cylinder(d = rod_d, h = h + 2 * eps); } -// Assiette dans son sens d'impression : dessus (usage) sur le plateau, jupe et entretoises vers +Z +// Plate in its print orientation: top (in use) on the bed, skirt and spacers towards +Z module plate_print(solid = false) { difference() { union() { @@ -88,12 +88,12 @@ module plate_print(solid = false) { module cap_print() { plate_print(solid = true); - // bras vers le mur (sur le plateau) + platine murale verticale (vers +Z = vers le bas en usage) + // arm towards the wall (on the bed) + vertical wall plate (towards +Z = downwards in use) difference() { union() { translate([-bracket_w / 2, 0, 0]) cube([bracket_w, wall_y, plate_t * 2]); translate([-bracket_w / 2, wall_y - bracket_t, 0]) cube([bracket_w, bracket_t, stack_h - plate_t]); - // goussets + // gussets for (x = [-bracket_w / 2, bracket_w / 2 - 3]) hull() { translate([x, plate_d / 2 - 2, 0]) cube([3, eps, plate_t * 2]); @@ -103,22 +103,22 @@ module cap_print() { for (z = [12, stack_h - plate_t - 12]) translate([0, wall_y + eps, z]) rotate([90, 0, 0]) { cylinder(d = wall_screw_d, h = bracket_t + 2 * eps); - cylinder(d1 = wall_screw_d, d2 = wall_screw_d + 4.4, h = 2.2); // fraisage côté capteur + cylinder(d1 = wall_screw_d, d2 = wall_screw_d + 4.4, h = 2.2); // countersink on the sensor side } } } -// Fond dans son sens d'usage (= sens d'impression) +// Base in its use orientation (= print orientation) module base_print() { difference() { cylinder(d = plate_d, h = plate_t); rod_holes(plate_t * 2); - // passe-câble sous le capteur + // cable pass under the sensor translate([0, 0, -eps]) cylinder(d = cable_d, h = plate_t * 2); // drainage for (a = [30 : 60 : 330]) rotate([0, 0, a]) translate([hole_d / 2 - 4, 0, -eps]) cylinder(d = 3, h = plate_t * 2); } - // glissière : deux rails en U qui tiennent les bords du breakout, posé verticalement + // slide: two U rails holding the edges of the breakout, standing upright for (s = [-1, 1]) translate([s * (sensor_w / 2 + 0.2) - (s > 0 ? 0 : 2), -2.5, 0]) difference() { cube([2, 5, rail_h]); @@ -127,45 +127,45 @@ module base_print() { } -// Adaptateur de clip, modélisé en coordonnées d'usage puis couché pour l'impression : -// X horizontal, Y vertical (haut), Z sortant du mur (dos sur le mur, Z = 0). +// Clip adapter, modelled in use coordinates then laid down for printing: +// X horizontal, Y vertical (up), Z out of the wall (back on the wall, Z = 0). module clip_use() { - y0 = plate_t; // bas de la platine murale - y1 = stack_h; // haut (dessus du chapeau) - ye = y1 + clip_gap; // bout de la patte du boîtier - yt = ye + clip_engage; // haut des branches - zi = ear_t + 0.3; // face intérieure des lèvres - zt = zi + lip; // épaisseur totale des branches (lèvre à 45°) + y0 = plate_t; // bottom of the wall plate + y1 = stack_h; // top (top of the cap) + ye = y1 + clip_gap; // end of the box tab + yt = ye + clip_engage; // top of the prongs + zi = ear_t + 0.3; // inner face of the lips + zt = zi + lip; // total prong thickness (45° lip) difference() { translate([-bracket_w / 2, y0, 0]) cube([bracket_w, y1 - y0, clip_t]); for (y = [y1 - 12, y0 + 12]) translate([0, y, -eps]) { cylinder(d = 4.4, h = clip_t + 2 * eps); - cylinder(d = 8.1 + 0.3, h = 3.4, $fn = 6); // écrou M4 noyé côté mur + cylinder(d = 8.1 + 0.3, h = 3.4, $fn = 6); // M4 nut sunk on the wall side } } for (sx = [-1, 1]) { - xi = sx * (ear_w / 2 + 0.2); // face intérieure de la branche + xi = sx * (ear_w / 2 + 0.2); // inner face of the prong xo = sx * (ear_w / 2 + 0.2 + prong_w); - // branche + // prong translate([min(xi, xo), y1 - 10, 0]) cube([prong_w, yt - y1 + 10, zt]); - // lèvre en biseau 45° : dessous incliné, imprimable dos au plateau ; - // elle appuie sur l'arête avant de la patte et la plaque contre le mur + // 45° bevelled lip: sloped underside, printable back on the bed; + // it presses on the front edge of the tab and holds it against the wall hull() { translate([xi - (sx > 0 ? eps : 0), ye, zi]) cube([eps, clip_engage, zt - zi]); translate([xi - sx * lip - (sx > 0 ? eps : 0), ye, zt - eps]) cube([eps, clip_engage, eps]); } - // bossage d'accroche + // snap bump translate([xi, ye + notch_y, 0]) cylinder(r = bump_r, h = zi); } } -module clip_print() { clip_use(); } // dos sur le plateau : Z d'usage = Z d'impression +module clip_print() { clip_use(); } // back on the bed: use Z = print Z if (part == "cap") cap_print(); else if (part == "plate") plate_print(); else if (part == "base") base_print(); else if (part == "clip") clip_print(); else { - // vue d'ensemble en position d'usage (Z vers le haut) + // overview in use position (Z up) color("White") base_print(); for (i = [1 : n_plates]) color("WhiteSmoke") translate([0, 0, i * gap + plate_t]) mirror([0, 0, 1]) plate_print(); color("Gainsboro") translate([0, 0, (n_plates + 1) * gap + plate_t]) mirror([0, 0, 1]) cap_print(); diff --git a/openscad/stand.scad b/openscad/stand.scad index bf7830f..11a566a 100644 --- a/openscad/stand.scad +++ b/openscad/stand.scad @@ -1,55 +1,55 @@ -// Pied de table : boîtier + abri de sonde clipsé, debout sur une table. -// Unités : mm. Matière : PETG. +// Table stand: box + clipped radiation shield, standing on a table. +// Units: mm. Material: PETG. // -// Le mât tient lieu de mur : le boîtier s'y visse par ses deux pattes murales -// (M4 tête fraisée + écrous noyés dans le dos du mât), l'abri reste clipsé sous -// la patte basse comme au mur. Le mât s'emboîte dans la semelle (1 vis M4). +// The mast stands in for the wall: the box screws onto it by its two wall tabs +// (M4 countersunk + nuts sunk in the back of the mast), the shield stays clipped under +// the bottom tab as on the wall. The mast fits into the foot (1 M4 screw). // -// Pièces (imprimables sans support) : -// mast : mât, à plat, dos sur le plateau x1 -// foot : semelle avec douille, dessous sur le plateau x1 -// Export : +// Parts (printable without support): +// mast: mast, flat, back on the bed x1 +// foot: foot with socket, underside on the bed x1 +// Export: // openscad -D 'part="mast"' -o stand_mast.3mf stand.scad // openscad -D 'part="foot"' -o stand_foot.3mf stand.scad // -// Repère = celui de tinysensor_box.scad (X droite, Y haut, Z sortant du mur, mur en Z = 0). +// Frame = the one of tinysensor_box.scad (X right, Y up, Z out of the wall, wall at Z = 0). -/* [Rendu] */ +/* [Render] */ part = "assembly"; // [mast, foot, assembly] -/* [Repris de tinysensor_box.scad / sensor_shield.scad — à garder alignés] */ +/* [Copied from tinysensor_box.scad / sensor_shield.scad — keep aligned] */ cav_w = 95.98; -ear_top = 102.154; // bout de la patte haute (cav_h + wall + flange + ear_l) -ear_holes_y = [96.154, -16.4]; // axes des trous de vis murales des pattes -shield_bottom = -80; // dessous de l'abri clipsé (oy de assembly.scad) -shield_front = 72; // avant de l'abri (wall_y + clip_t + plate_d / 2) +ear_top = 102.154; // end of the top tab (cav_h + wall + flange + ear_l) +ear_holes_y = [96.154, -16.4]; // axes of the tabs' wall screw holes +shield_bottom = -80; // underside of the clipped shield (oy of assembly.scad) +shield_front = 72; // front of the shield (wall_y + clip_t + plate_d / 2) -/* [Mât] */ +/* [Mast] */ mast_w = 30; mast_t = 6; -under_clr = 30; // dégagement sous l'abri : boucle du câble qui remonte par le fond +under_clr = 30; // clearance under the shield: loop of the cable coming up through the base -/* [Semelle] */ -foot_w = 130; // largeur (X) -foot_back = 40; // débord derrière le mât -foot_front = 90; // débord devant le mur (l'abri avance jusqu'à shield_front) +/* [Foot] */ +foot_w = 130; // width (X) +foot_back = 40; // overhang behind the mast +foot_front = 90; // overhang in front of the wall (the shield reaches shield_front) foot_t = 4; foot_r = 10; -socket_h = 25; // profondeur d'emboîtement du mât +socket_h = 25; // mast insertion depth socket_wall = 3; -fit = 0.3; // jeu mât / douille +fit = 0.3; // mast / socket clearance gusset_t = 3; -/* [Visserie] */ +/* [Screws] */ screw_d = 4.4; // M4 -nut_af = 8.4; // écrou M4 (7.9 sur plats + jeu) +nut_af = 8.4; // M4 nut (7.9 across flats + clearance) nut_h = 3.4; $fn = 48; eps = 0.01; cx = cav_w / 2; -y_bot = shield_bottom - under_clr; // pied du mât = dessus de la semelle +y_bot = shield_bottom - under_clr; // mast bottom = top of the foot y_floor = y_bot - foot_t; // table mast_l = ear_top - y_bot; socket_screw_y = y_bot + socket_h / 2; @@ -57,29 +57,29 @@ socket_screw_y = y_bot + socket_h / 2; module mast_use() { difference() { translate([cx - mast_w / 2, y_bot, -mast_t]) cube([mast_w, mast_l, mast_t]); - // vis des pattes du boîtier, écrous noyés dans le dos + // box tab screws, nuts sunk in the back for (y = ear_holes_y) translate([cx, y, -mast_t - eps]) { cylinder(d = screw_d, h = mast_t + 2 * eps); cylinder(d = nut_af / cos(30), h = nut_h, $fn = 6); } - // vis de la douille + // socket screw translate([cx, socket_screw_y, -mast_t - eps]) cylinder(d = screw_d, h = mast_t + 2 * eps); } } module foot_use() { - sx = mast_w / 2 + fit + socket_wall; // demi-largeur extérieure de la douille - z0 = -mast_t - fit - socket_wall; // dos de la douille - z1 = fit + socket_wall; // face avant de la douille + sx = mast_w / 2 + fit + socket_wall; // outer half-width of the socket + z0 = -mast_t - fit - socket_wall; // back of the socket + z1 = fit + socket_wall; // front face of the socket difference() { union() { - // semelle + // foot translate([0, y_floor, 0]) rotate([-90, 0, 0]) linear_extrude(foot_t) translate([cx - foot_w / 2, -foot_front]) offset(r = foot_r) offset(delta = -foot_r) square([foot_w, foot_front + mast_t + foot_back]); - // douille + // socket translate([cx - sx, y_bot - eps, z0]) cube([2 * sx, socket_h + eps, z1 - z0]); - // goussets avant / arrière (plan YZ) et latéraux (plan XY) + // front / rear gussets (YZ plane) and side ones (XY plane) for (g = [[z1, z1 + 30], [z0, z0 - 25]]) hull() { translate([cx - gusset_t / 2, y_bot - eps, g[0]]) cube([gusset_t, socket_h, eps]); @@ -91,15 +91,15 @@ module foot_use() { translate([cx + s * (sx + 25) - (s > 0 ? eps : 0), y_bot - eps, -mast_t / 2 - gusset_t / 2]) cube([eps, eps, gusset_t]); } } - // logement du mât + // mast pocket translate([cx - mast_w / 2 - fit, y_bot, -mast_t - fit]) cube([mast_w + 2 * fit, socket_h + eps, mast_t + 2 * fit]); - // vis traversante M4 x 20 + écrou + // through screw M4 x 20 + nut translate([cx, socket_screw_y, z0 - eps]) cylinder(d = screw_d, h = z1 - z0 + 2 * eps); } } -if (part == "mast") translate([0, 0, mast_t]) mast_use(); // dos sur le plateau -else if (part == "foot") translate([0, 0, -y_floor]) rotate([90, 0, 0]) foot_use(); // Y d'usage -> Z +if (part == "mast") translate([0, 0, mast_t]) mast_use(); // back on the bed +else if (part == "foot") translate([0, 0, -y_floor]) rotate([90, 0, 0]) foot_use(); // use Y -> Z else { color("Peru") mast_use(); color("Sienna") foot_use(); diff --git a/openscad/tinysensor_box.scad b/openscad/tinysensor_box.scad index 7cfe4d6..dfc1d31 100644 --- a/openscad/tinysensor_box.scad +++ b/openscad/tinysensor_box.scad @@ -1,88 +1,88 @@ -// TinySensor v2.2 — boîtier mural vissé avec joint -// Unités : mm. Matière visée : PETG. +// TinySensor v2.2 — screwed wall box with seal +// Units: mm. Target material: PETG. // -// Export : +// Export: // openscad -D 'part="base"' -o base.3mf tinysensor_box.scad // openscad -D 'part="lid"' -o lid.3mf tinysensor_box.scad -// Rendu boîtier ouvert (carte + accu, couvercle retourné à côté) : +// Open-box render (board + cell, lid turned over next to it): // openscad --preview -D 'part="open"' --camera=110,-230,420,110,30,0 --imgsize=1400,1000 -o rendu_ouvert.png tinysensor_box.scad // -// Repère : origine = coin "haut-gauche" du PCB vu de dessus (comme KiCad), -// X vers la droite, Y vers le bas (KiCad) converti en Y vers le haut ici. -// En pose murale, le côté "bas" (Y=0, côté support 18650) est orienté vers le sol. +// Frame: origin = "top-left" corner of the PCB seen from above (as in KiCad), +// X to the right, Y down (KiCad) turned into Y up here. +// Wall-mounted, the "bottom" side (Y=0, 18650 holder side) faces the ground. -/* [Rendu] */ +/* [Render] */ part = "assembly"; // [base, lid, assembly, open] usage = "ext"; // [int, ext] -gland_side = "left"; // [left, right, bottom] paroi du presse-étoupe (usage ext) +gland_side = "left"; // [left, right, bottom] cable gland wall (usage ext) -/* [PCB — tiré de hardware/tinysensor/tinySensorv1.kicad_pcb] */ +/* [PCB — from hardware/tinysensor/tinySensorv1.kicad_pcb] */ pcb_w = 93.98; pcb_h = 63.754; pcb_t = 1.6; -// Trous de fixation M2.5 (4 coins), coordonnées KiCad relatives au coin du PCB +// M2.5 mounting holes (4 corners), KiCad coordinates relative to the PCB corner pcb_holes_kicad = [[89.96, 3.97], [5.01, 4.79], [4.57, 57.40], [90.71, 57.48]]; /* [Volumes] */ -clr = 1.0; // jeu PCB / paroi -bay = 14; // baie câble côté bas (presse-étoupe, antenne) -standoff_h = 5; // hauteur sous PCB (pattes THT) -comp_h = 25; // composant le plus haut au-dessus du PCB (gros condensateurs) -top_clr = 2; // jeu sous le couvercle - -/* [Parois] */ -wall = 2.4; // 6 périmètres buse 0.4 +clr = 1.0; // PCB / wall clearance +bay = 14; // cable bay on the bottom side (cable gland, antenna) +standoff_h = 5; // height under the PCB (THT legs) +comp_h = 25; // tallest component above the PCB (large capacitors) +top_clr = 2; // clearance under the lid + +/* [Walls] */ +wall = 2.4; // 6 perimeters, 0.4 nozzle floor_t = 2.4; lid_t = 3; -r_out = 3; // rayon extérieur des angles +r_out = 3; // outer corner radius -/* [Joint et vis de couvercle] */ -flange = 8; // largeur du rebord extérieur -flange_t = 5; // épaisseur du rebord -groove_w = 2.2; // gorge pour cordon/joint 2 mm (TPU, mousse EPDM) +/* [Seal and lid screws] */ +flange = 8; // outer flange width +flange_t = 5; // flange thickness +groove_w = 2.2; // groove for a 2 mm cord/seal (TPU, EPDM foam) groove_d = 1.5; -rib_w = 1.2; // nervure du couvercle qui écrase le joint +rib_w = 1.2; // lid rib squeezing the seal rib_h = 1.0; -lid_screw_d_pilot = 2.5; // M3 auto-taraudeuse dans le PETG +lid_screw_d_pilot = 2.5; // M3 self-tapping in PETG lid_screw_d_clear = 3.4; -/* [Fixations] */ -pcb_screw_d_pilot = 2.1; // M2.5 auto-taraudeuse +/* [Fixings] */ +pcb_screw_d_pilot = 2.1; // M2.5 self-tapping standoff_d = 6; gland_d = 12.5; // PG7 -wall_screw_d = 4.5; // vis murale Ø4 -ear_l = 12; // dépassement de la patte au-delà du rebord -shield_notch = true; // encoches pour clipser l'abri de sonde sous le boîtier -shield_notch_y = 8; // position des encoches depuis le bout de la patte basse +wall_screw_d = 4.5; // Ø4 wall screw +ear_l = 12; // tab overhang beyond the flange +shield_notch = true; // notches to clip the radiation shield under the box +shield_notch_y = 8; // notch position from the end of the bottom tab use use $fn = 48; eps = 0.01; -// Presse-étoupe latéral : dans la baie (sous le PCB en Y), assez haut pour que -// l'écrou intérieur (PG7, 15 sur plats) passe au-dessus du bord du PCB, et -// sous le chanfrein du rebord qui le protège de la pluie. +// Side cable gland: in the bay (below the PCB in Y), high enough for the +// inner nut (PG7, 15 across flats) to clear the PCB edge, and +// under the flange chamfer that keeps the rain off. gland_z = gland_side == "bottom" ? 11.5 : 15; -gland_x = 22; // position en paroi basse : décalé de la patte murale centrale -gland_y = 8; // position en paroi latérale : milieu de la baie +gland_x = 22; // position in the bottom wall: offset from the central wall tab +gland_y = 8; // position in a side wall: middle of the bay -// ---------- dimensions dérivées ---------- +// ---------- derived dimensions ---------- cav_w = pcb_w + 2 * clr; cav_h = pcb_h + 2 * clr + bay; cav_d = standoff_h + pcb_t + comp_h + top_clr; base_h = floor_t + cav_d; -pcb_x0 = clr; // coin PCB dans le repère cavité +pcb_x0 = clr; // PCB corner in the cavity frame pcb_y0 = clr + bay; -groove_off = wall + 1.8; // axe de la gorge depuis le bord cavité -screw_off = wall + flange - 2.8; // axe des vis depuis le bord cavité +groove_off = wall + 1.8; // groove axis from the cavity edge +screw_off = wall + flange - 2.8; // screw axis from the cavity edge function holes() = [for (h = pcb_holes_kicad) [pcb_x0 + h[0], pcb_y0 + pcb_h - h[1]]]; module rrect(w, h, r) { translate([r, r]) offset(r = r) square([w - 2 * r, h - 2 * r]); } -// rectangle cavité décalé de d (d>0 vers l'extérieur) +// cavity rectangle offset by d (d>0 outwards) module ring_rect(d, r = 1) { translate([-d, -d]) rrect(cav_w + 2 * d, cav_h + 2 * d, max(r, 0.5)); } @@ -98,35 +98,35 @@ function lid_screws() = let(o = screw_off) [ module base() { difference() { union() { - // corps + // body linear_extrude(base_h) ring_rect(wall, r_out); - // rebord supérieur + // top flange translate([0, 0, base_h - flange_t]) linear_extrude(flange_t) ring_rect(wall + flange, r_out); - // chanfrein à 45° sous le rebord : imprimable sans support + // 45° chamfer under the flange: printable without support hull() { translate([0, 0, base_h - flange_t - flange]) linear_extrude(eps) ring_rect(wall, r_out); translate([0, 0, base_h - flange_t - eps]) linear_extrude(eps) ring_rect(wall + flange, r_out); } - // pattes murales (haut et bas) + // wall tabs (top and bottom) for (s = [-1, 1]) ear(s); } - // cavité + // cavity translate([0, 0, floor_t]) linear_extrude(cav_d + eps) ring_rect(0, 1); - // gorge du joint + // seal groove translate([0, 0, base_h - groove_d]) linear_extrude(groove_d + eps) groove_path(groove_off, groove_w); - // trous de vis du couvercle (borgnes) + // lid screw holes (blind) for (p = lid_screws()) translate([p[0], p[1], base_h - flange_t + 0.8]) cylinder(d = lid_screw_d_pilot, h = flange_t); - // presse-étoupe : paroi latérale (défaut) ou paroi basse + // cable gland: side wall (default) or bottom wall if (usage == "ext") { if (gland_side == "bottom") translate([gland_x, -wall - flange - eps, gland_z]) rotate([-90, 0, 0]) cylinder(d = gland_d, h = wall + flange + 2 * eps); else translate([gland_side == "left" ? -wall - eps : cav_w - eps, gland_y, gland_z]) rotate([0, 90, 0]) cylinder(d = gland_d, h = wall + 2 * eps); } - // aérations intérieures (fentes en paroi basse et haute) + // indoor vents (slots in the bottom and top walls) if (usage == "int") vents(); } - // entretoises PCB + // PCB standoffs for (h = holes()) translate([h[0], h[1], floor_t - eps]) difference() { cylinder(d = standoff_d, h = standoff_h + eps); cylinder(d = pcb_screw_d_pilot, h = standoff_h + 1); @@ -134,7 +134,7 @@ module base() { } module ear(s) { - // la patte dépasse du rebord pour laisser passer le tournevis + // the tab sticks out of the flange to let the screwdriver through reach = wall + flange + ear_l; y = s < 0 ? -reach : cav_h; hy = s < 0 ? -(wall + flange + ear_l / 2) : cav_h + wall + flange + ear_l / 2; @@ -144,7 +144,7 @@ module ear(s) { cylinder(d = wall_screw_d, h = 10); translate([0, 0, floor_t + 1.6 - 2.2]) cylinder(d1 = wall_screw_d, d2 = wall_screw_d + 4.4, h = 2.2 + eps); } - // encoches latérales de la patte basse : accroche du clip de l'abri de sonde (sensor_shield.scad, part="clip") + // side notches of the bottom tab: catch for the shield clip (sensor_shield.scad, part="clip") if (s < 0 && shield_notch) for (sx = [-1, 1]) translate([cav_w / 2 + sx * 10, -reach + shield_notch_y, -eps]) cylinder(r = 1, h = 10); } @@ -156,22 +156,22 @@ module vents() { translate([10 + i * pitch - sw / 2, yy, floor_t + 3]) cube([sw, wall + 2 * eps, sl]); } -// ---------- couvercle ---------- +// ---------- lid ---------- module lid() { difference() { linear_extrude(lid_t) ring_rect(wall + flange, r_out); for (p = lid_screws()) translate([p[0], p[1], -eps]) cylinder(d = lid_screw_d_clear, h = lid_t + 2 * eps); } - // nervure qui écrase le joint (face intérieure = z<0, retournée à l'impression) + // rib squeezing the seal (inner face = z<0, turned over for printing) translate([0, 0, -rib_h]) linear_extrude(rib_h + eps) groove_path(groove_off, rib_w); } if (part == "base") base(); -else if (part == "lid") translate([0, 0, lid_t]) mirror([0, 0, 1]) lid(); // face plate sur le plateau, nervure vers le haut +else if (part == "lid") translate([0, 0, lid_t]) mirror([0, 0, 1]) lid(); // flat face on the bed, rib up else if (part == "open") { color("DimGray") base(); translate([pcb_x0, pcb_y0, floor_t + standoff_h]) pcb_v22(); - // couvercle retourné à côté, face intérieure (nervure) visible + // lid turned over next to it, inner face (rib) visible color("Gray") translate([cav_w + 2 * (wall + flange) + 15, 0, lid_t]) mirror([0, 0, 1]) lid(); translate([cav_w / 2, -wall - flange - 25, 0]) euro2(); } diff --git a/openscad/update_3mf.py b/openscad/update_3mf.py index 04f0eb3..37af03e 100644 --- a/openscad/update_3mf.py +++ b/openscad/update_3mf.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 -"""Projets Orca de print/ (un par modèle) mis à jour depuis les .scad, sans toucher aux plaques. +"""Orca projects of print/ (one per model) updated from the .scad files, without touching the plates. -Usage : python3 update_3mf.py [projet.3mf ...] (par défaut : tous les projets de PROJECTS) +Usage: python3 update_3mf.py [project.3mf ...] (default: every project in PROJECTS) -Projet existant : chaque objet est reconnu par son nom (parts) ; son maillage est réexporté par OpenSCAD et -remplacé sur place, recentré comme le fait Orca, en gardant sa position et son orientation (le bas reste sur -le plateau). Les réglages Orca sont gardés ; ceux de settings ne sont posés que s'ils manquent. -Projet absent : il est créé avec une plaque par entrée de plates, réglages d'imprimante et de filament repris -de TEMPLATE (le projet du champignon, réglé pour l'Ender 3). +Existing project: each object is matched by its name (parts); its mesh is re-exported by OpenSCAD and +replaced in place, re-centred the way Orca does, keeping its position and orientation (the bottom stays on +the bed). Orca settings are kept; the ones in settings are only set when missing. +Missing project: it is created with one plate per entry of plates, printer and filament settings taken +from TEMPLATE (the mushroom project, set up for the Ender 3). """ import math import re @@ -22,7 +22,7 @@ from concurrent.futures import ThreadPoolExecutor from pathlib import Path -if not sys.flags.utf8_mode: # Windows : read_text/write_text en cp1252 sinon (noms accentués) +if not sys.flags.utf8_mode: # Windows: read_text/write_text in cp1252 otherwise (accented names) sys.exit(subprocess.call([sys.executable, "-X", "utf8", *sys.argv])) HERE = Path(__file__).resolve().parent @@ -30,11 +30,11 @@ OPENSCAD = (os.environ.get("OPENSCAD") or shutil.which("openscad") or sorted(Path.home().glob("bin/OpenSCAD-*.AppImage"))[-1]) TEMPLATE = PRINT / "mushroom.3mf" -BED = 220 # plateau de l'Ender 3 ; Orca espace ses plaques de 1,2 plateau -GAP = 10 # entre deux pièces d'une même plaque +BED = 220 # Ender 3 bed; Orca spaces its plates by 1.2 beds +GAP = 10 # between two parts on the same plate -# parts : nom dans Orca -> arguments OpenSCAD (pièce dans son sens d'impression) -# plates : [(nom de plaque, [pièces, une entrée par exemplaire])], pour créer le projet +# parts: name in Orca -> OpenSCAD arguments (part in its print orientation) +# plates: [(plate name, [parts, one entry per copy])], to create the project PROJECTS = { "mushroom.3mf": { "scad": "mushroom.scad", @@ -45,36 +45,36 @@ "Pois Ø6": ['part="dot"', "dot_d=6"], "Pois Ø8": ['part="dot"', "dot_d=8"], "Pois Ø10": ['part="dot"', "dot_d=10"], }, - # chapeau très haut : bord de 6 mm + brim ; sans support (les faces des consoles passent en pont en PETG, - # vérifié à l'impression du 2026-11) + # very tall cap: 6 mm edge + brim; no support (the corbel faces bridge in PETG, + # checked on the 2026-11 print) "settings": {"Chapeau": {"brim_type": "outer_only", "brim_width": "10", "sparse_infill_density": "10%"}, "Pied": {"wall_loops": "6"}}, - "add": {"Bouchon": ("Divers", (300.0, -154.0))}, # ajouté s'il manque, à gauche du plancher et du berceau - # objets d'avant le renommage (« OpenSCAD Model »), reconnus par leur fichier d'origine + "add": {"Bouchon": ("Divers", (300.0, -154.0))}, # added if missing, left of the deck and the cradle + # objects from before the renaming ("OpenSCAD Model"), matched by their source file "from_file": {"mushroom_cap": "Chapeau", "mushroom_gills": "Lamelles", "mushroom_chassis": "Châssis", "mushroom_deck": "Plancher", "mushroom_stem": "Pied", "mushroom_ground": "Sol", "mushroom_cradle": "Berceau", "mushroom_plug": "Bouchon", "mushroom_dot6": "Pois Ø6", "mushroom_dot8": "Pois Ø8", "mushroom_dot10": "Pois Ø10"}, }, - "box.3mf": { # boîtier mural : n'imprimer qu'un des deux fonds + "box.3mf": { # wall box: print only one of the two bases "scad": "tinysensor_box.scad", "parts": {"Fond extérieur": ['part="base"', 'usage="ext"'], "Fond intérieur": ['part="base"', 'usage="int"'], "Couvercle": ['part="lid"']}, "plates": [("Fond extérieur", ["Fond extérieur"]), ("Fond intérieur", ["Fond intérieur"]), ("Couvercle", ["Couvercle"])], }, - "shield.3mf": { # abri de sonde + "shield.3mf": { # radiation shield "scad": "sensor_shield.scad", "parts": {"Chapeau": ['part="cap"'], "Assiette": ['part="plate"'], "Fond": ['part="base"'], "Clip": ['part="clip"']}, "plates": [("Abri", ["Chapeau", "Fond", "Clip"]), ("Assiettes", ["Assiette"] * 4)], }, - "receiver.3mf": { # pupitre du récepteur tinyDecoders + "receiver.3mf": { # tinyDecoders receiver desk enclosure "scad": "receiver.scad", "parts": {"Coque": ['part="shell"'], "Fond": ['part="plate"']}, "plates": [("Pupitre", ["Coque", "Fond"])], }, - "stand.3mf": { # pied de table + "stand.3mf": { # table stand "scad": "stand.scad", "parts": {"Mât": ['part="mast"'], "Pied": ['part="foot"']}, "plates": [("Mât", ["Mât"]), ("Pied", ["Pied"])], @@ -99,7 +99,7 @@ def export(scad, args, out): v = [tuple(map(float, m)) for m in re.findall(r'\n' for x, y, z in v) + \ " \n \n" + "".join( @@ -111,14 +111,14 @@ def meta(el, key, value=None): m = el.find(f"metadata[@key='{key}']") if value is None: return None if m is None else m.get("value") - if m is None: # avec les autres métadonnées, avant les , + if m is None: # with the other metadata, before the , m = ET.Element("metadata", key=key) el.insert(len(el.findall("metadata")), m) m.set("value", value) def skeleton(src, plate_names): - """Projet vide : réglages d'imprimante et de filament du TEMPLATE, une plaque par nom.""" + """Empty project: printer and filament settings of the TEMPLATE, one plate per name.""" tpl = zipfile.ZipFile(TEMPLATE) for n in ("[Content_Types].xml", "Metadata/project_settings.config", "Metadata/slice_info.config"): (src / n).parent.mkdir(parents=True, exist_ok=True) @@ -150,7 +150,7 @@ def plate_origin(k, n): def layout(sizes): - """Pièces en rangées, bloc centré sur la plaque ; centres [(x, y)] relatifs au coin de la plaque.""" + """Parts in rows, block centred on the plate; centres [(x, y)] relative to the plate corner.""" rows, row, w = [], [], 0 for s in sizes: if row and w + GAP + s[0] > BED - 2 * GAP: @@ -163,7 +163,7 @@ def layout(sizes): total = sum(heights) + GAP * (len(rows) - 1) width = max(sum(s[0] for s in r) + GAP * (len(r) - 1) for r in rows) if total > BED or width > BED: - sys.exit(f"Pièces trop grandes pour une plaque de {BED} mm : {sizes}") + sys.exit(f"Parts too large for a {BED} mm plate: {sizes}") out, y = [], (BED + total) / 2 for r, h in zip(rows, heights): x = (BED - (sum(s[0] for s in r) + GAP * (len(r) - 1))) / 2 @@ -189,12 +189,12 @@ def update(path, spec): model = model_path.read_text() parts = spec["parts"] - # maillages : un export par pièce, en parallèle + # meshes: one export per part, in parallel with ThreadPoolExecutor() as ex: new = dict(zip(parts, ex.map(lambda ia: export(spec["scad"], ia[1], work / f"part{ia[0]}.3mf"), enumerate(parts.values())))) - # identification et renommage + # matching and renaming objs = {} for o in root.findall("object"): part = o.find("part") @@ -254,7 +254,7 @@ def add_object(name, plate_name, xy): add_object(name, plate_name, xy) missing = set(parts) - {nm for nm, _, _ in objs.values()} if missing: - print(f"{path.name} : absent du projet, à ajouter dans Orca : {', '.join(sorted(missing))}") + print(f"{path.name}: missing from the project, add in Orca: {', '.join(sorted(missing))}") for name, o, _ in objs.values(): for k, v in spec.get("settings", {}).get(name, {}).items(): @@ -268,15 +268,15 @@ def add_object(name, plate_name, xy): t = f.read_text() old = re.search(rf'(]*>\s*)(.*?|)', t, re.S) lo_old = min((float(z) for z in re.findall(r'z="([^"]+)"', old.group(2))), default=None) - if (f, mid) not in done: # des exemplaires peuvent partager un maillage (pois du champignon) + if (f, mid) not in done: # copies may share a mesh (mushroom dots) f.write_text(t[:old.start(2)] + new[name]["mesh"] + t[old.end(2):]) done.add((f, mid)) - # le bas de la pièce reste sur le plateau ; rotations autour de Z seulement + # the bottom of the part stays on the bed; rotations about Z only item = re.search(rf'(]*transform=")([^"]+)(")', model) m = [float(a) for a in item.group(2).split()] if max(abs(m[2]), abs(m[5]), abs(m[6]), abs(m[7])) > 1e-6 or abs(m[8] - 1) > 1e-6: - sys.exit(f"{path.name} : {name} basculé dans Orca, mise à jour refusée") - m[11] = -new[name]["lo_z"] if lo_old is None else m[11] + lo_old - new[name]["lo_z"] # objet neuf : posé + sys.exit(f"{path.name}: {name} tilted in Orca, update refused") + m[11] = -new[name]["lo_z"] if lo_old is None else m[11] + lo_old - new[name]["lo_z"] # new object: placed on the bed model = model[:item.start(2)] + " ".join(f"{a:.9g}" for a in m) + model[item.end(2):] for k, v in zip("xyz", new[name]["center"]): meta(part, "source_offset_" + k, repr(v)) @@ -285,7 +285,7 @@ def add_object(name, plate_name, xy): cfg.write(cfg_path, encoding="UTF-8", xml_declaration=True) out = work / "out.3mf" with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z: - for p in sorted(src.rglob("*"), key=lambda p: p.name != "[Content_Types].xml"): # en tête de l'archive + for p in sorted(src.rglob("*"), key=lambda p: p.name != "[Content_Types].xml"): # first in the archive if p.is_file(): z.write(p, p.relative_to(src).as_posix()) if not created: @@ -295,8 +295,8 @@ def add_object(name, plate_name, xy): counts = {} for nm, _, _ in objs.values(): counts[nm] = counts.get(nm, 0) + 1 - print(f"{path.name} : {'créé' if created else 'mis à jour'}, {len(objs)} objets {counts}" - + ("" if created else f" ; ancien fichier : {path.name}.bak")) + print(f"{path.name}: {'created' if created else 'updated'}, {len(objs)} objects {counts}" + + ("" if created else f"; previous file: {path.name}.bak")) targets = [Path(a) for a in sys.argv[1:]] or [PRINT / n for n in PROJECTS]