mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-15 12:50:04 +00:00
t096: shared PWM brightness for the heartbeat/TX LED (unpersisted)
Single LED (P0.28) doubles as heartbeat, message flash, shutdown flash and LoRa TX indicator, all sharing the same physical pin. Adds one PWM channel on that pin (heartbeat-pwm-led/lora-tx-pwm-led aliases) and a single 0-100 brightness knob (get/set led) covering every one of those events at once, deliberately RAM-only: resets to a 10% default on every reboot regardless of CLI changes made in a previous session. The existing persisted on/off gate (set leds on|off) is untouched. Boards without heartbeat-pwm-led/lora-tx-pwm-led aliases keep their current plain-GPIO behavior unchanged.
This commit is contained in:
@@ -994,6 +994,11 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, const char* command, cha
|
||||
CLI_HAS_HB_LED ? "" : " (no heartbeat LED on this board)");
|
||||
} else if (memcmp(config, "leds", 4) == 0) {
|
||||
snprintf(reply, CLI_REPLY_SIZE, "> %s", _prefs->leds_disabled ? "off" : "on");
|
||||
} else if (memcmp(config, "led", 3) == 0) {
|
||||
/* Shared heartbeat/TX brightness, RAM-only — see led_gate.h.
|
||||
* Checked after "leds" above so "leds" itself never falls
|
||||
* through to this shorter prefix. */
|
||||
snprintf(reply, CLI_REPLY_SIZE, "> %u%%", (unsigned)zephcore_led_brightness_pct());
|
||||
#ifndef ZEPHCORE_REPEATER
|
||||
} else if (memcmp(config, "buzzer", 6) == 0) {
|
||||
uint8_t mode = zephcore_buzzer_mode_from_prefs(_prefs->buzzer_quiet);
|
||||
@@ -1276,6 +1281,17 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, const char* command, cha
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
} else if (memcmp(config, "led ", 4) == 0) {
|
||||
/* Brightness only, on/off stays on "set leds" above. RAM-only —
|
||||
* no savePrefs() call here on purpose, see led_gate.h. Checked
|
||||
* after "leds " so that command never falls through here. */
|
||||
int pct = atoi(&config[4]);
|
||||
if (pct < 0 || pct > 100) {
|
||||
strcpy(reply, "Error: must be 0-100");
|
||||
} else {
|
||||
zephcore_led_set_brightness_pct((uint8_t)pct);
|
||||
snprintf(reply, CLI_REPLY_SIZE, "OK - led=%d%%", pct);
|
||||
}
|
||||
#ifndef ZEPHCORE_REPEATER
|
||||
} else if (memcmp(config, "buzzer ", 7) == 0) {
|
||||
/* 0 = silent, 1 = sound + vibration, 2 = vibration only,
|
||||
|
||||
@@ -74,3 +74,21 @@ void zephcore_led_radio_hold_pin(bool held)
|
||||
{
|
||||
atomic_set(&s_radio_holds_pin, held ? 1 : 0);
|
||||
}
|
||||
|
||||
/* Same cross-thread readers/writers as the gate above (heartbeat work queue,
|
||||
* TX path, CLI) — atomic for the same reason. RAM-only on purpose: no
|
||||
* savePrefs() call anywhere near this, see led_gate.h. */
|
||||
static atomic_t s_led_brightness_pct = ATOMIC_INIT(ZEPHCORE_LED_DEFAULT_BRIGHTNESS_PCT);
|
||||
|
||||
uint8_t zephcore_led_brightness_pct(void)
|
||||
{
|
||||
return (uint8_t)atomic_get(&s_led_brightness_pct);
|
||||
}
|
||||
|
||||
void zephcore_led_set_brightness_pct(uint8_t pct)
|
||||
{
|
||||
if (pct > 100) {
|
||||
pct = 100;
|
||||
}
|
||||
atomic_set(&s_led_brightness_pct, pct);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,19 @@ bool zephcore_leds_disabled(void);
|
||||
* progress. Safe to call from any role, with or without a UI. */
|
||||
void zephcore_leds_set_disabled(bool disabled);
|
||||
|
||||
/* Shared PWM brightness (0-100) for boards whose heartbeat/TX LED is
|
||||
* PWM-capable (DT_ALIAS(heartbeat_pwm_led) / DT_ALIAS(lora_tx_pwm_led)).
|
||||
* Deliberately RAM-only, not persisted: resets to
|
||||
* ZEPHCORE_LED_DEFAULT_BRIGHTNESS_PCT on every reboot regardless of any
|
||||
* "set led <pct>" done during a previous session. Independent of the on/off
|
||||
* gate above: brightness only matters while the gate is open. No-op on
|
||||
* boards without a PWM-capable LED (the getter still returns the compiled
|
||||
* default, simply unused). */
|
||||
#define ZEPHCORE_LED_DEFAULT_BRIGHTNESS_PCT 10
|
||||
|
||||
uint8_t zephcore_led_brightness_pct(void);
|
||||
void zephcore_led_set_brightness_pct(uint8_t pct);
|
||||
|
||||
/* Called by zephcore_leds_set_disabled() after the flag changes. Weak no-op in
|
||||
* led_gate.c; helpers/ui/ui_common.c overrides it to stop/restart the heartbeat
|
||||
* cycle and refresh the UI's LED page. Not meant to be called directly. */
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "led_gate.h" /* shared with the LoRa TX LED */
|
||||
|
||||
#include <zephyr/drivers/gpio.h>
|
||||
#include <zephyr/drivers/pwm.h>
|
||||
#include <zephyr/dt-bindings/input/input-event-codes.h>
|
||||
#include <zephyr/kernel.h>
|
||||
#include <string.h>
|
||||
@@ -76,21 +77,63 @@ void ui_play_startup_chime(void)
|
||||
* messages; with both LEDs present, companions light led1 for unread.
|
||||
*/
|
||||
|
||||
#if DT_NODE_HAS_PROP(DT_ALIAS(led0), gpios)
|
||||
/* PWM path (brightness-capable, shared with the LoRa TX LED via the same
|
||||
* DT_ALIAS(heartbeat_pwm_led)/DT_ALIAS(lora_tx_pwm_led) pair when a board's
|
||||
* heartbeat and TX indicator are the same physical LED, e.g. T096) takes
|
||||
* priority over the plain digital led0/led1 fallback. Brightness comes from
|
||||
* helpers/led_gate.h — one dimmer for every event on this LED, not a
|
||||
* heartbeat-only one. */
|
||||
#if DT_NODE_EXISTS(DT_ALIAS(heartbeat_pwm_led))
|
||||
static const struct pwm_dt_spec s_heartbeat_led_pwm =
|
||||
PWM_DT_SPEC_GET(DT_ALIAS(heartbeat_pwm_led));
|
||||
#define HAS_HEARTBEAT_LED_PWM 1
|
||||
#define HAS_HEARTBEAT_LED 0
|
||||
#elif DT_NODE_HAS_PROP(DT_ALIAS(led0), gpios)
|
||||
static const struct gpio_dt_spec s_heartbeat_led =
|
||||
GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios);
|
||||
#define HAS_HEARTBEAT_LED_PWM 0
|
||||
#define HAS_HEARTBEAT_LED 1
|
||||
#elif DT_NODE_HAS_PROP(DT_ALIAS(led1), gpios)
|
||||
static const struct gpio_dt_spec s_heartbeat_led =
|
||||
GPIO_DT_SPEC_GET(DT_ALIAS(led1), gpios);
|
||||
#define HAS_HEARTBEAT_LED_PWM 0
|
||||
#define HAS_HEARTBEAT_LED 1
|
||||
#else
|
||||
#define HAS_HEARTBEAT_LED_PWM 0
|
||||
#define HAS_HEARTBEAT_LED 0
|
||||
#endif
|
||||
|
||||
/* "Some heartbeat LED exists, PWM or plain GPIO" — used where the on/off
|
||||
* write differs but everything else (work queue plumbing, readiness check)
|
||||
* is identical either way. */
|
||||
#define HAS_ANY_HEARTBEAT_LED (HAS_HEARTBEAT_LED_PWM || HAS_HEARTBEAT_LED)
|
||||
|
||||
#if HAS_HEARTBEAT_LED_PWM
|
||||
static inline bool heartbeat_led_is_ready(void)
|
||||
{
|
||||
return pwm_is_ready_dt(&s_heartbeat_led_pwm);
|
||||
}
|
||||
static inline void heartbeat_led_write(bool on)
|
||||
{
|
||||
uint32_t pulse = on ? (uint32_t)((uint64_t)s_heartbeat_led_pwm.period *
|
||||
zephcore_led_brightness_pct() / 100)
|
||||
: 0;
|
||||
pwm_set_pulse_dt(&s_heartbeat_led_pwm, pulse);
|
||||
}
|
||||
#elif HAS_HEARTBEAT_LED
|
||||
static inline bool heartbeat_led_is_ready(void)
|
||||
{
|
||||
return gpio_is_ready_dt(&s_heartbeat_led);
|
||||
}
|
||||
static inline void heartbeat_led_write(bool on)
|
||||
{
|
||||
gpio_pin_set_dt(&s_heartbeat_led, on ? 1 : 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Second LED for unread-message indication. Repeaters use led1 for LoRa TX
|
||||
* (via lora-tx-led alias) — no offline queue, so this is companion-only. */
|
||||
#if HAS_HEARTBEAT_LED && DT_NODE_HAS_PROP(DT_ALIAS(led0), gpios) && \
|
||||
#if HAS_ANY_HEARTBEAT_LED && DT_NODE_HAS_PROP(DT_ALIAS(led0), gpios) && \
|
||||
DT_NODE_HAS_PROP(DT_ALIAS(led1), gpios) && !defined(ZEPHCORE_REPEATER)
|
||||
static const struct gpio_dt_spec s_msg_led =
|
||||
GPIO_DT_SPEC_GET(DT_ALIAS(led1), gpios);
|
||||
@@ -103,7 +146,7 @@ static const struct gpio_dt_spec s_msg_led =
|
||||
#define LED_ON_MS 20 /* Normal pulse width */
|
||||
#define LED_ON_MSG_MS 200 /* Pulse width when unread messages */
|
||||
|
||||
#if HAS_HEARTBEAT_LED
|
||||
#if HAS_ANY_HEARTBEAT_LED
|
||||
static struct k_work_delayable s_led_on_work;
|
||||
static struct k_work_delayable s_led_off_work;
|
||||
|
||||
@@ -143,9 +186,11 @@ static void led_off_work_handler(struct k_work *work)
|
||||
ARG_UNUSED(work);
|
||||
/* Yield the pin if radio activity is holding it (shared-pin boards only;
|
||||
* everywhere else this always reads false). Clearing here would blank the
|
||||
* LED in the middle of a transmit. */
|
||||
* LED in the middle of a transmit. Routed through heartbeat_led_write()
|
||||
* rather than a direct gpio_pin_set_dt(), since s_heartbeat_led doesn't
|
||||
* even exist on PWM-capable boards (see HAS_HEARTBEAT_LED_PWM above). */
|
||||
if (!zephcore_led_radio_holds_pin()) {
|
||||
gpio_pin_set_dt(&s_heartbeat_led, 0);
|
||||
heartbeat_led_write(false);
|
||||
}
|
||||
#if HAS_MSG_LED
|
||||
gpio_pin_set_dt(&s_msg_led, 0);
|
||||
@@ -163,7 +208,7 @@ static void led_on_work_handler(struct k_work *work)
|
||||
|
||||
if (!zephcore_leds_disabled()) {
|
||||
if (hb_should_light(mc) && !zephcore_led_radio_holds_pin()) {
|
||||
gpio_pin_set_dt(&s_heartbeat_led, 1);
|
||||
heartbeat_led_write(true);
|
||||
}
|
||||
#if HAS_MSG_LED
|
||||
/* The unread LED is a separate pin, so it is governed by the mode
|
||||
@@ -177,7 +222,7 @@ static void led_on_work_handler(struct k_work *work)
|
||||
}
|
||||
k_work_reschedule(&s_led_off_work, K_MSEC(on_ms));
|
||||
}
|
||||
#endif /* HAS_HEARTBEAT_LED */
|
||||
#endif /* HAS_ANY_HEARTBEAT_LED */
|
||||
|
||||
/*
|
||||
* Weak: called after s_leds_disabled changes so each UI variant can sync its
|
||||
@@ -188,9 +233,13 @@ __attribute__((weak)) void ui_led_on_disabled_changed(bool disabled) { ARG_UNUSE
|
||||
|
||||
void ui_led_heartbeat_init(void)
|
||||
{
|
||||
#if HAS_ANY_HEARTBEAT_LED
|
||||
if (heartbeat_led_is_ready()) {
|
||||
#if HAS_HEARTBEAT_LED
|
||||
if (gpio_is_ready_dt(&s_heartbeat_led)) {
|
||||
gpio_pin_configure_dt(&s_heartbeat_led, GPIO_OUTPUT_INACTIVE);
|
||||
#else
|
||||
heartbeat_led_write(false);
|
||||
#endif
|
||||
k_work_init_delayable(&s_led_on_work, led_on_work_handler);
|
||||
k_work_init_delayable(&s_led_off_work, led_off_work_handler);
|
||||
k_work_reschedule(&s_led_on_work, K_NO_WAIT);
|
||||
@@ -207,15 +256,15 @@ void ui_led_heartbeat_init(void)
|
||||
|
||||
void ui_set_heartbeat_led(bool enabled)
|
||||
{
|
||||
#if HAS_HEARTBEAT_LED
|
||||
#if HAS_ANY_HEARTBEAT_LED
|
||||
if (enabled && !zephcore_leds_disabled()) {
|
||||
if (gpio_is_ready_dt(&s_heartbeat_led)) {
|
||||
if (heartbeat_led_is_ready()) {
|
||||
k_work_reschedule(&s_led_on_work, K_NO_WAIT);
|
||||
}
|
||||
} else {
|
||||
k_work_cancel_delayable(&s_led_on_work);
|
||||
k_work_cancel_delayable(&s_led_off_work);
|
||||
gpio_pin_set_dt(&s_heartbeat_led, 0);
|
||||
heartbeat_led_write(false);
|
||||
#if HAS_MSG_LED
|
||||
gpio_pin_set_dt(&s_msg_led, 0);
|
||||
#endif
|
||||
@@ -233,18 +282,18 @@ void ui_set_heartbeat_led(bool enabled)
|
||||
*/
|
||||
void zephcore_leds_ui_sync(bool disabled)
|
||||
{
|
||||
#if HAS_HEARTBEAT_LED
|
||||
#if HAS_ANY_HEARTBEAT_LED
|
||||
if (disabled) {
|
||||
k_work_cancel_delayable(&s_led_on_work);
|
||||
k_work_cancel_delayable(&s_led_off_work);
|
||||
gpio_pin_set_dt(&s_heartbeat_led, 0);
|
||||
heartbeat_led_write(false);
|
||||
#if HAS_MSG_LED
|
||||
gpio_pin_set_dt(&s_msg_led, 0);
|
||||
#endif
|
||||
} else if (!k_work_delayable_is_pending(&s_led_on_work) &&
|
||||
!k_work_delayable_is_pending(&s_led_off_work)) {
|
||||
/* Restart heartbeat only if it was stopped (avoids spurious pulse) */
|
||||
if (gpio_is_ready_dt(&s_heartbeat_led)) {
|
||||
if (heartbeat_led_is_ready()) {
|
||||
k_work_reschedule(&s_led_on_work, K_NO_WAIT);
|
||||
}
|
||||
}
|
||||
@@ -268,11 +317,11 @@ void ui_set_leds_disabled(bool disabled)
|
||||
* No-op when LEDs are disabled or hardware is absent. */
|
||||
void ui_led_flash_msg(void)
|
||||
{
|
||||
#if HAS_HEARTBEAT_LED
|
||||
if (!zephcore_leds_disabled() && gpio_is_ready_dt(&s_heartbeat_led)) {
|
||||
#if HAS_ANY_HEARTBEAT_LED
|
||||
if (!zephcore_leds_disabled() && heartbeat_led_is_ready()) {
|
||||
k_work_cancel_delayable(&s_led_on_work);
|
||||
k_work_cancel_delayable(&s_led_off_work);
|
||||
gpio_pin_set_dt(&s_heartbeat_led, 1);
|
||||
heartbeat_led_write(true);
|
||||
k_work_reschedule(&s_led_off_work, K_MSEC(LED_ON_MSG_MS));
|
||||
}
|
||||
#endif
|
||||
@@ -284,12 +333,12 @@ void ui_led_flash_msg(void)
|
||||
* even at power-off. */
|
||||
void ui_led_flash_shutdown(void)
|
||||
{
|
||||
#if HAS_HEARTBEAT_LED
|
||||
if (!zephcore_leds_disabled() && gpio_is_ready_dt(&s_heartbeat_led)) {
|
||||
#if HAS_ANY_HEARTBEAT_LED
|
||||
if (!zephcore_leds_disabled() && heartbeat_led_is_ready()) {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
gpio_pin_set_dt(&s_heartbeat_led, 1);
|
||||
heartbeat_led_write(true);
|
||||
k_sleep(K_MSEC(100));
|
||||
gpio_pin_set_dt(&s_heartbeat_led, 0);
|
||||
heartbeat_led_write(false);
|
||||
if (i < 2) {
|
||||
k_sleep(K_MSEC(100));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user