Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/led-menu-guide.pdf
Binary file not shown.
84 changes: 68 additions & 16 deletions zephcore/adapters/board/ZephyrBoard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <zephyr/sys/reboot.h>
#include <zephyr/drivers/sensor.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/pwm.h>
#include <stdio.h>
#include <string.h>

Expand Down Expand Up @@ -97,19 +98,34 @@
#endif
#endif

/* LoRa radio activity LED (optional — defined per-board via DT alias). Still
* called tx_led after "set leds.radio" gave it an RX mode, because the DT alias
* it comes from is named lora-tx-led on every board that has one. */
#if DT_NODE_EXISTS(DT_ALIAS(lora_tx_led))
/* LoRa radio activity LED (optional — defined per-board via DT alias). Still
* called tx_led after "set leds.radio" gave it an RX mode, because the DT
* alias it comes from is named lora-tx-led on every board that has one.
*
* PWM path (DT_ALIAS(lora_tx_pwm_led)) takes priority when it exists — used
* on boards where the TX LED is the same physical LED as the heartbeat
* (e.g. T096, see helpers/ui/ui_common.c), so both share the one brightness
* in helpers/led_gate.h instead of the plain GPIO path fighting over the
* pin. */
#if DT_NODE_EXISTS(DT_ALIAS(lora_tx_pwm_led))
static const struct pwm_dt_spec tx_led_pwm = PWM_DT_SPEC_GET(DT_ALIAS(lora_tx_pwm_led));
#define HAS_TX_LED_PWM 1
#define HAS_TX_LED 0
/* Init deferred to the combined tx_led_init() below (with the GPIO path),
* since both now also need to arm s_rx_pulse_off -- see that block. */
#elif DT_NODE_EXISTS(DT_ALIAS(lora_tx_led))
static const struct gpio_dt_spec tx_led =
GPIO_DT_SPEC_GET(DT_ALIAS(lora_tx_led), gpios);
#define HAS_TX_LED_PWM 0
#define HAS_TX_LED 1
#else
#define HAS_TX_LED_PWM 0
#define HAS_TX_LED 0
#endif

/* True when this board wires the activity LED to the same pin as the heartbeat
* (8 of the supported boards do). Only those pay for the arbitration hold in
* (8 of the supported boards do, counting only the plain-GPIO ones -- see the
* PWM branch below for the rest). Only those pay for the arbitration hold in
* led_gate.c — everywhere else the two LEDs are independent and the calls
* compile out. led1 is checked as well because ui_common.c falls back to it
* when a board has no led0. */
Expand All @@ -120,10 +136,36 @@ static const struct gpio_dt_spec tx_led =
DT_NODE_EXISTS(DT_ALIAS(led1)) && \
DT_SAME_NODE(DT_ALIAS(led1), DT_ALIAS(lora_tx_led))
#define ZEPHCORE_LED_PIN_SHARED 1
/* PWM equivalent: true when heartbeat and TX activity are aliased to the same
* PWM LED node (T096, Wireless Tracker V2). Not the case on every PWM board --
* RAK3401 has two independent PWM LEDs (green/blue), so this stays 0 there and
* the hold below is correctly never taken. */
#elif HAS_TX_LED_PWM && DT_NODE_EXISTS(DT_ALIAS(heartbeat_pwm_led)) && \
DT_SAME_NODE(DT_ALIAS(heartbeat_pwm_led), DT_ALIAS(lora_tx_pwm_led))
#define ZEPHCORE_LED_PIN_SHARED 1
#else
#define ZEPHCORE_LED_PIN_SHARED 0
#endif

/* PWM-or-GPIO write for the activity LED, mirroring heartbeat_led_write() in
* helpers/ui/ui_common.c -- same reasoning: the write itself must not care
* which path is compiled in, only the callers (onBeforeTransmit/
* onAfterTransmit/onPacketReceived/rx_pulse_off_handler below) do. */
#if HAS_TX_LED_PWM
static inline void tx_led_write(bool on)
{
uint32_t pulse = on ? (uint32_t)((uint64_t)tx_led_pwm.period *
zephcore_led_brightness_pct() / 100)
: 0;
pwm_set_pulse_dt(&tx_led_pwm, pulse);
}
#elif HAS_TX_LED
static inline void tx_led_write(bool on)
{
gpio_pin_set_dt(&tx_led, on ? 1 : 0);
}
#endif

