Tasmota/lib/esp-knx-ip-0.5.2/esp-knx-ip.cpp
Johannes Morgenroth 05c45f7ad7 Remove flush() call after read of KNX packets
The flush() call after read() causes empty packets being sent as response
for all UDP packets received on that port.
2020-05-16 18:05:23 +02:00

663 lines
17 KiB
C++

/**
* esp-knx-ip library for KNX/IP communication on an ESP8266
* Author: Nico Weichbrodt <envy>
* 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;
}
}
}
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;
}
}
}
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;