Files
2026-06-16 04:59:02 +10:00

186 lines
6.6 KiB
C

/*
* test_sppro.c - host known-answer tests for the SP PRO parser. No hardware.
* make test
*/
#include "sppro.h"
#include "md5.h"
#include <stdio.h>
#include <string.h>
#include <math.h>
static int failures = 0;
static int checks = 0;
#define CHECK(cond, msg) do { \
checks++; \
if (!(cond)) { failures++; printf("FAIL: %s (%s:%d)\n", msg, __FILE__, __LINE__); } \
} while (0)
static int near(double a, double b) { return fabs(a - b) < 1e-6; }
static void hex(const uint8_t *p, size_t n, char *out)
{
static const char *d = "0123456789abcdef";
size_t i;
for (i = 0; i < n; i++) { out[i*2] = d[p[i] >> 4]; out[i*2+1] = d[p[i] & 0xf]; }
out[n*2] = 0;
}
/* selpi documented example: read 1 word at 0xa000. */
static const uint8_t QUERY_A000[] = { 0x51,0x00,0x00,0xa0,0x00,0x00,0x9d,0x4b };
static const uint8_t RESPONSE_A000[] = { 0x51,0x00,0x00,0xa0,0x00,0x00,0x9d,0x4b,0x01,0x00,0xd8,0x19 };
static void test_crc(void)
{
/* CRC over the 6-byte header equals the 0x4b9d carried little-endian. */
CHECK(sppro_crc16(QUERY_A000, 6) == 0x4b9d, "crc header 0xa000");
/* A full valid frame CRCs to zero. */
CHECK(sppro_crc16(QUERY_A000, sizeof(QUERY_A000)) == 0, "crc whole query == 0");
CHECK(sppro_crc16(RESPONSE_A000, sizeof(RESPONSE_A000)) == 0, "crc whole response == 0");
}
static void test_build_query(void)
{
uint8_t buf[8];
int n = sppro_build_query(buf, sizeof(buf), 0xa000, 1);
CHECK(n == 8, "build_query length");
CHECK(memcmp(buf, QUERY_A000, 8) == 0, "build_query matches documented bytes");
CHECK(sppro_build_query(buf, sizeof(buf), 0xa000, 0) == SPPRO_ERR_ARG, "reject 0 words");
CHECK(sppro_build_query(buf, sizeof(buf), 0xa000, 257) == SPPRO_ERR_ARG, "reject >256 words");
CHECK(sppro_build_query(buf, 4, 0xa000, 1) == SPPRO_ERR_BUFFER, "reject small buffer");
}
static void test_parse_response(void)
{
const uint8_t *data = NULL;
size_t data_len = 0;
int rc = sppro_parse_query_response(RESPONSE_A000, sizeof(RESPONSE_A000),
0xa000, 1, &data, &data_len);
CHECK(rc == SPPRO_OK, "parse response ok");
CHECK(data_len == 2, "data length 2");
CHECK(data && sppro_u16(data) == 1, "decoded word == 1");
/* Corrupt one byte -> CRC failure. */
uint8_t bad[sizeof(RESPONSE_A000)];
memcpy(bad, RESPONSE_A000, sizeof(bad));
bad[9] ^= 0xff;
CHECK(sppro_parse_query_response(bad, sizeof(bad), 0xa000, 1, NULL, NULL) == SPPRO_ERR_CRC,
"detect corrupted response");
/* Wrong expected address -> echo mismatch. */
CHECK(sppro_parse_query_response(RESPONSE_A000, sizeof(RESPONSE_A000), 0xb000, 1, NULL, NULL)
== SPPRO_ERR_ECHO, "detect wrong address echo");
}
static void test_build_write(void)
{
uint8_t data[16];
uint8_t frame[SPPRO_MAX_FRAME];
size_t i;
int n;
for (i = 0; i < sizeof(data); i++) data[i] = (uint8_t)(i + 1);
n = sppro_build_write(frame, sizeof(frame), 0x1f0000, data, sizeof(data));
CHECK(n == (int)(8 + 16 + 2), "write frame length");
CHECK(frame[0] == 'W' && frame[1] == 7, "write header type and word count");
CHECK(sppro_crc16(frame, 6) == sppro_u16(frame + 6), "write inner header crc");
CHECK(sppro_crc16(frame, (size_t)n) == 0, "write whole frame crc == 0");
CHECK(memcmp(frame + 8, data, sizeof(data)) == 0, "write payload copied");
CHECK(sppro_build_write(frame, sizeof(frame), 0, data, 3) == SPPRO_ERR_ARG, "reject odd length");
}
static void test_md5(void)
{
unsigned char d[16];
char s[33];
sppro_md5("", 0, d);
hex(d, 16, s);
CHECK(strcmp(s, "d41d8cd98f00b204e9800998ecf8427e") == 0, "md5 empty string");
sppro_md5("abc", 3, d);
hex(d, 16, s);
CHECK(strcmp(s, "900150983cd24fb0d6963f7d28e17f72") == 0, "md5 abc");
}
static void test_login_response(void)
{
/* Independently reproduce: md5(seed + password padded to 32 with spaces),
* then swap adjacent byte pairs. Validates buffer layout and the swap. */
uint8_t seed[16];
const char *pw = "secret";
uint8_t out[16], expect[16];
unsigned char digest[16];
uint8_t buf[48];
size_t i;
for (i = 0; i < 16; i++) seed[i] = (uint8_t)(0x10 + i);
memcpy(buf, seed, 16);
memcpy(buf + 16, pw, strlen(pw));
for (i = 16 + strlen(pw); i < 48; i++) buf[i] = ' ';
sppro_md5(buf, 48, digest);
for (i = 0; i < 16; i += 2) { expect[i] = digest[i+1]; expect[i+1] = digest[i]; }
sppro_login_response(seed, pw, out);
CHECK(memcmp(out, expect, 16) == 0, "login response md5+swap");
}
static void test_conversions(void)
{
sppro_scales_t s;
s.ac_volts = 32768; s.ac_current = 800;
s.dc_volts = 32768; s.dc_current = 100;
s.temperature = 32768; s.internal_voltages = 32768;
CHECK(near(sppro_convert(SPPRO_C_DC_V, 480, &s), 48.0), "dc_v 480 -> 48.0V");
CHECK(near(sppro_convert(SPPRO_C_PERCENT, 12800, &s), 50.0), "percent 12800 -> 50%");
CHECK(near(sppro_convert(SPPRO_C_TEMPERATURE, 25, &s), 25.0), "temperature 25 -> 25C");
CHECK(near(sppro_convert(SPPRO_C_AC_W, 1500, &s), 1500.0), "ac_w with unity scale");
/* dc_w: raw*dcv*dci/(32768*100) = raw*32768*100/(32768*100) = raw */
CHECK(near(sppro_convert(SPPRO_C_DC_W, -250, &s), -250.0), "dc_w signed");
CHECK(strcmp(sppro_shunt_name(1), "Solar") == 0, "shunt name 1 = Solar");
CHECK(strcmp(sppro_shunt_name(99), "Error") == 0, "shunt name out of range");
}
static void test_decode_via_reg(void)
{
sppro_scales_t s;
const sppro_reg_t *reg;
uint8_t data[2];
s.ac_volts = s.dc_volts = s.temperature = s.internal_voltages = 32768;
s.ac_current = 800; s.dc_current = 100;
reg = sppro_reg_by_name("BatteryVolts");
CHECK(reg != NULL, "lookup BatteryVolts");
data[0] = 0xe0; data[1] = 0x01; /* 0x01e0 = 480 */
CHECK(near(sppro_decode(reg, data, &s), 48.0), "decode BatteryVolts -> 48.0V");
CHECK(sppro_reg_by_name("DoesNotExist") == NULL, "unknown register name");
CHECK(sppro_type_words(SPPRO_T_U32) == 2 && sppro_type_words(SPPRO_T_U16) == 1, "type words");
}
static void test_parse_scales(void)
{
uint8_t d[12] = { 0,0x80, 0x20,0x03, 0,0x80, 0x64,0, 0,0x80, 0,0x80 };
sppro_scales_t s;
CHECK(sppro_parse_scales(d, sizeof(d), &s) == SPPRO_OK, "parse scales ok");
CHECK(s.ac_volts == 0x8000 && s.ac_current == 0x0320 && s.dc_current == 0x0064,
"scales fields little-endian");
}
int main(void)
{
test_crc();
test_build_query();
test_parse_response();
test_build_write();
test_md5();
test_login_response();
test_conversions();
test_decode_via_reg();
test_parse_scales();
printf("\n%d checks, %d failures\n", checks, failures);
return failures ? 1 : 0;
}