Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions AUDIT_TECH_DEBT.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,16 @@ Impact 2026-06-19 : **20 points** résolus (C5:20). Score cumulé : **618 points

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).

### 2026-10-25 — Restructuration de `main.cpp`

- **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*).

Impact 2026-10-25 (b) : **35 points** résolus (A2:15 + A3:20). Score cumulé : **757 points** (722 + 35).


---

Expand Down Expand Up @@ -206,8 +216,8 @@ Le plus haut score théorique est 50 ((5+5)×(6−1)). Je classe les items en qu
| ID | Item | Impact | Risque | Effort | Priorité |
|----|------|:------:|:------:|:------:|:--------:|
| A1 | **Arbre source dupliqué — risque accepté** : `TinySensor/` (projet Atmel Studio) est conservé intentionnellement pour le workflow de debug pas-à-pas et de flashage via debugWIRE/JTAG, que PlatformIO ne remplace pas complètement. Contient `main.cpp`, `bh1750.cpp/h`, `TinyI2CMaster.cpp/h` + `third_party/`. Risque résiduel : divergence silencieuse si un fix appliqué dans `src/` n'est pas reporté dans `TinySensor/main.cpp`. Mitigations recommandées : (1) commentaire croisé dans les deux `main.cpp` pointant l'un vers l'autre, (2) step CI `diff -u src/main.cpp TinySensor/main.cpp` en mode warn-only pour rendre la divergence visible. | 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()`. | 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. | 3 | 2 | 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 |
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ Protocol encoders receive a `Hal*` at construction and call only HAL methods —

Tests run on the host (`-e native_debug`, `-DNATIVE`). The test harness instantiates `TestHal_` and passes it to the encoder under test. Protocol correctness is verified by checking the `Orders` vector against expected character sequences — e.g., a zero bit must produce `{'H','D','L','P','H','D'}`. This verifies timing ordering without hardware.

Test suites: `test_oregon`, `test_lacrosseWS7000`, `test_x10`, `test_conversionTools`, `test_analogFiltering`, `test_power`, `test_led`, `test_busses`, `test_softSerial`, `test_ds18b20`, `test_x10_hal`.
Test suites: `test_oregon`, `test_lacrosseWS7000`, `test_x10`, `test_conversionTools`, `test_analogFiltering`, `test_power`, `test_led`, `test_busses`, `test_softSerial`, `test_ds18b20`, `test_x10_hal`, `test_environment`.

### `src/main.cpp`

`main()` is a single `while(1)` loop: power on sensors → read → transmit via environment encoder → power off → hibernate. Protocol and sensor selection are entirely via `#ifdef` chains (`USE_OREGON` / `USE_LACROSSE`, `USE_BMP280` / `USE_BME280` / `USE_DS18B20` / `USE_BH1750`). The `BMx280` wrapper (`include/BMx280.h`) abstracts BMP280/BME280 behind a common interface.
`main()` is a single `while(1)` loop: power on sensors → `ReadEnvironment()` fills a `Measurements` (`include/Measurements.h`) → X10 meters → luminosity → environment frame → power off → hibernate. The environment protocol is a build-time alias, `EnvironmentEncoder` (`include/EnvironmentEncoder.h`), loaded with `ApplyMeasurements()` — no virtual call. Luminosity (`LightSensor`, BH1750) always goes out through a dedicated Lacrosse encoder. Sensor selection stays via `#ifdef` (`USE_BMP280` / `USE_BME280` / `USE_DS18B20` / `USE_BH1750`); wrappers (`BMx280`, `Ds18b20`, `LightSensor`) take a `Hal*` and stub out on native builds. Details: `docs/architecture.md`, section *Main loop structure*.

### Dual source tree

Expand Down
37 changes: 32 additions & 5 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
# Architecture

