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).
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* victronble core host tests.
|
||||
*
|
||||
* Plain C, no framework: non-zero exit on failure. Positive vectors come
|
||||
* from test_vectors.h (openssl-encrypted, independent of the bundled AES);
|
||||
* negative cases are built inline.
|
||||
*
|
||||
* Build & run: ./run.sh (or see the gcc line inside it)
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
|
||||
#include "victronble.h"
|
||||
#include "test_vectors.h"
|
||||
|
||||
static int failures;
|
||||
|
||||
#define CHECK(cond) do { \
|
||||
if (!(cond)) { \
|
||||
printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
|
||||
failures++; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static int feq(float a, float b)
|
||||
{
|
||||
return fabsf(a - b) < 0.005f;
|
||||
}
|
||||
|
||||
static victronble_err_t decode(const uint8_t *frame, size_t len,
|
||||
const uint8_t key[16], victronble_record_t *rec)
|
||||
{
|
||||
memset(rec, 0xAA, sizeof(*rec));
|
||||
return victronble_decode(frame, len, key, rec);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
uint8_t key[VICTRONBLE_KEY_LEN];
|
||||
victronble_record_t rec;
|
||||
|
||||
CHECK(victronble_parse_key(VEC_KEY_HEX, key));
|
||||
|
||||
/* --- solar charger --- */
|
||||
CHECK(victronble_is_product_adv(VEC_SOLAR, sizeof(VEC_SOLAR)));
|
||||
CHECK(victronble_key_matches(VEC_SOLAR, sizeof(VEC_SOLAR), key));
|
||||
CHECK(decode(VEC_SOLAR, sizeof(VEC_SOLAR), key, &rec) == VICTRONBLE_OK);
|
||||
CHECK(rec.type == VICTRONBLE_DEV_SOLAR_CHARGER);
|
||||
CHECK(rec.model_id == 0xA060);
|
||||
CHECK(rec.nonce == 0x1234);
|
||||
CHECK(rec.u.solar.state == VICTRONBLE_STATE_BULK);
|
||||
CHECK(rec.u.solar.error == 0);
|
||||
CHECK(feq(rec.u.solar.battery_voltage, 13.24f));
|
||||
CHECK(feq(rec.u.solar.battery_current, 5.4f));
|
||||
CHECK(rec.u.solar.yield_today_wh == 1200);
|
||||
CHECK(feq(rec.u.solar.pv_power, 340.0f));
|
||||
CHECK(isnan(rec.u.solar.load_current));
|
||||
CHECK(strcmp(victronble_state_str(rec.u.solar.state), "bulk") == 0);
|
||||
|
||||
/* --- battery monitor --- */
|
||||
CHECK(decode(VEC_BATMON, sizeof(VEC_BATMON), key, &rec) == VICTRONBLE_OK);
|
||||
CHECK(rec.type == VICTRONBLE_DEV_BATTERY_MONITOR);
|
||||
CHECK(rec.u.batmon.remaining_minutes == 600);
|
||||
CHECK(feq(rec.u.batmon.voltage, 12.80f));
|
||||
CHECK(rec.u.batmon.alarm == 0x0005);
|
||||
CHECK(rec.u.batmon.aux_mode == 2);
|
||||
CHECK(feq(rec.u.batmon.temperature, 25.0f));
|
||||
CHECK(isnan(rec.u.batmon.aux_voltage));
|
||||
CHECK(feq(rec.u.batmon.current, -2.5f));
|
||||
CHECK(feq(rec.u.batmon.consumed_ah, -50.0f));
|
||||
CHECK(feq(rec.u.batmon.soc, 85.5f));
|
||||
|
||||
/* --- inverter --- */
|
||||
CHECK(decode(VEC_INVERTER, sizeof(VEC_INVERTER), key, &rec) == VICTRONBLE_OK);
|
||||
CHECK(rec.type == VICTRONBLE_DEV_INVERTER);
|
||||
CHECK(rec.u.inverter.state == VICTRONBLE_STATE_INVERTING);
|
||||
CHECK(feq(rec.u.inverter.battery_voltage, 25.86f));
|
||||
CHECK(feq(rec.u.inverter.battery_current, -12.34f));
|
||||
CHECK(feq(rec.u.inverter.ac_power, -230.0f));
|
||||
CHECK(rec.u.inverter.alarms == 0x08);
|
||||
|
||||
/* --- dc-dc converter --- */
|
||||
CHECK(decode(VEC_DCDC, sizeof(VEC_DCDC), key, &rec) == VICTRONBLE_OK);
|
||||
CHECK(rec.type == VICTRONBLE_DEV_DCDC_CONVERTER);
|
||||
CHECK(rec.u.dcdc.state == VICTRONBLE_STATE_FLOAT);
|
||||
CHECK(feq(rec.u.dcdc.input_voltage, 25.30f));
|
||||
CHECK(feq(rec.u.dcdc.output_voltage, 13.31f));
|
||||
CHECK(feq(rec.u.dcdc.output_current, 7.65f));
|
||||
CHECK(rec.nonce == 0xFFFF);
|
||||
|
||||
/* --- ac charger --- */
|
||||
CHECK(decode(VEC_ACCHARGER, sizeof(VEC_ACCHARGER), key, &rec) == VICTRONBLE_OK);
|
||||
CHECK(rec.type == VICTRONBLE_DEV_AC_CHARGER);
|
||||
CHECK(rec.u.ac.state == VICTRONBLE_STATE_ABSORPTION);
|
||||
CHECK(feq(rec.u.ac.voltage1, 14.40f));
|
||||
CHECK(feq(rec.u.ac.current1, 10.0f));
|
||||
CHECK(isnan(rec.u.ac.voltage2) && isnan(rec.u.ac.current2));
|
||||
CHECK(isnan(rec.u.ac.voltage3) && isnan(rec.u.ac.current3));
|
||||
CHECK(feq(rec.u.ac.temperature, 35.0f));
|
||||
CHECK(feq(rec.u.ac.ac_current, 1.2f));
|
||||
|
||||
/* --- multi RS collapses to the inverter decoder --- */
|
||||
CHECK(decode(VEC_MULTIRS, sizeof(VEC_MULTIRS), key, &rec) == VICTRONBLE_OK);
|
||||
CHECK(rec.type == VICTRONBLE_DEV_INVERTER);
|
||||
CHECK(rec.record_type == VICTRONBLE_DEV_MULTI_RS);
|
||||
CHECK(feq(rec.u.inverter.battery_voltage, 25.86f));
|
||||
|
||||
/* --- negative cases --- */
|
||||
|
||||
/* Known record type, no decoder */
|
||||
CHECK(decode(VEC_GX, sizeof(VEC_GX), key, &rec) == VICTRONBLE_ERR_UNSUPPORTED);
|
||||
|
||||
/* Truncated: shorter than the header */
|
||||
CHECK(decode(VEC_SOLAR, 9, key, &rec) == VICTRONBLE_ERR_SHORT);
|
||||
CHECK(!victronble_is_product_adv(VEC_SOLAR, 9));
|
||||
|
||||
/* Wrong company ID */
|
||||
{
|
||||
uint8_t bad[sizeof(VEC_SOLAR)];
|
||||
memcpy(bad, VEC_SOLAR, sizeof(bad));
|
||||
bad[0] = 0x4C; bad[1] = 0x00; /* Apple */
|
||||
CHECK(decode(bad, sizeof(bad), key, &rec) == VICTRONBLE_ERR_NOT_VICTRON);
|
||||
CHECK(!victronble_is_product_adv(bad, sizeof(bad)));
|
||||
}
|
||||
|
||||
/* Not a product advertisement */
|
||||
{
|
||||
uint8_t bad[sizeof(VEC_SOLAR)];
|
||||
memcpy(bad, VEC_SOLAR, sizeof(bad));
|
||||
bad[2] = 0x01;
|
||||
CHECK(decode(bad, sizeof(bad), key, &rec) == VICTRONBLE_ERR_NOT_PRODUCT);
|
||||
}
|
||||
|
||||
/* Wrong key: check byte catches it without decrypting */
|
||||
{
|
||||
uint8_t wrong_key[16];
|
||||
memcpy(wrong_key, key, 16);
|
||||
wrong_key[0] ^= 0xFF;
|
||||
CHECK(decode(VEC_SOLAR, sizeof(VEC_SOLAR), wrong_key, &rec) ==
|
||||
VICTRONBLE_ERR_KEY_MISMATCH);
|
||||
CHECK(!victronble_key_matches(VEC_SOLAR, sizeof(VEC_SOLAR), wrong_key));
|
||||
}
|
||||
|
||||
/* Wrong key with a matching check byte: decrypts to garbage but must
|
||||
* not crash; solar parser accepts any bytes, so OK with junk values is
|
||||
* acceptable — just require no error other than OK/SHORT. */
|
||||
{
|
||||
uint8_t wrong_key[16];
|
||||
memcpy(wrong_key, key, 16);
|
||||
wrong_key[15] ^= 0xFF;
|
||||
victronble_err_t err = decode(VEC_SOLAR, sizeof(VEC_SOLAR), wrong_key, &rec);
|
||||
CHECK(err == VICTRONBLE_OK || err == VICTRONBLE_ERR_SHORT);
|
||||
}
|
||||
|
||||
/* Key parsing */
|
||||
{
|
||||
uint8_t k[16];
|
||||
CHECK(!victronble_parse_key("00112233", k)); /* short */
|
||||
CHECK(!victronble_parse_key(NULL, k));
|
||||
CHECK(!victronble_parse_key("zz112233445566778899aabbccddeeff", k));
|
||||
CHECK(victronble_parse_key("00112233445566778899AABBCCDDEEFF", k));
|
||||
CHECK(k[0] == 0x00 && k[15] == 0xFF);
|
||||
}
|
||||
|
||||
if (failures == 0) {
|
||||
printf("victronble core: all tests passed\n");
|
||||
return 0;
|
||||
}
|
||||
printf("victronble core: %d FAILURE(S)\n", failures);
|
||||
return 1;
|
||||
}
|
||||
Reference in New Issue
Block a user