the repo is radiating seeed X1 vibes

This commit is contained in:
liquidraver
2026-08-08 15:19:20 +02:00
parent 17dfa78629
commit 41cc786496
44 changed files with 1420 additions and 127 deletions
+1
View File
@@ -10,6 +10,7 @@ nRF_boards=(
rak3401_1watt
wio_tracker_l1
t1000_e
meshtracker_x1
thinknode_m1
thinknode_m3
thinknode_m6
+14 -2
View File
@@ -770,14 +770,25 @@ Shutdown.
Single button; tap-count → key-code mapping comes from the board's devicetree `tap-codes` (up to 5). Typical mapping:
- 1 tap → Page next
- 2 taps → LED heartbeat toggle
- 3 taps → Buzzer toggle
- 3 taps → Notification mode (sound+vibrate → vibrate → silent → sound → …; boards with no motor fall back to a plain on/off toggle)
- 4 taps → GPS toggle
- 5 taps → Flood advert (immediate, no delay)
### 8.4 Buzzer
### 8.4 Buzzer and vibration
Non-blocking RTTTL parser on dedicated work queue. Predefined melodies for startup, shutdown, messages, ACKs. 2-second safety watchdog auto-silences on work queue stall.
Boards with a DRV2605 haptic driver (`ti,drv2605` in DT) also vibrate on every notification — `buzzer_play()` pulses the motor. The two outputs share one setting, the notification mode, which lives in `helpers/buzzer_gate.c` (always linked, same pattern as `led_gate.c`, so the CLI resolves its symbols on boards that compile no buzzer). `set buzzer 0|1|2|3` and the 3-tap button action both drive it:
| Mode | Name | Buzzer | Motor |
|------|------|--------|-------|
| 0 | silent | - | - |
| 1 | sound+vib | yes | yes |
| 2 | vibrate | - | yes |
| 3 | sound | yes | - |
Modes 2 and 3 are rejected on boards with no motor, where they would be indistinguishable from 0 and 1. The setting persists in the existing `buzzer_quiet` prefs byte — 0 and 1 keep their original meaning, 2 and 3 are new and read as "quiet" by older firmware, so a downgrade silences a node left on sound-only.
### 8.5 Doom Easter Egg
Wolf3D-style raycaster on OLED: textured walls, 2 enemy types, shooting, HUD. Bypasses CFB, writes directly to display. ~1.7KB RAM, ~5KB flash. Enabled via `CONFIG_ZEPHCORE_EASTER_EGG_DOOM`. Button UI: triple-press ENTER on Messages page. Joystick UI: Tools menu → "Doom".
@@ -852,6 +863,7 @@ Build strings and flash methods: `boards/supported_boards.md` and `boards/exampl
| RAK3401 1W | nRF52840 | SX1262+SKY66122 (30dBm) | u-blox MAX-7Q (opt) | - | I2C sensors |
| RAK WisMesh Tag | nRF52840 | SX1262 | AT6558R | - | Accelerometer, buzzer, multitap |
| T1000-E | nRF52840 | **LR1110** | AG3335 | - | Buzzer, button, multitap |
| SenseCAP MeshTracker X1 | nRF52840 | **LR2021** | AG3335M (L1+L5) | - | SPA06 barometer, DRV2605L vibration, YSN8900 RTC, QSPI 8MB, RGB LEDs, buzzer |
| ThinkNode M1 | nRF52840 | SX1262 | Air530Z | EPD 200x200 (SSD1681) | Buzzer, 2 buttons, QSPI 2MB, RGB LEDs |
| ThinkNode M3 | nRF52840 | **LR1110** | Yes | - | Buzzer, 2 buttons, RGB LEDs |
| ThinkNode M6 | nRF52840 | SX1262 | L76K | - | QSPI, RGB LEDs |
+9 -1
View File
@@ -265,7 +265,7 @@ set(ZEPHCORE_COMMON_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/zephcore_com
# Detect platform from board name and add platform-specific common config
# For boards with qualifiers like "wio_tracker_l1/nrf52840", check both the full BOARD
# string and the BOARD_QUALIFIERS which contains just the qualifier (e.g., "nrf52840")
if(BOARD MATCHES ".*nrf52.*" OR BOARD MATCHES "rak4631" OR BOARD MATCHES "rak_wismesh_tag" OR BOARD MATCHES "rak3401_1watt" OR BOARD MATCHES "wio_tracker" OR BOARD MATCHES "ikoka_nano" OR BOARD MATCHES "t1000_e" OR BOARD MATCHES "thinknode_m1" OR BOARD MATCHES "thinknode_m3" OR BOARD MATCHES "thinknode_m6" OR BOARD MATCHES "promicro_lr2021" OR BOARD MATCHES "promicro_sx1262" OR BOARD MATCHES "sensecap_solar" OR BOARD MATCHES "xiao_nrf52840" OR BOARD MATCHES "lilygo_techo" OR BOARD MATCHES "lilygo_timpulse_plus" OR BOARD MATCHES "heltec_t114" OR BOARD MATCHES "heltec_t096" OR BOARD MATCHES "gat562" OR BOARD MATCHES "muziworks_r1neo")
if(BOARD MATCHES ".*nrf52.*" OR BOARD MATCHES "rak4631" OR BOARD MATCHES "rak_wismesh_tag" OR BOARD MATCHES "rak3401_1watt" OR BOARD MATCHES "wio_tracker" OR BOARD MATCHES "ikoka_nano" OR BOARD MATCHES "t1000_e" OR BOARD MATCHES "meshtracker_x1" OR BOARD MATCHES "thinknode_m1" OR BOARD MATCHES "thinknode_m3" OR BOARD MATCHES "thinknode_m6" OR BOARD MATCHES "promicro_lr2021" OR BOARD MATCHES "promicro_sx1262" OR BOARD MATCHES "sensecap_solar" OR BOARD MATCHES "xiao_nrf52840" OR BOARD MATCHES "lilygo_techo" OR BOARD MATCHES "lilygo_timpulse_plus" OR BOARD MATCHES "heltec_t114" OR BOARD MATCHES "heltec_t096" OR BOARD MATCHES "gat562" OR BOARD MATCHES "muziworks_r1neo")
set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/nrf52_common.conf")
elseif(DEFINED BOARD_QUALIFIERS AND BOARD_QUALIFIERS MATCHES ".*nrf52.*")
set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/nrf52_common.conf")
@@ -571,6 +571,9 @@ target_sources(app PRIVATE
# LED master gate ("set leds on|off"). Always compiled: helpers/ui/ui_common.c
# only exists in UI builds, but a headless repeater still drives lora-tx-led.
helpers/led_gate.c
# Notification-mode gate. Same reason as led_gate: the CLI needs these
# symbols on boards that compile no buzzer at all.
helpers/buzzer_gate.c
)
# Boot-time hardware-RTC auto-discovery (compact raw-I2C). Always compiled so
@@ -579,6 +582,10 @@ target_sources(app PRIVATE
# present in DT.
target_sources(app PRIVATE adapters/clock/ZephyrRTCDiscover.c)
# SPA06-003 barometer. No upstream Zephyr driver; compiles to nothing when no
# goertek,spa06 node is present.
target_sources_ifdef(CONFIG_SENSOR app PRIVATE adapters/sensors/spa06.c)
# ========== Battery Curve Selection ==========
# helpers/battery_curve.c is always compiled — it provides battery_curve_lookup()
# and the weak battery_curve_default. A board-specific battery_curve.c is compiled
@@ -876,6 +883,7 @@ endif()
if(CONFIG_ZEPHCORE_UI_BUZZER)
target_sources(app PRIVATE
helpers/ui/buzzer.c
helpers/ui/haptic.c
)
endif()
+7 -1
View File
@@ -251,10 +251,16 @@ config ZEPHCORE_OFFLINE_QUEUE_SIZE
config ZEPHCORE_ACK_TABLE_SIZE
int "ACK tracking table size"
default 8
default 16
help
Number of pending ACKs that can be tracked.
Sizing this below the number of messages that can be in flight at
once costs delivery confirmations: a peer working through a backlog
can answer seconds later, and an entry evicted before its ACK
arrives leaves the message unconfirmed, prompting a retry that adds
further load. Each entry costs 16 bytes.
config ZEPHCORE_MAX_CONNECTIONS
int "Maximum server connections"
default 16
+20 -1
View File
@@ -5,7 +5,7 @@
* Auto-detects available sensors via Zephyr devicetree nodelabels.
*
* Environment sensors (temp/humidity/pressure):
* SHTC3, AHT20/DHT20/AM2301B, SHT4x, SHT3xD, BME280, BME680, BMP280, BMP388, LPS22HB
* SHTC3, AHT20/DHT20/AM2301B, SHT4x, SHT3xD, BME280, BME680, BMP280, BMP388, LPS22HB, SPA06
* MCU die temperature as fallback (nordic,nrf-temp)
*
* Power monitors (voltage/current/power):
@@ -144,6 +144,18 @@ check_pressure:
LOG_INF("Found pressure sensor: %s (BMP388)", dev->name);
goto done;
}
/* SPA06 — the two nodes are the same part at its two possible
* addresses; the one that isn't there fails its ID check. */
dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(spa06));
if (!dev || !device_is_ready(dev)) {
dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(spa06_alt));
}
if (dev && device_is_ready(dev)) {
pressure_dev = dev;
LOG_INF("Found pressure sensor: %s (SPA06)", dev->name);
goto done;
}
}
done:
@@ -204,6 +216,13 @@ int env_sensors_read(struct env_data *data)
data->pressure_hpa = sensor_value_to_float(&val) * 10.0f;
data->has_pressure = true;
}
/* Barometers carry a die temperature. It beats the MCU's own
* sensor as a fallback on boards with no dedicated temp part. */
if (!data->has_temperature &&
sensor_channel_get(pressure_dev, SENSOR_CHAN_AMBIENT_TEMP, &val) == 0) {
data->temperature_c = sensor_value_to_float(&val);
data->has_temperature = true;
}
}
/* === MCU die temperature — always read when available ===
+244
View File
@@ -0,0 +1,244 @@
/*
* Goertek SPA06-003 pressure + temperature sensor
* Copyright (c) 2026 ZephCore
* SPDX-License-Identifier: MIT
*/
#define DT_DRV_COMPAT goertek_spa06
#include <zephyr/device.h>
#include <zephyr/drivers/i2c.h>
#include <zephyr/drivers/sensor.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/byteorder.h>
#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT)
LOG_MODULE_REGISTER(spa06, CONFIG_SENSOR_LOG_LEVEL);
#define SPA06_REG_PSR_B2 0x00
#define SPA06_REG_PRS_CFG 0x06
#define SPA06_REG_TMP_CFG 0x07
#define SPA06_REG_MEAS_CFG 0x08
#define SPA06_REG_CFG_REG 0x09
#define SPA06_REG_RESET 0x0C
#define SPA06_REG_ID 0x0D
#define SPA06_REG_COEF 0x10
#define SPA06_CHIP_ID 0x11
#define SPA06_SOFT_RESET 0x09
#define SPA06_MEAS_COEF_RDY BIT(7)
#define SPA06_MEAS_SENSOR_RDY BIT(6)
#define SPA06_MEAS_CONT_BOTH 0x07
/* 8x oversampling for both channels: no result shift needed and the
* scaling factor below is the datasheet's kP/kT for that rate. */
#define SPA06_OVERSAMPLE_8 0x03
#define SPA06_RATE_1HZ 0x00
#define SPA06_SCALE_FACTOR 7864320.0f
#define SPA06_COEF_LEN 21
struct spa06_config {
struct i2c_dt_spec bus;
};
struct spa06_data {
int16_t c0, c1;
int32_t c00, c10;
int16_t c01, c11, c20, c21, c30;
int16_t c31, c40;
int32_t raw_press;
int32_t raw_temp;
};
/* 24-bit two's complement, MSB first */
static int32_t spa06_raw24(const uint8_t *buf)
{
int32_t v = ((int32_t)buf[0] << 16) | ((int32_t)buf[1] << 8) | buf[2];
return (v & 0x800000) ? (v | ~0xFFFFFF) : v;
}
static int16_t sign_extend_12(uint16_t v)
{
return (v & 0x800) ? (int16_t)(v | 0xF000) : (int16_t)v;
}
static int32_t sign_extend_20(uint32_t v)
{
return (v & 0x80000) ? (int32_t)(v | 0xFFF00000) : (int32_t)v;
}
static int spa06_read_coefficients(const struct device *dev)
{
const struct spa06_config *cfg = dev->config;
struct spa06_data *data = dev->data;
uint8_t c[SPA06_COEF_LEN];
int rc;
rc = i2c_burst_read_dt(&cfg->bus, SPA06_REG_COEF, c, sizeof(c));
if (rc < 0) {
return rc;
}
data->c0 = sign_extend_12(((uint16_t)c[0] << 4) | (c[1] >> 4));
data->c1 = sign_extend_12((((uint16_t)c[1] & 0x0F) << 8) | c[2]);
data->c00 = sign_extend_20(((uint32_t)c[3] << 12) |
((uint32_t)c[4] << 4) | (c[5] >> 4));
data->c10 = sign_extend_20((((uint32_t)c[5] & 0x0F) << 16) |
((uint32_t)c[6] << 8) | c[7]);
data->c01 = (int16_t)(((uint16_t)c[8] << 8) | c[9]);
data->c11 = (int16_t)(((uint16_t)c[10] << 8) | c[11]);
data->c20 = (int16_t)(((uint16_t)c[12] << 8) | c[13]);
data->c21 = (int16_t)(((uint16_t)c[14] << 8) | c[15]);
data->c30 = (int16_t)(((uint16_t)c[16] << 8) | c[17]);
data->c31 = sign_extend_12(((uint16_t)c[18] << 4) | (c[19] >> 4));
data->c40 = sign_extend_12((((uint16_t)c[19] & 0x0F) << 8) | c[20]);
return 0;
}
static int spa06_sample_fetch(const struct device *dev,
enum sensor_channel chan)
{
const struct spa06_config *cfg = dev->config;
struct spa06_data *data = dev->data;
uint8_t buf[6];
int rc;
if (chan != SENSOR_CHAN_ALL && chan != SENSOR_CHAN_PRESS &&
chan != SENSOR_CHAN_AMBIENT_TEMP) {
return -ENOTSUP;
}
rc = i2c_burst_read_dt(&cfg->bus, SPA06_REG_PSR_B2, buf, sizeof(buf));
if (rc < 0) {
return rc;
}
data->raw_press = spa06_raw24(&buf[0]);
data->raw_temp = spa06_raw24(&buf[3]);
return 0;
}
static int spa06_channel_get(const struct device *dev,
enum sensor_channel chan,
struct sensor_value *val)
{
struct spa06_data *data = dev->data;
float t_sc = (float)data->raw_temp / SPA06_SCALE_FACTOR;
float p_sc, p2, p3, p4, pa;
switch (chan) {
case SENSOR_CHAN_AMBIENT_TEMP:
return sensor_value_from_float(val,
(float)data->c0 * 0.5f +
(float)data->c1 * t_sc);
case SENSOR_CHAN_PRESS:
p_sc = (float)data->raw_press / SPA06_SCALE_FACTOR;
p2 = p_sc * p_sc;
p3 = p2 * p_sc;
p4 = p3 * p_sc;
pa = (float)data->c00 + (float)data->c10 * p_sc +
(float)data->c20 * p2 + (float)data->c30 * p3 +
(float)data->c40 * p4 +
t_sc * ((float)data->c01 + (float)data->c11 * p_sc +
(float)data->c21 * p2 + (float)data->c31 * p3);
/* Sensor API expects kPa */
return sensor_value_from_float(val, pa / 1000.0f);
default:
return -ENOTSUP;
}
}
static DEVICE_API(sensor, spa06_api) = {
.sample_fetch = spa06_sample_fetch,
.channel_get = spa06_channel_get,
};
static int spa06_init(const struct device *dev)
{
const struct spa06_config *cfg = dev->config;
uint8_t id, meas;
int rc;
if (!i2c_is_ready_dt(&cfg->bus)) {
return -ENODEV;
}
rc = i2c_reg_read_byte_dt(&cfg->bus, SPA06_REG_ID, &id);
if (rc < 0) {
return -ENODEV;
}
if (id != SPA06_CHIP_ID) {
LOG_DBG("no SPA06 at 0x%02x (id 0x%02x)", cfg->bus.addr, id);
return -ENODEV;
}
rc = i2c_reg_write_byte_dt(&cfg->bus, SPA06_REG_RESET, SPA06_SOFT_RESET);
if (rc < 0) {
return rc;
}
k_msleep(15);
/* Calibration data is only valid once the part reports both ready */
for (int i = 0; i < 20; i++) {
rc = i2c_reg_read_byte_dt(&cfg->bus, SPA06_REG_MEAS_CFG, &meas);
if (rc == 0 && (meas & SPA06_MEAS_COEF_RDY) &&
(meas & SPA06_MEAS_SENSOR_RDY)) {
goto ready;
}
k_msleep(10);
}
LOG_ERR("SPA06 not ready after reset");
return -EIO;
ready:
rc = spa06_read_coefficients(dev);
if (rc < 0) {
return rc;
}
rc = i2c_reg_write_byte_dt(&cfg->bus, SPA06_REG_PRS_CFG,
(SPA06_RATE_1HZ << 4) | SPA06_OVERSAMPLE_8);
if (rc == 0) {
rc = i2c_reg_write_byte_dt(&cfg->bus, SPA06_REG_TMP_CFG,
(SPA06_RATE_1HZ << 4) | SPA06_OVERSAMPLE_8);
}
if (rc == 0) {
/* No result shift: only needed above 8x oversampling */
rc = i2c_reg_write_byte_dt(&cfg->bus, SPA06_REG_CFG_REG, 0x00);
}
if (rc == 0) {
rc = i2c_reg_write_byte_dt(&cfg->bus, SPA06_REG_MEAS_CFG,
SPA06_MEAS_CONT_BOTH);
}
if (rc < 0) {
return rc;
}
LOG_INF("SPA06 at 0x%02x ready", cfg->bus.addr);
return 0;
}
#define SPA06_DEFINE(inst) \
static struct spa06_data spa06_data_##inst; \
static const struct spa06_config spa06_config_##inst = { \
.bus = I2C_DT_SPEC_INST_GET(inst), \
}; \
SENSOR_DEVICE_DT_INST_DEFINE(inst, spa06_init, NULL, \
&spa06_data_##inst, &spa06_config_##inst,\
POST_KERNEL, CONFIG_SENSOR_INIT_PRIORITY,\
&spa06_api);
DT_INST_FOREACH_STATUS_OKAY(SPA06_DEFINE)
#endif /* DT_HAS_COMPAT_STATUS_OKAY */
+12 -4
View File
@@ -187,7 +187,6 @@ CompanionMesh::CompanionMesh(mesh::Radio &radio, mesh::MillisecondClock &ms, mes
_offline_queue_count = 0;
_sync_pending = false;
memset(_ack_table, 0, sizeof(_ack_table));
_ack_next_overwrite = 0;
memset(_advert_paths, 0, sizeof(_advert_paths));
_next_advert_path_idx = 0;
_sign_data = nullptr;
@@ -652,13 +651,22 @@ void CompanionMesh::addPendingAck(uint32_t expected, int contact_idx)
return;
}
}
// Table full — circular overwrite
int idx = _ack_next_overwrite;
// Table full — evict the entry that has been waiting longest. A rotating
// write cursor is not equivalent: it advances independently of when each
// slot was filled, so under a burst it can discard an ACK registered
// moments ago while a much older one survives. Whichever entry we drop
// can never be confirmed, so drop the one least likely to still be
// answered.
int idx = 0;
for (int i = 1; i < ACK_TABLE_SIZE; i++) {
if ((int32_t)(_ack_table[i].sent_time - _ack_table[idx].sent_time) < 0) {
idx = i;
}
}
_ack_table[idx].expected_ack = expected;
_ack_table[idx].contact_idx = contact_idx;
_ack_table[idx].sent_time = now_ms;
_ack_table[idx].active = true;
_ack_next_overwrite = (idx + 1) % ACK_TABLE_SIZE;
}
int CompanionMesh::findAndRemoveAck(uint32_t ack, uint32_t *out_sent_time)
+1 -2
View File
@@ -57,7 +57,7 @@
#ifdef CONFIG_ZEPHCORE_ACK_TABLE_SIZE
#define ACK_TABLE_SIZE CONFIG_ZEPHCORE_ACK_TABLE_SIZE
#else
#define ACK_TABLE_SIZE 8
#define ACK_TABLE_SIZE 16
#endif
/* Recently-heard advert path slots */
@@ -387,7 +387,6 @@ private:
bool active;
};
AckEntry _ack_table[ACK_TABLE_SIZE];
int _ack_next_overwrite;
/* Advert path table for tracking recently heard nodes */
AdvertPath _advert_paths[ADVERT_PATH_TABLE_SIZE];
+26 -4
View File
@@ -192,12 +192,34 @@ uint8_t RepeaterMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t
LOG_INF("Login success");
client->last_timestamp = sender_timestamp;
client->last_activity = getRTCClock()->getCurrentTime();
client->permissions &= ~0x03;
client->permissions |= perms;
/* Role assignment only ever escalates. putClient() hands back the
* existing entry for a known pubkey, so writing the role bits
* unconditionally lets a later guest login on a key that already holds
* admin silently strip those rights the operator is then locked out
* of their own repeater with no way back in over the air. Revoking
* admin is a deliberate act and belongs to the CLI. */
if (!client->isAdmin()) {
client->permissions &= ~0x03;
client->permissions |= perms;
}
memcpy(client->shared_secret, secret, PUB_KEY_SIZE);
if (perms != PERM_ACL_GUEST) {
if (!dirty_contacts_expiry) dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
if (client->isAdmin()) {
/* Flush admin sessions now instead of leaving them on the lazy
* timer. The entry carries the shared secret this session's
* traffic is encrypted with; if power is lost before the timer
* fires, the client keeps a secret the repeater no longer knows
* and every later request decrypts to nothing, which presents as
* a wrong password or an unreachable node. A repeater is the
* device most likely to lose power unattended, so the few
* milliseconds are worth it. save() writes the whole ACL, so any
* other pending changes go out with it. */
if (_store) {
acl.save(_store->getAclPath());
dirty_contacts_expiry = 0;
} else if (!dirty_contacts_expiry) {
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
}
}
}
+1
View File
@@ -16,6 +16,7 @@ Supported Boards
| XIAO nRF52840 | `west build -b xiao_nrf52840 zephcore` | UF2 drag-drop or `west flash` |
| ProMicro SX1262 | `west build -b promicro_sx1262 zephcore` | UF2 drag-drop or `west flash` |
| T1000-E | `west build -b t1000_e zephcore` | UF2 drag-drop or `west flash` |
| SenseCAP MeshTracker X1 | `west build -b meshtracker_x1 zephcore` | UF2 drag-drop or `west flash` |
| ThinkNode M1 | `west build -b thinknode_m1 zephcore` | UF2 drag-drop or `west flash` |
| ThinkNode M3 | `west build -b thinknode_m3 zephcore` | UF2 drag-drop or `west flash` |
| ThinkNode M6 | `west build -b thinknode_m6 zephcore` | UF2 drag-drop or `west flash` |
@@ -0,0 +1,6 @@
# Copyright (c) 2026 ZephCore
#
# SPDX-License-Identifier: MIT
config BOARD_MESHTRACKER_X1
select SOC_NRF52840_QIAA
@@ -0,0 +1,26 @@
/*
* SPDX-License-Identifier: MIT
*
* MeshTracker X1 high-voltage LiPo OCV curve 21 points at 5% steps.
* 11-point base linearly interpolated to 5% resolution.
* Index 0 = 100% (4290 mV), index 20 = 0% (3100 mV).
*
* 4.37 V cut-off cell, so the top of the curve sits ~100 mV above a
* conventional 4.2 V pack.
*/
#include "battery_curve.h"
static const uint16_t ocv_meshtracker_x1[21] = {
4290, 4204, 4118, 4060, 4002, /* 100 .. 80% */
3946, 3889, 3844, 3798, 3763, /* 75 .. 55% */
3728, 3703, 3677, 3660, 3643, /* 50 .. 30% */
3619, 3594, 3547, 3500, 3300, /* 25 .. 5% */
3100, /* 0% */
};
const battery_curve_t battery_curve_default = {
.ocv_mv = ocv_meshtracker_x1,
.num_points = 21,
.num_cells = 1,
};
@@ -0,0 +1,8 @@
# SPDX-License-Identifier: MIT
board_runner_args(jlink "--device=nRF52840_xxAA" "--speed=4000")
board_runner_args(pyocd "--target=nrf52840" "--frequency=4000000")
include(${ZEPHYR_BASE}/boards/common/uf2.board.cmake)
include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake)
include(${ZEPHYR_BASE}/boards/common/pyocd.board.cmake)
@@ -0,0 +1,23 @@
# SenseCAP MeshTracker X1 (nRF52840 + LR2021)
#
# Hardware:
# - LR2021 LoRa on SPI2 (P0.12 CS, P1.10 RST, P0.07 BUSY, P1.01 DIO8/IRQ), TCXO 1.6V
# - Airoha AG3335M dual-band L1+L5 GNSS on UART0 (P0.13 TX, P0.14 RX) @ 115200
# - SPA06-003 barometer (0x77) + DRV2605L haptic (0x5A) on I2C0 (P1.15/P1.14)
# - YSN8900 RTC (0x32) on I2C1 (P0.26 SDA, P0.27 SCL), rail enable P0.09
# - GD25Q64E 8MB QSPI flash -> /ext, rail enable P0.15
# - RGB LED (P0.03 R, P0.24 G, P0.28 B), buzzer (P0.25), button (P0.06)
# - Battery ADC on AIN0 (P0.02), 2.0x divider, enable P1.06, BQ24045 charger
CONFIG_ZEPHCORE_BOARD_NAME="MeshTracker X1"
CONFIG_BT_DIS_MODEL_NUMBER_STR="Seeed MeshTracker X1"
# SoftDevice firmware ID (s140 v7.3.0 — Seeed bootloader)
CONFIG_ZEPHCORE_SD_FWID=0x0123
CONFIG_ZEPHCORE_RADIO_LR2021=y
# DRV2605L vibration motor
CONFIG_HAPTICS=y
CONFIG_HAPTICS_DRV2605=y
@@ -0,0 +1,10 @@
# Copyright (c) 2026 ZephCore
#
# SPDX-License-Identifier: MIT
board:
name: meshtracker_x1
full_name: Seeed SenseCAP MeshTracker X1
vendor: seeed
socs:
- name: nrf52840
@@ -0,0 +1,118 @@
/*
* Seeed SenseCAP MeshTracker X1 - Pin Control
* Copyright (c) 2026 ZephCore
* SPDX-License-Identifier: MIT
*/
&pinctrl {
/* GNSS UART - Airoha dual-band L1+L5 at 115200 baud */
uart0_default: uart0_default {
group1 {
psels = <NRF_PSEL(UART_TX, 0, 13)>;
};
group2 {
psels = <NRF_PSEL(UART_RX, 0, 14)>;
bias-pull-up;
};
};
uart0_sleep: uart0_sleep {
group1 {
psels = <NRF_PSEL(UART_TX, 0, 13)>,
<NRF_PSEL(UART_RX, 0, 14)>;
low-power-enable;
};
};
/* I2C0 - SPA06 barometer + DRV2605L haptic driver */
i2c0_default: i2c0_default {
group1 {
psels = <NRF_PSEL(TWIM_SDA, 1, 15)>,
<NRF_PSEL(TWIM_SCL, 1, 14)>;
};
};
i2c0_sleep: i2c0_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SDA, 1, 15)>,
<NRF_PSEL(TWIM_SCL, 1, 14)>;
low-power-enable;
};
};
/* I2C1 - YSN8900 RTC on its own bus */
i2c1_default: i2c1_default {
group1 {
psels = <NRF_PSEL(TWIM_SDA, 0, 26)>,
<NRF_PSEL(TWIM_SCL, 0, 27)>;
};
};
i2c1_sleep: i2c1_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SDA, 0, 26)>,
<NRF_PSEL(TWIM_SCL, 0, 27)>;
low-power-enable;
};
};
/* QSPI - GD25Q64E 8MB flash */
qspi_default: qspi_default {
group1 {
psels = <NRF_PSEL(QSPI_SCK, 0, 19)>,
<NRF_PSEL(QSPI_IO0, 0, 21)>,
<NRF_PSEL(QSPI_IO1, 0, 22)>,
<NRF_PSEL(QSPI_IO2, 0, 23)>,
<NRF_PSEL(QSPI_IO3, 1, 0)>,
<NRF_PSEL(QSPI_CSN, 0, 20)>;
};
};
qspi_sleep: qspi_sleep {
group1 {
psels = <NRF_PSEL(QSPI_SCK, 0, 19)>,
<NRF_PSEL(QSPI_IO0, 0, 21)>,
<NRF_PSEL(QSPI_IO1, 0, 22)>,
<NRF_PSEL(QSPI_IO2, 0, 23)>,
<NRF_PSEL(QSPI_IO3, 1, 0)>;
low-power-enable;
};
group2 {
psels = <NRF_PSEL(QSPI_CSN, 0, 20)>;
low-power-enable;
bias-pull-up;
};
};
/* PWM0 for buzzer on P0.25 */
pwm0_default: pwm0_default {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 25)>;
};
};
pwm0_sleep: pwm0_sleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 25)>;
low-power-enable;
};
};
/* SPI2 for LR2021 (SPI1 shares its peripheral slot with I2C1) */
spi2_default: spi2_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 11)>,
<NRF_PSEL(SPIM_MOSI, 1, 9)>,
<NRF_PSEL(SPIM_MISO, 1, 8)>;
};
};
spi2_sleep: spi2_sleep {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 11)>,
<NRF_PSEL(SPIM_MOSI, 1, 9)>,
<NRF_PSEL(SPIM_MISO, 1, 8)>;
low-power-enable;
};
};
};
@@ -0,0 +1,380 @@
/*
* Seeed SenseCAP MeshTracker X1 - nRF52840 + LR2021
* Copyright (c) 2026 ZephCore
*
* SPDX-License-Identifier: MIT
*
* Hardware:
* - nRF52840 SoC with BLE
* - LR2021 LoRa radio (TCXO 1.6V on DIO3, no external RF switch)
* - Airoha dual-band L1+L5 GNSS on UART0
* - SPA06 barometer + DRV2605L haptic driver (I2C0)
* - RGB LED, buzzer, single user button
* - 1100 mAh high-voltage LiPo, ADC on AIN0
*/
/dts-v1/;
#include <nordic/nrf52840_qiaa.dtsi>
#include <nordic/nrf52840_partition.dtsi>
#include <zephyr/dt-bindings/adc/nrf-saadc.h>
#include <zephyr/dt-bindings/input/input-event-codes.h>
#include <zephyr/dt-bindings/pwm/pwm.h>
#include "meshtracker_x1_nrf52840-pinctrl.dtsi"
/* Delete default partitions from nrf52840_partition.dtsi */
/delete-node/ &boot_partition;
/delete-node/ &slot0_partition;
/delete-node/ &slot1_partition;
/delete-node/ &storage_partition;
/ {
model = "Seeed SenseCAP MeshTracker X1";
compatible = "seeed,meshtracker-x1";
chosen {
zephyr,code-partition = &code_partition;
zephyr,console = &cdc_acm_uart;
zephyr,shell-uart = &cdc_acm_uart;
};
leds {
compatible = "gpio-leds";
led_green: led_0 {
gpios = <&gpio0 24 GPIO_ACTIVE_HIGH>;
label = "Green LED";
};
led_red: led_1 {
gpios = <&gpio0 3 GPIO_ACTIVE_HIGH>;
label = "Red LED";
};
led_blue: led_2 {
gpios = <&gpio0 28 GPIO_ACTIVE_HIGH>;
label = "Blue LED";
};
};
buttons: buttons {
compatible = "gpio-keys";
user_button: button_0 {
/* Active HIGH (pressed = VCC), external pull-down on board */
gpios = <&gpio0 6 GPIO_ACTIVE_HIGH>;
zephyr,code = <INPUT_KEY_0>;
label = "User Button";
};
};
/* Short-press → KEY_A, long-press → KEY_F */
user_btn_longpress {
compatible = "zephyr,input-longpress";
input = <&buttons>;
input-codes = <INPUT_KEY_0>;
short-codes = <INPUT_KEY_A>;
long-codes = <INPUT_KEY_F>;
long-delay-ms = <1000>;
};
/* 1 tap page next, 2 LED heartbeat, 3 buzzer mute,
* 4 GPS on/off, 5 flood advert */
user_btn_multitap {
compatible = "zephcore,input-multi-tap";
input-codes = <INPUT_KEY_A>;
tap-codes = <INPUT_KEY_1 INPUT_KEY_B INPUT_KEY_D INPUT_KEY_C INPUT_KEY_E>;
tap-delay-ms = <400>;
};
/* P1.07 — 3V3 rail feeding the I2C sensors (barometer, haptic driver) */
sensor_power: sensor-power {
compatible = "regulator-fixed";
regulator-name = "sensor-power";
enable-gpios = <&gpio1 7 GPIO_ACTIVE_HIGH>;
regulator-boot-on;
startup-delay-us = <5000>;
};
/* P1.06 — gates the battery ADC divider, enabled on demand */
vbat_enable: vbat-enable {
compatible = "regulator-fixed";
regulator-name = "vbat-enable";
enable-gpios = <&gpio1 6 GPIO_ACTIVE_HIGH>;
};
/* Battery ADC channel reference for ZephyrBoard */
zephyr,user {
io-channels = <&adc 0>;
vbat-mv-multiplier = <7236>; /* 2:1 divider, 3.6V ref; +0.5% for nRF SAADC gain error */
};
/* Buzzer on PWM0 channel 0 (P0.25) */
pwmbuzzer {
compatible = "pwm-leds";
buzzer: buzzer {
pwms = <&pwm0 0 PWM_MSEC(20) PWM_POLARITY_NORMAL>;
};
};
/* GNSS control pins */
gps_en: gps-enable {
compatible = "gpio-leds";
gps_enable_pin: gps_enable {
gpios = <&gpio1 11 GPIO_ACTIVE_HIGH>;
label = "GPS Enable";
};
};
gps_vrtc: gps-vrtc-enable {
compatible = "gpio-leds";
gps_vrtc_pin: gps_vrtc {
gpios = <&gpio1 13 GPIO_ACTIVE_HIGH>;
label = "GPS VRTC Enable";
};
};
gps_rst: gps-reset {
compatible = "gpio-leds";
gps_reset_pin: gps_reset {
gpios = <&gpio0 8 GPIO_ACTIVE_HIGH>;
label = "GPS Reset";
};
};
gps_sleep: gps-sleep-int {
compatible = "gpio-leds";
gps_sleep_pin: gps_sleep {
gpios = <&gpio0 30 GPIO_ACTIVE_HIGH>;
label = "GPS Sleep Int";
};
};
/* Held LOW during normal operation, HIGH wakes the GNSS RTC domain */
gps_rtcint: gps-rtc-int {
compatible = "gpio-leds";
gps_rtcint_pin: gps_rtc_int {
gpios = <&gpio0 29 GPIO_ACTIVE_HIGH>;
label = "GPS RTC Int";
};
};
aliases {
led0 = &led_green;
led1 = &led_red;
sw0 = &user_button;
watchdog0 = &wdt0;
buzzer = &buzzer;
lora0 = &lora;
gps-enable = &gps_enable_pin;
gps-vrtc-enable = &gps_vrtc_pin;
gps-reset = &gps_reset_pin;
gps-sleep-int = &gps_sleep_pin;
gps-rtc-int = &gps_rtcint_pin;
};
};
&reg0 {
status = "okay";
};
&reg1 {
regulator-initial-mode = <NRF5X_REG_MODE_DCDC>;
};
&adc {
status = "okay";
#address-cells = <1>;
#size-cells = <0>;
/* Battery voltage on P0.02 (AIN0) - 2x voltage divider */
channel@0 {
reg = <0>;
zephyr,gain = "ADC_GAIN_1_6";
zephyr,reference = "ADC_REF_INTERNAL";
zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 10)>;
zephyr,input-positive = <NRF_SAADC_AIN0>;
zephyr,resolution = <12>;
};
};
/* P0.09/P0.10 are the NFC antenna pins the board wires P0.09 to the RTC
* rail's power switch */
&uicr {
nfct-pins-as-gpios;
};
&gpiote {
status = "okay";
};
/* Both rails are hogged rather than modelled as regulators: a hog is applied
* at PRE_KERNEL_1, ahead of the QSPI NOR driver and the RTC probe, and neither
* rail is ever switched off cutting the RTC would lose wall-clock time. */
&gpio0 {
status = "okay";
/* RTC power switch (P0.09) */
rtc-power-on {
gpio-hog;
gpios = <9 GPIO_ACTIVE_HIGH>;
output-high;
};
/* QSPI flash power switch (P0.15) */
flash-power-on {
gpio-hog;
gpios = <15 GPIO_ACTIVE_HIGH>;
output-high;
};
};
&gpio1 {
status = "okay";
};
&pwm0 {
status = "okay";
pinctrl-0 = <&pwm0_default>;
pinctrl-1 = <&pwm0_sleep>;
pinctrl-names = "default", "sleep";
};
/* Airoha dual-band GNSS on UART0 - 115200 baud.
* Passive NMEA listener: the module streams NMEA at factory defaults, so no
* PAIR command exchange is needed and driver init cannot block on a timeout. */
&uart0 {
compatible = "nordic,nrf-uarte";
status = "okay";
current-speed = <115200>;
pinctrl-0 = <&uart0_default>;
pinctrl-1 = <&uart0_sleep>;
pinctrl-names = "default", "sleep";
gnss: gnss {
compatible = "gnss-nmea-generic";
/* ZephyrGPSManager powers the module up, then calls device_init() */
zephyr,deferred-init;
};
};
&i2c0 {
compatible = "nordic,nrf-twim";
status = "okay";
clock-frequency = <I2C_BITRATE_FAST>;
pinctrl-0 = <&i2c0_default>;
pinctrl-1 = <&i2c0_sleep>;
pinctrl-names = "default", "sleep";
/* Sealed enclosure — only these two parts are ever on this bus */
spa06: spa06@77 {
compatible = "goertek,spa06";
reg = <0x77>;
};
haptic: drv2605@5a {
compatible = "ti,drv2605";
reg = <0x5a>;
actuator-mode = "LRA";
en-gpios = <&gpio1 5 GPIO_ACTIVE_HIGH>;
};
};
/* I2C1 YSN8900 RTC, on its own bus and its own switched rail.
*
* The part is an RX8900 second-source (same 3.2x2.5x1.0 package, same
* 32.768 kHz TCXO grades), so it uses the RX8900 register layout rather than
* the RX8130CE one that rtc-i2c.dtsi assumes for address 0x32: time at 0x00,
* FLAG at 0x0E, VLF in bit 1. Boot-time discovery BCD-validates what it reads,
* so a wrong guess here is detected and skipped, not silently trusted. */
&i2c1 {
compatible = "nordic,nrf-twim";
status = "okay";
clock-frequency = <I2C_BITRATE_FAST>;
pinctrl-0 = <&i2c1_default>;
pinctrl-1 = <&i2c1_sleep>;
pinctrl-names = "default", "sleep";
rtc_ysn8900: rtc-ysn8900@32 {
compatible = "zephcore,rtc-i2c";
reg = <0x32>;
time-reg = <0x00>;
date-index = <4>;
status-reg = <0x0e>
status-mask = <0x02>
};
};
/* GD25Q64E — 8MB, all of it LittleFS for contacts, channels and blobs */
&qspi {
status = "okay";
pinctrl-0 = <&qspi_default>;
pinctrl-1 = <&qspi_sleep>;
pinctrl-names = "default", "sleep";
qspi_flash: qspi_flash@0 {
compatible = "nordic,qspi-nor";
reg = <0>;
writeoc = "pp4io";
readoc = "read4io";
sck-frequency = <16000000>;
jedec-id = [c8 40 17];
size = <67108864>; /* 64Mbit = 8MB, in bits */
has-dpd;
t-enter-dpd = <3000>;
t-exit-dpd = <30000>;
};
};
/* SPI2 for LR2021 (SPI1 shares its peripheral slot with I2C1) */
&spi2 {
compatible = "nordic,nrf-spim";
status = "okay";
cs-gpios = <&gpio0 12 GPIO_ACTIVE_LOW>;
pinctrl-0 = <&spi2_default>;
pinctrl-1 = <&spi2_sleep>;
pinctrl-names = "default", "sleep";
lora: lora@0 {
compatible = "semtech,lr2021";
reg = <0>;
spi-max-frequency = <8000000>;
reset-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>;
busy-gpios = <&gpio0 7 GPIO_ACTIVE_HIGH>;
dio1-gpios = <&gpio1 1 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>;
irq-dio = <8>;
tcxo-voltage-mv = <1600>;
tcxo-startup-delay-ms = <5>;
rx-boosted;
/* Single antenna path, no DIO-driven RF switch on this board */
};
};
zephyr_udc0: &usbd {
compatible = "nordic,nrf-usbd";
status = "okay";
cdc_acm_uart: cdc_acm_uart {
compatible = "zephyr,cdc-acm-uart";
};
};
#include "../../common/nrf52_partitions_sdv7.dtsi"
&qspi_flash {
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
qspi_storage_partition: partition@0 {
label = "qspi_storage";
reg = <0x00000000 0x00800000>;
};
};
};
#include "../../common/qspi-ext.dtsi"
#include "../../common/nrf52_wakeup.dtsi"
@@ -0,0 +1,21 @@
identifier: meshtracker_x1
name: Seeed SenseCAP MeshTracker X1
type: mcu
arch: arm
ram: 256
flash: 1024
toolchain:
- zephyr
- gnuarmemb
- xtools
supported:
- adc
- ble
- gpio
- i2c
- nvs
- pwm
- spi
- usb_device
- watchdog
vendor: seeed
@@ -0,0 +1,11 @@
# Copyright (c) 2026 ZephCore
# SPDX-License-Identifier: MIT
CONFIG_ARM_MPU=y
CONFIG_HW_STACK_PROTECTION=y
CONFIG_GPIO=y
CONFIG_SERIAL=y
CONFIG_CONSOLE=y
CONFIG_BUILD_OUTPUT_UF2=y
CONFIG_USE_DT_CODE_PARTITION=y
@@ -138,6 +138,7 @@
reset-gpios = <&gpio0 9 GPIO_ACTIVE_LOW>;
busy-gpios = <&gpio0 29 GPIO_ACTIVE_HIGH>;
dio1-gpios = <&gpio0 10 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>;
irq-dio = <9>; /* chip DIO9 → module pin 15 → MCU P0.10 */
/* TCXO: tried 1800mV on DIO3 (per Meshtastic diy_tcxo) it WEDGED
* the chip (HF-XOSC startup hangs, BUSY stuck, BLE never starts),
+3
View File
@@ -9,6 +9,7 @@ rak4631
rak3401_1watt
wio_tracker_l1
t1000_e
meshtracker_x1
thinknode_m1
thinknode_m3
thinknode_m6
@@ -28,6 +29,8 @@ muziworks_r1neo
>
> **Heltec T114** screenless build: append `boards/nrf52840/heltec_t114/no_display.conf` to `EXTRA_CONF_FILE` for units without the TFT module.
>
> **SenseCAP MeshTracker X1** (`meshtracker_x1`): nRF52840 + LR2021, AG3335M dual-band L1+L5 GNSS, SPA06 barometer, DRV2605L vibration, YSN8900 RTC, 8 MB QSPI flash (`/ext`), RGB LED, buzzer. Untested on hardware — first ZephCore board to use a real LR2021. The RTC is treated as an RX8900 second-source; boot-time discovery validates that before trusting it.
>
> **Heltec Mesh Node T096** (`heltec_t096`): nRF52840 with SX1262 + KCT8103L PA/FEM, UC6580 GNSS, and ST7735S 160x80 TFT companion display. The external SPI flash footprint is documented in the board notes but left disabled until the device parameters are confirmed.
## ESP32
@@ -0,0 +1,13 @@
# Copyright (c) 2026 ZephCore
# SPDX-License-Identifier: MIT
description: |
Goertek SPA06-003 barometric pressure and temperature sensor.
The driver runs the part in continuous background mode at 8x oversampling
and 1 Hz for both channels, so sample_fetch never blocks. Channels are
SENSOR_CHAN_PRESS (kPa) and SENSOR_CHAN_AMBIENT_TEMP (degrees C).
compatible: "goertek,spa06"
include: [sensor-device.yaml, i2c-device.yaml]
@@ -1 +1,2 @@
goertek Goertek Inc.
zephcore ZephCore
+39
View File
@@ -6,6 +6,8 @@
#include "CommonCLI.h"
#include "battery_curve.h"
#include "led_gate.h"
#include "buzzer_gate.h"
#include <helpers/ui/ui_task.h>
#include <helpers/MeshTimeSync.h>
#include <helpers/time_sync.h>
#include <adapters/clock/ZephyrRTCDiscover.h>
@@ -471,6 +473,12 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
snprintf(reply, CLI_REPLY_SIZE, "> %u", (uint32_t)_prefs->interference_threshold);
} else if (memcmp(config, "leds", 4) == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %s", _prefs->leds_disabled ? "off" : "on");
#ifndef ZEPHCORE_REPEATER
} else if (memcmp(config, "buzzer", 6) == 0) {
uint8_t mode = zephcore_buzzer_mode_from_prefs(_prefs->buzzer_quiet);
snprintf(reply, CLI_REPLY_SIZE, "> %u (%s)", mode,
zephcore_buzzer_mode_name(mode));
#endif
} else if (memcmp(config, "agc.reset.interval", 18) == 0) {
strcpy(reply, "Removed - use rxduty instead");
} else if (memcmp(config, "multi.acks", 10) == 0) {
@@ -668,6 +676,37 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
savePrefs();
strcpy(reply, "OK");
}
#ifndef ZEPHCORE_REPEATER
} else if (memcmp(config, "buzzer ", 7) == 0) {
/* 0 = silent, 1 = sound + vibration, 2 = vibration only,
* 3 = sound only. Modes 2 and 3 only mean something on a board
* with a vibration motor, which most boards don't have. */
const char* val = &config[7];
int mode;
if (memcmp(val, "vibrate", 7) == 0 || val[0] == '2') {
mode = ZEPHCORE_BUZZER_VIBRATE;
} else if (memcmp(val, "sound", 5) == 0 || val[0] == '3') {
mode = ZEPHCORE_BUZZER_SOUND;
} else if (memcmp(val, "on", 2) == 0 || val[0] == '1') {
mode = ZEPHCORE_BUZZER_ON;
} else if (memcmp(val, "off", 3) == 0 || val[0] == '0') {
mode = ZEPHCORE_BUZZER_OFF;
} else {
mode = -1;
}
if (mode < 0) {
strcpy(reply, "Error: 0 (silent), 1 (sound+vib), 2 (vibrate) or 3 (sound)");
} else if ((mode == ZEPHCORE_BUZZER_VIBRATE || mode == ZEPHCORE_BUZZER_SOUND) &&
!zephcore_buzzer_has_vibrate()) {
strcpy(reply, "Error: no vibration motor on this board - use 0 or 1");
} else {
_prefs->buzzer_quiet = zephcore_buzzer_prefs_from_mode((uint8_t)mode);
zephcore_buzzer_set_mode((uint8_t)mode, false);
ui_set_buzzer_mode((uint8_t)mode);
savePrefs();
strcpy(reply, "OK");
}
#endif
} else if (memcmp(config, "agc.reset.interval ", 19) == 0) {
/* Periodic AGC recalibration was removed: it reset the noise floor
* to its unseeded sentinel on every fire, forcing a fresh seed and
+91
View File
@@ -0,0 +1,91 @@
/*
* ZephCore - Notification mode (buzzer + vibration)
* Copyright (c) 2026 ZephCore
* SPDX-License-Identifier: MIT
*/
#include "buzzer_gate.h"
#include <zephyr/kernel.h>
#include <zephyr/sys/atomic.h>
/* Written from the main thread (boot / CLI) and the UI task, read by the
* render path same split as led_gate. */
static atomic_t s_mode = ATOMIC_INIT(ZEPHCORE_BUZZER_ON);
uint8_t zephcore_buzzer_mode(void)
{
return (uint8_t)atomic_get(&s_mode);
}
void zephcore_buzzer_set_mode(uint8_t mode, bool deferred)
{
if (mode > ZEPHCORE_BUZZER_MODE_MAX) {
mode = ZEPHCORE_BUZZER_ON;
}
atomic_set(&s_mode, (atomic_val_t)mode);
zephcore_buzzer_apply(mode, deferred);
}
uint8_t zephcore_buzzer_mode_from_prefs(uint8_t prefs_byte)
{
switch (prefs_byte) {
case 0: return ZEPHCORE_BUZZER_ON;
case 2: return ZEPHCORE_BUZZER_VIBRATE;
case 3: return ZEPHCORE_BUZZER_SOUND;
default: return ZEPHCORE_BUZZER_OFF;
}
}
uint8_t zephcore_buzzer_prefs_from_mode(uint8_t mode)
{
switch (mode) {
case ZEPHCORE_BUZZER_ON: return 0;
case ZEPHCORE_BUZZER_VIBRATE: return 2;
case ZEPHCORE_BUZZER_SOUND: return 3;
default: return 1;
}
}
uint8_t zephcore_buzzer_next_mode(uint8_t mode)
{
if (!zephcore_buzzer_has_vibrate()) {
/* VIBRATE is dead and SOUND is just ON, so it is a plain toggle */
return (mode == ZEPHCORE_BUZZER_ON) ? ZEPHCORE_BUZZER_OFF
: ZEPHCORE_BUZZER_ON;
}
switch (mode) {
case ZEPHCORE_BUZZER_ON: return ZEPHCORE_BUZZER_VIBRATE;
case ZEPHCORE_BUZZER_VIBRATE: return ZEPHCORE_BUZZER_OFF;
case ZEPHCORE_BUZZER_OFF: return ZEPHCORE_BUZZER_SOUND;
default: return ZEPHCORE_BUZZER_ON;
}
}
bool zephcore_buzzer_mode_audible(uint8_t mode)
{
return mode == ZEPHCORE_BUZZER_ON || mode == ZEPHCORE_BUZZER_SOUND;
}
const char *zephcore_buzzer_mode_name(uint8_t mode)
{
switch (mode) {
case ZEPHCORE_BUZZER_ON: return "sound+vib";
case ZEPHCORE_BUZZER_VIBRATE: return "vibrate";
case ZEPHCORE_BUZZER_SOUND: return "sound";
default: return "silent";
}
}
/* Overridden in helpers/ui/buzzer.c on boards that have a buzzer. */
__weak void zephcore_buzzer_apply(uint8_t mode, bool deferred)
{
ARG_UNUSED(mode);
ARG_UNUSED(deferred);
}
__weak bool zephcore_buzzer_has_vibrate(void)
{
return false;
}
+67
View File
@@ -0,0 +1,67 @@
/*
* ZephCore - Notification mode (buzzer + vibration)
* Copyright (c) 2026 ZephCore
* SPDX-License-Identifier: MIT
*
* Always linked, like led_gate the CLI needs these symbols on boards that
* compile no buzzer at all.
*/
#ifndef ZEPHCORE_BUZZER_GATE_H
#define ZEPHCORE_BUZZER_GATE_H
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* User-facing mode, as accepted by "set buzzer". */
#define ZEPHCORE_BUZZER_OFF 0 /* silent */
#define ZEPHCORE_BUZZER_ON 1 /* sound + vibration */
#define ZEPHCORE_BUZZER_VIBRATE 2 /* vibration only */
#define ZEPHCORE_BUZZER_SOUND 3 /* sound only */
#define ZEPHCORE_BUZZER_MODE_MAX 3
/*
* NodePrefs still stores the original buzzer_quiet byte: 0 = not quiet,
* 1 = quiet. Vibration-only and sound-only are the new values 2 and 3, which
* older firmware reads as "quiet" so a downgrade silences a node left on
* sound-only, which is the safe direction. Conversion lives here so the two
* numberings never leak into the rest of the tree.
*/
uint8_t zephcore_buzzer_mode_from_prefs(uint8_t prefs_byte);
uint8_t zephcore_buzzer_prefs_from_mode(uint8_t mode);
/* ON -> VIBRATE -> OFF -> SOUND -> ON. With no motor fitted only ON and OFF
* are reachable the other two would be indistinguishable from them. */
uint8_t zephcore_buzzer_next_mode(uint8_t mode);
/** @return true if the mode lets the buzzer make noise */
bool zephcore_buzzer_mode_audible(uint8_t mode);
/** @return short label for a mode, for the CLI and the display page */
const char *zephcore_buzzer_mode_name(uint8_t mode);
/** @return the mode currently in effect */
uint8_t zephcore_buzzer_mode(void);
/**
* Record a mode and apply it to the hardware.
* @param deferred when muting, let an in-flight melody finish first
*/
void zephcore_buzzer_set_mode(uint8_t mode, bool deferred);
/* Hardware half of zephcore_buzzer_set_mode(). Weak no-op with no buzzer;
* overridden in helpers/ui/buzzer.c. Call the setter, not this. */
void zephcore_buzzer_apply(uint8_t mode, bool deferred);
/** @return true if the board has a vibration motor. Weak false. */
bool zephcore_buzzer_has_vibrate(void);
#ifdef __cplusplus
}
#endif
#endif /* ZEPHCORE_BUZZER_GATE_H */
+7 -4
View File
@@ -17,6 +17,7 @@
#include "ui_pages.h"
#include "ui_task.h"
#include "display.h"
#include <helpers/buzzer_gate.h>
#include <time_sync.h>
#include <ZephyrSensorManager.h>
@@ -1266,13 +1267,15 @@ static void render_buzzer(void)
char buf[24];
int y = CONTENT_Y;
snprintf(buf, sizeof(buf), "Buzzer: %s",
state.buzzer_quiet ? "off" : "on");
uint8_t next = zephcore_buzzer_next_mode(state.buzzer_mode);
snprintf(buf, sizeof(buf), "Alert: %s",
zephcore_buzzer_mode_name(state.buzzer_mode));
mc_display_text(0, y, buf, false);
y += LINE_H;
draw_centered(y + 8,
state.buzzer_quiet ? "Press to Enable" : "Press to Disable");
snprintf(buf, sizeof(buf), "Press for %s", zephcore_buzzer_mode_name(next));
draw_centered(y + 8, buf);
}
static void render_leds_mono(void)
+1 -1
View File
@@ -91,7 +91,7 @@ struct ui_state {
uint32_t gps_next_search_s; /* seconds until next search (0=now/off) */
/* Buzzer page */
bool buzzer_quiet; /* true = muted */
uint8_t buzzer_mode; /* ZEPHCORE_BUZZER_{OFF,ON,VIBRATE} */
/* LEDs page */
bool leds_disabled; /* true = LEDs off */
+17 -14
View File
@@ -25,6 +25,7 @@
#include "ui_task.h"
#include "ui_pages.h"
#include <helpers/buzzer_gate.h>
#include <time_sync.h>
#ifdef CONFIG_ZEPHCORE_UI_BUZZER
@@ -408,26 +409,28 @@ static void action_flood_advert(void)
schedule_render();
}
/* Cycles sound -> vibrate -> silent -> sound. Boards with no motor skip the
* middle step, so they keep the plain on/off toggle they always had. */
static void action_buzzer_toggle(void)
{
#ifdef CONFIG_ZEPHCORE_UI_BUZZER
bool was_quiet = buzzer_is_quiet();
uint8_t mode = zephcore_buzzer_next_mode(get_state()->buzzer_mode);
if (was_quiet) {
/* Unmuting: enable first, then play ascending confirmation */
buzzer_set_quiet(false);
if (zephcore_buzzer_mode_audible(mode)) {
/* Enable first, then play the ascending confirmation */
zephcore_buzzer_set_mode(mode, false);
buzzer_play(MELODY_BUZZER_ON);
} else {
/* Muting: play descending confirmation while still enabled.
* Use deferred mute so the "off" melody plays out fully
* before the quiet flag suppresses future sounds. */
/* Play the descending confirmation while still audible — the
* deferred mute lets it finish before sounds are suppressed. */
buzzer_play(MELODY_BUZZER_OFF);
buzzer_set_quiet_deferred(true);
zephcore_buzzer_set_mode(mode, true);
}
/* Persist mute state across reboots */
mesh_set_buzzer_quiet(!was_quiet);
get_state()->buzzer_quiet = !was_quiet;
LOG_INF("buzzer %s", buzzer_is_quiet() ? "muted" : "unmuted");
/* Persist across reboots */
mesh_set_buzzer_mode(mode);
get_state()->buzzer_mode = mode;
LOG_INF("buzzer mode=%u", mode);
#endif
schedule_render();
}
@@ -1088,11 +1091,11 @@ void ui_set_ble_enabled(bool enabled)
s->ble_enabled = enabled;
}
void ui_set_buzzer_quiet(bool quiet)
void ui_set_buzzer_mode(uint8_t mode)
{
struct ui_state *s = get_state();
s->buzzer_quiet = quiet;
s->buzzer_mode = mode;
}
void ui_set_offgrid_mode(bool enabled)
@@ -275,6 +275,6 @@ extern "C" void ui_set_sensor_data(int16_t, uint32_t, uint16_t, uint16_t) {}
extern "C" void ui_set_gps_available(bool) {}
extern "C" void ui_set_gps_enabled(bool) {}
extern "C" void ui_set_gps_state(uint8_t, uint32_t, uint32_t) {}
extern "C" void ui_set_buzzer_quiet(bool) {}
extern "C" void ui_set_buzzer_mode(uint8_t) {}
extern "C" void ui_set_offgrid_mode(bool) {}
extern "C" void ui_set_msg_count(uint16_t count) {}
@@ -11,6 +11,7 @@
#include "joystick_defs.h"
#include "joystick_ui_hooks.h"
#include <helpers/input/zephcore_input_ascii.h>
#include <helpers/buzzer_gate.h>
#include <helpers/ui/ui_mesh_actions.h>
#include <helpers/ui/ui_task.h>
#include <helpers/AdvertDataHelpers.h>
@@ -958,15 +959,16 @@ bool JoystickUITask::isBuzzerQuiet() const
void JoystickUITask::toggleBuzzer()
{
#ifdef CONFIG_ZEPHCORE_UI_BUZZER
bool was_quiet = buzzer_is_quiet();
if (was_quiet) {
buzzer_set_quiet(false);
uint8_t mode = zephcore_buzzer_next_mode(zephcore_buzzer_mode());
if (zephcore_buzzer_mode_audible(mode)) {
zephcore_buzzer_set_mode(mode, false);
buzzer_play("bon:d=16,o=7,b=200:c,p,c,p,c,p,p,8e");
} else {
buzzer_play("bof:d=16,o=7,b=200:c,p,c,p,c,p,p,8g5");
buzzer_set_quiet_deferred(true);
zephcore_buzzer_set_mode(mode, true);
}
mesh_set_buzzer_quiet(!was_quiet);
mesh_set_buzzer_mode(mode);
#endif
}
+29
View File
@@ -16,6 +16,8 @@
*/
#include "buzzer.h"
#include "haptic.h"
#include "buzzer_gate.h"
#include <zephyr/device.h>
#include <zephyr/drivers/pwm.h>
@@ -364,6 +366,8 @@ static void note_work_handler(struct k_work *work)
int buzzer_init(void)
{
haptic_init();
/* Check for buzzer alias in devicetree */
const struct device *pwm_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(pwm0));
@@ -420,6 +424,10 @@ int buzzer_init(void)
void buzzer_play(const char *rtttl)
{
if (rtttl && *rtttl) {
haptic_pulse();
}
if (!ctx.initialized) {
return;
}
@@ -492,6 +500,27 @@ void buzzer_set_quiet_deferred(bool quiet)
LOG_INF("buzzer %s (deferred)", quiet ? "muted" : "enabled");
}
/* Strong overrides of the weak stubs in helpers/buzzer_gate.c */
void zephcore_buzzer_apply(uint8_t mode, bool deferred)
{
bool quiet = !zephcore_buzzer_mode_audible(mode);
if (quiet && deferred) {
buzzer_set_quiet_deferred(true);
} else {
buzzer_set_quiet(quiet);
}
haptic_set_enabled(mode == ZEPHCORE_BUZZER_ON ||
mode == ZEPHCORE_BUZZER_VIBRATE);
}
bool zephcore_buzzer_has_vibrate(void)
{
return haptic_available();
}
bool buzzer_is_quiet(void)
{
return ctx.quiet;
+86
View File
@@ -0,0 +1,86 @@
/*
* ZephCore - Haptic feedback (DRV2605)
* Copyright (c) 2026 ZephCore
* SPDX-License-Identifier: MIT
*/
#include "haptic.h"
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(zephcore_haptic, CONFIG_ZEPHCORE_SENSORS_LOG_LEVEL);
#if DT_HAS_COMPAT_STATUS_OKAY(ti_drv2605)
#include <zephyr/drivers/haptics.h>
#include <zephyr/drivers/haptics/drv2605.h>
#define HAPTIC_NODE DT_COMPAT_GET_ANY_STATUS_OKAY(ti_drv2605)
static const struct device *haptic_dev;
static bool haptic_enabled = true;
/* Library effect 1 = "Strong Click - 100%", short enough to sit under a
* notification melody without outlasting it. */
static struct drv2605_rom_data rom_data = {
.trigger = DRV2605_MODE_INTERNAL_TRIGGER,
.library = DRV2605_LIBRARY_LRA,
.seq_regs = { 1 },
};
int haptic_init(void)
{
const union drv2605_config_data cfg = { .rom_data = &rom_data };
int rc;
haptic_dev = DEVICE_DT_GET(HAPTIC_NODE);
if (!device_is_ready(haptic_dev)) {
LOG_INF("no haptic driver found");
haptic_dev = NULL;
return -ENODEV;
}
rc = drv2605_haptic_config(haptic_dev, DRV2605_HAPTICS_SOURCE_ROM, &cfg);
if (rc < 0) {
LOG_WRN("haptic config failed: %d", rc);
haptic_dev = NULL;
return rc;
}
LOG_INF("haptic driver ready");
return 0;
}
void haptic_pulse(void)
{
if (!haptic_dev || !haptic_enabled) {
return;
}
(void)haptics_start_output(haptic_dev);
}
void haptic_set_enabled(bool enabled)
{
haptic_enabled = enabled;
if (!enabled && haptic_dev) {
(void)haptics_stop_output(haptic_dev);
}
}
bool haptic_available(void)
{
return haptic_dev != NULL;
}
#else /* no ti,drv2605 node — link-compatible stubs */
int haptic_init(void) { return -ENODEV; }
void haptic_pulse(void) { }
void haptic_set_enabled(bool enabled) { ARG_UNUSED(enabled); }
bool haptic_available(void) { return false; }
#endif
+38
View File
@@ -0,0 +1,38 @@
/*
* ZephCore - Haptic feedback
* Copyright (c) 2026 ZephCore
* SPDX-License-Identifier: MIT
*
* Vibration alerts on boards carrying a DRV2605 haptic driver. Every call
* is a no-op on boards without one.
*/
#ifndef ZEPHCORE_HAPTIC_H
#define ZEPHCORE_HAPTIC_H
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Initialize the haptic driver from devicetree (ti,drv2605).
* @return 0 on success, -ENODEV if the board has no haptic driver
*/
int haptic_init(void);
/** Play a short notification buzz. No-op if disabled or uninitialized. */
void haptic_pulse(void);
/** Enable or disable vibration. Driven by the notification mode. */
void haptic_set_enabled(bool enabled);
/** @return true if the board has a working vibration motor */
bool haptic_available(void);
#ifdef __cplusplus
}
#endif
#endif /* ZEPHCORE_HAPTIC_H */
+1 -1
View File
@@ -115,7 +115,7 @@ WEAK void ui_set_ble_enabled(bool enabled)
ARG_UNUSED(enabled);
}
WEAK void ui_set_buzzer_quiet(bool quiet)
WEAK void ui_set_buzzer_mode(uint8_t mode)
{
ARG_UNUSED(quiet);
}
+8 -6
View File
@@ -11,6 +11,8 @@
*/
#include <zephyr/kernel.h>
#include <helpers/buzzer_gate.h>
#include <zephyr/device.h>
#include <zephyr/devicetree.h>
#include <zephyr/drivers/regulator.h>
@@ -61,7 +63,7 @@ static atomic_t pending_ui_actions;
* Written before atomic_or on pending_ui_actions, read after atomic_clear,
* so the atomic provides ordering. Using atomic_t for portability. */
static atomic_t pending_gps_enabled;
static atomic_t pending_buzzer_quiet;
static atomic_t pending_buzzer_mode;
static atomic_t pending_offgrid_enabled;
static atomic_t pending_leds_disabled;
static atomic_t pending_ble_disabled;
@@ -164,10 +166,10 @@ extern "C" void mesh_save_gps_duty_sec(uint32_t sec)
k_event_post(s_mesh_events, s_mesh_event_ui_action);
}
extern "C" void mesh_set_buzzer_quiet(bool quiet)
extern "C" void mesh_set_buzzer_mode(uint8_t mode)
{
/* Defer the flash write (savePrefs) to mesh thread */
atomic_set(&pending_buzzer_quiet, quiet ? 1 : 0);
atomic_set(&pending_buzzer_mode, (atomic_val_t)mode);
atomic_or(&pending_ui_actions, UI_ACTION_BUZZER_TOGGLE);
k_event_post(s_mesh_events, s_mesh_event_ui_action);
}
@@ -248,9 +250,9 @@ extern "C" void mesh_handle_ui_actions(void)
}
if (actions & UI_ACTION_BUZZER_TOGGLE) {
bool bq = atomic_get(&pending_buzzer_quiet) != 0;
s_mesh->prefs.buzzer_quiet = bq ? 1 : 0;
LOG_INF("buzzer_quiet=%d (button)", bq);
uint8_t mode = (uint8_t)atomic_get(&pending_buzzer_mode);
s_mesh->prefs.buzzer_quiet = zephcore_buzzer_prefs_from_mode(mode);
LOG_INF("buzzer mode=%u (button)", mode);
need_save = true;
}
+1 -1
View File
@@ -55,7 +55,7 @@ void mesh_send_flood_advert(void);
void mesh_send_zerohop_advert(void);
void mesh_gps_set_enabled(bool enable);
void mesh_ble_set_enabled(bool enable);
void mesh_set_buzzer_quiet(bool quiet);
void mesh_set_buzzer_mode(uint8_t mode);
void mesh_set_offgrid_mode(bool enable);
void mesh_set_leds_disabled(bool disabled);
void mesh_disable_power_regulators(void);
+1 -1
View File
@@ -17,7 +17,7 @@ __attribute__((weak)) void mesh_send_flood_advert(void) {}
__attribute__((weak)) void mesh_send_zerohop_advert(void) {}
__attribute__((weak)) void mesh_gps_set_enabled(bool enable) { ARG_UNUSED(enable); }
__attribute__((weak)) void mesh_ble_set_enabled(bool enable) { ARG_UNUSED(enable); }
__attribute__((weak)) void mesh_set_buzzer_quiet(bool quiet) { ARG_UNUSED(quiet); }
__attribute__((weak)) void mesh_set_buzzer_mode(uint8_t mode) { ARG_UNUSED(mode); }
__attribute__((weak)) void mesh_set_offgrid_mode(bool enable) { ARG_UNUSED(enable); }
__attribute__((weak)) void mesh_set_leds_disabled(bool disabled) { ARG_UNUSED(disabled); }
__attribute__((weak)) void mesh_disable_power_regulators(void) {}
+2 -2
View File
@@ -163,9 +163,9 @@ void ui_set_gps_state(uint8_t state, uint32_t last_fix_age_s, uint32_t next_sear
void ui_set_ble_enabled(bool enabled);
/**
* Set buzzer quiet state (for display page).
* Set notification mode (for display page).
*/
void ui_set_buzzer_quiet(bool quiet);
void ui_set_buzzer_mode(uint8_t mode);
/**
* Set LEDs disabled state (for display page).
@@ -46,6 +46,8 @@ struct lr20xx_config {
uint16_t tcxo_voltage_mv;
uint32_t tcxo_startup_delay_ms;
bool rx_boosted;
/* Chip-side DIO wired to the MCU IRQ line (5..11) */
uint8_t irq_dio;
/* RF switch DIO bitmasks (bit 0 = DIO5, bit 1 = DIO6, ...) */
uint8_t rfswitch_enable;
uint8_t rfswitch_standby;
@@ -194,6 +196,22 @@ static float bw_enum_to_khz(enum lora_signal_bandwidth bw)
}
}
/* ── IRQ DIO ────────────────────────────────────────────────────────── */
static inline lr20xx_system_dio_t lr20xx_irq_dio(const struct lr20xx_config *cfg)
{
return (lr20xx_system_dio_t)cfg->irq_dio;
}
/* DIO5 can only be pulled up; every other DIO gets a pull-down. */
static inline lr20xx_system_dio_drive_t
lr20xx_irq_dio_pull(const struct lr20xx_config *cfg)
{
return cfg->irq_dio == LR20XX_SYSTEM_DIO_5
? LR20XX_SYSTEM_DIO_DRIVE_PULL_UP
: LR20XX_SYSTEM_DIO_DRIVE_PULL_DOWN;
}
/* ── Configure RF switch DIOs ───────────────────────────────────────── */
static void lr20xx_configure_rfswitch(void *ctx, const struct lr20xx_config *cfg)
@@ -341,10 +359,9 @@ static void lr20xx_hardware_reset(struct lr20xx_data *data,
lr20xx_configure_rfswitch(ctx, cfg);
/* DIO9 is the physical IRQ line (pin 15 on NiceRF module → MCU P0.10) */
lr20xx_system_set_dio_function(ctx, LR20XX_SYSTEM_DIO_9,
lr20xx_system_set_dio_function(ctx, lr20xx_irq_dio(cfg),
LR20XX_SYSTEM_DIO_FUNC_IRQ,
LR20XX_SYSTEM_DIO_DRIVE_NONE);
lr20xx_irq_dio_pull(cfg));
lr20xx_radio_common_set_rx_tx_fallback_mode(ctx,
LR20XX_RADIO_FALLBACK_STDBY_RC);
@@ -417,16 +434,19 @@ static void lr20xx_apply_modem_config(struct lr20xx_data *data,
: LR20XX_RADIO_COMMON_RX_PATH_BOOST_MODE_NONE);
data->rx_boost_applied = data->rx_boost_enabled;
/* LR20xx uses PPM offset instead of explicit LDRO.
* PPM_1_4 (1 bin every 4) is equivalent to LDRO for high-SF
* wide-time-on-air configurations. Use recommended value. */
/* PPM offset is this chip's name for LDRO and occupies the same wire
* field. The Semtech "recommended offset" helper only ever enables it
* for SF11/SF12, which disagrees with the rest of the mesh below
* 125 kHz every other stack switches on symbol time > 16.38 ms. */
uint32_t bw_hz = (uint32_t)(bw_enum_to_khz(mc->bandwidth) * 1000.0f);
uint32_t symbol_time_us = ((1U << (uint8_t)mc->datarate) * 1000000U) / bw_hz;
lr20xx_radio_lora_mod_params_t mod = {
.sf = (lr20xx_radio_lora_sf_t)mc->datarate,
.bw = bw_enum_to_lr20xx(mc->bandwidth),
.cr = cr_enum_to_lr20xx(mc->coding_rate),
.ppm = lr20xx_radio_lora_get_recommended_ppm_offset(
(lr20xx_radio_lora_sf_t)mc->datarate,
bw_enum_to_lr20xx(mc->bandwidth)),
.ppm = (symbol_time_us > 16380) ? LR20XX_RADIO_LORA_PPM_1_4
: LR20XX_RADIO_LORA_NO_PPM,
};
rc = lr20xx_radio_lora_set_modulation_params(ctx, &mod);
LOG_DBG("modem_cfg: set_mod(SF%d BW%d CR%d PPM%d)=%d",
@@ -459,11 +479,7 @@ static void lr20xx_apply_modem_config(struct lr20xx_data *data,
lr20xx_radio_common_pa_cfg_t pa;
int8_t pa_val;
/* DEBUG/TEST: force MINIMUM power (-9dBm, 1 PA slice) to probe the
* supply-sag hypothesis. If TX keys here (mode=5 + carrier) but
* not at +22dBm (7 slices), the rail can't source PA current
* hardware power limit. REVERT to mc->tx_power after testing. */
lr20xx_get_pa_cfg_for_power(-9, &pa, &pa_val);
lr20xx_get_pa_cfg_for_power(mc->tx_power, &pa, &pa_val);
rc = lr20xx_radio_common_set_pa_cfg(ctx, &pa);
LOG_DBG("modem_cfg: set_pa_cfg(sel=%d mode=%d duty=%d slices=%d hf_duty=%d)=%d",
pa.pa_sel, pa.pa_lf_mode, pa.pa_lf_duty_cycle,
@@ -475,7 +491,7 @@ static void lr20xx_apply_modem_config(struct lr20xx_data *data,
pa_val, rc);
}
rc = lr20xx_system_set_dio_irq_cfg(ctx, LR20XX_SYSTEM_DIO_9,
rc = lr20xx_system_set_dio_irq_cfg(ctx, lr20xx_irq_dio(cfg),
LR20XX_SYSTEM_IRQ_ALL_MASK &
~(LR20XX_SYSTEM_IRQ_FIFO_RX | LR20XX_SYSTEM_IRQ_FIFO_TX));
LOG_DBG("modem_cfg: set_dio_irq=%d", rc);
@@ -952,28 +968,9 @@ static int lr20xx_lora_send_async(const struct device *dev,
lr20xx_radio_common_set_tx(ctx, 5000);
#if IS_ENABLED(CONFIG_LOG)
/* DEBUG: poll chip state right after SET_TX. Non-destructive
* (get_status does NOT clear IRQs). We want to see:
* - cmd= on the FIRST poll == SET_TX's own command status
* (2=OK accepted, 1=PERR rejected, 0=FAIL not executed)
* - mode transitions 1(STBY)->5(TX)->1(fallback) if it really TXes
* - irq gaining TX_DONE (bit19, 0x00080000) at the chip level
* - whether DIO9 physically asserts (the MCU IRQ line)
* Diagnostic only the spi_mutex is held, so the DIO9 work handler
* blocks until we unlock, then processes TX_DONE normally. */
for (int i = 0; i < 20; i++) {
lr20xx_system_stat1_t s1 = {0};
lr20xx_system_stat2_t s2 = {0};
lr20xx_system_irq_mask_t dbgirq = 0;
lr20xx_system_get_status(ctx, &s1, &s2, &dbgirq);
LOG_INF("TXpoll[%2d] cmd=%d mode=%d irq=0x%08x BUSY=%d DIO9=%d",
i, s1.command_status, s2.chip_mode, dbgirq,
gpio_pin_get_dt(&data->hal_ctx.busy),
gpio_pin_get_dt(&data->hal_ctx.dio1));
k_msleep(20);
}
#endif
/* Command status here is SET_TX's own (2=accepted, 1=rejected,
* 0=not executed) and the mode should have left standby. */
DUMP_CHIP_STATE(ctx, &data->hal_ctx, "post-SET_TX");
k_mutex_unlock(&data->spi_mutex);
@@ -1447,26 +1444,12 @@ static int lr20xx_hw_init(struct lr20xx_data *data,
LOG_INF("LR20xx SDK get_version: major=%u minor=%u", ver.major, ver.minor);
/* CRITICAL: Read raw 4 bytes from GET_VERSION (opcode 0x0101) to
* determine if this is LR11x0 or LR20xx silicon.
* LR11x0 returns: [hw_type, device_use, fw_major, fw_minor] (4 bytes)
* LR20xx returns: [major, minor] (2 bytes, extra bytes would be 0x00)
* If byte[0]=0x01/0x02/0x03, it's LR1110/LR1120/LR1121 (LR11x0!) */
{
const uint8_t cmd[2] = { 0x01, 0x01 };
uint8_t raw[4] = { 0 };
lr20xx_hal_read(ctx, cmd, 2, raw, 4);
LOG_DBG("GET_VERSION raw bytes: 0x%02x 0x%02x 0x%02x 0x%02x",
raw[0], raw[1], raw[2], raw[3]);
if (raw[0] == 0x01) {
LOG_WRN("*** CHIP IDENTIFIES AS LR1110 (LR11x0 family!) ***");
} else if (raw[0] == 0x02) {
LOG_WRN("*** CHIP IDENTIFIES AS LR1120 (LR11x0 family!) ***");
} else if (raw[0] == 0x03) {
LOG_WRN("*** CHIP IDENTIFIES AS LR1121 (LR11x0 family!) ***");
} else {
LOG_DBG("Chip type byte=0x%02x (LR20xx if not 0x01-0x03)", raw[0]);
}
/* The only base FW version the datasheet documents is 1.24 (0x01/0x18).
* Anything else still runs the mismatch is worth a line in the log
* when a bring-up goes sideways, not a hard failure. */
if (ver.major != 0x01 || ver.minor != 0x18) {
LOG_WRN("Unexpected LR2021 FW %u.%u (datasheet documents 1.24)",
ver.major, ver.minor);
}
DUMP_CHIP_STATE(ctx, &data->hal_ctx, "post-reset");
@@ -1497,22 +1480,13 @@ static int lr20xx_hw_init(struct lr20xx_data *data,
cfg->rfswitch_enable, cfg->rfswitch_standby, cfg->rfswitch_rx,
cfg->rfswitch_tx, cfg->rfswitch_tx_hp);
st = lr20xx_system_set_dio_function(ctx, LR20XX_SYSTEM_DIO_9,
st = lr20xx_system_set_dio_function(ctx, lr20xx_irq_dio(cfg),
LR20XX_SYSTEM_DIO_FUNC_IRQ,
LR20XX_SYSTEM_DIO_DRIVE_NONE);
lr20xx_irq_dio_pull(cfg));
st = lr20xx_radio_common_set_rx_tx_fallback_mode(ctx,
LR20XX_RADIO_FALLBACK_STDBY_RC);
/* DEBUG/TEST: disable the low-battery / EoL detector. The chip was
* raising LOW_BATTERY (IRQ bit10, 0x400) during FE cal and TX and
* refusing to enter TX. Disabling it disambiguates:
* - TX now keys it was a FALSE VBAT reading (sense pin), done.
* - TX still dead genuine supply brownout under RF (hardware).
* Default trim is 1.88V; we both disable AND set the lowest (1.60V). */
st = lr20xx_system_set_lbd_cfg(ctx, false, LR20XX_SYSTEM_LBD_TRIM_1_60_V);
LOG_DBG("init: disable LBD (low-battery detect)=%d", st);
DUMP_CHIP_STATE(ctx, &data->hal_ctx, "pre-cal");
lr20xx_system_clear_errors(ctx);
@@ -1661,6 +1635,7 @@ static DEVICE_API(lora, lr20xx_lora_api) = {
.tcxo_startup_delay_ms = \
DT_INST_PROP_OR(n, tcxo_startup_delay_ms, 5), \
.rx_boosted = DT_INST_PROP(n, rx_boosted), \
.irq_dio = DT_INST_PROP_OR(n, irq_dio, 9), \
.rfswitch_enable = DT_INST_PROP_OR(n, rfswitch_enable, 0), \
.rfswitch_standby = DT_INST_PROP_OR(n, rfswitch_standby, 0),\
.rfswitch_rx = DT_INST_PROP_OR(n, rfswitch_rx, 0), \
@@ -42,6 +42,13 @@ properties:
type: boolean
description: Enable RX boosted mode for better sensitivity (+~3dB, +~2mA).
irq-dio:
type: int
default: 9
description: |
Chip-side DIO wired to the MCU interrupt line, 5..11. This is the DIO
number on the LR2021 itself, not the SoC pin — the SoC side is dio1-gpios.
rfswitch-enable:
type: int
default: 0
+10 -8
View File
@@ -31,6 +31,7 @@ LOG_MODULE_REGISTER(zephcore_main, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL);
#include "ui_mesh_actions.h"
#include "oled_power.h"
#include "led_gate.h"
#include "buzzer_gate.h"
#if IS_ENABLED(CONFIG_ZEPHCORE_UI_BUZZER)
#include "buzzer.h"
#endif
@@ -1506,15 +1507,16 @@ int main(void)
LOG_INF("offgrid mode: %s (from prefs)",
companion_mesh.prefs.client_repeat ? "on" : "off");
/* Restore buzzer mute state from persisted prefs, then play
* startup chime only if buzzer is not muted.
* buzzer_init() defaults to quiet=true, so we must always
* call buzzer_set_quiet() to apply the saved preference. */
/* Restore the notification mode from persisted prefs, then play the
* startup chime only if sound is on. buzzer_init() defaults to
* quiet=true, so the saved preference must always be applied. */
#if IS_ENABLED(CONFIG_ZEPHCORE_UI_BUZZER)
buzzer_set_quiet(companion_mesh.prefs.buzzer_quiet);
ui_set_buzzer_quiet(companion_mesh.prefs.buzzer_quiet);
LOG_INF("buzzer: quiet=%s (from prefs)",
companion_mesh.prefs.buzzer_quiet ? "true" : "false");
{
uint8_t bmode = zephcore_buzzer_mode_from_prefs(companion_mesh.prefs.buzzer_quiet);
zephcore_buzzer_set_mode(bmode, false);
ui_set_buzzer_mode(bmode);
LOG_INF("buzzer: mode=%u (from prefs)", bmode);
}
ui_play_startup_chime();
#endif
+4
View File
@@ -422,6 +422,10 @@ static uint16_t get_battery_mv(void)
/* LR1110 via Zephyr LoRa driver */
static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0));
static mesh::LR1110Radio lora_radio(lora_dev, zephyr_board);
#elif IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR2021)
/* LR2021 via Zephyr LoRa driver */
static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0));
static mesh::LR2021Radio lora_radio(lora_dev, zephyr_board);
#elif IS_ENABLED(CONFIG_ZEPHCORE_RADIO_SX127X)
/* SX127x via Zephyr loramac-node driver */
static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0));
+4
View File
@@ -356,6 +356,10 @@ static uint16_t get_battery_mv(void)
/* LR1110 via Zephyr LoRa driver */
static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0));
static mesh::LR1110Radio lora_radio(lora_dev, zephyr_board);
#elif IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR2021)
/* LR2021 via Zephyr LoRa driver */
static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0));
static mesh::LR2021Radio lora_radio(lora_dev, zephyr_board);
#elif IS_ENABLED(CONFIG_ZEPHCORE_RADIO_SX127X)
/* SX127x via Zephyr loramac-node driver */
static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0));