Examples and samples ready for Zephyr release

This commit is contained in:
2026-08-26 00:58:57 +10:00
parent 67ff8721c0
commit 22af47854d
20 changed files with 1158 additions and 29 deletions
+30
View File
@@ -0,0 +1,30 @@
# 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
@@ -0,0 +1,84 @@
# 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
@@ -0,0 +1,251 @@
/**
* 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;
}