~/larhrissi
Retour aux projets

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.

Pipeline d'acquisition — 1 Hz, MVP end-to-endBME280T / P / Haddr 0x76I²C100 kHzSTM32F411REbare-metal HAL+ comp. Bosch+ NDJSONUART115200 8N1receiver.pyauto-detectUSB VID/PIDJSONLappend-onlyFlask+ Chart.jsgauges + chartssuperloop main() + HAL_GetTick() — 1 ligne JSON par mesure

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/Core/Src/main.c — boucle principale
c
// 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);    }}
Tous les projets