diff options
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 + + @@ -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/ + + + @@ -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; +} @@ -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 |