aboutsummaryrefslogtreecommitdiff
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/CMakeLists.txt2
-rw-r--r--client/cmd.cpp31
-rw-r--r--client/cmd.h49
-rw-r--r--client/parse.cpp26
-rw-r--r--client/pbc.194
-rw-r--r--client/rl.cpp25
-rw-r--r--client/rl.h2
7 files changed, 186 insertions, 43 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, ...);