aboutsummaryrefslogtreecommitdiff
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/CMakeLists.txt12
-rw-r--r--client/cmd.cpp82
-rw-r--r--client/cmd.h74
-rw-r--r--client/examples/puzbus-hello-world.cpp67
-rw-r--r--client/i2c.cpp67
-rw-r--r--client/i2c.h8
-rw-r--r--client/parse.cpp169
-rw-r--r--client/parse.h4
-rw-r--r--client/pbc.194
-rw-r--r--client/readme.md56
-rw-r--r--client/rl.cpp37
-rw-r--r--client/rl.h3
-rw-r--r--client/sock.cpp29
-rw-r--r--client/sock.h3
-rw-r--r--client/xxd.c44
-rw-r--r--client/xxd.h17
16 files changed, 565 insertions, 201 deletions
diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt
index 35a55b6..d838266 100644
--- a/client/CMakeLists.txt
+++ b/client/CMakeLists.txt
@@ -4,9 +4,14 @@ 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(../proto/include.cmake)
+include(../i2ctcp/include.cmake)
+include(../shared/include.cmake)
add_executable(pbc
main.cpp
@@ -14,12 +19,13 @@ add_executable(pbc
sock.cpp
cmd.cpp
parse.cpp
+ xxd.c
+ i2c.cpp
)
target_link_libraries(pbc
- puzbus
+ 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
index 1ec2cb8..5ac2ff3 100644
--- a/client/cmd.cpp
+++ b/client/cmd.cpp
@@ -1,11 +1,16 @@
#include <cstdio>
#include <cstdlib>
+#include <readline/readline.h>
#include <string.h>
#include "cmd.h"
-#include "sock.h"
+#include "pb/types.h"
+#include "rl.h"
+#include "i2c.h"
#include "parse.h"
+#include "pb/bus.h"
+
char* consume_token(char* input, const char* ifs) {
strtok(input, ifs);
return strtok(NULL, "\0");
@@ -32,11 +37,12 @@ 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"
);
}
-void cmd_send(char* addr_str) {
+void cmd_send(char * addr_str) {
char* data_str = consume_token(addr_str, IFS);
char* end;
@@ -55,9 +61,77 @@ void cmd_send(char* addr_str) {
return;
}
- // printf("(0x%02x) -> \"%.*s\"\n", addr, data_size, data);
+ 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_reset(char*) {
+ const char msg[] = {
+ PB_CMD_WRITE,
+ 0x00,
+ PB_GS_IDLE,
+ };
+ i2c_send(BUSADDR_MAIN, msg, sizeof(msg));
+}
+
+void cmd_skip(char*) {
+ const char msg[] = {
+ PB_CMD_WRITE,
+ 0x00,
+ PB_GS_SOLVED,
+ };
+ 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));
+}
+
+extern bool i2c_dump_send;
+extern bool i2c_dump_recv;
+const char * dump_modes[] = {
+ "none",
+ "send",
+ "recv",
+ "both",
+ NULL,
+};
+void cmd_dump(char * mode) {
+ consume_token(mode, IFS);
+ mode += strspn(mode, IFS);
+
+ for (int i = 0; dump_modes[i] != NULL; i++) {
+ if (strcmp(mode, dump_modes[i]) == 0) {
+ i2c_dump_send = (i >> 0) & 1;
+ i2c_dump_recv = (i >> 1) & 1;
+ return;
+ }
+ }
+
+ printf("mode \"%s\" unknown\n", mode);
+}
+char** cmd_dump_complete(const char * text, int begin, int end) {
+ int word = rl_word(rl_line_buffer, begin);
+ if (word != 1) return NULL;
+
+ return rl_completion_matches(text, [](const char * text, int state) -> char * {
+ static size_t i = 0;
+ if (state == 0) i = 0;
+
+ while (dump_modes[i] != NULL) {
+ const char * mode = dump_modes[i++];
+ if (strncmp(text, mode, strlen(text)) == 0)
+ return strdup(mode);
+ }
+ return NULL;
+ });
+
+ return NULL;
+}
diff --git a/client/cmd.h b/client/cmd.h
index 9d20328..961ef89 100644
--- a/client/cmd.h
+++ b/client/cmd.h
@@ -2,59 +2,71 @@
#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_send;
-cmd_fn_t cmd_status;
-cmd_fn_t cmd_reset;
-cmd_fn_t cmd_ls;
+cmd_handle_t cmd_exit;
+cmd_handle_t cmd_test;
+cmd_handle_t cmd_help;
+cmd_handle_t cmd_reset;
+cmd_handle_t cmd_ls;
+cmd_handle_t cmd_send;
+cmd_handle_t cmd_skip;
+cmd_handle_t cmd_dump;
+cmd_complete_t cmd_dump_complete;
-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_reset,
+ .name = "reset",
+ .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 and their state",
+ },
+#ifdef DEBUG
+ {
.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",
- // },
+ {
+ .handle = cmd_test,
+ .name = "test",
+ .info = "[debug] send a test puzbus message",
+ },
+ {
+ .handle = cmd_dump,
+ .name = "dump",
+ .info = "[debug] dump sent or received messages",
+ .complete = cmd_dump_complete,
+ },
+#endif
};
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
deleted file mode 100644
index dcc965b..0000000
--- a/client/examples/puzbus-hello-world.cpp
+++ /dev/null
@@ -1,67 +0,0 @@
-#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/i2c.cpp b/client/i2c.cpp
new file mode 100644
index 0000000..ee57e20
--- /dev/null
+++ b/client/i2c.cpp
@@ -0,0 +1,67 @@
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "i2ctcpv1.h"
+#include "sock.h"
+#include "xxd.h"
+
+#include "pb/bus.h"
+#include "pb/types.h"
+
+#include "pb/mod/main.h"
+
+bool i2c_dump_send = false;
+bool i2c_dump_recv = true;
+
+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);
+ if (i2c_dump_send) {
+ printf("[%s] addr(0x%02x) data(0x%02lx):\n", __FUNCTION__, addr, data_size);
+ xxd(data, data_size);
+ }
+
+ free(packed);
+}
+
+static void i2c_handle_cmd_read(uint16_t, const char *, size_t);
+
+void i2c_recv(uint16_t addr, const char * data, size_t data_size) {
+ if (i2c_dump_recv) {
+ printf("[%s] addr(0x%02x) data(0x%02lx):\n", __FUNCTION__, addr, data_size);
+ xxd(data, data_size);
+ }
+
+ if (data_size == 0) return;
+ enum pb_cmd cmd = (enum pb_cmd) data[0];
+ data++; data_size--;
+
+ switch (cmd) {
+ case PB_CMD_READ: return i2c_handle_cmd_read(addr, data, data_size);
+ default: return;
+ }
+}
+
+static void i2c_handle_cmd_read(uint16_t i2c_addr, const char * buf, size_t sz) {
+ if (sz < 2) return; // require data address + 1 byte of data
+ pb_cmd_read_t * cmd = (pb_cmd_read_t *) buf;
+ sz--; // sz now represents size of cmd->data
+
+ if (i2c_addr == BUSADDR_MAIN && cmd->address == 0x01) {
+ if (sz % 2 != 0) return; // invalid data
+ for (size_t offset = 0; offset < sz; offset += sizeof(pb_mod_main_mod_t)) {
+ pb_mod_main_mod_t * mod = (pb_mod_main_mod_t *) (cmd->data + offset);
+ printf("module at addr 0x%02x with state %d\n", mod->addr, mod->state);
+ }
+ }
+}
+
diff --git a/client/i2c.h b/client/i2c.h
new file mode 100644
index 0000000..f9f58f9
--- /dev/null
+++ b/client/i2c.h
@@ -0,0 +1,8 @@
+#pragma once
+
+#include <stdint.h>
+#include <stddef.h>
+
+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/parse.cpp b/client/parse.cpp
index 223dc5d..16f0781 100644
--- a/client/parse.cpp
+++ b/client/parse.cpp
@@ -1,14 +1,12 @@
-#include <math.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
+#include <netinet/in.h>
#include "parse.h"
-static int parse_str(const char* str, char* data, size_t* size) {
- char closing = str[0];
+static int parse_string(const char * str, char * data, size_t * offset) {
char escape = false;
- bool scan = data == NULL;
int i = 0;
size_t len = strlen(str);
@@ -22,34 +20,76 @@ static int parse_str(const char* str, char* data, size_t* size) {
default:
return -i;
}
+ char closing = str[i];
- for (i = 1; i < len && str[i] != '\0'; i++) {
+ for (i = 1; i < len && str[i] != '\0'; i++, *offset += 1) {
char c = str[i];
- if (c == closing) {
- if (scan) printf("string%s of length %d\n", escape ? " (w/ escape)" : "", i - 1);
+ 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 (scan) *size += 1;
+ if (data != NULL)
+ data[*offset] = c;
}
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;
+static int parse_hexstr(const char * str, char * data, size_t * offset) {
+ 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) {
+ size_t len = strcspn(str, IFS);
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
+ 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
@@ -60,58 +100,85 @@ static int parse_num(const char* str, char* data, size_t* size) {
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;
- }
+ if (base == 16) 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;
- }
- }
+ 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;
}
}
- if (scan) printf("number (base %d%s) of length %lu\n", base, bytestring ? " as bytestring" : "", len - i);
+ *offset += size;
return len;
}
-int strtodata(const char* str, char** data, size_t* size) {
- const char* ifs = IFS;
- *size = 0;
- size_t i;
+static int _strtodata_main(const char * str, char* data, size_t * offset) {
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;
+ 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_str(str + i, NULL, size)) > 0) { i += run; continue; }
- if ((run = parse_num(str + i, NULL, size)) > 0) { i += run; continue; }
+ 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;
}
- printf("end of string w/o parse errors\n");
- printf("buffer size is now %lu\n", *size);
- exit(0);
- *data = (char*) malloc(*size);
+ return i;
+}
- return 0;
+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
index 10274e7..94afe70 100644
--- a/client/parse.h
+++ b/client/parse.h
@@ -22,7 +22,7 @@
*
* \return the remaining data after \p token and the first \p ifs
*/
-char* consume_token(char* token, const char* ifs);
+char* consume_token(char * token, const char * ifs);
/**
* \brief convert string with literals into raw data
@@ -38,5 +38,5 @@ char* consume_token(char* token, const char* ifs);
* \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);
+int strtodata(const char * str, char ** data, size_t * size);
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/readme.md b/client/readme.md
index 04471d2..da48cf1 100644
--- a/client/readme.md
+++ b/client/readme.md
@@ -1,21 +1,47 @@
# 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
-```
+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.
+
+## WIP TODO
+
+- cleanup
+ - separate ../shared/pb/moddrv.c into a puzzle module specific and 'common' bit
+ - use the common bit in i2c.cpp instead
+ - cast to structs in ../shared/pb/moddrv.c
+- functionality
+ - print pretty tree of connected puzzle modules
+ - add enum to string functions in CLIENT ONLY
+
+## 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
```
-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)
+ 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
index 3f93e99..b8113aa 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(text, 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);
@@ -84,3 +107,15 @@ int cli_main() {
return EXIT_SUCCESS;
}
+int rl_word(const char * line, int cursor) {
+ int word = -1;
+ for (int i = 0; line[i] != '\0';) {
+ i += strspn(line + i, IFS);
+ int len = strcspn(line + i, IFS);
+ word++;
+ i += len;
+ if (i > cursor) break;
+ }
+ return word;
+}
+
diff --git a/client/rl.h b/client/rl.h
index 503225f..c3bf2c7 100644
--- a/client/rl.h
+++ b/client/rl.h
@@ -6,5 +6,6 @@
#define CLI_PROMPT "(" COLOR_BOLD "pbc" COLOR_OFF ") "
int cli_main();
-void rl_printf(const char *fmt, ...);
+void rl_printf(const char * fmt, ...);
+int rl_word(const char * line, int cursor);
diff --git a/client/sock.cpp b/client/sock.cpp
index f967f64..95a3685 100644
--- a/client/sock.cpp
+++ b/client/sock.cpp
@@ -7,12 +7,12 @@
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
-
#include <thread>
-#include "puzbusv1.h"
+#include "i2ctcpv1.h"
#include "sock.h"
#include "rl.h"
+#include "i2c.h"
using std::logic_error;
using std::thread;
@@ -72,7 +72,8 @@ void PBSocket::send(const char * buf, size_t buf_sz) {
}
void PBSocket::sock_task() {
- struct pb_msg input;
+ i2ctcp_msg_t input;
+ i2ctcp_read_reset(&input);
while(1) {
char buf[80];
@@ -86,11 +87,11 @@ void PBSocket::sock_task() {
// skip empty frames
if (bytes == 0) continue;
- int ret = pb_read(&input, buf, bytes);
+ int ret = i2ctcp_read(&input, buf, bytes);
// header read error
if (ret < 0) {
- rl_printf("pb_read error!\n");
+ rl_printf("i2ctcp_read error!\n");
break;
}
@@ -105,21 +106,3 @@ void PBSocket::sock_task() {
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
index 42eba3b..0dee09e 100644
--- a/client/sock.h
+++ b/client/sock.h
@@ -29,6 +29,3 @@ private:
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
+