Projet personnel
Sensor Node STM32 — T/H/P + dashboard temps réel
Firmware bare-metal STM32F411RE qui lit un BME280 en I²C, applique la compensation Bosch entière, et stream du NDJSON vers un dashboard Flask + Chart.js.
- C
- STM32 HAL
- STM32F411RE
- BME280
- I²C
- UART
- Python 3
- pyserial
- Flask
- Chart.js
Problème
Lire un capteur BME280 proprement (compensation conforme datasheet), exposer les mesures en temps réel, et faire le pipeline jusqu'au navigateur — sans empiler une stack disproportionnée pour un MVP.
Architecture
Le firmware tourne en bare-metal sur STM32F411RE Nucleo. Modules séparés (bme280, i2c_scan, protocol, uart_log) avec API claires. Le main() est un superloop 1 Hz piloté par HAL_GetTick() : lecture BME280 en I²C → compensation Bosch → encodage NDJSON → envoi sur UART2. Côté PC, receiver.py lit le port série, append chaque ligne à readings.jsonl, et une mini-app Flask sert un dashboard Chart.js qui se rafraîchit en live.
Points clés
- STM32F411RE Nucleo, Cortex-M4F @ 84 MHz (HSI + PLL)
- BME280 sur I²C 100 kHz (PB6 / PB7), adresse 0x76
- Superloop 1 Hz piloté par HAL_GetTick()
- Compensation Bosch en entier 32 bits → °C, %RH, hPa
- NDJSON sur UART2 115200 8N1, transporté en USB-CDC via le VCP ST-Link
- Receiver Python qui auto-détecte le port via USB VID/PID
- Dashboard Flask + Chart.js : gauges, courbes, indicateur online / stale / offline
- Stockage append-only readings.jsonl
Décisions techniques
01.Bare-metal HAL plutôt que RTOS
Une seule tâche périodique, deux périphériques. Un RTOS introduirait de la complexité sans bénéfice. Le superloop fait le job, le code reste linéaire et lisible.
02.Timing par HAL_GetTick() au lieu d'un timer ISR
À 1 Hz, le déclenchement par SysTick (HAL_GetTick) est largement suffisant et garde le code dans le main(). Un timer matériel + ISR serait du sur-engineering ici.
03.Compensation Bosch en entiers 32 bits
La datasheet fournit deux versions des formules : flottants et entiers. Choix de la version entière pour éviter de tirer libm et soft-FPU dans le flot critique. Le résultat final est exposé en float dans le NDJSON pour la lisibilité.
04.I²C en polling
Le BME280 est lu une fois par seconde. Les API HAL bloquantes (Transmit / Receive) suffisent. Pas de DMA, pas de callbacks I²C — économie de complexité réelle.
05.NDJSON, ligne par ligne
Une JSON par ligne, terminée par \r\n. Lisible directement dans un terminal série pour debug, parseable trivialement côté PC, et robuste : si une ligne est corrompue par un glitch UART, on perd une mesure et pas plus.
06.Auto-détection du port via USB VID/PID
Pas de COMx hardcodé. Le receiver scanne les ports, repère le couple VID/PID du ST-Link et s'y connecte tout seul. Branche-débranche-rebranche sans toucher au code.
07.Dashboard Flask 1 fichier + Chart.js
Un Flask de trois routes et une page HTML qui parle directement à Chart.js. Pas de framework JS, pas de bundler, pas de DB. Cycle d'itération rapide, parfait pour un MVP.
Extrait de code
// firmware main() — superloop 1 Hz (extrait réel)while (1) { if ((int32_t)(HAL_GetTick() - next_tick) >= 0) { next_tick += CFG_SAMPLE_PERIOD_MS; bme280_status_t rs = bme280_read(&data); if (rs != BME280_OK) { LOG_ERROR("bme280_read failed: %d", rs); continue; } int n = proto_build_reading(jsonbuf, sizeof(jsonbuf), &data); if (n < 0 || (size_t)n >= sizeof(jsonbuf)) { LOG_ERROR("Encoding error or truncated"); continue; } HAL_UART_Transmit(&huart2, (uint8_t *)jsonbuf, (uint16_t)n, 100); }}