diff options
author | ThomasintAnker <thomasintanker1@gmail.com> | 2024-06-18 16:23:51 +0200 |
---|---|---|
committer | ThomasintAnker <thomasintanker1@gmail.com> | 2024-06-18 16:23:51 +0200 |
commit | a55d0bed6240c54f6173b1e38e80212c02c302de (patch) | |
tree | 07c15eebc8cd84e1071a3f72d3c74475017372f3 /lib | |
parent | b45b5d04daa29fcdd456233a931dcbb5b287769f (diff) | |
parent | 245fde65808ce902064ab438296f04f691d007e7 (diff) |
Merge branch 'master' into wip/handover
Diffstat (limited to 'lib')
31 files changed, 806 insertions, 0 deletions
diff --git a/lib/Arduino-CMake-Toolchain b/lib/Arduino-CMake-Toolchain new file mode 160000 +Subproject e745a9bed3c3fb83442d55bf05630f31574674f diff --git a/lib/i2ctcp/CMakeLists.txt b/lib/i2ctcp/CMakeLists.txt new file mode 100644 index 0000000..956dcf7 --- /dev/null +++ b/lib/i2ctcp/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.29) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_EXPORT_COMPILE_COMMANDS 1) + +project(i2ctcp C CXX) + +add_subdirectory(lib/mpack) + +add_library(i2ctcp STATIC i2ctcpv1.c) +target_link_libraries(i2ctcp mpack) +target_include_directories(i2ctcp SYSTEM INTERFACE .) + diff --git a/lib/i2ctcp/i2ctcpv1.c b/lib/i2ctcp/i2ctcpv1.c new file mode 100644 index 0000000..f70cbb1 --- /dev/null +++ b/lib/i2ctcp/i2ctcpv1.c @@ -0,0 +1,43 @@ +#include <mpack.h> +#ifndef MIN +#define MIN(a,b) (((a) < (b)) ? (a) : (b)) +#endif + +#include "i2ctcpv1.h" + +int i2ctcp_read(i2ctcp_msg_t * target, const char * buf, size_t buf_sz) { + mpack_reader_t reader; + mpack_reader_init_data(&reader, buf, buf_sz); + + // at start of message + if (target->_rdata == 0) { + target->addr = mpack_expect_u16(&reader); + target->length = target->_rdata = mpack_expect_bin(&reader); + if (mpack_reader_error(&reader) != mpack_ok) return -1; + target->data = (char *) malloc(target->length); + } + + // continue reading chunks of target->data until the amount of bytes + // specified in target->length + char * data = target->data + target->length - target->_rdata; + target->_rdata -= mpack_read_remaining_bytes(&reader, data, target->_rdata); + + // 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/lib/i2ctcp/i2ctcpv1.h b/lib/i2ctcp/i2ctcpv1.h new file mode 100644 index 0000000..799b668 --- /dev/null +++ b/lib/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/lib/i2ctcp/lib b/lib/i2ctcp/lib new file mode 120000 index 0000000..58677dd --- /dev/null +++ b/lib/i2ctcp/lib @@ -0,0 +1 @@ +../../lib
\ No newline at end of file diff --git a/lib/i2ctcp/makefile b/lib/i2ctcp/makefile new file mode 100644 index 0000000..5ca3fd1 --- /dev/null +++ b/lib/i2ctcp/makefile @@ -0,0 +1,4 @@ +TARGET = $(BUILD_DIR)/libi2ctcp.a + +include ../../lazy.mk + diff --git a/lib/i2ctcp/readme.md b/lib/i2ctcp/readme.md new file mode 100644 index 0000000..d5bfe6d --- /dev/null +++ b/lib/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/lib/mpack/CMakeLists.txt b/lib/mpack/CMakeLists.txt new file mode 100644 index 0000000..0e4359d --- /dev/null +++ b/lib/mpack/CMakeLists.txt @@ -0,0 +1,28 @@ +if(TARGET mpack) + return() +endif() + +cmake_minimum_required(VERSION 3.29) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_EXPORT_COMPILE_COMMANDS 1) + +project(mpack C) + +add_library(mpack STATIC + src/src/mpack/mpack-common.c + src/src/mpack/mpack-expect.c + src/src/mpack/mpack-node.c + src/src/mpack/mpack-platform.c + src/src/mpack/mpack-reader.c + src/src/mpack/mpack-writer.c + read-remaining.c + ) +target_include_directories(mpack SYSTEM INTERFACE + . + src/src/mpack + ) + +# causes some wild crashes, please leave off +add_compile_definitions(MPACK_READ_TRACKING=0) + diff --git a/lib/mpack/makefile b/lib/mpack/makefile new file mode 100644 index 0000000..e96794a --- /dev/null +++ b/lib/mpack/makefile @@ -0,0 +1,4 @@ +TARGET = $(BUILD_DIR)/libmpack.a + +include ../../lazy.mk + diff --git a/lib/mpack/mpack.h b/lib/mpack/mpack.h new file mode 100644 index 0000000..7c0c089 --- /dev/null +++ b/lib/mpack/mpack.h @@ -0,0 +1,19 @@ +#pragma once + +#include "src/src/mpack/mpack.h" + +/** + * \brief read remaining bytes in reader without opening a tag first + * + * \param reader pointer to mpack reader object + * \param p pointer to write data to + * \param count maximum number of bytes to read + * + * This function reads *up to* the amount of bytes specified in \p count, or + * less if there is less remaining data in the buffer. If \p count is equal to + * 0, all remaining data in the buffer is read. + * + * \return amount of bytes read + */ +size_t mpack_read_remaining_bytes(mpack_reader_t * reader, char * p, size_t count); + diff --git a/lib/mpack/read-remaining.c b/lib/mpack/read-remaining.c new file mode 100644 index 0000000..ebc9b56 --- /dev/null +++ b/lib/mpack/read-remaining.c @@ -0,0 +1,10 @@ +#include "mpack.h" + +size_t mpack_read_remaining_bytes(mpack_reader_t * reader, char * p, size_t count) { + size_t limit =mpack_reader_remaining(reader, NULL); + if (0 < count && count < limit) + limit = count; + memcpy(p, reader->data, limit); + return limit; +} + diff --git a/lib/mpack b/lib/mpack/src -Subproject 79d3fcd3e04338b06e82d01a62f4aa98c7bad5f +Subproject 79d3fcd3e04338b06e82d01a62f4aa98c7bad5f diff --git a/lib/pbdrv/CMakeLists.txt b/lib/pbdrv/CMakeLists.txt new file mode 100644 index 0000000..ca85b2b --- /dev/null +++ b/lib/pbdrv/CMakeLists.txt @@ -0,0 +1,28 @@ +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(pbdrv C CXX) + +add_subdirectory(lib/mpack) + +# generic puzzle bus (de)serializer library +add_library(pbdrv STATIC pb-read.c pb-write.c) +target_link_libraries(pbdrv mpack) +target_include_directories(pbdrv SYSTEM INTERFACE .) + +# puzzle bus module specific code (superset of pbdrv) +add_library(pbdrv-mod STATIC pb-mod.c) +target_link_libraries(pbdrv-mod pbdrv) +target_include_directories(pbdrv-mod SYSTEM INTERFACE .) + +# supported puzzle bus drivers +include(drv/arduino/cfg.cmake) +include(drv/rp2040/cfg.cmake) + diff --git a/lib/pbdrv/drv/arduino/cfg.cmake b/lib/pbdrv/drv/arduino/cfg.cmake new file mode 100644 index 0000000..36716e3 --- /dev/null +++ b/lib/pbdrv/drv/arduino/cfg.cmake @@ -0,0 +1,7 @@ +if(NOT DEFINED ARDUINO) + return() +endif() + +target_sources(pbdrv-mod PRIVATE "${CMAKE_CURRENT_LIST_DIR}/mod.cpp") +target_link_arduino_libraries(pbdrv-mod core Wire) + diff --git a/lib/pbdrv/drv/arduino/mod.cpp b/lib/pbdrv/drv/arduino/mod.cpp new file mode 100644 index 0000000..8a38a5b --- /dev/null +++ b/lib/pbdrv/drv/arduino/mod.cpp @@ -0,0 +1,37 @@ +#ifndef ARDUINO +#error This driver only works on the Arduino platform! +#endif + +#include <Arduino.h> +#include <Wire.h> + +#include <stdlib.h> +#include <stdint.h> + +#include "../../pb.h" +#include "../../pb-mod.h" +#include "mod.h" + +static void recv_event(int bytes) { + uint8_t * data = (uint8_t *) malloc(bytes); + size_t size = 0; + while (Wire.available()) + data[size++] = Wire.read(); + + pbdrv_i2c_recv(data, size); +} + +void pbdrv_setup() { + Wire.begin((int) PBDRV_MOD_ADDR); + Wire.setWireTimeout(PB_TIMEOUT_US, true); + Wire.setClock(PB_CLOCK_SPEED_HZ); + Wire.onReceive(recv_event); +} + +__weak void pbdrv_i2c_send(i2c_addr_t addr, const uint8_t * buf, size_t sz) { + Wire.beginTransmission((int) addr); + Wire.write(buf, sz); + Wire.endTransmission(true); + Wire.setWireTimeout(PB_TIMEOUT_US, true); +} + diff --git a/lib/pbdrv/drv/arduino/mod.h b/lib/pbdrv/drv/arduino/mod.h new file mode 100644 index 0000000..079941a --- /dev/null +++ b/lib/pbdrv/drv/arduino/mod.h @@ -0,0 +1,17 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \brief puzzle bus driver setup + * + * This function should be called from the Arduino \c setup() function. + */ +void pbdrv_setup(); + +#ifdef __cplusplus +} +#endif + diff --git a/lib/pbdrv/drv/rp2040/cfg.cmake b/lib/pbdrv/drv/rp2040/cfg.cmake new file mode 100644 index 0000000..0fbad18 --- /dev/null +++ b/lib/pbdrv/drv/rp2040/cfg.cmake @@ -0,0 +1,7 @@ +if(NOT PICO_PLATFORM STREQUAL "rp2040") + return() +endif() + +target_sources(pbdrv-mod PRIVATE "${CMAKE_CURRENT_LIST_DIR}/mod.c") +target_link_libraries(pbdrv-mod hardware_i2c pico_i2c_slave) + diff --git a/lib/pbdrv/drv/rp2040/mod.c b/lib/pbdrv/drv/rp2040/mod.c new file mode 100644 index 0000000..6becdee --- /dev/null +++ b/lib/pbdrv/drv/rp2040/mod.c @@ -0,0 +1,73 @@ +#include "mod.h" + +#include "../../pb.h" +#include "../../pb-types.h" +#include "../../pb-mod.h" + +#include <hardware/i2c.h> +#include <hardware/gpio.h> +#include <pico/i2c_slave.h> + +#define PBDRV_I2C i2c0 +#define BUF_SIZE 256 + +static volatile bool pbdrv_i2c_msg_avail = false; +static uint8_t data[BUF_SIZE]; +static size_t size = 0; + +// TODO: create event group instead of pbdrv_i2c_msg_avail +// TODO: create freertos task that monitors the pbdrv_i2c_msg_avail flag and +// calls pbdrv_i2c_recv when a message is received + + +/** + * \note this function is called from the I2C ISR, and should return as quickly + * as possible. + */ +static void recv_event(i2c_inst_t *i2c, i2c_slave_event_t event) { + // message needs to be handled first + if (pbdrv_i2c_msg_avail) return; + + switch (event) { + case I2C_SLAVE_RECEIVE: { + if (size == BUF_SIZE) return; + data[size++] = i2c_read_byte_raw(PBDRV_I2C); + break; + } + case I2C_SLAVE_FINISH: { + // TODO: handle this w/ queue mechanism instead? + // pbdrv_i2c_recv(data, size); + pbdrv_i2c_msg_avail = true; + size = 0; + break; + } + default: break; + } +} + +void pbdrv_setup() { + i2c_init(PBDRV_I2C, PB_CLOCK_SPEED_HZ); + i2c_slave_init(PBDRV_I2C, PBDRV_MOD_ADDR, &recv_event); +} + +/** + * While the RP2040's datasheet claims it supports multi-master configurations + * by implementing bus arbitration, it does not natively support a mode where + * it is configured as a (multi-)master with a slave address, such that it can + * be addressed by other multi-masters. This function includes a hacky + * workaround that teporarily sets the RP2040 to I2C master mode to send a + * message, and then restores it back to slave mode. + * + * This approach results in some received frames being (partially) dropped in + * the time period between the invocation of this function and the bus becoming + * idle (and the message is sent). + */ +__weak void pbdrv_i2c_send(i2c_addr_t addr, const uint8_t * buf, size_t sz) { + i2c_set_slave_mode(PBDRV_I2C, false, PBDRV_MOD_ADDR); + + // false to write stop condition to i2c bus + i2c_write_timeout_us(PBDRV_I2C, addr, buf, sz, false, PB_TIMEOUT_US); + + i2c_set_slave_mode(PBDRV_I2C, true, PBDRV_MOD_ADDR); +} + diff --git a/lib/pbdrv/drv/rp2040/mod.h b/lib/pbdrv/drv/rp2040/mod.h new file mode 100644 index 0000000..0cf2e63 --- /dev/null +++ b/lib/pbdrv/drv/rp2040/mod.h @@ -0,0 +1,13 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +//! puzzle bus driver setup +void pbdrv_setup(); + +#ifdef __cplusplus +} +#endif + diff --git a/lib/pbdrv/lib b/lib/pbdrv/lib new file mode 120000 index 0000000..58677dd --- /dev/null +++ b/lib/pbdrv/lib @@ -0,0 +1 @@ +../../lib
\ No newline at end of file diff --git a/lib/pbdrv/makefile b/lib/pbdrv/makefile new file mode 100644 index 0000000..c87d1af --- /dev/null +++ b/lib/pbdrv/makefile @@ -0,0 +1,4 @@ +TARGET = $(BUILD_DIR)/libpbdrv.a + +include ../../lazy.mk + diff --git a/lib/pbdrv/mod/main.h b/lib/pbdrv/mod/main.h new file mode 100644 index 0000000..535ce06 --- /dev/null +++ b/lib/pbdrv/mod/main.h @@ -0,0 +1,13 @@ +#pragma once + +#include "../types.h" + +typedef struct { + const i2c_addr_t mod_addr; + const pb_global_state_t mod_state; +} pb_mod_main_mod_t; + +enum { + PB_MOD_MAIN_ADDR_MODS = 0x01, //!< connected puzzle modules +}; + diff --git a/lib/pbdrv/pb-mod.c b/lib/pbdrv/pb-mod.c new file mode 100644 index 0000000..740f2a5 --- /dev/null +++ b/lib/pbdrv/pb-mod.c @@ -0,0 +1,26 @@ +#include "pb-types.h" +#include "pb.h" + +//! fallback module name +__weak const char * PBDRV_MOD_NAME = "???"; + +//! [private] placeholder global state variable +static pb_global_state_t _global_state = PB_GS_NOINIT; + +//! [private] main controller global state +static pb_global_state_t _main_state = PB_GS_NOINIT; + +// __weak enum pb_state pbdrv_hook_mod_state_read() { +// return _global_state; +// } + +// __weak void pbdrv_hook_mod_state_write(enum pb_state state) { +// _global_state = state; +// } + +__weak void pbdrv_i2c_recv(const uint8_t * buf, size_t sz) { + return; +} + +__weak void pbdrv_hook_main_state_update(pb_global_state_t state) { } + diff --git a/lib/pbdrv/pb-mod.h b/lib/pbdrv/pb-mod.h new file mode 100644 index 0000000..fa290bf --- /dev/null +++ b/lib/pbdrv/pb-mod.h @@ -0,0 +1,34 @@ +#pragma once + +/** + * \file puzzle bus driver implementation + * + * Most \c pbdrv_* functions have a weak implementation, which may be + * overwritten by a custom implementation. This allows you to use the default + * implementation where possible, and only implement extensions required for + * your puzzle module. Please see spec.adoc for more information about how to + * use the puzzle bus driver library. + */ + +#include <stdint.h> +#include <stddef.h> +#include <stdbool.h> + +#include "pb-types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +//! puzzle module name (optional, default = "???") +extern const char * PBDRV_MOD_NAME; +//! puzzle module bus address (required) +extern const i2c_addr_t PBDRV_MOD_ADDR; + +void pbdrv_i2c_recv(const uint8_t * buf, size_t sz); +void pbdrv_i2c_send(i2c_addr_t i2c_addr, const uint8_t * buf, size_t sz); + +#ifdef __cplusplus +} +#endif + diff --git a/lib/pbdrv/pb-read.c b/lib/pbdrv/pb-read.c new file mode 100644 index 0000000..843420d --- /dev/null +++ b/lib/pbdrv/pb-read.c @@ -0,0 +1,3 @@ +#include "pb-read.h" + + diff --git a/lib/pbdrv/pb-read.h b/lib/pbdrv/pb-read.h new file mode 100644 index 0000000..7a6f0df --- /dev/null +++ b/lib/pbdrv/pb-read.h @@ -0,0 +1,10 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef __cplusplus +} +#endif + diff --git a/lib/pbdrv/pb-types.h b/lib/pbdrv/pb-types.h new file mode 100644 index 0000000..96ffc37 --- /dev/null +++ b/lib/pbdrv/pb-types.h @@ -0,0 +1,98 @@ +#pragma once +#include <stdint.h> +#include <stddef.h> + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef __GNUC__ +#define __weak __attribute__((weak)) +#endif +#ifndef __weak +#error Could not determine weak attribute for current compiler +#define __weak +#endif + +typedef uint16_t i2c_addr_t; + +//! puzzle bus command types +enum pb_cmd_id { + PB_CMD_REQ_READ, //!< request a puzzle module property + PB_CMD_RES_READ, //!< respond to a puzzle module property request + PB_CMD_REQ_WRITE, //!< request to write a puzzle module property + PB_CMD_REQ_STATE, //!< request global state + PB_CMD_RES_STATE, //!< respond to a global state request + PB_CMD_REQ_SET_STATE, //!< request to overwrite module global state + PB_CMD_MAGIC, //!< magic message (regular i2c command) +}; +typedef enum pb_cmd_id pb_cmd_id_t; + +//! magic sent from main controller to puzzle module +static const char pb_cmd_magic_msg[] = { 0x70, 0x75, 0x7a, 0x62, 0x75, 0x73 }; +//! magic reply from puzzle module back to main controller +static const char pb_cmd_magic_res[] = { 0x67, 0x61, 0x6d, 0x69, 0x6e, 0x67 }; + +//! 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 +}; +typedef enum pb_global_state pb_global_state_t; + +//! puzzle bus message header (shared by all commands) +typedef struct { + const pb_cmd_id_t type; //!< command type + const i2c_addr_t sender; //!< i2c address of sender +} pb_msg_header_t; + +//! PB_CMD_REQ_READ data +typedef struct { + const pb_msg_header_t header; + const uint8_t propid; //!< state property id to return +} pb_cmd_req_read_t; + +//! PB_CMD_RES_READ data +typedef struct { + const pb_msg_header_t header; + const uint8_t propid; //!< id of returned state property + const uint8_t value[]; +} pb_cmd_res_read_t; + +//! PB_CMD_REQ_WRITE data +typedef struct { + const pb_msg_header_t header; + const uint8_t propid; //!< state property id to write + const uint8_t value[]; //!< new value of property +} pb_cmd_req_write_t; + +//! PB_CMD_REQ_STATE data +typedef struct { + const pb_msg_header_t header; + const pb_global_state_t state; //!< global state of sender +} pb_cmd_req_state_t; + +//! PB_CMD_RES_STATE data +typedef struct { + const pb_msg_header_t header; + const pb_global_state_t state; //!< global state of sender +} pb_cmd_res_state_t; + +//! PB_CMD_REQ_SET_STATE data +typedef struct { + const pb_msg_header_t header; + const pb_global_state_t state; //!< new global state +} pb_cmd_req_set_state_t; + +//! PB_CMD_MAGIC data +typedef struct { + const pb_msg_header_t header; + const char magic[]; //!< magic value +} pb_cmd_magic_t; + +#ifdef __cplusplus +} +#endif + diff --git a/lib/pbdrv/pb-write.c b/lib/pbdrv/pb-write.c new file mode 100644 index 0000000..752a4ac --- /dev/null +++ b/lib/pbdrv/pb-write.c @@ -0,0 +1,35 @@ +#include <mpack.h> + +#include "pb-write.h" + +typedef struct { + mpack_writer_t writer; + pbdrv_buf_t buf; +} pbdrv_writer_t; + +static pbdrv_writer_t pbdrv_write_init() { + pbdrv_writer_t writer; + mpack_writer_init_growable(&writer.writer, &writer.buf.data, &writer.buf.size); + return writer; +} + +static pbdrv_buf_t pbdrv_write_finish(pbdrv_writer_t * writer) { + if (mpack_writer_destroy(&writer->writer) != mpack_ok) { + writer->buf.data = NULL; + writer->buf.size = 0; + } + return writer->buf; +} + +static void pbdrv_write_msg_header(pbdrv_writer_t * writer, pb_msg_header_t header) { + mpack_write_u8(&writer->writer, header.type); + mpack_write_u16(&writer->writer, header.sender); +} + +pbdrv_buf_t pbdrv_write_cmd_req_set_state(pb_cmd_req_set_state_t data) { + pbdrv_writer_t writer = pbdrv_write_init(); + pbdrv_write_msg_header(&writer, data.header); + mpack_write_u8(&writer.writer, data.state); + return pbdrv_write_finish(&writer); +} + diff --git a/lib/pbdrv/pb-write.h b/lib/pbdrv/pb-write.h new file mode 100644 index 0000000..3245898 --- /dev/null +++ b/lib/pbdrv/pb-write.h @@ -0,0 +1,26 @@ +#pragma once + +#include "pb-types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +//! binary buffer struct +typedef struct { + char * data; //! pointer to data + size_t size; //! size of data +} pbdrv_buf_t; + +pbdrv_buf_t pbdrv_write_cmd_req_read(pb_cmd_req_read_t data); +pbdrv_buf_t pbdrv_write_cmd_res_read(pb_cmd_res_read_t data); +pbdrv_buf_t pbdrv_write_cmd_req_write(pb_cmd_req_write_t data); +pbdrv_buf_t pbdrv_write_cmd_req_state(pb_cmd_req_state_t data); +pbdrv_buf_t pbdrv_write_cmd_res_state(pb_cmd_res_state_t data); +pbdrv_buf_t pbdrv_write_cmd_req_set_state(pb_cmd_req_set_state_t data); +pbdrv_buf_t pbdrv_write_cmd_magic(pb_cmd_magic_t data); + +#ifdef __cplusplus +} +#endif + diff --git a/lib/pbdrv/pb.h b/lib/pbdrv/pb.h new file mode 100644 index 0000000..b6efed0 --- /dev/null +++ b/lib/pbdrv/pb.h @@ -0,0 +1,22 @@ +#pragma once + +#define PB_CLOCK_SPEED_HZ 100000 +#define PB_TIMEOUT_MS 10 +#define PB_TIMEOUT_US (1e3 * PB_TIMEOUT_MS) + +// Adafruit NeoTrellis modules +#define PB_ADDR_ADA_NEO_1 0x2E +#define PB_ADDR_ADA_NEO_2 0x2F +#define PB_ADDR_ADA_NEO_3 0x30 +#define PB_ADDR_ADA_NEO_4 0x32 + +// TODO: ??? +#define PB_ADDR_MOD_NEOTRELLIS 0 +#define PB_ADDR_MOD_SOFTWARE 0 +#define PB_ADDR_MOD_HARDWARE 0 +#define PB_ADDR_MOD_VAULT 0 +// #define BUSADDR_MOD_AUTOMATION 0 + +// main controller +#define PB_ADDR_MOD_MAIN 0x00 + diff --git a/lib/pbdrv/spec.adoc b/lib/pbdrv/spec.adoc new file mode 100644 index 0000000..3172e84 --- /dev/null +++ b/lib/pbdrv/spec.adoc @@ -0,0 +1,133 @@ += Puzzle module specification + +This folder contains an implementation of the puzzle bus protocol +specification, and is targeted at puzzle module developers. This document +describes the required implementation steps for integrating a new game into the +puzzle module framework. + +== The bus + +The puzzle bus carries data over a standard I^2^C bus. Additional details about +this bus can be found in the link:../../docs/design.adoc[Design document]. + +The following details are important to puzzle module developers, as they may +cause unexpected behavior: + +- *Addresses influence the puzzle box's behavior*. The order of puzzles is + determined by the puzzle module address. Two puzzle modules may use the same + address, but this will mean that they cannot be used simultaniously in the + same puzzle box. Known addresses are documented in link:bus.h[]. +- *The read/write bit of an I^2^C frame determines how it's handled*. I^2^C + *read* frames are treated as requests, while *write* frames are treated as + responses. + +== Puzzle bus driver (pbdrv) + +The library in this folder is a partial implementation of the puzzle bus +specification *for puzzle modules*. Most functions in the driver are marked +with the 'weak' attribute, which allows you to override them by providing an +implementation. + +In order to utilize this driver, the following must be done: + +- The ``pbdrv_i2c_recv`` function must be *called* for every received *I^2^C + read* frame +- The ``pbdrv_i2c_send`` function must be *implemented* with the + platform-specific *I^2^C write* function + +This is enough to get the puzzle module registered. You may also want to +implement some of the following integrations: + +- If your game uses the global state variable, you should implement the + <<sec:state-global,global state hooks>> to point the driver to your own + global state variable, and be notified of reads/writes to it. +- If you want to expose additional game state variables over the puzzle bus, + you should implement the <<sec:state-aux,auxiliary state hooks>>. +- If you want to implement custom puzzle bus commands, you can implement the + <<sec:cmd,command hook>>. + +All other kinds of integrations/hooks can likely be realized by overriding the +default implementations, but this is discouraged. + +[[sec:state-global]] +== Global state + +If your puzzle module defines its own global ``enum pb_state``, you can tell +the driver to use it by implementing the ``pbdrv_hook_state_read`` and +``pbdrv_hook_state_write`` functions. These functions are also used by the +default implementation of the read/write commands to address 0 (global state). + +Example: + +```c +pb_state_t global_state = PB_GS_NOINIT; + +pb_state_t pbdrv_hook_mod_state_read() { + return global_state; +} + +void pbdrv_hook_mod_state_write(pb_state_t state) { + global_state = state; +} +``` + +[[sec:state-aux]] +== Auxiliary state + +You can expose additional state variables by implementing the +``pbdrv_hook_read`` and ``pbdrv_hook_write`` functions. These functions should +return ``true`` for state addresses you want to override. + +Example: + +```c +#define CUSTOM_VAR_ADDR 0x01 +uint8_t my_custom_variable = 10; + +bool pbdrv_hook_read(uint16_t i2c_addr, uint8_t addr) { + switch (addr) { + case CUSTOM_VAR_ADDR: { + char res[] = { PB_CMD_READ, addr, my_custom_variable }; + pbdrv_i2c_send(i2c_addr, res, sizeof(res)); + break; + } + default: return false; + } + + return true; +} + +bool pbdrv_hook_write(uint16_t i2c_addr, uint8_t addr, const char * buf, size_t sz) { + switch (addr) { + case CUSTOM_VAR_ADDR: { + if (sz != 1) return false; + my_custom_variable = buf[0]; + break; + } + default: return false; + } + + return true; +} +``` + +[[sec:cmd]] +== Custom commands + +Similar to the auxiliary state, custom commands can be added by implementing +the ``pbdrv_hook_cmd`` function, which should return ``true`` for the +command(s) that you want to overwrite. + +Example: + +```c +bool pbdrv_hook_cmd(uint16_t i2c_addr, enum pb_cmd cmd, const char * buf, size_t sz) { + if (cmd == 0x54) { + printf("custom command received!\n"); + return true; + } + + return false; +} +``` + |