Skip to content
Merged
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
16 changes: 13 additions & 3 deletions include/opendisplay_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@
* new version heading on each bump -- see AGENT INSTRUCTIONS below)
* --------------------------------------------------------------------------
* Unreleased (since 2.0)
* - LedFlags bit4: OD_LED_FLAG_BUTTON_PRESS (short LED flash on physical
* button press / button-wake synthetic click; default off).
* - BuzzerFlags bit1: OD_BUZZER_FLAG_BUTTON_PRESS (short chirp on physical
* button press / button-wake synthetic click; default off).
* - MsdStatusBits bit3: OD_MSD_STATUS_ENCRYPTION_ENABLED (1 = app-layer
* encryption active). Firmware sets it when encryption_enabled and master
* key are non-zero; dynamic[11] is all 0xFF when no display is configured.
* - Doc-only: fixed two comment shapes the codegen parser mis-read and added
* the CODEGEN AUTHORING RULES banner section to prevent recurrence. Split the
* combined BusFlags/PinBitmap @bits comment into one comment per group; folded
Expand Down Expand Up @@ -735,11 +742,12 @@ enum LedType {
OD_LED_TYPE_FOUR_SEPARATE = 3 /**< @doc "four separate LEDs" */
};

/* LedConfig.led_flags @bits LedFlags (bits 4-7 reserved). */
/* LedConfig.led_flags @bits LedFlags (bits 5-7 reserved). */
#define OD_LED_FLAG_LED1_INVERT (1u << 0) /* @doc "invert LED channel 1 polarity" */
#define OD_LED_FLAG_LED2_INVERT (1u << 1) /* @doc "invert LED channel 2 polarity" */
#define OD_LED_FLAG_LED3_INVERT (1u << 2) /* @doc "invert LED channel 3 polarity" */
#define OD_LED_FLAG_LED4_INVERT (1u << 3) /* @doc "invert LED channel 4 polarity" */
#define OD_LED_FLAG_BUTTON_PRESS (1u << 4) /* @doc "short flash on physical button press (incl. button-wake synthetic click); default off" */

/** @struct LedConfig @packet 0x21 @repeatable max=4
* @doc "LED channel pins + invert flags. Up to 4 instances. 22 bytes. NOTE: the
Expand Down Expand Up @@ -963,8 +971,9 @@ OD_STATIC_ASSERT(sizeof(struct TouchController) == 32, "TouchController wire siz
* 0x29 buzzer (config.yaml packet name: passive_buzzer)
* ----------------------------------------------------------------------- */

/* BuzzerConfig.flags @bits BuzzerFlags (bits 1-7 reserved). */
/* BuzzerConfig.flags @bits BuzzerFlags (bits 2-7 reserved). */
#define OD_BUZZER_FLAG_ENABLE_ACTIVE_HIGH (1u << 0) /* @doc "enable pin is active-high when set; otherwise active-low" */
#define OD_BUZZER_FLAG_BUTTON_PRESS (1u << 1) /* @doc "short chirp on physical button press (incl. button-wake synthetic click); default off" */

/** @struct BuzzerConfig @packet 0x29 @repeatable max=4
* @doc "Buzzer (passive piezo, PWM-driven). Up to 4 instances. 32 bytes. The tone
Expand Down Expand Up @@ -1225,7 +1234,8 @@ OD_STATIC_ASSERT(sizeof(struct AuthProof) == 32, "AuthProof wire size");
#define OD_MSD_STATUS_BATTERY_VOLTAGE_BIT8 (1u << 0) /* @doc "high bit of the 10-bit battery voltage (units of 10 mV); combine with battery_voltage_low" */
#define OD_MSD_STATUS_REBOOT_FLAG (1u << 1) /* @doc "device rebooted since last read" */
#define OD_MSD_STATUS_CONNECTION_REQUESTED (1u << 2) /* @doc "device is requesting a connection" */
#define OD_MSD_STATUS_RESERVED_3 (1u << 3) /* @reserved @doc "reserved; must be 0 (placeholder name for a future status flag; sits between the flags and the bits 4-7 counter)" */
#define OD_MSD_STATUS_ENCRYPTION_ENABLED (1u << 3) /* @doc "application-layer encryption active (encryption_enabled and non-zero master key)" */
#define OD_MSD_STATUS_RESERVED_3 OD_MSD_STATUS_ENCRYPTION_ENABLED /* legacy doc name */
#define OD_MSD_STATUS_MAIN_LOOP_COUNTER_SHIFT 4u /* @doc "bits 4-7: free-running main-loop nibble counter (liveness)" */
#define OD_MSD_STATUS_MAIN_LOOP_COUNTER_MASK 0xF0u /* @doc "mask for the bits 4-7 main-loop counter nibble" */

