aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorlonkaars <loek@pipeframe.xyz>2023-01-07 14:43:03 +0100
committerlonkaars <loek@pipeframe.xyz>2023-01-07 14:43:03 +0100
commitadb70d5ee1987cfb1680114d2db9786923aad1f1 (patch)
treea15de82808f40153cd285994cab7534ed9a10538
parent46ede2c109fe2aa390936ec4f0a30321fb7b5d86 (diff)
parentb083c8e08d610a4e74f39f66f3daa191301893b1 (diff)
Merge branch 'qt-gui-meshconnector-serial' into dev
-rw-r--r--confui/confui.pro46
-rw-r--r--confui/main.cpp18
-rw-r--r--confui/main.h7
-rw-r--r--confui/mainwindow.cpp45
-rw-r--r--confui/mainwindow.h7
-rw-r--r--confui/makefile9
-rw-r--r--confui/mesh_connector.cpp112
-rw-r--r--confui/mesh_connector.h35
-rw-r--r--confui/serial.cpp198
-rw-r--r--confui/serial.h42
-rw-r--r--confui/ui_automation.cpp4
-rw-r--r--confui/ui_node.cpp7
-rw-r--r--shared/bin.c101
-rw-r--r--shared/bin.h81
-rw-r--r--shared/consts.h10
-rw-r--r--shared/pclient.c164
-rw-r--r--shared/pclient.h91
-rw-r--r--shared/protocol-tests/.gitignore1
-rw-r--r--shared/protocol-tests/get-node-response.src81
-rw-r--r--shared/protocol-tests/makefile15
-rw-r--r--shared/protocol-tests/ping-response.src10
-rw-r--r--shared/protocol-tests/ping.src4
-rw-r--r--shared/protocol.c58
-rw-r--r--shared/protocol.h183
-rw-r--r--shared/protocol.md15
-rw-r--r--shared/serial_parse.c61
-rw-r--r--shared/serial_parse.h26
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(&current_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
+