diff --git a/app/src/split/CMakeLists.txt b/app/src/split/CMakeLists.txt index 27abb82ad06..d4b673571b9 100644 --- a/app/src/split/CMakeLists.txt +++ b/app/src/split/CMakeLists.txt @@ -3,4 +3,8 @@ if (CONFIG_ZMK_SPLIT_BLE) add_subdirectory(bluetooth) -endif() \ No newline at end of file +endif() + +if (CONFIG_ZMK_SPLIT_SERIAL) + add_subdirectory(serial) +endif() diff --git a/app/src/split/Kconfig b/app/src/split/Kconfig index dbe5f092644..187d41998e8 100644 --- a/app/src/split/Kconfig +++ b/app/src/split/Kconfig @@ -18,9 +18,13 @@ config ZMK_SPLIT_BLE select BT_USER_PHY_UPDATE select BT_AUTO_PHY_UPDATE +config ZMK_SPLIT_SERIAL + bool "Serial" + endchoice #ZMK_SPLIT endif rsource "bluetooth/Kconfig" +rsource "serial/Kconfig" diff --git a/app/src/split/serial/CMakeLists.txt b/app/src/split/serial/CMakeLists.txt new file mode 100644 index 00000000000..42f9c956cb3 --- /dev/null +++ b/app/src/split/serial/CMakeLists.txt @@ -0,0 +1,13 @@ +# Copyright (c) 2022 The ZMK Contributors +# SPDX-License-Identifier: MIT +target_link_libraries(app PRIVATE + COBS +) + +if (CONFIG_ZMK_SPLIT_ROLE_CENTRAL) + target_sources(app PRIVATE central.c) + target_sources(app PRIVATE interrupt.c) +else () + target_sources(app PRIVATE peripheral.c) + target_sources(app PRIVATE polling.c) +endif() diff --git a/app/src/split/serial/Kconfig b/app/src/split/serial/Kconfig new file mode 100644 index 00000000000..f2ccdf65157 --- /dev/null +++ b/app/src/split/serial/Kconfig @@ -0,0 +1,44 @@ +# Copyright (c) 2022 The ZMK Contributors +# SPDX-License-Identifier: MIT + +if ZMK_SPLIT && ZMK_SPLIT_SERIAL + +menu "Serial Transport" + +if ZMK_SPLIT_ROLE_CENTRAL + +config ZMK_SPLIT_SERIAL_CENTRAL_PRIORITY + int "Serial split peripheral workqueue thread priority" + default 5 + +config ZMK_SPLIT_SERIAL_CENTRAL_POSITION_QUEUE_SIZE + int "Max number of key position state events to queue when received from peripherals" + default 5 + +config ZMK_SPLIT_SERIAL_CENTRAL_RUN_STACK_SIZE + int "Serial split central write thread stack size" + default 512 + +endif # ZMK_SPLIT_ROLE_CENTRAL + +if !ZMK_SPLIT_ROLE_CENTRAL + +config ZMK_SPLIT_SERIAL_PERIPHERAL_STACK_SIZE + int "Serial split peripheral notify thread stack size" + default 650 + +config ZMK_SPLIT_SERIAL_PERIPHERAL_PRIORITY + int "Serial split peripheral notify thread priority" + default 5 + +config ZMK_SPLIT_SERIAL_PERIPHERAL_POSITION_QUEUE_SIZE + int "Max number of key position state events to queue to send to the central" + default 10 + +#!ZMK_SPLIT_ROLE_CENTRAL +endif + +endmenu + +#ZMK_SPLIT_SERIAL +endif diff --git a/app/src/split/serial/central.c b/app/src/split/serial/central.c new file mode 100644 index 00000000000..986c0992c6c --- /dev/null +++ b/app/src/split/serial/central.c @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#include "private.h" + +/* Let's provide enough space for multiple messages to reduce the risk of + * having to drop new ones. */ +RING_BUF_DECLARE(zmk_split_serial_rx_ringbuf, MAX_MESSAGE_LEN * 2); + +static void on_rx_done(struct net_buf_simple *const buf) { + static uint8_t positions[POSITION_STATE_DATA_LEN]; + const int64_t timestamp = k_uptime_get(); + + if (buf->len < 3) { + LOG_ERR("Message is smaller than it's header"); + return; + } + + const uint16_t crc_received = net_buf_simple_remove_le16(buf); + const uint16_t crc_calculated = crc16_ccitt(0, buf->data, buf->len); + if (crc_received != crc_calculated) { + LOG_ERR("Invalid checksum. received=%04X calculated=%04x", crc_received, crc_calculated); + return; + } + + const uint8_t event_type = net_buf_simple_pull_u8(buf); + switch (event_type) { + case SPLIT_EVENT_POSITION: { + const uint8_t *const new_positions = buf->data; + const size_t new_positions_len = buf->len; + + if (new_positions_len > ARRAY_SIZE(positions)) { + LOG_ERR("Got %zu positions but we only support %zu", new_positions_len, + ARRAY_SIZE(positions)); + return; + } + + for (size_t positions_index = 0; positions_index < new_positions_len; + positions_index += 1) { + + const uint8_t state = new_positions[positions_index]; + const uint8_t changed = state ^ positions[positions_index]; + positions[positions_index] = state; + + for (size_t bit_index = 0; bit_index < 8; bit_index += 1) { + if (changed & BIT(bit_index)) { + const struct zmk_position_state_changed ev = { + .source = 0, + .position = positions_index * 8 + bit_index, + .state = state & BIT(bit_index), + .timestamp = timestamp, + }; + LOG_DBG("Trigger key position state change for %d", ev.position); + + ZMK_EVENT_RAISE(new_zmk_position_state_changed(ev)); + } + } + } + break; + } + default: + LOG_ERR("Unsupported event type: %02X", event_type); + break; + } +} + +static void rx_work_handler(struct k_work *work) { + ARG_UNUSED(work); + + NET_BUF_SIMPLE_DEFINE_STATIC(rx_buf, MAX_MESSAGE_LEN); + static struct cobs_decode cobs_decode; + + for (;;) { + uint8_t encoded_byte; + const size_t num_read = + ring_buf_get(&zmk_split_serial_rx_ringbuf, &encoded_byte, sizeof(encoded_byte)); + if (num_read == 0) { + /* No data, we're done here. */ + return; + } + __ASSERT_NO_MSG(num_read == 1); + + uint8_t decoded_byte = 0x00; + bool decoded_byte_available = false; + enum cobs_decode_result decode_result = + cobs_decode_stream(&cobs_decode, encoded_byte, &decoded_byte, &decoded_byte_available); + + if (decoded_byte_available) { + if (net_buf_simple_tailroom(&rx_buf) == 0) { + LOG_DBG("message is too big"); + net_buf_simple_reset(&rx_buf); + cobs_decode_reset(&cobs_decode); + continue; + } + + net_buf_simple_add_u8(&rx_buf, decoded_byte); + } + + switch (decode_result) { + case COBS_DECODE_RESULT_CONSUMED: + break; + + case COBS_DECODE_RESULT_FINISHED: { + cobs_decode_reset(&cobs_decode); + on_rx_done(&rx_buf); + net_buf_simple_reset(&rx_buf); + break; + } + + case COBS_DECODE_RESULT_UNEXPECTED_ZERO: + LOG_DBG("unexpected zero in COBS data"); + net_buf_simple_reset(&rx_buf); + cobs_decode_reset(&cobs_decode); + break; + + case COBS_DECODE_RESULT_ERROR: + LOG_DBG("COBS error"); + net_buf_simple_reset(&rx_buf); + cobs_decode_reset(&cobs_decode); + break; + } + } +} +K_WORK_DEFINE(zmk_split_serial_rx_work, rx_work_handler); diff --git a/app/src/split/serial/interrupt.c b/app/src/split/serial/interrupt.c new file mode 100644 index 00000000000..5872bf36d5e --- /dev/null +++ b/app/src/split/serial/interrupt.c @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include + +#include +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#include "private.h" + +static const struct device *const uart_dev = DEVICE_DT_GET(DT_CHOSEN(zmk_split_serial)); + +static void clear_fifo(const struct device *const dev) { + uint8_t c; + while (uart_fifo_read(dev, &c, 1) > 0) { + } +} + +/** + * Read from the FIFO until it returns a size of 0. + * + * This is a workaround because some drivers read max 1 character per call. + */ +static int uart_fifo_read_all(const struct device *dev, uint8_t *rx_data, int size) { + int ret; + int num_read = 0; + + while (size > 0) { + ret = uart_fifo_read(dev, rx_data, size); + if (ret < 0) { + LOG_ERR("Failed to read fifo: %d", ret); + return ret; + } + if (ret == 0) { + break; + } + + __ASSERT_NO_MSG(num_read <= size); + + rx_data += ret; + size -= ret; + num_read += ret; + } + + return num_read; +} + +static void irq_rx_callback(const struct device *const dev) { + int ret; + bool had_data = false; + + for (;;) { + uint8_t *data = NULL; + const uint32_t max_size = + ring_buf_put_claim(&zmk_split_serial_rx_ringbuf, &data, UINT32_MAX); + if (max_size == 0) { + clear_fifo(uart_dev); + break; + } + const int max_size_int = MIN(max_size, INT_MAX); + + uint32_t num_read; + ret = uart_fifo_read_all(dev, data, max_size_int); + if (ret < 0) { + LOG_ERR("Failed to read fifo: %d", ret); + num_read = 0; + } else { + num_read = ret; + had_data = true; + } + + ret = ring_buf_put_finish(&zmk_split_serial_rx_ringbuf, num_read); + __ASSERT_NO_MSG(ret == 0); + + if (num_read < max_size_int) { + break; + } + + /* There's may still be data in the FIFO, if: + * - The ring buffer didn't return it's full capacity because + * it's about to wrap. Another attempt will return the rest. + * - In between claim and finish, data was read from the ring + * buffer so another attempt will return more data. + * - The ring buffer is still full. Another attempt will stop + * the loop. + */ + } + + if (had_data) { + k_work_submit(&zmk_split_serial_rx_work); + } +} + +static void irq_callback(const struct device *const dev, void *const user_data) { + ARG_UNUSED(dev); + + if (!uart_irq_update(dev)) { + return; + } + + if (uart_irq_rx_ready(dev)) { + irq_rx_callback(dev); + } +} + +static int init(const struct device *dev) { + ARG_UNUSED(dev); + + if (!device_is_ready(uart_dev)) { + LOG_ERR("split uart device is not ready"); + return -EAGAIN; + } + + uart_irq_rx_disable(uart_dev); + uart_irq_tx_disable(uart_dev); + clear_fifo(uart_dev); + + uart_irq_callback_user_data_set(uart_dev, irq_callback, NULL); + uart_irq_rx_enable(uart_dev); + + return 0; +} +SYS_INIT(init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); diff --git a/app/src/split/serial/peripheral.c b/app/src/split/serial/peripheral.c new file mode 100644 index 00000000000..9fc3929edce --- /dev/null +++ b/app/src/split/serial/peripheral.c @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#include "private.h" + +K_THREAD_STACK_DEFINE(service_q_stack, CONFIG_ZMK_SPLIT_SERIAL_PERIPHERAL_STACK_SIZE); +static struct k_work_q service_work_q; + +static uint8_t position_state[POSITION_STATE_DATA_LEN]; +K_MSGQ_DEFINE(position_state_msgq, sizeof(position_state), + CONFIG_ZMK_SPLIT_SERIAL_PERIPHERAL_POSITION_QUEUE_SIZE, 4); +NET_BUF_SIMPLE_DEFINE_STATIC(message_buf, MAX_MESSAGE_LEN); +static uint8_t tx_buf[COBS_MAX_ENCODED_SIZE(MAX_MESSAGE_LEN) + 1]; + +static void send_position_handler(struct k_work *work) { + ARG_UNUSED(work); + + uint8_t state[sizeof(position_state)]; + while (k_msgq_get(&position_state_msgq, &state, K_NO_WAIT) == 0) { + LOG_INF("send position"); + + net_buf_simple_reset(&message_buf); + net_buf_simple_add_u8(&message_buf, SPLIT_EVENT_POSITION); + net_buf_simple_add_mem(&message_buf, state, sizeof(state)); + + const uint16_t crc = crc16_ccitt(0, message_buf.data, message_buf.len); + net_buf_simple_add_le16(&message_buf, crc); + + const size_t encoded_length = cobs_encode(message_buf.data, message_buf.len, tx_buf); + tx_buf[encoded_length] = 0x00; + zmk_split_serial_send(tx_buf, encoded_length + 1); + } +}; +K_WORK_DEFINE(send_position_work, send_position_handler); + +static int queue_sending_position_state(void) { + int err = k_msgq_put(&position_state_msgq, position_state, K_MSEC(100)); + if (err) { + switch (err) { + case -EAGAIN: { + LOG_WRN("Position state message queue full, popping first message and queueing again"); + uint8_t discarded_state[sizeof(position_state)]; + k_msgq_get(&position_state_msgq, &discarded_state, K_NO_WAIT); + return queue_sending_position_state(); + } + default: + LOG_WRN("Failed to queue position state to send (%d)", err); + return err; + } + } + + k_work_submit_to_queue(&service_work_q, &send_position_work); + + return 0; +} + +static int position_pressed(const uint8_t position) { + WRITE_BIT(position_state[position / 8], position % 8, true); + return queue_sending_position_state(); +} + +static int position_released(const uint8_t position) { + WRITE_BIT(position_state[position / 8], position % 8, false); + return queue_sending_position_state(); +} + +#if ZMK_KEYMAP_HAS_SENSORS +K_MSGQ_DEFINE(sensor_state_msgq, sizeof(struct sensor_event), + CONFIG_ZMK_SPLIT_SERIAL_PERIPHERAL_POSITION_QUEUE_SIZE, 4); + +static void send_sensor_state_callback(struct k_work *work) { + while (k_msgq_get(&sensor_state_msgq, &last_sensor_event, K_NO_WAIT) == 0) { + LOG_INF("send sensor state"); + // TODO + } +}; +K_WORK_DEFINE(service_sensor_notify_work, send_sensor_state_callback); + +static int send_sensor_state(struct sensor_event ev) { + int err = k_msgq_put(&sensor_state_msgq, &ev, K_MSEC(100)); + if (err) { + // retry... + switch (err) { + case -EAGAIN: { + LOG_WRN("Sensor state message queue full, popping first message and queueing again"); + struct sensor_event discarded_state; + k_msgq_get(&sensor_state_msgq, &discarded_state, K_NO_WAIT); + return send_sensor_state(ev); + } + default: + LOG_WRN("Failed to queue sensor state to send (%d)", err); + return err; + } + } + + k_work_submit_to_queue(&service_work_q, &service_sensor_notify_work); + return 0; +} + +static int sensor_triggered(uint8_t sensor_index, + const struct zmk_sensor_channel_data channel_data[], + size_t channel_data_size) { + if (channel_data_size > ZMK_SENSOR_EVENT_MAX_CHANNELS) { + return -EINVAL; + } + + struct sensor_event ev = + (struct sensor_event){.sensor_index = sensor_index, .channel_data_size = channel_data_size}; + memcpy(ev.channel_data, channel_data, + channel_data_size * sizeof(struct zmk_sensor_channel_data)); + return send_sensor_state(ev); +} +#endif /* ZMK_KEYMAP_HAS_SENSORS */ + +static int split_listener(const zmk_event_t *eh) { + LOG_DBG(""); + const struct zmk_position_state_changed *pos_ev; + if ((pos_ev = as_zmk_position_state_changed(eh)) != NULL) { + if (pos_ev->state) { + return position_pressed(pos_ev->position); + } else { + return position_released(pos_ev->position); + } + } + +#if ZMK_KEYMAP_HAS_SENSORS + const struct zmk_sensor_event *sensor_ev; + if ((sensor_ev = as_zmk_sensor_event(eh)) != NULL) { + return sensor_triggered(sensor_ev->sensor_index, sensor_ev->channel_data, + sensor_ev->channel_data_size); + } +#endif /* ZMK_KEYMAP_HAS_SENSORS */ + + return ZMK_EV_EVENT_BUBBLE; +} +ZMK_LISTENER(split_listener, split_listener); +ZMK_SUBSCRIPTION(split_listener, zmk_position_state_changed); +#if ZMK_KEYMAP_HAS_SENSORS +ZMK_SUBSCRIPTION(split_listener, zmk_sensor_event); +#endif /* ZMK_KEYMAP_HAS_SENSORS */ + +static int init(const struct device *_arg) { + static const struct k_work_queue_config queue_config = { + .name = "Split Peripheral Notification Queue"}; + k_work_queue_start(&service_work_q, service_q_stack, K_THREAD_STACK_SIZEOF(service_q_stack), + CONFIG_ZMK_SPLIT_SERIAL_PERIPHERAL_PRIORITY, &queue_config); + + return 0; +} +SYS_INIT(init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); diff --git a/app/src/split/serial/polling.c b/app/src/split/serial/polling.c new file mode 100644 index 00000000000..08ccc99ebf0 --- /dev/null +++ b/app/src/split/serial/polling.c @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +#include +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#include "private.h" + +static const struct device *const uart_dev = DEVICE_DT_GET(DT_CHOSEN(zmk_split_serial)); + +void zmk_split_serial_send(const void *const data_, const size_t length) { + const uint8_t *const data = data_; + for (size_t position = 0; position < length; position += 1) { + uart_poll_out(uart_dev, data[position]); + } +} + +static int init(const struct device *dev) { + ARG_UNUSED(dev); + + if (!device_is_ready(uart_dev)) { + LOG_ERR("split uart device is not ready"); + return -EAGAIN; + } + + return 0; +} +SYS_INIT(init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); diff --git a/app/src/split/serial/private.h b/app/src/split/serial/private.h new file mode 100644 index 00000000000..230857c17d7 --- /dev/null +++ b/app/src/split/serial/private.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#ifndef PRIVATE_H +#define PRIVATE_H + +#include +#include +#include + +#define POSITION_STATE_DATA_LEN 16 +/* event_type, event_payload, CRC16 */ +#define MAX_MESSAGE_LEN \ + (sizeof(uint8_t) + MAX(POSITION_STATE_DATA_LEN, sizeof(struct sensor_event)) + sizeof(uint16_t)) + +#define SPLIT_EVENT_POSITION 0 +#define SPLIT_EVENT_SENSOR 1 + +struct sensor_event { + uint8_t sensor_index; + + uint8_t channel_data_size; + struct zmk_sensor_channel_data channel_data[ZMK_SENSOR_EVENT_MAX_CHANNELS]; +} __packed; + +#ifdef CONFIG_ZMK_SPLIT_ROLE_CENTRAL +extern struct ring_buf zmk_split_serial_rx_ringbuf; +extern struct k_work zmk_split_serial_rx_work; +#else +void zmk_split_serial_send(const void *const data, const size_t length); +#endif + +#endif /* PRIVATE_H */ diff --git a/app/west.yml b/app/west.yml index ffa36ca36e9..92a9787468f 100644 --- a/app/west.yml +++ b/app/west.yml @@ -4,6 +4,9 @@ manifest: url-base: https://github.com/zephyrproject-rtos - name: zmkfirmware url-base: https://github.com/zmkfirmware + - name: grandcentrix + url-base: https://github.com/grandcentrix + projects: - name: zephyr remote: zmkfirmware @@ -30,5 +33,11 @@ manifest: - edtt - trusted-firmware-m - sof + - name: cobs + repo-path: cobs + remote: grandcentrix + revision: 0876ef80209618b10508af728731f4944f9d2fd2 + path: modules/lib/cobs + self: west-commands: scripts/west-commands.yml