54 Commits
Author SHA1 Message Date
scottpandClaude Fable 5 037af33154 v0.7.0: pure C core + Zephyr module
- Extract all decode/decrypt into a dependency-free C99 core
  (include/victronble.h, src/victronble_core.c): victronble_decode(),
  is_product_adv/key_matches pre-filters, NAN sentinels, LE accessors.
- AES-128-CTR behind a hook: weak-symbol bundled tiny-AES default,
  runtime override (victronble_set_aes_ctr) for PSA/mbedTLS/hardware.
- Arduino VictronBLE class becomes a thin wrapper over the core
  (registry + nonce dedup + rate limit); public C++ API unchanged,
  NAN converted back to the legacy 0 convention.
- Host test vectors (tests/vectors): openssl-generated ciphertext,
  independent of the bundled AES; all five payload shapes + negatives.
- Zephyr module: zephyr/module.yml + Kconfig (CONFIG_VICTRONBLE) +
  observer backend (victronble_zephyr.{h,c}) — scan cb pre-filters and
  queues, dedicated decode thread, listener callbacks, slow passive
  scan defaults, stats counters. docs/ZEPHYR_PORT.md records the plan.
- library.properties: fix URL (gitea, not the nonexistent GitHub).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 11:59:19 +10:00
scottp cc8c4d36d5 check version before release 2026-06-08 17:14:39 +10:00
scottp 712259eec5 Remove claude entries from old session 2026-06-08 16:17:26 +10:00
scottp 57ae49fd09 Add a rak4631 board definition testing 2026-06-07 23:22:36 +10:00
scottp 97e57b0306 Fix chips supported 2026-06-07 19:27:12 +10:00
scottp 05ee88cd31 Working across ESP32 S3, ESP32 C3 and nRF52 2026-06-07 19:23:13 +10:00
scottp ef50829c81 Cleanup READMED and release notes 2026-06-05 00:00:25 +10:00
scottp 53fa6fa0e1 ignore pka file 2026-06-04 23:48:47 +10:00
scottp de6607d870 Branch version ready for testing with nRF52 2026-06-04 23:47:55 +10:00
scottp 105d66b07a Update info 2026-06-04 22:55:40 +10:00
scottp cd3b4626d1 ignore local 2026-06-04 21:21:55 +10:00
scottp 7706f88399 Bug fixes for reported issues 2026-06-04 21:21:18 +10:00
scottp e7024d9983 Plans for part 2 - multiple bluetooth devices 2026-02-28 15:00:45 +11:00
scottp 261cc0d1fe Improve readme ready for v0.4 release 2026-02-28 14:40:41 +11:00
scottp 39a89c816c Versions v0.4 ready for release 2026-02-28 14:38:30 +11:00
scottp 4944757903 Fix to be non blocking without tasks 2026-02-28 14:31:42 +11:00
scottp 31765c7ac8 Update notes 2026-02-28 13:52:16 +11:00
scottp 84d153c9a8 Single callback version - vastly simplified. 2026-02-28 13:34:20 +11:00
scottp 8a4e010324 More planning 2026-02-28 12:31:04 +11:00
scottp 977641b093 Forwarding code 2026-02-17 09:27:48 +11:00
scottp 24712c206a Work on receiver and sender 2026-02-15 19:20:03 +11:00
scottp 8a2402cb63 Repeater and Test code for ESP Now 2026-02-15 19:10:19 +11:00
scottp a64fef899b New version with smaller memory footprint etc 2026-02-12 18:33:56 +11:00
scottp a843eb924b Keep v0.3.1 2026-02-12 18:10:35 +11:00
scottp 5a210fb88f Experimenting with a claude file and created new logging example 2026-02-12 18:04:02 +11:00
scottp 30c93af18b Fix status 2026-01-29 18:45:37 +11:00
scottp d9577be900 Scripts and automatic claude update 2026-01-29 18:42:41 +11:00
scottp f9e72a68fe Examples todo 2025-12-30 20:54:04 +11:00
scottp a8d40ba260 Fix notes on testing 2025-12-29 20:35:47 +11:00
scottp af39db8732 Fix name 2025-12-29 20:31:07 +11:00
scottp 26b0196791 Decoding working for MPPT 2025-12-29 20:22:41 +11:00
scottp 0863f8572c Finally working decode MPPT 2025-12-29 20:00:05 +11:00
scottp d3b1c632db work on better mac address 2025-12-29 19:26:32 +11:00
scottp 03d8da3b7d Cleaning up by using structs and reusing data blocks 2025-12-29 19:12:47 +11:00
scottp 6a517246ea Experimental version 2025-12-29 13:38:47 +11:00
scottp 4bbab345b0 Working serial on S3 too. Interesting... 2025-12-29 11:40:46 +11:00
scottp 1a651b149d Merge branch 'main' of https://gitea.sh3d.com.au/sh3d/VictronBLE 2025-12-29 11:16:43 +11:00
scottp cec45524d3 Add core2 2025-12-29 11:16:41 +11:00
scottp 2bd6094955 Cleanup only 2025-12-29 11:09:33 +11:00
scottp 9f0f2ce8fd Improved structs 2025-12-28 23:35:27 +11:00
scottp 8e5eba47d7 Working C3 build 2025-12-28 23:27:40 +11:00
scottp 95d83b492a Playing with debug 2025-12-19 12:46:28 +11:00
scottp 139c6f961d Work on decoding using structs 2025-12-18 22:27:15 +11:00
scottp 2ccac7b0c8 Experimenting and decoding - seems some structs are wrong, check original code 2025-12-18 21:49:04 +11:00
scottp 97a71ce34c TODO and m5stick and debug 2025-12-18 20:43:10 +11:00
scottp e827dea4e5 Ignore builds 2025-12-18 18:26:27 +11:00
scottp 364462a4ed Fix version for examples 2025-12-18 18:23:31 +11:00
scottp bc766f7cfc Fix library 2025-12-18 18:22:33 +11:00
scottp fcd84b92eb Unsure name 2025-12-18 18:17:49 +11:00
scottp 16e81071c8 Fix name 2025-12-18 18:10:05 +11:00
scottp c8fff522b1 Fix version 2025-12-18 18:08:08 +11:00
scottp d8128dae2e Initial version only 2025-12-18 17:53:39 +11:00
scottp 3daf61d78a Copyright etc 2025-12-18 17:48:55 +11:00
scottp d5f3d3ecbe Initial readme up 2025-12-18 17:39:13 +11:00
23 changed files with 35 additions and 1205 deletions
-9
View File
@@ -72,12 +72,3 @@ venv/
env/ env/
*.tar.gz *.tar.gz
# Compiled host binaries
tests/vectors/victronble_test
examples/NativeDecode/nativedecode
# Zephyr build trees
samples/*/build/
build/
twister-out/
twister-out.*/
+2 -2
View File
@@ -43,7 +43,7 @@ your-project/
### Step 3: Update the Example Code ### Step 3: Update the Example Code
Open `examples/MultiDevice/src/main.cpp` and update these lines with YOUR device information: Open `examples/MultiDevice/main.cpp` and update these lines with YOUR device information:
```cpp ```cpp
// Replace these with YOUR actual device details: // Replace these with YOUR actual device details:
@@ -81,7 +81,7 @@ pio run -t upload && pio device monitor
``` ```
#### Arduino IDE: #### Arduino IDE:
1. Open `examples/MultiDevice/src/main.cpp` as an .ino file 1. Open `examples/MultiDevice/main.cpp` as an .ino file
2. Select your ESP32 board from Tools → Board 2. Select your ESP32 board from Tools → Board
3. Select your COM port from Tools → Port 3. Select your COM port from Tools → Port
4. Click Upload 4. Click Upload
+19 -125
View File
@@ -1,22 +1,22 @@
# VictronBLE # VictronBLE
A portable library for reading Victron Energy device data via Bluetooth Low Energy (BLE) advertisements. Use it as an **Arduino** library on ESP32 and nRF52840, as a **Zephyr** module on any Bluetooth-capable board, or drop the **pure C99 core** into anything else. A portable Arduino library for reading Victron Energy device data via Bluetooth Low Energy (BLE) advertisements — runs on both **ESP32** and **nRF52840**.
v0.7 splits the library into a **dependency-free pure C99 core** (decode + decrypt, no BLE stack, no allocation, no I/O) plus thin platform layers: the Arduino C++ wrapper, and a **Zephyr module** with its own observer API. (v0.6 added multi-platform support for ESP32 + nRF52840; v0.5 brought the decoding accuracy fixes and AC charger support.) See [VERSIONS](VERSIONS) for full details. A stable **v1.0** release with a consistent, long-term API is coming soon. v0.6 adds **multi-platform support** (ESP32 + nRF52840) via a hardware-abstracted BLE backend and dependency-free bundled crypto. (v0.5 brought the decoding accuracy fixes and AC charger support; v0.4 reworked the internals — function-pointer callback API, reduced memory usage, non-blocking scanning.) See [VERSIONS](VERSIONS) for full details. A stable **v1.0** release with a consistent, long-term API is coming soon.
--- ---
Why another library? Most of the Victron BLE examples are built into other frameworks (e.g. ESPHome) or are locked to a single chip. The goal here is one library that works across ESP32 and nRF52 (and is easy to extend to more), usable standalone or inside ESPHome and other frameworks, with a long-term plan to move others onto it and improve the code with many eyes. Why another library? Most of the Victron BLE examples are built into other frameworks (e.g. ESPHome) or are locked to a single chip. The goal here is one library that works across ESP32 and nRF52 (and is easy to extend to more), usable standalone or inside ESPHome and other frameworks, with a long-term plan to move others onto it and improve the code with many eyes.
Under Arduino it supports **ESP32** (original, S and C series — tested on older ESP32, ESP32-S3 and ESP32-C3) and **nRF52840** (Adafruit/Seeed Bluefruit core, e.g. Seeed XIAO nRF52840). Under **Zephyr** it is board-agnostic — anything with a Bluetooth controller and the observer role (tested on nRF52840DK and RAK4631). All decoding and decryption is shared; only the BLE scanning layer is platform-specific, so other stacks can be added by implementing one more backend. Supports **ESP32** (original, S and C series — tested on older ESP32, ESP32-S3 and ESP32-C3) and **nRF52840** (Adafruit/Seeed Bluefruit core, e.g. Seeed XIAO nRF52840). All decoding and decryption is shared; only a thin BLE scanning backend is platform-specific (`src/esp32/`, `src/nrf52/`), so other chipsets can be added by implementing one more backend.
## Features ## Features
-**Multi-Platform**: ESP32 and nRF52840 under Arduino, any Bluetooth board under Zephyr -**Multi-Platform**: One API for ESP32 and nRF52840; backend chosen at compile time
-**No External Dependencies**: Bundled AES-128-CTR — no mbedTLS or crypto library needed -**No External Dependencies**: Bundled AES-128-CTR — no mbedTLS or crypto library needed
-**Multiple Device Support**: Monitor multiple Victron devices simultaneously -**Multiple Device Support**: Monitor multiple Victron devices simultaneously
-**All Device Types**: Solar chargers, battery monitors, inverters, DC-DC converters, AC chargers -**All Device Types**: Solar chargers, battery monitors, inverters, DC-DC converters, AC chargers
-**Framework Friendly**: Arduino library, Zephyr module, or the bare C core -**Framework Friendly**: Works with Arduino (and ESP-IDF on ESP32)
-**Clean API**: Simple, intuitive interface with callback support -**Clean API**: Simple, intuitive interface with callback support
-**No Pairing Required**: Reads BLE advertisement data directly -**No Pairing Required**: Reads BLE advertisement data directly
-**Low Power**: Uses passive BLE scanning -**Low Power**: Uses passive BLE scanning
@@ -35,13 +35,11 @@ Under Arduino it supports **ESP32** (original, S and C series — tested on olde
## Hardware Requirements ## Hardware Requirements
- An ESP32 (original / S / C series) **or** an nRF52840 board (Adafruit/Seeed - An ESP32 (original / S / C series) **or** an nRF52840 board (Adafruit/Seeed
Bluefruit core — e.g. Seeed XIAO nRF52840) for the Arduino API Bluefruit core — e.g. Seeed XIAO nRF52840)
- Or any Zephyr-supported board with a Bluetooth controller (tested on
nRF52840DK and RAK4631)
- Victron devices with BLE "Instant Readout" enabled - Victron devices with BLE "Instant Readout" enabled
Under Arduino the BLE backend is selected automatically at compile time from The BLE backend is selected automatically at compile time from the board's
the board's architecture — no code changes are needed to switch platforms. architecture — no code changes are needed to switch platforms.
## Installation ## Installation
@@ -89,78 +87,6 @@ example's `platformio.ini` includes ready-made ESP32 and nRF52 environments.
2. Move the `VictronBLE` folder to your Arduino libraries directory 2. Move the `VictronBLE` folder to your Arduino libraries directory
3. Restart Arduino IDE 3. Restart Arduino IDE
### Zephyr
The repository is a Zephyr module (`zephyr/module.yml`), so Zephyr finds it
automatically once it is in your workspace. Add it to your `west.yml`:
```yaml
manifest:
remotes:
- name: sh3d
url-base: https://gitea.sh3d.com.au/Sh3d
projects:
- name: VictronBLE
remote: sh3d
revision: main
path: modules/lib/victronble
```
Then `west update`, and enable it in your `prj.conf`:
```
CONFIG_BT=y
CONFIG_BT_OBSERVER=y
CONFIG_VICTRONBLE=y
CONFIG_CBPRINTF_FP_SUPPORT=y # only if you print the float fields
```
`CONFIG_VICTRONBLE` depends on `CONFIG_BT_OBSERVER`, and the application must
call `bt_enable()` before `victronble_start()` — the library scans, it does not
own the Bluetooth stack.
To build against a local checkout that is not in the manifest, point Zephyr at
it directly instead:
```sh
west build -b nrf52840dk/nrf52840 -d /tmp/build /path/to/app \
-- -DZEPHYR_EXTRA_MODULES=/path/to/VictronBLE
```
#### Zephyr API
```c
#include "victronble_zephyr.h"
int victronble_cb_register(struct victronble_cb *cb);
int victronble_device_add(const bt_addr_le_t *addr, const uint8_t key[16]);
int victronble_device_remove(const bt_addr_le_t *addr);
void victronble_watch_set(bool on); /* log every advert, no keys needed */
int victronble_start(void);
int victronble_stop(void);
void victronble_get_stats(struct victronble_stats *out);
```
Records are decoded on a dedicated thread, not the Bluetooth RX thread, so
your `record` callback can log freely without stalling the controller.
#### Kconfig options
| Option | Default | Purpose |
|---|---|---|
| `VICTRONBLE_MAX_DEVICES` | 4 | Size of the monitored-device registry |
| `VICTRONBLE_QUEUE_DEPTH` | 8 | Adverts buffered between the RX and decode threads |
| `VICTRONBLE_THREAD_STACK_SIZE` | 2048 | Decode thread stack |
| `VICTRONBLE_THREAD_PRIORITY` | 10 | Decode thread priority (preemptible) |
| `VICTRONBLE_DEDUP` | y | Suppress repeated adverts by nonce |
| `VICTRONBLE_SCAN_INTERVAL` | 2048 | Scan interval, 0.625 ms units (1.28 s) |
| `VICTRONBLE_SCAN_WINDOW` | 18 | Scan window, 0.625 ms units (11.25 ms) |
| `VICTRONBLE_LOG_LEVEL` | — | Standard Zephyr per-module log level |
Working applications are in [`samples/`](samples/) — start with
[`samples/scan`](samples/scan/) to discover your devices, then
[`samples/observer`](samples/observer/) to read them.
## Quick Start ## Quick Start
### 1. Get Your Encryption Keys ### 1. Get Your Encryption Keys
@@ -453,12 +379,6 @@ void setup() {
5. **Disconnect VictronConnect**: App must be disconnected from device 5. **Disconnect VictronConnect**: App must be disconnected from device
6. **Enable debug**: `victron.setDebug(true);` to see detailed logs 6. **Enable debug**: `victron.setDebug(true);` to see detailed logs
On **Zephyr**, the stats line from `victronble_get_stats()` narrows this down
fast. If `adverts` climbs but `queued` and `decoded` stay at zero, the device
is being heard but never matched: check the Bluetooth address **type**, which
must be `random` for Victron devices, not `public`. Or run `samples/scan`,
which needs neither addresses nor keys.
### Decryption Failures ### Decryption Failures
- Encryption key must match exactly - Encryption key must match exactly
@@ -487,45 +407,31 @@ Based on official [Victron BLE documentation](https://www.victronenergy.com/live
The library keeps everything platform-independent except the BLE radio: The library keeps everything platform-independent except the BLE radio:
``` ```
include/
├── victronble.h Pure C99 core API — decode one advert, no I/O
└── victronble_zephyr.h Zephyr observer API
src/ src/
├── victronble_core.c Decrypt + parse; no BLE, no alloc, reentrant ├── VictronBLE.{h,cpp} Common API, device management, payload decoding
├── crypto/vble_aes.{h,c} Bundled AES-128-CTR (no external dependency) ├── crypto/vble_aes.{h,c} Bundled AES-128-CTR (no external dependency)
├── VictronBLE.{h,cpp} Arduino C++ wrapper over the core
├── esp32/ ESP32 backend — Bluedroid BLEScan ├── esp32/ ESP32 backend — Bluedroid BLEScan
── nrf52/ nRF52 backend — Bluefruit passive scan ── nrf52/ nRF52 backend — Bluefruit passive scan
└── victronble_zephyr.c Zephyr backend — passive scan + decode thread
CMakeLists.txt, Kconfig Zephyr module glue (ignored by PlatformIO)
``` ```
- **A portable core.** `victronble_core.c` is C99 with no dependencies: no
Arduino, no BLE stack, no allocation, no I/O, reentrant. Give it a
manufacturer-data blob and a key, get a record back. Everything else —
scanning, device registries, rate limiting, logging — belongs to the
platform layers. `examples/NativeDecode` runs it on a PC.
- **One BLE HAL.** Each backend extracts the manufacturer data, MAC and RSSI - **One BLE HAL.** Each backend extracts the manufacturer data, MAC and RSSI
from a scan result and hands it to the core. Under Arduino the correct from a scan result and calls the shared `onAdvertisement()`. All decryption and
backend is selected automatically at compile time from the board decoding is common code. The correct backend is selected automatically at
architecture (`ARDUINO_ARCH_ESP32` / `ARDUINO_ARCH_NRF52`) — there is compile time from the board architecture (`ARDUINO_ARCH_ESP32` /
nothing platform-specific in your sketch. Under Zephyr the backend is `ARDUINO_ARCH_NRF52`) — there is nothing platform-specific in your sketch.
`victronble_zephyr.c`, selected by `CONFIG_VICTRONBLE`.
- **No external crypto.** AES-128-CTR is bundled (a trimmed, NIST-verified - **No external crypto.** AES-128-CTR is bundled (a trimmed, NIST-verified
tiny-AES), so the library no longer depends on mbedTLS or any crypto library tiny-AES), so the library no longer depends on mbedTLS or any crypto library
and builds identically on every target. and builds identically on every target.
- **Adding a platform** means implementing one more backend (scan → extract → - **Adding a platform** means implementing one more backend (scan → extract →
hand to the core); the rest is reused unchanged. `onAdvertisement`); the rest is reused unchanged.
> Under Arduino the data callback runs in the BLE event context (the scan task > The data callback runs in the BLE event context (the scan task on ESP32, the
> on ESP32, the SoftDevice/Bluefruit handler on nRF52). Keep work in the > SoftDevice/Bluefruit handler on nRF52). Keep work in the callback light — copy
> callback light — copy what you need and process it from `loop()`. > what you need and process it from `loop()`.
> Under Zephyr this does not apply: records are delivered from the library's
> own decode thread, so callbacks may log and block.
## Examples ## Examples
Arduino / PlatformIO, in [`examples/`](examples/): See the `examples/` directory for:
- **MultiDevice**: Monitor multiple devices with callbacks. One sketch, multiple - **MultiDevice**: Monitor multiple devices with callbacks. One sketch, multiple
PlatformIO environments — builds for ESP32 (`esp32dev`, …) and nRF52840 PlatformIO environments — builds for ESP32 (`esp32dev`, …) and nRF52840
@@ -535,18 +441,6 @@ Arduino / PlatformIO, in [`examples/`](examples/):
- **Receiver**: Receive ESPNow packets from a Repeater and display data - **Receiver**: Receive ESPNow packets from a Repeater and display data
- **FakeRepeater**: Generate test ESPNow packets without real Victron hardware - **FakeRepeater**: Generate test ESPNow packets without real Victron hardware
Zephyr, in [`samples/`](samples/):
- **scan**: List every Victron device advertising nearby. No keys needed —
run this first to find your MAC addresses.
- **observer**: Monitor known devices and log every decoded field. The
reference for the Zephyr API.
No hardware at all, in [`examples/`](examples/):
- **NativeDecode**: Decode an advertisement on your PC with plain `make`.
Good for checking a key or a sniffer capture before you flash anything.
## Contributing ## Contributing
The primary repository is hosted on [Gitea](https://gitea.sh3d.com.au/Sh3d/VictronBLE), The primary repository is hosted on [Gitea](https://gitea.sh3d.com.au/Sh3d/VictronBLE),
-23
View File
@@ -38,32 +38,9 @@ Pure C core + Zephyr support. One repo now serves three ecosystems: Arduino
a west project or via `-DZEPHYR_EXTRA_MODULES=<path>`; see a west project or via `-DZEPHYR_EXTRA_MODULES=<path>`; see
`docs/ZEPHYR_PORT.md` for the porting plan this implements. `docs/ZEPHYR_PORT.md` for the porting plan this implements.
- Watch mode (`victronble_watch_set(true)`): logs every Victron product
advert heard, registered or not — MAC, RSSI, record type, length and
key-check byte. Reads only the plaintext header, so it needs no keys.
Discovery and key debugging; `samples/scan` is built around it.
### Samples and examples
- `samples/observer/` — Zephyr reference app: known devices from a table of
MAC + key, full field decode for every device type, and a 30-second stats
line for diagnosing a quiet console. Builds for `nrf52840dk/nrf52840` and
`rak4631/nrf52840`.
- `samples/scan/` — Zephyr discovery app: watch mode only, no keys or
addresses needed. Run it first to find what you have.
- Both carry a `sample.yaml`, so `west twister -T samples` build-tests them.
- `examples/NativeDecode/` — decode an advertisement on a PC with plain
`make`. No board, no BLE stack; exercises the pure C core directly, which
makes it useful for checking a key or a sniffer capture.
### Fixed ### Fixed
- `library.properties` URL now points at the real repo (gitea) instead of a - `library.properties` URL now points at the real repo (gitea) instead of a
nonexistent GitHub mirror. nonexistent GitHub mirror.
- `library.json` no longer claims the `espidf` framework. There is no ESP-IDF
BLE backend — `src/esp32/` is Arduino/Bluedroid only — so the claim was
never true. The pure C core works fine under ESP-IDF; scanning is the
missing piece, and a native backend is future work.
- `QUICK_START.md` pointed at `examples/MultiDevice/main.cpp`; the file is at
`examples/MultiDevice/src/main.cpp`.
## 0.6.0 (2026-06-04) ## 0.6.0 (2026-06-04)
+5 -12
View File
@@ -344,13 +344,12 @@ victronble/
├── src/ ├── src/
│ ├── victronble_core.c # pure C99, no dependencies │ ├── victronble_core.c # pure C99, no dependencies
│ ├── victronble_aes_sw.c │ ├── victronble_aes_sw.c
│ ├── victronble_aes_psa.c # not implemented — Kconfig ships software only │ ├── victronble_aes_psa.c
│ ├── victronble_zephyr.c # scan + workqueue + device registry │ ├── victronble_zephyr.c # scan + workqueue + device registry
│ ├── VictronBLE.cpp # Arduino wrapper │ ├── VictronBLE.cpp # Arduino wrapper
│ └── ble_backend_*.cpp # NimBLE / Bluefruit │ └── ble_backend_*.cpp # NimBLE / Bluefruit
├── samples/ ├── samples/
── observer/ # Zephyr sample app — known devices, full records ── observer/ # Zephyr sample app
│ └── scan/ # Zephyr sample app — watch mode discovery
└── tests/ └── tests/
└── vectors/ # host-runnable, also Ztest under native_sim └── vectors/ # host-runnable, also Ztest under native_sim
``` ```
@@ -521,17 +520,11 @@ configure — it will pay for itself during the record-type work.
## Stage 5 — Publish ## Stage 5 — Publish
1. **Done.** `samples/observer/` and `samples/scan/` build for 1. `samples/observer/` that builds for `nrf52840dk/nrf52840` and `rak4631/nrf52840`.
`nrf52840dk/nrf52840` and `rak4631/nrf52840` (verified against Zephyr
v4.4.0), each with a `sample.yaml` so twister can build-test them:
`west twister -T samples -p nrf52840dk/nrf52840 -p rak4631/nrf52840`.
`examples/NativeDecode/` covers the no-hardware case with plain `make`.
A sample that builds for a DK anyone owns is what makes people try it. A sample that builds for a DK anyone owns is what makes people try it.
2. GitHub Actions: host vector tests, plus `west build` for both boards and `native_sim`. 2. GitHub Actions: host vector tests, plus `west build` for both boards and `native_sim`.
3. **Done.** README has a Zephyr section with the west manifest snippet, the 3. README with the west manifest snippet up front — the first question every Zephyr user
`ZEPHYR_EXTRA_MODULES` alternative for local development, the API summary has is how to add it to their workspace:
and the Kconfig table — the first question every Zephyr user has is how to
add it to their workspace:
```yaml ```yaml
manifest: manifest:
-30
View File
@@ -1,30 +0,0 @@
# Build the native (host) VictronBLE example.
#
# make build ./nativedecode
# make run build and decode the built-in sample advert
# make clean
#
# No board, no PlatformIO, no toolchain beyond a C compiler — the library core
# is plain C99. Mirrors the build line in tests/vectors/run.sh.
ROOT := ../..
CC ?= cc
CFLAGS ?= -std=c99 -Wall -Wextra -O2
LDLIBS := -lm
SRCS := \
$(ROOT)/src/victronble_core.c \
$(ROOT)/src/victronble_aes_sw.c \
$(ROOT)/src/crypto/vble_aes.c \
main.c
nativedecode: $(SRCS)
$(CC) $(CFLAGS) -I$(ROOT)/include -I$(ROOT)/src $(SRCS) $(LDLIBS) -o $@
run: nativedecode
./nativedecode
clean:
rm -f nativedecode
.PHONY: run clean
-84
View File
@@ -1,84 +0,0 @@
# NativeDecode — decode an advertisement on your PC
Decodes one Victron "Instant Readout" advertisement on the host. No board, no
BLE stack, no PlatformIO — just a C compiler. The library's core
(`src/victronble_core.c`) is plain C99 with no dependencies, so the same
decoder that runs on an ESP32, an nRF52 or under Zephyr also runs here.
Handy for:
- checking an advertisement key before you flash anything
- decoding a capture from nRF Connect, `btmon` or a sniffer
- seeing the record layout and which fields your device actually sends
## Build and run
```sh
make
./nativedecode
```
With no arguments it decodes a built-in sample advert (a SmartSolar MPPT in
bulk charge, taken from `tests/vectors/`):
```
advert 31 bytes
device type solar charger (0x01)
model id 0xa060
nonce 4660 (0x1234)
fields:
state bulk (error 0)
battery 13.24 V
current 5.4 A
pv power 340 W
yield today 1200 Wh
load current n/a
```
## Your own capture
```sh
./nativedecode <advert-hex> <key-hex>
```
`advert-hex` is the manufacturer-specific data **starting at the company ID**
(`e1 02 ...`), exactly as a sniffer reports it. Separators are ignored, so
`e1:02:10…` and `e10210…` both work. `key-hex` is the 32-character
advertisement key from VictronConnect → device → gear icon → Product info →
*Instant readout via Bluetooth*.
```sh
./nativedecode "e1021089a30002efbe0d4108532f0d44a51c62a051e97c1fae2fe9e82a0e15" \
0df4d0395b7d5d4f5a0d0af52e1b4c1e
```
## Reading the output
`n/a` means the device did not send that field — the core returns `NAN` for
absent floats and `0xFFFF` for an unavailable time-to-go, and this example
renders both as `n/a`. An MPPT with no load output always shows
`load current n/a`; that is not a fault.
Three failure messages are worth telling apart:
| Message | Meaning |
|---|---|
| `not a Victron product advertisement` | wrong company ID, or not a product record — you captured something else |
| `key check failed` | the advert is Victron's, but this key belongs to a different device |
| `decode failed: unsupported type` | a real Victron record the library has no decoder for yet (e.g. GX devices) |
## Which API this shows
```c
victronble_parse_key() /* 32 hex chars -> 16 bytes */
victronble_is_product_adv() /* cheap pre-filter, no crypto */
victronble_key_matches() /* key-check byte, still no crypto */
victronble_decode() /* decrypt + parse into a record */
victronble_strerror()
victronble_device_type_str()
victronble_state_str()
```
That is the whole portable core. See `include/victronble.h` for the record
structures, and `samples/observer/` for the same printing logic driven by a
live BLE scan under Zephyr.
-251
View File
@@ -1,251 +0,0 @@
/**
* VictronBLE native (host) example.
*
* Decodes a Victron "Instant Readout" advertisement on your PC — no board, no
* BLE stack, no toolchain beyond a C compiler. The library's core is plain
* C99, so the same code that runs on an ESP32, an nRF52 or under Zephyr also
* runs here.
*
* Useful for checking a key, understanding the record layout, or debugging a
* capture from a BLE sniffer before you flash anything.
*
* make && ./nativedecode # built-in sample advert
* ./nativedecode <advert-hex> <key-hex> # your own capture
*
* The advert hex is the manufacturer-specific data starting at the company ID
* (e1 02 ...), exactly as a sniffer or nRF Connect reports it. Separators are
* ignored, so "e1:02:10" and "e10210" both work.
*
* Copyright (c) 2026 Scott Penrose
* License: MIT
*/
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "victronble.h"
/*
* Sample advertisement from the library's own test vectors
* (tests/vectors/test_vectors.h): a SmartSolar MPPT in bulk charge.
*/
static const char DEFAULT_ADVERT[] =
"e1021060a000013412 0d53b0254c65d34a923470 3a6c19737e65f6a442d056";
static const char DEFAULT_KEY[] = "0df4d0395b7d5d4f5a0d0af52e1b4c1e";
/* --- input parsing ----------------------------------------------------- */
static int hex_nibble(char c)
{
if (c >= '0' && c <= '9') {
return c - '0';
}
if (c >= 'a' && c <= 'f') {
return c - 'a' + 10;
}
if (c >= 'A' && c <= 'F') {
return c - 'A' + 10;
}
return -1;
}
/*
* Parse a hex string into bytes, skipping any separator (space, colon, dash).
* Returns the byte count, or -1 on a stray character or an odd digit count.
* (Keys are 32 hex characters exactly, so those use the library's own
* victronble_parse_key() instead of this.)
*/
static int parse_hex(const char *s, uint8_t *out, size_t max)
{
size_t n = 0;
int hi = -1;
for (; *s != '\0'; s++) {
if (*s == ' ' || *s == ':' || *s == '-' || *s == '\t') {
continue;
}
int v = hex_nibble(*s);
if (v < 0) {
fprintf(stderr, "bad hex character '%c'\n", *s);
return -1;
}
if (hi < 0) {
hi = v;
continue;
}
if (n >= max) {
fprintf(stderr, "advert too long (max %zu bytes)\n",
max);
return -1;
}
out[n++] = (uint8_t)((hi << 4) | v);
hi = -1;
}
if (hi >= 0) {
fprintf(stderr, "hex string has an odd number of digits\n");
return -1;
}
return (int)n;
}
/* --- record printing --------------------------------------------------- */
/* Fields the device did not send come back as NAN — report them as "n/a"
* rather than printing "nan", which reads like a fault. */
static void pf(const char *label, float v, const char *unit, int dp)
{
if (isnan(v)) {
printf(" %-16s n/a\n", label);
} else {
printf(" %-16s %.*f %s\n", label, dp, (double)v, unit);
}
}
static void print_solar(const victronble_solar_charger_t *s)
{
printf(" %-16s %s (error %u)\n", "state",
victronble_state_str(s->state), s->error);
pf("battery", s->battery_voltage, "V", 2);
pf("current", s->battery_current, "A", 1);
pf("pv power", s->pv_power, "W", 0);
printf(" %-16s %u Wh\n", "yield today", s->yield_today_wh);
pf("load current", s->load_current, "A", 1);
}
static void print_batmon(const victronble_battery_monitor_t *m)
{
static const char *const aux_mode[] = { "aux voltage", "midpoint",
"temperature", "none" };
pf("battery", m->voltage, "V", 2);
pf("current", m->current, "A", 2);
pf("soc", m->soc, "%", 1);
pf("consumed", m->consumed_ah, "Ah", 1);
/* 0xFFFF is the wire's "not available", not 45 days of runtime. */
if (m->remaining_minutes == 0xFFFF) {
printf(" %-16s n/a\n", "time to go");
} else {
printf(" %-16s %u min\n", "time to go", m->remaining_minutes);
}
printf(" %-16s %s\n", "aux mode",
m->aux_mode < 4 ? aux_mode[m->aux_mode] : "?");
pf("aux voltage", m->aux_voltage, "V", 2);
pf("temperature", m->temperature, "degC", 1);
printf(" %-16s 0x%04x\n", "alarm", m->alarm);
}
static void print_record(const victronble_record_t *rec)
{
printf("device type %s (0x%02x)\n",
victronble_device_type_str(rec->type), rec->record_type);
printf("model id 0x%04x\n", rec->model_id);
printf("nonce %u (0x%04x)\n", rec->nonce, rec->nonce);
printf("fields:\n");
switch (rec->type) {
case VICTRONBLE_DEV_SOLAR_CHARGER:
print_solar(&rec->u.solar);
break;
case VICTRONBLE_DEV_BATTERY_MONITOR:
print_batmon(&rec->u.batmon);
break;
case VICTRONBLE_DEV_INVERTER:
printf(" %-16s %s\n", "state",
victronble_state_str(rec->u.inverter.state));
pf("battery", rec->u.inverter.battery_voltage, "V", 2);
pf("current", rec->u.inverter.battery_current, "A", 2);
pf("ac power", rec->u.inverter.ac_power, "W", 0);
printf(" %-16s 0x%02x\n", "alarms", rec->u.inverter.alarms);
break;
case VICTRONBLE_DEV_DCDC_CONVERTER:
printf(" %-16s %s (error %u)\n", "state",
victronble_state_str(rec->u.dcdc.state),
rec->u.dcdc.error);
pf("input", rec->u.dcdc.input_voltage, "V", 2);
pf("output", rec->u.dcdc.output_voltage, "V", 2);
pf("output current", rec->u.dcdc.output_current, "A", 1);
break;
case VICTRONBLE_DEV_AC_CHARGER:
printf(" %-16s %s (error %u)\n", "state",
victronble_state_str(rec->u.ac.state), rec->u.ac.error);
pf("output 1", rec->u.ac.voltage1, "V", 2);
pf("current 1", rec->u.ac.current1, "A", 1);
pf("output 2", rec->u.ac.voltage2, "V", 2);
pf("current 2", rec->u.ac.current2, "A", 1);
pf("output 3", rec->u.ac.voltage3, "V", 2);
pf("current 3", rec->u.ac.current3, "A", 1);
pf("ac current", rec->u.ac.ac_current, "A", 1);
pf("temperature", rec->u.ac.temperature, "degC", 1);
break;
default:
printf(" (decoded, but this build has no field printer for "
"that device type)\n");
break;
}
}
/* --- main -------------------------------------------------------------- */
int main(int argc, char **argv)
{
const char *advert_hex = argc > 1 ? argv[1] : DEFAULT_ADVERT;
const char *key_hex = argc > 2 ? argv[2] : DEFAULT_KEY;
uint8_t advert[VICTRONBLE_MIN_MFG_LEN + VICTRONBLE_MAX_CIPHER_LEN];
uint8_t key[VICTRONBLE_KEY_LEN];
victronble_record_t rec;
victronble_err_t err;
int len;
if (argc > 3 || (argc == 2 && strcmp(argv[1], "-h") == 0)) {
fprintf(stderr, "usage: %s [advert-hex [key-hex]]\n", argv[0]);
return 2;
}
if (argc == 1) {
printf("(no arguments — decoding the built-in sample advert)\n\n");
}
len = parse_hex(advert_hex, advert, sizeof(advert));
if (len < 0) {
return 1;
}
if (!victronble_parse_key(key_hex, key)) {
fprintf(stderr, "key must be exactly 32 hex characters\n");
return 1;
}
printf("advert %d bytes\n", len);
/* Cheap pre-filter: company ID and record type only, no crypto. On a
* real scanner this is what keeps every other BLE beacon out of the
* decode path. */
if (!victronble_is_product_adv(advert, (size_t)len)) {
printf("not a Victron product advertisement\n");
return 1;
}
/* Key-check byte. Lets a scanner pick the right key out of several
* without doing the AES work — and tells you a wrong key apart from a
* corrupt payload. */
if (!victronble_key_matches(advert, (size_t)len, key)) {
printf("key check failed — this key is not for this device\n");
return 1;
}
err = victronble_decode(advert, (size_t)len, key, &rec);
if (err != VICTRONBLE_OK) {
printf("decode failed: %s (%d)\n", victronble_strerror(err),
err);
return 1;
}
print_record(&rec);
return 0;
}
-6
View File
@@ -56,12 +56,6 @@ int victronble_device_add(const bt_addr_le_t *addr,
/** Stop monitoring a device. Returns -ENOENT if unknown. */ /** Stop monitoring a device. Returns -ENOENT if unknown. */
int victronble_device_remove(const bt_addr_le_t *addr); int victronble_device_remove(const bt_addr_le_t *addr);
/** Watch mode: when on, every Victron product advert heard — registered or
* not — is logged (MAC, RSSI, record type, key-check byte). Discovery and
* key debugging; unregistered devices are not nonce-deduped, so expect a
* line or two per device per second. */
void victronble_watch_set(bool on);
/** Start the passive scan (bt_enable() must have succeeded first). */ /** Start the passive scan (bt_enable() must have succeeded first). */
int victronble_start(void); int victronble_start(void);
+3 -11
View File
@@ -1,8 +1,8 @@
{ {
"name": "victronble", "name": "victronble",
"version": "0.7.0", "version": "0.7.0",
"description": "Portable Arduino library for reading Victron Energy device data via Bluetooth Low Energy (BLE) advertisements. Runs on ESP32, ESP32-S3, ESP32-C3 and nRF52 (nRF52840, nRF52832). Supports SmartSolar MPPT, SmartShunt, BMV, MultiPlus, Orion, Blue Smart AC chargers and other Victron devices. No external crypto dependency. Also ships a Zephyr module (CONFIG_VICTRONBLE) and a dependency-free pure C99 core usable on any platform.", "description": "Portable Arduino library for reading Victron Energy device data via Bluetooth Low Energy (BLE) advertisements. Runs on ESP32, ESP32-S3, ESP32-C3 and nRF52 (nRF52840, nRF52832). Supports SmartSolar MPPT, SmartShunt, BMV, MultiPlus, Orion, Blue Smart AC chargers and other Victron devices. No external crypto dependency.",
"keywords": "victron, ble, zephyr, bluetooth, solar, mppt, battery, smartshunt, smartsolar, bmv, inverter, multiplus, esp32, esp32-s3, esp32-c3, nrf52, nrf52840, nrf52832, xiao, iot, energy, monitoring", "keywords": "victron, ble, bluetooth, solar, mppt, battery, smartshunt, smartsolar, bmv, inverter, multiplus, esp32, esp32-s3, esp32-c3, nrf52, nrf52840, nrf52832, xiao, iot, energy, monitoring",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://gitea.sh3d.com.au/Sh3d/VictronBLE.git" "url": "https://gitea.sh3d.com.au/Sh3d/VictronBLE.git"
@@ -17,7 +17,7 @@
], ],
"license": "MIT", "license": "MIT",
"homepage": "https://gitea.sh3d.com.au/Sh3d/VictronBLE", "homepage": "https://gitea.sh3d.com.au/Sh3d/VictronBLE",
"frameworks": ["arduino"], "frameworks": ["arduino", "espidf"],
"platforms": ["espressif32", "nordicnrf52"], "platforms": ["espressif32", "nordicnrf52"],
"headers": ["VictronBLE.h"], "headers": ["VictronBLE.h"],
"dependencies": [], "dependencies": [],
@@ -46,19 +46,11 @@
"name": "FakeRepeater", "name": "FakeRepeater",
"base": "examples/FakeRepeater", "base": "examples/FakeRepeater",
"files": ["src/main.cpp"] "files": ["src/main.cpp"]
},
{
"name": "NativeDecode",
"base": "examples/NativeDecode",
"files": ["main.c", "Makefile", "README.md"]
} }
], ],
"export": { "export": {
"exclude": [ "exclude": [
"examples/*/.pio", "examples/*/.pio",
"examples/NativeDecode/nativedecode",
"samples/*/build",
"experiment",
"examples/*/.vscode", "examples/*/.vscode",
"examples/*/test", "examples/*/test",
"test", "test",
-44
View File
@@ -1,44 +0,0 @@
# Zephyr samples
Zephyr applications live here; `examples/` holds the Arduino/PlatformIO ones.
| Sample | What it does |
|---|---|
| [`scan/`](scan/) | Lists every Victron device advertising nearby. No keys needed. **Start here.** |
| [`observer/`](observer/) | Monitors known devices and decodes their records. |
Both need `CONFIG_VICTRONBLE=y`, which depends on `CONFIG_BT_OBSERVER=y`. See
the Zephyr section of the top-level [README](../README.md) for how to add this
library to a west workspace.
## Building
The library is a Zephyr module. If it is already in your `west.yml`, the
samples build with no extra flags:
```sh
west build -p -b nrf52840dk/nrf52840 samples/observer
```
For local development against a checkout that is *not* in the manifest, point
Zephyr at it directly:
```sh
west build -p -b nrf52840dk/nrf52840 -d /tmp/vb_obs \
/path/to/VictronBLE/samples/observer \
-- -DZEPHYR_EXTRA_MODULES=/path/to/VictronBLE
```
Then `west flash`, and watch the console at 115200 baud.
Tested on `nrf52840dk/nrf52840` and `rak4631/nrf52840` with Zephyr v4.4.0. Any
board with a Bluetooth controller and the observer role should work — nothing
in the library is nRF-specific.
Build-test both samples without hardware. For a checkout outside the manifest,
twister needs the same module hint via the environment:
```sh
ZEPHYR_EXTRA_MODULES=$PWD \
west twister -T samples -p nrf52840dk/nrf52840 -p rak4631/nrf52840
```
-7
View File
@@ -1,7 +0,0 @@
# SPDX-License-Identifier: MIT
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(victronble_observer)
target_sources(app PRIVATE src/main.c)
-88
View File
@@ -1,88 +0,0 @@
# observer — decode records from known devices
Monitors a list of Victron devices, decrypts their Instant Readout
advertisements and logs every field. This is the reference for using the
Zephyr API.
Don't know your devices' addresses yet? Build [`../scan`](../scan/) first.
## Configure your devices
Edit the `known_devices` table at the top of [`src/main.c`](src/main.c):
```c
static const struct { ... } known_devices[] = {
{
.name = "Rainbow48V",
.addr = "E4:05:42:34:14:F3",
.addr_type = "random",
.key = "0ec3adf7433dd61793ff2f3b8ad32ed8",
},
};
```
The key is in VictronConnect: device → gear icon → Product info → *Instant
readout via Bluetooth* → show/copy the encryption key (32 hex characters).
**The address type must be `random`.** Victron devices use random static
Bluetooth addresses, and the registry lookup compares the type as well as the
bytes. Get it wrong and the symptom is quiet and confusing: `adverts` climbs
in the stats line but `decoded` stays at zero, because the adverts arrive and
never match a registered device.
Add more than four devices and you also need to raise
`CONFIG_VICTRONBLE_MAX_DEVICES` in `prj.conf`.
## Build and run
```sh
west build -p -b nrf52840dk/nrf52840 -d /tmp/vb_obs samples/observer \
-- -DZEPHYR_EXTRA_MODULES=$PWD
west flash -d /tmp/vb_obs
```
Console (115200 baud):
```
<inf> observer: VictronBLE observer starting
<inf> observer: monitoring Rainbow48V (E4:05:42:34:14:F3)
<inf> victronble: observing (interval 2048 window 18)
<inf> observer: E4:05:42:34:14:F3 (random) solar charger rssi -67 model 0xa060 nonce 4660
<inf> observer: state bulk error 0
<inf> observer: battery 13.24 V 5.4 A pv 340 W yield today 1200 Wh
<inf> observer: load n/a
<inf> observer: stats: adverts 61 queued 30 dropped 0 decoded 28 dup 2 err 0
```
## Reading the stats line
Printed every 30 s, and the fastest way to diagnose a quiet console:
| Counter | Meaning if it misbehaves |
|---|---|
| `adverts` | Victron adverts seen from **any** device. Zero → nothing in range, or Instant Readout is off on the device. |
| `queued` | adverts from your registered devices. Zero while `adverts` climbs → wrong address or wrong address type. |
| `dropped` | queue was full. Raise `CONFIG_VICTRONBLE_QUEUE_DEPTH`. |
| `decoded` | records delivered to the callback. |
| `dup` | repeats suppressed by nonce dedup. A steady trickle is normal — each advert is broadcast on three channels. |
| `err` | decode failures. A wrong key shows up here, and in the `decode failed: key mismatch` warning. |
## What the code demonstrates
- `bt_enable()` **before** `victronble_start()` — the application owns the
Bluetooth stack, the library only scans.
- `bt_addr_le_from_str()` + `victronble_parse_key()` to turn human-readable
config into what `victronble_device_add()` wants.
- `victronble_cb_register()` with both `record` and `decode_error` set.
Callbacks run on the library's decode thread, not the Bluetooth RX thread,
so logging in them is safe.
- Formatting a record: `isnan()` guards on every float, and
`remaining_minutes == 0xFFFF` for time-to-go. Absent fields are normal —
an MPPT with no load output always reports `load n/a`.
- `victronble_get_stats()` for the periodic health line.
Printing floats needs `CONFIG_CBPRINTF_FP_SUPPORT=y`; without it the numbers
come out empty.
To decode a captured advert on your PC instead — no board involved — see
[`examples/NativeDecode`](../../examples/NativeDecode/).
-19
View File
@@ -1,19 +0,0 @@
# Bluetooth: observer role only — no connections, no pairing, no advertising.
CONFIG_BT=y
CONFIG_BT_OBSERVER=y
CONFIG_BT_DEVICE_NAME="victron-observer"
# Advertising reports are "discardable" HCI events. The default pool is easy
# to exhaust with a passive scan in a dense RF environment; if the stats line
# shows adverts arriving in bursts and then stalling, raise this further.
CONFIG_BT_BUF_EVT_DISCARDABLE_COUNT=20
CONFIG_VICTRONBLE=y
CONFIG_VICTRONBLE_MAX_DEVICES=4
CONFIG_LOG=y
CONFIG_VICTRONBLE_LOG_LEVEL_INF=y
# Records carry floats (volts, amps, watts). Without this, %f in LOG_INF()
# and snprintk() prints nothing useful.
CONFIG_CBPRINTF_FP_SUPPORT=y
-10
View File
@@ -1,10 +0,0 @@
sample:
name: VictronBLE observer
description: Decode Victron Instant Readout advertisements from known devices
tests:
sample.victronble.observer:
build_only: true
platform_allow:
- nrf52840dk/nrf52840
- rak4631/nrf52840
tags: bluetooth victron
-278
View File
@@ -1,278 +0,0 @@
/**
* VictronBLE Zephyr observer sample.
*
* Monitors a fixed list of Victron devices, decodes their Instant Readout
* advertisements and logs every record. Prints a statistics line every 30 s
* so a silent console can be diagnosed without a sniffer.
*
* Don't know your devices' MAC addresses yet? Build samples/scan first — it
* lists every Victron device in range.
*
* Copyright (c) 2026 Scott Penrose
* License: MIT
*/
#include <math.h>
#include <string.h>
#include <zephyr/kernel.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/util.h>
#include "victronble_zephyr.h"
LOG_MODULE_REGISTER(observer, LOG_LEVEL_INF);
#define STATS_INTERVAL K_SECONDS(30)
/*
* Your devices. The advertisement key is in VictronConnect:
* device -> gear icon -> Product info -> "Instant readout via Bluetooth"
* -> SHOW / copy the encryption key (32 hex characters).
*
* Victron devices use a random static Bluetooth address, so the address type
* is "random" — not "public". Getting this wrong is the usual reason a device
* never matches: the adverts arrive (the stats line counts them) but no
* record is ever decoded, because the registry lookup compares the type too.
*/
static const struct {
const char *name;
const char *addr;
const char *addr_type;
const char *key;
} known_devices[] = {
{
.name = "Rainbow48V",
.addr = "E4:05:42:34:14:F3",
.addr_type = "random",
.key = "0ec3adf7433dd61793ff2f3b8ad32ed8",
},
{
.name = "ScottTrailer",
.addr = "E6:45:59:78:3C:FB",
.addr_type = "random",
.key = "3fa658aded4f309b9bc17a2318cb1f56",
},
};
/* --- record formatting ------------------------------------------------- */
#define FBUF_LEN 20
/*
* A float field the device did not send comes back as NAN (see
* include/victronble.h). Render that as "n/a" rather than letting "nan" leak
* into the log — a missing load-current reading is not a fault. The unit goes
* in here too, so an absent field reads "n/a" and not "n/a A".
*
* Needs CONFIG_CBPRINTF_FP_SUPPORT=y, or every number comes out empty.
*/
static const char *flt(char *buf, float v, int dp, const char *unit)
{
if (isnan(v)) {
strcpy(buf, "n/a");
} else {
snprintk(buf, FBUF_LEN, "%.*f %s", dp, (double)v, unit);
}
return buf;
}
static void print_solar(const victronble_solar_charger_t *s)
{
char a[FBUF_LEN], b[FBUF_LEN], c[FBUF_LEN], d[FBUF_LEN];
LOG_INF(" state %s error %u", victronble_state_str(s->state),
s->error);
LOG_INF(" battery %s %s pv %s yield today %u Wh",
flt(a, s->battery_voltage, 2, "V"),
flt(b, s->battery_current, 1, "A"),
flt(c, s->pv_power, 0, "W"), s->yield_today_wh);
LOG_INF(" load %s", flt(d, s->load_current, 1, "A"));
}
static void print_batmon(const victronble_battery_monitor_t *m)
{
char a[FBUF_LEN], b[FBUF_LEN], c[FBUF_LEN], d[FBUF_LEN];
LOG_INF(" battery %s %s soc %s", flt(a, m->voltage, 2, "V"),
flt(b, m->current, 2, "A"), flt(c, m->soc, 1, "%"));
LOG_INF(" consumed %s alarm 0x%04x",
flt(d, m->consumed_ah, 1, "Ah"), m->alarm);
/* 0xFFFF is the wire's "not available", not 45 days of runtime. */
if (m->remaining_minutes == 0xFFFF) {
LOG_INF(" time to go n/a");
} else {
LOG_INF(" time to go %u min", m->remaining_minutes);
}
/* The aux channel is one of three things, chosen on the device. */
switch (m->aux_mode) {
case 0:
LOG_INF(" aux voltage %s", flt(a, m->aux_voltage, 2, "V"));
break;
case 2:
LOG_INF(" temperature %s", flt(a, m->temperature, 1, "degC"));
break;
default:
break;
}
}
static void print_record(const bt_addr_le_t *addr, int8_t rssi,
const victronble_record_t *rec)
{
char addr_str[BT_ADDR_LE_STR_LEN];
char a[FBUF_LEN], b[FBUF_LEN], c[FBUF_LEN];
bt_addr_le_to_str(addr, addr_str, sizeof(addr_str));
LOG_INF("%s %s rssi %d model 0x%04x nonce %u", addr_str,
victronble_device_type_str(rec->type), rssi, rec->model_id,
rec->nonce);
switch (rec->type) {
case VICTRONBLE_DEV_SOLAR_CHARGER:
print_solar(&rec->u.solar);
break;
case VICTRONBLE_DEV_BATTERY_MONITOR:
print_batmon(&rec->u.batmon);
break;
case VICTRONBLE_DEV_INVERTER:
LOG_INF(" state %s battery %s %s ac %s",
victronble_state_str(rec->u.inverter.state),
flt(a, rec->u.inverter.battery_voltage, 2, "V"),
flt(b, rec->u.inverter.battery_current, 2, "A"),
flt(c, rec->u.inverter.ac_power, 0, "W"));
break;
case VICTRONBLE_DEV_DCDC_CONVERTER:
LOG_INF(" state %s in %s out %s %s",
victronble_state_str(rec->u.dcdc.state),
flt(a, rec->u.dcdc.input_voltage, 2, "V"),
flt(b, rec->u.dcdc.output_voltage, 2, "V"),
flt(c, rec->u.dcdc.output_current, 1, "A"));
break;
case VICTRONBLE_DEV_AC_CHARGER:
LOG_INF(" state %s out1 %s %s temp %s",
victronble_state_str(rec->u.ac.state),
flt(a, rec->u.ac.voltage1, 2, "V"),
flt(b, rec->u.ac.current1, 1, "A"),
flt(c, rec->u.ac.temperature, 1, "degC"));
break;
default:
LOG_INF(" (no decoder for record type 0x%02x)",
rec->record_type);
break;
}
}
/* --- callbacks --------------------------------------------------------- */
/*
* These run on the victronble decode thread, not the Bluetooth RX thread, so
* logging here is fine — it cannot stall the controller.
*/
static void on_record(const bt_addr_le_t *addr, int8_t rssi,
const victronble_record_t *rec)
{
print_record(addr, rssi, rec);
}
static void on_decode_error(const bt_addr_le_t *addr, victronble_err_t err)
{
char addr_str[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(addr, addr_str, sizeof(addr_str));
LOG_WRN("%s decode failed: %s", addr_str, victronble_strerror(err));
}
static struct victronble_cb callbacks = {
.record = on_record,
.decode_error = on_decode_error,
};
/* --- setup ------------------------------------------------------------- */
static int register_devices(void)
{
int registered = 0;
for (size_t i = 0; i < ARRAY_SIZE(known_devices); i++) {
bt_addr_le_t addr;
uint8_t key[VICTRONBLE_KEY_LEN];
int err;
err = bt_addr_le_from_str(known_devices[i].addr,
known_devices[i].addr_type, &addr);
if (err != 0) {
LOG_ERR("%s: bad address '%s' (%d)",
known_devices[i].name, known_devices[i].addr,
err);
continue;
}
if (!victronble_parse_key(known_devices[i].key, key)) {
LOG_ERR("%s: key must be 32 hex characters",
known_devices[i].name);
continue;
}
err = victronble_device_add(&addr, key);
if (err != 0) {
LOG_ERR("%s: victronble_device_add failed (%d)",
known_devices[i].name, err);
continue;
}
LOG_INF("monitoring %s (%s)", known_devices[i].name,
known_devices[i].addr);
registered++;
}
return registered;
}
int main(void)
{
struct victronble_stats stats;
int err;
LOG_INF("VictronBLE observer starting");
/* The application owns the Bluetooth stack: victronble_start() needs
* it up already. */
err = bt_enable(NULL);
if (err != 0) {
LOG_ERR("bt_enable failed (%d)", err);
return 0;
}
err = victronble_cb_register(&callbacks);
if (err != 0) {
LOG_ERR("victronble_cb_register failed (%d)", err);
return 0;
}
if (register_devices() == 0) {
LOG_ERR("no devices registered — nothing to observe");
return 0;
}
err = victronble_start();
if (err != 0) {
LOG_ERR("victronble_start failed (%d)", err);
return 0;
}
while (1) {
k_sleep(STATS_INTERVAL);
victronble_get_stats(&stats);
LOG_INF("stats: adverts %u queued %u dropped %u decoded %u dup %u err %u",
stats.adverts, stats.queued, stats.dropped,
stats.decoded, stats.duplicates, stats.errors);
}
return 0;
}
-7
View File
@@ -1,7 +0,0 @@
# SPDX-License-Identifier: MIT
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(victronble_scan)
target_sources(app PRIVATE src/main.c)
-58
View File
@@ -1,58 +0,0 @@
# scan — find your Victron devices
Logs every Victron "Instant Readout" advertisement in range. **No
advertisement keys and no device list required**, so this is the first thing
to run: it tells you what you have and what its Bluetooth address is.
The whole sample is `victronble_watch_set(true)` plus `victronble_start()`
all the output comes from the library's own log module.
## Build and run
```sh
west build -p -b nrf52840dk/nrf52840 -d /tmp/vb_scan samples/scan \
-- -DZEPHYR_EXTRA_MODULES=$PWD
west flash -d /tmp/vb_scan
```
Console (115200 baud):
```
<inf> scan: VictronBLE discovery — logging every Victron advert in range
<inf> victronble: observing (interval 96 window 48)
<inf> victronble: watch: E4:05:42:34:14:F3 (random) rssi -67 type 0x01 (solar charger) len 31 keycheck 0x0d
<inf> victronble: watch: E6:45:59:78:3C:FB (random) rssi -82 type 0x02 (battery monitor) len 31 keycheck 0x3f
<inf> scan: stats: adverts 61 queued 61 dropped 0
```
Each line gives you everything you need for `samples/observer`:
| Field | Use |
|---|---|
| address + `(random)` | Victron uses **random** static addresses — that address type matters |
| `type` | which device family it is, before any decryption |
| `keycheck` | first byte of the advertisement key; confirms you copied the right key |
| `rssi` | how well you can hear it — useful for placing the node |
Nothing here is decrypted. Watch mode reads only the plaintext header, which
is why it works without keys.
## Next step
Copy the addresses into the `known_devices` table in
[`../observer/src/main.c`](../observer/src/main.c) along with each device's key
from VictronConnect (device → gear icon → Product info → *Instant readout via
Bluetooth*), then build `observer`.
## Notes
- Unregistered devices are **not** deduplicated by nonce, so expect roughly
one line per device per second. That is the point — it shows liveness.
- `prj.conf` raises the scan duty cycle to 60 ms / 30 ms
(`BT_GAP_SCAN_FAST_*`) instead of the library default 1.28 s / 11.25 ms.
Discovery should be quick; `observer` uses the low-power default.
- `CONFIG_VICTRONBLE_QUEUE_DEPTH=16` because watch mode queues every advert,
not just the ones from known devices. If `dropped` climbs on a busy site,
raise it further.
- `adverts 0` after a minute means either nothing is in range or the device
has *Instant readout via Bluetooth* switched off in VictronConnect.
-25
View File
@@ -1,25 +0,0 @@
# Bluetooth: observer role only — no connections, no pairing, no advertising.
CONFIG_BT=y
CONFIG_BT_OBSERVER=y
CONFIG_BT_DEVICE_NAME="victron-scan"
# Discovery hears every Victron in range, so the advertising-report pool and
# the library's decode queue both see more traffic than in normal operation.
CONFIG_BT_BUF_EVT_DISCARDABLE_COUNT=20
CONFIG_VICTRONBLE=y
# Watch mode queues every Victron advert, not just the registered ones.
CONFIG_VICTRONBLE_QUEUE_DEPTH=16
# No devices are registered — this sample never decrypts anything.
CONFIG_VICTRONBLE_MAX_DEVICES=1
# Discovery wants to find things quickly, so trade power for a much higher
# duty cycle than the library's defaults: 60 ms interval / 30 ms window
# (BT_GAP_SCAN_FAST_*) instead of 1.28 s / 11.25 ms.
CONFIG_VICTRONBLE_SCAN_INTERVAL=96
CONFIG_VICTRONBLE_SCAN_WINDOW=48
CONFIG_LOG=y
CONFIG_VICTRONBLE_LOG_LEVEL_INF=y
-10
View File
@@ -1,10 +0,0 @@
sample:
name: VictronBLE scan
description: List every Victron device advertising nearby, no keys required
tests:
sample.victronble.scan:
build_only: true
platform_allow:
- nrf52840dk/nrf52840
- rak4631/nrf52840
tags: bluetooth victron
-66
View File
@@ -1,66 +0,0 @@
/**
* VictronBLE Zephyr discovery sample.
*
* Turns on the library's watch mode and does nothing else. No advertisement
* keys, no device list: every Victron Instant Readout advert in range is
* logged with its address, RSSI, device type and key-check byte.
*
* Run this first to find out what you have and what its MAC address is, then
* put the addresses and keys into samples/observer.
*
* Copyright (c) 2026 Scott Penrose
* License: MIT
*/
#include <zephyr/kernel.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/logging/log.h>
#include "victronble_zephyr.h"
LOG_MODULE_REGISTER(scan, LOG_LEVEL_INF);
#define STATS_INTERVAL K_SECONDS(30)
int main(void)
{
struct victronble_stats stats;
int err;
LOG_INF("VictronBLE discovery — logging every Victron advert in range");
/* The application owns the Bluetooth stack: victronble_start() needs
* it up already. */
err = bt_enable(NULL);
if (err != 0) {
LOG_ERR("bt_enable failed (%d)", err);
return 0;
}
/* All output comes from the library's own log module (victronble).
* Unregistered devices are not nonce-deduplicated, so expect roughly
* one line per device per second. */
victronble_watch_set(true);
err = victronble_start();
if (err != 0) {
LOG_ERR("victronble_start failed (%d)", err);
return 0;
}
while (1) {
k_sleep(STATS_INTERVAL);
victronble_get_stats(&stats);
LOG_INF("stats: adverts %u queued %u dropped %u", stats.adverts,
stats.queued, stats.dropped);
if (stats.adverts == 0) {
LOG_WRN("nothing heard yet — check the device has "
"'Instant readout via Bluetooth' enabled in "
"VictronConnect");
}
}
return 0;
}
+6 -40
View File
@@ -48,12 +48,6 @@ static struct k_mutex dev_mtx;
static sys_slist_t callbacks = SYS_SLIST_STATIC_INIT(&callbacks); static sys_slist_t callbacks = SYS_SLIST_STATIC_INIT(&callbacks);
static struct victronble_stats stats; static struct victronble_stats stats;
static bool scanning; static bool scanning;
static bool watch_mode;
void victronble_watch_set(bool on)
{
watch_mode = on;
}
static struct vb_device *find_device(const bt_addr_le_t *addr) static struct vb_device *find_device(const bt_addr_le_t *addr)
{ {
@@ -86,9 +80,8 @@ static bool ad_cb(struct bt_data *data, void *user_data)
stats.adverts++; stats.adverts++;
/* Registry check is a handful of compares — cheap enough here, and /* Registry check is a handful of compares — cheap enough here, and
* it keeps other people's Victrons out of the queue. Watch mode * it keeps other people's Victrons out of the queue. */
* queues everything so the decode thread can log it. */ if (find_device(ctx->addr) == NULL) {
if (!watch_mode && find_device(ctx->addr) == NULL) {
return false; return false;
} }
@@ -128,17 +121,8 @@ static void decode_frame(const struct vb_frame *frame)
k_mutex_lock(&dev_mtx, K_FOREVER); k_mutex_lock(&dev_mtx, K_FOREVER);
struct vb_device *dev = find_device(&frame->addr); struct vb_device *dev = find_device(&frame->addr);
if (dev == NULL) { /* unregistered (watch mode) or removed while queued */ if (dev == NULL) { /* removed while queued */
k_mutex_unlock(&dev_mtx); k_mutex_unlock(&dev_mtx);
if (watch_mode && frame->len > 9) {
char mac[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(&frame->addr, mac, sizeof(mac));
LOG_INF("watch: %s rssi %d type 0x%02x (%s) len %u keycheck 0x%02x",
mac, frame->rssi, frame->data[6],
victronble_device_type_str(frame->data[6]),
frame->len, frame->data[9]);
}
return; return;
} }
memcpy(key, dev->key, sizeof(key)); memcpy(key, dev->key, sizeof(key));
@@ -153,15 +137,7 @@ static void decode_frame(const struct vb_frame *frame)
if (err != VICTRONBLE_OK) { if (err != VICTRONBLE_OK) {
stats.errors++; stats.errors++;
if (watch_mode) { LOG_DBG("decode failed: %s", victronble_strerror(err));
char mac[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(&frame->addr, mac, sizeof(mac));
LOG_INF("watch: %s decode failed: %s", mac,
victronble_strerror(err));
} else {
LOG_DBG("decode failed: %s", victronble_strerror(err));
}
SYS_SLIST_FOR_EACH_CONTAINER(&callbacks, cb, node) { SYS_SLIST_FOR_EACH_CONTAINER(&callbacks, cb, node) {
if (cb->decode_error != NULL) { if (cb->decode_error != NULL) {
cb->decode_error(&frame->addr, err); cb->decode_error(&frame->addr, err);
@@ -185,18 +161,8 @@ static void decode_frame(const struct vb_frame *frame)
k_mutex_unlock(&dev_mtx); k_mutex_unlock(&dev_mtx);
stats.decoded++; stats.decoded++;
if (watch_mode) { LOG_DBG("%s record, nonce 0x%04x, rssi %d",
char mac[BT_ADDR_LE_STR_LEN]; victronble_device_type_str(rec.type), rec.nonce, frame->rssi);
bt_addr_le_to_str(&frame->addr, mac, sizeof(mac));
LOG_INF("watch: %s decoded %s nonce 0x%04x rssi %d", mac,
victronble_device_type_str(rec.type), rec.nonce,
frame->rssi);
} else {
LOG_DBG("%s record, nonce 0x%04x, rssi %d",
victronble_device_type_str(rec.type), rec.nonce,
frame->rssi);
}
SYS_SLIST_FOR_EACH_CONTAINER(&callbacks, cb, node) { SYS_SLIST_FOR_EACH_CONTAINER(&callbacks, cb, node) {
if (cb->record != NULL) { if (cb->record != NULL) {
BIN
View File
Binary file not shown.