/** * esp-knx-ip library for KNX/IP communication on an ESP8266 * Author: Nico Weichbrodt * License: MIT */ #include "esp-knx-ip.h" char const *string_defaults[] = { "Do this", "True", "False", "" }; ESPKNXIP::ESPKNXIP() : server(nullptr), registered_callback_assignments(0), free_callback_assignment_slots(0), registered_callbacks(0), free_callback_slots(0), registered_configs(0), registered_feedbacks(0) { DEBUG_PRINTLN(); DEBUG_PRINTLN("ESPKNXIP starting up"); // Default physical address is 1.1.0 physaddr.bytes.high = (/*area*/1 << 4) | /*line*/1; physaddr.bytes.low = /*member*/0; memset(callback_assignments, 0, MAX_CALLBACK_ASSIGNMENTS * sizeof(callback_assignment_t)); memset(callbacks, 0, MAX_CALLBACKS * sizeof(callback_fptr_t)); memset(custom_config_data, 0, MAX_CONFIG_SPACE * sizeof(uint8_t)); memset(custom_config_default_data, 0, MAX_CONFIG_SPACE * sizeof(uint8_t)); memset(custom_configs, 0, MAX_CONFIGS * sizeof(config_t)); } void ESPKNXIP::load() { memcpy(custom_config_default_data, custom_config_data, MAX_CONFIG_SPACE); EEPROM.begin(EEPROM_SIZE); restore_from_eeprom(); } void ESPKNXIP::start(ESP8266WebServer *srv) { server = srv; __start(); } void ESPKNXIP::start() { server = new ESP8266WebServer(80); __start(); } void ESPKNXIP::__start() { if (server != nullptr) { server->on(ROOT_PREFIX, [this](){ __handle_root(); }); server->on(__ROOT_PATH, [this](){ __handle_root(); }); server->on(__REGISTER_PATH, [this](){ __handle_register(); }); server->on(__DELETE_PATH, [this](){ __handle_delete(); }); server->on(__PHYS_PATH, [this](){ __handle_set(); }); #if !DISABLE_EEPROM_BUTTONS server->on(__EEPROM_PATH, [this](){ __handle_eeprom(); }); #endif server->on(__CONFIG_PATH, [this](){ __handle_config(); }); server->on(__FEEDBACK_PATH, [this](){ __handle_feedback(); }); #if !DISABLE_RESTORE_BUTTON server->on(__RESTORE_PATH, [this](){ __handle_restore(); }); #endif #if !DISABLE_REBOOT_BUTTON server->on(__REBOOT_PATH, [this](){ __handle_reboot(); }); #endif server->begin(); } udp.beginMulticast(WiFi.localIP(), MULTICAST_IP, MULTICAST_PORT); } void ESPKNXIP::save_to_eeprom() { uint32_t address = 0; uint64_t magic = EEPROM_MAGIC; EEPROM.put(address, magic); address += sizeof(uint64_t); EEPROM.put(address++, registered_callback_assignments); for (uint8_t i = 0; i < MAX_CALLBACK_ASSIGNMENTS; ++i) { EEPROM.put(address, callback_assignments[i].address); address += sizeof(address_t); } for (uint8_t i = 0; i < MAX_CALLBACK_ASSIGNMENTS; ++i) { EEPROM.put(address, callback_assignments[i].callback_id); address += sizeof(callback_id_t); } EEPROM.put(address, physaddr); address += sizeof(address_t); EEPROM.put(address, custom_config_data); address += sizeof(custom_config_data); EEPROM.commit(); DEBUG_PRINT("Wrote to EEPROM: 0x"); DEBUG_PRINTLN(address, HEX); } void ESPKNXIP::restore_from_eeprom() { uint32_t address = 0; uint64_t magic = 0; EEPROM.get(address, magic); if (magic != EEPROM_MAGIC) { DEBUG_PRINTLN("No valid magic in EEPROM, aborting restore."); DEBUG_PRINT("Expected 0x"); DEBUG_PRINT((unsigned long)(EEPROM_MAGIC >> 32), HEX); DEBUG_PRINT(" 0x"); DEBUG_PRINT((unsigned long)(EEPROM_MAGIC), HEX); DEBUG_PRINT(" got 0x"); DEBUG_PRINT((unsigned long)(magic >> 32), HEX); DEBUG_PRINT(" 0x"); DEBUG_PRINTLN((unsigned long)magic, HEX); return; } address += sizeof(uint64_t); EEPROM.get(address++, registered_callback_assignments); for (uint8_t i = 0; i < MAX_CALLBACK_ASSIGNMENTS; ++i) { EEPROM.get(address, callback_assignments[i].address); if (callback_assignments[i].address.value != 0) { // if address is not 0/0/0 then mark slot as used callback_assignments[i].slot_flags |= SLOT_FLAGS_USED; DEBUG_PRINTLN("used slot"); } else { // if address is 0/0/0, then we found a free slot, yay! // however, only count those slots, if we have not reached registered_callback_assignments yet if (i < registered_callback_assignments) { DEBUG_PRINTLN("free slot before reaching registered_callback_assignments"); free_callback_assignment_slots++; } else { DEBUG_PRINTLN("free slot"); } } address += sizeof(address_t); } for (uint8_t i = 0; i < MAX_CALLBACK_ASSIGNMENTS; ++i) { EEPROM.get(address, callback_assignments[i].callback_id); address += sizeof(callback_id_t); } EEPROM.get(address, physaddr); address += sizeof(address_t); //EEPROM.get(address, custom_config_data); //address += sizeof(custom_config_data); uint32_t conf_offset = address; for (uint8_t i = 0; i < registered_configs; ++i) { // First byte is flags. config_flags_t flags = CONFIG_FLAGS_NO_FLAGS; flags = (config_flags_t)EEPROM.read(address); DEBUG_PRINT("Flag in EEPROM @ "); DEBUG_PRINT(address - conf_offset); DEBUG_PRINT(": "); DEBUG_PRINTLN(flags, BIN); custom_config_data[custom_configs[i].offset] = flags; if (flags & CONFIG_FLAGS_VALUE_SET) { DEBUG_PRINTLN("Non-default value"); for (int j = 0; j < custom_configs[i].len - sizeof(uint8_t); ++j) { custom_config_data[custom_configs[i].offset + sizeof(uint8_t) + j] = EEPROM.read(address + sizeof(uint8_t) + j); } } address += custom_configs[i].len; } DEBUG_PRINT("Restored from EEPROM: 0x"); DEBUG_PRINTLN(address, HEX); } uint16_t ESPKNXIP::__ntohs(uint16_t n) { return (uint16_t)((((uint8_t*)&n)[0] << 8) | (((uint8_t*)&n)[1])); } callback_assignment_id_t ESPKNXIP::__callback_register_assignment(address_t address, callback_id_t id) { if (registered_callback_assignments >= MAX_CALLBACK_ASSIGNMENTS) return -1; if (free_callback_assignment_slots == 0) { callback_assignment_id_t aid = registered_callback_assignments; callback_assignments[aid].slot_flags |= SLOT_FLAGS_USED; callback_assignments[aid].address = address; callback_assignments[aid].callback_id = id; registered_callback_assignments++; return aid; } else { // find the free slot for (callback_assignment_id_t aid = 0; aid < registered_callback_assignments; ++aid) { if (callback_assignments[aid].slot_flags & SLOT_FLAGS_USED) { // found a used slot continue; } // and now an empty one callback_assignments[aid].slot_flags |= SLOT_FLAGS_USED; callback_assignments[aid].address = address; callback_assignments[aid].callback_id = id; free_callback_assignment_slots--; return id; } } return -1; } void ESPKNXIP::__callback_delete_assignment(callback_assignment_id_t id) { // TODO this can be optimized if we are deleting the last element // as then we can decrement registered_callback_assignments // clear slot and mark it as empty callback_assignments[id].slot_flags = SLOT_FLAGS_EMPTY; callback_assignments[id].address.value = 0; callback_assignments[id].callback_id = 0; if (id == registered_callback_assignments - 1) { DEBUG_PRINTLN("last cba deleted"); // If this is the last callback, we can delete it by decrementing registered_callbacks. registered_callback_assignments--; // However, if the assignment before this slot are also empty, we can decrement even further // First check if this was also the first element if (id == 0) { DEBUG_PRINTLN("really last cba"); // If this was the last, then we are done. return; } id--; while(true) { DEBUG_PRINT("checking "); DEBUG_PRINTLN((int32_t)id); if ((callback_assignments[id].slot_flags & SLOT_FLAGS_USED) == 0) { DEBUG_PRINTLN("merged free slot"); // Slot before is empty free_callback_assignment_slots--; registered_callback_assignments--; } else { DEBUG_PRINTLN("aborted on used slot"); // Slot is used, abort return; } id--; if (id == CALLBACK_ASSIGNMENT_ID_MAX) { DEBUG_PRINTLN("abort on wrap"); // Wrap around, abort return; } } } else { DEBUG_PRINTLN("free slot created"); // there is now one more free slot free_callback_assignment_slots++; } } bool ESPKNXIP::__callback_is_id_valid(callback_id_t id) { if (id < registered_callbacks) return true; if (callbacks[id].slot_flags & SLOT_FLAGS_USED) return true; return false; } callback_id_t ESPKNXIP::callback_register(String name, callback_fptr_t cb, void *arg, enable_condition_t cond) { if (registered_callbacks >= MAX_CALLBACKS) return -1; if (free_callback_slots == 0) { callback_id_t id = registered_callbacks; callbacks[id].slot_flags |= SLOT_FLAGS_USED; callbacks[id].name = name; callbacks[id].fkt = cb; callbacks[id].cond = cond; callbacks[id].arg = arg; registered_callbacks++; return id; } else { // find the free slot for (callback_id_t id = 0; id < registered_callbacks; ++id) { if (callbacks[id].slot_flags & SLOT_FLAGS_USED) { // found a used slot continue; } // and now an empty one callbacks[id].slot_flags |= SLOT_FLAGS_USED; callbacks[id].name = name; callbacks[id].fkt = cb; callbacks[id].cond = cond; callbacks[id].arg = arg; free_callback_slots--; return id; } } return -1; } void ESPKNXIP::callback_deregister(callback_id_t id) { if (!__callback_is_id_valid(id)) return; // clear slot and mark it as empty callbacks[id].slot_flags = SLOT_FLAGS_EMPTY; callbacks[id].fkt = nullptr; callbacks[id].cond = nullptr; callbacks[id].arg = nullptr; if (id == registered_callbacks - 1) { // If this is the last callback, we can delete it by decrementing registered_callbacks. registered_callbacks--; // However, if the callbacks before this slot are also empty, we can decrement even further // First check if this was also the first element if (id == 0) { // If this was the last, then we are done. return; } id--; while(true) { if ((callbacks[id].slot_flags & SLOT_FLAGS_USED) == 0) { // Slot is empty free_callback_slots--; registered_callbacks--; } else { // Slot is used, abort return; } id--; if (id == CALLBACK_ASSIGNMENT_ID_MAX) { // Wrap around, abort return; } } } else { // there is now one more free slot free_callback_slots++; } } callback_assignment_id_t ESPKNXIP::callback_assign(callback_id_t id, address_t val) { if (!__callback_is_id_valid(id)) return -1; return __callback_register_assignment(val, id); } void ESPKNXIP::callback_unassign(callback_assignment_id_t id) { if (!__callback_is_id_valid(id)) return; __callback_delete_assignment(id); } /** * Feedback functions start here */ feedback_id_t ESPKNXIP::feedback_register_int(String name, int32_t *value, enable_condition_t cond) { if (registered_feedbacks >= MAX_FEEDBACKS) return -1; feedback_id_t id = registered_feedbacks; feedbacks[id].type = FEEDBACK_TYPE_INT; feedbacks[id].name = name; feedbacks[id].cond = cond; feedbacks[id].data = (void *)value; registered_feedbacks++; return id; } feedback_id_t ESPKNXIP::feedback_register_float(String name, float *value, uint8_t precision, char const *prefix, char const *suffix, enable_condition_t cond) { if (registered_feedbacks >= MAX_FEEDBACKS) return -1; feedback_id_t id = registered_feedbacks; feedbacks[id].type = FEEDBACK_TYPE_FLOAT; feedbacks[id].name = name; feedbacks[id].cond = cond; feedbacks[id].data = (void *)value; feedbacks[id].options.float_options.precision = precision; feedbacks[id].options.float_options.prefix = prefix ? strdup(prefix) : STRING_DEFAULT_EMPTY; feedbacks[id].options.float_options.suffix = suffix ? strdup(suffix) : STRING_DEFAULT_EMPTY; registered_feedbacks++; return id; } feedback_id_t ESPKNXIP::feedback_register_bool(String name, bool *value, char const *true_text, char const *false_text, enable_condition_t cond) { if (registered_feedbacks >= MAX_FEEDBACKS) return -1; feedback_id_t id = registered_feedbacks; feedbacks[id].type = FEEDBACK_TYPE_BOOL; feedbacks[id].name = name; feedbacks[id].cond = cond; feedbacks[id].data = (void *)value; feedbacks[id].options.bool_options.true_text = true_text ? strdup(true_text) : STRING_DEFAULT_TRUE; feedbacks[id].options.bool_options.false_text = false_text ? strdup(false_text) : STRING_DEFAULT_FALSE; registered_feedbacks++; return id; } feedback_id_t ESPKNXIP::feedback_register_action(String name, feedback_action_fptr_t value, const char *btn_text, void *arg, enable_condition_t cond) { if (registered_feedbacks >= MAX_FEEDBACKS) return -1; feedback_id_t id = registered_feedbacks; feedbacks[id].type = FEEDBACK_TYPE_ACTION; feedbacks[id].name = name; feedbacks[id].cond = cond; feedbacks[id].data = (void *)value; feedbacks[id].options.action_options.arg = arg; feedbacks[id].options.action_options.btn_text = btn_text ? strdup(btn_text) : STRING_DEFAULT_DO_THIS; registered_feedbacks++; return id; } void ESPKNXIP::loop() { __loop_knx(); if (server != nullptr) { __loop_webserver(); } } void ESPKNXIP::__loop_webserver() { server->handleClient(); } void ESPKNXIP::__loop_knx() { int read = udp.parsePacket(); if (!read) { return; } DEBUG_PRINTLN(F("")); DEBUG_PRINT(F("LEN: ")); DEBUG_PRINTLN(read); uint8_t buf[read]; udp.read(buf, read); DEBUG_PRINT(F("Got packet:")); #ifdef ESP_KNX_DEBUG for (int i = 0; i < read; ++i) { DEBUG_PRINT(F(" 0x")); DEBUG_PRINT(buf[i], 16); } #endif DEBUG_PRINTLN(F("")); knx_ip_pkt_t *knx_pkt = (knx_ip_pkt_t *)buf; DEBUG_PRINT(F("ST: 0x")); DEBUG_PRINTLN(__ntohs(knx_pkt->service_type), 16); if (knx_pkt->header_len != 0x06 && knx_pkt->protocol_version != 0x10 && knx_pkt->service_type != KNX_ST_ROUTING_INDICATION) return; cemi_msg_t *cemi_msg = (cemi_msg_t *)knx_pkt->pkt_data; DEBUG_PRINT(F("MT: 0x")); DEBUG_PRINTLN(cemi_msg->message_code, 16); if (cemi_msg->message_code != KNX_MT_L_DATA_IND) return; DEBUG_PRINT(F("ADDI: 0x")); DEBUG_PRINTLN(cemi_msg->additional_info_len, 16); cemi_service_t *cemi_data = &cemi_msg->data.service_information; if (cemi_msg->additional_info_len > 0) cemi_data = (cemi_service_t *)(((uint8_t *)cemi_data) + cemi_msg->additional_info_len); DEBUG_PRINT(F("C1: 0x")); DEBUG_PRINTLN(cemi_data->control_1.byte, 16); DEBUG_PRINT(F("C2: 0x")); DEBUG_PRINTLN(cemi_data->control_2.byte, 16); DEBUG_PRINT(F("DT: 0x")); DEBUG_PRINTLN(cemi_data->control_2.bits.dest_addr_type, 16); if (cemi_data->control_2.bits.dest_addr_type != 0x01) return; DEBUG_PRINT(F("HC: 0x")); DEBUG_PRINTLN(cemi_data->control_2.bits.hop_count, 16); DEBUG_PRINT(F("EFF: 0x")); DEBUG_PRINTLN(cemi_data->control_2.bits.extended_frame_format, 16); DEBUG_PRINT(F("Source: 0x")); DEBUG_PRINT(cemi_data->source.bytes.high, 16); DEBUG_PRINT(F(" 0x")); DEBUG_PRINTLN(cemi_data->source.bytes.low, 16); DEBUG_PRINT(F("Dest: 0x")); DEBUG_PRINT(cemi_data->destination.bytes.high, 16); DEBUG_PRINT(F(" 0x")); DEBUG_PRINTLN(cemi_data->destination.bytes.low, 16); knx_command_type_t ct = (knx_command_type_t)(((cemi_data->data[0] & 0xC0) >> 6) | ((cemi_data->pci.apci & 0x03) << 2)); DEBUG_PRINT(F("CT: 0x")); DEBUG_PRINTLN(ct, 16); #ifdef ESP_KNX_DEBUG for (int i = 0; i < cemi_data->data_len; ++i) { DEBUG_PRINT(F(" 0x")); DEBUG_PRINT(cemi_data->data[i], 16); } #endif DEBUG_PRINTLN(F("==")); // Call callbacks for (int i = 0; i < registered_callback_assignments; ++i) { DEBUG_PRINT(F("Testing: 0x")); DEBUG_PRINT(callback_assignments[i].address.bytes.high, 16); DEBUG_PRINT(F(" 0x")); DEBUG_PRINTLN(callback_assignments[i].address.bytes.low, 16); if (cemi_data->destination.value == callback_assignments[i].address.value) { DEBUG_PRINTLN(F("Found match")); if (callbacks[callback_assignments[i].callback_id].cond && !callbacks[callback_assignments[i].callback_id].cond()) { DEBUG_PRINTLN(F("But it's disabled")); #if ALLOW_MULTIPLE_CALLBACKS_PER_ADDRESS continue; #else return; #endif } uint8_t data[cemi_data->data_len]; memcpy(data, cemi_data->data, cemi_data->data_len); data[0] = data[0] & 0x3F; message_t msg = {}; msg.ct = ct; msg.received_on = cemi_data->destination; msg.data_len = cemi_data->data_len; msg.data = data; callbacks[callback_assignments[i].callback_id].fkt(msg, callbacks[callback_assignments[i].callback_id].arg); #if ALLOW_MULTIPLE_CALLBACKS_PER_ADDRESS continue; #else return; #endif } } return; } // Global "singleton" object ESPKNXIP knx;