Expand Down
6 changes: 4 additions & 2 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,12 @@ default_envs =
[env]
lib_deps =
; Pinned: bb_epaper has no releases, so an unpinned URL means CI and local
; builds get whatever HEAD was at install time. 5dccfbb is "Added support for
; builds get whatever HEAD was at install time. Full SHA required: PlatformIO
; shallow-fetches the #ref (`git fetch --depth=1 origin <ref>`), and GitHub
; does not resolve abbreviated SHAs as refs. 5dccfbbf… is "Added support for
; the Seeed reTerminal E1004 and its 13.3 Spectra6 1200x1600 panel" -- the
; commit that landed E1004 upstream, replacing the limengdu fork pin.
https://github.com/bitbank2/bb_epaper.git#5dccfbb
https://github.com/bitbank2/bb_epaper.git#5dccfbbf553a9b0fe2547cbc4e60138e1ff2fb43
h2zero/NimBLE-Arduino@^2.5.0
extra_scripts =
pre:scripts/factory_config_gen.py
Expand Down
51 changes: 49 additions & 2 deletions src/ble_transport_esp32.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,30 @@ static volatile uint16_t s_disconnectReason = 0;
static volatile uint32_t s_connectedWord = 0; // identity of the last connect
static volatile uint32_t s_disconnectedWord = 0; // identity of the last disconnect

static uint32_t s_advBoostUntil = 0;
static constexpr uint32_t ESP_ADV_BOOST_MS = 3000u;
static constexpr uint16_t ESP_ADV_INTERVAL_MIN = 0x0100u; // 160 ms
static constexpr uint16_t ESP_ADV_INTERVAL_MAX = 0x0640u; // 1000 ms
static constexpr uint16_t ESP_ADV_BOOST_MIN = 0x0020u; // 20 ms
static constexpr uint16_t ESP_ADV_BOOST_MAX = 0x0030u; // 30 ms

static void applyAdvInterval(BLEAdvertising* pAdvertising, bool fast) {
if (pAdvertising == nullptr) {
return;
}
if (fast) {
pAdvertising->setMinInterval(ESP_ADV_BOOST_MIN);
pAdvertising->setMaxInterval(ESP_ADV_BOOST_MAX);
} else {
pAdvertising->setMinInterval(ESP_ADV_INTERVAL_MIN);
pAdvertising->setMaxInterval(ESP_ADV_INTERVAL_MAX);
}
}

static bool advBoostActive(uint32_t now) {
return s_advBoostUntil != 0u && now < s_advBoostUntil;
}

// --- the instance table (CONNECTION_POLICY R3 requirement 5) -----------------
// Sized by the connection cap. CONFIG_BT_NIMBLE_MAX_CONNECTIONS is 3 in the
// precompiled sdkconfig.h for S3/C3/C6 and absent for classic ESP32 (NimBLE's own
Expand Down Expand Up @@ -381,6 +405,7 @@ void BleTransport::startAdvertising() {
// name). Scan response is off by default in NimBLE 2.x, so no
// enableScanResponse() needed.
pAdvertising->setAdvertisementData(s_advertisementData);
applyAdvInterval(pAdvertising, advBoostActive(millis()));
s_server->getAdvertising()->start();
od_log_info("=== BLE advertising started successfully ===");
}
Expand Down Expand Up @@ -550,6 +575,7 @@ void BleTransport::setManufacturerData(const uint8_t* msd, uint8_t len) {
// enableScanResponse()/setPreferredParams() reset NimBLE's custom-data flag
// and would make start() drop this manufacturer-data payload.
pAdvertising->setAdvertisementData(fresh);
applyAdvInterval(pAdvertising, advBoostActive(millis()));
delay(50);
pAdvertising->start();
}
Expand Down Expand Up @@ -584,11 +610,32 @@ void BleTransport::requestFastLink() {
}

void BleTransport::boostAdvertising() {
// No-op: the temporary fast-advertising interval is nRF-only today.
s_advBoostUntil = millis() + ESP_ADV_BOOST_MS;
}

void BleTransport::tick() {
// No-op: nothing periodic to restore, since boostAdvertising() is a no-op.
static bool was_boosted = false;
const uint32_t now = millis();
const bool boosting = advBoostActive(now);
if (boosting) {
was_boosted = true;
return;
}
if (!was_boosted || s_server == nullptr || connectedCount() > 0) {
was_boosted = false;
s_advBoostUntil = 0;
return;
}
was_boosted = false;
s_advBoostUntil = 0;
BLEAdvertising* pAdvertising = s_server->getAdvertising();
if (pAdvertising == nullptr) {
return;
}
pAdvertising->stop();
applyAdvInterval(pAdvertising, false);
delay(50);
pAdvertising->start();
}

