rushabhcodes/fidget-counter
A tscircuit package created by rushabhcodes.
- Version
- 1.0.31
- License
- unset
- Stars
- 0
firmware/src/main.c
#include <errno.h>
#include <limits.h>
#include <string.h>
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/devicetree.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/i2c.h>
#include <zephyr/drivers/flash.h>
#include <zephyr/fs/nvs.h>
#include <zephyr/storage/flash_map.h>
#include <zephyr/sys/atomic.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/gatt.h>
#include <zephyr/bluetooth/uuid.h>
#include "rotation_counter.h"
#include "tmag3001_frame.h"
/* Custom RevC board only. No LED, UART, cap button, bootloader or SoftDevice. */
#define SERVICE_UUID BT_UUID_128_ENCODE(0x76b8a000, 0x5aa4, 0x4c10, 0xb5df, 0x0da684303f21)
#define STATE_UUID BT_UUID_128_ENCODE(0x76b8a001, 0x5aa4, 0x4c10, 0xb5df, 0x0da684303f21)
#define CONTROL_UUID BT_UUID_128_ENCODE(0x76b8a002, 0x5aa4, 0x4c10, 0xb5df, 0x0da684303f21)
#define SENSOR_FAULT 1U
#define STORAGE_FAULT 2U
#define UNSAVED 4U
#define SAMPLE_GAP 8U
#define SATURATED 16U
static const struct device *const bus = DEVICE_DT_GET(DT_NODELABEL(i2c0));
static const struct device *const gpio = DEVICE_DT_GET(DT_NODELABEL(gpio0));
static struct rotation_counter counter;
static uint8_t flags = SENSOR_FAULT;
static uint16_t current_angle;
static uint32_t sequence;
static K_MUTEX_DEFINE(state_lock);
static atomic_t control_request;
static atomic_t connected;
static atomic_t advertising_requested = ATOMIC_INIT(1);
static struct nvs_fs storage;
static bool storage_ready;
static bool sensor_ready;
static void encode_state(uint8_t data[20])
{
k_mutex_lock(&state_lock, K_FOREVER);
data[0] = 1;
data[1] = flags;
if (counter.forward == UINT32_MAX || counter.reverse == UINT32_MAX ||
UINT32_MAX - counter.forward < counter.reverse) { data[1] |= SATURATED; }
sys_put_le16(current_angle, data + 2);
sys_put_le32(counter.forward, data + 4);
sys_put_le32(counter.reverse, data + 8);
sys_put_le32(rotation_total(&counter), data + 12);
sys_put_le32(sequence, data + 16);
k_mutex_unlock(&state_lock);
}
static ssize_t read_state(struct bt_conn *conn, const struct bt_gatt_attr *attr,
void *buf, uint16_t len, uint16_t offset)
{
uint8_t data[20];
encode_state(data);
return bt_gatt_attr_read(conn, attr, buf, len, offset, data, sizeof(data));
}
static ssize_t write_control(struct bt_conn *conn, const struct bt_gatt_attr *attr,
const void *buf, uint16_t len, uint16_t offset, uint8_t write_flags)
{
ARG_UNUSED(conn); ARG_UNUSED(attr);
if (offset || (write_flags & BT_GATT_WRITE_FLAG_PREPARE)) {
return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
}
if (len != 4) { return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); }
int request = !memcmp(buf, "ZERO", 4) ? 1 : !memcmp(buf, "SAVE", 4) ? 2 : 0;
if (!request) { return BT_GATT_ERR(BT_ATT_ERR_VALUE_NOT_ALLOWED); }
if (!atomic_cas(&control_request, 0, request)) {
return BT_GATT_ERR(BT_ATT_ERR_UNLIKELY);
}
return len;
}
static void ccc_changed(const struct bt_gatt_attr *attr, uint16_t value)
{
ARG_UNUSED(attr); ARG_UNUSED(value);
}
BT_GATT_SERVICE_DEFINE(fidget_service,
BT_GATT_PRIMARY_SERVICE(BT_UUID_DECLARE_128(SERVICE_UUID)),
BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_128(STATE_UUID),
BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, BT_GATT_PERM_READ,
read_state, NULL, NULL),
BT_GATT_CCC(ccc_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_128(CONTROL_UUID),
BT_GATT_CHRC_WRITE, BT_GATT_PERM_WRITE, NULL, write_control, NULL)
);
static void on_connected(struct bt_conn *conn, uint8_t err)
{
ARG_UNUSED(conn);
if (err) { atomic_set(&advertising_requested, 1); }
else { atomic_set(&connected, 1); }
}
static void on_disconnected(struct bt_conn *conn, uint8_t reason)
{
ARG_UNUSED(conn); ARG_UNUSED(reason);
atomic_set(&connected, 0);
atomic_set(&advertising_requested, 1);
}
BT_CONN_CB_DEFINE(connection_callbacks) = {
.connected = on_connected, .disconnected = on_disconnected,
};
static const struct bt_data advertisement[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR),
BT_DATA_BYTES(BT_DATA_UUID128_ALL, SERVICE_UUID),
};
static const struct bt_data scan_response[] = {
BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME, sizeof(CONFIG_BT_DEVICE_NAME) - 1),
};
static int sensor_write(uint8_t reg, uint8_t value)
{
return i2c_reg_write_byte(bus, TMAG3001_ADDRESS, reg, value);
}
static int sensor_init(void)
{
uint8_t identity[3], check[4];
int err = i2c_burst_read(bus, TMAG3001_ADDRESS, 0x0d, identity, sizeof(identity));
if (err) { return err; }
if ((identity[0] & 0x0c) != 0x08 || identity[1] != 0x49 || identity[2] != 0x54) {
return -ENODEV;
}
/* TI SLYS053C: A2, 4x averaging, XY angle, wide XY range, 5ms W&S.
* Explicitly latch address0x34 before W&S: the ADDR pin is not sampled there.
* INT is unused in this polling pilot and P0.28 remains a high-impedance input.
*/
const uint8_t setup[][2] = {
{0x01, 0x00}, {0x00, 0x08}, {0x02, 0x31}, {0x03, 0x06},
{0x07, 0x00}, {0x08, 0x00}, {0x09, 0x00}, {0x0a, 0x00},
{0x0b, 0x00}, {0x0c, 0x69}, {0x18, 0x10}, {0x01, 0x03},
};
for (size_t i = 0; i < ARRAY_SIZE(setup); i++) {
err = sensor_write(setup[i][0], setup[i][1]);
if (err) { return err; }
}
err = i2c_burst_read(bus, TMAG3001_ADDRESS, 0x00, check, sizeof(check));
if (err) { return err; }
/* Reads may return W&S to standby. Accept only those two operating modes. */
if (check[0] != 0x08 || (check[1] != 0x00 && check[1] != 0x03) ||
check[2] != 0x31 || check[3] != 0x06) { return -EIO; }
return sensor_write(0x01, 0x03);
}
static int sensor_read(struct hall_sample *sample)
{
uint8_t frame[TMAG3001_FRAME_BYTES];
int err = i2c_burst_read(bus, TMAG3001_ADDRESS, 0x12, frame, sizeof(frame));
if (err) { return err; }
/* Rearm after every read: W&S must be restored after an interrupt/read wake. */
err = sensor_write(0x01, 0x03);
if (err) { return err; }
switch (hall_decode(frame, sample)) {
case HALL_VALID: return 0;
case HALL_NOT_READY: return -EAGAIN;
default: return -EIO;
}
}
static int storage_init(void)
{
storage.flash_device = FIXED_PARTITION_DEVICE(storage_partition);
if (!device_is_ready(storage.flash_device)) { return -ENODEV; }
storage.offset = FIXED_PARTITION_OFFSET(storage_partition);
struct flash_pages_info page;
int err = flash_get_page_info_by_offs(storage.flash_device, storage.offset, &page);
if (err) { return err; }
storage.sector_size = page.size;
storage.sector_count = FIXED_PARTITION_SIZE(storage_partition) / page.size;
if (storage.sector_count < 2) { return -EINVAL; }
err = nvs_mount(&storage);
if (err) { return err; }
uint8_t record[12];
ssize_t bytes = nvs_read(&storage, 1, record, sizeof(record));
if (bytes == -ENOENT) { return 0; }
if (bytes != (ssize_t)sizeof(record) || memcmp(record, "FCR1", 4)) { return -EIO; }
counter.forward = sys_get_le32(record + 4);
counter.reverse = sys_get_le32(record + 8);
return 0;
}
static void save_counts(void)
{
if (!storage_ready) { return; }
uint8_t record[12];
memcpy(record, "FCR1", 4);
/* Main thread owns count mutations; BLE callbacks only queue commands. */
k_mutex_lock(&state_lock, K_FOREVER);
sys_put_le32(counter.forward, record + 4);
sys_put_le32(counter.reverse, record + 8);
k_mutex_unlock(&state_lock);
ssize_t result = nvs_write(&storage, 1, record, sizeof(record));
k_mutex_lock(&state_lock, K_FOREVER);
if (result < 0) { flags |= STORAGE_FAULT; }
else { flags &= ~(UNSAVED | STORAGE_FAULT); }
sequence++;
k_mutex_unlock(&state_lock);
}
int main(void)
{
if (!device_is_ready(bus) || !device_is_ready(gpio)) { return -ENODEV; }
/* External10k pull-up exists. Do not drive the Hall INT line or unused P0.29. */
int err = gpio_pin_configure(gpio, 28, GPIO_INPUT);
if (err) { return err; }
storage_ready = storage_init() == 0;
if (!storage_ready) { flags |= STORAGE_FAULT; }
err = bt_enable(NULL);
if (err) { return err; }
uint32_t last_advertise = (uint32_t)k_uptime_get_32() - 1000U;
uint32_t last_sensor_retry = last_advertise;
uint32_t last_valid = k_uptime_get_32();
uint32_t last_motion = last_valid, last_save = last_valid, last_notify = last_valid;
k_sleep(K_MSEC(5));
for (;;) {
uint32_t now = k_uptime_get_32();
if (!atomic_get(&connected) && atomic_get(&advertising_requested) &&
(uint32_t)(now - last_advertise) >= 1000U) {
last_advertise = now;
err = bt_le_adv_start(BT_LE_ADV_PARAM(BT_LE_ADV_OPT_CONNECTABLE, 800, 1600, NULL),
advertisement, ARRAY_SIZE(advertisement),
scan_response, ARRAY_SIZE(scan_response));
if (!err || err == -EALREADY) { atomic_set(&advertising_requested, 0); }
}
if (!sensor_ready && (uint32_t)(now - last_sensor_retry) >= 1000U) {
last_sensor_retry = now;
sensor_ready = sensor_init() == 0;
if (sensor_ready) { last_valid = now; }
}
struct hall_sample sample;
err = sensor_ready ? sensor_read(&sample) : -ENODEV;
k_mutex_lock(&state_lock, K_FOREVER);
if (!err) {
last_valid = now;
current_angle = sample.angle_q4;
flags &= ~SENSOR_FAULT;
bool tracking = counter.tracking;
enum rotation_result moved = rotation_update(&counter, sample.angle_q4, now);
if (tracking && moved == ROTATION_REBASED) { flags |= SAMPLE_GAP; }
if (moved == ROTATION_MOVED || moved == ROTATION_COUNTED) { last_motion = now; }
if (moved == ROTATION_COUNTED) { flags |= UNSAVED; sequence++; }
} else if (err != -EAGAIN || (uint32_t)(now - last_valid) > 100U) {
flags |= SENSOR_FAULT;
rotation_invalidate(&counter);
sensor_ready = false;
}
int request = atomic_set(&control_request, 0);
if (request == 1) {
rotation_reset(&counter);
flags = (flags & (SENSOR_FAULT | STORAGE_FAULT)) | UNSAVED;
sequence++;
}
bool dirty = flags & UNSAVED;
k_mutex_unlock(&state_lock);
if (dirty && (request || (uint32_t)(now - last_motion) >= 2000U ||
(uint32_t)(now - last_save) >= 60000U)) {
/* A failed write is retried at most once per second. */
if (request || (uint32_t)(now - last_save) >= 1000U) {
save_counts(); last_save = now;
}
}
if (atomic_get(&connected) && (uint32_t)(now - last_notify) >= 100U) {
uint8_t data[20];
encode_state(data);
(void)bt_gatt_notify(NULL, &fidget_service.attrs[2], data, sizeof(data));
last_notify = now;
}
k_sleep(K_MSEC(10));
}
}