#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(zephcore_board, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL);

Expand Down Expand Up @@ -173,8 +215,8 @@ static const struct device *const fuel_gauge_dev =
#define HAS_FUEL_GAUGE 0
#endif

/* Initialize activity LED GPIO at boot */
#if HAS_TX_LED
/* Initialize activity LED (PWM or GPIO) at boot */
#if HAS_TX_LED_PWM || HAS_TX_LED
/* Width of the receive blink. A transmit holds the LED for its whole airtime,
* but a receive is a single edge — the packet is over by the time the driver
* hands it up — so RX has to be a fixed one-shot. 30 ms is deliberately longer
Expand All @@ -187,7 +229,9 @@ static const struct device *const fuel_gauge_dev =
* interlock that keeps an RX one-shot from clearing a pin that TX still owns:
* the two are driven from different threads (radio TX path vs system work
* queue), so without it a pulse landing mid-transmit would blank the LED for
* the rest of the packet. */
* the rest of the packet. Applies equally to the PWM-shared boards (T096,
* Wireless Tracker V2): same race, same fix, just written through
* tx_led_write() instead of a bare gpio_pin_set_dt(). */
static atomic_t s_tx_lit;
static struct k_work_delayable s_rx_pulse_off;

Expand All @@ -199,17 +243,23 @@ static void rx_pulse_off_handler(struct k_work *work)
if (atomic_get(&s_tx_lit)) {
return;
}
gpio_pin_set_dt(&tx_led, 0);
tx_led_write(false);
#if ZEPHCORE_LED_PIN_SHARED
zephcore_led_radio_hold_pin(false);
#endif
}

static int tx_led_init(void)
{
#if HAS_TX_LED_PWM
if (device_is_ready(tx_led_pwm.dev)) {
pwm_set_pulse_dt(&tx_led_pwm, 0); /* start dark, same as GPIO_OUTPUT_INACTIVE */
}
#else
if (gpio_is_ready_dt(&tx_led)) {
gpio_pin_configure_dt(&tx_led, GPIO_OUTPUT_INACTIVE);
}
#endif
k_work_init_delayable(&s_rx_pulse_off, rx_pulse_off_handler);
return 0;
}
Expand Down Expand Up @@ -359,12 +409,14 @@ const char *ZephyrBoard::getManufacturerName() const

void ZephyrBoard::onBeforeTransmit()
{
#if HAS_TX_LED
/* Honour the LED master gate ("set leds off") and then the activity mode
* ("set leds.radio"). On a headless repeater this is the only LED that ever
* lights, so both have to be checked here and not just in the UI layer.
* onAfterTransmit() still clears the pin unconditionally, so a gate or mode
* flipped mid-transmit can't strand it lit. */
* flipped mid-transmit can't strand it lit. Same interlock on PWM-shared
* boards (T096, Wireless Tracker V2) as on plain-GPIO ones -- only the
* final write (tx_led_write()) differs. */
#if HAS_TX_LED_PWM || HAS_TX_LED
uint8_t mode = zephcore_leds_radio_mode();
if (!zephcore_leds_disabled() &&
(mode == LEDS_RADIO_TX || mode == LEDS_RADIO_ALL)) {
Expand All @@ -375,16 +427,16 @@ void ZephyrBoard::onBeforeTransmit()
#if ZEPHCORE_LED_PIN_SHARED
zephcore_led_radio_hold_pin(true);
#endif
gpio_pin_set_dt(&tx_led, 1);
tx_led_write(true);
}
#endif
}

