diff options
Diffstat (limited to 'client')
-rw-r--r-- | client/CMakeLists.txt | 30 | ||||
-rw-r--r-- | client/cmd.cpp | 94 | ||||
-rw-r--r-- | client/cmd.h | 62 | ||||
l--------- | client/compile_commands.json | 1 | ||||
l--------- | client/lib | 1 | ||||
-rw-r--r-- | client/main.cpp | 38 | ||||
-rw-r--r-- | client/makefile | 2 | ||||
-rw-r--r-- | client/parse.cpp | 176 | ||||
-rw-r--r-- | client/parse.h | 42 | ||||
l--------- | client/pbc | 1 | ||||
-rw-r--r-- | client/readme.md | 37 | ||||
-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 | ||||
-rw-r--r-- | client/xxd.c | 44 | ||||
-rw-r--r-- | client/xxd.h | 17 |
17 files changed, 800 insertions, 0 deletions
diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt new file mode 100644 index 0000000..57a2447 --- /dev/null +++ b/client/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.29) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_EXPORT_COMPILE_COMMANDS 1) + +# enable debug features +set(CMAKE_BUILD_TYPE Debug) +add_compile_definitions(DEBUG) + +project(puzzlebox_client C CXX) + +include(../i2ctcp/include.cmake) + +add_executable(pbc + main.cpp + rl.cpp + sock.cpp + cmd.cpp + parse.cpp + xxd.c + ) + +target_link_libraries(pbc + i2ctcp + 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..ab101e9 --- /dev/null +++ b/client/cmd.cpp @@ -0,0 +1,94 @@ +#include <cstdio> +#include <cstdlib> +#include <string.h> + +#include "cmd.h" +#include "i2ctcpv1.h" +#include "sock.h" +#include "parse.h" + +#include "../shared/puzbus.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("sending char data[%lu = 0x%02lx] to 0x%02x\n", data_size, data_size, addr); + i2c_send(addr, data, data_size); + + free(data); +} + +void cmd_status(char*) { + const char msg[] = { + PB_CMD_READ, + 0x00, // addr 0 = global state + }; + i2c_send(BUSADDR_MAIN, msg, sizeof(msg)); + // NOTE: the reply handler will automatically print the state once it's + // received +} + +void cmd_reset(char*) { + const char msg[] = { + PB_CMD_WRITE, + 0x00, + PB_GS_IDLE, + }; + i2c_send(BUSADDR_MAIN, msg, sizeof(msg)); +} + +void cmd_ls(char*) { + return; + const char msg[] = { + PB_CMD_READ, + // TODO: which address is this? + }; + i2c_send(BUSADDR_MAIN, msg, sizeof(msg)); +} + diff --git a/client/cmd.h b/client/cmd.h new file mode 100644 index 0000000..932f3a2 --- /dev/null +++ b/client/cmd.h @@ -0,0 +1,62 @@ +#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_status; +cmd_fn_t cmd_reset; +cmd_fn_t cmd_ls; +cmd_fn_t cmd_send; + +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_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", + }, +#ifdef DEBUG + { + .handle = cmd_send, + .name = "send", + .info = "[debug] send raw message", + }, +#endif +}; +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/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..f31e802 --- /dev/null +++ b/client/parse.cpp @@ -0,0 +1,176 @@ +#include <stdlib.h> +#include <string.h> +#include <stdio.h> +#include <netinet/in.h> + +#include "parse.h" + +static int parse_string(const char * str, char * data, size_t * offset) { + char closing = str[0]; + char escape = false; + 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++, *offset += 1) { + char c = str[i]; + + // TODO: handle escaped characters + + if (c == closing) + return i + 1; // +1 for closing quote + + if (data != NULL) + data[*offset] = c; + } + + return -i; +} + +static int parse_hexstr(const char * str, char * data, size_t * offset) { + const char* ifs = IFS; + size_t len = strcspn(str, ifs); + int i = 0; + + // check if token contains at least one colon + const char* colon = strchr(str, ':'); + if (colon == NULL) return -i; + if (colon >= str + len) return -i; + + // check if token only contains allowed characters [0-9a-fA-F:] + size_t len_ok = strspn(str + i, SET_HEX_STR) + i; + if (len != len_ok) return -len_ok; + + size_t c = 0; + while (c < len) { // count bytes in bytestring + if (strspn(str + c, SET_HEX) != 2) + return -i -c; + + if (data != NULL) + data[*offset] = strtol(str + c, NULL, 16) & 0xff; + + c += 2; + *offset += 1; + + if (str[c] == ':') { + c += 1; + continue; + } + break; + } + + i += len; + return i; +} + +static int parse_number(const char * str, char * data, size_t * offset) { + const char* ifs = IFS; + size_t len = strcspn(str, ifs); + int i = 0; + int base = 10; + bool bytestring = false; + + 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) set = SET_HEX; + + size_t len_ok = strspn(str + i, set) + i; + if (len != len_ok) return -len_ok; + + size_t size = 1; // default integer size in bytes + if (base == 16) { + size_t prefixless = len - i; + switch (prefixless) { + case 2: // 8-bit (2 hex characters) + case 4: // 16-bit + case 8: // 32-bit + case 16: // 64-bit + break; + default: + return -i; + } + size = prefixless / 2; + } + + if (data != NULL) { + unsigned long number = strtol(str + i, NULL, base); + long long mask = (1 << 8 * size) - 1; + number &= mask; + // NOTE: the hton? functions are used to convert host endianness to network + // endianness (big), and are required + switch (size) { + case 1: + data[*offset] = number & 0xff; + break; + case 2: + number = htons(number); + // TODO: check if the endianness is OK, or reverse these *offset indices* + data[*offset + 1] = (number) & 0xff; + data[*offset + 0] = (number >>= 8) & 0xff; + break; + case 4: + number = htonl(number); + data[*offset + 3] = (number) & 0xff; + data[*offset + 2] = (number >>= 8) & 0xff; + data[*offset + 1] = (number >>= 8) & 0xff; + data[*offset + 0] = (number >>= 8) & 0xff; + break; + } + } + + *offset += size; + return len; +} + +static int _strtodata_main(const char * str, char* data, size_t * offset) { + const char* ifs = IFS; + size_t len = strlen(str); + + int i, run; + for (i = 0; i < len; i += run) { + i += strspn(&str[i], ifs); // skip whitespace + if (str[i] == '\0') break; // end of string + + if ((run = parse_string(str + i, data, offset)) > 0) continue; + if ((run = parse_hexstr(str + i, data, offset)) > 0) continue; + if ((run = parse_number(str + i, data, offset)) > 0) continue; + + // no format detected + return -i + run; + } + + return i; +} + +int strtodata(const char * str, char ** data, size_t * size) { + *size = 0; + + // 1st pass: check data format + int ret = _strtodata_main(str, NULL, size); + if (ret <= 0) return ret; // on error + + // 2nd pass: convert string literals into binary data + *data = (char*) malloc(*size); + size_t written = 0; + return _strtodata_main(str, *data, &written); +} + diff --git a/client/parse.h b/client/parse.h new file mode 100644 index 0000000..94afe70 --- /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..ea3e034 --- /dev/null +++ b/client/readme.md @@ -0,0 +1,37 @@ +# puzzle box client + +This folder contains the source code for the puzzle box client (pbc). This is a +desktop application that communicates with the main controller over TCP to +send/receive I<sup>2</sup>C messages. This application is not only used by a +game operator to control and monitor the state of a puzzle box, but is also a +useful debugging tool when developing puzzle modules, as it allows you to send +arbitrary data over the puzzle bus. + +## Features + +- List detected puzzle modules +- Reset puzzle modules (individually or all to reset the box) +- Skip puzzle modules (individually or all) +- Request puzzle box state + +Debug only: +- Send arbitrary messages + +## Building + +PBC is a standard CMake project, but a [makefile](./makefile) is provided for +convenience (still requires CMake and Ninja are installed). + +## Send data + +``` + ADDRESS DATA + v~~~ v~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +send 0x39 68:65:6c:6c:6f 44 0x20 'world' 33 + ^~~~~~~~~~~~~~ ^~ ^~~~ ^~~~~~~ ^~ + HEXSTR NUMBER NUMBER STRING NUMBER + (binary) (dec) (hex) (literal) (dec) +``` + +The data is concatenated, and may contain mixed types of literals + 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..2d5787d --- /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 "i2ctcpv1.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() { + i2ctcp_msg_t 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 = i2ctcp_read(&input, buf, bytes); + + // header read error + if (ret < 0) { + rl_printf("i2ctcp_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) { + i2ctcp_msg_t msg = { + .addr = addr, + .data = (char *) data, + .length = data_size, + }; + + char* packed; + size_t size; + if (!i2ctcp_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/client/xxd.c b/client/xxd.c new file mode 100644 index 0000000..5d83635 --- /dev/null +++ b/client/xxd.c @@ -0,0 +1,44 @@ +#include <stdio.h> +#include <ctype.h> + +#include "xxd.h" + +void xxd(const char * data, size_t size) { + size_t fake_size = size + (16 - size % 16) % 16; + + for (size_t base = 0; base < fake_size; base += 16) { + printf("%08lx: ", base); + + // print bytes + for (size_t offset = 0; offset < 16; offset++) { + size_t i = base + offset; + + if (offset == 8) printf(" "); + + if (i >= size) { + printf(" "); + continue; + } + + printf("%02x ", data[i] & 0xff); + } + + // print ascii representation + printf(" |"); + for (size_t offset = 0; offset < 16; offset++) { + size_t i = base + offset; + + if (i >= size) { + printf(" "); + continue; + } + + if (isprint(data[i])) + printf("%c", data[i]); + else + printf("."); + } + printf("|\n"); + } +} + diff --git a/client/xxd.h b/client/xxd.h new file mode 100644 index 0000000..fb28bb1 --- /dev/null +++ b/client/xxd.h @@ -0,0 +1,17 @@ +#pragma once + +#include <stddef.h> + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \brief utility function that prints hexdump of data + */ +void xxd(const char * data, size_t size); + +#ifdef __cplusplus +} +#endif + |