Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.pio
/.tools/
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ Also, [read our FAQ](https://wiki.bruce.computer/faq/)
<summary><h2>RF</h2></summary>

- [x] Scan/Copy
- [x] [Wireless M-Bus: passive T1/C1 reception](docs/wireless-mbus.md)
- [x] [Custom SubGhz](https://wiki.bruce.computer/features/rf/#replay-payloads-like-flipper)
- [x] Spectrum
- [x] Jammer Full (sends a full squared wave into output)
Expand Down
162 changes: 162 additions & 0 deletions docs/wireless-mbus.de.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Wireless M-Bus für Bruce

[English instructions](wireless-mbus.md)

Diese experimentelle Erweiterung ergänzt das RF-Menü von Bruce um einen rein
empfangenden Wireless-M-Bus-Empfänger für den CC1101. Sie sendet nicht, nimmt keine
Schlüssel entgegen und entschlüsselt keine geschützten Zählerwerte.
Es ist eine native Firmware-Erweiterung, kein JavaScript zum Kopieren auf die
SD-Karte. Die vorhandene Bruce-JavaScript-API stellt den benötigten Paketempfang
nicht bereit.

## Wofür Wireless M-Bus verwendet wird

Wireless M-Bus (wM-Bus, EN 13757-4) überträgt Zählerdaten per Funk, zum Beispiel
von Wasser-, Gas-, Wärme- und Stromzählern oder Heizkostenverteilern zu einem
Lesegerät oder Gateway. Dadurch lassen sich Verbrauchsdaten aus der Ferne erfassen.

Es ist besonders in Europa verbreitet. Regionale Profile und Frequenzbänder
unterscheiden sich; diese Implementierung empfängt gezielt T1/C1 auf 868,95 MHz.
Siehe die [M-Bus-Übersicht](https://m-bus.com/) und
[TIs Wireless-M-Bus-Übersicht](https://www.ti.com/en-us/technologies/wired-wireless-connectivity/wireless-m-bus).

## Hardware und Teststand

- Getestet wurde ein **LilyGO T-Embed CC1101 Plus** mit dem Build-Ziel
`lilygo-t-embed-cc1101`. Ein eigenes Plus-Ziel gibt es in diesem Quellstand nicht.
- Implementiert sind **T1 und C1 auf 868,95 MHz**, bei C1 die Frameformate A und B.
- Der Gerätebesitzer hat am **11. November 2026** einen sehr gut funktionierenden
Funkempfang bestätigt. Damit sind noch nicht alle Modi, Zählertypen,
Datenfelder und SD-Funktionen einzeln geprüft.
- Andere Boards und externe CC1101-Aufbauten wurden hier nicht auf Hardware getestet.
- Für Decoder und Empfänger gibt es lokale C++-Tests; siehe unten.

## Firmware bauen und installieren

Baue den Checkout, der diese Erweiterung enthält. Die ursprüngliche Entwicklungsbasis ist
der offizielle Bruce-Commit `a59213f3ec6cd302fa32c2042b2700c9c24d991a`.

Der lokale Build wurde mit **Python 3.13**, **PlatformIO 6.2.0** und dem Ziel
`lilygo-t-embed-cc1101` durchgeführt. Der Branch enthält außerdem die für den
lokalen Windows-Build benötigte Festlegung auf FastLED 3.10.3 und eine Korrektur
am objcopy-Build-Hook.

Beispiel für Windows PowerShell im Repository-Verzeichnis, mit verfügbarem Python 3.13:

```powershell
py -3.13 -m venv .tools\venv
$env:PLATFORMIO_CORE_DIR = "$PWD\.tools\platformio"
$env:PLATFORMIO_CACHE_DIR = "$PWD\.tools\platformio-cache"
$env:TEMP = "$PWD\.tools\tmp"
$env:TMP = $env:TEMP
New-Item -ItemType Directory -Path $env:TEMP -Force | Out-Null
& .\.tools\venv\Scripts\python.exe -m pip install platformio==6.2.0 --cache-dir .tools\pip-cache
& .\.tools\venv\Scripts\python.exe -m platformio run -e lilygo-t-embed-cc1101
```

Danach liegen das reine App-Image unter
`.pio/build/lilygo-t-embed-cc1101/firmware.bin` und das zusammengefügte Komplettimage
`Bruce-lilygo-t-embed-cc1101.bin` im Repository-Hauptverzeichnis.
Für eine normale Installation als einzige Firmware auf dem ESP32-S3 wird das
Komplettimage mit esptool ab Adresse **`0x0`** geschrieben. Das reine App-Image darf
**nicht** an Adresse `0x0` geschrieben werden. Wähle den COM-Port deines Geräts.

Beispiel zum Flashen als einzelne Firmware:

```powershell
& .\.tools\venv\Scripts\python.exe -m pip install esptool --cache-dir .tools\pip-cache
$devicePort = 'COM7' # Durch den COM-Port deines Geräts ersetzen.
& .\.tools\venv\Scripts\python.exe -m esptool --chip esp32s3 --port $devicePort write-flash 0x0 Bruce-lilygo-t-embed-cc1101.bin
```

Das Ende des Schreibvorgangs und die Datenprüfung abwarten. Anschließend das
RF-Menü auf dem Gerät öffnen.

Das Komplettimage enthält Bootloader, Partitionstabelle und Anwendung für Bruce
als einzelne Firmware auf dem ESP32-S3 mit 16 MB Flash. Beim dokumentierten
Hardwaretest wurde der interne Flash gelöscht und Bruce anschließend neu installiert.

Beim Build bleiben Warnungen aus vorhandenem Bruce-, Display-, Bluetooth- und
Audiocode bestehen. Ein erfolgreicher Build bestätigt nicht alle Funktionen der
gesamten Firmware.

## Bedienung

1. Falls erforderlich unter **Radio Frequency > Config** den CC1101 als RF-Modul auswählen.
2. **Radio Frequency > Wireless M-Bus > Scan T1/C1 868.95** öffnen.
3. Auf vollständige Telegramme mit gültigen Link-Layer-Prüfsummen warten.
Die Sendeintervalle eines Zählers können lang sein.
4. Mit dem Drehrad ein Gerät auswählen und durch Klicken die Details öffnen.
5. In den Details mit dem Drehrad scrollen. Währenddessen pausiert der Empfang.
Klicken oder Zurück setzt ihn fort.
6. Zurück in der Scanansicht stoppt den Empfang und öffnet das App-Menü.
7. **Export devices to SD** speichert die Geräteübersicht; **Clear results** leert
sie. Beim Verlassen der App endet die Sitzung und die RAM-Liste wird verworfen.

Die Liste enthält höchstens **96 unterschiedliche Geräte**. Gruppiert wird nach
Hersteller und vollständiger Link-Layer-Adresse aus ID, Version und Typ.
Angezeigt werden unter anderem unterstützte Header, RSSI, LQI, Telegrammzahl und
Empfangszeit seit Gerätestart. Bekannte numerische DIF/VIF-Datensätze werden nur
bei ausdrücklich unverschlüsselten Anwendungsdaten interpretiert.
Unbekannte Formate bleiben als Rohdaten sichtbar. Der Hexdump kann auch
verschlüsselte Bytes enthalten und ist entsprechend beschriftet.

## SD-Export

Die Dateien heißen `/wmbus/devices-<boot-ms>-<nummer>.jsonl`. Jede JSON-Zeile
enthält ein Gerät mit seinem letzten gültigen Telegramm und Metadaten. Das ist
eine Momentaufnahme, keine lückenlose Telegrammaufzeichnung.
Bei `frame_hex` sind die DLL-Prüfsummen entfernt und das L-Byte normalisiert;
es handelt sich nicht um eine unveränderte Aufzeichnung des Funksignals.
Der ursprüngliche L-Wert bleibt in `wire_l` erhalten. Zeiten sind Millisekunden
seit Gerätestart, keine Uhrzeiten.

## Grenzen und Rückmeldungen

Unterstützt werden nur die genannten Modi und die genannte Frequenz. S-, R- und
N-Modi, 169 MHz und andere Frequenzen sind nicht implementiert. Geschützte Werte
bleiben geschützt; Herstellerformate, Kompakttelegramme und nicht unterstützte
Datensätze lassen sich nicht universell interpretieren.
Display, SD und Funkmodul teilen sich einen SPI-Bus. Bei Bildschirmupdates sind
Empfangslücken möglich. Ungültige Prüfsummen, unvollständige Telegramme und
FIFO-Überläufe werden verworfen. Die Tabelle ist auf 96 Geräte begrenzt.
Eine gültige CRC ist keine Authentifizierung des Senders.

Für weitere Tests sind besonders hilfreich: Vergleich von T1/C1-A/C1-B mit einem
Referenzempfänger, wiederholte Telegramme, offene und geschützte Datensätze,
SD-Export samt Fehlerfällen, Bedienung und andere RF-Apps nach dem Verlassen.
Bei Rückmeldungen bitte Board, Firmware-Commit, bekannten Empfangsmodus und
Schritte zum Nachstellen nennen. Geräte-IDs und persönliche Verbrauchsdaten in
öffentlichen Beispielen unkenntlich machen.

## Lokale Tests und Quellen

Mit Python 3 und einem GCC-/Clang-C++17-Compiler mit UndefinedBehaviorSanitizer:

```sh
python tests/wmbus/run.py
```

Mit `--cxx /pfad/zum/compiler` lässt sich ein anderer Compiler wählen.
Alternativ das Python-Paket `ziglang` in einer lokalen virtuellen Umgebung
installieren und `python tests/wmbus/run.py --zig` ausführen. Dieser Weg wurde
für die lokalen Windows-Tests verwendet. Ausgaben liegen unter
`.pio/wmbus-host-tests` und gehören nicht zum Quellcode.

Geprüft werden CRC, T1-/C1-A/B-Framing, beschädigte und abgeschnittene Telegramme,
Verschlüsselungsgrenzen, 20.000 Zufallseingaben, FIFO-Stückelung, lange Frames,
Erholung nach Überlauf/Timeout und Wiederherstellung der Funkregister.
Der Test mit simuliertem CC1101 übersetzt die tatsächliche Receiver-Implementierung;
er prüft weder Funkausbreitung noch Benutzeroberfläche.

Referenzen: [TI AN067](https://www.ti.com/lit/an/swra234a/swra234a.pdf),
[TI C/T-Framing](https://www.ti.com/lit/an/swra522d/swra522d.pdf),
[CC1101-Datenblatt](https://www.ti.com/lit/ds/symlink/cc1101.pdf),
[CC1101-Errata](https://www.ti.com/lit/er/swrz020e/swrz020e.pdf),
[wMbus-lib](https://github.com/SzczepanLeon/wMbus-lib),
[wmbusmeters](https://github.com/wmbusmeters/wmbusmeters) und
[libmbus](https://github.com/rscada/libmbus).
Die Quellen dienten dem Abgleich von Funkparametern und Telegrammformaten;
es wird keine Entschlüsselungsbibliothek eingebunden. Die Implementierung
entstand mit Unterstützung von Codex. Lizenz: AGPL-3.0-or-later;
siehe [LICENSE](../LICENSE).
143 changes: 143 additions & 0 deletions docs/wireless-mbus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# Wireless M-Bus for Bruce

[Deutsche Anleitung](wireless-mbus.de.md)

This experimental native RF app adds passive Wireless M-Bus reception through
the CC1101. It does not transmit, accept keys, or decrypt protected meter values.
It is a firmware extension, not a JavaScript app that can be copied to an SD card.
Bruce's current JavaScript interface does not provide the packet reception needed here.

## What Wireless M-Bus is used for

Wireless M-Bus (wM-Bus, EN 13757-4) carries meter readings over radio, for example
from water, gas, heat and electricity meters or heat-cost allocators to a reader
or gateway. This enables remote collection of consumption data.

It is particularly widespread in Europe. Regional profiles and frequency bands
vary; this implementation specifically receives T1/C1 at 868.95 MHz.
See the [M-Bus overview](https://m-bus.com/) and
[TI's Wireless M-Bus overview](https://www.ti.com/en-us/technologies/wired-wireless-connectivity/wireless-m-bus).

## Hardware and validation

- Tested hardware: LilyGO T-Embed CC1101 Plus, using the
`lilygo-t-embed-cc1101` build target. This source has no separate Plus target.
- T1 and C1 at 868.95 MHz; C1 frame formats A and B are implemented.
- On 11 November 2026, the device owner reported very good reception on this board.
This does not separately verify every mode, meter type, data field or SD operation.
- Other boards and external CC1101 arrangements have not been tested by this project.
- The implemented receiver and decoder have host tests; see below.

## Build and installation

Build the checkout containing this feature. The original development base is
`a59213f3ec6cd302fa32c2042b2700c9c24d991a`.

The tested local build used Python 3.13, PlatformIO 6.2.0 and the
`lilygo-t-embed-cc1101` environment. The branch also includes the FastLED 3.10.3
pin and an objcopy build-hook compatibility fix needed by the local Windows build.

One Windows PowerShell setup, run from the repository root with Python 3.13 available:

```powershell
py -3.13 -m venv .tools\venv
$env:PLATFORMIO_CORE_DIR = "$PWD\.tools\platformio"
$env:PLATFORMIO_CACHE_DIR = "$PWD\.tools\platformio-cache"
$env:TEMP = "$PWD\.tools\tmp"
$env:TMP = $env:TEMP
New-Item -ItemType Directory -Path $env:TEMP -Force | Out-Null
& .\.tools\venv\Scripts\python.exe -m pip install platformio==6.2.0 --cache-dir .tools\pip-cache
& .\.tools\venv\Scripts\python.exe -m platformio run -e lilygo-t-embed-cc1101
```

The build creates the app at `.pio/build/lilygo-t-embed-cc1101/firmware.bin`
and the merged image `Bruce-lilygo-t-embed-cc1101.bin` in the repository root.
For a normal standalone ESP32-S3 installation, write the merged image at address
`0x0` using esptool. Never write the app-only image at `0x0`.
Choose the COM port that belongs to your device.

A standalone flash example is:

```powershell
& .\.tools\venv\Scripts\python.exe -m pip install esptool --cache-dir .tools\pip-cache
$devicePort = 'COM7' # Replace with your device's port.
& .\.tools\venv\Scripts\python.exe -m esptool --chip esp32s3 --port $devicePort write-flash 0x0 Bruce-lilygo-t-embed-cc1101.bin
```

Wait for the write verification to finish and then open the RF menu on the device.

The full image contains the bootloader, partition table and application for
standalone Bruce on an ESP32-S3 with 16 MB flash. The documented hardware test
used an internal-flash erase followed by a fresh Bruce installation.

The build still reports warnings in existing Bruce/display/Bluetooth/audio code.
Successful compilation is not evidence that every existing firmware function works.

## Use

1. In **Radio Frequency > Config**, select the CC1101 RF module if necessary.
2. Open **Radio Frequency > Wireless M-Bus > Scan T1/C1 868.95**.
3. Wait for complete telegrams with valid link-layer CRCs. Meter intervals can be long.
4. Use the wheel to select a device; click to open its details.
5. Scroll through the details. Reception pauses in this view; click or go back to resume.
6. Go back from scanning to stop reception and return to the app menu.
7. Select **Export devices to SD** to save the device overview, or **Clear results**
to reset it. Leaving the app discards the in-memory session.

The list holds up to 96 distinct devices, grouped by manufacturer and complete
link-layer address (ID, version and type). Details include supported headers,
RSSI, LQI, count and reception time since boot. Known numeric DIF/VIF records
are interpreted only when the application data is explicitly unencrypted.
Unknown formats remain raw bytes. A hex dump may include ciphertext and is labelled accordingly.

## SD export

Files are written as `/wmbus/devices-<boot-ms>-<number>.jsonl`, with one JSON object
per device containing its latest valid telegram and metadata. This is a snapshot,
not continuous packet logging. `frame_hex` omits DLL CRC bytes and normalizes the
L byte; it is not a byte-for-byte recording of the on-air signal. The original L
value is retained in `wire_l`. Timestamps are milliseconds since boot.

## Limits and feedback

Only the stated modes and frequency are supported. S/R/N modes, 169 MHz and other
frequencies are outside this implementation. Encrypted values remain encrypted;
manufacturer-specific, compact and unsupported records are not universally decoded.
The shared display/SD/radio SPI bus can cause reception gaps during display updates.
Invalid CRCs, truncated packets and FIFO overflows are discarded. The device table
is limited to 96 entries. A valid CRC does not authenticate the sender.

Further testing should cover T1/C1-A/C1-B against a reference receiver, repeated
telegram counts, unencrypted and protected records, SD export/error handling,
navigation and use of other RF apps after exiting. When reporting a result,
include board, firmware commit, mode if known, and steps to reproduce; redact meter
IDs and personal consumption data from public examples.

## Host tests and sources

With Python 3 and a GCC/Clang C++17 compiler supporting UndefinedBehaviorSanitizer:

```sh
python tests/wmbus/run.py
```

Use `--cxx /path/to/compiler` to select a compiler. Alternatively, install the
`ziglang` Python package in a local virtual environment and run
`python tests/wmbus/run.py --zig` (the local Windows tests use this route).
Outputs go under `.pio/wmbus-host-tests` and are not source files.

Tests cover CRC, T1/C1 A/B framing, corruption, truncation, encryption boundaries,
20,000 randomized inputs, FIFO fragmentation, long frames, overflow/timeout
recovery and radio-register restoration. The fake-radio test compiles the actual
production Receiver implementation with a simulated CC1101; it does not test RF propagation or the UI.

Protocol references: [TI AN067](https://www.ti.com/lit/an/swra234a/swra234a.pdf),
[TI C/T framing](https://www.ti.com/lit/an/swra522d/swra522d.pdf),
[CC1101 datasheet](https://www.ti.com/lit/ds/symlink/cc1101.pdf),
[CC1101 errata](https://www.ti.com/lit/er/swrz020e/swrz020e.pdf),
[wMbus-lib](https://github.com/SzczepanLeon/wMbus-lib),
[wmbusmeters](https://github.com/wmbusmeters/wmbusmeters), and
[libmbus](https://github.com/rscada/libmbus).
These projects were consulted for radio settings and format definitions; no
decryption library is integrated. The implementation was developed with Codex
assistance. Code is distributed under AGPL-3.0-or-later; see [LICENSE](../LICENSE).
20 changes: 16 additions & 4 deletions patch.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import hashlib
from typing import TYPE_CHECKING, Any
import requests
import subprocess

if TYPE_CHECKING:
Import: Any = None
Expand Down Expand Up @@ -33,10 +34,21 @@
elif mcu=="esp32p4":
"""Do nothing"""
else:
env.Execute(
"pio pkg exec -p toolchain-xtensa-%s -- xtensa-%s-elf-objcopy --weaken-symbol=ieee80211_raw_frame_sanity_check %s %s"
% (mcu, mcu, original_file, patched_file)
)
# Current pioarduino uses one Xtensa package for all three chips.
# Calling the old package name through an external `pio` can fail on
# fresh Windows installations; fail before renaming any input archive.
toolchain_dir = env.PioPlatform().get_package_dir("toolchain-xtensa-esp-elf")
if not toolchain_dir:
toolchain_dir = env.PioPlatform().get_package_dir("toolchain-xtensa-%s" % mcu)
if not toolchain_dir:
raise RuntimeError("Xtensa toolchain not found")
objcopy = join(toolchain_dir, "bin", "xtensa-%s-elf-objcopy" % mcu)
if isfile(objcopy + ".exe"):
objcopy += ".exe"
subprocess.run([
objcopy, "--weaken-symbol=ieee80211_raw_frame_sanity_check",
original_file, patched_file,
], check=True)

if isfile("%s.old" % (original_file)):
remove("%s.old" % (original_file))
Expand Down
2 changes: 1 addition & 1 deletion platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ lib_deps =
bitbank2/AnimatedGIF
bitbank2/PNGdec @ ^1.1.2
ESP32Async/ESPAsyncWebServer
fastled/FastLED @^3.10.3
fastled/FastLED @3.10.3
jgromes/RadioLib @ ^7.4.0
; Pinned to fixed commits so upstream changes never break our patches
; (applied by patch_library_conflicts.py): IRQ-less I2C polling for
Expand Down
2 changes: 2 additions & 0 deletions src/core/menu_items/RFMenu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@
#include "modules/rf/rf_utils.h"
#include "modules/rf/rf_spectrum.h"
#include "modules/rf/rf_waterfall.h"
#include "modules/rf/rf_wmbus.h"

void RFMenu::optionsMenu() {
options = {
{"Scan/copy", [=]() { RFScan(); } },
#if !defined(LITE_VERSION)
{"Record RAW", rf_raw_record }, // Pablo-Ortiz-Lopez
{"Wireless M-Bus", rf_wmbus },
{"Custom SubGhz", sendCustomRF },
#endif
{"Spectrum", rf_spectrum },
Expand Down
Loading