aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLoek Le Blansch <loek@pipeframe.xyz>2024-05-25 18:30:13 +0200
committerLoek Le Blansch <loek@pipeframe.xyz>2024-05-25 18:30:13 +0200
commit4525f60f29359b7ba88e47880d79fb9869913656 (patch)
treed6fa4be85ddfcee9861304eee53660bff7487d26
parent4fc192eb9ba949276c47c1bbd86164d955d3548c (diff)
parent5d5b186a5a82b7e2415eddd77ef93af851034a5b (diff)
Merge branch 'wip/main-controller' into wip/i2c-communication
-rw-r--r--client/.gitignore1
-rw-r--r--client/CMakeLists.txt9
-rw-r--r--client/cmd.cpp63
-rw-r--r--client/cmd.h60
-rw-r--r--client/examples/puzbus-hello-world.cpp67
-rw-r--r--client/main.cpp72
-rw-r--r--client/parse.cpp117
-rw-r--r--client/parse.h42
l---------client/pbc1
-rw-r--r--client/readme.md21
-rw-r--r--client/rl.cpp86
-rw-r--r--client/rl.h10
-rw-r--r--client/sock.cpp125
-rw-r--r--client/sock.h34
-rw-r--r--main/CMakeLists.txt1
-rw-r--r--main/sock.c52
-rw-r--r--main/sock.h4
-rw-r--r--proto/puzbusv1.c45
-rw-r--r--proto/puzbusv1.h58
19 files changed, 799 insertions, 69 deletions
diff --git a/client/.gitignore b/client/.gitignore
deleted file mode 100644
index ba2906d..0000000
--- a/client/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-main
diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt
index bcef4c0..35a55b6 100644
--- a/client/CMakeLists.txt
+++ b/client/CMakeLists.txt
@@ -8,13 +8,18 @@ project(puzzlebox_client C CXX)
include(../proto/include.cmake)
-add_executable(main
+add_executable(pbc
main.cpp
+ rl.cpp
+ sock.cpp
+ cmd.cpp
+ parse.cpp
)
-target_link_libraries(main
+target_link_libraries(pbc
puzbus
mpack
+ readline # this is such a common library that I did not bother adding it as a submodule
)
diff --git a/client/cmd.cpp b/client/cmd.cpp
new file mode 100644
index 0000000..1ec2cb8
--- /dev/null
+++ b/client/cmd.cpp
@@ -0,0 +1,63 @@
+#include <cstdio>
+#include <cstdlib>
+#include <string.h>
+
+#include "cmd.h"
+#include "sock.h"
+#include "parse.h"
+
+char* consume_token(char* input, const char* ifs) {
+ strtok(input, ifs);
+ return strtok(NULL, "\0");
+}
+
+void cmd_exit(char*) {
+ exit(EXIT_SUCCESS);
+}
+
+void cmd_test(char*) {
+ const char* data = "Hello world!";
+ i2c_send(0x39, data, strlen(data));
+}
+
+void cmd_help(char*) {
+ printf("List of available commands:\n");
+ for (size_t i = 0; i < cmds_length; i++) {
+ struct cmd cmd = cmds[i];
+ printf(" %-*s", 10, cmd.name);
+ if (cmd.info != NULL)
+ printf(" %s", cmd.info);
+ printf("\n");
+ }
+
+ printf(
+ "\n"
+ "You can also use the TAB key to autocomplete commands\n"
+ );
+}
+
+void cmd_send(char* addr_str) {
+ char* data_str = consume_token(addr_str, IFS);
+
+ char* end;
+ uint16_t addr = strtol(addr_str, &end, 0);
+ if (addr_str + strlen(addr_str) != end) {
+ printf("address format error\n");
+ return;
+ }
+
+ char* data;
+ size_t data_size;
+ int err = strtodata(data_str, &data, &data_size);
+ if (err <= 0) {
+ printf("data format error at index %d:\n%s\n%*s^\n",
+ -err, data_str, -err, "");
+ return;
+ }
+
+ // printf("(0x%02x) -> \"%.*s\"\n", addr, data_size, data);
+ i2c_send(addr, data, data_size);
+
+ free(data);
+}
+
diff --git a/client/cmd.h b/client/cmd.h
new file mode 100644
index 0000000..9d20328
--- /dev/null
+++ b/client/cmd.h
@@ -0,0 +1,60 @@
+#pragma once
+
+#include <stddef.h>
+
+typedef void cmd_fn_t(char *);
+
+struct cmd {
+ void (* handle)(char *);
+ const char* name;
+ const char* info;
+ // TODO: tab completion function?
+};
+
+cmd_fn_t cmd_exit;
+cmd_fn_t cmd_test;
+cmd_fn_t cmd_help;
+cmd_fn_t cmd_send;
+cmd_fn_t cmd_status;
+cmd_fn_t cmd_reset;
+cmd_fn_t cmd_ls;
+
+static const struct cmd cmds[] = {
+ {
+ .handle = cmd_exit,
+ .name = "exit",
+ .info = "exit pbc",
+ },
+ {
+ .handle = cmd_test,
+ .name = "test",
+ .info = "send a test puzbus message",
+ },
+ {
+ .handle = cmd_help,
+ .name = "help",
+ .info = "show this help",
+ },
+ {
+ .handle = cmd_send,
+ .name = "send",
+ .info = "[debug] send raw message",
+ },
+ // {
+ // .handle = cmd_status,
+ // .name = "status",
+ // .info = "show global puzzle box state (main controller state)",
+ // },
+ // {
+ // .handle = cmd_reset,
+ // .name = "reset",
+ // .info = "reset entire game state",
+ // },
+ // {
+ // .handle = cmd_ls,
+ // .name = "ls",
+ // .info = "list connected puzzle modules",
+ // },
+};
+static const size_t cmds_length = sizeof(cmds) / sizeof(cmds[0]);
+
diff --git a/client/examples/puzbus-hello-world.cpp b/client/examples/puzbus-hello-world.cpp
new file mode 100644
index 0000000..dcc965b
--- /dev/null
+++ b/client/examples/puzbus-hello-world.cpp
@@ -0,0 +1,67 @@
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <unistd.h>
+
+#include "puzbusv1.h"
+
+int send_message() {
+ const char* data = "Test message data!";
+ struct pb_msg output = {
+ .addr = 0x39,
+ .data = (char*) data,
+ .length = strlen(data),
+ };
+
+ char* packed;
+ size_t size;
+ if (!pb_write(&output, &packed, &size)) {
+ printf("error writing!\n");
+ return EXIT_FAILURE;
+ }
+
+ fwrite(packed, sizeof(packed[0]), size, stdout);
+ fflush(stdout);
+
+ return EXIT_SUCCESS;
+}
+
+int read_message() {
+ freopen(NULL, "rb", stdin); // allow binary on stdin
+ struct pb_msg input;
+
+ char buf[4]; // extremely small buffer to test chunked message parsing
+ size_t bytes = 0;
+
+ while ((bytes = fread(buf, sizeof(buf[0]), sizeof(buf), stdin)) > 0) {
+ int ret = pb_read(&input, buf, bytes);
+
+ // header read error
+ if (ret < 0) {
+ printf("error reading!\n");
+ return EXIT_FAILURE;
+ }
+
+ // continue reading if more bytes needed...
+ if (ret > 0) continue;
+
+ // message read completely!
+ printf("address: 0x%02x\n", input.addr);
+ printf("data: \"%.*s\"\n", input.length, input.data);
+ free(input.data);
+ return EXIT_SUCCESS;
+ }
+
+ // if we reach this point, data was read but it did not contain a complete
+ // message, and is thus considered a failure
+ return EXIT_FAILURE;
+}
+
+int main() {
+ if (!isatty(fileno(stdout))) return send_message();
+ if (!isatty(fileno(stdin))) return read_message();
+
+ printf("please pipe some data in or out to use this program\n");
+ return EXIT_SUCCESS;
+}
+
diff --git a/client/main.cpp b/client/main.cpp
index 30d7045..5c26107 100644
--- a/client/main.cpp
+++ b/client/main.cpp
@@ -1,54 +1,38 @@
#include <cstdio>
#include <cstdlib>
-#include <cstring>
-#include <unistd.h>
-
-#include "puzbusv1.h"
-
-int send_message() {
- const char* data = "Test message data!";
- struct pb_msg output = {
- .addr = 0x39,
- .data = (char*) data,
- .length = strlen(data),
- };
-
- char* packed;
- size_t size;
- if (!pb_write(&output, &packed, &size)) {
- printf("error writing!\n");
- return 1;
- }
+#include <cstdint>
+#include <exception>
- fwrite(packed, sizeof(packed[0]), size, stdout);
- fflush(stdout);
+#include "rl.h"
+#include "sock.h"
- return 0;
-}
+PBSocket* sock;
-int read_message() {
- freopen(NULL, "rb", stdin); // allow binary on stdin
- struct pb_msg input;
-
- char buf[8]; // extremely small buffer to test chunked message parsing
- size_t bytes = 0;
- while ((bytes = fread(buf, sizeof(buf[0]), sizeof(buf), stdin)) > 0) {
- if (!pb_read(&input, buf, bytes)) continue;
-
- printf("address: 0x%02x\n", input.addr);
- printf("data: \"%.*s\"\n", input.length, input.data);
- free(input.data);
- return 0;
+int main(int argc, char** argv) {
+ if (argc < 2) {
+ printf("usage: %s addr [port]\n", argv[0]);
+ return EXIT_FAILURE;
}
- return 1;
-}
+ // parse arguments
+ char* addr = argv[1];
+ uint16_t port = 9191;
+ if (argc >= 3) port = atoi(argv[2]);
+
+ sock = new PBSocket(addr, port);
+ try {
+ // connect to TCP socket (automatically spawns thread)
+ sock->sock_connect();
+ } catch (const std::exception& e) {
+ printf("error: %s\n", e.what());
+ return EXIT_FAILURE;
+ }
+
+ // enter main CLI (using GNU readline for comfyness)
+ int ret = cli_main();
+
+ delete sock;
-int main() {
- if (!isatty(fileno(stdout))) return send_message();
- if (!isatty(fileno(stdin))) return read_message();
-
- printf("please pipe some data in or out to use this program\n");
- return 0;
+ return ret;
}
diff --git a/client/parse.cpp b/client/parse.cpp
new file mode 100644
index 0000000..223dc5d
--- /dev/null
+++ b/client/parse.cpp
@@ -0,0 +1,117 @@
+#include <math.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+#include "parse.h"
+
+static int parse_str(const char* str, char* data, size_t* size) {
+ char closing = str[0];
+ char escape = false;
+ bool scan = data == NULL;
+ int i = 0;
+ size_t len = strlen(str);
+
+ switch (str[i]) {
+ case '\'':
+ escape = false;
+ break;
+ case '\"':
+ escape = true;
+ break;
+ default:
+ return -i;
+ }
+
+ for (i = 1; i < len && str[i] != '\0'; i++) {
+ char c = str[i];
+
+ if (c == closing) {
+ if (scan) printf("string%s of length %d\n", escape ? " (w/ escape)" : "", i - 1);
+ return i + 1; // +1 for closing quote
+ }
+
+ if (scan) *size += 1;
+ }
+
+ return -i;
+}
+
+static int parse_num(const char* str, char* data, size_t* size) {
+ const char* ifs = IFS;
+ size_t len = strcspn(str, ifs);
+ bool scan = data == NULL;
+ int i = 0;
+ int base = 10;
+ bool bytestring = false;
+
+ const char* colon = strchr(str, ':');
+ if (colon != NULL && colon < str + len) { // byte string
+ base = 16;
+ bytestring = true;
+ } else if (len > 2 && strncmp(str, "0x", 2) == 0) { // hexadecimal prefix
+ base = 16;
+ i += 2;
+ }/* else if (len > 1 && strncmp(str, "0", 1) == 0) { // octal prefix
+ base = 8;
+ i += 1;
+ }*/
+
+ const char* set;
+ // if (base == 8) set = SET_OCT;
+ if (base == 10) set = SET_DEC;
+ if (base == 16) {
+ if (bytestring) set = SET_HEX_STR;
+ else set = SET_HEX;
+ }
+
+ size_t len_ok = strspn(str + i, set) + i;
+ if (len != len_ok) return -len_ok;
+
+ if (scan) {
+ if (base == 10) *size += 1;
+ else if (base == 16) {
+ if (!bytestring) {
+ *size += (len - i + 1) / 2;
+ } else {
+ for (; colon != NULL && colon < str + len; colon = strchr(str, ':')) {
+ *size += 1;
+ }
+ }
+ }
+ }
+
+ if (scan) printf("number (base %d%s) of length %lu\n", base, bytestring ? " as bytestring" : "", len - i);
+ return len;
+}
+
+int strtodata(const char* str, char** data, size_t* size) {
+ const char* ifs = IFS;
+ *size = 0;
+ size_t i;
+ size_t len = strlen(str);
+
+ for (i = 0; i < len;) {
+ // skip whitespace
+ int run;
+ run = strspn(&str[i], ifs);
+ if (run > 0) printf("skipping whitespace for %d bytes...\n", run);
+ i += run;
+ // end of string
+ if (str[i] == '\0') break;
+
+ if ((run = parse_str(str + i, NULL, size)) > 0) { i += run; continue; }
+ if ((run = parse_num(str + i, NULL, size)) > 0) { i += run; continue; }
+
+ // no format detected
+ return -i + run;
+ }
+ printf("end of string w/o parse errors\n");
+ printf("buffer size is now %lu\n", *size);
+ exit(0);
+
+ *data = (char*) malloc(*size);
+
+ return 0;
+}
+
diff --git a/client/parse.h b/client/parse.h
new file mode 100644
index 0000000..10274e7
--- /dev/null
+++ b/client/parse.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <stddef.h>
+
+#define IFS " \t\n"
+
+#define SET_OCT "01234567"
+#define SET_DEC "0123456789"
+#define SET_HEX SET_DEC"abcdefABCDEF"
+#define SET_HEX_STR SET_HEX":"
+
+/**
+ * \brief modify \p token to point to the first token when broken up on \p ifs
+ * and return the remaining data
+ *
+ * \p token will be null-terminated to indicate the end of the first token. A
+ * pointer to the remaining line after the NULL byte is returned, or NULL when
+ * the end of the string has been reached.
+ *
+ * \param token input string
+ * \param ifs string containing field separators
+ *
+ * \return the remaining data after \p token and the first \p ifs
+ */
+char* consume_token(char* token, const char* ifs);
+
+/**
+ * \brief convert string with literals into raw data
+ *
+ * \param str input string containing literals
+ * \param data pointer to \c char* that will store the resulting data
+ * \param size size of \p data
+ *
+ * \return 0 or a negative integer representing the index where there is a
+ * syntax error if there was an error, or a positive integer representing the
+ * amount of bytes parsed from \p str
+ *
+ * \note The pointer that \p data refers to will not be initialized by this
+ * function if parsing fails
+ */
+int strtodata(const char* str, char** data, size_t* size);
+
diff --git a/client/pbc b/client/pbc
new file mode 120000
index 0000000..51eda50
--- /dev/null
+++ b/client/pbc
@@ -0,0 +1 @@
+build/pbc \ No newline at end of file
diff --git a/client/readme.md b/client/readme.md
new file mode 100644
index 0000000..04471d2
--- /dev/null
+++ b/client/readme.md
@@ -0,0 +1,21 @@
+# puzzle box client
+
+goal (in order of implementation):
+```
+(pbc) help
+ exit exit pbc
+ test send a test puzbus message
+ help show this help
+ send <addr> <data> [debug] send raw message
+ status show global puzzle box state (main controller state)
+ reset reset entire game state
+ ls list connected puzzle modules
+```
+
+
+```
+send 0x39 "Hello world!" de:ad:be:ef 0xff 5 0a 0750
+ ^~~~~~~~~~~~~~ ^~~~~~~~~~~ ~^~~ ~^ ~^ ~~~~^
+ STR_INTP BYTE_ARR UNSIGNED UNSIGNED UNSIGNED UNSIGNED
+ (hex+0x) (dec) (hex) (oct)
+```
diff --git a/client/rl.cpp b/client/rl.cpp
new file mode 100644
index 0000000..3f93e99
--- /dev/null
+++ b/client/rl.cpp
@@ -0,0 +1,86 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdbool.h>
+#include <stdarg.h>
+
+#include <readline/readline.h>
+#include <readline/history.h>
+
+#include "rl.h"
+#include "cmd.h"
+#include "parse.h"
+
+void rl_printf(const char *fmt, ...) {
+ // save line
+ char* saved_line = rl_copy_text(0, rl_end);
+ int saved_point = rl_point;
+ int saved_end = rl_end;
+
+ // clear line
+ rl_save_prompt();
+ rl_replace_line("", 0);
+ rl_redisplay();
+
+ // printf
+ va_list args;
+ va_start(args, fmt);
+ vprintf(fmt, args);
+ va_end(args);
+
+ // restore line
+ rl_restore_prompt();
+ rl_replace_line(saved_line, 0);
+ rl_point = saved_point;
+ rl_end = saved_end;
+ rl_redisplay();
+
+ free(saved_line);
+}
+
+static void cli_cmd(char* cmd) {
+ char* line = consume_token(cmd, IFS);
+
+ for (size_t i = 0; i < cmds_length; i++) {
+ if (strncmp(cmds[i].name, cmd, strlen(cmd)) != 0)
+ continue;
+
+ cmds[i].handle(line);
+ return;
+ }
+
+ printf("unknown command!\n");
+}
+
+static char* rl_completion_entries(const char *text, int state) {
+ static size_t i = 0;
+ if (state == 0) i = 0;
+
+ while (i < cmds_length) {
+ struct cmd cmd = cmds[i];
+ i++;
+ if (strncmp(text, cmd.name, strlen(text)) == 0) {
+ return strdup(cmd.name);
+ }
+ }
+
+ return NULL;
+}
+
+int cli_main() {
+ char* input = NULL;
+ rl_completion_entry_function = rl_completion_entries;
+
+ while (1) {
+ if (input != NULL) free(input);
+ input = readline(CLI_PROMPT);
+
+ if (input == NULL) return EXIT_SUCCESS; // exit on ^D (EOF)
+ if (*input == '\0') continue; // ignore empty lines
+ add_history(input);
+
+ cli_cmd(input);
+ }
+
+ return EXIT_SUCCESS;
+}
+
diff --git a/client/rl.h b/client/rl.h
new file mode 100644
index 0000000..503225f
--- /dev/null
+++ b/client/rl.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#define COLOR_OFF "\x1b[0m"
+#define COLOR_BOLD "\x1b[1m"
+
+#define CLI_PROMPT "(" COLOR_BOLD "pbc" COLOR_OFF ") "
+
+int cli_main();
+void rl_printf(const char *fmt, ...);
+
diff --git a/client/sock.cpp b/client/sock.cpp
new file mode 100644
index 0000000..f967f64
--- /dev/null
+++ b/client/sock.cpp
@@ -0,0 +1,125 @@
+#include <arpa/inet.h>
+#include <cstring>
+#include <stdexcept>
+#include <unistd.h>
+#include <cstdio>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <errno.h>
+
+#include <thread>
+
+#include "puzbusv1.h"
+#include "sock.h"
+#include "rl.h"
+
+using std::logic_error;
+using std::thread;
+
+PBSocket::PBSocket() { }
+PBSocket::PBSocket(const char * addr, uint16_t port) : PBSocket() {
+ set_server(addr, port);
+}
+
+PBSocket::~PBSocket() {
+ // stop TCP listen thread
+ if (_thread != nullptr) {
+ _thread->detach();
+ delete _thread;
+ }
+
+ sock_close();
+}
+
+void PBSocket::set_server(const char * addr, uint16_t port) {
+ _addr = addr;
+ _port = port;
+}
+
+void PBSocket::sock_connect() {
+ if (_addr == NULL) throw logic_error("no server address defined");
+ if (_port == 0) throw logic_error("no server port defined");
+
+ if (_thread != nullptr) throw logic_error("already connected");
+
+ rl_printf("connecting to %s on port %d...\n", _addr, _port);
+
+ _fd = socket(AF_INET, SOCK_STREAM, 0);
+ if (_fd < 0) throw logic_error("socket create failed");
+
+ struct sockaddr_in server = {
+ .sin_family = AF_INET,
+ .sin_port = htons(_port),
+ .sin_addr = {
+ .s_addr = inet_addr(_addr),
+ },
+ };
+ int ret = connect(_fd, (struct sockaddr*) &server, sizeof(server));
+ if (ret != 0) throw logic_error(strerror(errno));
+
+ this->_thread = new thread(&PBSocket::sock_task, this);
+}
+
+void PBSocket::sock_close() {
+ if (_fd < 0) return; // already closed
+ close(_fd);
+ _fd = -1;
+}
+
+void PBSocket::send(const char * buf, size_t buf_sz) {
+ write(_fd, buf, buf_sz);
+}
+
+void PBSocket::sock_task() {
+ struct pb_msg input;
+
+ while(1) {
+ char buf[80];
+ ssize_t bytes = read(_fd, buf, sizeof(buf));
+
+ if (bytes == -1) {
+ rl_printf("error: %s (%d)\n", strerror(errno), errno);
+ break;
+ }
+
+ // skip empty frames
+ if (bytes == 0) continue;
+
+ int ret = pb_read(&input, buf, bytes);
+
+ // header read error
+ if (ret < 0) {
+ rl_printf("pb_read error!\n");
+ break;
+ }
+
+ // continue reading if more bytes needed...
+ if (ret > 0) continue;
+
+ // message read completely!
+ i2c_recv(input.addr, input.data, input.length);
+ free(input.data);
+ }
+
+ sock_close();
+}
+
+void i2c_send(uint16_t addr, const char * data, size_t data_size) {
+ struct pb_msg msg = {
+ .addr = addr,
+ .data = (char *) data,
+ .length = data_size,
+ };
+
+ char* packed;
+ size_t size;
+ if (!pb_write(&msg, &packed, &size)) return;
+
+ sock->send(packed, size);
+}
+
+void i2c_recv(uint16_t addr, const char * data, size_t data_size) {
+ rl_printf("[0x%02x]: %.*s\n", addr, data_size, data);
+}
+
diff --git a/client/sock.h b/client/sock.h
new file mode 100644
index 0000000..42eba3b
--- /dev/null
+++ b/client/sock.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <cstdint>
+#include <thread>
+
+class PBSocket {
+public:
+ PBSocket();
+ PBSocket(const char * addr, uint16_t port);
+ virtual ~PBSocket();
+
+ void set_server(const char * addr, uint16_t port);
+
+ void sock_connect();
+
+ void send(const char * buf, size_t buf_sz);
+
+private:
+ void sock_task();
+ void sock_close();
+
+ std::thread* _thread = nullptr;
+
+ const char * _addr = NULL;
+ uint16_t _port = 0;
+
+ int _fd = -1;
+};
+
+extern PBSocket* sock;
+
+void i2c_send(uint16_t addr, const char * data, size_t data_size);
+void i2c_recv(uint16_t addr, const char * data, size_t data_size);
+
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index 88abf60..30685a4 100644
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -33,6 +33,7 @@ target_link_libraries(main
hardware_i2c
FreeRTOS-Kernel
FreeRTOS-Kernel-Heap4
+ puzbus
mpack
)
diff --git a/main/sock.c b/main/sock.c
index dac62af..4f50981 100644
--- a/main/sock.c
+++ b/main/sock.c
@@ -3,19 +3,63 @@
#include <lwip/opt.h>
#include <lwip/sys.h>
#include <lwip/api.h>
+#include <string.h>
#include "init.h"
#include "config.h"
+#include "puzbusv1.h"
+#include "sock.h"
struct netconn* current_connection = NULL;
+struct pb_msg recv_msg;
+
+void i2c_send(uint16_t addr, const char * data, size_t data_size) {
+ if (current_connection == NULL) return;
+
+ struct pb_msg send_msg = {
+ .addr = addr,
+ .data = (char *) data,
+ .length = data_size,
+ };
+
+ char * buf;
+ size_t buf_sz;
+
+ if (!pb_write(&send_msg, &buf, &buf_sz)) return;
+
+ // NOTE: netconn does return an error code, but the data needs to be freed
+ // whether netconn throws an error or not, so it remains unused
+ netconn_write(current_connection, buf, buf_sz, NETCONN_COPY);
+
+ free(buf);
+}
+
+void i2c_recv(uint16_t addr, const char * data, size_t data_size) {
+ printf("address: 0x%02x\n", addr);
+ printf("data: \"%.*s\"\n", data_size, data);
+
+ // send message back
+ char reply[] = "Test message back!";
+ i2c_send(0x69, reply, strlen(reply));
+
+ // TODO: this function should forward the recieved message onto the puzzle
+ // bus instead of printing/replying
+}
void recv_handler(struct netconn* conn, struct netbuf* buf) {
- void *data;
- uint16_t len;
+ pb_read_reset(&recv_msg);
do {
- netbuf_data(buf, &data, &len);
- printf("got %d bytes!\n", len);
+ char* data;
+ uint16_t len;
+ netbuf_data(buf, (void**)&data, &len);
+
+ // continue early if more data is needed to complete message
+ if (!pb_read(&recv_msg, data, len)) continue;
+
+ // forward received message to puzzle bus
+ i2c_recv(recv_msg.addr, recv_msg.data, recv_msg.length);
+ free(recv_msg.data);
} while (netbuf_next(buf) >= 0);
netbuf_delete(buf);
diff --git a/main/sock.h b/main/sock.h
index 2a73418..f2db35d 100644
--- a/main/sock.h
+++ b/main/sock.h
@@ -6,6 +6,6 @@
/** \brief start listening for TCP socket requests */
void serve_task();
-void i2c_send(uint16_t addr, char* data, size_t data_size);
-void i2c_recv(uint16_t addr, char* data, size_t data_size);
+void i2c_send(uint16_t addr, const char * data, size_t data_size);
+void i2c_recv(uint16_t addr, const char * data, size_t data_size);
diff --git a/proto/puzbusv1.c b/proto/puzbusv1.c
index 3ff7c63..73deda5 100644
--- a/proto/puzbusv1.c
+++ b/proto/puzbusv1.c
@@ -1,25 +1,48 @@
#include <mpack.h>
#include <stdio.h>
+// MIN() macro
+#include <sys/param.h>
+// TODO: check if this works on pico as well
+
#include "puzbusv1.h"
-int pb_read(struct pb_msg* target, char* buf, size_t buf_sz) {
+int pb_read(struct pb_msg * target, const char * buf, size_t buf_sz) {
+ // a new reader is used per buffer block passed to this function
mpack_reader_t reader;
- printf("read %lu bytes...\n", buf_sz);
-
mpack_reader_init_data(&reader, buf, buf_sz);
- uint16_t address = mpack_expect_u16(&reader);
- char data_buf[80];
- size_t data_size = mpack_expect_bin_buf(&reader, data_buf, sizeof(data_buf));
-
- printf("0x%02x\n", address);
- printf("\"%.*s\"\n", data_size, data_buf);
+ // at start of message
+ if (target->_rdata == 0) {
+ // NOTE: The entire start of a message needs to be readable from the buffer
+ // at this point. When target->addr can be read and target->length is past
+ // the end of the current buffer block, this function will crash and burn.
+ // This is a highly unlikely scenario, as pb_read is called for each chunk
+ // of a TCP frame, and frames (should) include only one puzzle bus message.
+ // The check here is kind of optional.
+ if (buf_sz < 4) return -1;
+
+ target->addr = mpack_expect_u16(&reader);
+ target->length = target->_rdata = mpack_expect_bin(&reader);
+ target->data = (char *) malloc(target->length);
+ }
+
+ // continue reading chunks of target->data until the amount of bytes
+ // specified in target->length
+ size_t to_read = MIN(mpack_reader_remaining(&reader, NULL), target->_rdata);
+ char * data = target->data + target->length - target->_rdata;
+ mpack_read_bytes(&reader, data, to_read);
+ target->_rdata -= to_read;
+
+ // if rdata = 0, the message was completely read
+ return target->_rdata;
+}
- return 0;
+void pb_read_reset(struct pb_msg * target) {
+ target->_rdata = 0;
}
-int pb_write(struct pb_msg* target, char** buf, size_t* buf_sz) {
+bool pb_write(const struct pb_msg * target, char ** buf, size_t * buf_sz) {
mpack_writer_t writer;
mpack_writer_init_growable(&writer, buf, buf_sz);
diff --git a/proto/puzbusv1.h b/proto/puzbusv1.h
index 116dbf9..0985b2b 100644
--- a/proto/puzbusv1.h
+++ b/proto/puzbusv1.h
@@ -7,14 +7,62 @@
extern "C" {
#endif
+/** \brief Puzzle bus message (v1) */
struct pb_msg {
- uint16_t addr;
- char* data;
- size_t length;
+ uint16_t addr; //!< I^2^C address
+ char * data; //!< message content
+ size_t length; //!< message size
+ size_t _rdata; //!< \private remaining bytes to read until message is complete
};
-int pb_read(struct pb_msg* target, char* buf, size_t buf_sz);
-int pb_write(struct pb_msg* target, char** buf, size_t* buf_sz);
+/**
+ * \brief Read chunk of input stream, and store resulting message in \p target
+ *
+ * This function is called for each chunk of data from an input stream, and
+ * will parse the next puzzle bus message into \p target. The input stream is
+ * assumed to only contain messages encoded by \p pb_write()
+ *
+ * \param target pointer to struct that will contain the finished message data
+ * \param buf pointer to input stream data chunk
+ * \param buf_sz size of \p buf
+ *
+ * \returns Integer representing amount of bytes required to finish message, or
+ * -1 if the message header could not be read. If this function returns 0, the
+ * message in \p target is complete.
+ *
+ * \note target->data will automatically be allocated by this function, even if
+ * the message is not fully parsed. This variable must be `free()`d by the
+ * caller after each complete message to prevent memory leaks.
+ */
+int pb_read(struct pb_msg * target, const char * buf, size_t buf_sz);
+
+/**
+ * \brief reset the remaining message data counter
+ *
+ * Calling this function has the effect of forcing \c pb_read() to parse the
+ * next buffer chunk as the start of a new message. This function may be called
+ * before reading a TCP frame's data to mitigate any synchronization issues
+ * arising from earlier corrupt or otherwise malformed messages.
+ */
+void pb_read_reset(struct pb_msg * target);
+
+/**
+ * \brief Allocate and write a msgpack-formatted message to \p buf
+ *
+ * This function allocates a buffer large enough to fit the message specified
+ * in \p target, and encodes the data in \p target in a format that can be
+ * decoded later using \p pb_read()
+ *
+ * \param target pointer to struct that contains the message data
+ * \param buf pointer to \c char* that will contain the formatted message
+ * \param buf_sz pointer to \c size_t that will represent the final size of \p buf
+ *
+ * \returns boolean true if a the message could be encoded successfully, false
+ * if there was some kind of error
+ *
+ * \note the pointer stored in \p buf must be `free()`d by the caller afterwards
+ */
+bool pb_write(const struct pb_msg * target, char ** buf, size_t * buf_sz);
#ifdef __cplusplus
}