diff options
-rw-r--r-- | confui/confui.pro | 46 | ||||
-rw-r--r-- | confui/main.cpp | 18 | ||||
-rw-r--r-- | confui/main.h | 7 | ||||
-rw-r--r-- | confui/mainwindow.cpp | 45 | ||||
-rw-r--r-- | confui/mainwindow.h | 7 | ||||
-rw-r--r-- | confui/makefile | 9 | ||||
-rw-r--r-- | confui/mesh_connector.cpp | 112 | ||||
-rw-r--r-- | confui/mesh_connector.h | 35 | ||||
-rw-r--r-- | confui/serial.cpp | 198 | ||||
-rw-r--r-- | confui/serial.h | 42 | ||||
-rw-r--r-- | confui/ui_automation.cpp | 4 | ||||
-rw-r--r-- | confui/ui_node.cpp | 7 | ||||
-rw-r--r-- | shared/bin.c | 101 | ||||
-rw-r--r-- | shared/bin.h | 81 | ||||
-rw-r--r-- | shared/consts.h | 10 | ||||
-rw-r--r-- | shared/pclient.c | 164 | ||||
-rw-r--r-- | shared/pclient.h | 91 | ||||
-rw-r--r-- | shared/protocol-tests/.gitignore | 1 | ||||
-rw-r--r-- | shared/protocol-tests/get-node-response.src | 81 | ||||
-rw-r--r-- | shared/protocol-tests/makefile | 15 | ||||
-rw-r--r-- | shared/protocol-tests/ping-response.src | 10 | ||||
-rw-r--r-- | shared/protocol-tests/ping.src | 4 | ||||
-rw-r--r-- | shared/protocol.c | 58 | ||||
-rw-r--r-- | shared/protocol.h | 183 | ||||
-rw-r--r-- | shared/protocol.md | 15 | ||||
-rw-r--r-- | shared/serial_parse.c | 61 | ||||
-rw-r--r-- | shared/serial_parse.h | 26 |
27 files changed, 1351 insertions, 80 deletions
diff --git a/confui/confui.pro b/confui/confui.pro index 2c4df61..f73465b 100644 --- a/confui/confui.pro +++ b/confui/confui.pro @@ -1,24 +1,38 @@ -QT += core gui widgets +QT += core gui widgets serialport SOURCES += \ - main.cpp \ - mainwindow.cpp \ - mesh_connector.cpp \ - ui_tab_automations.cpp \ - ui_tab_node_overview.cpp \ - ui_node.cpp \ - ui_automation.cpp \ - ui_scroll_container.cpp + main.cpp \ + mainwindow.cpp \ + mesh_connector.cpp \ + ui_tab_automations.cpp \ + ui_tab_node_overview.cpp \ + ui_node.cpp \ + ui_automation.cpp \ + ui_scroll_container.cpp \ + serial.cpp \ + ../shared/bin.c \ + ../shared/protocol.c \ + ../shared/serial_parse.c \ + ../shared/pclient.c HEADERS += \ - mainwindow.h \ - mesh_connector.h \ - ui_tab_automations.h \ - ui_tab_node_overview.h \ - ui_node.h \ - ui_automation.h \ - ui_scroll_container.h + mainwindow.h \ + mesh_connector.h \ + ui_tab_automations.h \ + ui_tab_node_overview.h \ + ui_node.h \ + ui_automation.h \ + ui_scroll_container.h \ + serial.h \ + ../shared/bin.h \ + ../shared/protocol.h \ + ../shared/serial_parse.h \ + ../shared/consts.h \ + main.h \ + ../shared/pclient.h CONFIG += c++17 CONFIG += force_debug_info QMAKE_CXXFLAGS += -Wno-missing-field-initializers +QMAKE_CFLAGS += -std=c11 +QMAKE_CFLAGS += -Wno-c99-designator diff --git a/confui/main.cpp b/confui/main.cpp index 6c3ab56..ae52a61 100644 --- a/confui/main.cpp +++ b/confui/main.cpp @@ -1,10 +1,18 @@ +#include "main.h" #include "mainwindow.h" -#include <QApplication> +extern "C" { +static const uint16_t _test = 1; +static const uint8_t *_ptest = (uint8_t *)&_test; +uint8_t g_cd_endianness; +} + +CDMainWindow* g_cd_main_window = nullptr; int main(int argc, char *argv[]) { - QApplication a(argc, argv); - CDMainWindow w; - w.show(); - return a.exec(); + g_cd_endianness = *_ptest; + g_cd_app = new QApplication(argc, argv); + g_cd_main_window = new CDMainWindow(); + g_cd_main_window->show(); + return g_cd_app->exec(); } diff --git a/confui/main.h b/confui/main.h new file mode 100644 index 0000000..10e9da6 --- /dev/null +++ b/confui/main.h @@ -0,0 +1,7 @@ +#pragma once + +#include <QApplication> + +extern QApplication* g_cd_app; +int main(int argc, char *argv[]); + diff --git a/confui/mainwindow.cpp b/confui/mainwindow.cpp index cff5002..4c9b857 100644 --- a/confui/mainwindow.cpp +++ b/confui/mainwindow.cpp @@ -1,18 +1,29 @@ #include <QGridLayout> #include <QMenuBar> #include <QTabWidget> +#include <iostream> #include "mainwindow.h" #include "ui_tab_automations.h" #include "ui_tab_node_overview.h" +#include "serial.h" +#include "../shared/pclient.h" +#include "main.h" CDMeshConnector *g_cd_mesh_connector = nullptr; +CDSerialConnector *g_cd_serial = nullptr; +QApplication* g_cd_app = nullptr; -CDMainWindow::~CDMainWindow() { delete this->mesh_connector; } +CDMainWindow::~CDMainWindow() { + delete g_cd_mesh_connector; + delete g_cd_serial; +} CDMainWindow::CDMainWindow(QWidget *parent) : QMainWindow(parent) { g_cd_mesh_connector = new CDMeshConnector(); this->mesh_connector = g_cd_mesh_connector; + g_cd_serial = new CDSerialConnector(); + this->serial_connector = g_cd_serial; menu_bar = new QMenuBar(this); @@ -24,6 +35,10 @@ CDMainWindow::CDMainWindow(QWidget *parent) : QMainWindow(parent) { tab_bar_widget->addTab(this->node_overview_tab, "node overview"); tab_bar_widget->addTab(this->automations_tab, "automations"); + // manually connect to serial port + if (g_cd_app->arguments().length() > 1 && g_cd_app->arguments().at(1).length() > 0) + g_cd_serial->connect(g_cd_app->arguments().at(1).toStdString()); + setMenuBar(menu_bar); setCentralWidget(tab_bar_widget); update(); @@ -45,10 +60,24 @@ void CDMainWindow::update() { QAction *menu_options_add_automation = menu_options->addAction("add automation"); connect(menu_options_add_automation, &QAction::triggered, this, &CDMainWindow::menu_add_automation); - QMenu *menu_options_serialport = menu_options->addMenu("serial port (FIXME)"); - - menu_options_serialport->addAction("FIXME A"); - menu_options_serialport->addAction("FIXME B"); + QString serial_port_menu_label = "serial port"; + string port_name = g_cd_serial->get_port(); + if (port_name.size() > 0) { + serial_port_menu_label.append(" ("); + serial_port_menu_label.append(QString::fromStdString(port_name)); + serial_port_menu_label.append(")"); + } + QMenu *menu_options_serialport = menu_options->addMenu(serial_port_menu_label); + + vector<string> ports = CDSerialConnector::get_ports(); + for (string port : ports) { + QAction* menu_port = menu_options_serialport->addAction(QString::fromStdString(port)); + connect(menu_port, &QAction::triggered, this, [this, port](){ menu_set_serial_port(port); }); + } + + cd_s_bin* msg = cd_cmd_gen_get_node(true, NULL); + cd_pclient_send(msg); + free(msg); } void CDMainWindow::menu_refresh() { update(); } @@ -57,3 +86,9 @@ void CDMainWindow::menu_add_automation() { g_cd_mesh_connector->create_link(); update(); } + +void CDMainWindow::menu_set_serial_port(string new_port) { + g_cd_serial->disconnect(); + g_cd_serial->connect(new_port); + update(); +} diff --git a/confui/mainwindow.h b/confui/mainwindow.h index 3a0b4f7..4093cd1 100644 --- a/confui/mainwindow.h +++ b/confui/mainwindow.h @@ -6,9 +6,13 @@ #include <QDebug> #include "mesh_connector.h" +#include "serial.h" class CDAutomationsTabWidget; class CDNodeOverviewTabWidget; +class CDMainWindow; + +extern CDMainWindow *g_cd_main_window; /** * @brief main window @@ -26,6 +30,7 @@ private: public: CDMeshConnector *mesh_connector = nullptr; + CDSerialConnector *serial_connector = nullptr; CDMainWindow(QWidget *parent = nullptr); ~CDMainWindow(); @@ -35,4 +40,6 @@ public: virtual void menu_refresh(); /** @brief menu bar add automation action handler */ virtual void menu_add_automation(); + /** @brief menu bar set serial port action handler */ + void menu_set_serial_port(string new_port); }; diff --git a/confui/makefile b/confui/makefile index 930daed..2e22e20 100644 --- a/confui/makefile +++ b/confui/makefile @@ -7,12 +7,6 @@ endif include confui.mk -OBJECTS += $(patsubst %.c,%.o, $(wildcard ../shared/*.c)) -confui: $(OBJECTS) - -../shared/%.o: ../shared/%.c - $(CC) -c $(CFLAGS) -w $< -o $@ - FMT_FILES := $(DIST) FMT_FILES := $(filter-out .%,$(FMT_FILES)) # filter hidden files FMT_FILES := $(filter-out /%,$(FMT_FILES)) # filter files outside working directory @@ -24,3 +18,6 @@ FMT_FILES := $(filter-out %.pro,$(FMT_FILES)) # filter *.pro format: clang-format -i $(FMT_FILES) clang-tidy --fix-errors $(FMT_FILES) + +compile_commands: + compiledb make -Bn diff --git a/confui/mesh_connector.cpp b/confui/mesh_connector.cpp index 5b65d31..7ef0f02 100644 --- a/confui/mesh_connector.cpp +++ b/confui/mesh_connector.cpp @@ -4,6 +4,7 @@ #include <cstring> #include <stdio.h> +#include "../shared/pclient.h" #include "mesh_connector.h" using std::pair; @@ -16,31 +17,32 @@ cd_link_t CDMeshConnector::get_new_link_id() { return _fresh_link_id++; } cd_uid_t CDMeshConnector::get_new_node_id() { return _fresh_node_id++; } CDMeshConnector::CDMeshConnector() { - cd_uid_t berta = create_node({ - .address = {0x00, 0xff, 0x21, 0x69, 0xf2, 0x31}, - .name_len = 5, - .name = "berta", - .light_on = false, - .provisioned = false, - }); - - cd_uid_t gerrit = create_node({ - .address = {0x0e, 0xf9, 0x46, 0x4d, 0xe8, 0x02}, - .name_len = 6, - .name = "gerrit", - .light_on = false, - .provisioned = false, - }); - - create_link(berta, berta, CD_AUTOMATION_TYPE_TOGGLE); - create_link(berta, berta, CD_AUTOMATION_TYPE_TOGGLE); - create_link(gerrit, berta, CD_AUTOMATION_TYPE_TURN_OFF); - create_link(gerrit, gerrit, CD_AUTOMATION_TYPE_TURN_ON); + // cd_uid_t berta = create_node({ + // .address = {0x00, 0xff, 0x21, 0x69, 0xf2, 0x31}, + // .name_len = 5, + // .name = "berta", + // .light_on = false, + // .provisioned = false, + // }); + + // cd_uid_t gerrit = create_node({ + // .address = {0x0e, 0xf9, 0x46, 0x4d, 0xe8, 0x02}, + // .name_len = 6, + // .name = "gerrit", + // .light_on = false, + // .provisioned = false, + // }); + + // create_link(berta, berta, CD_AUTOMATION_TYPE_TOGGLE); + // create_link(berta, berta, CD_AUTOMATION_TYPE_TOGGLE); + // create_link(gerrit, berta, CD_AUTOMATION_TYPE_TURN_OFF); + // create_link(gerrit, gerrit, CD_AUTOMATION_TYPE_TURN_ON); return; } cd_uid_t CDMeshConnector::create_node(cd_s_node node) { cd_s_node *_node = (cd_s_node *)malloc(sizeof(cd_s_node)); + // TODO: handle empty structs // id cd_uid_t id = get_new_node_id(); @@ -98,7 +100,7 @@ map<cd_link_t, cd_s_automation *> CDMeshConnector::get_links(bool valid) { return links; } -void CDMeshConnector::update_link(cd_s_automation *automation) { +void CDMeshConnector::update_link(cd_s_automation *automation, bool publish) { printf("link[%d]", automation->id); if (automation->valid) { printf(" = %.*s %s %.*s", (int)automation->button->name_len, automation->button->name, @@ -110,9 +112,16 @@ void CDMeshConnector::update_link(cd_s_automation *automation) { printf(" (invalid)"); } printf("\n"); + + if (!publish) return; + if (!automation->valid) return; + + cd_s_bin* msg = cd_cmd_gen_post_link_add(automation->button->uuid, automation->light->uuid, automation->type); + cd_pclient_send(msg); + free(msg); } -cd_link_t CDMeshConnector::create_link(cd_uid_t button, cd_uid_t light, enum cd_e_automation_type type) { +cd_link_t CDMeshConnector::create_link(cd_uid_t button, cd_uid_t light, cd_e_automation_type type) { cd_link_t id = get_new_link_id(); cd_s_automation *automation = (cd_s_automation *)malloc(sizeof(cd_s_automation)); @@ -146,11 +155,19 @@ cd_link_t CDMeshConnector::create_link() { return id; } -void CDMeshConnector::remove_link(cd_link_t link_handle) { +void CDMeshConnector::remove_link(cd_link_t link_handle, bool publish) { printf("remove link[%d]\n", link_handle); - if (_links[link_handle] != nullptr) free(_links[link_handle]); + if (_links.count(link_handle) == 0) return; // invalid handle + if (_links[link_handle] == nullptr) return; // already removed link + + if (publish) { + cd_s_bin* msg = cd_cmd_gen_post_link_rm(_links[link_handle]->button->uuid, _links[link_handle]->light->uuid); + cd_pclient_send(msg); + free(msg); + } + + free(_links[link_handle]); _links[link_handle] = nullptr; - return; } void CDMeshConnector::remove_node(cd_uid_t node_handle) { @@ -160,21 +177,32 @@ void CDMeshConnector::remove_node(cd_uid_t node_handle) { return; } -void CDMeshConnector::update_node(cd_s_node *node_ptr) { +void CDMeshConnector::update_node(cd_s_node *node_ptr, bool publish) { printf("turning %.*s %s\n", (int)node_ptr->name_len, node_ptr->name, node_ptr->light_on ? "on" : "off"); - return; + + if (!publish) return; + + cd_s_bin* msg = cd_cmd_gen_post_led(node_ptr->light_on, node_ptr->uuid); + cd_pclient_send(msg); + free(msg); } void CDMeshConnector::network_join_node(cd_s_node *node_ptr) { - node_ptr->provisioned = true; + node_ptr->provisioned = true; //TODO: await success printf("join %.*s into network\n", (int)node_ptr->name_len, node_ptr->name); - return; + + cd_s_bin* msg = cd_cmd_gen_post_net_add(node_ptr->uuid); + cd_pclient_send(msg); + free(msg); } void CDMeshConnector::network_remove_node(cd_s_node *node_ptr) { - node_ptr->provisioned = false; + node_ptr->provisioned = false; //TODO: await success printf("remove %.*s from network\n", (int)node_ptr->name_len, node_ptr->name); - return; + + cd_s_bin* msg = cd_cmd_gen_post_net_rm(node_ptr->uuid); + cd_pclient_send(msg); + free(msg); } string CDMeshConnector::cd_mac_to_string(cd_mac_addr_t mac) { @@ -185,6 +213,28 @@ string CDMeshConnector::cd_mac_to_string(cd_mac_addr_t mac) { return ret; } +string CDMeshConnector::cd_uuid_to_string(cd_uuid_t uuid) { + char *addr = nullptr; + asprintf(&addr, "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", uuid[0], uuid[1], uuid[2], uuid[3], uuid[4], uuid[5], uuid[6], uuid[7], uuid[8], uuid[9], uuid[10], uuid[11], uuid[12], uuid[13], uuid[14], uuid[15]); + string ret = addr; + free(addr); + return ret; +} + cd_s_automation *CDMeshConnector::get_link(cd_link_t id) { return _links[id]; } cd_s_node *CDMeshConnector::get_node(cd_uid_t id) { return _nodes[id]; } + +cd_uid_t CDMeshConnector::get_or_create_node_by_uuid(cd_uuid_t uuid) { + for (pair<cd_uid_t, cd_s_node *> node : _nodes) + if (memcmp(node.second->uuid, uuid, sizeof(cd_uuid_t)) == 0) return node.first; + return create_node({}); +} + +cd_link_t CDMeshConnector::get_or_create_link_by_uuid(cd_uuid_t button, cd_uuid_t light) { + for (pair<cd_uid_t, cd_s_automation *> link : _links) + if (link.second != nullptr && link.second->valid == true && + memcmp(link.second->button->uuid, button, sizeof(cd_uuid_t)) == 0 && + memcmp(link.second->light->uuid, light, sizeof(cd_uuid_t)) == 0) return link.first; + return create_link(); +} diff --git a/confui/mesh_connector.h b/confui/mesh_connector.h index 176778f..8c1fc91 100644 --- a/confui/mesh_connector.h +++ b/confui/mesh_connector.h @@ -5,6 +5,8 @@ #include <string> #include <vector> +#include "../shared/protocol.h" + using std::array; using std::map; using std::size_t; @@ -15,22 +17,19 @@ using std::vector; typedef uint32_t cd_uid_t; /** @brief link/automation id type */ typedef uint32_t cd_link_t; -/** @brief node mac address type */ -typedef uint8_t cd_mac_addr_t[6]; - -/** @brief automation types/actions */ -enum cd_e_automation_type { - CD_AUTOMATION_TYPE_TOGGLE, /** @brief button toggles light */ - CD_AUTOMATION_TYPE_TURN_ON, /** @brief button always turns on light (regardless of previous state) */ - CD_AUTOMATION_TYPE_TURN_OFF, /** @brief button always turns off light (regardless of previous state) */ -}; + +typedef cd_e_cmd_link_type cd_e_automation_type; +#define CD_AUTOMATION_TYPE_TOGGLE CD_CMD_LINK_TYPE_TOGGLE +#define CD_AUTOMATION_TYPE_TURN_ON CD_CMD_LINK_TYPE_TURN_ON +#define CD_AUTOMATION_TYPE_TURN_OFF CD_CMD_LINK_TYPE_TURN_OFF /** @brief GUI node representation */ typedef struct { cd_uid_t id; /** @brief GUI-specific id (used as handle) */ cd_mac_addr_t address; /** @brief node bluetooth mac address */ + cd_uuid_t uuid; /** @brief node uuid */ size_t name_len; /** @brief name length in bytes */ - const char *name; /** @brief user-friendly node name */ + char *name; /** @brief user-friendly node name */ bool light_on; /** @brief state of light on node */ bool provisioned; /** @brief whether the node is provisioned into the network */ } cd_s_node; @@ -102,6 +101,8 @@ public: virtual cd_s_automation *get_link(cd_link_t id); /** @brief get node pointer by node id */ virtual cd_s_node *get_node(cd_uid_t id); + virtual cd_uid_t get_or_create_node_by_uuid(cd_uuid_t uuid); + virtual cd_link_t get_or_create_link_by_uuid(cd_uuid_t button, cd_uuid_t light); // network modification functions /** @brief create empty automation */ @@ -114,7 +115,7 @@ public: * @param light node id for node whose light will be used for this automation. * @param action action/automation type (toggle, on, off). */ - virtual cd_link_t create_link(cd_uid_t button, cd_uid_t light, enum cd_e_automation_type action); + virtual cd_link_t create_link(cd_uid_t button, cd_uid_t light, cd_e_automation_type action); /** * @brief overwrite link id with new automation and update on network. * @@ -123,14 +124,16 @@ public: * properties. * * @param automation pointer to automation struct (with new/modified values) + * @param publish `true` to send POST_LINK command */ - virtual void update_link(cd_s_automation *automation); + virtual void update_link(cd_s_automation *automation, bool publish = false); /** * @brief remove automation and update on network. * * @param link_handle automation id + * @param publish `true` to send POST_LINK command */ - virtual void remove_link(cd_link_t link_handle); + virtual void remove_link(cd_link_t link_handle, bool publish = false); /** * @brief overwrite node id with new node and update on network. @@ -139,8 +142,9 @@ public: * allocated using malloc()). used to update existing node properties. * * @param node_ptr pointer to node struct (with new/modified state) + * @param publish `true` to send POST_LED command */ - virtual void update_node(cd_s_node *node_ptr); + virtual void update_node(cd_s_node *node_ptr, bool publish = false); /** * @brief provision node into network * @@ -157,6 +161,9 @@ public: // conversion functions /** @brief convert `cd_mac_addr_t` to `std::string` for printing/GUI */ static string cd_mac_to_string(cd_mac_addr_t mac); + /** @brief convert `cd_uuid_t` to `std::string` for printing/GUI */ + static string cd_uuid_to_string(cd_uuid_t uuid); + }; /** @brief global pointer to mesh connector, initialized in CDMainWindow */ diff --git a/confui/serial.cpp b/confui/serial.cpp new file mode 100644 index 0000000..b3f1cd6 --- /dev/null +++ b/confui/serial.cpp @@ -0,0 +1,198 @@ +#include "serial.h" +#include "../shared/serial_parse.h" +#include "../shared/bin.h" +#include "../shared/pclient.h" +#include "mainwindow.h" +#include "mesh_connector.h" + +#include <iostream> +#include <QDebug> +#include <QSerialPort> +#include <QSerialPortInfo> + +CDSerialConnector::CDSerialConnector() { + this->_serial = new QSerialPort; + + if (!_serial->setBaudRate(QSerialPort::Baud115200)) qDebug() << _serial->errorString(); + if (!_serial->setDataBits(QSerialPort::Data8)) // data is 8 bits + qDebug() << _serial->errorString(); + if (!_serial->setParity(QSerialPort::NoParity)) qDebug() << _serial->errorString(); + if (!_serial->setFlowControl(QSerialPort::NoFlowControl)) // default: QSerialPort::NoFlowControl + qDebug() << _serial->errorString(); + if (!_serial->setStopBits(QSerialPort::OneStop)) // default + qDebug() << _serial->errorString(); +} + +CDSerialConnector::~CDSerialConnector() { delete this->_serial; } + +void CDSerialConnector::action() { + int bytes = _serial->bytesAvailable(); + if (bytes > 0) _msg = _serial->readAll(); + + string std_string = _msg.toStdString(); + size_t size = std_string.size(); + const char* data = std_string.c_str(); + for (size_t i = 0; i < size; i++) + cd_serial_parse(data[i]); +} + +void CDSerialConnector::write(QByteArray msg) { + if (-1 == _serial->write(msg)) + qDebug() << _serial->errorString(); +} + +void CDSerialConnector::connect(string port) { + _serial->setPortName(QString::fromStdString(port)); + + if (!_serial->open(QIODevice::ReadWrite)) + qDebug() << _serial->errorString(); + + QObject::connect(_serial, &QSerialPort::readyRead, [&] { action(); }); +} + +void CDSerialConnector::disconnect() { + if (_serial->isOpen() == false) return; + _serial->disconnect(); +} + +QByteArray CDSerialConnector::get_data() { return _msg; } + +vector<string> CDSerialConnector::get_ports() { + vector<string> ports; + for (QSerialPortInfo port : QSerialPortInfo::availablePorts()) + ports.push_back(port.portName().toStdString()); + return ports; +} + +string CDSerialConnector::get_port() { + return _serial->portName().toStdString(); +} + +extern "C" { + +void cd_pclient_send(cd_s_bin* data) { + QByteArray converted; + converted.append("\xff", 1); + for (size_t i = 0; i < data->bytes; i++) { + size_t byte = data->data[i]; + byte == 0xff ? converted.append("\xff\xff", 2) + : converted.append((char *) &byte, 1); + } + g_cd_serial->write(converted); +} + +// receive handlers (node only) +void cd_cmd_get_node(cd_s_bin* data) { (void) data; } +void cd_cmd_post_led(cd_s_bin* data) { (void) data; } +void cd_cmd_post_link(cd_s_bin* data) { (void) data; } +void cd_cmd_post_net(cd_s_bin* data) { (void) data; } + +void cd_cmd_ping(cd_s_bin* data) { + CD_CAST_BIN(cd_s_cmd_ping, data, cast); + + cd_bin_repl_ntoh16(&cast->id); // fix endianness + + std::cout << "ping request with id " << cast->id << " received!" << std::endl; + + cd_s_bin* response = cd_cmd_res_status((cd_e_scmds) cast->opcode, cast->id, false); + cd_pclient_send(response); + free(response); +} + +void cd_cmd_response_get_node_parse_node(cd_s_cmd_node* node) { + printf("yes i am node with name '%.*s'\n", node->name_len, node->remaining_data); + printf("my light is %s and i am%s provisioned\n", node->light_on ? "on" : "off", node->provisioned ? "" : " not"); + + // get node handle + cd_uid_t node_id = g_cd_mesh_connector->get_or_create_node_by_uuid(node->uuid); + cd_s_node* gui_node = g_cd_mesh_connector->get_node(node_id); + + // fill current node + memcpy(gui_node->address, node->address, sizeof(cd_mac_addr_t)); + memcpy(gui_node->uuid, node->uuid, sizeof(cd_uuid_t)); + gui_node->name_len = node->name_len; + if (gui_node->name != nullptr) free(gui_node->name); + char* name = (char*) malloc(node->name_len); + memcpy(name, node->remaining_data, node->name_len); + gui_node->name = name; + gui_node->light_on = !!node->light_on; + gui_node->provisioned = !!node->provisioned; + + cd_uuid_t* light_publish_addresses = (cd_uuid_t*) (&node->remaining_data[0] + node->name_len); + for (unsigned i = 0; i < node->link_count; i++) { + // find or create light node + cd_uid_t light_id = g_cd_mesh_connector->get_or_create_node_by_uuid(light_publish_addresses[i]); + cd_s_node* gui_light = g_cd_mesh_connector->get_node(light_id); + memcpy(gui_light->uuid, light_publish_addresses[i], sizeof(cd_uuid_t)); // fill at least uuid (if node is not yet known) + + // find or create automation handle + cd_link_t link_id = g_cd_mesh_connector->get_or_create_link_by_uuid(gui_light->uuid, light_publish_addresses[i]); + cd_s_automation* gui_link = g_cd_mesh_connector->get_link(link_id); + + // fill automation + gui_link->button = gui_node; + gui_link->light = gui_light; + gui_link->type = CD_AUTOMATION_TYPE_TOGGLE; //TODO: read from incoming data in future + gui_link->valid = true; + } + + g_cd_main_window->update(); +} + +void cd_cmd_response_get_node(cd_s_bin* data) { + CD_CAST_BIN(cd_s_cmd_response, data, response_cast); + cd_s_cmd_response_get_node* nodes = (cd_s_cmd_response_get_node*) &response_cast->response_info[0]; // yes + cd_bin_repl_ntoh16(&nodes->node_count); + cd_bin_repl_ntoh16(&nodes->remaining_size); + + std::cout << "get nodes response with id " << response_cast->response_id << " received!" << std::endl; + printf("counting %d node%s\n", nodes->node_count, nodes->node_count == 1 ? "" : "s"); + + cd_s_cmd_node* cursor = &nodes->nodes[0]; + for (unsigned int i = 0; i < nodes->node_count; i++) { + cd_bin_repl_ntoh16(&cursor->remaining_size); + cd_bin_repl_ntoh16(&cursor->link_count); + cd_bin_repl_ntoh32(&cursor->button_pub); + + cd_cmd_response_get_node_parse_node(cursor); + cursor += sizeof(cd_s_cmd_node) + cd_bin_ntoh16(cursor->remaining_size); + } +} + +void cd_cmd_response_ping(cd_s_bin* data) { + CD_CAST_BIN(cd_s_cmd_response, data, cast); + std::cout << "ping response with id " << cast->response_id << " received!" << std::endl; +} + +void cd_cmd_response_post_led(cd_s_bin* data) { + CD_CAST_BIN(cd_s_cmd_response, data, cast); + if (cast->error) printf("POST_LED response with error for msg id 0x%04x", cast->response_id); +} + +void cd_cmd_response_post_link(cd_s_bin* data) { + CD_CAST_BIN(cd_s_cmd_response, data, cast); + if (cast->error) printf("POST_LINK response with error for msg id 0x%04x", cast->response_id); +} + +void cd_cmd_response_post_net(cd_s_bin* data) { + CD_CAST_BIN(cd_s_cmd_response, data, cast); + if (cast->error) printf("POST_NET response with error for msg id 0x%04x", cast->response_id); +} + +void cd_cmd_response(cd_s_bin* data) { + CD_CAST_BIN(cd_s_cmd_response, data, cast); + + cd_bin_repl_ntoh16(&cast->id); + cd_bin_repl_ntoh16(&cast->response_id); + + switch (cast->response_type) { + case CD_CMD_PING: return cd_cmd_response_ping(data); + case CD_CMD_GET_NODE: return cd_cmd_response_get_node(data); + case CD_CMD_POST_LED: return cd_cmd_response_post_led(data); + case CD_CMD_POST_LINK: return cd_cmd_post_link(data); + case CD_CMD_POST_NET: return cd_cmd_response_post_net(data); + default: return; + } +} + +} diff --git a/confui/serial.h b/confui/serial.h new file mode 100644 index 0000000..aa0508f --- /dev/null +++ b/confui/serial.h @@ -0,0 +1,42 @@ +#pragma once + +#include <QSerialPort> +#include <QString> +#include <vector> + +using std::string; +using std::vector; + +/** @brief manage asynchronous serial connection */ +class CDSerialConnector { +public: + CDSerialConnector(); + virtual ~CDSerialConnector(); + + /** @brief get list of available serial ports */ + static vector<string> get_ports(); + + /** @brief open serial port */ + virtual void connect(string port); + /** @brief close serial port */ + virtual void disconnect(); + + /** @brief write to serial port */ + virtual void write(QByteArray msg); + /** @brief get last message */ + virtual QByteArray get_data(); + + /** @brief get current port name */ + virtual string get_port(); + +private: + /** @brief executed when new data is received */ + virtual void action(); + + QSerialPort *_serial = nullptr; + QByteArray _msg; +}; + +/** @brief global pointer to serial connector, initialized in CDMainWindow */ +extern CDSerialConnector *g_cd_serial; + diff --git a/confui/ui_automation.cpp b/confui/ui_automation.cpp index eb6e1f6..2ade734 100644 --- a/confui/ui_automation.cpp +++ b/confui/ui_automation.cpp @@ -74,11 +74,11 @@ void CDAutomationWidget::apply() { if (!conf_valid()) return; _automation->button = g_cd_mesh_connector->get_node(dropdown_button->findData(dropdown_button->currentIndex())); - _automation->type = (enum cd_e_automation_type)dropdown_action->findData(dropdown_action->currentIndex()); + _automation->type = (cd_e_automation_type) dropdown_action->findData(dropdown_action->currentIndex()); _automation->light = g_cd_mesh_connector->get_node(dropdown_light->findData(dropdown_light->currentIndex())); _automation->valid = true; - g_cd_mesh_connector->update_link(_automation); + g_cd_mesh_connector->update_link(_automation, true); } void CDAutomationWidget::remove() { diff --git a/confui/ui_node.cpp b/confui/ui_node.cpp index 552b62a..09daaaa 100644 --- a/confui/ui_node.cpp +++ b/confui/ui_node.cpp @@ -1,4 +1,5 @@ #include "ui_node.h" +#include "../shared/pclient.h" CDNodeWidget::~CDNodeWidget() {} CDNodeWidget::CDNodeWidget(QWidget *parent) : QWidget(parent) { @@ -43,6 +44,10 @@ void CDNodeWidget::update() { switch_on_off->setChecked(_node->light_on); button_add_remove->setText(_node->provisioned ? "Remove from network" : "Join network"); + + cd_s_bin* msg = cd_cmd_gen_get_node(false, this->_node->uuid); + cd_pclient_send(msg); + free(msg); } void CDNodeWidget::toggle_provision() { @@ -54,7 +59,7 @@ void CDNodeWidget::toggle_provision() { void CDNodeWidget::update_led(bool on) { _node->light_on = on; - g_cd_mesh_connector->update_node(_node); + g_cd_mesh_connector->update_node(_node, true); update(); } diff --git a/shared/bin.c b/shared/bin.c new file mode 100644 index 0000000..875d013 --- /dev/null +++ b/shared/bin.c @@ -0,0 +1,101 @@ +#include <stdlib.h> +#include <string.h> + +#include "bin.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define CD_ENDIAN_LITTLE (1) +#define CD_ENDIAN_BIG (0) + +#define _SHIFT_0B (8 * 0) +#define _SHIFT_1B (8 * 1) +#define _SHIFT_2B (8 * 2) +#define _SHIFT_3B (8 * 3) +#define _BYTE_0 ((uint32_t)0xff << (_SHIFT_0B)) +#define _BYTE_1 ((uint32_t)0xff << (_SHIFT_1B)) +#define _BYTE_2 ((uint32_t)0xff << (_SHIFT_2B)) +#define _BYTE_3 ((uint32_t)0xff << (_SHIFT_3B)) + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wshift-count-overflow" +cd_s_bin *cd_bin_from_uint8_t(uint8_t data) { + size_t size = 1; + cd_s_bin *ret = malloc(sizeof(cd_s_bin) + sizeof(uint8_t) * size); + ret->bytes = size; + ret->data[0] = data; + return ret; +} + +cd_s_bin *cd_bin_from_uint16_t(uint16_t data) { + size_t size = 2; + cd_s_bin *ret = malloc(sizeof(cd_s_bin) + sizeof(uint8_t) * size); + data = cd_bin_hton16(data); + ret->bytes = size; + ret->data[0] = (data & _BYTE_1) >> _SHIFT_1B; + ret->data[1] = (data & _BYTE_0) >> _SHIFT_0B; + return ret; +} + +cd_s_bin *cd_bin_from_uint32_t(uint32_t data) { + size_t size = 4; + cd_s_bin *ret = malloc(sizeof(cd_s_bin) + sizeof(uint8_t) * size); + data = cd_bin_hton32(data); + ret->bytes = size; + ret->data[0] = (data & _BYTE_3) >> _SHIFT_3B; + ret->data[1] = (data & _BYTE_2) >> _SHIFT_2B; + ret->data[2] = (data & _BYTE_1) >> _SHIFT_1B; + ret->data[3] = (data & _BYTE_0) >> _SHIFT_0B; + return ret; +} + +uint32_t cd_bin_hton32(uint32_t h32) { + if (g_cd_endianness == CD_ENDIAN_BIG) return h32; + return ((h32 & _BYTE_0) << _SHIFT_3B) | ((h32 & _BYTE_1) << _SHIFT_1B) | + ((h32 & _BYTE_2) >> _SHIFT_1B) | ((h32 & _BYTE_3) >> _SHIFT_3B); +} +#pragma GCC diagnostic pop + +uint16_t cd_bin_hton16(uint16_t h16) { + if (g_cd_endianness == CD_ENDIAN_BIG) return h16; + return ((h16 & _BYTE_0) << _SHIFT_1B) | ((h16 & _BYTE_1) >> _SHIFT_1B); +} + +uint32_t cd_bin_ntoh32(uint32_t n32) { return cd_bin_hton32(n32); } +uint16_t cd_bin_ntoh16(uint16_t n16) { return cd_bin_hton16(n16); } +uint32_t cd_bin_ntohd(uint8_t* n, size_t s) { return cd_bin_htond(n, s); } + +uint32_t cd_bin_htond(uint8_t* h, size_t s) { + if (s == sizeof(uint8_t)) return *h; + else if (s == sizeof(uint16_t)) return cd_bin_hton16(*(uint16_t*) h); + else if (s == sizeof(uint32_t)) return cd_bin_hton32(*(uint32_t*) h); + else return 0; +} + +cd_s_bin *cd_bin_s_alloc(uint16_t bytes, uint8_t *data) { + cd_s_bin *temp = malloc(sizeof(cd_s_bin) + sizeof(uint8_t) * bytes); + temp->bytes = bytes; + memcpy(&temp->data, data, bytes); + return temp; +} + +cd_s_bin *cd_bin_s_cat(cd_s_bin *a, cd_s_bin *b) { + uint8_t data[a->bytes + b->bytes]; + memcpy(data, a->data, a->bytes); + memcpy(data + a->bytes, b->data, b->bytes); + cd_s_bin *c = cd_bin_s_alloc(a->bytes + b->bytes, data); + free(a); + free(b); + return c; +} + +void cd_bin_repl_hton32(uint32_t *h32) { *h32 = cd_bin_hton32(*h32); } +void cd_bin_repl_hton16(uint16_t *h16) { *h16 = cd_bin_hton16(*h16); } +void cd_bin_repl_ntoh32(uint32_t *h32) { *h32 = cd_bin_ntoh32(*h32); } +void cd_bin_repl_ntoh16(uint16_t *h16) { *h16 = cd_bin_ntoh16(*h16); } + +#ifdef __cplusplus +} +#endif diff --git a/shared/bin.h b/shared/bin.h new file mode 100644 index 0000000..7506655 --- /dev/null +++ b/shared/bin.h @@ -0,0 +1,81 @@ +#pragma once + +/** @file bin.h */ + +/** + * helper file for binary data + * + * - fix endianness with functions inspired by UNIX arpa/inet.h + * - convert uint16_t and uint32_t to cd_s_bin + */ + +#include <stdint.h> +#include <malloc.h> + +#ifdef __cplusplus +extern "C" { +#endif + +extern uint8_t g_cd_endianness; + +/** @brief cast `in.data` to `type out` */ +#define CD_CAST_BIN(type, in, out) type *out = (type *)&in->data; +#define CD_CREATE_MSG_BIN(type, normal, bin) CD_CREATE_MSG_SIZE_BIN(type, sizeof(type), normal, bin) +/** @brief */ +#define CD_CREATE_MSG_SIZE_BIN(type, size, normal, bin) \ + cd_s_bin *bin = malloc(sizeof(cd_s_bin) + size); \ + bin->bytes = size; \ + type *normal = (type *)&bin->data; + +/** @brief hold binary data with fixed size */ +typedef struct { + uint16_t bytes; /** @brief data size */ + uint8_t data[]; /** @brief data */ +} cd_s_bin; + +/** @brief allocate new cd_s_bin struct and fill with `*data` for `bytes` bytes */ +cd_s_bin *cd_bin_s_alloc(uint16_t bytes, uint8_t *data); +/** @brief concatenate 2 cd_s_bin structs, deallocates `a` and `b` */ +cd_s_bin *cd_bin_s_cat(cd_s_bin *a, cd_s_bin *b); + +cd_s_bin *cd_bin_from_uint8_t(uint8_t data); +cd_s_bin *cd_bin_from_uint16_t(uint16_t data); +cd_s_bin *cd_bin_from_uint32_t(uint32_t data); + +/** @brief convert 32-bit value from host endian to network (big-endian) */ +uint32_t cd_bin_hton32(uint32_t h32); +/** @brief convert 16-bit value from host endian to network (big-endian) */ +uint16_t cd_bin_hton16(uint16_t h16); +/** @brief convert 32-bit value from network (big-endian) to host endian */ +uint32_t cd_bin_ntoh32(uint32_t n32); +/** @brief convert 16-bit value from network (big-endian) to host endian */ +uint16_t cd_bin_ntoh16(uint16_t n16); + +/** + * @brief convert (8*s)-bit value from network (big-endian) to host endian + * (dynamic size) + * + * @param n pointer to number + * @param s size of number in bytes + * + * @return 32-bit integer regardless of `s` + * + * this function is exclusively used by the CD_DYN_MEMBER_SIZEOF macro in + * shared/protocol.c + */ +uint32_t cd_bin_ntohd(uint8_t* n, size_t s); +uint32_t cd_bin_htond(uint8_t* h, size_t s); + +/** @brief replace 32-bit value from host endian to network (big-endian) */ +void cd_bin_repl_hton32(uint32_t *h32); +/** @brief replace 16-bit value from host endian to network (big-endian) */ +void cd_bin_repl_hton16(uint16_t *h16); +/** @brief replace 32-bit value from network (big-endian) to host endian */ +void cd_bin_repl_ntoh32(uint32_t *n32); +/** @brief replace 16-bit value from network (big-endian) to host endian */ +void cd_bin_repl_ntoh16(uint16_t *n16); + +#ifdef __cplusplus +} +#endif + diff --git a/shared/consts.h b/shared/consts.h index 6f70f09..57426bf 100644 --- a/shared/consts.h +++ b/shared/consts.h @@ -1 +1,11 @@ #pragma once + +/** @file consts.h */ + +/** @brief size of input (receive) buffer (in bytes) */ +#define CD_SERIAL_READ_BUFFER_SIZE 255 +/** @brief size of the error handling buffer (in errors, not bytes) */ +#define CD_ERROR_BUFFER_SIZE 16 +/** @brief size of the serial communication buffer (in messages, not bytes) */ +#define CD_SERCOMM_BUFFER_SIZE 16 + diff --git a/shared/pclient.c b/shared/pclient.c new file mode 100644 index 0000000..1b8e4e5 --- /dev/null +++ b/shared/pclient.c @@ -0,0 +1,164 @@ +#include <memory.h> + +#include "protocol.h" +#include "pclient.h" +#include "bin.h" + +#ifdef __cplusplus +extern "C" { +#endif + +cd_s_bin* cd_cmd_gen_ping() { + CD_CREATE_MSG_BIN(cd_s_cmd_ping, msg, bin); + + msg->opcode = CD_CMD_PING; + msg->id = cd_bin_hton16(cd_protocol_fresh_message_id()); + + return bin; +} + +cd_s_bin* cd_cmd_gen_get_node(bool all, cd_uuid_t uuid) { + CD_CREATE_MSG_BIN(cd_s_cmd_get_node, msg, bin); + + msg->opcode = CD_CMD_GET_NODE; + msg->id = cd_bin_hton16(cd_protocol_fresh_message_id()); + msg->all = all; + if (uuid != NULL) memcpy(msg->uuid, uuid, sizeof(cd_uuid_t)); + else memset(msg->uuid, 0, sizeof(cd_uuid_t)); + + return bin; +} + +cd_s_bin* cd_cmd_gen_post_led(bool on, cd_uuid_t uuid) { + CD_CREATE_MSG_BIN(cd_s_cmd_post_led, msg, bin); + + msg->opcode = CD_CMD_POST_LED; + msg->id = cd_bin_hton16(cd_protocol_fresh_message_id()); + msg->on = on; + memcpy(msg->uuid, uuid, sizeof(cd_uuid_t)); + + return bin; +} + +cd_s_bin* cd_cmd_gen_post_link_add(cd_uuid_t button, cd_uuid_t light, cd_e_cmd_link_type type) { + CD_CREATE_MSG_BIN(cd_s_cmd_post_link, msg, bin); + + msg->opcode = CD_CMD_POST_LINK; + msg->id = cd_bin_hton16(cd_protocol_fresh_message_id()); + msg->add = true; + memcpy(msg->button, button, sizeof(cd_uuid_t)); + memcpy(msg->led, light, sizeof(cd_uuid_t)); + msg->type = type; + + return bin; +} + +cd_s_bin* cd_cmd_gen_post_link_rm(cd_uuid_t button, cd_uuid_t light) { + CD_CREATE_MSG_BIN(cd_s_cmd_post_link, msg, bin); + + msg->opcode = CD_CMD_POST_LINK; + msg->id = cd_bin_hton16(cd_protocol_fresh_message_id()); + msg->add = false; + memcpy(msg->button, button, sizeof(cd_uuid_t)); + memcpy(msg->led, light, sizeof(cd_uuid_t)); + msg->type = 0; + + return bin; +} + +cd_s_bin* cd_cmd_gen_post_net_add(cd_uuid_t uuid) { + CD_CREATE_MSG_BIN(cd_s_cmd_post_net, msg, bin); + + msg->opcode = CD_CMD_POST_NET; + msg->id = cd_bin_hton16(cd_protocol_fresh_message_id()); + msg->join = true; + memcpy(msg->uuid, uuid, sizeof(cd_uuid_t)); + + return bin; +} + +cd_s_bin* cd_cmd_gen_post_net_rm(cd_uuid_t uuid) { + CD_CREATE_MSG_BIN(cd_s_cmd_post_net, msg, bin); + + msg->opcode = CD_CMD_POST_NET; + msg->id = cd_bin_hton16(cd_protocol_fresh_message_id()); + msg->join = false; + memcpy(msg->uuid, uuid, sizeof(cd_uuid_t)); + + return bin; +} + +cd_s_bin* cd_cmd_res_status(cd_e_scmds cmd, cd_cmd_id_t id, bool error) { + CD_CREATE_MSG_BIN(cd_s_cmd_response, msg, bin); + + msg->opcode = CD_CMD_RESPONSE; + msg->id = cd_bin_hton16(cd_protocol_fresh_message_id()); + msg->response_type = cmd; + msg->response_id = id; + msg->error = error; + msg->response_size = 0; + + return bin; +} + +cd_s_bin* cd_cmd_res(cd_e_scmds cmd, cd_cmd_id_t id, uint16_t len, uint8_t* data) { + CD_CREATE_MSG_SIZE_BIN(cd_s_cmd_response, sizeof(cd_s_cmd_response) + len, msg, bin); + + msg->opcode = CD_CMD_RESPONSE; + msg->id = cd_bin_hton16(cd_protocol_fresh_message_id()); + msg->response_type = cmd; + msg->response_id = id; + msg->error = false; + msg->response_size = cd_bin_hton16(len); + memcpy(msg->response_info, data, len); + + return bin; +} + +cd_s_cmd_node* cd_cmd_node_alloc(const char* name, cd_s_cmd_node base, uint16_t link_count, cd_uuid_t* links) { + size_t name_len = strlen(name); + size_t links_size = sizeof(cd_uuid_t) * link_count; + size_t remaining_size = sizeof(char) * name_len + links_size; + cd_s_cmd_node* node = malloc(sizeof(cd_s_cmd_node) + remaining_size); + + memcpy(node->uuid, base.uuid, sizeof(cd_uuid_t)); + memcpy(node->address, base.address, sizeof(cd_mac_addr_t)); + node->name_len = name_len; + node->light_on = base.light_on; + node->provisioned = base.provisioned; + node->button_pub = cd_bin_hton32(base.button_pub); + node->link_count = cd_bin_hton16(link_count); + node->remaining_size = cd_bin_hton16(remaining_size); + void* cursor = (void*) &node->remaining_data[0]; + memcpy(cursor, name, name_len); // copy name + cursor += name_len; + memcpy(cursor, links, links_size); // copy links + + return node; +} + +cd_s_cmd_response_get_node* cd_cmd_get_node_res_from_node_arr(uint16_t size, cd_s_cmd_node* arr[]) { + size_t remaining_size = 0; + + for (unsigned int i = 0; i < size; i++) { + remaining_size += sizeof(cd_s_cmd_node) + cd_bin_ntoh16(arr[i]->remaining_size); + } + + cd_s_cmd_response_get_node* response = malloc(sizeof(cd_s_cmd_response_get_node) + remaining_size); + response->node_count = cd_bin_hton16(size); + response->remaining_size = cd_bin_hton16(remaining_size); + + void* cursor = response->nodes; + for (unsigned int i = 0; i < size; i++) { + size_t size = sizeof(cd_s_cmd_node) + cd_bin_ntoh16(arr[i]->remaining_size); + memcpy(cursor, arr[i], size); + cursor += size; + } + + return response; +} + +#ifdef __cplusplus +} +#endif + diff --git a/shared/pclient.h b/shared/pclient.h new file mode 100644 index 0000000..03a8a25 --- /dev/null +++ b/shared/pclient.h @@ -0,0 +1,91 @@ +#pragma once + +/** @file pclient.h */ + +#include <stdbool.h> + +#include "protocol.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** @brief send data over platform standard serial out (doesn't free `data`) */ +void cd_pclient_send(cd_s_bin* data); + +/** @brief generate PING command */ +cd_s_bin* cd_cmd_gen_ping(); +/** + * @brief generate GET_NODE command + * @param all get all nodes + * @param uuid get specific node by uuid + */ +cd_s_bin* cd_cmd_gen_get_node(bool all, cd_uuid_t uuid); +/** + * @brief generate POST_LED command + * @param on light status + * @param uuid node to set light of + */ +cd_s_bin* cd_cmd_gen_post_led(bool on, cd_uuid_t uuid); +/** + * @brief generate POST_LINK command to add or update link + * @param button button node uuid + * @param light light node uuid + * @param type type of link to set + */ +cd_s_bin* cd_cmd_gen_post_link_add(cd_uuid_t button, cd_uuid_t light, cd_e_cmd_link_type type); +/** + * @brief generate POST_LINK command to remove link + * @param button button node uuid + * @param light light node uuid + */ +cd_s_bin* cd_cmd_gen_post_link_rm(cd_uuid_t button, cd_uuid_t light); +/** + * @brief generate POST_NET command to provision node into network + * @param uuid node uuid + */ +cd_s_bin* cd_cmd_gen_post_net_add(cd_uuid_t uuid); +/** + * @brief generate POST_NET command to provision node out of network + * @param uuid node uuid + */ +cd_s_bin* cd_cmd_gen_post_net_rm(cd_uuid_t uuid); + +/** + * @brief generate generic RESPONSE command with error field and no response_info + * @param cmd original command opcode + * @param id original command id + * @param error `true` if some error occurred + */ +cd_s_bin* cd_cmd_res_status(cd_e_scmds cmd, cd_cmd_id_t id, bool error); +/** + * @brief generate RESPONSE command with response_info + * @param cmd original command opcode + * @param id original command id + * @param len size of `data` in bytes + * @param data pointer to data + */ +cd_s_bin* cd_cmd_res(cd_e_scmds cmd, cd_cmd_id_t id, uint16_t len, uint8_t* data); +/** + * @brief generate cd_s_cmd_response_get_node struct from array of cd_s_cmd_node pointers + * @param size length of array + * @param arr array of pointer to cd_s_cmd_node + */ +cd_s_cmd_response_get_node* cd_cmd_get_node_res_from_node_arr(uint16_t size, cd_s_cmd_node* arr[]); +/** + * @brief allocate and fill cd_s_cmd_node struct + * + * @param base base struct with values that can be initialized using an initialization list + * @param name node name (length is calculated at runtime using strlen()) + * @param link_count amount of lights this node controls + * @param links array of light node uuids + */ +cd_s_cmd_node* cd_cmd_node_alloc(const char* name, cd_s_cmd_node base, uint16_t link_count, cd_uuid_t* links); + +#define cd_remaining_sizeof(type, input_struct) ((sizeof(type) + cd_bin_ntoh16(input_struct->remaining_size)) /* NOLINT */) +#define cd_cmd_node_sizeof(node) (cd_remaining_sizeof(cd_s_cmd_node, node) /* NOLINT */) +#define cd_cmd_response_get_node_sizeof(res) (cd_remaining_sizeof(cd_s_cmd_response_get_node, res) /* NOLINT */) + +#ifdef __cplusplus +} +#endif diff --git a/shared/protocol-tests/.gitignore b/shared/protocol-tests/.gitignore new file mode 100644 index 0000000..a8a0dce --- /dev/null +++ b/shared/protocol-tests/.gitignore @@ -0,0 +1 @@ +*.bin diff --git a/shared/protocol-tests/get-node-response.src b/shared/protocol-tests/get-node-response.src new file mode 100644 index 0000000..31bf1ff --- /dev/null +++ b/shared/protocol-tests/get-node-response.src @@ -0,0 +1,81 @@ +00: ff ; start byte +01: 05 ; opcode (0x05 = response) +02: 00 ; message id (0x0000) +03: 00 ; ^ +04: 00 ; error byte (0x00 = false) +05: 01 ; response type (0x01 = get node) +06: 8f ; original message id (0x8ff8) +07: f8 ; ^ +08: 00 ; remaining response size (0x39 = 57 bytes) +09: 39 ; ^ +-------; |- remaining response (cd_s_cmd_response_get_node) +0a: 00 ; | node count (0x0001 = 1) +0b: 01 ; | ^ +0c: 00 ; | remaining response size (0x35 = 53 bytes) +0d: 35 ; | ^ +-------; | |- remaining response (cd_s_cmd_node[1]) +0e: ff ; | | [esc] +0f: ff ; | | uuid (ffffffff-0000-0000-dead-beef00000000) +10: ff ; | | [esc] +11: ff ; | | ^ +12: ff ; | | [esc] +13: ff ; | | ^ +14: ff ; | | [esc] +15: ff ; | | ^ +16: 00 ; | | ^ +17: 00 ; | | ^ +18: 00 ; | | ^ +19: 00 ; | | ^ +1a: de ; | | ^ +1b: ad ; | | ^ +1c: be ; | | ^ +1d: ef ; | | ^ +1e: 00 ; | | ^ +1f: 00 ; | | ^ +20: 00 ; | | ^ +21: 00 ; | | ^ +22: ff ; | | [esc] +23: ff ; | | mac address (ff:00:ff:00:ff:00) +24: 00 ; | | ^ +25: ff ; | | [esc] +26: ff ; | | ^ +27: 00 ; | | ^ +28: ff ; | | [esc] +29: ff ; | | ^ +2a: 00 ; | | ^ +2b: 04 ; | | name length (0x04 = 4) +2c: 00 ; | | light on (0x00 = false) +2d: 00 ; | | provisioned (0x00 = false) +2e: de ; | | button publish address (0xdeadbeef) +2f: ad ; | | ^ +30: be ; | | ^ +31: ef ; | | ^ +32: 00 ; | | link count (0x0001 = 1) +33: 01 ; | | ^ +34: 00 ; | | remaining size (0x14 = 20 bytes) +35: 14 ; | | ^ +-------; | | |- remaining response (char[4], cd_uuid_t[1]) +36: 67 ; | | | node name ("gert") +37: 65 ; | | | ^ +38: 72 ; | | | ^ +39: 74 ; | | | ^ +3a: ff ; | | | [esc] +3b: ff ; | | | link[0] uuid (ffffffff-0000-0000-dead-beef00000000) +3c: ff ; | | | [esc] +3d: ff ; | | | ^ +3e: ff ; | | | [esc] +3f: ff ; | | | ^ +40: ff ; | | | [esc] +41: ff ; | | | ^ +42: 00 ; | | | ^ +43: 00 ; | | | ^ +44: 00 ; | | | ^ +45: 00 ; | | | ^ +46: de ; | | | ^ +47: ad ; | | | ^ +48: be ; | | | ^ +49: ef ; | | | ^ +4a: 00 ; | | | ^ +4b: 00 ; | | | ^ +4c: 00 ; | | | ^ +4d: 00 ; | | | ^ diff --git a/shared/protocol-tests/makefile b/shared/protocol-tests/makefile new file mode 100644 index 0000000..f8ac490 --- /dev/null +++ b/shared/protocol-tests/makefile @@ -0,0 +1,15 @@ +XXD := xxd +RM := rm -f + +SRCS := $(wildcard *.src) +TARGET := $(SRCS:.src=.bin) + +.PHONY: all clean + +all: $(TARGET) + +%.bin: %.src + $(XXD) -r -c1 $< $@ + +clean: + $(RM) $(TARGET) diff --git a/shared/protocol-tests/ping-response.src b/shared/protocol-tests/ping-response.src new file mode 100644 index 0000000..57418d5 --- /dev/null +++ b/shared/protocol-tests/ping-response.src @@ -0,0 +1,10 @@ +00: ff ; start byte +01: 05 ; response opcode +02: 00 ; message id (0x0001) +03: 01 ; ^ +04: 01 ; error (true) +05: 00 ; response type (0x00 = ping) +06: 00 ; original message id (0x0000) +07: 00 ; ^ +08: 00 ; remainder size (0x0000) +09: 00 ; ^ diff --git a/shared/protocol-tests/ping.src b/shared/protocol-tests/ping.src new file mode 100644 index 0000000..beec10a --- /dev/null +++ b/shared/protocol-tests/ping.src @@ -0,0 +1,4 @@ +00: ff ; start byte +01: 00 ; ping opcode +02: f8 ; message id (used as ping identifier) +03: 8f ; ^ diff --git a/shared/protocol.c b/shared/protocol.c new file mode 100644 index 0000000..fcc0f41 --- /dev/null +++ b/shared/protocol.c @@ -0,0 +1,58 @@ +#include <stddef.h> + +#include "protocol.h" + +#ifdef __cplusplus +extern "C" { +#endif + +cd_cmd_id_t g_cd_protocol_fresh_message_id = 0; + +size_t cd_cmd_sizeof(uint8_t data[CD_SERIAL_READ_BUFFER_SIZE], uint8_t data_length) { + cd_cmd_opcode_t opcode = data[0]; + if (CD_CMD_HANDLERS_SIZE[opcode] > 0) return CD_CMD_HANDLERS_SIZE[opcode]; + + cd_s_bin *copy = cd_bin_s_alloc(data_length, data); + size_t length = (*CD_CMD_HANDLERS_SIZEOF[opcode])(copy); + + free(copy); + + return length; +} + +/** + * @brief macro to calculate size of message based on struct with member to + * indicate length of dynamic (last) field + * + * @param data cd_s_bin pointer to currently received data + * @param struct_t message struct + * @param length_field struct field with dynamic length + * + * @return size_t with calculated size + * + * equivalent c code: + * + * size_t size = sizeof(struct_t); + * size_t dyn_member_offset = offsetof(struct_t, length_field); + * size_t dyn_member_size = sizeof(((struct_t*)0)->length_field); + * if (data->bytes >= (dyn_member_offset + dyn_member_size)) + * size += cd_bin_ntohd(&data->data[dyn_member_offset], dyn_member_size); + * return size; + */ +#define CD_DYN_MEMBER_SIZEOF(data, struct_t, length_field) \ + sizeof(struct_t) + ( \ + (data->bytes >= (offsetof(struct_t, length_field) + sizeof(((struct_t*)0)->length_field))) ? \ + (cd_bin_ntohd(&data->data[offsetof(struct_t, length_field)], sizeof(((struct_t*)0)->length_field))) :\ + 0); + +size_t cd_cmd_response_sizeof(cd_s_bin* data) { + return CD_DYN_MEMBER_SIZEOF(data, cd_s_cmd_response, response_size); +} + +cd_cmd_id_t cd_protocol_fresh_message_id() { + return g_cd_protocol_fresh_message_id++; +} + +#ifdef __cplusplus +} +#endif diff --git a/shared/protocol.h b/shared/protocol.h new file mode 100644 index 0000000..b9a2c93 --- /dev/null +++ b/shared/protocol.h @@ -0,0 +1,183 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +/** @file protocol.h */ + +#define CD_SERIAL_START_BYTE 0xff + +#include <stdint.h> +#include <stdlib.h> + +#include "bin.h" +#include "consts.h" + +/** @brief mac address (00:11:22:33:44:55) */ +typedef uint8_t cd_mac_addr_t[6]; +/** @brief uuid (ffeeddcc-bbaa-9988-7766-554433221100) */ +typedef uint8_t cd_uuid_t[16]; + +/** @brief pub/sub address type */ +typedef uint32_t cd_mesh_psub_addr; + +/** @brief command opcode (identifies message type) */ +typedef uint8_t cd_cmd_opcode_t; +/** @brief command id (identifies messages uniquely) */ +typedef uint16_t cd_cmd_id_t; +/** @brief smallest boolean type */ +typedef uint8_t cd_cmd_bool_t; + +/** @brief cmd handler function signature */ +typedef void (cd_cmd_handler_t)(cd_s_bin *data); + +/** @brief used for numbering messages */ +extern cd_cmd_id_t g_cd_protocol_fresh_message_id; + +/** @brief get new message id */ +cd_cmd_id_t cd_protocol_fresh_message_id(); + +#pragma pack(push, 1) + +typedef struct { + cd_cmd_opcode_t opcode; /** @brief cmd opcode */ + cd_cmd_id_t id; /** @brief message id */ +} cd_s_cmd_ping; + +typedef struct { + cd_cmd_opcode_t opcode; /** @brief cmd opcode */ + cd_cmd_id_t id; /** @brief message id */ + cd_cmd_bool_t all; /** @brief get all known nodes */ + cd_uuid_t uuid; /** @brief node uuid to get details from, ignored if `all` = `true` */ +} cd_s_cmd_get_node; + +typedef struct { + cd_cmd_opcode_t opcode; /** @brief cmd opcode */ + cd_cmd_id_t id; /** @brief message id */ + cd_cmd_bool_t on; /** @brief new led status */ + cd_uuid_t uuid; /** @brief node uuid to set */ +} cd_s_cmd_post_led; + +typedef enum { + CD_CMD_LINK_TYPE_TOGGLE = 0x00, /** @brief button toggles light */ + CD_CMD_LINK_TYPE_TURN_ON = 0x01, /** @brief button always turns on light (regardless of previous state) */ + CD_CMD_LINK_TYPE_TURN_OFF = 0x02, /** @brief button always turns off light (regardless of previous state) */ +} cd_e_cmd_link_type; + +typedef struct { + cd_cmd_opcode_t opcode; /** @brief cmd opcode */ + cd_cmd_id_t id; /** @brief message id */ + cd_uuid_t button; /** @brief uuid of button node */ + cd_uuid_t led; /** @brief uuid of led node */ + cd_cmd_bool_t add; /** @brief `true` to create/overwrite link, `false` to remove link */ + uint8_t type; /** @brief link type cd_e_cmd_link_type */ +} cd_s_cmd_post_link; + +typedef struct { + cd_cmd_opcode_t opcode; /** @brief cmd opcode */ + cd_cmd_id_t id; /** @brief message id */ + cd_uuid_t uuid; /** @brief node uuid */ + cd_cmd_bool_t join; /** @brief `true` to join network, `false` to leave network */ +} cd_s_cmd_post_net; + +/** @brief protocol node */ +typedef struct { + cd_uuid_t uuid; /** @brief node network uuid */ + cd_mac_addr_t address; /** @brief node bluetooth mac address */ + uint8_t name_len; /** @brief name length in bytes */ + cd_cmd_bool_t light_on; /** @brief state of light on node */ + cd_cmd_bool_t provisioned; /** @brief whether the node is provisioned into the network */ + cd_mesh_psub_addr button_pub; /** @brief button publish address */ + uint16_t link_count; /** @brief amount of addresses to publish button press to */ + uint16_t remaining_size; /** @brief calculated size of remaining_data for convenience */ + const uint8_t remaining_data[]; /** + * @brief remaining data (name and link array) + * + * this data is stored adjacently in memory + * and is cast when reading/writing this + * struct + * + * 1. char[] name + * 2. cd_uuid_t[] light_publish_addresses; + */ +} cd_s_cmd_node; + +typedef struct { + uint16_t node_count; /** amount of nodes in nodes[] */ + uint16_t remaining_size; /** remaining size (for convenience) */ + cd_s_cmd_node nodes[]; /** nodes adjacent in memory (should be accessed using pointer arithmetic) */ +} cd_s_cmd_response_get_node; + +typedef struct { + cd_cmd_opcode_t opcode; /** @brief cmd opcode */ + cd_cmd_id_t id; /** @brief response message id */ + cd_cmd_bool_t error; /** @brief `true` if some error occurred */ + cd_cmd_opcode_t response_type; /** @brief response type, used to cast type of `response_info` */ + cd_cmd_id_t response_id; /** @brief original message id */ + uint16_t response_size; /** @brief size of remaining response */ + uint8_t response_info[]; /** @brief (CAST) remaining response struct, not read if `response_size`=`0` */ +} cd_s_cmd_response; + +#pragma pack(pop) + +/** @brief global handler for complete messages */ +void cd_cmd_handle(uint8_t data[CD_SERIAL_READ_BUFFER_SIZE], uint8_t length); +/** @brief calculate message length for any message */ +size_t cd_cmd_sizeof(uint8_t data[CD_SERIAL_READ_BUFFER_SIZE], uint8_t length); + +/** @brief calculate dynamic size of response message */ +size_t cd_cmd_response_sizeof(cd_s_bin* data); + +// down here is the garbage manual duplicate code section for constants + +#define CD_CMD_COUNT 6 +typedef enum { + CD_CMD_PING = 0x00, /** @brief send ping */ + CD_CMD_GET_NODE = 0x01, /** @brief get node(s) */ + CD_CMD_POST_LED = 0x02, /** @brief set led */ + CD_CMD_POST_LINK = 0x03, /** @brief set/remove link */ + CD_CMD_POST_NET = 0x04, /** @brief (un)register node with network */ + CD_CMD_RESPONSE = 0x05, /** @brief response message */ +} cd_e_scmds; + +cd_cmd_handler_t cd_cmd_ping, + cd_cmd_get_node, + cd_cmd_post_led, + cd_cmd_post_link, + cd_cmd_post_net, + cd_cmd_response; + +/** @brief constant message sizes, 0 for dynamic size */ +static const size_t CD_CMD_HANDLERS_SIZE[CD_CMD_COUNT] = { + [CD_CMD_PING] = sizeof(cd_s_cmd_ping), + [CD_CMD_GET_NODE] = sizeof(cd_s_cmd_get_node), + [CD_CMD_POST_LED] = sizeof(cd_s_cmd_post_led), + [CD_CMD_POST_LINK] = sizeof(cd_s_cmd_post_link), + [CD_CMD_POST_NET] = sizeof(cd_s_cmd_post_net), + [CD_CMD_RESPONSE] = 0, +}; + +/** @brief constant message sizes, 0 for dynamic size */ +static size_t (* const CD_CMD_HANDLERS_SIZEOF[CD_CMD_COUNT])(cd_s_bin*) = { + [CD_CMD_PING] = NULL, + [CD_CMD_GET_NODE] = NULL, + [CD_CMD_POST_LED] = NULL, + [CD_CMD_POST_LINK] = NULL, + [CD_CMD_POST_NET] = NULL, + [CD_CMD_RESPONSE] = &cd_cmd_response_sizeof, +}; + +/** @brief stores message handlers in array with opcode as index */ +static cd_cmd_handler_t* const CD_CMD_HANDLERS[CD_CMD_COUNT] = { + [CD_CMD_PING] = &cd_cmd_ping, + [CD_CMD_GET_NODE] = &cd_cmd_get_node, + [CD_CMD_POST_LED] = &cd_cmd_post_led, + [CD_CMD_POST_LINK] = &cd_cmd_post_link, + [CD_CMD_POST_NET] = &cd_cmd_post_net, + [CD_CMD_RESPONSE] = &cd_cmd_response, +}; + +#ifdef __cplusplus +} +#endif diff --git a/shared/protocol.md b/shared/protocol.md new file mode 100644 index 0000000..c3c229f --- /dev/null +++ b/shared/protocol.md @@ -0,0 +1,15 @@ +# protocol specs + +## commands + +each command consists of a start byte, opcode, and a payload. each opcode +defines logic to handle payload length, so certain commands might expect a +fixed-length payload, a variable-length payload, or none at all. the start byte +is `0xff`, and because most data sent is in binary format, if the data contains +an `0xff` byte, it will be escaped by replacing it with two `0xff` bytes. this +is converted to a single `0xff` on the receiving end, so these duplicated bytes +and the starting byte don't count towards message length. + +opcodes are picked sequentially, and are stored as enum constants inside +shared/protocol.h for code readability. + diff --git a/shared/serial_parse.c b/shared/serial_parse.c new file mode 100644 index 0000000..bfc374a --- /dev/null +++ b/shared/serial_parse.c @@ -0,0 +1,61 @@ +#include <string.h> + +#include "consts.h" +#include "serial_parse.h" + +#ifdef __cplusplus +extern "C" { +#endif + +bool cd_serial_parse(uint8_t byte) { + static uint8_t current_message[CD_SERIAL_READ_BUFFER_SIZE] = {0}; + static uint8_t current_message_index = 0; + static uint8_t complete_message_length = 2; + + static bool attentive = false; + static bool listening = false; + + if (byte == CD_SERIAL_START_BYTE) { + attentive = !attentive; + if (attentive && listening) return CD_SERIAL_READ_SUCCESS; + } else if (attentive) { + attentive = false; + listening = !listening; + if (!listening) return CD_SERIAL_READ_FAILURE; + } + + if (!listening) return CD_SERIAL_READ_SUCCESS; + current_message[current_message_index++] = byte; + + complete_message_length = cd_cmd_sizeof(current_message, current_message_index); + + if (current_message_index == complete_message_length) { + cd_cmd_handle(current_message, current_message_index); + + memset(¤t_message, 0, CD_SERIAL_READ_BUFFER_SIZE); + current_message_index = 0; + complete_message_length = 1; + attentive = false; + listening = false; + return CD_SERIAL_READ_SUCCESS; + } + + return CD_SERIAL_READ_SUCCESS; +} + +void cd_cmd_handle(uint8_t data[CD_SERIAL_READ_BUFFER_SIZE], uint8_t data_length) { + cd_s_bin *copy = cd_bin_s_alloc(data_length, data); + + if (data[0] >= CD_CMD_COUNT) return; + cd_cmd_handler_t* handler = CD_CMD_HANDLERS[data[0]]; + + if (handler == NULL) return; + (*handler)(copy); + + free(copy); +} + +#ifdef __cplusplus +} +#endif + diff --git a/shared/serial_parse.h b/shared/serial_parse.h new file mode 100644 index 0000000..1e9c404 --- /dev/null +++ b/shared/serial_parse.h @@ -0,0 +1,26 @@ +#pragma once + +/** @file serial_parse.h */ + +#include <stdint.h> +#include <stdbool.h> + +#include "protocol.h" + +#define CD_SERIAL_READ_SUCCESS true +#define CD_SERIAL_READ_FAILURE false + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * parse serial data byte by byte + * @return true if read success, false if read fails + */ +bool cd_serial_parse(uint8_t byte); + +#ifdef __cplusplus +} +#endif + |