/* * test_sppro.c - host known-answer tests for the SP PRO parser. No hardware. * make test */ #include "sppro.h" #include "md5.h" #include #include #include 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; }