First version getting ready to release
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* test_sppro.c - host known-answer tests for the SP PRO parser. No hardware.
|
||||
* make test
|
||||
*/
|
||||
#include "../src/sppro.h"
|
||||
#include "../src/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;
|
||||
}
|
||||
Reference in New Issue
Block a user