void ZephyrBoard::onAfterTransmit()
{
#if HAS_TX_LED
#if HAS_TX_LED_PWM || HAS_TX_LED
atomic_set(&s_tx_lit, 0);
gpio_pin_set_dt(&tx_led, 0);
tx_led_write(false);
#if ZEPHCORE_LED_PIN_SHARED
zephcore_led_radio_hold_pin(false);
#endif
Expand All @@ -393,7 +445,7 @@ void ZephyrBoard::onAfterTransmit()

void ZephyrBoard::onPacketReceived()
{
#if HAS_TX_LED
#if HAS_TX_LED_PWM || HAS_TX_LED
uint8_t mode = zephcore_leds_radio_mode();
if (zephcore_leds_disabled() ||
(mode != LEDS_RADIO_RX && mode != LEDS_RADIO_ALL)) {
Expand All @@ -409,7 +461,7 @@ void ZephyrBoard::onPacketReceived()
#if ZEPHCORE_LED_PIN_SHARED
zephcore_led_radio_hold_pin(true);
#endif
gpio_pin_set_dt(&tx_led, 1);
tx_led_write(true);
/* Reschedule rather than schedule: back-to-back packets should extend the
* blink, not have the first one's handler cut the second one short. */
k_work_reschedule(&s_rx_pulse_off, K_MSEC(RX_PULSE_MS));
Expand Down
12 changes: 11 additions & 1 deletion zephcore/adapters/datastore/ZephyrDataStore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -907,6 +907,14 @@ void ZephyrDataStore::loadPrefs(NodePrefs &prefs)
if (off < len) {
prefs.leds_hb_mode = buf[off++];
}
/* Offset 174: led_brightness (ZephCore extension, since 1.17.4). Absent
* in pre-existing files -> stays at the initNodePrefs() default of
* ZEPHCORE_LED_DEFAULT_BRIGHTNESS_PCT (10%), same value a fresh node
* already showed before this field was persisted. Range re-checked by
* sanitizeNodePrefs() below. */
if (off < len) {
prefs.led_brightness = buf[off++];
}

sanitizeNodePrefs(&prefs);
}
Expand Down Expand Up @@ -1022,7 +1030,9 @@ void ZephyrDataStore::savePrefs(const NodePrefs &prefs)
/* Offset 172-173: leds_radio_mode / leds_hb_mode (ZephCore extension). */
buf[off++] = prefs.leds_radio_mode;
buf[off++] = prefs.leds_hb_mode;
/* Total: 174 bytes */
/* Offset 174: led_brightness (ZephCore extension, since 1.17.4). */
buf[off++] = prefs.led_brightness;
/* Total: 175 bytes */

bool ok = atomicReplaceFile(PREFS_FILE, buf, off);
LOG_DBG("savePrefs: wrote %s, ok=%d (%d bytes), name='%.16s'",
Expand Down
7 changes: 7 additions & 0 deletions zephcore/app/RepeaterDataStore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,11 @@ bool RepeaterDataStore::loadPrefs(NodePrefs& prefs) {
* (activity LED on transmit, heartbeat with unread indication). */
fs_read(&file, &prefs.leds_radio_mode, sizeof(prefs.leds_radio_mode));
fs_read(&file, &prefs.leds_hb_mode, sizeof(prefs.leds_hb_mode));
/* LED brightness, offset 311 (ZephCore extension, since 1.17.4). Absent
* in <312-byte files; leaves the initNodePrefs() default of
* ZEPHCORE_LED_DEFAULT_BRIGHTNESS_PCT (10%), same value a fresh node
* already showed before this field was persisted. */
fs_read(&file, &prefs.led_brightness, sizeof(prefs.led_brightness));

fs_close(&file);

Expand Down Expand Up @@ -467,6 +472,8 @@ bool RepeaterDataStore::savePrefs(const NodePrefs& prefs) {
/* LED activity/heartbeat modes (offsets 309-310) */
fs_write(&file, &prefs.leds_radio_mode, sizeof(prefs.leds_radio_mode));
fs_write(&file, &prefs.leds_hb_mode, sizeof(prefs.leds_hb_mode));
/* LED brightness (offset 311, since 1.17.4) */
fs_write(&file, &prefs.led_brightness, sizeof(prefs.led_brightness));

ret = fs_sync(&file);
fs_close(&file);
Expand Down
1 change: 1 addition & 0 deletions zephcore/app/main_observer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ int main(void)
zephcore_leds_set_disabled(leds_off);
zephcore_leds_set_radio_mode(prefs->leds_radio_mode);
zephcore_leds_set_hb_mode(prefs->leds_hb_mode);
zephcore_led_set_brightness_pct(prefs->led_brightness);
LOG_INF("LEDs: %s (from prefs)", leds_off ? "disabled" : "enabled");
}

Expand Down
15 changes: 15 additions & 0 deletions zephcore/boards/common/esp32s3_pwm_led.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Enable PWM on ESP32-S3 sysbuild targets (shared heartbeat/TX LED brightness,
# see the heartbeat-pwm-led / lora-tx-pwm-led devicetree aliases consumed by
# helpers/ui/ui_common.c and adapters/board/ZephyrBoard.cpp).
#
# Deliberately NOT in the board *_defconfig: that file is shared by every
# sysbuild image for the board, including mcuboot, whose minimal kernel
# config has no k_sem_give()/k_sem_take() -- CONFIG_PWM=y there links fine for
# the app image but fails mcuboot's link with "undefined reference to
# z_impl_k_sem_give/take" (drivers/pwm/pwm_led_esp32.c uses a semaphore).
# EXTRA_CONF_FILE scopes this to the app ("zephcore") image only.
#
# Build with:
# -DEXTRA_CONF_FILE="boards/common/esp32s3_usb.conf;boards/common/esp32s3_pwm_led.conf"

CONFIG_PWM=y
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@
led0 = &led0;
pwm-0 = &ledc0;
pwm-led0 = &pwm_led_white;
/* Single physical LED, shared between the heartbeat and LoRa TX
* activity roles (same node aliased twice), same pattern as T096 and
* Wireless Tracker V2 -- see helpers/ui/ui_common.c and
* adapters/board/ZephyrBoard.cpp (ZEPHCORE_LED_PIN_SHARED covers the
* arbitration between the two roles automatically). */
heartbeat-pwm-led = &pwm_led_white;
lora-tx-pwm-led = &pwm_led_white;
uart-0 = &uart0;
i2c-0 = &i2c0;
lora0 = &lora0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,11 @@
output-low;
};
};

ledc0_default: ledc0_default {
group1 {
pinmux = <LEDC_CH0_GPIO18>;
output-enable;
};
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
#include <zephyr/dt-bindings/input/input-event-codes.h>
#include <zephyr/dt-bindings/lora/sx126x.h>
#include <zephyr/dt-bindings/gpio/espressif-esp32-gpio.h>
#include <zephyr/dt-bindings/pwm/pwm.h>
#include "heltec_wireless_tracker_v2-pinctrl.dtsi"

/ {
Expand All @@ -39,6 +40,8 @@
aliases {
led0 = &status_led;
lora-tx-led = &status_led;
heartbeat-pwm-led = &pwm_status_led;
lora-tx-pwm-led = &pwm_status_led;
uart-0 = &uart0;
uart-2 = &uart2;
i2c-0 = &i2c0;
Expand Down Expand Up @@ -69,6 +72,22 @@
};
};

/* Same GPIO18 net as status_led above, driven through the LEDC PWM
* peripheral instead of a raw GPIO write. Single LED doubling as
* heartbeat, message/shutdown flash and LoRa TX indicator (same
* "Option 1" shared-brightness pattern as T096, see helpers/ui/ui_common.c
* and adapters/board/ZephyrBoard.cpp: heartbeat-pwm-led/lora-tx-pwm-led
* aliases give every one of those events a single 0-100 brightness knob,
* RAM-only, on top of the existing persisted on/off gate. */
pwm_leds {
compatible = "pwm-leds";

pwm_status_led: pwm_led_gpio18 {
label = "Status PWM LED";
pwms = <&ledc0 0 PWM_MSEC(10) PWM_POLARITY_NORMAL>;
};
};

buttons: buttons {
compatible = "gpio-keys";

Expand Down Expand Up @@ -231,6 +250,19 @@
status = "okay";
};

&ledc0 {
pinctrl-0 = <&ledc0_default>;
pinctrl-names = "default";
status = "okay";
#address-cells = <1>;
#size-cells = <0>;

channel0@0 {
reg = <0x0>;
timer = <0>;
};
};

&i2c0 {
status = "okay";
clock-frequency = <I2C_BITRATE_FAST>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,20 @@
low-power-enable;
};
};

/* Heartbeat/TX LED, PWM path (see heltec_t096_nrf52840.dts, pwm_led_white).
* Same P0.28 net as the plain gpio-leds node — free in pinctrl, no
* other peripheral claims it. */
pwm0_default: pwm0_default {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 28)>;
};
};

pwm0_sleep: pwm0_sleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 28)>;
low-power-enable;
};
};
};
Loading