Université Savoie Mont-Blanc — Licence 3 ESET
Maram Mezlini & Benjamin Avocat-Maulaz
- Introduction
- Part 1 - Hardware
- Part 2 - AI Models
- Part 3 - LoRaWAN
- Part 4 - Power Consumption
- Part 5 - Conclusion
- Repository Structure
- Complete Project Implementation
- Dependencies
MetAI is an embedded AI project that runs a weather prediction model directly on an ultra-low-power STM32U545 microcontroller. Using onboard sensors (temperature, humidity, pressure), the system infers the current weather condition locally — no cloud compute required — and transmits the result over LoRaWAN for remote monitoring. The project demonstrates that meaningful AI inference can coexist with strict energy budgets, making it relevant for battery-operated or energy-harvesting IoT nodes.
The brain of the project is the STM32U545, a member of STMicroelectronics' ultra-low-power STM32U5 family. Key characteristics relevant to this project:
| Feature | Value |
|---|---|
| Core | Arm Cortex-M33, up to 160 MHz |
| Flash | 1 MB |
| RAM | 786 KB (SRAM1 + SRAM2 + ICACHE) |
| Supply voltage | 1.71 V - 3.6 V |
| Low-power modes | Stop 0/1/2/3, Standby, Shutdown |
| Neural-ART Accelerator | Hardware MAC units for AI inference |
| Development board | NUCLEO-U545RE-Q |
The U545's Neural-ART Accelerator is what makes on-device AI inference viable at milliwatt-level power: it offloads the multiply-accumulate operations of the neural network from the CPU, dramatically reducing inference time and energy per prediction.
The U545 board is connected to an IKS01A3 extension board. It carries all kind of sensors such as :
- temperature (°C)
- relative humidity (%)
- barometric pressure (hPa) The three inputs fed to the AI model.
Handles the LoRaWAN radio link (see Part 3).
Both models were trained in Python (TensorFlow/Keras) on historical meteorological data sourced via Meteostat, using a weather station near Le Bourget du Lac, France. They take three scalar inputs:
| Input | Unit |
|---|---|
| Temperature | °C |
| Relative humidity | % |
| Barometric pressure | hPa |
The first and simpler model answers a single question: will it rain? It outputs a sigmoid probability and is thresholded at 0.5 to produce a binary label. This model is extremely lightweight and was used as a baseline to validate the implementation of an AI model on the board.
The second model extends the output to 12 weather classes, enabling a richer description of conditions. After training and quantization, it is converted to a TFLite FlatBuffer and deployed on the U545 using STM32Cube.AI with the Neural-ART runtime.
The 12 predicted classes are:
| # | French label | Description | International |
|---|---|---|---|
| 0 | Clair / ensoleillé | Clear sky | ☀️ |
| 1 | Peu nuageux | Mostly sunny | 🌤️ |
| 2 | Partiellement nuageux | Partly cloudy | ⛅ |
| 3 | Nuageux / couvert | Overcast | ☁️ |
| 4 | Pluie | Rain | 🌧️ |
| 5 | Averses | Showers | 🌦️ |
| 6 | Neige | Snow | ❄️ |
| 7 | Neige légère / averses de neige | Light snow / snow showers | 🌨️ |
| 8 | Pluie et neige mêlées | Sleet | 🌨️🌧️ |
| 9 | Orage | Thunderstorm | ⛈️ |
| 10 | Brouillard / brume | Fog / mist | 🌫️ |
| 11 | Vent fort | Strong wind | 💨 |
| 12 | Orage violent | Severe thunderstorm | 🌩️ |
The firmware reads the argmax of the softmax output and encodes both the class index (predicted_class) and its French label (prediction_fr) into the LoRaWAN uplink payload.
This model does not deliver the highest possible accuracy: predicting upcoming weather conditions from only three instantaneous meteorological measurements, without temporal context, is inherently challenging. A key improvement would be to include additional temporal features (for example, measurements from the previous hour) to increase predictive reliability. Due to project time constraints, we could not fully validate this approach and therefore kept the current model:
LoRa is a spread-spectrum radio modulation developed by Semtech, designed for low-power, long-range communication (up to tens of kilometres in open terrain). LoRaWAN is the MAC layer protocol built on top of LoRa that defines how devices connect to a network of gateways and route packets to an application server. Its key properties for embedded IoT are:
- Very low transmit power (typically 14–20 dBm)
- Extremely low device power budget — devices can run for years on a battery
- Star-of-stars topology: end-nodes → gateways → Network Server (e.g. TTN) → Application Server
On the STM32U545, the uplink payload is encoded as a compact binary frame carrying:
- Temperature (signed 16-bit, ×100 for two-decimal precision)
- Humidity (unsigned 8-bit, integer %)
- Pressure (unsigned 16-bit, integer hPa)
- Predicted class index (unsigned 8-bit)
On The Things Network (TTN), a JavaScript Payload Formatter (uplink codec) decodes this binary frame back into a structured JSON object:
function decodeUplink(input) {
var classesFr = [
"Clair / ensoleille",
"Peu nuageux",
"Partiellement nuageux",
"Nuageux / couvert",
"Pluie",
"Averses",
"Neige",
"Neige legere / averses de neige",
"Pluie et neige melees",
"Orage",
"Brouillard / brume",
"Vent fort",
"Orage violent"
];
var text = "";
for (var i = 0; i < input.bytes.length; i++) {
text += String.fromCharCode(input.bytes[i]);
}
var data = {};
var parts = text.split(",");
for (var j = 0; j < parts.length; j++) {
var kv = parts[j].split("=");
if (kv.length !== 2) continue;
var key = kv[0].trim();
var raw = kv[1].trim();
if (key === "C") {
var cls = parseInt(raw, 10);
if (!isNaN(cls)) {
data.predicted_class = cls;
data.prediction_fr =
(cls >= 0 && cls < classesFr.length) ? classesFr[cls] : "Classe inconnue";
}
continue;
}
var val = parseFloat(raw);
if (isNaN(val)) continue;
if (key === "P") data.pressure_hpa = val;
else if (key === "H") data.humidity_percent = val;
else if (key === "T") data.temperature_deg_c = val;
else data[key] = val;
}
return {
data: data
};
}
Once decoded by TTN, the data is forwarded to a Node-RED flow that performs an HTTP POST to a Request Baskets endpoint. This makes the payload immediately inspectable from a browser, as shown in the screenshot below, and provides a convenient webhook URL that any downstream service can subscribe to.
Note on the network side: Routing, cloud dashboards, and persistent storage fall outside our electronics/embedded speciality, so we kept the network stack intentionally minimal and modular. In practice, Node-RED already emits a complete JSON payload (device_id, timestamp, sensors, prediction), so the ingestion layer can be swapped without touching the STM32 firmware. We currently run a lightweight Docker setup with a FastAPI service for /uplink ingestion and /stats exposure, plus a Streamlit dashboard for visualization; adding another backend (InfluxDB/TimescaleDB) or another frontend (Grafana/Datacake) is therefore mostly a wiring/configuration task.
The server receives uplinks from Node-RED, stores each sample in a local SQLite database, fetches real weather from Open-Meteo for comparison, then serves both raw history and aggregate metrics to the dashboard. This gives a full end-to-end pipeline (sensor -> TTN -> Node-RED -> API -> dashboard) while keeping deployment simple and reproducible with Docker Compose.
Neural network inference is inherently compute-intensive. On a general-purpose server, a single forward pass through even a small model draws hundreds of milliwatts. As an example, a simple google search consumes 0.3 W while a ChatGPT request is 3 W !
The U545's Neural-ART accelerator helps significantly: by executing MAC operations in dedicated hardware rather than running them on the Cortex-M33, inference completes faster and at lower energy per operation than a pure software implementation.
STMicroelectronics' X-NUCLEO-LPM01A (Power Shield) is the reference tool for accurate current measurement on NUCLEO boards. The procedure is as follows:
- Hardware setup: Remove the IDD jumper on the NUCLEO-U545RE-Q and connect the LPM01A in series on the 3.3 V / VDD supply rail using the dedicated headers.
- Software: Install STM32CubeMonitor-Power on the host PC and connect to the LPM01A over USB.
- Run the acquisition: Flash the firmware, trigger a measurement session, and observe the current waveform. STM32CubeMonitor-Power integrates the waveform to give average current, peak current, and total charge (µAh) per acquisition cycle.
Our system operates at 1.8 V supply and draws approximately 3 mA in the active sensing + inference phase, corresponding to:
Between acquisitions, the MCU enters a low-power Stop mode, bringing average consumption well below the active peak.
This project was an introduction to on-device artificial intelligence in a constrained embedded context. Building and training the models highlighted how much representational power even a small dense network can have — the multi-class classifier reaches solid accuracy using only three sensor inputs. Deploying that model on a Cortex-M33 with a hardware neural accelerator, and watching it produce correct predictions at 5 mW, made the energy argument for edge AI very concrete.
At the same time, the project reinforced that energy consumption is a first-class constraint in embedded AI, not an afterthought. Every design choice — quantization, model depth, duty cycle, supply voltage — has a direct impact on battery life. Future work could explore INT8 quantization (vs the current float32 baseline) and adaptive duty cycling to extend autonomy further.
More broadly, the number of connected objects in our daily lives keeps growing — smartphones, cars, dishwashers, toothbrushes — and it seems inevitable that AI will progressively find its way into all of them. MetAI is a small but concrete glimpse of what that future could look like: intelligence running locally, efficiently, at the very edge of the network.
MetAI/
├── LICENSE
├── README.md
├── AI Models/ # Exported TFLite models
│ ├── meteo_multiclasse.tflite
│ └── rain_model.tflite
├── Binaries/ # Compiled binaries grouped by test/project
│ ├── Final project/
│ ├── First AI model/
│ └── Simple board test/
├── Firmwares/ # STM32CubeIDE firmware projects
│ ├── implementation_gros_model/
│ ├── model_IA/
│ └── test/
├── Images/ # Figures used in the README
│ └── ...
├── Jupyter Notebooks/ # Model training/testing notebooks
│ ├── Model_final.ipynb
│ └── Model_test.ipynb
├── NodeRED/ # Node-RED assets/flows
│ └── flows.json
├── scripts/ # Utility scripts
├── Serveur/ # Backend API + Streamlit dashboard + compose
│ ├── docker-compose.yml
│ ├── api/
│ │ ├── Dockerfile
│ │ ├── main.py
│ │ └── requirements.txt
│ ├── dashboard/
│ │ ├── app.py
│ │ ├── Dockerfile
│ │ └── requirements.txt
│ └── data/
└── TTN/ # TTN payload formatter
└── function decodeUplink.txt
This section gives a practical end-to-end sequence to run the full MetAI chain: STM32 board -> LoRaWAN (TTN) -> Node-RED -> server API/dashboard.
git clone https://github.com/MetAI/MetAI.git
cd MetAIThis project supports two flash paths:
- Prebuilt binaries from Binaries/ for fast deployment.
- Build + flash from Firmwares/ when you modify the source code.
The firmware projects in Firmwares/ are generated with Makefiles, so you can work from VS Code and a terminal (not only STM32CubeIDE).
If you want to compile from source before flashing:
- Go to the target firmware folder (example):
cd Firmwares/implementation_gros_model
- Build:
make -j
- Flash with Make (when target is configured):
make flash
To flash a prebuilt image with STM32CubeProgrammer:
- Connect the NUCLEO-U545RE-Q over USB (ST-LINK).
- Open STM32CubeProgrammer and select ST-LINK as the connection type.
- Pick the firmware image you want from Binaries/:
- Binaries/Final project/implementation_gros_model.hex for the complete AI + LoRaWAN project.
- Binaries/First AI model/model_IA.hex for the first AI model test.
- Binaries/Simple board test/test.hex for basic board validation.
- Program the image, verify flashing, then reset/run the board.
- Open a serial terminal at 115200 baud to check runtime logs and LoRa responses.
- In TTN, create an Application and register an End Device (OTAA).
- Keep your device identifiers and keys ready:
- JoinEUI/AppEUI
- DevEUI
- AppKey
- Provision the LoRa-E5 module once over UART (example AT sequence):
ATAT+MODE=LWOTAAAT+ID=DevEui,"<YOUR_DEV_EUI>"AT+ID=AppEui,"<YOUR_JOIN_EUI>"AT+KEY=APPKEY,"<YOUR_APP_KEY>"AT+DR=EU868(or your TTN regional band)AT+JOIN
- Confirm the join is accepted in TTN (Live data).
In this project firmware, uplinks are sent as text payloads through AT+MSG in the format:
P=<pressure>,H=<humidity>,T=<temperature>,C=<predicted_class>
- Open TTN Console -> your Application -> Payload formatters -> Uplink.
- Copy/paste the decoder function from:
- TTN/function decodeUplink.txt
- Save the formatter.
- Trigger an uplink and verify decoded fields in TTN Live data:
pressure_hpahumidity_percenttemperature_deg_cpredicted_classprediction_fr
Once this is done, TTN can forward decoded JSON to Node-RED, and Node-RED can POST it to the API /uplink endpoint used by the dashboard.
- Go to Serveur/.
- Start the stack with Docker Compose:
docker compose up --build -d
- Validate services:
- API on http://localhost:8000
- Dashboard on http://localhost:8501
- Optional checks:
docker compose psdocker compose logs -f apidocker compose logs -f dashboard
The compose file starts two services:
- api (FastAPI ingestion and stats)
- dashboard (Streamlit visualization)
| Tool | Purpose |
|---|---|
| STM32CubeIDE | Firmware development |
| STM32Cube.AI / STEdgeAI | Model conversion and deployment |
| TensorFlow / Keras | Model training |
| Meteostat | Historical weather data |
| The Things Network | LoRaWAN network server |
| Node-RED | Payload forwarding |
| STM32CubeMonitor-Power | Power consumption measurement |
| Docker | Deployment of the web server |









