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
8 changes: 7 additions & 1 deletion include/opendisplay_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@
* Unreleased (since 2.1)
* - LedFlags bit4: OD_LED_FLAG_BUTTON_PRESS (short LED flash on physical
* button press / button-wake synthetic click; default off).
* - LedFlags bit5: OD_LED_FLAG_NFC (short white LED flash on NFC field
* detect rising edge; default off).
* - LedFlags bit6: OD_LED_FLAG_NFC_FANCY (multi-colour LED blink on NFC
* field detect; default off; takes precedence over bit5 when both set).
* - 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
Expand Down Expand Up @@ -750,12 +754,14 @@ enum LedType {
OD_LED_TYPE_FOUR_SEPARATE = 3 /**< @doc "four separate LEDs" */
};

/* LedConfig.led_flags @bits LedFlags (bits 5-7 reserved). */
/* LedConfig.led_flags @bits LedFlags (bit 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" */
#define OD_LED_FLAG_NFC (1u << 5) /* @doc "short white flash on NFC field-detect rising edge; default off" */
#define OD_LED_FLAG_NFC_FANCY (1u << 6) /* @doc "multi-colour blink on NFC field-detect rising edge; default off; overrides OD_LED_FLAG_NFC when both set" */

/** @struct LedConfig @packet 0x21 @repeatable max=4
* @doc "LED channel pins + invert flags. Up to 4 instances. 22 bytes. NOTE: the
Expand Down
7 changes: 3 additions & 4 deletions src/ble_transport.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,9 @@ class BleTransport {
// Each implementation keeps its own restart semantics (see the .cpp).
void setManufacturerData(const uint8_t* msd, uint8_t len);

// True where the stack re-arms advertising by itself after a disconnect
// (nRF: Bluefruit.Advertising.restartOnDisconnect(true)). Where it is false
// the application must schedule the restart itself. A genuine capability
// difference, stated as a query so callers need no target #ifdef.
// True where the stack re-arms advertising by itself after a disconnect.
// nRF and ESP32 both return false: the app owns slow re-arm so a disconnect
// cannot inherit a SoftDevice "fast" advertising phase.
bool restartsAdvertisingOnDisconnect() const;

// --- link policy (no-op where the stack does not support it) ---
Expand Down
13 changes: 12 additions & 1 deletion src/ble_transport_esp32.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -413,8 +413,15 @@ void BleTransport::startAdvertising() {
void BleTransport::restartAdvertising() {
// Unconditional by contract: the caller owns the "still connected / mid-EPD
// refresh / stack not up" deferral policy (see serviceBleAdvertisingRestart
// in main.cpp). The delay mirrors the historical sequence.
// in main.cpp). Force slow intervals so a disconnect restart never inherits
// a prior button-boost burst.
delay(100);
s_advBoostUntil = 0;
BLEAdvertising* pAdvertising = (s_server != nullptr) ? s_server->getAdvertising()
: BLEDevice::getAdvertising();
if (pAdvertising != nullptr) {
applyAdvInterval(pAdvertising, false);
}
BLEDevice::startAdvertising();
od_log_info("BLE advertising restarted");
}
Expand Down Expand Up @@ -587,6 +594,10 @@ void BleTransport::setManufacturerData(const uint8_t* msd, uint8_t len) {
// Called from loop() when the connect event is consumed, not from the connect
// callback -- these are host-stack calls, which the callback contract excludes.
void BleTransport::requestFastLink() {
// Connected: drop any button boost so a later advertising restart does not
// inherit 20–30 ms intervals from a still-open boost window.
s_advBoostUntil = 0;

// Tune the OWNER's link. This used to read the single s_connHandle scalar,
// which the newest connect overwrote -- so with a contender attached, link
// tuning targeted the wrong link (one of the shared-scalar defects R3 names).
Expand Down
64 changes: 46 additions & 18 deletions src/ble_transport_nrf.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,22 +59,25 @@ static volatile bool s_instanceSubscribed = false;
static volatile uint32_t s_instanceDecidedWord = 0;

// --- advertising interval policy --------------------------------------------
// Bluefruit always restarts advertising after disconnect in its "fast" phase
// (setFastTimeout). Use the slow interval for both phases so a disconnect does
// not burn a multi-second high-rate burst. Button wakes still call
// boostAdvertising() for a short 20–30 ms window.
static uint32_t s_advBoostUntil = 0;

static constexpr uint16_t NRF_ADV_INTERVAL_MIN = 256; // 160 ms
static constexpr uint16_t NRF_ADV_INTERVAL_MAX = 1600; // 1000 ms
static constexpr uint16_t NRF_ADV_BOOST_MIN = 32; // 20 ms
static constexpr uint16_t NRF_ADV_BOOST_MAX = 48; // 30 ms
static constexpr uint32_t NRF_ADV_BOOST_MS = 3000;

static void applyAdvInterval() {
if (!globalConfig.loaded) {
Bluefruit.Advertising.setInterval(NRF_ADV_BOOST_MIN, NRF_ADV_BOOST_MAX);
} else if (s_advBoostUntil != 0 && millis() < s_advBoostUntil) {
if (!globalConfig.loaded || (s_advBoostUntil != 0 && millis() < s_advBoostUntil)) {
Bluefruit.Advertising.setInterval(NRF_ADV_BOOST_MIN, NRF_ADV_BOOST_MAX);
} else {
s_advBoostUntil = 0;
Bluefruit.Advertising.setInterval(NRF_ADV_INTERVAL_MIN, NRF_ADV_INTERVAL_MAX);
// Identical fast/slow intervals: restartOnDisconnect starts in "fast"
// mode but at the slow rate — no post-disconnect advertising burst.
Bluefruit.Advertising.setInterval(NRF_ADV_INTERVAL_MAX, NRF_ADV_INTERVAL_MAX);
}
}

Expand Down Expand Up @@ -249,6 +252,9 @@ bool BleTransport::begin(const char* deviceName) {
od_log_info("BLE callbacks registered");
Bluefruit.setName(deviceName);
od_log_info("Device name set to: %s", deviceName);
// Preferred supervision timeout 1 s (was Bluefruit default 2 s). Central may
// still negotiate longer; shortens unclean link-loss radio hang when accepted.
Bluefruit.Periph.setConnSupervisionTimeoutMS(1000);
od_log_info("Configuring power management...");
sd_power_mode_set(NRF_POWER_MODE_LOWPWR);
sd_power_dcdc_mode_set(NRF_POWER_DCDC_ENABLE);
Expand All @@ -263,20 +269,29 @@ void BleTransport::startAdvertising() {
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addName();
// Deliberately kept inside this sequence rather than hoisted to the caller:
// updatemsdata() lands in setManufacturerData() below, which itself sets
// setFastTimeout(1). Calling it after the setFastTimeout(10) line would leave
// the fast-advertising window at 1 s instead of 10 s -- a real behaviour
// change. Phase 1 keeps the historical order byte-for-byte.
// updatemsdata() lands in setManufacturerData() below.
updatemsdata();
Bluefruit.Advertising.restartOnDisconnect(true);
// App-owned restart after disconnect (see restartAdvertising): SoftDevice
// auto-restart always began a timed "fast" phase and fought our slow policy.
Bluefruit.Advertising.restartOnDisconnect(false);
s_advBoostUntil = 0;
applyAdvInterval();
Bluefruit.Advertising.setFastTimeout(10);
// duration 0 + equal slow/slow intervals → continuous slow advertising, no
// timed burst phase after start or (via restartAdvertising) after disconnect.
Bluefruit.Advertising.setFastTimeout(0);
od_log_info("Starting BLE advertising...");
Bluefruit.Advertising.start(0);
}

void BleTransport::restartAdvertising() {
Bluefruit.Advertising.stop();
// Slow-only re-arm after disconnect. Do not call updatemsdata() here — that
// stop/start + possible MSD rebuild is what reintroduced radio bursts.
s_advBoostUntil = 0;
applyAdvInterval();
Bluefruit.Advertising.setFastTimeout(0);
if (Bluefruit.Advertising.isRunning()) {
Bluefruit.Advertising.stop();
}
Bluefruit.Advertising.start(0);
}

Expand Down Expand Up @@ -368,7 +383,16 @@ void BleTransport::setManufacturerData(const uint8_t* msd, uint8_t len) {
Bluefruit.Advertising.addName();
Bluefruit.Advertising.addData(BLE_GAP_AD_TYPE_MANUFACTURER_SPECIFIC_DATA, msd, len);
applyAdvInterval();
Bluefruit.Advertising.setFastTimeout(1);
// Boost window: short timed phase at boost intervals. Otherwise continuous
// slow (fast timeout 0 with equal slow/slow intervals).
if (s_advBoostUntil != 0 && millis() < s_advBoostUntil) {
Bluefruit.Advertising.setFastTimeout((uint16_t)((NRF_ADV_BOOST_MS + 999) / 1000));
} else {
Bluefruit.Advertising.setFastTimeout(0);
}
// Only touch the radio while disconnected. Rebuilding MSD while connected
// used to stop/start advertising and could leave a burst after the link dropped.
if (Bluefruit.connected()) return;
Bluefruit.Advertising.stop();
Bluefruit.Advertising.start(0);
}
Expand All @@ -384,6 +408,11 @@ void BleTransport::setManufacturerData(const uint8_t* msd, uint8_t len) {
// nothing -- the central's own request arrives later than this either way, which
// is why the diagnostics below log twice.
void BleTransport::requestFastLink() {
// Connected: drop any button boost so the next restartOnDisconnect does not
// inherit 20–30 ms intervals from a still-open boost window.
s_advBoostUntil = 0;
Bluefruit.Advertising.setInterval(NRF_ADV_INTERVAL_MAX, NRF_ADV_INTERVAL_MAX);

BLEConnection* conn = Bluefruit.Connection(s_connHandle);
if (conn == nullptr) return;

Expand Down Expand Up @@ -427,7 +456,8 @@ void BleTransport::tick() {
}
was_boosted = false;
s_advBoostUntil = 0;
Bluefruit.Advertising.setInterval(NRF_ADV_INTERVAL_MIN, NRF_ADV_INTERVAL_MAX);
Bluefruit.Advertising.setInterval(NRF_ADV_INTERVAL_MAX, NRF_ADV_INTERVAL_MAX);
Bluefruit.Advertising.setFastTimeout(0);
Bluefruit.Advertising.stop();
Bluefruit.Advertising.start(0);
}
Expand Down Expand Up @@ -476,10 +506,8 @@ bool BleTransport::takeDisconnectedEvent(uint16_t* reason, uint32_t* instanceWor
}

bool BleTransport::restartsAdvertisingOnDisconnect() const {
// Bluefruit.Advertising.restartOnDisconnect(true) in startAdvertising(): the
// SoftDevice re-arms the radio itself, so the application must not also
// schedule a restart or the two fight over the advertising state.
return true;
// restartOnDisconnect(false): loop() owns slow re-arm via restartAdvertising().
return false;
}

const char* BleTransport::addressString() {
Expand Down
5 changes: 2 additions & 3 deletions src/communication.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,8 @@ void resetAuthAbuseCounter(void);
/// transport that OWNS the transfer is confirmed gone.
void requestTransferSessionCleanup(void);

/// Re-arm BLE advertising when it is safe to. A no-op on targets whose stack
/// re-arms itself (see BleTransport::restartsAdvertisingOnDisconnect), so
/// callers need no target guard.
/// Re-arm BLE advertising when it is safe to. Schedules application-owned slow
/// re-arm on both targets (see BleTransport::restartsAdvertisingOnDisconnect).
void requestAdvertisingRestart(void);

#endif
55 changes: 52 additions & 3 deletions src/display_service.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -343,12 +343,17 @@ static void pwrmgmLockGive(void) {
// refresh looked correct, then the image darkened in over the following minutes.
static const uint16_t EPD_POST_SLEEP_BLEED_MS = 200;

static bool panel_is_ep426(void) {
return bbep.type == EP426_800x480 || bbep.type == EP426_800x480_4GRAY;
}

// SSD16xx bbepSleep() only sends deep-sleep; it does not run the analog/HV
// shutdown. Force EOPT discharge frames + the GoodDisplay/GxEPD2 power-off
// sequence (enable clock → disable analog → disable OSC) while SPI is still up.
// shutdown. The EOPT + 0x83 discharge ("gray fix") is required on EP42B 400x300
// but burns ~1 s @ high current on EP426 800x480 — skip the whole sequence there.
static void epdSsd16xxPowerOffDischarge(void) {
if (bbep.chip_type != BBEP_CHIP_SSD16xx) return;
if (!bbep.is_awake) return;
if (panel_is_ep426()) return;
bbepCMD2(&bbep, 0x3F, 0x22); // EOPT: TFT discharge frames + sequenced VCOM/HV
bbepCMD2(&bbep, SSD1608_DISP_CTRL2, 0x83);
bbepWriteCmd(&bbep, SSD1608_MASTER_ACTIVATE);
Expand Down Expand Up @@ -376,7 +381,7 @@ static void epdSessionForceOffLocked(void) {
odWatchdogFeed(); // reload before entering bb_epaper (may block ~240 s)
epdSsd16xxPowerOffDischarge();
bbepSleep(&bbep, 1);
delay(EPD_POST_SLEEP_BLEED_MS);
if (!panel_is_ep426()) delay(EPD_POST_SLEEP_BLEED_MS);
}
pwrmgm(false); // -> PWR_OFF, clears deadline
epdPlanesPrepared = false;
Expand Down Expand Up @@ -3381,9 +3386,53 @@ static bool partial_write_stream_bytes(uint8_t* data, uint32_t len) {
return true;
}

// EP426 (SSD1677) OTP Mode-2 under-drives W→B. Register LUT follows EP426B's
// VS row layout (white on row1 / black on row2). Single ~50-frame phase —
// dual 0x32/0x32 looked settled after the first HV spike, so the second
// phase was mostly wasted energy. Activate with 0xDC (register LUT, no OTP).
static const uint8_t epd426_partial_lut[105] = {
// VS: same channel order as epd426b_init_part (not WW/BW/WB labels)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// TP/RP groups 0..9 — one ~50-frame pulse (was 0x32,0x32)
0x32, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
0x22, 0x22, 0x22, 0x22, 0x22,
};

static void epd426_load_custom_partial_lut(void) {
uint8_t ctrl1[2] = {0x00, 0x00};
uint8_t src[3] = {0x41, 0xa8, 0x32};
bbepWriteCmd(&bbep, SSD1608_WRITE_LUT);
bbepWriteData(&bbep, (uint8_t *)epd426_partial_lut, (int)sizeof(epd426_partial_lut));
bbepCMD2(&bbep, SSD1608_GATE_VOLTAGE, 0x17);
bbepWriteCmd(&bbep, SSD1608_SOURCE_VOLTAGE);
bbepWriteData(&bbep, src, 3);
bbepCMD2(&bbep, SSD1608_WRITE_VCOM, 0x40); // EP426B partial VCOM (0x30 drifts white→gray)
bbepWriteCmd(&bbep, SSD1608_DISP_CTRL1);
bbepWriteData(&bbep, ctrl1, 2);
}

static bool partial_trigger_refresh(int refreshMode) {
if (refreshMode < 0 || refreshMode > 3) refreshMode = REFRESH_PARTIAL;
if (panel_skips_reinit_on_partial_refresh(&bbep)) {
if (refreshMode == REFRESH_PARTIAL && bbep.type == EP426_800x480) {
epd426_load_custom_partial_lut();
bbepCMD2(&bbep, SSD1608_DISP_CTRL2, 0xdc);
bbepWriteCmd(&bbep, SSD1608_MASTER_ACTIVATE);
return waitforrefresh(60);
}
if (panel_uses_ep397_y_decrement(&bbep)) {
static const uint8_t u8CMDz3[4] = {0xf7, 0xd7, 0xff, 0};
bbepCMD2(&bbep, SSD1608_DISP_CTRL2, u8CMDz3[refreshMode]);
Expand Down
11 changes: 3 additions & 8 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -530,13 +530,8 @@ static void serviceBleDisconnectCleanup() {
// "not yet" paths so a later pass retries.
static void serviceBleAdvertisingRestart() {
if (!s_advertisingRestartPending) return;
// Capability gate, and the reason this helper is safe for ANY caller to
// raise the flag: where the stack re-arms advertising itself (nRF's
// restartOnDisconnect(true)), driving our own stop()/start() would fight it.
// Refusing here rather than at each raise site means a new raiser -- the
// post-refresh hook in display_service.cpp, or a future portable
// requestAdvertisingRestart() -- cannot reintroduce that conflict by
// forgetting a target guard.
// Capability gate: where the stack re-arms advertising itself, driving our
// own stop()/start() would fight it.
if (ble.restartsAdvertisingOnDisconnect()) {
s_advertisingRestartPending = false;
return;
Expand All @@ -548,8 +543,8 @@ static void serviceBleAdvertisingRestart() {
}
if (epdRefreshInProgress) return; // never mid-refresh
s_advertisingRestartPending = false;
// Slow re-arm only — no updatemsdata() (that stop/starts advertising again).
ble.restartAdvertising();
updatemsdata();
}

// Translate the transport's consume-once connect/disconnect events into the
Expand Down
Loading