From 86510d96d628b9dadf424756da7ca4b3dbd571f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Labrosse?= Date: Sun, 25 Oct 2026 16:52:44 +0100 Subject: [PATCH 1/2] tech: restructure main loop around Measurements and EnvironmentEncoder Audit A2 + A3. No change in transmitted frames. - Measurements (include/Measurements.h): one cycle's values with a present flag each, replacing the NOT_SET = -1000 sentinel floats. - EnvironmentEncoder (include/EnvironmentEncoder.h): build-time alias to OregonV3 or LacrosseWS7000, ConfigureEncoder() and one ApplyMeasurements() overload per encoder. Static, no virtual table. - LightSensor (include/LightSensor.h): BH1750 wrapper with Hal* and a native stub; the BH1750 and the dedicated Lacrosse light encoder are now created once instead of every cycle. Luminosity still goes out as a Lacrosse type-5 frame, twice (LIGHT_FRAME_REPEAT). - Fixes: direct _delay_ms(150) in main.cpp (now Hal::Delay30ms x5); vccMv declared only in the battery block but used by the analog block; missing native BMx280::Shutdown() stub. - test_environment: HAL orders through ApplyMeasurements() match the former direct encoder calls (Oregon, Lacrosse, battery-low flag). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RNYLnB9ATL4uFdUmr9bSfN --- include/BMx280.h | 1 + include/EnvironmentEncoder.h | 71 +++++++++ include/LightSensor.h | 47 ++++++ include/Measurements.h | 43 ++++++ src/main.cpp | 138 +++++++++-------- test/test_environment/environmentTest.cpp | 174 ++++++++++++++++++++++ 6 files changed, 404 insertions(+), 70 deletions(-) create mode 100644 include/EnvironmentEncoder.h create mode 100644 include/LightSensor.h create mode 100644 include/Measurements.h create mode 100644 test/test_environment/environmentTest.cpp diff --git a/include/BMx280.h b/include/BMx280.h index 6b36194..9f368d9 100644 --- a/include/BMx280.h +++ b/include/BMx280.h @@ -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 diff --git a/include/EnvironmentEncoder.h b/include/EnvironmentEncoder.h new file mode 100644 index 0000000..51d6e95 --- /dev/null +++ b/include/EnvironmentEncoder.h @@ -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 +#include +#include + +/* + * 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(m.humidity)); + } + if (m.Has(Measurements::PRESSURE)) { + encoder.SetPressure(static_cast(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 diff --git a/include/LightSensor.h b/include/LightSensor.h new file mode 100644 index 0000000..c657e7e --- /dev/null +++ b/include/LightSensor.h @@ -0,0 +1,47 @@ +#pragma once +#include +#include + +#if defined(AVR) +#include +#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 +}; diff --git a/include/Measurements.h b/include/Measurements.h new file mode 100644 index 0000000..0911dc2 --- /dev/null +++ b/include/Measurements.h @@ -0,0 +1,43 @@ +#pragma once +#include + +/* + * 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; + } +}; diff --git a/src/main.cpp b/src/main.cpp index b910408..c08d451 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -19,20 +19,7 @@ #include #include - -#if defined(USE_BH1750) -#include -#endif - -#if defined(USE_DS18B20) -#include -#endif - -#if defined(USE_LACROSSE) -#include -#elif defined(USE_OREGON) -#include -#endif +#include #if defined(__AVR_ATtiny84__) #include @@ -42,21 +29,36 @@ Attiny84aHal hal; TestHal_ hal; #endif +#if defined(USE_LACROSSE) or defined(USE_OREGON) +#include +#define HAS_ENVIRONMENT_ENCODER +#endif + #if defined(USE_BME280) or defined(USE_BMP280) #include BMx280 bmx280(&hal); #endif #if defined(USE_DS18B20) +#include Ds18b20 ds18b20sensor(&hal); #endif +#if defined(USE_BH1750) +// Luminosity always goes out as a Lacrosse type-5 frame: Oregon has no light +// frame, and a Lacrosse frame carrying luminosity carries nothing else. +#include +#include +LightSensor lightSensor(&hal); +LacrosseWS7000 lightEncoder(&hal); +#define LIGHT_FRAME_REPEAT 2 // sent twice for reception reliability +#endif + #if defined(BATTERY_VOLTAGE_X10_ID) or defined(ANALOG1_X10_ID) #include x10rf x10encoder(&hal, 2); #endif -#define NOT_SET (-1000.0f) #define TRANSMIT_WITH_LED(code) \ do { \ hal.LedOn(); \ @@ -81,6 +83,30 @@ static void SerialPrintInfo(const char *name, uint16_t value, static void SerialPrintInfo(const char *, uint16_t, const char *) {} #endif +// Temperature, humidity and pressure from the fitted environment sensors. +static Measurements ReadEnvironment() { + Measurements m; +#if defined(USE_BME280) || defined(USE_BMP280) + bmx280.Begin(); + hal.Delay30ms(); + m.SetTemperature(bmx280.GetTemperature()); +#if defined(USE_BME280) + m.SetHumidity(bmx280.GetHumidity()); + m.SetPressure(bmx280.GetPressure() / 100); +#endif + bmx280.Shutdown(); +#endif + +#if defined(USE_DS18B20) + ds18b20sensor.Convert(); + int16_t temp = ds18b20sensor.Read(); + if (temp != 0) { // 0 = read error + m.SetTemperature((float)temp); + } +#endif + return m; +} + int main(void) { // Initialize hardware once at startup hal.Init(); @@ -88,43 +114,25 @@ int main(void) { uint16_t batteryVoltageInMv = 0; bool lowBattery = false; -#if defined(USE_LACROSSE) - LacrosseWS7000 environmentEncoder(&hal); - environmentEncoder.SetAddress(LACROSSE_ID); -#elif defined(USE_OREGON) - OregonV3 environmentEncoder(&hal); - environmentEncoder.SetChannel(OREGON_CHANNEL); - environmentEncoder.SetRollingCode(OREGON_RCODE); +#if defined(HAS_ENVIRONMENT_ENCODER) + EnvironmentEncoder environmentEncoder(&hal); + ConfigureEncoder(environmentEncoder); +#endif + +#if defined(USE_BH1750) + lightEncoder.SetAddress(LACROSSE_ID); #endif while (1) { hal.PowerOnSensors(); - float temperature = NOT_SET; - float humidity = NOT_SET; - float pressure = NOT_SET; - -#if defined(USE_BME280) || defined(USE_BMP280) - bmx280.Begin(); - hal.Delay30ms(); - temperature = bmx280.GetTemperature(); -#if defined(USE_BME280) - humidity = bmx280.GetHumidity(); - pressure = bmx280.GetPressure() / 100; -#endif - bmx280.Shutdown(); -#endif + Measurements measurements = ReadEnvironment(); -#if defined(USE_DS18B20) - ds18b20sensor.Convert(); - int16_t temp = ds18b20sensor.Read(); - if (temp != 0) { - temperature = (float)temp; - } +#if defined(BATTERY_VOLTAGE_X10_ID) or defined(ANALOG1_X10_ID) + uint16_t vccMv = hal.ComputeVccMv(INTERNAL_1v1); #endif #if defined(BATTERY_VOLTAGE_X10_ID) - uint16_t vccMv = hal.ComputeVccMv(INTERNAL_1v1); #if defined(BATTERY_IS_VCC) batteryVoltageInMv = vccMv; #else @@ -154,40 +162,30 @@ int main(void) { #if defined(USE_BH1750) if (!lowBattery) { - LacrosseWS7000 lightEncoder(&hal); - lightEncoder.SetAddress(LACROSSE_ID); - BH1750 lightMeter(0x23); - lightMeter.begin(BH1750::ONE_TIME_HIGH_RES_MODE); - _delay_ms(150); - uint16_t lux = (uint16_t)lightMeter.readLightLevel(); - SerialPrintInfo("Luminosity", lux, " Lux"); - lightEncoder.SetLuminosity(lux); - TRANSMIT_WITH_LED(lightEncoder.Send()); - TRANSMIT_WITH_LED(lightEncoder.Send()); + measurements.SetLuminosity(lightSensor.Read()); + SerialPrintInfo("Luminosity", measurements.luminosity, " Lux"); + lightEncoder.SetLuminosity(measurements.luminosity); + for (uint8_t i = 0; i < LIGHT_FRAME_REPEAT; i++) { + TRANSMIT_WITH_LED(lightEncoder.Send()); + } } #endif -#if defined(USE_LACROSSE) or defined(USE_OREGON) - if (temperature != NOT_SET) { - environmentEncoder.SetTemperature(temperature); +#if defined(HAS_ENVIRONMENT_ENCODER) + bool sendEnvironment = + ApplyMeasurements(environmentEncoder, measurements, lowBattery); + + if (measurements.Has(Measurements::TEMPERATURE)) { + SerialPrintInfo("Temp.", measurements.temperature * 10, " °C"); } - if (humidity != NOT_SET) { - environmentEncoder.SetHumidity(humidity); + if (measurements.Has(Measurements::HUMIDITY)) { + SerialPrintInfo("RH ", measurements.humidity, " %"); } - if (pressure != NOT_SET) { - environmentEncoder.SetPressure(pressure); + if (measurements.Has(Measurements::PRESSURE)) { + SerialPrintInfo("Pressure ", measurements.pressure, " hPA"); } -#if defined(USE_SERIAL_LOG) - SerialPrintInfo("Temp.", temperature * 10, " °C"); - SerialPrintInfo("RH ", humidity, " %"); - SerialPrintInfo("Pressure ", pressure, " hPA"); -#endif - - if (temperature != NOT_SET) { -#if defined(USE_OREGON) - environmentEncoder.SetBatteryLow(lowBattery); -#endif + if (sendEnvironment) { TRANSMIT_WITH_LED(environmentEncoder.Send()); } #endif diff --git a/test/test_environment/environmentTest.cpp b/test/test_environment/environmentTest.cpp new file mode 100644 index 0000000..4f756c6 --- /dev/null +++ b/test/test_environment/environmentTest.cpp @@ -0,0 +1,174 @@ +/* + * This file is part of the TinySensor distribution + * (https://github.com/arcadien/TinySensor) + * + * Copyright (c) 2019 Aurélien Labrosse + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#if defined(NATIVE) + +#include +#include +#include +#include +#include +#include + +/* + * ApplyMeasurements() must drive the encoders exactly as main.cpp did before + * the refactor. Each test builds a reference encoder with the former direct + * calls, sends both frames and compares the recorded HAL orders. + */ + +static std::vector SendAndCapture(OregonV3 &encoder) { + TestHal.ClearOrders(); + encoder.Send(); + return TestHal.Orders; +} +static std::vector SendAndCapture(LacrosseWS7000 &encoder) { + TestHal.ClearOrders(); + encoder.Send(); + return TestHal.Orders; +} + +static void AssertSameOrders(const std::vector &expected, + const std::vector &actual) { + TEST_ASSERT_EQUAL_MESSAGE(expected.size(), actual.size(), + "Frames differ in length"); + TEST_ASSERT_EQUAL_CHAR_ARRAY(expected.data(), actual.data(), expected.size()); +} + +// @req FUNC-OREGON-012 +void Expect_oregon_frame_unchanged_for_full_measurements() { + Measurements m; + m.SetTemperature(21.5f); + m.SetHumidity(48.7f); + m.SetPressure(1013.4f); + + OregonV3 reference(&TestHal); + reference.SetChannel(1); + reference.SetRollingCode(0x20); + reference.SetTemperature(21.5f); + reference.SetHumidity(48.7f); // former implicit float -> uint8_t + reference.SetPressure(1013.4f); + reference.SetBatteryLow(false); + + OregonV3 encoder(&TestHal); + encoder.SetChannel(1); + encoder.SetRollingCode(0x20); + TEST_ASSERT_TRUE(ApplyMeasurements(encoder, m, false)); + + AssertSameOrders(SendAndCapture(reference), SendAndCapture(encoder)); +} + +// @req FUNC-BATTERY-003 +void Expect_oregon_battery_low_flag_applied_with_temperature() { + Measurements m; + m.SetTemperature(-3.2f); + + OregonV3 reference(&TestHal); + reference.SetTemperature(-3.2f); + reference.SetBatteryLow(true); + + OregonV3 encoder(&TestHal); + TEST_ASSERT_TRUE(ApplyMeasurements(encoder, m, true)); + + AssertSameOrders(SendAndCapture(reference), SendAndCapture(encoder)); +} + +// @req FUNC-OREGON-012 +void Expect_no_environment_frame_without_temperature() { + Measurements m; + m.SetHumidity(50.0f); + + OregonV3 oregon(&TestHal); + LacrosseWS7000 lacrosse(&TestHal); + TEST_ASSERT_FALSE(ApplyMeasurements(oregon, m, false)); + TEST_ASSERT_FALSE(ApplyMeasurements(lacrosse, m, false)); +} + +// @req FUNC-LACROSSE-002 +void Expect_lacrosse_frame_unchanged_for_full_measurements() { + Measurements m; + m.SetTemperature(18.3f); + m.SetHumidity(62.1f); + m.SetPressure(998.6f); + + LacrosseWS7000 reference(&TestHal); + reference.SetAddress(LacrosseWS7000::Address::THREE); + reference.SetTemperature(18.3f); + reference.SetHumidity(62.1f); + reference.SetPressure(998.6f); + + LacrosseWS7000 encoder(&TestHal); + encoder.SetAddress(LacrosseWS7000::Address::THREE); + TEST_ASSERT_TRUE(ApplyMeasurements(encoder, m, true)); + + AssertSameOrders(SendAndCapture(reference), SendAndCapture(encoder)); +} + +// @req FUNC-LACROSSE-002 +void Expect_lacrosse_temperature_only_frame_unchanged() { + Measurements m; + m.SetTemperature(5.0f); + + LacrosseWS7000 reference(&TestHal); + reference.SetAddress(LacrosseWS7000::Address::ONE); + reference.SetTemperature(5.0f); + + LacrosseWS7000 encoder(&TestHal); + encoder.SetAddress(LacrosseWS7000::Address::ONE); + TEST_ASSERT_TRUE(ApplyMeasurements(encoder, m, false)); + + AssertSameOrders(SendAndCapture(reference), SendAndCapture(encoder)); +} + +// @req FUNC-SENSOR-003 +void Expect_measurements_track_present_fields() { + Measurements m; + TEST_ASSERT_FALSE(m.Has(Measurements::TEMPERATURE)); + TEST_ASSERT_FALSE(m.Has(Measurements::LUMINOSITY)); + + m.SetLuminosity(1234); + TEST_ASSERT_TRUE(m.Has(Measurements::LUMINOSITY)); + TEST_ASSERT_FALSE(m.Has(Measurements::TEMPERATURE)); + TEST_ASSERT_EQUAL_UINT16(1234, m.luminosity); +} + +// @req FUNC-SENSOR-003 +void Expect_light_sensor_stub_returns_configured_lux() { + LightSensor sensor(&TestHal); + sensor.stubLux = 742; + TEST_ASSERT_EQUAL_UINT16(742, sensor.Read()); +} + +void setUp(void) { + TestHal.ClearOrders(); +} +void tearDown(void) {} + +int main(int, char **) { + UnityBegin("environmentTest.cpp"); + + RUN_TEST(Expect_oregon_frame_unchanged_for_full_measurements); + RUN_TEST(Expect_oregon_battery_low_flag_applied_with_temperature); + RUN_TEST(Expect_no_environment_frame_without_temperature); + RUN_TEST(Expect_lacrosse_frame_unchanged_for_full_measurements); + RUN_TEST(Expect_lacrosse_temperature_only_frame_unchanged); + RUN_TEST(Expect_measurements_track_present_fields); + RUN_TEST(Expect_light_sensor_stub_returns_configured_lux); + + return UNITY_END(); +} +#endif From bd876512d0c9ee37fd1ed89587e60f72a59f2e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Labrosse?= Date: Sun, 25 Oct 2026 16:52:44 +0100 Subject: [PATCH 2/2] arch: document main loop structure, close audit A2/A3 docs/architecture.md: component diagram (EnvironmentEncoder, LightSensor, Measurements), responsibilities, and a Main loop structure section with the design choices. CLAUDE.md and audit updated. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RNYLnB9ATL4uFdUmr9bSfN --- AUDIT_TECH_DEBT.md | 14 ++++++++++++-- CLAUDE.md | 4 ++-- docs/architecture.md | 37 ++++++++++++++++++++++++++++++++----- 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/AUDIT_TECH_DEBT.md b/AUDIT_TECH_DEBT.md index f856b74..4afdd38 100644 --- a/AUDIT_TECH_DEBT.md +++ b/AUDIT_TECH_DEBT.md @@ -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). + --- @@ -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 | diff --git a/CLAUDE.md b/CLAUDE.md index 0c81003..90f378e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/docs/architecture.md b/docs/architecture.md index 94370e0..5bf6618 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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)"] @@ -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 | @@ -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.