diff --git a/CMakeLists.txt b/CMakeLists.txt index bae47fb..df59c6f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,6 +89,7 @@ set(NL_FW_SOURCES src/sys/sdk_stubs.c src/net/nl_names.c src/net/nl_http.c + src/net/nl_e131.c src/net/nl_api.c src/net/nl_wifi_sm.c src/net/nl_wifi_w600.c diff --git a/src/net/nl_e131.c b/src/net/nl_e131.c new file mode 100644 index 0000000..5671318 --- /dev/null +++ b/src/net/nl_e131.c @@ -0,0 +1,57 @@ +#include "net/nl_e131.h" + +#include + +static uint16_t be16(const uint8_t *p) { return (uint16_t)(((uint16_t)p[0] << 8) | p[1]); } + +static uint32_t be32(const uint8_t *p) +{ + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3]; +} + +/* Flags (obere 4 Bit) muessen 0x7 sein, die Laenge (untere 12 Bit) muss exakt den Rest des + * Datagramms ab `from` beschreiben. Ein Paket, dessen Laengenfelder nicht zueinander passen, + * ist entweder verstuemmelt oder ein Angriffsversuch - in beiden Faellen: weg damit. */ +static bool pdu_len_ok(const uint8_t *p, size_t len, size_t from) +{ + uint16_t fl = be16(p + from); + if ((fl & 0xf000u) != 0x7000u) + return false; + return (size_t)(fl & 0x0fffu) == len - from; +} + +bool nl_e131_parse(const uint8_t *buf, size_t len, nl_e131_packet_t *out) +{ + static const uint8_t acn_pid[12] = { 'A', 'S', 'C', '-', 'E', '1', '.', '1', '7', 0, 0, 0 }; + uint16_t pvc; + + if (len < NL_E131_MIN_PACKET || len > NL_E131_MIN_PACKET + 512u) + return false; + if (be16(buf) != 0x0010u || be16(buf + 2) != 0x0000u) + return false; + if (memcmp(buf + 4, acn_pid, sizeof acn_pid) != 0) + return false; + if (!pdu_len_ok(buf, len, 16) || be32(buf + 18) != 0x00000004u) + return false; + if (!pdu_len_ok(buf, len, 38) || be32(buf + 40) != 0x00000002u) + return false; + if (!pdu_len_ok(buf, len, 115) || buf[117] != 0x02u) + return false; + if (buf[118] != 0xa1u || be16(buf + 119) != 0x0000u || be16(buf + 121) != 0x0001u) + return false; + pvc = be16(buf + 123); + if (pvc < 1u || (size_t)(pvc - 1u) != len - NL_E131_MIN_PACKET) + return false; + if (buf[125] != 0x00u) /* nur Start-Code 0 ist Lichtinhalt (0xdd waere Kanalprioritaet) */ + return false; + if (buf[112] & 0x80u) /* Preview-Daten duerfen laut E1.31 nicht ausgegeben werden */ + return false; + + out->universe = be16(buf + 113); + out->priority = buf[108]; + out->sequence = buf[111]; + out->terminated = (buf[112] & 0x40u) != 0u; + out->slot_count = (uint16_t)(pvc - 1u); + out->slots = buf + NL_E131_MIN_PACKET; + return true; +} diff --git a/src/net/nl_e131.h b/src/net/nl_e131.h new file mode 100644 index 0000000..9a403ce --- /dev/null +++ b/src/net/nl_e131.h @@ -0,0 +1,26 @@ +#ifndef NL_E131_H +#define NL_E131_H + +#include +#include +#include + +#define NL_E131_PORT 5568u +#define NL_E131_MAX_PACKET 640u /* ein volles Paket ist 638 Byte; 640 laesst Luft zum Erkennen + * ueberlanger Datagramme, ohne den Puffer unnoetig aufzublaehen */ +#define NL_E131_MIN_PACKET 126u + +typedef struct { + uint16_t universe; + uint8_t priority; + uint8_t sequence; + bool terminated; /* Options-Bit "stream terminated" */ + uint16_t slot_count; /* Nutzdaten ohne Start-Code, 0..512 */ + const uint8_t *slots; /* zeigt in den uebergebenen Puffer, wird nicht kopiert */ +} nl_e131_packet_t; + +/* true = gueltiges DMX-Datenpaket (Start-Code 0, kein Preview). Alles andere: false. + * `out` wird nur bei true beschrieben. */ +bool nl_e131_parse(const uint8_t *buf, size_t len, nl_e131_packet_t *out); + +#endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 1c4a5c8..d618089 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -44,6 +44,7 @@ nl_test(test_reset_counter test_reset_counter.c fake_flash.c ${NL_SRC}/config/nl nl_test(test_console_cmd test_console_cmd.c ${NL_SRC}/sys/nl_console_cmd.c) nl_test(test_names test_names.c ${NL_SRC}/net/nl_names.c) nl_test(test_http test_http.c ${NL_SRC}/net/nl_http.c) +nl_test(test_e131 test_e131.c ${NL_SRC}/net/nl_e131.c) nl_test(test_api test_api.c fake_api_ctx.c ${NL_SRC}/net/nl_api.c ${NL_SRC}/board/nl_board.c ${NL_SRC}/light/nl_boot_state.c ${NL_SRC}/config/nl_settings.c ${NL_SRC}/util/nl_crc.c fake_flash.c) target_link_libraries(test_api PRIVATE nl_cjson) diff --git a/test/test_e131.c b/test/test_e131.c new file mode 100644 index 0000000..f583f2b --- /dev/null +++ b/test/test_e131.c @@ -0,0 +1,201 @@ +#include +#include "unity.h" +#include "net/nl_e131.h" + +static uint8_t pkt[NL_E131_MAX_PACKET]; +static size_t pkt_len; + +static void be16(size_t off, uint16_t v) { pkt[off] = (uint8_t)(v >> 8); pkt[off + 1] = (uint8_t)v; } +static void be32(size_t off, uint32_t v) +{ + pkt[off] = (uint8_t)(v >> 24); pkt[off + 1] = (uint8_t)(v >> 16); + pkt[off + 2] = (uint8_t)(v >> 8); pkt[off + 3] = (uint8_t)v; +} + +/* Baut ein gueltiges Paket mit `slots` Slots, aufsteigend gefuellt (1, 2, 3, ...). */ +static void build(uint16_t universe, uint8_t prio, uint8_t seq, uint8_t options, uint16_t slots) +{ + pkt_len = 126u + slots; + memset(pkt, 0, sizeof pkt); + be16(0, 0x0010); + be16(2, 0x0000); + memcpy(pkt + 4, "ASC-E1.17\0\0\0", 12); + be16(16, (uint16_t)(0x7000u | (pkt_len - 16u))); + be32(18, 0x00000004u); + be16(38, (uint16_t)(0x7000u | (pkt_len - 38u))); + be32(40, 0x00000002u); + memcpy(pkt + 44, "ninlight test", 13); + pkt[108] = prio; + pkt[111] = seq; + pkt[112] = options; + be16(113, universe); + be16(115, (uint16_t)(0x7000u | (pkt_len - 115u))); + pkt[117] = 0x02; + pkt[118] = 0xa1; + be16(119, 0x0000); + be16(121, 0x0001); + be16(123, (uint16_t)(slots + 1u)); + pkt[125] = 0x00; /* Start-Code */ + for (uint16_t i = 0; i < slots; i++) + pkt[126 + i] = (uint8_t)(i + 1u); +} + +void setUp(void) { build(1, 100, 0, 0, 6); } +void tearDown(void) {} + +static void test_valid_packet(void) +{ + nl_e131_packet_t p; + TEST_ASSERT_TRUE(nl_e131_parse(pkt, pkt_len, &p)); + TEST_ASSERT_EQUAL_UINT16(1, p.universe); + TEST_ASSERT_EQUAL_UINT8(100, p.priority); + TEST_ASSERT_EQUAL_UINT8(0, p.sequence); + TEST_ASSERT_FALSE(p.terminated); + TEST_ASSERT_EQUAL_UINT16(6, p.slot_count); + TEST_ASSERT_EQUAL_UINT8(1, p.slots[0]); + TEST_ASSERT_EQUAL_UINT8(6, p.slots[5]); +} + +static void test_full_universe(void) +{ + nl_e131_packet_t p; + build(1, 100, 0, 0, 512); + TEST_ASSERT_EQUAL_size_t(638, pkt_len); + TEST_ASSERT_TRUE(nl_e131_parse(pkt, pkt_len, &p)); + TEST_ASSERT_EQUAL_UINT16(512, p.slot_count); +} + +static void test_empty_slot_set(void) +{ + nl_e131_packet_t p; + build(1, 100, 0, 0, 0); + TEST_ASSERT_EQUAL_size_t(126, pkt_len); + TEST_ASSERT_TRUE(nl_e131_parse(pkt, pkt_len, &p)); + TEST_ASSERT_EQUAL_UINT16(0, p.slot_count); +} + +static void test_terminated_bit(void) +{ + nl_e131_packet_t p; + build(1, 100, 0, 0x40, 6); + TEST_ASSERT_TRUE(nl_e131_parse(pkt, pkt_len, &p)); + TEST_ASSERT_TRUE(p.terminated); +} + +static void test_preview_bit_rejected(void) +{ + nl_e131_packet_t p; + build(1, 100, 0, 0x80, 6); + TEST_ASSERT_FALSE(nl_e131_parse(pkt, pkt_len, &p)); +} + +static void test_bad_preamble(void) +{ + nl_e131_packet_t p; + pkt[1] = 0x11; + TEST_ASSERT_FALSE(nl_e131_parse(pkt, pkt_len, &p)); +} + +static void test_bad_package_identifier(void) +{ + nl_e131_packet_t p; + pkt[4] = 'X'; + TEST_ASSERT_FALSE(nl_e131_parse(pkt, pkt_len, &p)); +} + +static void test_bad_root_vector(void) +{ + nl_e131_packet_t p; + be32(18, 0x00000008u); + TEST_ASSERT_FALSE(nl_e131_parse(pkt, pkt_len, &p)); +} + +static void test_bad_framing_vector(void) +{ + nl_e131_packet_t p; + be32(40, 0x00000001u); /* VECTOR_E131_EXTENDED_SYNCHRONIZATION */ + TEST_ASSERT_FALSE(nl_e131_parse(pkt, pkt_len, &p)); +} + +static void test_bad_dmp_vector(void) +{ + nl_e131_packet_t p; + pkt[117] = 0x03; + TEST_ASSERT_FALSE(nl_e131_parse(pkt, pkt_len, &p)); +} + +static void test_bad_address_type(void) +{ + nl_e131_packet_t p; + pkt[118] = 0xa2; + TEST_ASSERT_FALSE(nl_e131_parse(pkt, pkt_len, &p)); +} + +static void test_per_channel_priority_start_code_rejected(void) +{ + nl_e131_packet_t p; + pkt[125] = 0xdd; + TEST_ASSERT_FALSE(nl_e131_parse(pkt, pkt_len, &p)); +} + +static void test_truncated_length_fields(void) +{ + nl_e131_packet_t p; + /* Root-Laenge behauptet mehr, als das Datagramm hergibt. */ + be16(16, (uint16_t)(0x7000u | (pkt_len - 16u + 4u))); + TEST_ASSERT_FALSE(nl_e131_parse(pkt, pkt_len, &p)); + build(1, 100, 0, 0, 6); + be16(38, (uint16_t)(0x7000u | (pkt_len - 38u - 2u))); + TEST_ASSERT_FALSE(nl_e131_parse(pkt, pkt_len, &p)); + build(1, 100, 0, 0, 6); + be16(115, (uint16_t)(0x7000u | (pkt_len - 115u + 1u))); + TEST_ASSERT_FALSE(nl_e131_parse(pkt, pkt_len, &p)); +} + +static void test_property_value_count_mismatch(void) +{ + nl_e131_packet_t p; + be16(123, 9); /* behauptet 8 Slots, das Paket traegt 6 */ + TEST_ASSERT_FALSE(nl_e131_parse(pkt, pkt_len, &p)); +} + +static void test_too_short_and_too_long(void) +{ + nl_e131_packet_t p; + TEST_ASSERT_FALSE(nl_e131_parse(pkt, 125, &p)); + TEST_ASSERT_FALSE(nl_e131_parse(pkt, 0, &p)); + build(1, 100, 0, 0, 512); + TEST_ASSERT_FALSE(nl_e131_parse(pkt, 639, &p)); /* laenger als jedes gueltige Paket */ +} + +static void test_universe_is_read(void) +{ + nl_e131_packet_t p; + build(63999, 200, 7, 0, 6); + TEST_ASSERT_TRUE(nl_e131_parse(pkt, pkt_len, &p)); + TEST_ASSERT_EQUAL_UINT16(63999, p.universe); + TEST_ASSERT_EQUAL_UINT8(200, p.priority); + TEST_ASSERT_EQUAL_UINT8(7, p.sequence); +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_valid_packet); + RUN_TEST(test_full_universe); + RUN_TEST(test_empty_slot_set); + RUN_TEST(test_terminated_bit); + RUN_TEST(test_preview_bit_rejected); + RUN_TEST(test_bad_preamble); + RUN_TEST(test_bad_package_identifier); + RUN_TEST(test_bad_root_vector); + RUN_TEST(test_bad_framing_vector); + RUN_TEST(test_bad_dmp_vector); + RUN_TEST(test_bad_address_type); + RUN_TEST(test_per_channel_priority_start_code_rejected); + RUN_TEST(test_truncated_length_fields); + RUN_TEST(test_property_value_count_mismatch); + RUN_TEST(test_too_short_and_too_long); + RUN_TEST(test_universe_is_read); + return UNITY_END(); +}