Examples and samples ready for Zephyr release
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
# 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)
|
||||
@@ -0,0 +1,88 @@
|
||||
# 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/).
|
||||
@@ -0,0 +1,19 @@
|
||||
# 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
|
||||
@@ -0,0 +1,10 @@
|
||||
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
|
||||
@@ -0,0 +1,278 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user