aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomasintAnker <thomasintanker1@gmail.com>2024-05-31 15:06:06 +0200
committerThomasintAnker <thomasintanker1@gmail.com>2024-05-31 15:06:06 +0200
commit0278037aaf3fd497aae57d90f2638ceda3b12a6d (patch)
tree3354a14d4551b9524108d02971bfa42fc1ad4993
parentb865921e5dcf2ae2d6532b88eba1a0a49998eb27 (diff)
parent18d06c79b9f6a625eb218a15c8216556fb99dc02 (diff)
Merge branch 'wip/client' into wip/i2c-communication
-rw-r--r--.editorconfig12
-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
-rw-r--r--i2ctcp/i2ctcpv1.c60
-rw-r--r--i2ctcp/i2ctcpv1.h (renamed from proto/puzbusv1.h)23
-rw-r--r--i2ctcp/include.cmake (renamed from proto/include.cmake)7
l---------i2ctcp/lib (renamed from proto/lib)0
-rw-r--r--i2ctcp/readme.md25
-rw-r--r--main/CMakeLists.txt4
-rw-r--r--main/sock.c12
-rw-r--r--proto/puzbusv1.c55
-rw-r--r--puzzle/neo/arduino-neopuzzle/arduino-neopuzzle.ino95
-rw-r--r--puzzle/neo/console-neopuzzle/neo.cpp100
-rw-r--r--puzzle/vault/arduino-vaultpuzzle/arduino-vaultpuzzle.ino150
-rw-r--r--puzzle/vault/console-vaultpuzzle/vault.cpp130
-rw-r--r--readme.md55
-rw-r--r--shared/include.cmake5
-rw-r--r--shared/pb/bus.h18
-rw-r--r--shared/pb/mod/main.h13
-rw-r--r--shared/pb/moddrv.c118
-rw-r--r--shared/pb/moddrv.h62
-rw-r--r--shared/pb/spec.adoc133
-rw-r--r--shared/pb/types.h74
37 files changed, 1633 insertions, 284 deletions
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..cd37156
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,12 @@
+root = true
+
+[*]
+indent_style = tab
+end_of_line = lf
+insert_final_newline = true
+
+[*.md]
+indent_style = space
+indent_size = 2
+
+
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
+
diff --git a/i2ctcp/i2ctcpv1.c b/i2ctcp/i2ctcpv1.c
new file mode 100644
index 0000000..b406d8d
--- /dev/null
+++ b/i2ctcp/i2ctcpv1.c
@@ -0,0 +1,60 @@
+#include <mpack.h>
+#include <stdio.h>
+
+// MIN() macro
+#include <sys/param.h>
+
+#include "i2ctcpv1.h"
+
+int i2ctcp_read(i2ctcp_msg_t * target, const char * buf, size_t buf_sz) {
+ // at start of message
+ if (target->_rdata == 0) {
+ // NOTE: The entire start of a message needs to be readable from the buffer
+ // at this point. When target->addr can be read and target->length is past
+ // the end of the current buffer block, this function will crash and burn.
+ // This is a highly unlikely scenario, as i2ctcp_read is called for each
+ // chunk of a TCP frame, and frames (should) include only one puzzle bus
+ // message. The check here is kind of optional.
+ if (buf_sz < 4) return -1;
+
+ // mpack reader is used for the first buffer block, as it contains the data
+ // size info
+ mpack_reader_t reader;
+ mpack_reader_init_data(&reader, buf, buf_sz);
+
+ target->addr = mpack_expect_u16(&reader);
+ target->length = target->_rdata = mpack_expect_bin(&reader);
+ target->data = (char *) malloc(target->length);
+
+ // read remaining data in (header) packet
+ size_t to_read = mpack_reader_remaining(&reader, NULL);
+ mpack_read_bytes(&reader, target->data, to_read);
+ target->_rdata -= to_read;
+ } else {
+ // continue reading chunks of target->data until the amount of bytes
+ // specified in target->length
+ size_t to_read = MIN(buf_sz, target->_rdata);
+ char * data = target->data + target->length - target->_rdata;
+ memcpy(data, buf, to_read);
+ target->_rdata -= to_read;
+ }
+
+ // if rdata = 0, the message was completely read
+ return target->_rdata;
+}
+
+void i2ctcp_read_reset(i2ctcp_msg_t * target) {
+ target->_rdata = 0;
+}
+
+bool i2ctcp_write(const i2ctcp_msg_t * target, char ** buf, size_t * buf_sz) {
+ mpack_writer_t writer;
+ mpack_writer_init_growable(&writer, buf, buf_sz);
+
+ mpack_write_u16(&writer, target->addr);
+ mpack_write_bin(&writer, target->data, target->length);
+
+ // finish writing
+ return mpack_writer_destroy(&writer) == mpack_ok;
+}
+
diff --git a/proto/puzbusv1.h b/i2ctcp/i2ctcpv1.h
index 0985b2b..799b668 100644
--- a/proto/puzbusv1.h
+++ b/i2ctcp/i2ctcpv1.h
@@ -7,20 +7,21 @@
extern "C" {
#endif
-/** \brief Puzzle bus message (v1) */
-struct pb_msg {
+/** \brief I2C over TCP message (v1) */
+struct i2ctcp_msg {
uint16_t addr; //!< I^2^C address
char * data; //!< message content
size_t length; //!< message size
size_t _rdata; //!< \private remaining bytes to read until message is complete
};
+typedef struct i2ctcp_msg i2ctcp_msg_t;
/**
* \brief Read chunk of input stream, and store resulting message in \p target
*
* This function is called for each chunk of data from an input stream, and
* will parse the next puzzle bus message into \p target. The input stream is
- * assumed to only contain messages encoded by \p pb_write()
+ * assumed to only contain messages encoded by \p i2ctcp_write()
*
* \param target pointer to struct that will contain the finished message data
* \param buf pointer to input stream data chunk
@@ -34,24 +35,24 @@ struct pb_msg {
* the message is not fully parsed. This variable must be `free()`d by the
* caller after each complete message to prevent memory leaks.
*/
-int pb_read(struct pb_msg * target, const char * buf, size_t buf_sz);
+int i2ctcp_read(i2ctcp_msg_t * target, const char * buf, size_t buf_sz);
/**
* \brief reset the remaining message data counter
*
- * Calling this function has the effect of forcing \c pb_read() to parse the
- * next buffer chunk as the start of a new message. This function may be called
- * before reading a TCP frame's data to mitigate any synchronization issues
- * arising from earlier corrupt or otherwise malformed messages.
+ * Calling this function has the effect of forcing \c i2ctcp_read() to parse
+ * the next buffer chunk as the start of a new message. This function may be
+ * called before reading a TCP frame's data to mitigate any synchronization
+ * issues arising from earlier corrupt or otherwise malformed messages.
*/
-void pb_read_reset(struct pb_msg * target);
+void i2ctcp_read_reset(i2ctcp_msg_t * target);
/**
* \brief Allocate and write a msgpack-formatted message to \p buf
*
* This function allocates a buffer large enough to fit the message specified
* in \p target, and encodes the data in \p target in a format that can be
- * decoded later using \p pb_read()
+ * decoded later using \p i2ctcp_read()
*
* \param target pointer to struct that contains the message data
* \param buf pointer to \c char* that will contain the formatted message
@@ -62,7 +63,7 @@ void pb_read_reset(struct pb_msg * target);
*
* \note the pointer stored in \p buf must be `free()`d by the caller afterwards
*/
-bool pb_write(const struct pb_msg * target, char ** buf, size_t * buf_sz);
+bool i2ctcp_write(const i2ctcp_msg_t * target, char ** buf, size_t * buf_sz);
#ifdef __cplusplus
}
diff --git a/proto/include.cmake b/i2ctcp/include.cmake
index ac1305e..b61b2a4 100644
--- a/proto/include.cmake
+++ b/i2ctcp/include.cmake
@@ -1,6 +1,6 @@
include_directories(${CMAKE_CURRENT_LIST_DIR})
-add_library(puzbus STATIC
- ${CMAKE_CURRENT_LIST_DIR}/puzbusv1.c
+add_library(i2ctcp STATIC
+ ${CMAKE_CURRENT_LIST_DIR}/i2ctcpv1.c
)
# mpack
@@ -14,3 +14,6 @@ add_library(mpack STATIC
${CMAKE_CURRENT_LIST_DIR}/lib/mpack/src/mpack/mpack-writer.c
)
+# causes some wild crashes, please leave off
+add_compile_definitions(MPACK_READ_TRACKING=0)
+
diff --git a/proto/lib b/i2ctcp/lib
index dc598c5..dc598c5 120000
--- a/proto/lib
+++ b/i2ctcp/lib
diff --git a/i2ctcp/readme.md b/i2ctcp/readme.md
new file mode 100644
index 0000000..d5bfe6d
--- /dev/null
+++ b/i2ctcp/readme.md
@@ -0,0 +1,25 @@
+# i2ctcp (I<sup>2</sup>C over TCP)
+
+This folder includes protocol (de)serialization functions for sending and
+receiving I<sup>2</sup>C messages over TCP. These functions are used by the
+[main controller](../main) and the [puzzle box client (pbc)](../client). This
+folder does not include any puzzle bus specific code, and the headers for
+puzbus are in the [shared](../shared) folder instead.
+
+[MessagePack][msgpack] (specifically the [mpack][mpack] implementation) is used
+for the actual serialization/deserializtion, and the functions in this folder
+act as helpers for parsing from chunked data streams.
+
+To use these functions, include the following statement in your CMakeLists.txt:
+```cmake
+include(../i2ctcp/include.cmake)
+```
+
+The functions are available by `#include`ing the `i2ctcpv1.h` header, and are
+extensively documented using Doxygen-style comments.
+
+[msgpack]: https://msgpack.org/
+[mpack]: https://github.com/ludocode/mpack/
+
+
+
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index 30685a4..6390d7c 100644
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -7,7 +7,7 @@ set(PICO_BOARD pico_w)
include(lib/pico-sdk/pico_sdk_init.cmake)
include(lib/FreeRTOS-Kernel/portable/ThirdParty/GCC/RP2040/FreeRTOS_Kernel_import.cmake)
-include(../proto/include.cmake)
+include(../i2ctcp/include.cmake)
project(puzzlebox_main C CXX ASM)
@@ -33,7 +33,7 @@ target_link_libraries(main
hardware_i2c
FreeRTOS-Kernel
FreeRTOS-Kernel-Heap4
- puzbus
+ i2ctcp
mpack
)
diff --git a/main/sock.c b/main/sock.c
index 434694f..33da03c 100644
--- a/main/sock.c
+++ b/main/sock.c
@@ -10,18 +10,18 @@
#include "init.h"
#include "config.h"
-#include "puzbusv1.h"
+#include "i2ctcpv1.h"
#include "sock.h"
extern QueueHandle_t queue;
struct netconn* current_connection = NULL;
-struct pb_msg recv_msg;
+i2ctcp_msg_t recv_msg;
void i2c_send(uint16_t addr, const char * data, size_t data_size) {
if (current_connection == NULL) return;
- struct pb_msg send_msg = {
+ i2ctcp_msg_t send_msg = {
.addr = addr,
.data = (char *) data,
.length = data_size,
@@ -30,7 +30,7 @@ void i2c_send(uint16_t addr, const char * data, size_t data_size) {
char * buf;
size_t buf_sz;
- if (!pb_write(&send_msg, &buf, &buf_sz)) return;
+ if (!i2ctcp_write(&send_msg, &buf, &buf_sz)) return;
// NOTE: netconn does return an error code, but the data needs to be freed
// whether netconn throws an error or not, so it remains unused
@@ -62,7 +62,7 @@ void i2c_recv(uint16_t addr, const char * data, size_t data_size) {
}
void recv_handler(struct netconn* conn, struct netbuf* buf) {
- pb_read_reset(&recv_msg);
+ i2ctcp_read_reset(&recv_msg);
do {
char* data;
@@ -71,7 +71,7 @@ void recv_handler(struct netconn* conn, struct netbuf* buf) {
// continue early if more data is needed to complete message
printf("yeetus deletus defeatus");
- if (!pb_read(&recv_msg, data, len)) continue;
+ if (!i2ctcp_read(&recv_msg, data, len)) continue;
printf("yeetus deletus defeatus v2!");
// forward received message to puzzle bus
diff --git a/proto/puzbusv1.c b/proto/puzbusv1.c
deleted file mode 100644
index 73deda5..0000000
--- a/proto/puzbusv1.c
+++ /dev/null
@@ -1,55 +0,0 @@
-#include <mpack.h>
-#include <stdio.h>
-
-// MIN() macro
-#include <sys/param.h>
-// TODO: check if this works on pico as well
-
-#include "puzbusv1.h"
-
-int pb_read(struct pb_msg * target, const char * buf, size_t buf_sz) {
- // a new reader is used per buffer block passed to this function
- mpack_reader_t reader;
- mpack_reader_init_data(&reader, buf, buf_sz);
-
- // at start of message
- if (target->_rdata == 0) {
- // NOTE: The entire start of a message needs to be readable from the buffer
- // at this point. When target->addr can be read and target->length is past
- // the end of the current buffer block, this function will crash and burn.
- // This is a highly unlikely scenario, as pb_read is called for each chunk
- // of a TCP frame, and frames (should) include only one puzzle bus message.
- // The check here is kind of optional.
- if (buf_sz < 4) return -1;
-
- target->addr = mpack_expect_u16(&reader);
- target->length = target->_rdata = mpack_expect_bin(&reader);
- target->data = (char *) malloc(target->length);
- }
-
- // continue reading chunks of target->data until the amount of bytes
- // specified in target->length
- size_t to_read = MIN(mpack_reader_remaining(&reader, NULL), target->_rdata);
- char * data = target->data + target->length - target->_rdata;
- mpack_read_bytes(&reader, data, to_read);
- target->_rdata -= to_read;
-
- // if rdata = 0, the message was completely read
- return target->_rdata;
-}
-
-void pb_read_reset(struct pb_msg * target) {
- target->_rdata = 0;
-}
-
-bool pb_write(const struct pb_msg * target, char ** buf, size_t * buf_sz) {
- mpack_writer_t writer;
- mpack_writer_init_growable(&writer, buf, buf_sz);
-
- mpack_write_u16(&writer, target->addr);
- mpack_write_bin(&writer, target->data, target->length);
-
- // finish writing
- return mpack_writer_destroy(&writer) == mpack_ok;
-}
-
diff --git a/puzzle/neo/arduino-neopuzzle/arduino-neopuzzle.ino b/puzzle/neo/arduino-neopuzzle/arduino-neopuzzle.ino
new file mode 100644
index 0000000..b334677
--- /dev/null
+++ b/puzzle/neo/arduino-neopuzzle/arduino-neopuzzle.ino
@@ -0,0 +1,95 @@
+#include <Wire.h>
+#include <Adafruit_NeoTrellis.h>
+
+#define MATRIX_SIZE 8
+#define LED_COLOR_ON 0xFFFFFF // Color of the LEDs in ON state
+#define LED_COLOR_OFF 0x000000 // Color of the LEDs in OFF state
+
+Adafruit_NeoTrellis t_array[MATRIX_SIZE / 4][MATRIX_SIZE / 4] = {
+ {Adafruit_NeoTrellis(0x2E), Adafruit_NeoTrellis(0x2F)},
+ {Adafruit_NeoTrellis(0x30), Adafruit_NeoTrellis(0x32)}
+};
+
+Adafruit_MultiTrellis trellis((Adafruit_NeoTrellis *)t_array, MATRIX_SIZE / 4, MATRIX_SIZE / 4);
+
+bool neoMatrix[MATRIX_SIZE][MATRIX_SIZE]; // To track state of each pixel
+
+enum NeoState {
+ NEO_UNINITIALIZED,
+ NEO_PLAYING,
+ NEO_SOLVED
+};
+
+NeoState neoState = NEO_UNINITIALIZED;
+
+void setup() {
+ Serial.begin(115200);
+ while (!Serial); // Wait for Serial to be ready
+
+ if (!trellis.begin()) {
+ Serial.println("Failed to initialize NeoTrellis");
+ while (1) delay(1);
+ }
+
+ // Initialize the matrix with a checkerboard pattern
+ bool toggle = false;
+ for (int i = 0; i < MATRIX_SIZE; i++) {
+ for (int j = 0; j < MATRIX_SIZE; j++) {
+ neoMatrix[i][j] = toggle;
+ toggle = !toggle;
+ trellis.setPixelColor(i * MATRIX_SIZE + j, neoMatrix[i][j] ? LED_COLOR_ON : LED_COLOR_OFF);
+ }
+ toggle = !toggle;
+ }
+ trellis.show();
+ neoState = NEO_PLAYING;
+
+ // Register the callback for each key
+ for (int i = 0; i < MATRIX_SIZE * MATRIX_SIZE; i++) {
+ trellis.activateKey(i, SEESAW_KEYPAD_EDGE_RISING, true);
+ trellis.activateKey(i, SEESAW_KEYPAD_EDGE_FALLING, true);
+ trellis.registerCallback(i, buttonCallback);
+ }
+}
+
+void loop() {
+ trellis.read(); // Process button events
+ delay(20);
+}
+
+TrellisCallback buttonCallback(keyEvent evt) {
+ int x = evt.bit.NUM / MATRIX_SIZE;
+ int y = evt.bit.NUM % MATRIX_SIZE;
+
+ if (evt.bit.EDGE == SEESAW_KEYPAD_EDGE_RISING) {
+ toggleAdjacentLEDs(x, y);
+ trellis.show();
+ if (isNeoPuzzleSolved()) {
+ neoState = NEO_SOLVED;
+ Serial.println("The NeoTrellis puzzle is solved!");
+ }
+ }
+ return 0;
+}
+
+void toggleAdjacentLEDs(int x, int y) {
+ for (int dx = -1; dx <= 1; ++dx) {
+ for (int dy = -1; dy <= 1; ++dy) {
+ if (dx == 0 && dy == 0) continue; // Skip the center button itself
+ int nx = x + dx, ny = y + dy;
+ if (nx >= 0 && nx < MATRIX_SIZE && ny >= 0 && ny < MATRIX_SIZE) {
+ neoMatrix[nx][ny] = !neoMatrix[nx][ny];
+ trellis.setPixelColor(nx * MATRIX_SIZE + ny, neoMatrix[nx][ny] ? LED_COLOR_ON : LED_COLOR_OFF);
+ }
+ }
+ }
+}
+
+bool isNeoPuzzleSolved() {
+ for (int i = 0; i < MATRIX_SIZE; i++) {
+ for (int j = 0; j < MATRIX_SIZE; j++) {
+ if (neoMatrix[i][j]) return false; // If any LED is on, puzzle is not solved
+ }
+ }
+ return true;
+}
diff --git a/puzzle/neo/console-neopuzzle/neo.cpp b/puzzle/neo/console-neopuzzle/neo.cpp
new file mode 100644
index 0000000..56d90f7
--- /dev/null
+++ b/puzzle/neo/console-neopuzzle/neo.cpp
@@ -0,0 +1,100 @@
+#include <iostream>
+#include <array>
+
+#define MATRIX_SIZE 8
+
+enum NeoState {
+ NEO_UNINITIALIZED,
+ NEO_PLAYING,
+ NEO_SOLVED
+};
+
+// Simulate the 8x8 LED matrix with a 2D array
+std::array<std::array<bool, MATRIX_SIZE>, MATRIX_SIZE> neoMatrix;
+
+NeoState neoState = NEO_UNINITIALIZED;
+
+// Helper function to toggle LEDs if within bounds
+void toggleIfValid(int x, int y) {
+ if (x >= 0 && x < MATRIX_SIZE && y >= 0 && y < MATRIX_SIZE) {
+ neoMatrix[x][y] = !neoMatrix[x][y];
+ }
+}
+
+void initializeNeoMatrix() {
+ // The initial pattern from the Appendix A example (assuming red is 'true'/on and white is 'false'/off)
+ std::array<std::array<bool, MATRIX_SIZE>, MATRIX_SIZE> initialPattern = {{
+ {false, true, false, true, false, true, false, true},
+ {true, false, true, false, true, false, true, false},
+ {false, true, false, true, false, true, false, true},
+ {true, false, true, false, true, false, true, false},
+ {false, true, false, true, false, true, false, true},
+ {true, false, true, false, true, false, true, false},
+ {false, true, false, true, false, true, false, true},
+ {true, false, true, false, true, false, true, false}
+ }};
+
+ for (int i = 0; i < MATRIX_SIZE; i++) {
+ for (int j = 0; j < MATRIX_SIZE; j++) {
+ neoMatrix[i][j] = initialPattern[i][j];
+ }
+ }
+
+ neoState = NEO_PLAYING;
+}
+
+
+void printNeoMatrix() {
+ // Print the matrix state to the console
+ for (int i = 0; i < MATRIX_SIZE; i++) {
+ for (int j = 0; j < MATRIX_SIZE; j++) {
+ std::cout << (neoMatrix[i][j] ? 1 : 0) << " ";
+ }
+ std::cout << std::endl;
+ }
+}
+
+void toggleAdjacentLEDs(int x, int y) {
+ // Toggle the LED at (x, y) and adjacent LEDs
+ toggleIfValid(x, y); // Center
+ toggleIfValid(x - 1, y); // Up
+ toggleIfValid(x + 1, y); // Down
+ toggleIfValid(x, y - 1); // Left
+ toggleIfValid(x, y + 1); // Right
+}
+
+
+bool isNeoPuzzleSolved() {
+ for (int i = 0; i < MATRIX_SIZE; i++) {
+ for (int j = 0; j < MATRIX_SIZE; j++) {
+ if (neoMatrix[i][j]) return false; // If any LED is on, puzzle is not solved
+ }
+ }
+ return true;
+}
+
+/// Integration needed
+int main() {
+ initializeNeoMatrix();
+ printNeoMatrix();
+
+ while (neoState != NEO_SOLVED) {
+ int x, y;
+ std::cout << "Enter the coordinates of the button pressed (x y): ";
+ std::cin >> x >> y;
+
+ if (x >= 0 && x < MATRIX_SIZE && y >= 0 && y < MATRIX_SIZE) {
+ toggleAdjacentLEDs(x, y);
+ printNeoMatrix();
+
+ if (isNeoPuzzleSolved()) {
+ neoState = NEO_SOLVED;
+ std::cout << "The NeoTrellis puzzle is solved!\n";
+ }
+ } else {
+ std::cout << "Invalid coordinates. Please enter values between 0 and " << MATRIX_SIZE - 1 << ".\n";
+ }
+ }
+
+ return 0;
+}
diff --git a/puzzle/vault/arduino-vaultpuzzle/arduino-vaultpuzzle.ino b/puzzle/vault/arduino-vaultpuzzle/arduino-vaultpuzzle.ino
new file mode 100644
index 0000000..4dd8ac8
--- /dev/null
+++ b/puzzle/vault/arduino-vaultpuzzle/arduino-vaultpuzzle.ino
@@ -0,0 +1,150 @@
+#include <Wire.h>
+#include <TM1637Display.h>
+
+// Definitions for GPIO numbers, change these according to your hardware setup
+#define TOTAL_LEVELS 5
+#define TAG "VaultPuzzle"
+
+// Key Matrix Pin Configuration
+#define ROWS 4
+#define COLS 3
+
+// Module connection pins (Digital Pins for TM1637)
+#define CLK 2
+#define DIO 3
+
+// Pin to indicate puzzle solved state
+#define SOLVED_PIN 53
+
+// Initialize the TM1637 display
+TM1637Display display(CLK, DIO);
+
+//TODO Update these pin numbers based on your Arduino setup
+const int ROW_PINS[ROWS] = {7, 6, 5, 4};
+const int COL_PINS[COLS] = {10, 9, 8};
+
+typedef enum {
+ STATE_UNINITIALIZED = 0x00,
+ STATE_RESET = 0x01,
+ STATE_PLAYING = 0x02,
+ STATE_SOLVED = 0x03,
+ STATE_ERROR = 0x04
+} PuzzleState;
+
+const char* validButtons[TOTAL_LEVELS] = {"A2", "B1", "D3", "C2", "C1"};
+PuzzleState puzzleState = STATE_UNINITIALIZED;
+int currentLevel = 0;
+
+// Function prototypes
+void display_code(int level);
+void initialize_system();
+void check_button_press();
+void update_state_after_button_press(bool validPress);
+void play_error_sound();
+void blink_display();
+
+void setup() {
+ Serial.begin(115200); // Initialize default Serial for debug messages
+ pinMode(SOLVED_PIN, OUTPUT); // Initialize the solved indicator pin
+ digitalWrite(SOLVED_PIN, LOW); // Start with the solved pin LOW
+
+ display.setBrightness(0x0f); // Set the brightness of the TM1637 display
+ initialize_system();
+ Serial.println("GPIO and display initialized.");
+
+ // Test to light up all segments
+ uint8_t allOn[] = {0xFF, 0xFF, 0xFF, 0xFF}; // All segments on
+ display.setSegments(allOn);
+ delay(2000); // Keep it on for 2 seconds before proceeding
+
+ // Initialize the game
+ if (true) { // Simulating isVaultClosed
+ puzzleState = STATE_RESET;
+ currentLevel = 0;
+ display_code(currentLevel);
+ } else {
+ Serial.println("Vault door is open. Please close the door to start the puzzle.");
+ }
+}
+
+void initialize_system() {
+ // Configure the rows as input with pull-up
+ for (int i = 0; i < ROWS; i++) {
+ pinMode(ROW_PINS[i], INPUT_PULLUP);
+ }
+
+ // Configure the columns as output
+ for (int i = 0; i < COLS; i++) {
+ pinMode(COL_PINS[i], OUTPUT);
+ digitalWrite(COL_PINS[i], HIGH);
+ }
+}
+
+void loop() {
+ while (puzzleState != STATE_SOLVED) {
+ check_button_press();
+ delay(100); // Non-blocking delay
+ }
+ // When puzzle is solved, you might want to display a final message and set the solved pin high
+ if (puzzleState == STATE_SOLVED) {
+ digitalWrite(SOLVED_PIN, HIGH); // Set the solved pin high
+ display.showNumberDec(currentLevel, true); // Show final level or a special message
+ Serial.println("Final display shown. Puzzle complete.");
+ while (1) { delay(1000); } // Hold on the final display
+ }
+}
+
+void display_code(int level) {
+ Serial.print("Displaying code for level "); Serial.println(level);
+ // Display the level on the TM1637 4-digit 7-segment display
+ display.showNumberDec(level, true); // True to show leading zeros
+ Serial.print("Code for level "); Serial.print(level); Serial.println(" displayed successfully.");
+}
+
+void check_button_press() {
+ char keyPress[3] = {0};
+ for (int col = 0; col < COLS; col++) {
+ digitalWrite(COL_PINS[col], LOW); // Activate column
+ for (int row = 0; row < ROWS; row++) {
+ if (digitalRead(ROW_PINS[row]) == LOW) { // Detect if any row is activated
+ delay(50); // Debounce delay
+ if (digitalRead(ROW_PINS[row]) == LOW) { // Confirm the button is still pressed
+ keyPress[0] = 'A' + row;
+ keyPress[1] = '1' + col;
+ keyPress[2] = '\0';
+ Serial.print("Keypress detected: "); Serial.println(keyPress);
+ if (strcmp(keyPress, validButtons[currentLevel]) == 0) {
+ currentLevel++;
+ if (currentLevel >= TOTAL_LEVELS) {
+ puzzleState = STATE_SOLVED;
+ Serial.println("Puzzle solved!");
+ display.showNumberDec(currentLevel + 1, true); // Display the final level
+ digitalWrite(SOLVED_PIN, HIGH); // Set the solved pin high
+ } else {
+ puzzleState = STATE_PLAYING;
+ display_code(currentLevel);
+ }
+ } else {
+ play_error_sound();
+ blink_display();
+ puzzleState = STATE_ERROR;
+ currentLevel = 0;
+ display_code(currentLevel);
+ }
+ while (digitalRead(ROW_PINS[row]) == LOW) {} // Wait for release
+ }
+ }
+ }
+ digitalWrite(COL_PINS[col], HIGH); // Deactivate column
+ }
+}
+
+void play_error_sound() {
+ // Simulate error sound - connect a buzzer to play actual sound
+ Serial.println("Playing error sound.");
+}
+
+void blink_display() {
+ // Simulate blinking the display - use LEDs or other methods to show visual feedback
+ Serial.println("7-segment display is blinking to indicate an error.");
+}
diff --git a/puzzle/vault/console-vaultpuzzle/vault.cpp b/puzzle/vault/console-vaultpuzzle/vault.cpp
new file mode 100644
index 0000000..3566b3e
--- /dev/null
+++ b/puzzle/vault/console-vaultpuzzle/vault.cpp
@@ -0,0 +1,130 @@
+#include <iostream>
+#include <string>
+#include <array>
+
+// Definitions for puzzle requirements
+constexpr int TOTAL_LEVELS = 5;
+
+// Enumeration for the states of the puzzle
+enum PuzzleState {
+ STATE_UNINITIALIZED,
+ STATE_RESET,
+ STATE_PLAYING,
+ STATE_SOLVED,
+ STATE_ERROR
+};
+
+// This array maps each level to the correct button press
+const std::array<std::string, TOTAL_LEVELS> validButtons = {"A3", "F1", "U4", "C2", "L1"};
+
+PuzzleState puzzleState = STATE_UNINITIALIZED;
+int currentLevel = 0;
+
+// Function prototypes
+void displayCode(int level);
+void sendI2CUpdate(PuzzleState state);
+
+// Simulate sending an I2C update
+void sendI2CUpdate(PuzzleState state) {
+ std::cout << "Sending state " << state << " to main controller via I2C.\n";
+}
+
+// Simulate checking if the vault door is closed
+bool isVaultClosed() {
+ return true; // Return true if the door sensor indicates closed
+}
+
+// Function to display a code on the 7-segment display
+void displayCode(int level) {
+ std::cout << "Displaying code for level " << level << " on the 7-segment display.\n";
+}
+
+// Function to initialize the puzzle
+void initializePuzzle() {
+ if (isVaultClosed()) {
+ puzzleState = STATE_RESET;
+ currentLevel = 1; // Start at level 1
+ std::cout << "Puzzle initialized. Starting at level " << currentLevel << ".\n";
+ displayCode(currentLevel); // Show the first code
+ } else {
+ std::cout << "Vault door is open. Please close the door to start the puzzle.\n";
+ }
+}
+
+// Function to lock the vault
+void lockVault() {
+ std::cout << "Vault locked.\n";
+}
+
+// Function to unlock the vault
+void unlockVault() {
+ std::cout << "Vault unlocked!\n";
+}
+
+// Function to simulate the buzzer sound
+void playErrorSound() {
+ std::cout << "Playing error sound.\n";
+}
+
+// Function to simulate blinking the 7-segment display
+void blinkDisplay() {
+ std::cout << "7-segment display is blinking to indicate an error.\n";
+}
+
+// Validate the button press for the current level
+bool isValidButtonPress(const std::string& button, int level) {
+ return button == validButtons[level - 1];
+}
+
+// Function to update the state of the puzzle based on the current level
+void updateStateAfterButtonPress(bool validPress) {
+ if (validPress) {
+ if (currentLevel >= TOTAL_LEVELS) {
+ puzzleState = STATE_SOLVED;
+ unlockVault();
+ } else {
+ puzzleState = STATE_PLAYING;
+ displayCode(currentLevel);
+ }
+ } else {
+ puzzleState = STATE_ERROR;
+ playErrorSound();
+ blinkDisplay();
+ lockVault();
+ currentLevel = 1; // Reset to level 1
+ displayCode(currentLevel);
+ }
+ sendI2CUpdate(puzzleState); // Notify main controller of the state change
+}
+
+int main() {
+ initializePuzzle();
+
+ std::string buttonInput;
+
+ while (puzzleState != STATE_SOLVED) {
+ std::cout << "Enter the button pressed for level " << currentLevel << " (format Xn, e.g., A3): ";
+ std::getline(std::cin, buttonInput);
+
+ if (!buttonInput.empty() && isValidButtonPress(buttonInput, currentLevel)) {
+ currentLevel++;
+ if (currentLevel > TOTAL_LEVELS) {
+ puzzleState = STATE_SOLVED;
+ unlockVault();
+ std::cout << "The puzzle is solved and the vault is open!\n";
+ } else {
+ displayCode(currentLevel);
+ }
+ } else {
+ playErrorSound();
+ blinkDisplay();
+ lockVault();
+ puzzleState = STATE_RESET;
+ currentLevel = 1;
+ displayCode(currentLevel);
+ }
+ sendI2CUpdate(puzzleState);
+ }
+
+ return 0;
+}
diff --git a/readme.md b/readme.md
index 1ad72e9..ac703b7 100644
--- a/readme.md
+++ b/readme.md
@@ -1,8 +1,43 @@
-# puzzle box
+# Puzzle box
-Avans University of Applied Sciences project puzzle box.
+This repository contains the source code for the puzzle framework designed and
+implemented during the 2023-2024 run of the Puzzlebox project. This year's run
+of the project consists of only software students, and was developed using the
+hardware from the 21-22 run of the project.
-## submodules
+Improved hardware was designed but not realised during the 22-23 run of the
+project. This hardware is recommended for future groups participating in the
+project. The software in this repository should be easily portable to various
+other microcontrollers, and a recommendation is made in the [design
+document](docs/design.adoc).
+
+## Tidyness
+
+Please keep this repository tidy by being aware of the following conventions!
+
+### Folder structure
+
+|folder|contains|
+|-|-|
+|`/client`|Desktop PC application for controlling the puzzle box
+|`/docs`|Project documentation in AsciiDoc(tor) format
+|`/i2ctcp`|I<sup>2</sup>C over TCP protocol functions (used by main and client)
+|`/lib`|Libraries (tracked as [submodules](#submodules))
+|`/main`|Main controller (RPi pico) software
+|`/puzzle/<name>`|Puzzle sources, each puzzle has its own subdirectory
+|`/shared`|Shared code
+|`/test`|Unit test framework (currently unutilized)
+
+### Code style
+
+An `.editorconfig` file is provided in this repository. Please install the
+[EditorConfig](https://editorconfig.org/) plugin for your text editor of choice
+to automatically use these.
+
+Currently, no linter/formatter is configured for maintaining consistent code
+style.
+
+## Submodules
This repository tracks (most) dependencies via git submodules.
@@ -14,6 +49,7 @@ git submodule update --init --recursive --depth 1
until your problems go away.
+<!--
## Tests
```
@@ -23,10 +59,15 @@ cmake ..
make
make test
```
+-->
+
+## ESP SDK setup
-## ESP
-1. Install ESP-IDF extension in vscode
+1. Install ESP-IDF extension in Visual Studio Code
2. Install using 'express' option
3. Install ESP-IDF v5.2.1 (release version)
-4. For windows: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/get-started/windows-setup.html#get-started-windows-first-steps
-5. For Linux: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/get-started/linux-macos-setup.html#get-started-linux-macos-first-steps
+
+ Additional help:
+ - [For windows](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/get-started/windows-setup.html#get-started-windows-first-steps)
+ - [For Linux](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/get-started/linux-macos-setup.html#get-started-linux-macos-first-steps)
+
diff --git a/shared/include.cmake b/shared/include.cmake
new file mode 100644
index 0000000..c4b01c2
--- /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/moddrv.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/mod/main.h b/shared/pb/mod/main.h
new file mode 100644
index 0000000..56ccd3d
--- /dev/null
+++ b/shared/pb/mod/main.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "../types.h"
+
+typedef struct __packed {
+ const uint8_t addr;
+ const enum pb_state state;
+} pb_mod_main_mod_t;
+
+enum __packed {
+ PB_MOD_MAIN_ADDR_MODS = 0x01, //!< connected puzzle modules
+};
+
diff --git a/shared/pb/moddrv.c b/shared/pb/moddrv.c
new file mode 100644
index 0000000..1f7fab8
--- /dev/null
+++ b/shared/pb/moddrv.c
@@ -0,0 +1,118 @@
+#include <memory.h>
+
+#include "types.h"
+#include "moddrv.h"
+
+/** \brief [private] placeholder global state variable */
+static enum pb_state _global_state = PB_GS_NOINIT;
+
+/** \brief [private] main controller global state */
+static enum pb_state _main_state = PB_GS_NOINIT;
+
+__weak enum pb_state pbdrv_hook_mod_state_read() {
+ return _global_state;
+}
+
+__weak void pbdrv_hook_mod_state_write(enum pb_state state) {
+ _global_state = state;
+}
+
+__weak void pbdrv_i2c_recv(uint16_t i2c_addr, const char * buf, size_t sz) {
+ if (sz == 0) return;
+ enum pb_cmd cmd = (enum pb_cmd) buf[0];
+
+ // shift buffer pointer to only contain the puzzle bus message buf
+ buf++;
+ sz--;
+
+ // allow user to implement custom commands
+ if (pbdrv_hook_cmd(i2c_addr, cmd, buf, sz))
+ return;
+
+ switch (cmd) {
+ case PB_CMD_READ: return pbdrv_handle_read(i2c_addr, buf, sz);
+ case PB_CMD_WRITE: return pbdrv_handle_write(i2c_addr, buf, sz);
+ case PB_CMD_MAGIC: return pbdrv_handle_magic(i2c_addr, buf, sz);
+ case PB_CMD_SEX: return pbdrv_handle_sex(i2c_addr, buf, sz);
+ default: return;
+ }
+}
+
+__weak void pbdrv_handle_read(uint16_t i2c_addr, const char * buf, size_t sz) {
+ if (sz == 0) return;
+ pb_cmd_read_t * cmd = (pb_cmd_read_t *) buf;
+
+ // allow user to addrimplement custom read handlers
+ if (pbdrv_hook_read(i2c_addr, cmd->address))
+ return;
+
+ switch (cmd->address) {
+ case PB_ADDR_GS: {
+ char res[] = {
+ PB_CMD_READ,
+ PB_ADDR_GS,
+ pbdrv_hook_mod_state_read(),
+ };
+ return pbdrv_i2c_send(i2c_addr, res, sizeof(res));
+ }
+ default: return;
+ }
+}
+
+__weak void pbdrv_handle_write(uint16_t i2c_addr, const char * buf, size_t sz) {
+ if (sz < 2) return; // must have address and at least 1 byte data
+ pb_cmd_write_t * cmd = (pb_cmd_write_t *) buf;
+
+ // allow user to implement custom read handlers
+ if (pbdrv_hook_write(i2c_addr, cmd->address, (char *) cmd->data, sz - 1))
+ return;
+
+ switch (cmd->address) {
+ case PB_ADDR_GS:
+ pbdrv_hook_mod_state_write(cmd->data[0]);
+ break;
+ default: return;
+ }
+}
+
+__weak void pbdrv_handle_magic(uint16_t i2c_addr, const char * buf, size_t sz) {
+ if (sz != sizeof(pb_magic_msg)) return;
+ if (memcmp(buf, pb_magic_msg, sizeof(pb_magic_msg)) != 0) return;
+
+ size_t res_size = sizeof(pb_cmd_t) + sizeof(pb_magic_res);
+ char res[res_size];
+ res[0] = PB_CMD_MAGIC;
+ memcpy(res, pb_magic_res, sizeof(pb_magic_res));
+
+ pbdrv_i2c_send(i2c_addr, res, res_size);
+}
+
+__weak void pbdrv_handle_sex(uint16_t i2c_addr, const char * buf, size_t sz) {
+ if (sz == 0) return;
+ pb_cmd_sex_t * cmd = (pb_cmd_sex_t *) buf;
+
+ // send own state
+ char res[] = {
+ PB_CMD_SEX,
+ pbdrv_hook_mod_state_read(),
+ };
+ pbdrv_i2c_send(i2c_addr, res, sizeof(res));
+
+ if (cmd->main_state == _main_state) return;
+ // keep main controller state
+ _main_state = cmd->main_state;
+ // call update if main state changed
+ pbdrv_hook_main_state_update(_main_state);
+}
+
+__weak void pbdrv_hook_main_state_update(enum pb_state state) { }
+__weak bool pbdrv_hook_cmd(uint16_t i2c_addr, enum pb_state cmd, const char * buf, size_t sz) {
+ return false;
+}
+__weak bool pbdrv_hook_read(uint16_t i2c_addr, uint8_t addr) {
+ return false;
+}
+__weak bool pbdrv_hook_write(uint16_t i2c_addr, uint8_t addr, const char * buf, size_t sz) {
+ return false;
+}
+
diff --git a/shared/pb/moddrv.h b/shared/pb/moddrv.h
new file mode 100644
index 0000000..ecfc13a
--- /dev/null
+++ b/shared/pb/moddrv.h
@@ -0,0 +1,62 @@
+#pragma once
+
+/**
+ * \file puzzle bus driver implementation
+ *
+ * Most \c pbdrv_* functions have a weak implementation, which may be
+ * overwritten by a custom implementation. This allows you to use the default
+ * implementation where possible, and only implement extensions required for
+ * your puzzle module. Please see spec.adoc for more information about how to
+ * use the puzzle bus driver library.
+ */
+
+#include <stdint.h>
+#include <stddef.h>
+#include <stdbool.h>
+
+#include "types.h"
+
+#ifndef PBDRV_MOD_NAME
+#define PBDRV_MOD_NAME "???"
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void pbdrv_i2c_recv(uint16_t i2c_addr, const char * buf, size_t sz);
+void pbdrv_i2c_send(uint16_t i2c_addr, const char * buf, size_t sz);
+
+enum pb_state pbdrv_hook_mod_state_read();
+void pbdrv_hook_mod_state_write(enum pb_state state);
+void pbdrv_hook_main_state_update(enum pb_state state);
+
+/**
+ * \name hooks
+ *
+ * Implementing this function allows you to use the weak implementation of \c
+ * pbdrv_i2c_recv() while being able to implement custom command handlers.
+ *
+ * \return true if the cmd was recognized, or false to forward the command to
+ * the default handlers
+ *
+ * \{
+ */
+
+/** \brief cmd receive hook */
+bool pbdrv_hook_cmd(uint16_t i2c_addr, enum pb_state cmd, const char * buf, size_t sz);
+/** \brief read cmd hook */
+bool pbdrv_hook_read(uint16_t i2c_addr, uint8_t addr);
+/** \brief write cmd hook */
+bool pbdrv_hook_write(uint16_t i2c_addr, uint8_t addr, const char * buf, size_t sz);
+//! \}
+
+void pbdrv_handle_read(uint16_t i2c_addr, const char * buf, size_t sz);
+void pbdrv_handle_write(uint16_t i2c_addr, const char * buf, size_t sz);
+void pbdrv_handle_magic(uint16_t i2c_addr, const char * buf, size_t sz);
+void pbdrv_handle_sex(uint16_t i2c_addr, const char * buf, size_t sz);
+
+#ifdef __cplusplus
+}
+#endif
+
diff --git a/shared/pb/spec.adoc b/shared/pb/spec.adoc
new file mode 100644
index 0000000..3172e84
--- /dev/null
+++ b/shared/pb/spec.adoc
@@ -0,0 +1,133 @@
+= Puzzle module specification
+
+This folder contains an implementation of the puzzle bus protocol
+specification, and is targeted at puzzle module developers. This document
+describes the required implementation steps for integrating a new game into the
+puzzle module framework.
+
+== The bus
+
+The puzzle bus carries data over a standard I^2^C bus. Additional details about
+this bus can be found in the link:../../docs/design.adoc[Design document].
+
+The following details are important to puzzle module developers, as they may
+cause unexpected behavior:
+
+- *Addresses influence the puzzle box's behavior*. The order of puzzles is
+ determined by the puzzle module address. Two puzzle modules may use the same
+ address, but this will mean that they cannot be used simultaniously in the
+ same puzzle box. Known addresses are documented in link:bus.h[].
+- *The read/write bit of an I^2^C frame determines how it's handled*. I^2^C
+ *read* frames are treated as requests, while *write* frames are treated as
+ responses.
+
+== Puzzle bus driver (pbdrv)
+
+The library in this folder is a partial implementation of the puzzle bus
+specification *for puzzle modules*. Most functions in the driver are marked
+with the 'weak' attribute, which allows you to override them by providing an
+implementation.
+
+In order to utilize this driver, the following must be done:
+
+- The ``pbdrv_i2c_recv`` function must be *called* for every received *I^2^C
+ read* frame
+- The ``pbdrv_i2c_send`` function must be *implemented* with the
+ platform-specific *I^2^C write* function
+
+This is enough to get the puzzle module registered. You may also want to
+implement some of the following integrations:
+
+- If your game uses the global state variable, you should implement the
+ <<sec:state-global,global state hooks>> to point the driver to your own
+ global state variable, and be notified of reads/writes to it.
+- If you want to expose additional game state variables over the puzzle bus,
+ you should implement the <<sec:state-aux,auxiliary state hooks>>.
+- If you want to implement custom puzzle bus commands, you can implement the
+ <<sec:cmd,command hook>>.
+
+All other kinds of integrations/hooks can likely be realized by overriding the
+default implementations, but this is discouraged.
+
+[[sec:state-global]]
+== Global state
+
+If your puzzle module defines its own global ``enum pb_state``, you can tell
+the driver to use it by implementing the ``pbdrv_hook_state_read`` and
+``pbdrv_hook_state_write`` functions. These functions are also used by the
+default implementation of the read/write commands to address 0 (global state).
+
+Example:
+
+```c
+pb_state_t global_state = PB_GS_NOINIT;
+
+pb_state_t pbdrv_hook_mod_state_read() {
+ return global_state;
+}
+
+void pbdrv_hook_mod_state_write(pb_state_t state) {
+ global_state = state;
+}
+```
+
+[[sec:state-aux]]
+== Auxiliary state
+
+You can expose additional state variables by implementing the
+``pbdrv_hook_read`` and ``pbdrv_hook_write`` functions. These functions should
+return ``true`` for state addresses you want to override.
+
+Example:
+
+```c
+#define CUSTOM_VAR_ADDR 0x01
+uint8_t my_custom_variable = 10;
+
+bool pbdrv_hook_read(uint16_t i2c_addr, uint8_t addr) {
+ switch (addr) {
+ case CUSTOM_VAR_ADDR: {
+ char res[] = { PB_CMD_READ, addr, my_custom_variable };
+ pbdrv_i2c_send(i2c_addr, res, sizeof(res));
+ break;
+ }
+ default: return false;
+ }
+
+ return true;
+}
+
+bool pbdrv_hook_write(uint16_t i2c_addr, uint8_t addr, const char * buf, size_t sz) {
+ switch (addr) {
+ case CUSTOM_VAR_ADDR: {
+ if (sz != 1) return false;
+ my_custom_variable = buf[0];
+ break;
+ }
+ default: return false;
+ }
+
+ return true;
+}
+```
+
+[[sec:cmd]]
+== Custom commands
+
+Similar to the auxiliary state, custom commands can be added by implementing
+the ``pbdrv_hook_cmd`` function, which should return ``true`` for the
+command(s) that you want to overwrite.
+
+Example:
+
+```c
+bool pbdrv_hook_cmd(uint16_t i2c_addr, enum pb_cmd cmd, const char * buf, size_t sz) {
+ if (cmd == 0x54) {
+ printf("custom command received!\n");
+ return true;
+ }
+
+ return false;
+}
+```
+
diff --git a/shared/pb/types.h b/shared/pb/types.h
new file mode 100644
index 0000000..f2e2078
--- /dev/null
+++ b/shared/pb/types.h
@@ -0,0 +1,74 @@
+#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_SEX, //!< state exchange
+ PB_CMD_MAGIC, //!< magic message
+};
+// typedef enum pb_cmd pb_cmd_t;
+
+/** \brief magic sent from main controller to puzzle module */
+static const char pb_magic_msg[] = { 0x70, 0x75, 0x7a, 0x62, 0x75, 0x73 };
+/** \brief magic reply from puzzle module back to main controller */
+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 {
+ const enum pb_cmd cmd;
+ const uint8_t data[];
+} pb_cmd_t;
+
+typedef struct __packed {
+ const uint8_t address;
+ const uint8_t data[];
+} pb_cmd_read_t;
+
+typedef struct __packed {
+ const uint8_t address;
+ const uint8_t data[];
+} pb_cmd_write_t;
+
+typedef struct __packed {
+ const enum pb_state main_state;
+} pb_cmd_sex_t;
+
+enum __packed {
+ PB_ADDR_GS = 0x00, //!< global state address
+};
+
+#ifdef __cplusplus
+}
+#endif
+