diff options
author | Loek Le Blansch <loek@pipeframe.xyz> | 2024-05-25 18:30:13 +0200 |
---|---|---|
committer | Loek Le Blansch <loek@pipeframe.xyz> | 2024-05-25 18:30:13 +0200 |
commit | 4525f60f29359b7ba88e47880d79fb9869913656 (patch) | |
tree | d6fa4be85ddfcee9861304eee53660bff7487d26 /client | |
parent | 4fc192eb9ba949276c47c1bbd86164d955d3548c (diff) | |
parent | 5d5b186a5a82b7e2415eddd77ef93af851034a5b (diff) |
Merge branch 'wip/main-controller' into wip/i2c-communication
Diffstat (limited to 'client')
-rw-r--r-- | client/.gitignore | 1 | ||||
-rw-r--r-- | client/CMakeLists.txt | 9 | ||||
-rw-r--r-- | client/cmd.cpp | 63 | ||||
-rw-r--r-- | client/cmd.h | 60 | ||||
-rw-r--r-- | client/examples/puzbus-hello-world.cpp | 67 | ||||
-rw-r--r-- | client/main.cpp | 72 | ||||
-rw-r--r-- | client/parse.cpp | 117 | ||||
-rw-r--r-- | client/parse.h | 42 | ||||
l--------- | client/pbc | 1 | ||||
-rw-r--r-- | client/readme.md | 21 | ||||
-rw-r--r-- | client/rl.cpp | 86 | ||||
-rw-r--r-- | client/rl.h | 10 | ||||
-rw-r--r-- | client/sock.cpp | 125 | ||||
-rw-r--r-- | client/sock.h | 34 |
14 files changed, 661 insertions, 47 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); + |