aboutsummaryrefslogtreecommitdiff
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/CMakeLists.txt25
-rw-r--r--client/cmd.cpp63
-rw-r--r--client/cmd.h60
l---------client/compile_commands.json1
-rw-r--r--client/examples/puzbus-hello-world.cpp67
l---------client/lib1
-rw-r--r--client/main.cpp38
-rw-r--r--client/makefile2
-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
16 files changed, 693 insertions, 0 deletions
diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt
new file mode 100644
index 0000000..35a55b6
--- /dev/null
+++ b/client/CMakeLists.txt
@@ -0,0 +1,25 @@
+cmake_minimum_required(VERSION 3.29)
+
+set(CMAKE_C_STANDARD 11)
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_EXPORT_COMPILE_COMMANDS 1)
+
+project(puzzlebox_client C CXX)
+
+include(../proto/include.cmake)
+
+add_executable(pbc
+ main.cpp
+ rl.cpp
+ sock.cpp
+ cmd.cpp
+ parse.cpp
+ )
+
+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/compile_commands.json b/client/compile_commands.json
new file mode 120000
index 0000000..25eb4b2
--- /dev/null
+++ b/client/compile_commands.json
@@ -0,0 +1 @@
+build/compile_commands.json \ No newline at end of file
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/lib b/client/lib
new file mode 120000
index 0000000..dc598c5
--- /dev/null
+++ b/client/lib
@@ -0,0 +1 @@
+../lib \ No newline at end of file
diff --git a/client/main.cpp b/client/main.cpp
new file mode 100644
index 0000000..5c26107
--- /dev/null
+++ b/client/main.cpp
@@ -0,0 +1,38 @@
+#include <cstdio>
+#include <cstdlib>
+#include <cstdint>
+#include <exception>
+
+#include "rl.h"
+#include "sock.h"
+
+PBSocket* sock;
+
+int main(int argc, char** argv) {
+ if (argc < 2) {
+ printf("usage: %s addr [port]\n", argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ // 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;
+
+ return ret;
+}
+
diff --git a/client/makefile b/client/makefile
new file mode 100644
index 0000000..8352615
--- /dev/null
+++ b/client/makefile
@@ -0,0 +1,2 @@
+include ../lazy.mk
+
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);
+