aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.editorconfig12
-rw-r--r--.gitignore3
-rw-r--r--.gitmodules18
-rw-r--r--client/CMakeLists.txt30
-rw-r--r--client/cmd.cpp94
-rw-r--r--client/cmd.h62
l---------client/compile_commands.json1
l---------client/lib1
-rw-r--r--client/main.cpp38
-rw-r--r--client/makefile2
-rw-r--r--client/parse.cpp176
-rw-r--r--client/parse.h42
l---------client/pbc1
-rw-r--r--client/readme.md37
-rw-r--r--client/rl.cpp86
-rw-r--r--client/rl.h10
-rw-r--r--client/sock.cpp125
-rw-r--r--client/sock.h34
-rw-r--r--client/xxd.c44
-rw-r--r--client/xxd.h17
-rw-r--r--i2ctcp/i2ctcpv1.c54
-rw-r--r--i2ctcp/i2ctcpv1.h71
-rw-r--r--i2ctcp/include.cmake16
l---------i2ctcp/lib1
-rw-r--r--i2ctcp/readme.md25
-rw-r--r--lazy.mk20
m---------lib/FreeRTOS-Kernel0
m---------lib/googletest (renamed from test/lib/googletest)0
m---------lib/mpack0
m---------lib/pico-sdk (renamed from main/lib/pico-sdk)0
-rw-r--r--main/.gitignore2
-rw-r--r--main/CMakeLists.txt33
-rw-r--r--main/FreeRTOSConfig.h69
-rw-r--r--main/config.def.h11
-rw-r--r--main/i2c.c93
-rw-r--r--main/i2c.h44
-rw-r--r--main/init.c59
-rw-r--r--main/init.h38
l---------main/lib1
-rw-r--r--main/lwipopts.h23
-rw-r--r--main/main.c32
-rw-r--r--main/main.cpp36
-rw-r--r--main/makefile9
-rw-r--r--main/readme.md21
-rw-r--r--main/sock.c96
-rw-r--r--main/sock.h11
-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/puzbus.h39
-rw-r--r--test/CMakeLists.txt2
l---------test/lib1
54 files changed, 2102 insertions, 68 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/.gitignore b/.gitignore
index 0902ca8..19dc4f4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
build
-.vscode/** \ No newline at end of file
+.vscode/**
+.cache
diff --git a/.gitmodules b/.gitmodules
index c951407..1a813e0 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,10 +1,20 @@
-[submodule "main/lib/pico-sdk"]
- path = main/lib/pico-sdk
+[submodule "pico-sdk"]
+ path = lib/pico-sdk
url = https://github.com/raspberrypi/pico-sdk
branch = 1.5.1
shallow = true
-[submodule "test/lib/googletest"]
- path = test/lib/googletest
+[submodule "googletest"]
+ path = lib/googletest
url = https://github.com/google/googletest
branch = v1.14.0
shallow = true
+[submodule "FreeRTOS-Kernel"]
+ path = lib/FreeRTOS-Kernel
+ url = https://github.com/FreeRTOS/FreeRTOS-Kernel
+ branch = V11.1.0
+ shallow = true
+[submodule "lib/mpack"]
+ path = lib/mpack
+ url = https://github.com/ludocode/mpack
+ branch = v1.1.1
+ shallow = true
diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt
new file mode 100644
index 0000000..57a2447
--- /dev/null
+++ b/client/CMakeLists.txt
@@ -0,0 +1,30 @@
+cmake_minimum_required(VERSION 3.29)
+
+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(../i2ctcp/include.cmake)
+
+add_executable(pbc
+ main.cpp
+ rl.cpp
+ sock.cpp
+ cmd.cpp
+ parse.cpp
+ xxd.c
+ )
+
+target_link_libraries(pbc
+ 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
new file mode 100644
index 0000000..ab101e9
--- /dev/null
+++ b/client/cmd.cpp
@@ -0,0 +1,94 @@
+#include <cstdio>
+#include <cstdlib>
+#include <string.h>
+
+#include "cmd.h"
+#include "i2ctcpv1.h"
+#include "sock.h"
+#include "parse.h"
+
+#include "../shared/puzbus.h"
+
+char* consume_token(char* input, const char* ifs) {
+ strtok(input, ifs);
+ return strtok(NULL, "\0");
+}
+
+void cmd_exit(char*) {
+ exit(EXIT_SUCCESS);
+}
+
+void cmd_test(char*) {
+ const char* data = "Hello world!";
+ i2c_send(0x39, data, strlen(data));
+}
+
+void cmd_help(char*) {
+ printf("List of available commands:\n");
+ for (size_t i = 0; i < cmds_length; i++) {
+ struct cmd cmd = cmds[i];
+ printf(" %-*s", 10, cmd.name);
+ if (cmd.info != NULL)
+ printf(" %s", cmd.info);
+ printf("\n");
+ }
+
+ printf(
+ "\n"
+ "You can also use the TAB key to autocomplete commands\n"
+ );
+}
+
+void cmd_send(char* addr_str) {
+ char* data_str = consume_token(addr_str, IFS);
+
+ char* end;
+ uint16_t addr = strtol(addr_str, &end, 0);
+ if (addr_str + strlen(addr_str) != end) {
+ printf("address format error\n");
+ return;
+ }
+
+ char* data;
+ size_t data_size;
+ int err = strtodata(data_str, &data, &data_size);
+ if (err <= 0) {
+ printf("data format error at index %d:\n%s\n%*s^\n",
+ -err, data_str, -err, "");
+ return;
+ }
+
+ 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_status(char*) {
+ const char msg[] = {
+ PB_CMD_READ,
+ 0x00, // addr 0 = global state
+ };
+ i2c_send(BUSADDR_MAIN, msg, sizeof(msg));
+ // NOTE: the reply handler will automatically print the state once it's
+ // received
+}
+
+void cmd_reset(char*) {
+ const char msg[] = {
+ PB_CMD_WRITE,
+ 0x00,
+ PB_GS_IDLE,
+ };
+ 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));
+}
+
diff --git a/client/cmd.h b/client/cmd.h
new file mode 100644
index 0000000..932f3a2
--- /dev/null
+++ b/client/cmd.h
@@ -0,0 +1,62 @@
+#pragma once
+
+#include <stddef.h>
+
+typedef void cmd_fn_t(char *);
+
+struct cmd {
+ void (* handle)(char *);
+ const char* name;
+ const char* info;
+ // TODO: tab completion function?
+};
+
+cmd_fn_t cmd_exit;
+cmd_fn_t cmd_test;
+cmd_fn_t cmd_help;
+cmd_fn_t cmd_status;
+cmd_fn_t cmd_reset;
+cmd_fn_t cmd_ls;
+cmd_fn_t cmd_send;
+
+static const struct cmd cmds[] = {
+ {
+ .handle = cmd_exit,
+ .name = "exit",
+ .info = "exit pbc",
+ },
+ {
+ .handle = cmd_test,
+ .name = "test",
+ .info = "send a test puzbus message",
+ },
+ {
+ .handle = cmd_help,
+ .name = "help",
+ .info = "show this help",
+ },
+ {
+ .handle = cmd_status,
+ .name = "status",
+ .info = "show global puzzle box state (main controller state)",
+ },
+ {
+ .handle = cmd_reset,
+ .name = "reset",
+ .info = "reset entire game state",
+ },
+ {
+ .handle = cmd_ls,
+ .name = "ls",
+ .info = "list connected puzzle modules",
+ },
+#ifdef DEBUG
+ {
+ .handle = cmd_send,
+ .name = "send",
+ .info = "[debug] send raw message",
+ },
+#endif
+};
+static const size_t cmds_length = sizeof(cmds) / sizeof(cmds[0]);
+
diff --git a/client/compile_commands.json b/client/compile_commands.json
new file mode 120000
index 0000000..25eb4b2
--- /dev/null
+++ b/client/compile_commands.json
@@ -0,0 +1 @@
+build/compile_commands.json \ No newline at end of file
diff --git a/client/lib b/client/lib
new file mode 120000
index 0000000..dc598c5
--- /dev/null
+++ b/client/lib
@@ -0,0 +1 @@
+../lib \ No newline at end of file
diff --git a/client/main.cpp b/client/main.cpp
new file mode 100644
index 0000000..5c26107
--- /dev/null
+++ b/client/main.cpp
@@ -0,0 +1,38 @@
+#include <cstdio>
+#include <cstdlib>
+#include <cstdint>
+#include <exception>
+
+#include "rl.h"
+#include "sock.h"
+
+PBSocket* sock;
+
+int main(int argc, char** argv) {
+ if (argc < 2) {
+ printf("usage: %s addr [port]\n", argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ // parse arguments
+ char* addr = argv[1];
+ uint16_t port = 9191;
+ if (argc >= 3) port = atoi(argv[2]);
+
+ sock = new PBSocket(addr, port);
+ try {
+ // connect to TCP socket (automatically spawns thread)
+ sock->sock_connect();
+ } catch (const std::exception& e) {
+ printf("error: %s\n", e.what());
+ return EXIT_FAILURE;
+ }
+
+ // enter main CLI (using GNU readline for comfyness)
+ int ret = cli_main();
+
+ delete sock;
+
+ return ret;
+}
+
diff --git a/client/makefile b/client/makefile
new file mode 100644
index 0000000..8352615
--- /dev/null
+++ b/client/makefile
@@ -0,0 +1,2 @@
+include ../lazy.mk
+
diff --git a/client/parse.cpp b/client/parse.cpp
new file mode 100644
index 0000000..f31e802
--- /dev/null
+++ b/client/parse.cpp
@@ -0,0 +1,176 @@
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <netinet/in.h>
+
+#include "parse.h"
+
+static int parse_string(const char * str, char * data, size_t * offset) {
+ char closing = str[0];
+ char escape = false;
+ int i = 0;
+ size_t len = strlen(str);
+
+ switch (str[i]) {
+ case '\'':
+ escape = false;
+ break;
+ case '\"':
+ escape = true;
+ break;
+ default:
+ return -i;
+ }
+
+ for (i = 1; i < len && str[i] != '\0'; i++, *offset += 1) {
+ char c = str[i];
+
+ // TODO: handle escaped characters
+
+ if (c == closing)
+ return i + 1; // +1 for closing quote
+
+ if (data != NULL)
+ data[*offset] = c;
+ }
+
+ return -i;
+}
+
+static int parse_hexstr(const char * str, char * data, size_t * offset) {
+ const char* ifs = IFS;
+ 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) {
+ const char* ifs = IFS;
+ size_t len = strcspn(str, ifs);
+ int i = 0;
+ int base = 10;
+ bool bytestring = false;
+
+ 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
+ base = 8;
+ i += 1;
+ }*/
+
+ const char* set;
+ // if (base == 8) set = SET_OCT;
+ if (base == 10) set = SET_DEC;
+ if (base == 16) set = SET_HEX;
+
+ size_t len_ok = strspn(str + i, set) + i;
+ if (len != len_ok) return -len_ok;
+
+ 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;
+ }
+ }
+
+ *offset += size;
+ return len;
+}
+
+static int _strtodata_main(const char * str, char* data, size_t * offset) {
+ const char* ifs = IFS;
+ size_t len = strlen(str);
+
+ int i, run;
+ for (i = 0; i < len; i += run) {
+ i += strspn(&str[i], ifs); // skip whitespace
+ if (str[i] == '\0') break; // end of string
+
+ 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;
+ }
+
+ return i;
+}
+
+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
new file mode 100644
index 0000000..94afe70
--- /dev/null
+++ b/client/parse.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <stddef.h>
+
+#define IFS " \t\n"
+
+#define SET_OCT "01234567"
+#define SET_DEC "0123456789"
+#define SET_HEX SET_DEC"abcdefABCDEF"
+#define SET_HEX_STR SET_HEX":"
+
+/**
+ * \brief modify \p token to point to the first token when broken up on \p ifs
+ * and return the remaining data
+ *
+ * \p token will be null-terminated to indicate the end of the first token. A
+ * pointer to the remaining line after the NULL byte is returned, or NULL when
+ * the end of the string has been reached.
+ *
+ * \param token input string
+ * \param ifs string containing field separators
+ *
+ * \return the remaining data after \p token and the first \p ifs
+ */
+char* consume_token(char * token, const char * ifs);
+
+/**
+ * \brief convert string with literals into raw data
+ *
+ * \param str input string containing literals
+ * \param data pointer to \c char* that will store the resulting data
+ * \param size size of \p data
+ *
+ * \return 0 or a negative integer representing the index where there is a
+ * syntax error if there was an error, or a positive integer representing the
+ * amount of bytes parsed from \p str
+ *
+ * \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);
+
diff --git a/client/pbc b/client/pbc
new file mode 120000
index 0000000..51eda50
--- /dev/null
+++ b/client/pbc
@@ -0,0 +1 @@
+build/pbc \ No newline at end of file
diff --git a/client/readme.md b/client/readme.md
new file mode 100644
index 0000000..ea3e034
--- /dev/null
+++ b/client/readme.md
@@ -0,0 +1,37 @@
+# puzzle box client
+
+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.
+
+## 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
+
+```
+ 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
new file mode 100644
index 0000000..3f93e99
--- /dev/null
+++ b/client/rl.cpp
@@ -0,0 +1,86 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdbool.h>
+#include <stdarg.h>
+
+#include <readline/readline.h>
+#include <readline/history.h>
+
+#include "rl.h"
+#include "cmd.h"
+#include "parse.h"
+
+void rl_printf(const char *fmt, ...) {
+ // save line
+ char* saved_line = rl_copy_text(0, rl_end);
+ int saved_point = rl_point;
+ int saved_end = rl_end;
+
+ // clear line
+ rl_save_prompt();
+ rl_replace_line("", 0);
+ rl_redisplay();
+
+ // printf
+ va_list args;
+ va_start(args, fmt);
+ vprintf(fmt, args);
+ va_end(args);
+
+ // restore line
+ rl_restore_prompt();
+ rl_replace_line(saved_line, 0);
+ rl_point = saved_point;
+ rl_end = saved_end;
+ rl_redisplay();
+
+ free(saved_line);
+}
+
+static void cli_cmd(char* cmd) {
+ char* line = consume_token(cmd, IFS);
+
+ for (size_t i = 0; i < cmds_length; i++) {
+ if (strncmp(cmds[i].name, cmd, strlen(cmd)) != 0)
+ continue;
+
+ cmds[i].handle(line);
+ return;
+ }
+
+ printf("unknown command!\n");
+}
+
+static char* rl_completion_entries(const char *text, int state) {
+ static size_t i = 0;
+ if (state == 0) i = 0;
+
+ while (i < cmds_length) {
+ struct cmd cmd = cmds[i];
+ i++;
+ if (strncmp(text, cmd.name, strlen(text)) == 0) {
+ return strdup(cmd.name);
+ }
+ }
+
+ return NULL;
+}
+
+int cli_main() {
+ char* input = NULL;
+ rl_completion_entry_function = rl_completion_entries;
+
+ while (1) {
+ if (input != NULL) free(input);
+ input = readline(CLI_PROMPT);
+
+ if (input == NULL) return EXIT_SUCCESS; // exit on ^D (EOF)
+ if (*input == '\0') continue; // ignore empty lines
+ add_history(input);
+
+ cli_cmd(input);
+ }
+
+ return EXIT_SUCCESS;
+}
+
diff --git a/client/rl.h b/client/rl.h
new file mode 100644
index 0000000..503225f
--- /dev/null
+++ b/client/rl.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#define COLOR_OFF "\x1b[0m"
+#define COLOR_BOLD "\x1b[1m"
+
+#define CLI_PROMPT "(" COLOR_BOLD "pbc" COLOR_OFF ") "
+
+int cli_main();
+void rl_printf(const char *fmt, ...);
+
diff --git a/client/sock.cpp b/client/sock.cpp
new file mode 100644
index 0000000..2d5787d
--- /dev/null
+++ b/client/sock.cpp
@@ -0,0 +1,125 @@
+#include <arpa/inet.h>
+#include <cstring>
+#include <stdexcept>
+#include <unistd.h>
+#include <cstdio>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <errno.h>
+
+#include <thread>
+
+#include "i2ctcpv1.h"
+#include "sock.h"
+#include "rl.h"
+
+using std::logic_error;
+using std::thread;
+
+PBSocket::PBSocket() { }
+PBSocket::PBSocket(const char * addr, uint16_t port) : PBSocket() {
+ set_server(addr, port);
+}
+
+PBSocket::~PBSocket() {
+ // stop TCP listen thread
+ if (_thread != nullptr) {
+ _thread->detach();
+ delete _thread;
+ }
+
+ sock_close();
+}
+
+void PBSocket::set_server(const char * addr, uint16_t port) {
+ _addr = addr;
+ _port = port;
+}
+
+void PBSocket::sock_connect() {
+ if (_addr == NULL) throw logic_error("no server address defined");
+ if (_port == 0) throw logic_error("no server port defined");
+
+ if (_thread != nullptr) throw logic_error("already connected");
+
+ rl_printf("connecting to %s on port %d...\n", _addr, _port);
+
+ _fd = socket(AF_INET, SOCK_STREAM, 0);
+ if (_fd < 0) throw logic_error("socket create failed");
+
+ struct sockaddr_in server = {
+ .sin_family = AF_INET,
+ .sin_port = htons(_port),
+ .sin_addr = {
+ .s_addr = inet_addr(_addr),
+ },
+ };
+ int ret = connect(_fd, (struct sockaddr*) &server, sizeof(server));
+ if (ret != 0) throw logic_error(strerror(errno));
+
+ this->_thread = new thread(&PBSocket::sock_task, this);
+}
+
+void PBSocket::sock_close() {
+ if (_fd < 0) return; // already closed
+ close(_fd);
+ _fd = -1;
+}
+
+void PBSocket::send(const char * buf, size_t buf_sz) {
+ write(_fd, buf, buf_sz);
+}
+
+void PBSocket::sock_task() {
+ i2ctcp_msg_t input;
+
+ while(1) {
+ char buf[80];
+ ssize_t bytes = read(_fd, buf, sizeof(buf));
+
+ if (bytes == -1) {
+ rl_printf("error: %s (%d)\n", strerror(errno), errno);
+ break;
+ }
+
+ // skip empty frames
+ if (bytes == 0) continue;
+
+ int ret = i2ctcp_read(&input, buf, bytes);
+
+ // header read error
+ if (ret < 0) {
+ rl_printf("i2ctcp_read error!\n");
+ break;
+ }
+
+ // continue reading if more bytes needed...
+ if (ret > 0) continue;
+
+ // message read completely!
+ i2c_recv(input.addr, input.data, input.length);
+ free(input.data);
+ }
+
+ sock_close();
+}
+
+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);
+}
+
+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
new file mode 100644
index 0000000..42eba3b
--- /dev/null
+++ b/client/sock.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <cstdint>
+#include <thread>
+
+class PBSocket {
+public:
+ PBSocket();
+ PBSocket(const char * addr, uint16_t port);
+ virtual ~PBSocket();
+
+ void set_server(const char * addr, uint16_t port);
+
+ void sock_connect();
+
+ void send(const char * buf, size_t buf_sz);
+
+private:
+ void sock_task();
+ void sock_close();
+
+ std::thread* _thread = nullptr;
+
+ const char * _addr = NULL;
+ uint16_t _port = 0;
+
+ int _fd = -1;
+};
+
+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..36a5dbd
--- /dev/null
+++ b/i2ctcp/i2ctcpv1.c
@@ -0,0 +1,54 @@
+#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) {
+ // 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 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;
+
+ 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 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/i2ctcp/i2ctcpv1.h b/i2ctcp/i2ctcpv1.h
new file mode 100644
index 0000000..799b668
--- /dev/null
+++ b/i2ctcp/i2ctcpv1.h
@@ -0,0 +1,71 @@
+#pragma once
+
+#include <stddef.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** \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 i2ctcp_write()
+ *
+ * \param target pointer to struct that will contain the finished message data
+ * \param buf pointer to input stream data chunk
+ * \param buf_sz size of \p buf
+ *
+ * \returns Integer representing amount of bytes required to finish message, or
+ * -1 if the message header could not be read. If this function returns 0, the
+ * message in \p target is complete.
+ *
+ * \note target->data will automatically be allocated by this function, even if
+ * the message is not fully parsed. This variable must be `free()`d by the
+ * caller after each complete message to prevent memory leaks.
+ */
+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 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 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 i2ctcp_read()
+ *
+ * \param target pointer to struct that contains the message data
+ * \param buf pointer to \c char* that will contain the formatted message
+ * \param buf_sz pointer to \c size_t that will represent the final size of \p buf
+ *
+ * \returns boolean true if a the message could be encoded successfully, false
+ * if there was some kind of error
+ *
+ * \note the pointer stored in \p buf must be `free()`d by the caller afterwards
+ */
+bool i2ctcp_write(const i2ctcp_msg_t * target, char ** buf, size_t * buf_sz);
+
+#ifdef __cplusplus
+}
+#endif
+
diff --git a/i2ctcp/include.cmake b/i2ctcp/include.cmake
new file mode 100644
index 0000000..d755b57
--- /dev/null
+++ b/i2ctcp/include.cmake
@@ -0,0 +1,16 @@
+include_directories(${CMAKE_CURRENT_LIST_DIR})
+add_library(i2ctcp STATIC
+ ${CMAKE_CURRENT_LIST_DIR}/i2ctcpv1.c
+ )
+
+# mpack
+include_directories(${CMAKE_CURRENT_LIST_DIR}/lib/mpack/src/mpack)
+add_library(mpack STATIC
+ ${CMAKE_CURRENT_LIST_DIR}/lib/mpack/src/mpack/mpack-common.c
+ ${CMAKE_CURRENT_LIST_DIR}/lib/mpack/src/mpack/mpack-expect.c
+ ${CMAKE_CURRENT_LIST_DIR}/lib/mpack/src/mpack/mpack-node.c
+ ${CMAKE_CURRENT_LIST_DIR}/lib/mpack/src/mpack/mpack-platform.c
+ ${CMAKE_CURRENT_LIST_DIR}/lib/mpack/src/mpack/mpack-reader.c
+ ${CMAKE_CURRENT_LIST_DIR}/lib/mpack/src/mpack/mpack-writer.c
+ )
+
diff --git a/i2ctcp/lib b/i2ctcp/lib
new file mode 120000
index 0000000..dc598c5
--- /dev/null
+++ b/i2ctcp/lib
@@ -0,0 +1 @@
+../lib \ No newline at end of file
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/lazy.mk b/lazy.mk
new file mode 100644
index 0000000..2620961
--- /dev/null
+++ b/lazy.mk
@@ -0,0 +1,20 @@
+# this file is for lazy people (loek)
+
+BUILD_DIR ?= build
+TARGET ?= $(BUILD_DIR)/main
+
+.PHONY: FORCE
+
+all: FORCE $(TARGET)
+
+$(BUILD_DIR)/build.ninja: CMakeLists.txt
+ mkdir -p $(BUILD_DIR)
+ cmake -B $(BUILD_DIR) -G Ninja --fresh --log-level WARNING
+
+$(TARGET): $(BUILD_DIR)/build.ninja FORCE
+ ninja -C $(BUILD_DIR)
+# ninja automatically builds in parallel, so is preferred
+
+clean: FORCE
+ $(RM) -r $(BUILD_DIR)
+
diff --git a/lib/FreeRTOS-Kernel b/lib/FreeRTOS-Kernel
new file mode 160000
+Subproject dbf70559b27d39c1fdb68dfb9a32140b6a6777a
diff --git a/test/lib/googletest b/lib/googletest
-Subproject 5197b1a8e6a1ef9f214f4aa537b0be17cbf9194
+Subproject 5197b1a8e6a1ef9f214f4aa537b0be17cbf9194
diff --git a/lib/mpack b/lib/mpack
new file mode 160000
+Subproject 79d3fcd3e04338b06e82d01a62f4aa98c7bad5f
diff --git a/main/lib/pico-sdk b/lib/pico-sdk
-Subproject 6a7db34ff63345a7badec79ebea3aaef1712f37
+Subproject 6a7db34ff63345a7badec79ebea3aaef1712f37
diff --git a/main/.gitignore b/main/.gitignore
index 7c3ba25..0e56cf2 100644
--- a/main/.gitignore
+++ b/main/.gitignore
@@ -1,3 +1 @@
config.h
-build
-.cache
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index e24d9a5..6390d7c 100644
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -1,22 +1,39 @@
cmake_minimum_required(VERSION 3.29)
-include(lib/pico-sdk/pico_sdk_init.cmake)
-
-project(puzzlebox_main C CXX ASM)
-
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_EXPORT_COMPILE_COMMANDS 1)
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(../i2ctcp/include.cmake)
+
+project(puzzlebox_main C CXX ASM)
+
pico_sdk_init()
add_executable(main
- main.cpp
-)
+ main.c
+ init.c
+ sock.c
+ i2c.c
+ )
pico_enable_stdio_usb(main 1)
-# pico_enable_stdio_uart(main 1)
+pico_enable_stdio_uart(main 0)
pico_add_extra_outputs(main)
+
+include_directories(lib/pico-sdk/lib/lwip/contrib/ports/freertos/include)
+
target_include_directories(main PRIVATE ${CMAKE_CURRENT_LIST_DIR})
-target_link_libraries(main pico_cyw43_arch_lwip_threadsafe_background pico_stdlib)
+target_link_libraries(main
+ pico_cyw43_arch_lwip_sys_freertos
+ pico_stdlib
+ hardware_i2c
+ FreeRTOS-Kernel
+ FreeRTOS-Kernel-Heap4
+ i2ctcp
+ mpack
+ )
+
diff --git a/main/FreeRTOSConfig.h b/main/FreeRTOSConfig.h
new file mode 100644
index 0000000..c811296
--- /dev/null
+++ b/main/FreeRTOSConfig.h
@@ -0,0 +1,69 @@
+#pragma once
+// values from pico-examples/pico_w/wifi/freertos
+
+#define configUSE_PREEMPTION 1
+#define configUSE_TICKLESS_IDLE 0
+#define configUSE_IDLE_HOOK 0
+#define configUSE_TICK_HOOK 0
+#define configTICK_RATE_HZ ((TickType_t) 1000)
+#define configMAX_PRIORITIES 32
+#define configMINIMAL_STACK_SIZE ((configSTACK_DEPTH_TYPE) 512)
+#define configUSE_16_BIT_TICKS 0
+#define configIDLE_SHOULD_YIELD 1
+#define configUSE_MUTEXES 1
+#define configUSE_RECURSIVE_MUTEXES 1
+#define configUSE_APPLICATION_TASK_TAG 0
+#define configUSE_COUNTING_SEMAPHORES 1
+#define configQUEUE_REGISTRY_SIZE 8
+#define configUSE_QUEUE_SETS 1
+#define configUSE_TIME_SLICING 1
+#define configUSE_NEWLIB_REENTRANT 0
+#define configENABLE_BACKWARD_COMPATIBILITY 1
+#define configNUM_THREAD_LOCAL_STORAGE_POINTERS 5
+#define configSTACK_DEPTH_TYPE uint32_t
+#define configMESSAGE_BUFFER_LENGTH_TYPE size_t
+#define configSUPPORT_STATIC_ALLOCATION 0
+#define configSUPPORT_DYNAMIC_ALLOCATION 1
+#define configTOTAL_HEAP_SIZE (128 * 1024)
+#define configAPPLICATION_ALLOCATED_HEAP 0
+#define configCHECK_FOR_STACK_OVERFLOW 0
+#define configUSE_MALLOC_FAILED_HOOK 0
+#define configUSE_DAEMON_TASK_STARTUP_HOOK 0
+#define configGENERATE_RUN_TIME_STATS 0
+#define configUSE_TRACE_FACILITY 1
+#define configUSE_STATS_FORMATTING_FUNCTIONS 0
+#define configUSE_CO_ROUTINES 0
+#define configMAX_CO_ROUTINE_PRIORITIES 1
+#define configUSE_TIMERS 1
+#define configTIMER_TASK_PRIORITY (configMAX_PRIORITIES - 1)
+#define configTIMER_QUEUE_LENGTH 10
+#define configTIMER_TASK_STACK_DEPTH 1024
+
+// #define configNUM_CORES 2
+// #define configTICK_CORE 0
+// #define configRUN_MULTIPLE_PRIORITIES 1
+// #define configUSE_CORE_AFFINITY 1
+
+#define configSUPPORT_PICO_SYNC_INTEROP 1
+#define configSUPPORT_PICO_TIME_INTEROP 1
+
+#include <assert.h>
+#define configASSERT(x) assert(x)
+
+#define INCLUDE_vTaskPrioritySet 1
+#define INCLUDE_uxTaskPriorityGet 1
+#define INCLUDE_vTaskDelete 1
+#define INCLUDE_vTaskSuspend 1
+#define INCLUDE_vTaskDelayUntil 1
+#define INCLUDE_vTaskDelay 1
+#define INCLUDE_xTaskGetSchedulerState 1
+#define INCLUDE_xTaskGetCurrentTaskHandle 1
+#define INCLUDE_uxTaskGetStackHighWaterMark 1
+#define INCLUDE_xTaskGetIdleTaskHandle 1
+#define INCLUDE_eTaskGetState 1
+#define INCLUDE_xTimerPendFunctionCall 1
+#define INCLUDE_xTaskAbortDelay 1
+#define INCLUDE_xTaskGetHandle 1
+#define INCLUDE_xTaskResumeFromISR 1
+#define INCLUDE_xQueueGetMutexHolder 1
+
diff --git a/main/config.def.h b/main/config.def.h
index 48de559..7fcaed9 100644
--- a/main/config.def.h
+++ b/main/config.def.h
@@ -1,8 +1,17 @@
#pragma once
+#include <pico/cyw43_arch.h>
+// wifi credentials
#define CONF_NET_SSID "network name"
#define CONF_NET_PASS "network password"
+#define CONF_NET_AUTH CYW43_AUTH_WPA2_AES_PSK
+// max duration (milliseconds) for establishing wifi connection
+#define CONF_NET_CONN_TIMEOUT 10e3
-#include "cyw43_country.h"
+#include <cyw43_country.h>
#define CONF_NET_COUNTRY CYW43_COUNTRY_NETHERLANDS
+#define CONF_SRV_PORT 9191
+
+#define LED_PIN CYW43_WL_GPIO_LED_PIN
+
diff --git a/main/i2c.c b/main/i2c.c
new file mode 100644
index 0000000..b324124
--- /dev/null
+++ b/main/i2c.c
@@ -0,0 +1,93 @@
+#include "i2c.h"
+#include "init.h"
+
+#include <stdio.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <pico/stdlib.h>
+#include <hardware/i2c.h>
+
+void init_i2c() {
+ i2c_init(I2C_PORT, 100 * 1000); // currently at 100kHz
+
+ // Initialize I2C pins - sda(16), scl(17)
+ gpio_set_function(SDA_PIN, GPIO_FUNC_I2C);
+ gpio_set_function(SCL_PIN, GPIO_FUNC_I2C);
+
+ gpio_pull_up(SDA_PIN);
+ gpio_pull_up(SCL_PIN);
+}
+
+int read_i2c(uint8_t addr, uint8_t *output, size_t len) {
+ // false - finished with bus
+ return i2c_read_blocking (I2C_PORT, addr, output, len, false);
+}
+
+int write_i2c(uint8_t addr, uint8_t *input, size_t len) {
+ // true to keep master control of bus
+ return i2c_write_blocking (I2C_PORT, addr, input, len, true);
+}
+
+bool reserved_addr(uint8_t addr) {
+ return (addr & 0x78) == 0 || (addr & 0x78) == 0x78;
+}
+
+void init_addr_array(uint8_t array[], int size) {
+ for(int i = 0; i < size; i++){
+ array[i] = 0x00;
+ }
+}
+
+uint8_t* scan_bus(uint8_t *array) {
+ int ret;
+ int i = 0;
+ uint8_t rxdata;
+
+ for(int addr = 0; addr < (1<<7); addr++) {
+ // ignore reserved addresses
+ // These are any addresses of the form 000 0xxx or 111 1xxx
+ if( reserved_addr(addr) ){
+ ret = PICO_ERROR_GENERIC;
+ }else{
+ ret = i2c_read_blocking(I2C_PORT, addr, &rxdata, 1, false);
+ }
+
+ // if acknowledged -> ret == number of bytes sent
+ if(ret > 0){
+ printf("found i2c slave on addr: %d\n", addr);
+ array[i] = addr;
+ i++;
+ }
+ }
+
+ return array;
+}
+
+void bus_task() {
+ // scan bus for slaves
+ // send updates at regular intervals
+ await_init();
+
+ int i = 0;
+ uint8_t found[MAX_SLAVES];
+ init_addr_array(found, MAX_SLAVES);
+
+ while(1) {
+ // printf("Bus scan!");
+ scan_bus(found);
+
+ for(int i = 0; i < MAX_SLAVES; i++){
+ if( found[i] == 0x00 )
+ break;
+
+ uint8_t data = 0x01;
+ // send data to found slave address
+ write_i2c(found[i], &data, 1);
+
+ data = 0x02;
+ write_i2c(found[i], &data, 1);
+ // request update from slave addr at found[i]
+ //write_i2c();
+ }
+ }
+}
diff --git a/main/i2c.h b/main/i2c.h
new file mode 100644
index 0000000..5ad5cfb
--- /dev/null
+++ b/main/i2c.h
@@ -0,0 +1,44 @@
+#pragma once
+// https://github.com/raspberrypi/pico-examples/tree/master/i2c
+
+#include <stddef.h>
+#include <stdint.h>
+#include <hardware/i2c.h>
+
+#define SDA_PIN 16
+#define SCL_PIN 17
+#define I2C_PORT i2c0
+#define MAX_SLAVES 10
+
+/**
+ * \brief initialize all required gpio for i2c usage on the pico
+ *
+ * This functions only initializes the standard gpio required to start i2c
+ * communications.
+ *
+ * \note Tasks shouldn't depend on any other module in the main controller
+ */
+void init_i2c();
+
+/**
+ * \brief read data from addr with length len from i2c bus.
+ *
+ * This functions reads data from a specific address on the i2c bus,
+ * the output var will hold the data which was read from said address with
+ * length len.
+ */
+int read_i2c(uint8_t addr, uint8_t *output, size_t len);
+
+/**
+ * \brief write data to addr with length len from i2c bus.
+ * \param addr
+ * \param input
+ * \param len
+ * This functions writes data to a specific address on the i2c bus,
+ * the input var holds the data which will be written to the given
+ * address with length len.
+ */
+int write_i2c(uint8_t addr, uint8_t *input, size_t len);
+
+/** \brief looking for slave addresses and requesting updates */
+void bus_task();
diff --git a/main/init.c b/main/init.c
new file mode 100644
index 0000000..08177c7
--- /dev/null
+++ b/main/init.c
@@ -0,0 +1,59 @@
+#include "config.h"
+#include "init.h"
+#include "i2c.h"
+
+#include <FreeRTOS.h>
+#include <task.h>
+#include <event_groups.h>
+
+#include <pico/stdio.h>
+#include <pico/cyw43_arch.h>
+
+EventGroupHandle_t init_complete;
+
+static void init_stdio() {
+ stdio_init_all();
+}
+
+static void init_cyw34() {
+ if (cyw43_arch_init_with_country(CONF_NET_COUNTRY))
+ panic("cyw43_arch_init_with_country failed\n");
+}
+
+static void init_wifi() {
+ // enable 'station' mode (connect to an access point instead of acting like one)
+ cyw43_arch_enable_sta_mode();
+
+ // if (cyw43_arch_wifi_connect_timeout_ms(CONF_NET_SSID, CONF_NET_PASS, CONF_NET_AUTH, CONF_NET_CONN_TIMEOUT))
+ // panic("cyw43_arch_wifi_connect failed\n");
+
+ printf("connected to Wi-Fi\n");
+
+ // TODO: announce hostname(?)
+}
+
+static void async_init() {
+ init_cyw34();
+ init_i2c();
+ init_wifi();
+
+ xEventGroupSetBits(init_complete, 1);
+
+ // delete self
+ vTaskDelete(NULL);
+}
+
+void init() {
+ init_complete = xEventGroupCreate();
+
+ // used for debug `printf` and `panic` on errors
+ init_stdio();
+
+ // defer other initialization until the task scheduler is running (important)
+ xTaskCreate((TaskFunction_t) async_init, "init", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 4, NULL);
+}
+
+void await_init() {
+ xEventGroupWaitBits(init_complete, 1, pdFALSE, pdFALSE, portMAX_DELAY);
+}
+
diff --git a/main/init.h b/main/init.h
new file mode 100644
index 0000000..de9023c
--- /dev/null
+++ b/main/init.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include <FreeRTOS.h>
+#include <event_groups.h>
+
+/**
+ * \brief init function complete event group handle
+ *
+ * This is required to make sure the main task waits until initialization is
+ * complete. Due to the combination of FreeRTOS + lwIP, the initialization
+ * should be done while the task scheduler is running. Specifically the
+ * cyw43_arch_init functions make the pico hang indefinitely when used while
+ * the task scheduler is not running.
+ *
+ * \note `init_complete` only utilizes LSB, so `uxBitsToWaitFor` should always
+ * be set to *1*
+ */
+extern EventGroupHandle_t init_complete;
+
+/**
+ * \brief initialize all peripherals on the pico
+ *
+ * This function only synchronously initializes the standard input/output (used
+ * for `printf` and `panic`), and queues all other types of initialization in
+ * the `init` task using FreeRTOS.
+ *
+ * \note Tasks dependent on the wifi being initialized should use the
+ * `init_complete` event group to wait for initialization to complete!
+ */
+void init();
+
+/**
+ * \brief block task until all initialization is complete
+ *
+ * utility function, see above comments
+ */
+void await_init();
+
diff --git a/main/lib b/main/lib
new file mode 120000
index 0000000..dc598c5
--- /dev/null
+++ b/main/lib
@@ -0,0 +1 @@
+../lib \ No newline at end of file
diff --git a/main/lwipopts.h b/main/lwipopts.h
index 75a57ee..b2b6e76 100644
--- a/main/lwipopts.h
+++ b/main/lwipopts.h
@@ -1,13 +1,7 @@
#pragma once
-// allow override in some examples
-#ifndef NO_SYS
-#define NO_SYS 1
-#endif
-
-#ifndef LWIP_SOCKET
-#define LWIP_SOCKET 0
-#endif
+#define NO_SYS 0
+#define LWIP_SOCKET 1
#if PICO_CYW43_ARCH_POLL
#define MEM_LIBC_MALLOC 1
@@ -83,3 +77,16 @@
#define SLIP_DEBUG LWIP_DBG_OFF
#define DHCP_DEBUG LWIP_DBG_OFF
+#define TCPIP_THREAD_STACKSIZE 2048
+#define DEFAULT_THREAD_STACKSIZE 1024
+#define DEFAULT_RAW_RECVMBOX_SIZE 8
+#define TCPIP_MBOX_SIZE 8
+
+#define DEFAULT_UDP_RECVMBOX_SIZE TCPIP_MBOX_SIZE
+#define DEFAULT_TCP_RECVMBOX_SIZE TCPIP_MBOX_SIZE
+#define DEFAULT_ACCEPTMBOX_SIZE TCPIP_MBOX_SIZE
+
+#define LWIP_TIMEVAL_PRIVATE 0
+
+#define LWIP_TCPIP_CORE_LOCKING_INPUT 1
+
diff --git a/main/main.c b/main/main.c
new file mode 100644
index 0000000..19dd3cd
--- /dev/null
+++ b/main/main.c
@@ -0,0 +1,32 @@
+#include <FreeRTOS.h>
+#include <task.h>
+
+#include <pico/stdlib.h>
+#include <pico/time.h>
+
+#include "config.h"
+#include "init.h"
+#include "sock.h"
+#include "i2c.h"
+
+void blink_task() {
+ await_init(); // `blink_task` uses GPIO
+
+ while (true) {
+ cyw43_arch_gpio_put(LED_PIN, 0);
+ vTaskDelay(250 / portTICK_PERIOD_MS);
+ cyw43_arch_gpio_put(LED_PIN, 1);
+ vTaskDelay(250 / portTICK_PERIOD_MS);
+ }
+}
+
+int main() {
+ init();
+
+ xTaskCreate((TaskFunction_t) blink_task, "blink", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 2, NULL);
+ //xTaskCreate((TaskFunction_t) serve_task, "serve", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 2, NULL);
+ xTaskCreate((TaskFunction_t) bus_task, "bus", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 2, NULL);
+
+ vTaskStartScheduler();
+}
+
diff --git a/main/main.cpp b/main/main.cpp
deleted file mode 100644
index 9fd3123..0000000
--- a/main/main.cpp
+++ /dev/null
@@ -1,36 +0,0 @@
-#include <stdio.h>
-
-#include "config.h"
-
-#include "pico/stdlib.h"
-#include "pico/cyw43_arch.h"
-
-const unsigned int LED_PIN = CYW43_WL_GPIO_LED_PIN;
-
-int main() {
- stdio_init_all();
- sleep_ms(2000);
-
- if (cyw43_arch_init_with_country(CONF_NET_COUNTRY)) {
- printf("failed to initialize\n");
- return 1;
- }
- cyw43_arch_gpio_put(LED_PIN, 1);
- printf("initialised\n");
-
- cyw43_arch_enable_sta_mode();
-
- if (cyw43_arch_wifi_connect_timeout_ms(CONF_NET_SSID, CONF_NET_PASS, CYW43_AUTH_WPA2_AES_PSK, 10000)) {
- printf("failed to connect\n");
- return 1;
- }
- printf("connected\n");
-
- while (true) {
- cyw43_arch_gpio_put(LED_PIN, 0);
- sleep_ms(250);
- cyw43_arch_gpio_put(LED_PIN, 1);
- sleep_ms(250);
- }
-}
-
diff --git a/main/makefile b/main/makefile
new file mode 100644
index 0000000..9df4f09
--- /dev/null
+++ b/main/makefile
@@ -0,0 +1,9 @@
+TARGET = $(BUILD_DIR)/main.uf2
+
+include ../lazy.mk
+
+flash: $(TARGET) FORCE
+ picotool load -fx $<
+# -f forces a reboot of the pico before flashing
+# -x resets the pico after flashing
+
diff --git a/main/readme.md b/main/readme.md
new file mode 100644
index 0000000..425a00b
--- /dev/null
+++ b/main/readme.md
@@ -0,0 +1,21 @@
+# main controller firmware
+
+This directory contains the software for the main controller of the Puzzle Box.
+
+## building
+
+1. make sure the submodules are initialized
+2. copy [`config.def.h`](./config.def.h) to `config.h` and edit the defaults
+3. `mkdir build`
+4. `cmake -B build`
+5. `make -C build` or `ninja -C build` (choose your preference)
+
+alternatively, a makefile is provided for convenience
+
+## "flashing"
+
+1. [build](#building)
+2. (re)connect the raspberry pi pico while holding the BOOTSEL button (this is
+ the only button)
+3. `picotool load build/main.uf2`
+
diff --git a/main/sock.c b/main/sock.c
new file mode 100644
index 0000000..fe932bb
--- /dev/null
+++ b/main/sock.c
@@ -0,0 +1,96 @@
+#include <pico/stdio.h>
+
+#include <lwip/opt.h>
+#include <lwip/sys.h>
+#include <lwip/api.h>
+#include <string.h>
+
+#include "init.h"
+#include "config.h"
+#include "i2ctcpv1.h"
+#include "sock.h"
+
+struct netconn* current_connection = NULL;
+i2ctcp_msg_t recv_msg;
+
+void i2c_send(uint16_t addr, const char * data, size_t data_size) {
+ if (current_connection == NULL) return;
+
+ i2ctcp_msg_t send_msg = {
+ .addr = addr,
+ .data = (char *) data,
+ .length = data_size,
+ };
+
+ char * buf;
+ size_t buf_sz;
+
+ 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
+ netconn_write(current_connection, buf, buf_sz, NETCONN_COPY);
+
+ free(buf);
+}
+
+void i2c_recv(uint16_t addr, const char * data, size_t data_size) {
+ printf("address: 0x%02x\n", addr);
+ printf("data: \"%.*s\"\n", data_size, data);
+
+ // send message back
+ char reply[] = "Test message back!";
+ i2c_send(0x69, reply, strlen(reply));
+
+ // TODO: this function should forward the recieved message onto the puzzle
+ // bus instead of printing/replying
+}
+
+void recv_handler(struct netconn* conn, struct netbuf* buf) {
+ i2ctcp_read_reset(&recv_msg);
+
+ do {
+ char* data;
+ uint16_t len;
+ netbuf_data(buf, (void**)&data, &len);
+
+ // continue early if more data is needed to complete message
+ if (!i2ctcp_read(&recv_msg, data, len)) continue;
+
+ // forward received message to puzzle bus
+ i2c_recv(recv_msg.addr, recv_msg.data, recv_msg.length);
+ free(recv_msg.data);
+ } while (netbuf_next(buf) >= 0);
+
+ netbuf_delete(buf);
+}
+
+void accept_handler(struct netconn* conn) {
+ current_connection = conn;
+
+ struct netbuf* buf;
+ while (netconn_recv(conn, &buf) == ERR_OK)
+ recv_handler(conn, buf);
+
+ netconn_close(conn);
+ netconn_delete(conn);
+
+ current_connection = NULL;
+}
+
+void serve_task() {
+ await_init();
+
+ printf("starting server...\n");
+ struct netconn* conn = netconn_new(NETCONN_TCP);
+ netconn_bind(conn, IP_ADDR_ANY, CONF_SRV_PORT);
+ netconn_listen(conn);
+
+ printf("listening on %s:%d\n", ip4addr_ntoa(netif_ip4_addr(netif_list)), CONF_SRV_PORT);
+ while (1) {
+ struct netconn* incoming;
+ if (netconn_accept(conn, &incoming) == ERR_OK)
+ accept_handler(incoming);
+ }
+}
+
diff --git a/main/sock.h b/main/sock.h
new file mode 100644
index 0000000..f2db35d
--- /dev/null
+++ b/main/sock.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include <stdint.h>
+#include <stddef.h>
+
+/** \brief start listening for TCP socket requests */
+void serve_task();
+
+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/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/puzbus.h b/shared/puzbus.h
new file mode 100644
index 0000000..59a8867
--- /dev/null
+++ b/shared/puzbus.h
@@ -0,0 +1,39 @@
+#pragma once
+
+/** \file bus address reference */
+
+// Adafruit NeoTrellis modules
+#define BUSADDR_ADA_NEO_1 0x2E
+#define BUSADDR_ADA_NEO_2 0x2F
+#define BUSADDR_ADA_NEO_3 0x30
+#define BUSADDR_ADA_NEO_4 0x32
+
+// TODO: ???
+#define BUSADDR_MOD_NEOTRELLIS 0
+#define BUSADDR_MOD_SOFTWARE 0
+#define BUSADDR_MOD_HARDWARE 0
+#define BUSADDR_MOD_VAULT 0
+#define BUSADDR_MOD_AUTOMATION 0
+
+// main controller
+#define BUSADDR_MAIN 0x00
+
+/**
+ * \brief puzzle bus command types
+ *
+ * The first byte of a puzzle bus message's data indicates the command type.
+ */
+enum pb_cmd {
+ PB_CMD_READ, //!< read a puzzle module property
+ PB_CMD_WRITE, //!< write to a puzzle module property
+ // PB_CMD_UPDATE, //!< request an update
+};
+
+/** \brief Puzzle bus global states */
+enum pb_global_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
+};
+
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index a0bd099..a280a86 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -20,4 +20,4 @@ target_link_libraries(tests PRIVATE gtest_main)
add_test(
NAME tests
COMMAND tests
-) \ No newline at end of file
+)
diff --git a/test/lib b/test/lib
new file mode 120000
index 0000000..dc598c5
--- /dev/null
+++ b/test/lib
@@ -0,0 +1 @@
+../lib \ No newline at end of file