bool BleTransport::eventPending() const {
Expand Down
27 changes: 27 additions & 0 deletions src/buzzer_control.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -385,3 +385,30 @@ void passiveBuzzerPowerOffAlert(void) {
buzzer_set_enable(b, false);
buzzer_drive_off(b);
}

void passiveBuzzerButtonPressAlert(void) {
buzzer_stop_internal();
const BuzzerConfig* b = nullptr;
for (uint8_t i = 0; i < globalConfig.passive_buzzer_count; i++) {
const BuzzerConfig* cand = &globalConfig.passive_buzzers[i];
const uint8_t pin = cand->drive_pin;
if (pin == 0 || pin == 0xFF) {
continue;
}
if ((cand->flags & OD_BUZZER_FLAG_BUTTON_PRESS) == 0u) {
continue;
}
b = cand;
break;
}
if (!b) {
return;
}
const uint32_t centihz = buzzer_index_to_centihz(nA5);
buzzer_set_enable(b, true);
buzzer_hw_tone_start(b->drive_pin, centihz, b->duty_percent);
delay(60);
buzzer_hw_tone_stop(b->drive_pin);
buzzer_set_enable(b, false);
buzzer_drive_off(b);
}
1 change: 1 addition & 0 deletions src/buzzer_control.h
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ enum BuzzerNote : uint8_t {
void initPassiveBuzzers(void);
void handleBuzzerActivate(uint8_t* data, uint16_t len);
void passiveBuzzerPowerOffAlert(void);
void passiveBuzzerButtonPressAlert(void);
void buzzerService(void); // non-blocking playback tick, called from loop()
/**
* Silence the buzzer immediately. DEEP SLEEP ONLY -- not a session-teardown API.
Expand Down
133 changes: 114 additions & 19 deletions src/device_control.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
#include "touch_input.h"
#include "power_latch.h"
#include "buzzer_control.h"
#include "display_service.h"
#include "ble_transport.h"
#include "od_log.h"
#include <string.h>

#ifdef TARGET_ESP32
#include "wake_button.h"
void enterDeepSleep(bool force = false, uint16_t overrideSleepSeconds = 0);
#endif

Expand Down Expand Up @@ -235,7 +238,13 @@ static void pollAdcButtons() {
((l->press_count & 0x0F) << 3) |
((state & 0x01) << 7));
if (l->byte_index < 11) dynamicreturndata[l->byte_index] = data;
if (state != 0u) {
ble.boostAdvertising();
}
updatemsdata();
if (state != 0u) {
buttonPressFeedback();
}
od_log_debug("ADC btn pin %u adc=%d idx=%d id=%u cnt=%u state=%u",
l->pin, adc, btn, l->last_button_id, l->press_count, state);
}
Expand Down Expand Up @@ -330,9 +339,10 @@ static void led_all_off(struct LedConfig* led) {
if (led == NULL) {
return;
}
bool invertRed = (led->led_flags & 0x01) != 0;
bool invertGreen = (led->led_flags & 0x02) != 0;
bool invertBlue = (led->led_flags & 0x04) != 0;
bool invertRed = (led->led_flags & OD_LED_FLAG_LED1_INVERT) != 0;
bool invertGreen = (led->led_flags & OD_LED_FLAG_LED2_INVERT) != 0;
bool invertBlue = (led->led_flags & OD_LED_FLAG_LED3_INVERT) != 0;
bool invert4 = (led->led_flags & OD_LED_FLAG_LED4_INVERT) != 0;
if (led->led_1_r != 0xFF) {
digitalWrite(led->led_1_r, invertRed ? HIGH : LOW);
}
Expand All @@ -342,6 +352,9 @@ static void led_all_off(struct LedConfig* led) {
if (led->led_3_b != 0xFF) {
digitalWrite(led->led_3_b, invertBlue ? HIGH : LOW);
}
if (led->led_4 != 0xFF) {
digitalWrite(led->led_4, invert4 ? HIGH : LOW);
}
}

static void led_stop_internal(bool clear_mode) {
Expand Down Expand Up @@ -596,6 +609,97 @@ void handleLedStop(uint8_t* data, uint16_t len) {
sendResponse(successResponse, sizeof(successResponse));
}

static void led_channels_on(struct LedConfig* led) {
if (led == NULL) {
return;
}
const bool invert1 = (led->led_flags & OD_LED_FLAG_LED1_INVERT) != 0;
const bool invert2 = (led->led_flags & OD_LED_FLAG_LED2_INVERT) != 0;
const bool invert3 = (led->led_flags & OD_LED_FLAG_LED3_INVERT) != 0;
const bool invert4 = (led->led_flags & OD_LED_FLAG_LED4_INVERT) != 0;
if (led->led_1_r != 0xFF) {
digitalWrite(led->led_1_r, invert1 ? LOW : HIGH);
}
if (led->led_2_g != 0xFF) {
digitalWrite(led->led_2_g, invert2 ? LOW : HIGH);
}
if (led->led_3_b != 0xFF) {
digitalWrite(led->led_3_b, invert3 ? LOW : HIGH);
}
if (led->led_4 != 0xFF) {
digitalWrite(led->led_4, invert4 ? LOW : HIGH);
}
}

static void ledButtonPressAlert(void) {
if (s_led.active) {
return;
}
struct LedConfig* led = nullptr;
for (uint8_t i = 0; i < globalConfig.led_count; i++) {
struct LedConfig* cand = &globalConfig.leds[i];
if ((cand->led_flags & OD_LED_FLAG_BUTTON_PRESS) == 0u) {
continue;
}
if (cand->led_1_r == 0xFF && cand->led_2_g == 0xFF &&
cand->led_3_b == 0xFF && cand->led_4 == 0xFF) {
continue;
}
led = cand;
break;
}
if (!led) {
return;
}
led_channels_on(led);
delay(60);
led_all_off(led);
}

void buttonPressFeedback(void) {
ledButtonPressAlert();
passiveBuzzerButtonPressAlert();
}

static void publishButtonMsd(ButtonState* btn, uint8_t pressed) {
if (btn == nullptr || btn->byte_index >= 11) {
return;
}
btn->current_state = pressed ? 1u : 0u;
const uint8_t buttonData = (uint8_t)((btn->button_id & 0x07u) |
((btn->press_count & 0x0Fu) << 3) |
((btn->current_state & 0x01u) << 7));
dynamicreturndata[btn->byte_index] = buttonData;
ble.boostAdvertising();
updatemsdata();
}

#if defined(TARGET_ESP32)
void buttonWakeDeliverSyntheticClick(void) {
const int idx = wakeButtonFindIndex();
if (idx < 0) {
return;
}
ButtonState* btn = &buttonStates[idx];
btn->press_count = (uint8_t)((btn->press_count + 1u) & 0x0Fu);
od_log_info("Button wake: synthetic click id=%u pin=%u", btn->button_id, btn->pin);
// Publish pressed MSD first so scanners see it before blocking feedback delays.
publishButtonMsd(btn, 1u);
buttonPressFeedback();

// If the finger is still down, leave current_state=1 so the real release edge
// can fire the ISR. Only synthesize the up if the pin already released during
// boot (otherwise hosts never see a lift after a held wake press).
const bool pinState = digitalRead(btn->pin);
const bool stillHeld = btn->inverted ? !pinState : pinState;
if (!stillHeld) {
delay(80);
publishButtonMsd(btn, 0u);
}
ble.tick();
}
#endif

void processButtonEvents() {
powerButtonPoll();
pollConfiguredPowerOffButtons(); // no-op unless the board declares a latch
Expand All @@ -615,24 +719,15 @@ void processButtonEvents() {
uint8_t logicalState = logicalPressed ? 1 : 0;
btn->current_state = logicalState;
od_log_debug("Button: %u, Press count: %u, Current state: %u", btn->button_id, btn->press_count, btn->current_state);
uint8_t buttonData = (btn->button_id & 0x07) |
((btn->press_count & 0x0F) << 3) |
((btn->current_state & 0x01) << 7);
if (btn->byte_index < 11) {
dynamicreturndata[btn->byte_index] = buttonData;
// Publish before feedback: LED/buzzer use blocking delays and would
// otherwise postpone the pressed advertisement.
publishButtonMsd(btn, logicalState);
if (logicalState != 0u) {
buttonPressFeedback();
}
}
// ORDER IS LOAD-BEARING: boost first, publish second. updatemsdata() ends in
// setManufacturerData(), which calls applyAdvInterval() and then restarts
// advertising -- so the interval is chosen DURING the publish. Boosting
// afterwards set the deadline too late to affect the packet it exists for:
// the press went out at the 160 ms slow interval (~1 advertisement in a
// typical 230 ms press window, which a passive scanner routinely misses)
// while the release 230 ms later got the 20 ms boosted interval, because by
// then s_advBoostUntil was set. Net effect: a host saw "not pressed"
// reliably and "pressed" almost never.
ble.boostAdvertising(); // no-op where the stack has no fast-adv window
updatemsdata();
// boostAdvertising() runs inside publishButtonMsd(); order is load-bearing
// for nRF (interval chosen during setManufacturerData restart).
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/device_control.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

void reboot();
void processButtonEvents();
void buttonPressFeedback(void);
#if defined(TARGET_ESP32)
void buttonWakeDeliverSyntheticClick(void);
#endif
void flashLed(uint8_t color, uint8_t brightness);
void processLedFlash();
void initButtons();
Expand Down
Loading
Loading