esp-knx-ip.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
  1. /**
  2. * esp-knx-ip library for KNX/IP communication on an ESP8266
  3. * Author: Nico Weichbrodt <envy>
  4. * License: MIT
  5. */
  6. #include "esp-knx-ip.h"
  7. char const *string_defaults[] =
  8. {
  9. "Do this",
  10. "True",
  11. "False",
  12. ""
  13. };
  14. ESPKNXIP::ESPKNXIP() : server(nullptr),
  15. registered_callback_assignments(0),
  16. free_callback_assignment_slots(0),
  17. registered_callbacks(0),
  18. free_callback_slots(0),
  19. registered_configs(0),
  20. registered_feedbacks(0)
  21. {
  22. DEBUG_PRINTLN();
  23. DEBUG_PRINTLN("ESPKNXIP starting up");
  24. // Default physical address is 1.1.0
  25. physaddr.bytes.high = (/*area*/1 << 4) | /*line*/1;
  26. physaddr.bytes.low = /*member*/0;
  27. memset(callback_assignments, 0, MAX_CALLBACK_ASSIGNMENTS * sizeof(callback_assignment_t));
  28. memset(callbacks, 0, MAX_CALLBACKS * sizeof(callback_fptr_t));
  29. memset(custom_config_data, 0, MAX_CONFIG_SPACE * sizeof(uint8_t));
  30. memset(custom_config_default_data, 0, MAX_CONFIG_SPACE * sizeof(uint8_t));
  31. memset(custom_configs, 0, MAX_CONFIGS * sizeof(config_t));
  32. }
  33. void ESPKNXIP::load()
  34. {
  35. memcpy(custom_config_default_data, custom_config_data, MAX_CONFIG_SPACE);
  36. EEPROM.begin(EEPROM_SIZE);
  37. restore_from_eeprom();
  38. }
  39. void ESPKNXIP::start(ESP8266WebServer *srv)
  40. {
  41. server = srv;
  42. __start();
  43. }
  44. void ESPKNXIP::start()
  45. {
  46. server = new ESP8266WebServer(80);
  47. __start();
  48. }
  49. void ESPKNXIP::__start()
  50. {
  51. if (server != nullptr)
  52. {
  53. server->on(ROOT_PREFIX, [this](){
  54. __handle_root();
  55. });
  56. server->on(__ROOT_PATH, [this](){
  57. __handle_root();
  58. });
  59. server->on(__REGISTER_PATH, [this](){
  60. __handle_register();
  61. });
  62. server->on(__DELETE_PATH, [this](){
  63. __handle_delete();
  64. });
  65. server->on(__PHYS_PATH, [this](){
  66. __handle_set();
  67. });
  68. #if !DISABLE_EEPROM_BUTTONS
  69. server->on(__EEPROM_PATH, [this](){
  70. __handle_eeprom();
  71. });
  72. #endif
  73. server->on(__CONFIG_PATH, [this](){
  74. __handle_config();
  75. });
  76. server->on(__FEEDBACK_PATH, [this](){
  77. __handle_feedback();
  78. });
  79. #if !DISABLE_RESTORE_BUTTON
  80. server->on(__RESTORE_PATH, [this](){
  81. __handle_restore();
  82. });
  83. #endif
  84. #if !DISABLE_REBOOT_BUTTON
  85. server->on(__REBOOT_PATH, [this](){
  86. __handle_reboot();
  87. });
  88. #endif
  89. server->begin();
  90. }
  91. udp.beginMulticast(WiFi.localIP(), MULTICAST_IP, MULTICAST_PORT);
  92. }
  93. void ESPKNXIP::save_to_eeprom()
  94. {
  95. uint32_t address = 0;
  96. uint64_t magic = EEPROM_MAGIC;
  97. EEPROM.put(address, magic);
  98. address += sizeof(uint64_t);
  99. EEPROM.put(address++, registered_callback_assignments);
  100. for (uint8_t i = 0; i < MAX_CALLBACK_ASSIGNMENTS; ++i)
  101. {
  102. EEPROM.put(address, callback_assignments[i].address);
  103. address += sizeof(address_t);
  104. }
  105. for (uint8_t i = 0; i < MAX_CALLBACK_ASSIGNMENTS; ++i)
  106. {
  107. EEPROM.put(address, callback_assignments[i].callback_id);
  108. address += sizeof(callback_id_t);
  109. }
  110. EEPROM.put(address, physaddr);
  111. address += sizeof(address_t);
  112. EEPROM.put(address, custom_config_data);
  113. address += sizeof(custom_config_data);
  114. EEPROM.commit();
  115. DEBUG_PRINT("Wrote to EEPROM: 0x");
  116. DEBUG_PRINTLN(address, HEX);
  117. }
  118. void ESPKNXIP::restore_from_eeprom()
  119. {
  120. uint32_t address = 0;
  121. uint64_t magic = 0;
  122. EEPROM.get(address, magic);
  123. if (magic != EEPROM_MAGIC)
  124. {
  125. DEBUG_PRINTLN("No valid magic in EEPROM, aborting restore.");
  126. DEBUG_PRINT("Expected 0x");
  127. DEBUG_PRINT((unsigned long)(EEPROM_MAGIC >> 32), HEX);
  128. DEBUG_PRINT(" 0x");
  129. DEBUG_PRINT((unsigned long)(EEPROM_MAGIC), HEX);
  130. DEBUG_PRINT(" got 0x");
  131. DEBUG_PRINT((unsigned long)(magic >> 32), HEX);
  132. DEBUG_PRINT(" 0x");
  133. DEBUG_PRINTLN((unsigned long)magic, HEX);
  134. return;
  135. }
  136. address += sizeof(uint64_t);
  137. EEPROM.get(address++, registered_callback_assignments);
  138. for (uint8_t i = 0; i < MAX_CALLBACK_ASSIGNMENTS; ++i)
  139. {
  140. EEPROM.get(address, callback_assignments[i].address);
  141. if (callback_assignments[i].address.value != 0)
  142. {
  143. // if address is not 0/0/0 then mark slot as used
  144. callback_assignments[i].slot_flags |= SLOT_FLAGS_USED;
  145. DEBUG_PRINTLN("used slot");
  146. }
  147. else
  148. {
  149. // if address is 0/0/0, then we found a free slot, yay!
  150. // however, only count those slots, if we have not reached registered_callback_assignments yet
  151. if (i < registered_callback_assignments)
  152. {
  153. DEBUG_PRINTLN("free slot before reaching registered_callback_assignments");
  154. free_callback_assignment_slots++;
  155. }
  156. else
  157. {
  158. DEBUG_PRINTLN("free slot");
  159. }
  160. }
  161. address += sizeof(address_t);
  162. }
  163. for (uint8_t i = 0; i < MAX_CALLBACK_ASSIGNMENTS; ++i)
  164. {
  165. EEPROM.get(address, callback_assignments[i].callback_id);
  166. address += sizeof(callback_id_t);
  167. }
  168. EEPROM.get(address, physaddr);
  169. address += sizeof(address_t);
  170. //EEPROM.get(address, custom_config_data);
  171. //address += sizeof(custom_config_data);
  172. uint32_t conf_offset = address;
  173. for (uint8_t i = 0; i < registered_configs; ++i)
  174. {
  175. // First byte is flags.
  176. config_flags_t flags = CONFIG_FLAGS_NO_FLAGS;
  177. flags = (config_flags_t)EEPROM.read(address);
  178. DEBUG_PRINT("Flag in EEPROM @ ");
  179. DEBUG_PRINT(address - conf_offset);
  180. DEBUG_PRINT(": ");
  181. DEBUG_PRINTLN(flags, BIN);
  182. custom_config_data[custom_configs[i].offset] = flags;
  183. if (flags & CONFIG_FLAGS_VALUE_SET)
  184. {
  185. DEBUG_PRINTLN("Non-default value");
  186. for (int j = 0; j < custom_configs[i].len - sizeof(uint8_t); ++j)
  187. {
  188. custom_config_data[custom_configs[i].offset + sizeof(uint8_t) + j] = EEPROM.read(address + sizeof(uint8_t) + j);
  189. }
  190. }
  191. address += custom_configs[i].len;
  192. }
  193. DEBUG_PRINT("Restored from EEPROM: 0x");
  194. DEBUG_PRINTLN(address, HEX);
  195. }
  196. uint16_t ESPKNXIP::__ntohs(uint16_t n)
  197. {
  198. return (uint16_t)((((uint8_t*)&n)[0] << 8) | (((uint8_t*)&n)[1]));
  199. }
  200. callback_assignment_id_t ESPKNXIP::__callback_register_assignment(address_t address, callback_id_t id)
  201. {
  202. if (registered_callback_assignments >= MAX_CALLBACK_ASSIGNMENTS)
  203. return -1;
  204. if (free_callback_assignment_slots == 0)
  205. {
  206. callback_assignment_id_t aid = registered_callback_assignments;
  207. callback_assignments[aid].slot_flags |= SLOT_FLAGS_USED;
  208. callback_assignments[aid].address = address;
  209. callback_assignments[aid].callback_id = id;
  210. registered_callback_assignments++;
  211. return aid;
  212. }
  213. else
  214. {
  215. // find the free slot
  216. for (callback_assignment_id_t aid = 0; aid < registered_callback_assignments; ++aid)
  217. {
  218. if (callback_assignments[aid].slot_flags & SLOT_FLAGS_USED)
  219. {
  220. // found a used slot
  221. continue;
  222. }
  223. // and now an empty one
  224. callback_assignments[aid].slot_flags |= SLOT_FLAGS_USED;
  225. callback_assignments[aid].address = address;
  226. callback_assignments[aid].callback_id = id;
  227. free_callback_assignment_slots--;
  228. return id;
  229. }
  230. }
  231. }
  232. void ESPKNXIP::__callback_delete_assignment(callback_assignment_id_t id)
  233. {
  234. // TODO this can be optimized if we are deleting the last element
  235. // as then we can decrement registered_callback_assignments
  236. // clear slot and mark it as empty
  237. callback_assignments[id].slot_flags = SLOT_FLAGS_EMPTY;
  238. callback_assignments[id].address.value = 0;
  239. callback_assignments[id].callback_id = 0;
  240. if (id == registered_callback_assignments - 1)
  241. {
  242. DEBUG_PRINTLN("last cba deleted");
  243. // If this is the last callback, we can delete it by decrementing registered_callbacks.
  244. registered_callback_assignments--;
  245. // However, if the assignment before this slot are also empty, we can decrement even further
  246. // First check if this was also the first element
  247. if (id == 0)
  248. {
  249. DEBUG_PRINTLN("really last cba");
  250. // If this was the last, then we are done.
  251. return;
  252. }
  253. id--;
  254. while(true)
  255. {
  256. DEBUG_PRINT("checking ");
  257. DEBUG_PRINTLN((int32_t)id);
  258. if ((callback_assignments[id].slot_flags & SLOT_FLAGS_USED) == 0)
  259. {
  260. DEBUG_PRINTLN("merged free slot");
  261. // Slot before is empty
  262. free_callback_assignment_slots--;
  263. registered_callback_assignments--;
  264. }
  265. else
  266. {
  267. DEBUG_PRINTLN("aborted on used slot");
  268. // Slot is used, abort
  269. return;
  270. }
  271. id--;
  272. if (id == CALLBACK_ASSIGNMENT_ID_MAX)
  273. {
  274. DEBUG_PRINTLN("abort on wrap");
  275. // Wrap around, abort
  276. return;
  277. }
  278. }
  279. }
  280. else
  281. {
  282. DEBUG_PRINTLN("free slot created");
  283. // there is now one more free slot
  284. free_callback_assignment_slots++;
  285. }
  286. }
  287. bool ESPKNXIP::__callback_is_id_valid(callback_id_t id)
  288. {
  289. if (id < registered_callbacks)
  290. return true;
  291. if (callbacks[id].slot_flags & SLOT_FLAGS_USED)
  292. return true;
  293. return false;
  294. }
  295. callback_id_t ESPKNXIP::callback_register(String name, callback_fptr_t cb, void *arg, enable_condition_t cond)
  296. {
  297. if (registered_callbacks >= MAX_CALLBACKS)
  298. return -1;
  299. if (free_callback_slots == 0)
  300. {
  301. callback_id_t id = registered_callbacks;
  302. callbacks[id].slot_flags |= SLOT_FLAGS_USED;
  303. callbacks[id].name = name;
  304. callbacks[id].fkt = cb;
  305. callbacks[id].cond = cond;
  306. callbacks[id].arg = arg;
  307. registered_callbacks++;
  308. return id;
  309. }
  310. else
  311. {
  312. // find the free slot
  313. for (callback_id_t id = 0; id < registered_callbacks; ++id)
  314. {
  315. if (callbacks[id].slot_flags & SLOT_FLAGS_USED)
  316. {
  317. // found a used slot
  318. continue;
  319. }
  320. // and now an empty one
  321. callbacks[id].slot_flags |= SLOT_FLAGS_USED;
  322. callbacks[id].name = name;
  323. callbacks[id].fkt = cb;
  324. callbacks[id].cond = cond;
  325. callbacks[id].arg = arg;
  326. free_callback_slots--;
  327. return id;
  328. }
  329. }
  330. }
  331. void ESPKNXIP::callback_deregister(callback_id_t id)
  332. {
  333. if (!__callback_is_id_valid(id))
  334. return;
  335. // clear slot and mark it as empty
  336. callbacks[id].slot_flags = SLOT_FLAGS_EMPTY;
  337. callbacks[id].fkt = nullptr;
  338. callbacks[id].cond = nullptr;
  339. callbacks[id].arg = nullptr;
  340. if (id == registered_callbacks - 1)
  341. {
  342. // If this is the last callback, we can delete it by decrementing registered_callbacks.
  343. registered_callbacks--;
  344. // However, if the callbacks before this slot are also empty, we can decrement even further
  345. // First check if this was also the first element
  346. if (id == 0)
  347. {
  348. // If this was the last, then we are done.
  349. return;
  350. }
  351. id--;
  352. while(true)
  353. {
  354. if ((callbacks[id].slot_flags & SLOT_FLAGS_USED) == 0)
  355. {
  356. // Slot is empty
  357. free_callback_slots--;
  358. registered_callbacks--;
  359. }
  360. else
  361. {
  362. // Slot is used, abort
  363. return;
  364. }
  365. id--;
  366. if (id == CALLBACK_ASSIGNMENT_ID_MAX)
  367. {
  368. // Wrap around, abort
  369. return;
  370. }
  371. }
  372. }
  373. else
  374. {
  375. // there is now one more free slot
  376. free_callback_slots++;
  377. }
  378. }
  379. callback_assignment_id_t ESPKNXIP::callback_assign(callback_id_t id, address_t val)
  380. {
  381. if (!__callback_is_id_valid(id))
  382. return -1;
  383. return __callback_register_assignment(val, id);
  384. }
  385. void ESPKNXIP::callback_unassign(callback_assignment_id_t id)
  386. {
  387. if (!__callback_is_id_valid(id))
  388. return;
  389. __callback_delete_assignment(id);
  390. }
  391. /**
  392. * Feedback functions start here
  393. */
  394. feedback_id_t ESPKNXIP::feedback_register_int(String name, int32_t *value, enable_condition_t cond)
  395. {
  396. if (registered_feedbacks >= MAX_FEEDBACKS)
  397. return -1;
  398. feedback_id_t id = registered_feedbacks;
  399. feedbacks[id].type = FEEDBACK_TYPE_INT;
  400. feedbacks[id].name = name;
  401. feedbacks[id].cond = cond;
  402. feedbacks[id].data = (void *)value;
  403. registered_feedbacks++;
  404. return id;
  405. }
  406. feedback_id_t ESPKNXIP::feedback_register_float(String name, float *value, uint8_t precision, char const *prefix, char const *suffix, enable_condition_t cond)
  407. {
  408. if (registered_feedbacks >= MAX_FEEDBACKS)
  409. return -1;
  410. feedback_id_t id = registered_feedbacks;
  411. feedbacks[id].type = FEEDBACK_TYPE_FLOAT;
  412. feedbacks[id].name = name;
  413. feedbacks[id].cond = cond;
  414. feedbacks[id].data = (void *)value;
  415. feedbacks[id].options.float_options.precision = precision;
  416. feedbacks[id].options.float_options.prefix = prefix ? strdup(prefix) : STRING_DEFAULT_EMPTY;
  417. feedbacks[id].options.float_options.suffix = suffix ? strdup(suffix) : STRING_DEFAULT_EMPTY;
  418. registered_feedbacks++;
  419. return id;
  420. }
  421. feedback_id_t ESPKNXIP::feedback_register_bool(String name, bool *value, char const *true_text, char const *false_text, enable_condition_t cond)
  422. {
  423. if (registered_feedbacks >= MAX_FEEDBACKS)
  424. return -1;
  425. feedback_id_t id = registered_feedbacks;
  426. feedbacks[id].type = FEEDBACK_TYPE_BOOL;
  427. feedbacks[id].name = name;
  428. feedbacks[id].cond = cond;
  429. feedbacks[id].data = (void *)value;
  430. feedbacks[id].options.bool_options.true_text = true_text ? strdup(true_text) : STRING_DEFAULT_TRUE;
  431. feedbacks[id].options.bool_options.false_text = false_text ? strdup(false_text) : STRING_DEFAULT_FALSE;
  432. registered_feedbacks++;
  433. return id;
  434. }
  435. feedback_id_t ESPKNXIP::feedback_register_action(String name, feedback_action_fptr_t value, const char *btn_text, void *arg, enable_condition_t cond)
  436. {
  437. if (registered_feedbacks >= MAX_FEEDBACKS)
  438. return -1;
  439. feedback_id_t id = registered_feedbacks;
  440. feedbacks[id].type = FEEDBACK_TYPE_ACTION;
  441. feedbacks[id].name = name;
  442. feedbacks[id].cond = cond;
  443. feedbacks[id].data = (void *)value;
  444. feedbacks[id].options.action_options.arg = arg;
  445. feedbacks[id].options.action_options.btn_text = btn_text ? strdup(btn_text) : STRING_DEFAULT_DO_THIS;
  446. registered_feedbacks++;
  447. return id;
  448. }
  449. void ESPKNXIP::loop()
  450. {
  451. __loop_knx();
  452. if (server != nullptr)
  453. {
  454. __loop_webserver();
  455. }
  456. }
  457. void ESPKNXIP::__loop_webserver()
  458. {
  459. server->handleClient();
  460. }
  461. void ESPKNXIP::__loop_knx()
  462. {
  463. int read = udp.parsePacket();
  464. if (!read)
  465. {
  466. return;
  467. }
  468. DEBUG_PRINTLN(F(""));
  469. DEBUG_PRINT(F("LEN: "));
  470. DEBUG_PRINTLN(read);
  471. uint8_t buf[read];
  472. udp.read(buf, read);
  473. udp.flush();
  474. DEBUG_PRINT(F("Got packet:"));
  475. #ifdef ESP_KNX_DEBUG
  476. for (int i = 0; i < read; ++i)
  477. {
  478. DEBUG_PRINT(F(" 0x"));
  479. DEBUG_PRINT(buf[i], 16);
  480. }
  481. #endif
  482. DEBUG_PRINTLN(F(""));
  483. knx_ip_pkt_t *knx_pkt = (knx_ip_pkt_t *)buf;
  484. DEBUG_PRINT(F("ST: 0x"));
  485. DEBUG_PRINTLN(__ntohs(knx_pkt->service_type), 16);
  486. if (knx_pkt->header_len != 0x06 && knx_pkt->protocol_version != 0x10 && knx_pkt->service_type != KNX_ST_ROUTING_INDICATION)
  487. return;
  488. cemi_msg_t *cemi_msg = (cemi_msg_t *)knx_pkt->pkt_data;
  489. DEBUG_PRINT(F("MT: 0x"));
  490. DEBUG_PRINTLN(cemi_msg->message_code, 16);
  491. if (cemi_msg->message_code != KNX_MT_L_DATA_IND)
  492. return;
  493. DEBUG_PRINT(F("ADDI: 0x"));
  494. DEBUG_PRINTLN(cemi_msg->additional_info_len, 16);
  495. cemi_service_t *cemi_data = &cemi_msg->data.service_information;
  496. if (cemi_msg->additional_info_len > 0)
  497. cemi_data = (cemi_service_t *)(((uint8_t *)cemi_data) + cemi_msg->additional_info_len);
  498. DEBUG_PRINT(F("C1: 0x"));
  499. DEBUG_PRINTLN(cemi_data->control_1.byte, 16);
  500. DEBUG_PRINT(F("C2: 0x"));
  501. DEBUG_PRINTLN(cemi_data->control_2.byte, 16);
  502. DEBUG_PRINT(F("DT: 0x"));
  503. DEBUG_PRINTLN(cemi_data->control_2.bits.dest_addr_type, 16);
  504. if (cemi_data->control_2.bits.dest_addr_type != 0x01)
  505. return;
  506. DEBUG_PRINT(F("HC: 0x"));
  507. DEBUG_PRINTLN(cemi_data->control_2.bits.hop_count, 16);
  508. DEBUG_PRINT(F("EFF: 0x"));
  509. DEBUG_PRINTLN(cemi_data->control_2.bits.extended_frame_format, 16);
  510. DEBUG_PRINT(F("Source: 0x"));
  511. DEBUG_PRINT(cemi_data->source.bytes.high, 16);
  512. DEBUG_PRINT(F(" 0x"));
  513. DEBUG_PRINTLN(cemi_data->source.bytes.low, 16);
  514. DEBUG_PRINT(F("Dest: 0x"));
  515. DEBUG_PRINT(cemi_data->destination.bytes.high, 16);
  516. DEBUG_PRINT(F(" 0x"));
  517. DEBUG_PRINTLN(cemi_data->destination.bytes.low, 16);
  518. knx_command_type_t ct = (knx_command_type_t)(((cemi_data->data[0] & 0xC0) >> 6) | ((cemi_data->pci.apci & 0x03) << 2));
  519. DEBUG_PRINT(F("CT: 0x"));
  520. DEBUG_PRINTLN(ct, 16);
  521. #ifdef ESP_KNX_DEBUG
  522. for (int i = 0; i < cemi_data->data_len; ++i)
  523. {
  524. DEBUG_PRINT(F(" 0x"));
  525. DEBUG_PRINT(cemi_data->data[i], 16);
  526. }
  527. #endif
  528. DEBUG_PRINTLN(F("=="));
  529. // Call callbacks
  530. for (int i = 0; i < registered_callback_assignments; ++i)
  531. {
  532. DEBUG_PRINT(F("Testing: 0x"));
  533. DEBUG_PRINT(callback_assignments[i].address.bytes.high, 16);
  534. DEBUG_PRINT(F(" 0x"));
  535. DEBUG_PRINTLN(callback_assignments[i].address.bytes.low, 16);
  536. if (cemi_data->destination.value == callback_assignments[i].address.value)
  537. {
  538. DEBUG_PRINTLN(F("Found match"));
  539. if (callbacks[callback_assignments[i].callback_id].cond && !callbacks[callback_assignments[i].callback_id].cond())
  540. {
  541. DEBUG_PRINTLN(F("But it's disabled"));
  542. #if ALLOW_MULTIPLE_CALLBACKS_PER_ADDRESS
  543. continue;
  544. #else
  545. return;
  546. #endif
  547. }
  548. uint8_t data[cemi_data->data_len];
  549. memcpy(data, cemi_data->data, cemi_data->data_len);
  550. data[0] = data[0] & 0x3F;
  551. message_t msg = {};
  552. msg.ct = ct;
  553. msg.received_on = cemi_data->destination;
  554. msg.data_len = cemi_data->data_len;
  555. msg.data = data;
  556. callbacks[callback_assignments[i].callback_id].fkt(msg, callbacks[callback_assignments[i].callback_id].arg);
  557. #if ALLOW_MULTIPLE_CALLBACKS_PER_ADDRESS
  558. continue;
  559. #else
  560. return;
  561. #endif
  562. }
  563. }
  564. return;
  565. }
  566. // Global "singleton" object
  567. ESPKNXIP knx;