Examples and samples ready for Zephyr release
This commit is contained in:
@@ -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