aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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
-rw-r--r--shared/include.cmake5
-rw-r--r--shared/pb/bus.h18
-rw-r--r--shared/pb/driver.c31
-rw-r--r--shared/pb/driver.h24
-rw-r--r--shared/pb/types.h58
-rw-r--r--shared/puzbus.h46
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;
-