/** * 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 # 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 #include #include #include #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; }