_Last updated: 2026-10-25 — requirements: TECH-SERIAL-001_
_Last updated: 2026-10-25 — main loop refactor (audit A2, A3), see [Main loop structure](#main-loop-structure)_

## Component Diagram

```mermaid
graph TD
main["main.cpp (loop)"] -->|Hal*| HAL
main -->|Hal*| OregonV3
main -->|Hal*| LacrosseWS7000
main -->|"Measurements"| EnvEnc["EnvironmentEncoder (build-time alias)"]
EnvEnc -->|"USE_OREGON"| OregonV3
EnvEnc -->|"USE_LACROSSE"| LacrosseWS7000
main -->|"luminosity (always Lacrosse)"| LacrosseWS7000
main -->|Hal*| x10rf
main -->|Hal*| BMx280
main -->|Hal*| SoftSerial
main -->|Hal*| Ds18b20["Ds18b20 (1-Wire wrapper)"]
Ds18b20 -->|"1-Wire PA3"| ds18b20["ds18b20 (1-Wire driver library)"]
main -->|"I2C 0x23"| BH1750["BH1750 (I2C light sensor)"]
main -->|Hal*| LightSensor["LightSensor (BH1750 wrapper)"]
LightSensor -->|"I2C 0x23"| BH1750["BH1750 (I2C light sensor)"]
LightSensor -->|Delay30ms| HAL

HAL["Hal (interface)"] --> Attiny84aHal["Attiny84aHal (AVR)"]
HAL --> TestHal["TestHal_ (host/test)"]
Expand All @@ -41,7 +45,10 @@ graph TD
| `Ds18b20` | Wrapper class encapsulating DS18B20 1-Wire access; accepts `Hal*` at construction; exposes `Begin()`, `Convert()`, `Read()`; uses `hal->Delay1s()` between convert and read on AVR; stubs on non-AVR | TECH-SENSOR-001 |
| `SoftSerial` | Software UART for debug logging (optional, `USE_SERIAL_LOG`) | — |
| `ds18b20` | 1-Wire driver library on PA3; called only through `Ds18b20` wrapper | FUNC-SENSOR-002 |
| `BH1750` | I2C light sensor at 0x23; ONE_TIME_HIGH_RES_MODE; result in lux | FUNC-SENSOR-003 |
| `BH1750` | I2C light sensor at 0x23; ONE_TIME_HIGH_RES_MODE; result in lux; called only through `LightSensor` | FUNC-SENSOR-003 |
| `LightSensor` | Wrapper for BH1750 (`include/LightSensor.h`); `Read()` starts a one-shot measurement, waits 150 ms through the HAL, returns lux; stub on non-AVR | FUNC-SENSOR-003 |
| `Measurements` | Values of one cycle (`include/Measurements.h`): temperature, humidity, pressure, luminosity, each with a "present" flag | — |
| `EnvironmentEncoder` | Build-time alias to `OregonV3` or `LacrosseWS7000` (`include/EnvironmentEncoder.h`) + `ConfigureEncoder()` and `ApplyMeasurements()` overloads | FUNC-OREGON-012, FUNC-LACROSSE-002, FUNC-BATTERY-003 |
| `main.cpp` | Measurement loop: power on → read sensors → encode → transmit → hibernate | — |
| `AnalogFilter` | Accumulates ADC samples with configurable warm-up exclusion; returns integer floor mean | TECH-FILTER-001, TECH-FILTER-002 |
| `ConversionTools` | BCD-in-hex conversion utilities for 16-bit and 32-bit decimal values | TECH-CONVERT-001, TECH-CONVERT-002 |
Expand All @@ -56,9 +63,29 @@ graph TD
| `BMx280` | `Hal*` | constructor |
| `Ds18b20` | `Hal*` | constructor |
| `SoftSerial` | `Hal*` | constructor |
| `LightSensor` | `Hal*` | constructor |

All protocol encoders and sensor wrappers receive `Hal*` at construction. They must never access AVR registers directly.

## Main loop structure

`src/main.cpp` only orchestrates; protocol and sensor details live in the encoders and wrappers.

1. `hal.PowerOnSensors()`.
2. `ReadEnvironment()` fills a `Measurements` from the fitted sensors (`BMx280`, `Ds18b20`). A field is flagged present only if its sensor is fitted and read correctly (DS18B20: `0` means read error).
3. Battery and analog X10 meters (`BATTERY_VOLTAGE_X10_ID`, `ANALOG1_X10_ID`), suppressed when the battery is low. VCC is computed once when either is enabled.
4. Luminosity (`USE_BH1750`, battery not low): `LightSensor::Read()`, then a dedicated Lacrosse encoder sends a type-5 frame `LIGHT_FRAME_REPEAT` (2) times.
5. Environment frame (`USE_OREGON` / `USE_LACROSSE`): `ApplyMeasurements()` loads the present fields into the encoder and returns whether to send (only when a temperature is present). Oregon also gets the battery-low flag.
6. `hal.PowerOffSensors()`, `hal.Hibernate(SLEEP_TIME_IN_SECONDS)`.

Design choices:

- **Static interface, no virtual call.** The protocol is fixed at build time, so `EnvironmentEncoder` is a `typedef` and `ApplyMeasurements()` an overload per encoder. On an ATtiny84a (8 KB flash) this avoids a vtable and indirect calls while keeping a single call site in `main.cpp`.
- **Luminosity always uses Lacrosse.** Oregon v3 has no light frame, and a Lacrosse frame carrying luminosity cannot carry other fields (`LacrosseWS7000::Send()` gives luminosity priority). Targets with `USE_BH1750` therefore need `LACROSSE_ID`, whatever the environment protocol.
- **Encoders and sensors are created once** (globals or before the loop) and reconfigured each cycle. Fields not present in a cycle keep their previous value in the encoder, as before the refactor.
- **Targets without a radio protocol** (`S_05`, `devmodule`) still read their environment sensor but only transmit X10 meters: this is intended (test modules).
- Equivalence with the former `main.cpp` is checked by `test_environment`, which compares the HAL orders produced through `ApplyMeasurements()` with direct encoder calls.

## Build-time Configuration

Every physical board is a separate PlatformIO environment with its own `build_flags`. There is no runtime configuration. See `docs/environments.md` for the full table.
Expand Down
1 change: 1 addition & 0 deletions include/BMx280.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class BMx280 {
float GetTemperature() { return 0; }
float GetPressure() { return 0; }
float GetHumidity() { return 0; }
void Shutdown() {}
BMx280(Hal *hal) : hal(hal) {}
#endif

Expand Down
71 changes: 71 additions & 0 deletions include/EnvironmentEncoder.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#pragma once

/*
* Common entry point for the environment radio encoders (temperature,
* humidity, pressure).
*
* The protocol is chosen at build time (USE_OREGON / USE_LACROSSE), so the
* "interface" is static: both encoders get an ApplyMeasurements() overload
* and main.cpp uses the EnvironmentEncoder alias. No virtual table, no
* indirect call: the generated code is the same as calling the encoder
* directly.
*
* Luminosity is not handled here: Oregon has no light frame, and a Lacrosse
* frame carrying luminosity cannot carry anything else. It uses its own
* Lacrosse encoder in main.cpp, whatever the environment protocol.
*/

#include <LacrosseWS7000.h>
#include <Measurements.h>
#include <Oregon_v3.h>

/*
* Loads the available measurements into the encoder.
* Returns true when a frame must be sent, i.e. when a temperature is
* available. Fields not present keep the value of a previous cycle.
*/
inline bool ApplyMeasurements(OregonV3 &encoder, const Measurements &m,
bool lowBattery) {
if (m.Has(Measurements::TEMPERATURE)) {
encoder.SetTemperature(m.temperature);
}
if (m.Has(Measurements::HUMIDITY)) {
encoder.SetHumidity(static_cast<uint8_t>(m.humidity));
}
if (m.Has(Measurements::PRESSURE)) {
encoder.SetPressure(static_cast<uint16_t>(m.pressure));
}
if (!m.Has(Measurements::TEMPERATURE)) {
return false;
}
encoder.SetBatteryLow(lowBattery);
return true;
}

// Lacrosse WS7000 frames have no battery-low flag: lowBattery is ignored.
inline bool ApplyMeasurements(LacrosseWS7000 &encoder, const Measurements &m,
bool /* lowBattery */) {
if (m.Has(Measurements::TEMPERATURE)) {
encoder.SetTemperature(m.temperature);
}
if (m.Has(Measurements::HUMIDITY)) {
encoder.SetHumidity(m.humidity);
}
if (m.Has(Measurements::PRESSURE)) {
encoder.SetPressure(m.pressure);
}
return m.Has(Measurements::TEMPERATURE);
}

#if defined(USE_LACROSSE)
typedef LacrosseWS7000 EnvironmentEncoder;
inline void ConfigureEncoder(EnvironmentEncoder &encoder) {
encoder.SetAddress(LACROSSE_ID);
}
#elif defined(USE_OREGON)
typedef OregonV3 EnvironmentEncoder;
inline void ConfigureEncoder(EnvironmentEncoder &encoder) {
encoder.SetChannel(OREGON_CHANNEL);
encoder.SetRollingCode(OREGON_RCODE);
}
#endif
47 changes: 47 additions & 0 deletions include/LightSensor.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#pragma once
#include <Hal.h>
#include <stdint.h>

#if defined(AVR)
#include <BH1750.h>
#endif

/*
* BH1750 ambient light sensor at I2C address 0x23, read in one-shot
* high-resolution mode. Same pattern as BMx280 / Ds18b20: hardware access on
* AVR, stub on native builds.
*/
class LightSensor {
public:
explicit LightSensor(Hal *hal)
: _hal(hal)
#if defined(AVR)
,
_meter(0x23)
#endif
{
}

// Starts a one-shot measurement, waits for it, returns lux.
uint16_t Read() {
#if defined(AVR)
_meter.begin(BH1750::ONE_TIME_HIGH_RES_MODE);
for (uint8_t i = 0; i < 5; i++) { // 150 ms conversion time
_hal->Delay30ms();
}
return (uint16_t)_meter.readLightLevel();
#else
return stubLux;
#endif
}

#if !defined(AVR)
uint16_t stubLux = 0; // value returned on native builds
#endif

private:
Hal *_hal;
#if defined(AVR)
BH1750 _meter;
#endif
};
43 changes: 43 additions & 0 deletions include/Measurements.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#pragma once
#include <stdint.h>

/*
* Values read during one measurement cycle.
*
* Each field is only meaningful when its flag is set: a sensor that is not
* fitted, or that failed to read, simply leaves its flag cleared. This
* replaces the former "NOT_SET = -1000" sentinel floats.
*/
struct Measurements {
enum Field : uint8_t {
TEMPERATURE = 0x01,
HUMIDITY = 0x02,
PRESSURE = 0x04,
LUMINOSITY = 0x08,
};

float temperature = 0; // °C
float humidity = 0; // %RH
float pressure = 0; // hPa
uint16_t luminosity = 0; // lux
uint8_t present = 0; // bit field of Field

bool Has(Field field) const { return (present & field) != 0; }

void SetTemperature(float value) {
temperature = value;
present |= TEMPERATURE;
}
void SetHumidity(float value) {
humidity = value;
present |= HUMIDITY;
}
void SetPressure(float value) {
pressure = value;
present |= PRESSURE;
}
void SetLuminosity(uint16_t value) {
luminosity = value;
present |= LUMINOSITY;
}
};
Loading
Loading