diff options
-rw-r--r-- | client/CMakeLists.txt | 2 | ||||
-rw-r--r-- | client/cmd.cpp | 31 | ||||
-rw-r--r-- | client/cmd.h | 49 | ||||
-rw-r--r-- | client/parse.cpp | 26 | ||||
-rw-r--r-- | client/pbc.1 | 94 | ||||
-rw-r--r-- | client/rl.cpp | 25 | ||||
-rw-r--r-- | client/rl.h | 2 | ||||
-rw-r--r-- | shared/include.cmake | 5 | ||||
-rw-r--r-- | shared/pb/bus.h | 18 | ||||
-rw-r--r-- | shared/pb/driver.c | 31 | ||||
-rw-r--r-- | shared/pb/driver.h | 24 | ||||
-rw-r--r-- | shared/pb/types.h | 58 | ||||
-rw-r--r-- | shared/puzbus.h | 46 |
13 files changed, 322 insertions, 89 deletions
diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 57a2447..50d3cd7 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -11,6 +11,7 @@ add_compile_definitions(DEBUG) project(puzzlebox_client C CXX) include(../i2ctcp/include.cmake) +include(../shared/include.cmake) add_executable(pbc main.cpp @@ -23,6 +24,7 @@ add_executable(pbc target_link_libraries(pbc i2ctcp + 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 index ab101e9..b7adfae 100644 --- a/client/cmd.cpp +++ b/client/cmd.cpp @@ -1,13 +1,16 @@ #include <cstdio> #include <cstdlib> +#include <readline/readline.h> #include <string.h> #include "cmd.h" #include "i2ctcpv1.h" +#include "pb/types.h" +#include "rl.h" #include "sock.h" #include "parse.h" -#include "../shared/puzbus.h" +#include "pb/bus.h" char* consume_token(char* input, const char* ifs) { strtok(input, ifs); @@ -35,7 +38,8 @@ void cmd_help(char*) { printf( "\n" - "You can also use the TAB key to autocomplete commands\n" + "See man pbc(1) for more info about specific commands\n" + "Hint: you can use the TAB key to autocomplete commands\n" ); } @@ -64,21 +68,30 @@ void cmd_send(char* addr_str) { free(data); } -void cmd_status(char*) { +// 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_READ, - 0x00, // addr 0 = global state + PB_CMD_WRITE, + 0x00, + PB_GS_IDLE, }; i2c_send(BUSADDR_MAIN, msg, sizeof(msg)); - // NOTE: the reply handler will automatically print the state once it's - // received } -void cmd_reset(char*) { +void cmd_skip(char*) { const char msg[] = { PB_CMD_WRITE, 0x00, - PB_GS_IDLE, + PB_GS_SOLVED, }; i2c_send(BUSADDR_MAIN, msg, sizeof(msg)); } diff --git a/client/cmd.h b/client/cmd.h index 932f3a2..7fefe98 100644 --- a/client/cmd.h +++ b/client/cmd.h @@ -2,53 +2,51 @@ #include <stddef.h> -typedef void cmd_fn_t(char *); +typedef void cmd_handle_t(char *); +typedef char** cmd_complete_t(const char*, int, int); struct cmd { - void (* handle)(char *); + cmd_handle_t * handle; const char* name; const char* info; - // TODO: tab completion function? + cmd_complete_t * complete; }; +typedef struct cmd cmd_t; -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; +cmd_handle_t cmd_exit; +cmd_handle_t cmd_test; +cmd_handle_t cmd_help; +cmd_complete_t cmd_help_complete; +cmd_handle_t cmd_reset; +cmd_handle_t cmd_ls; +cmd_handle_t cmd_send; +cmd_handle_t cmd_skip; -static const struct cmd cmds[] = { +static const cmd_t 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", + .info = "set game state to 'idle' for one or more puzzle modules", + }, + { + .handle = cmd_skip, + .name = "skip", + .info = "set game state to 'solved' for one or more puzzle modules", }, { .handle = cmd_ls, .name = "ls", - .info = "list connected puzzle modules", + .info = "list connected puzzle modules and their state", }, #ifdef DEBUG { @@ -56,6 +54,11 @@ static const struct cmd cmds[] = { .name = "send", .info = "[debug] send raw message", }, + { + .handle = cmd_test, + .name = "test", + .info = "[debug] send a test puzbus message", + }, #endif }; static const size_t cmds_length = sizeof(cmds) / sizeof(cmds[0]); diff --git a/client/parse.cpp b/client/parse.cpp index f31e802..16f0781 100644 --- a/client/parse.cpp +++ b/client/parse.cpp @@ -6,7 +6,6 @@ #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); @@ -21,15 +20,27 @@ static int parse_string(const char * str, char * data, size_t * offset) { default: return -i; } + char closing = str[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 (escape && c == '\\') { + char x = str[i + 1]; + if (x == '0') c = '\0'; + else if (x == 't') c = '\t'; + else if (x == 'n') c = '\n'; + else if (x == 'r') c = '\r'; + else if (x == '\\') c = '\\'; + else if (x == '\"') c = '\"'; + else if (x == '\'') c = '\''; + else break; + i++; + } + if (data != NULL) data[*offset] = c; } @@ -38,8 +49,7 @@ static int parse_string(const char * str, char * data, size_t * offset) { } static int parse_hexstr(const char * str, char * data, size_t * offset) { - const char* ifs = IFS; - size_t len = strcspn(str, ifs); + size_t len = strcspn(str, IFS); int i = 0; // check if token contains at least one colon @@ -74,8 +84,7 @@ static int parse_hexstr(const char * str, char * data, size_t * offset) { } static int parse_number(const char * str, char * data, size_t * offset) { - const char* ifs = IFS; - size_t len = strcspn(str, ifs); + size_t len = strcspn(str, IFS); int i = 0; int base = 10; bool bytestring = false; @@ -142,12 +151,11 @@ static int parse_number(const char * str, char * data, size_t * offset) { } 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 + 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; diff --git a/client/pbc.1 b/client/pbc.1 new file mode 100644 index 0000000..f5a2198 --- /dev/null +++ b/client/pbc.1 @@ -0,0 +1,94 @@ +\# vim: ft=groff +.de I2C +I\*{2\*}C +.. +.TH pbc 1 +.SH NAME +pbc \- puzzle box client +.SH SYNPOSIS +pbc <addr> [port] +.SH DESCRIPTION +Connect to a puzzle box at the IPv4 address specified by \fIaddr\fP and +optionally port specified by \fIport\fP. The default port is 9191. Once +connected, a +.MR readline 3 -based +CLI is started, and commands can be sent. +.SH COMMANDS +.TP +exit +Disconnect from the puzzle box and exit pbc. This command takes no arguments. +.TP +help +Print a list of available commands with descriptions. This command takes no +arguments. +.TP +ls +List all puzzle modules, their state, and the combined state of all puzzle +modules (global state of the main controller). +.TP +reset [mod ...] +Set the main controller or specific puzzle module's global state to \fIidle\fP. +If no modules are specified, the main controller's state is updated. One or +more modules can be specified to update them at once. +.TP +skip [mod ...] +Set the main controller or specific puzzle module's global state to +\fIsolved\fP. If no modules are specified, the main controller's state is +updated. One or more modules can be specified to update them at once. +.SH DEBUG COMMANDS +The commands detailed under this section are only available in version of pbc +compiled with debug support. +.TP +send <addr> <data> +Send arbitrary data specified by \fIdata\fP to the +.I2C +address specified by \fIaddr\fP. \fIdata\fP may consist of multiple arguments +separated by IFS, in which case the arguments are concatenated. +.TP +test +Send a test command containing the ASCII string "Hello world!" to +.I2C +address 0x39. This command takes no arguments. +.SH DATA FORMATS +.TP +number +Numbers can be specified as decimal or hexadecimal using a "0x" prefix. All +numbers are unsigned. Decimal literals are always cast to 8-bit integers, while +hexadecimal literals are cast to the smallest type that will fit the specified +number. Numbers are always sent as little endian. + +Examples: 0 123 255 0x10 0x1245 0xdeadBEEF +.TP +hexstr +Hexadecimal string literals are specified by hexadecimal bytes separated by +colons. Each byte must be exactly 2 hexadecimal characters long and followed by +a colon (except for the last byte). The minimum length of a hexstr is 2 bytes, +as it must include at least a single colon. + +Examples: de:ad:be:ef 00:00 +.TP +string +A string literal starts and ends with a single quote. All characters within +this literal are sent as-is, and no escaping is possible. + +Examples: 'Hello world!' 'string' ' hello ' + +When double quotes are used instead of single quotes, the following escape +sequences are recognised and replaced with special characters: + +\\0 -> 0x00 (null) +.br +\\t -> 0x09 (tab) +.br +\\n -> 0x0a (newline) +.br +\\r -> 0x0d (carriage return) +.br +\\\\ -> 0x5c (backslash) +.br +\\" -> 0x22 (double quote) +.br +\\' -> 0x27 (single quote) + +Examples: "Hello world!\\0" "foo\\nbar" + diff --git a/client/rl.cpp b/client/rl.cpp index 3f93e99..2fdd356 100644 --- a/client/rl.cpp +++ b/client/rl.cpp @@ -38,6 +38,7 @@ void rl_printf(const char *fmt, ...) { } static void cli_cmd(char* cmd) { + cmd += strspn(cmd, IFS); // skip leading whitespace char* line = consume_token(cmd, IFS); for (size_t i = 0; i < cmds_length; i++) { @@ -66,9 +67,31 @@ static char* rl_completion_entries(const char *text, int state) { return NULL; } +static char** rl_attempted_completion(const char * text, int start, int end) { + // do not suggest filenames + rl_attempted_completion_over = 1; + + // if first word in line buffer -> complete commands from cmds[] + size_t cmd_start = strspn(rl_line_buffer, IFS); + if (start == cmd_start) + return rl_completion_matches(text, rl_completion_entries); + + // else, check specialized completion functions + size_t cmd_len = strcspn(rl_line_buffer + cmd_start, IFS); + for (size_t i = 0; i < cmds_length; i++) { + cmd_t cmd = cmds[i]; + if (cmd.complete == NULL) continue; + if (strncmp(cmd.name, rl_line_buffer + cmd_start, cmd_len) != 0) continue; + return cmd.complete(rl_line_buffer, start, end); + } + + // else, no completion available + return NULL; +} + int cli_main() { char* input = NULL; - rl_completion_entry_function = rl_completion_entries; + rl_attempted_completion_function = rl_attempted_completion; while (1) { if (input != NULL) free(input); diff --git a/client/rl.h b/client/rl.h index 503225f..5e80d1a 100644 --- a/client/rl.h +++ b/client/rl.h @@ -6,5 +6,5 @@ #define CLI_PROMPT "(" COLOR_BOLD "pbc" COLOR_OFF ") " int cli_main(); -void rl_printf(const char *fmt, ...); +void rl_printf(const char * fmt, ...); diff --git a/shared/include.cmake b/shared/include.cmake new file mode 100644 index 0000000..f07a78b --- /dev/null +++ b/shared/include.cmake @@ -0,0 +1,5 @@ +include_directories(${CMAKE_CURRENT_LIST_DIR}) +add_library(puzbus STATIC + ${CMAKE_CURRENT_LIST_DIR}/pb/driver.c + ) + diff --git a/shared/pb/bus.h b/shared/pb/bus.h new file mode 100644 index 0000000..6f464c3 --- /dev/null +++ b/shared/pb/bus.h @@ -0,0 +1,18 @@ +#pragma once + +// Adafruit NeoTrellis modules +#define BUSADDR_ADA_NEO_1 0x2E +#define BUSADDR_ADA_NEO_2 0x2F +#define BUSADDR_ADA_NEO_3 0x30 +#define BUSADDR_ADA_NEO_4 0x32 + +// TODO: ??? +#define BUSADDR_MOD_NEOTRELLIS 0 +#define BUSADDR_MOD_SOFTWARE 0 +#define BUSADDR_MOD_HARDWARE 0 +#define BUSADDR_MOD_VAULT 0 +// #define BUSADDR_MOD_AUTOMATION 0 + +// main controller +#define BUSADDR_MAIN 0x00 + diff --git a/shared/pb/driver.c b/shared/pb/driver.c new file mode 100644 index 0000000..6b675ca --- /dev/null +++ b/shared/pb/driver.c @@ -0,0 +1,31 @@ +#include "types.h" +#include "driver.h" + +__weak bool pbdrv_hook_cmd() { + return false; +} + +__weak void pbdrv_i2c_recv(uint16_t addr, const char * buf, size_t sz) { + if (sz == 0) return; + pb_cmd_t cmd = (enum pb_cmd) buf[0]; + + // shift buffer pointer to only contain the puzzle bus message buf + buf++; + sz--; + + // allow user to override command handler while still using this weak + // function + if (pbdrv_hook_cmd(cmd, buf, sz)) return; + + switch (cmd) { + case PB_CMD_READ: return pbdrv_handle_read(buf, sz); + // case PB_CMD_WRITE: return pbdrv_handle_write(buf, sz); + // case PB_CMD_MAGIC: return pbdrv_handle_magic(buf, sz); + default: return; + } +} + +__weak void pbdrv_i2c_send(uint16_t addr, const char * buf, size_t sz) { + return; +} + diff --git a/shared/pb/driver.h b/shared/pb/driver.h new file mode 100644 index 0000000..b5b2784 --- /dev/null +++ b/shared/pb/driver.h @@ -0,0 +1,24 @@ +#pragma once + +#include <stdint.h> +#include <stddef.h> +#include <stdbool.h> + +#include "types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +void pbdrv_i2c_recv(uint16_t addr, const char * buf, size_t sz); +void pbdrv_i2c_send(uint16_t addr, const char * buf, size_t sz); + +void pbdrv_hook_state(pb_state_t * state, bool rw); +bool pbdrv_hook_cmd(pb_cmd_t cmd, const char * buf, size_t sz); + +void pbdrv_handle_read(const char * buf, size_t sz); + +#ifdef __cplusplus +} +#endif + diff --git a/shared/pb/types.h b/shared/pb/types.h new file mode 100644 index 0000000..93e2f0c --- /dev/null +++ b/shared/pb/types.h @@ -0,0 +1,58 @@ +#pragma once +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef __GNUC__ +#define __packed __attribute__((packed)) +#define __weak __attribute__((weak)) +#endif +#ifndef __packed +#error Could not determine packed attribute for current compiler +#define __packed +#endif +#ifndef __weak +#error Could not determine weak attribute for current compiler +#define __weak +#endif + +/** + * \brief puzzle bus command types + * + * The first byte of a puzzle bus message's data indicates the command type. + */ +enum __packed pb_cmd { + PB_CMD_READ, //!< read a puzzle module property + PB_CMD_WRITE, //!< write to a puzzle module property + // PB_CMD_UPDATE, //!< request an update + PB_CMD_MAGIC = 0x69, //!< magic message +}; +typedef enum pb_cmd pb_cmd_t; + +static const char pb_magic_msg[] = { 0x70, 0x75, 0x7a, 0x62, 0x75, 0x73 }; +static const char pb_magic_res[] = { 0x67, 0x61, 0x6d, 0x69, 0x6e, 0x67 }; + +/** \brief Puzzle bus global states */ +enum __packed pb_state { + PB_GS_NOINIT, //!< uninitialized (only used by puzzle modules) + PB_GS_IDLE, //!< puzzle not started yet + PB_GS_PLAYING, //!< puzzle actively being solved + PB_GS_SOLVED, //!< puzzle completed +}; +typedef enum pb_state pb_state_t; + +typedef struct __packed { + uint8_t address; +} pb_cmd_read_t; + +typedef struct __packed { + uint8_t address; + uint8_t data[]; +} pb_cmd_write_t; + +#ifdef __cplusplus +} +#endif + diff --git a/shared/puzbus.h b/shared/puzbus.h deleted file mode 100644 index 685fd6c..0000000 --- a/shared/puzbus.h +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once - -/** \file bus address reference */ - -// Adafruit NeoTrellis modules -#define BUSADDR_ADA_NEO_1 0x2E -#define BUSADDR_ADA_NEO_2 0x2F -#define BUSADDR_ADA_NEO_3 0x30 -#define BUSADDR_ADA_NEO_4 0x32 - -// TODO: ??? -#define BUSADDR_MOD_NEOTRELLIS 0 -#define BUSADDR_MOD_SOFTWARE 0 -#define BUSADDR_MOD_HARDWARE 0 -#define BUSADDR_MOD_VAULT 0 -#define BUSADDR_MOD_AUTOMATION 0 - -// main controller -#define BUSADDR_MAIN 0x00 - -/** - * \brief puzzle bus command types - * - * The first byte of a puzzle bus message's data indicates the command type. - */ -enum pb_cmd { - PB_CMD_READ, //!< read a puzzle module property - PB_CMD_WRITE, //!< write to a puzzle module property - PB_CMD_EXCHANGE, //!< state exchange command - // PB_CMD_UPDATE, //!< request an update - PB_CMD_MAGIC = 0x69, //!< magic message -}; -typedef enum pb_cmd pb_cmd_t; - -static const char pb_magic_msg[] = { 0x70, 0x75, 0x7a, 0x62, 0x75, 0x73 }; -static const char pb_magic_res[] = { 0x67, 0x61, 0x6d, 0x69, 0x6e, 0x67 }; - -/** \brief Puzzle bus global states */ -enum pb_state { - PB_STATE_NOINIT, //!< uninitialized (only used by puzzle modules) - PB_STATE_IDLE, //!< puzzle not started yet - PB_STATE_PLAYING, //!< puzzle actively being solved - PB_STATE_SOLVED, //!< puzzle completed -}; -typedef enum pb_state pb_state_t; - |