From 43df87df4148f23bf0b7f807243fd1a13c9b434f Mon Sep 17 00:00:00 2001 From: rcarteraz Date: Wed, 2 Sep 2026 15:14:04 -0700 Subject: [PATCH 1/5] Add DFU screen support for I2C mono OLEDs Adds an SSD1306/SH1106 display path alongside the existing SPI TFT one, so boards with an I2C OLED show a screen while waiting in UF2 or BLE OTA DFU mode instead of leaving the panel dark. Enabled on RAK4631, RAK3401, Wio Tracker L1, ProMicro nRF52840 and XIAO nRF52840/Sense. Boards with no display build byte-identical. Also fixes an out-of-bounds write in screen.c's printicon(): on the 160px-wide T096 and T1 the third drag-screen icon was drawn 386 bytes past the end of frame_buf. --- src/boards/boards.c | 220 ++++++++++++++++++++- src/boards/boards.h | 32 ++- src/boards/heltec_t096/board.h | 3 + src/boards/heltec_t1/board.h | 3 + src/boards/promicro_nrf52840/board.h | 24 +++ src/boards/wio_tracker_l1/board.h | 20 ++ src/boards/wiscore_rak3401/board.h | 26 +++ src/boards/wiscore_rak4631_board/board.h | 30 +++ src/boards/xiao_nrf52840_ble/board.h | 24 +++ src/boards/xiao_nrf52840_ble_sense/board.h | 24 +++ src/images.c | 2 +- src/main.c | 7 +- src/screen.c | 128 +++++++++++- src/usb/usb.c | 7 +- 14 files changed, 536 insertions(+), 14 deletions(-) diff --git a/src/boards/boards.c b/src/boards/boards.c index f12e459bd..fe319760a 100644 --- a/src/boards/boards.c +++ b/src/boards/boards.c @@ -159,7 +159,7 @@ void board_teardown(void) { neopixel_teardown(); #endif -#ifdef DISPLAY_PIN_SCK +#ifdef BOARD_HAS_DISPLAY board_display_teardown(); #endif @@ -238,7 +238,7 @@ static void tft_cmd(uint8_t cmd, uint8_t const* data, size_t narg) { tft_cs(true); } -void board_display_init(void) { +bool board_display_init(void) { //------------- SPI init -------------// // highspeed SPIM should set SCK and MOSI to high drive nrf_gpio_cfg(DISPLAY_PIN_SCK, NRF_GPIO_PIN_DIR_OUTPUT, NRF_GPIO_PIN_INPUT_CONNECT, @@ -277,6 +277,9 @@ void board_display_init(void) { #endif tft_controller_init(); + + // A soldered-on TFT is always present; nothing here can fail. + return true; } void board_display_teardown(void) { @@ -928,4 +931,215 @@ static void tft_controller_init(void) { } } -#endif \ No newline at end of file +#endif +//--------------------------------------------------------------------+ +// Display controller - I2C mono OLED (SSD1306 / SH1106) +//--------------------------------------------------------------------+ +// Unlike the SPI TFTs above, an I2C OLED is frequently a plug-in module +// (the RAK1921 in a WisBlock IO slot, for instance) that may simply not be +// fitted. Every transfer here is therefore bounded and failure-tolerant: a +// bootloader that spins forever waiting on an absent panel is far worse +// than one that shows no picture. + +#ifdef DISPLAY_PIN_SDA +#include "nrf_twim.h" + +#ifndef DISPLAY_I2C_ADDR +#define DISPLAY_I2C_ADDR 0x3C +#endif + +#ifndef DISPLAY_COL_OFFSET +// SH1106 and friends have 132 columns of GDDRAM behind a 128-column glass, +// centred, so the visible window starts at column 2. +#define DISPLAY_COL_OFFSET 0 +#endif + +// Total addressable columns, which may exceed the visible width. Only the +// blanking pass cares: columns outside the glass still hold undefined data +// at power-up, and on a panel whose offset is slightly off they show. +#ifndef DISPLAY_GDDRAM_WIDTH +#define DISPLAY_GDDRAM_WIDTH (DISPLAY_COL_OFFSET + DISPLAY_WIDTH) +#endif + +#ifndef DISPLAY_CONTRAST +#define DISPLAY_CONTRAST 0xCF +#endif + +// Milliseconds to let the panel's rail come up before we talk to it. +#ifndef DISPLAY_POWER_ON_MS +#define DISPLAY_POWER_ON_MS 100 +#endif + +// TWIM0 is the same peripheral ID as SPIM0, which the TFT path uses; boards.h +// rejects a board.h that asks for both. +#define _twim NRF_TWIM0 + +// I2C control bytes: Co=0, D/C# selects the command or data stream. +#define OLED_CTRL_CMD 0x00 +#define OLED_CTRL_DATA 0x40 + +// Bound every wait. At 400 kHz a 129-byte page is ~2.9 ms; this is a couple +// of orders of magnitude beyond that, so it only ever fires on a dead bus. +#define TWI_GUARD_LOOPS 2000000UL + +static bool _display_present = false; + +static void oled_write_page(uint8_t page, uint8_t col, uint8_t const* buf, size_t nbytes); + +// Blocking write of a whole I2C transaction. Returns false on NACK, bus +// error, or timeout, leaving the peripheral idle and ready for the next try. +static bool twi_write(uint8_t const* buf, size_t len) { + nrf_twim_event_clear(_twim, NRF_TWIM_EVENT_STOPPED); + nrf_twim_event_clear(_twim, NRF_TWIM_EVENT_ERROR); + (void) nrf_twim_errorsrc_get_and_clear(_twim); + + nrf_twim_tx_buffer_set(_twim, buf, len); + nrf_twim_shorts_set(_twim, NRF_TWIM_SHORT_LASTTX_STOP_MASK); + nrf_twim_task_trigger(_twim, NRF_TWIM_TASK_STARTTX); + + uint32_t guard = TWI_GUARD_LOOPS; + while (!nrf_twim_event_check(_twim, NRF_TWIM_EVENT_STOPPED) && + !nrf_twim_event_check(_twim, NRF_TWIM_EVENT_ERROR) && + --guard) {} + + bool const ok = (guard != 0) && !nrf_twim_event_check(_twim, NRF_TWIM_EVENT_ERROR); + + if (!ok) { + // An address NACK or a stuck bus can leave the transfer half-done. + nrf_twim_task_trigger(_twim, NRF_TWIM_TASK_STOP); + guard = TWI_GUARD_LOOPS; + while (!nrf_twim_event_check(_twim, NRF_TWIM_EVENT_STOPPED) && --guard) {} + (void) nrf_twim_errorsrc_get_and_clear(_twim); + } + + return ok; +} + +// EasyDMA can only read from RAM, so command lists must live on the stack, +// never in flash. +static bool oled_cmds(uint8_t* buf, size_t len) { + buf[0] = OLED_CTRL_CMD; + return twi_write(buf, len); +} + +bool board_display_init(void) { + _display_present = false; + +#if defined(DISPLAY_VSENSOR_PIN) && DISPLAY_VSENSOR_PIN >= 0 + nrf_gpio_cfg_output(DISPLAY_VSENSOR_PIN); + nrf_gpio_pin_write(DISPLAY_VSENSOR_PIN, DISPLAY_VSENSOR_ON); + NRFX_DELAY_MS(DISPLAY_POWER_ON_MS); +#endif + + // Open-drain with the internal pull-ups engaged. Boards that carry their + // own pull-ups are unaffected; boards with an empty socket get a bus that + // idles high, so a missing panel NACKs cleanly instead of hanging. + nrf_gpio_cfg(DISPLAY_PIN_SDA, NRF_GPIO_PIN_DIR_INPUT, NRF_GPIO_PIN_INPUT_CONNECT, + NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_S0D1, NRF_GPIO_PIN_NOSENSE); + nrf_gpio_cfg(DISPLAY_PIN_SCL, NRF_GPIO_PIN_DIR_INPUT, NRF_GPIO_PIN_INPUT_CONNECT, + NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_S0D1, NRF_GPIO_PIN_NOSENSE); + + nrf_twim_pins_set(_twim, DISPLAY_PIN_SCL, DISPLAY_PIN_SDA); + nrf_twim_frequency_set(_twim, NRF_TWIM_FREQ_400K); + nrf_twim_address_set(_twim, DISPLAY_I2C_ADDR); + nrf_twim_enable(_twim); + + // Display-off doubles as the presence probe: if nobody ACKs the address + // there is no panel, and we leave the bus alone from here on. + uint8_t probe[] = { OLED_CTRL_CMD, 0xAE }; + if (!oled_cmds(probe, sizeof(probe))) { + nrf_twim_disable(_twim); + return false; + } + + uint8_t init[] = { + OLED_CTRL_CMD, + 0xD5, 0x80, // clock divide / oscillator frequency + 0xA8, (uint8_t)(DISPLAY_HEIGHT - 1), // multiplex ratio + 0xD3, 0x00, // display offset + 0x40, // start line 0 +#ifdef DISPLAY_CONTROLLER_SH1106 + 0xAD, 0x8B, // SH1106 DC-DC converter on +#else + 0x8D, 0x14, // SSD1306 charge pump on +#endif + // Page addressing mode. Both parts support it, and it lets one + // draw-page routine cover the SH1106, which has no horizontal mode. + 0x20, 0x02, + 0xA1, // segment remap: column 127 -> SEG0 + 0xC8, // COM scan direction: reversed +#if DISPLAY_HEIGHT >= 64 + 0xDA, 0x12, // COM pins: alternative, no remap +#else + 0xDA, 0x02, // COM pins: sequential, no remap +#endif + 0x81, DISPLAY_CONTRAST, + 0xD9, 0xF1, // pre-charge period + 0xDB, 0x40, // VCOMH deselect level + 0xA4, // resume from RAM contents + 0xA6, // non-inverted + 0x2E, // scrolling off + }; + if (!oled_cmds(init, sizeof(init))) { + nrf_twim_disable(_twim); + return false; + } + + _display_present = true; + + // GDDRAM contents are undefined at power-up, and the panel is still off + // from the probe. Blank the RAM first so switching on cannot flash noise. + uint8_t blank[DISPLAY_GDDRAM_WIDTH]; + memset(blank, 0, sizeof(blank)); + for (uint8_t page = 0; page < DISPLAY_HEIGHT / 8; ++page) { + oled_write_page(page, 0, blank, sizeof(blank)); + } + + // Display on. The probe above sent 0xAE; without this the panel stays + // dark no matter what we write to it. + uint8_t on[] = { OLED_CTRL_CMD, 0xAF }; + if (!oled_cmds(on, sizeof(on))) { + _display_present = false; + nrf_twim_disable(_twim); + return false; + } + + return true; +} + +void board_display_teardown(void) { + if (_display_present) { + uint8_t off[] = { OLED_CTRL_CMD, 0xAE }; + (void) oled_cmds(off, sizeof(off)); + _display_present = false; + } + nrf_twim_disable(_twim); +} + +static void oled_write_page(uint8_t page, uint8_t col, uint8_t const* buf, size_t nbytes) { + if (!_display_present || nbytes > DISPLAY_GDDRAM_WIDTH) return; + + // Page addressing: set the page, then the low and high nibbles of the + // starting column, then stream the row. + uint8_t addr[] = { + OLED_CTRL_CMD, + (uint8_t)(0xB0 | (page & 0x0F)), + (uint8_t)(0x00 | (col & 0x0F)), + (uint8_t)(0x10 | ((col >> 4) & 0x0F)), + }; + if (!oled_cmds(addr, sizeof(addr))) return; + + // The control byte has to be contiguous with the pixels for EasyDMA, and + // the caller's buffer has no room in front of it. + static uint8_t page_buf[1 + DISPLAY_GDDRAM_WIDTH]; + page_buf[0] = OLED_CTRL_DATA; + memcpy(page_buf + 1, buf, nbytes); + (void) twi_write(page_buf, nbytes + 1); +} + +void board_display_draw_page(uint8_t page, uint8_t const* buf, size_t nbytes) { + if (nbytes > DISPLAY_WIDTH) return; + oled_write_page(page, DISPLAY_COL_OFFSET, buf, nbytes); +} + +#endif // DISPLAY_PIN_SDA diff --git a/src/boards/boards.h b/src/boards/boards.h index e85f76d33..4c1d91f77 100644 --- a/src/boards/boards.h +++ b/src/boards/boards.h @@ -117,12 +117,38 @@ bool is_ota(void); //--------------------------------------------------------------------+ // Display //--------------------------------------------------------------------+ -#ifdef DISPLAY_PIN_SCK -void board_display_init(void); +// A board opts into the DFU screens by naming a display bus in board.h: +// DISPLAY_PIN_SCK for the SPI TFT path, DISPLAY_PIN_SDA for the I2C mono +// path. They are mutually exclusive: TWIM0 and SPIM0 are the same +// peripheral ID and cannot both be enabled. +#if defined(DISPLAY_PIN_SCK) && defined(DISPLAY_PIN_SDA) +#error "board.h defines both DISPLAY_PIN_SCK and DISPLAY_PIN_SDA - pick one display bus" +#endif + +#if defined(DISPLAY_PIN_SCK) || defined(DISPLAY_PIN_SDA) +#define BOARD_HAS_DISPLAY 1 +#endif + +// Mono (1bpp paged) panels vs 16-bit colour TFTs. +#if defined(DISPLAY_CONTROLLER_SSD1306) || defined(DISPLAY_CONTROLLER_SH1106) +#define DISPLAY_MONO 1 +#endif + +#ifdef BOARD_HAS_DISPLAY +// Returns false if no panel answered, in which case nothing else here may be +// called. Only the I2C path can fail: an OLED is a plug-in module on some +// boards, so its absence is a normal condition, not an error. +bool board_display_init(void); void board_display_teardown(void); -void board_display_draw_line(uint16_t y, uint8_t const* buf, size_t nbytes); void screen_draw_drag(void); void screen_draw_ble(void); + +#ifdef DISPLAY_MONO +// One 8-row page, DISPLAY_WIDTH bytes, LSB = topmost row of the page. +void board_display_draw_page(uint8_t page, uint8_t const* buf, size_t nbytes); +#else +void board_display_draw_line(uint16_t y, uint8_t const* buf, size_t nbytes); +#endif #endif //--------------------------------------------------------------------+ diff --git a/src/boards/heltec_t096/board.h b/src/boards/heltec_t096/board.h index cf52e1ae6..62eeae709 100644 --- a/src/boards/heltec_t096/board.h +++ b/src/boards/heltec_t096/board.h @@ -100,6 +100,9 @@ #define BLE_OTA_Y 38 #define DRAG 34 #define DRAGX 4 +// Three 32px icons have to fit in 160px: the default pendriveLogo_X of +// 129 puts the last one at x=133..164, off the right edge of the panel. +#define pendriveLogo_X 124 //--------------------------------------------------------------------+ // BLE OTA diff --git a/src/boards/heltec_t1/board.h b/src/boards/heltec_t1/board.h index a8ba1be2e..1bd71e146 100644 --- a/src/boards/heltec_t1/board.h +++ b/src/boards/heltec_t1/board.h @@ -99,6 +99,9 @@ #define BLE_OTA_Y 38 #define DRAG 34 #define DRAGX 4 +// Three 32px icons have to fit in 160px: the default pendriveLogo_X of +// 129 puts the last one at x=133..164, off the right edge of the panel. +#define pendriveLogo_X 124 //--------------------------------------------------------------------+ // BLE OTA diff --git a/src/boards/promicro_nrf52840/board.h b/src/boards/promicro_nrf52840/board.h index 2a85aaea6..3c968f264 100644 --- a/src/boards/promicro_nrf52840/board.h +++ b/src/boards/promicro_nrf52840/board.h @@ -44,6 +44,30 @@ #define BUTTON_2 _PINNUM(1, 0) #define BUTTON_PULL NRF_GPIO_PIN_PULLUP +/*------------------------------------------------------------------*/ +/* DISPLAY - optional SSD1306/SH1106 OLED (commonly user-wired) + * + * Pins follow the default Meshtastic promicro variant. The EASYPROMICRO + * arrangement puts SCL on P1.06 instead; on such a board the probe simply + * finds nothing and the screen stays blank, which is harmless. + *------------------------------------------------------------------*/ +#define DISPLAY_CONTROLLER_SSD1306 +#define DISPLAY_I2C_ADDR 0x3C + +#define DISPLAY_PIN_SDA _PINNUM(1, 4) +#define DISPLAY_PIN_SCL _PINNUM(0, 11) + +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 64 +// Not yet verified on hardware. Offset 0 is right for a true 128-column +// SSD1306; a 132-column SH1106 will sit 2px left of centre, in which case set +// DISPLAY_COL_OFFSET to 2. Blanking spans 132 either way so no uncovered +// column can show power-up garbage. +#define DISPLAY_COL_OFFSET 0 +#define DISPLAY_GDDRAM_WIDTH 132 + +#define DISPLAY_TITLE "ProMicro" + //--------------------------------------------------------------------+ // BLE OTA //--------------------------------------------------------------------+ diff --git a/src/boards/wio_tracker_l1/board.h b/src/boards/wio_tracker_l1/board.h index 67670f749..ba0c566d1 100644 --- a/src/boards/wio_tracker_l1/board.h +++ b/src/boards/wio_tracker_l1/board.h @@ -44,6 +44,26 @@ #define BUTTON_2 _PINNUM(0, 8) #define BUTTON_PULL NRF_GPIO_PIN_PULLUP +/*------------------------------------------------------------------*/ +/* DISPLAY - SSD1306 OLED (variant declares HAS_SCREEN 1, USE_SSD1306 1) + *------------------------------------------------------------------*/ +#define DISPLAY_CONTROLLER_SSD1306 +#define DISPLAY_I2C_ADDR 0x3C + +#define DISPLAY_PIN_SDA _PINNUM(0, 9) +#define DISPLAY_PIN_SCL _PINNUM(0, 10) + +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 64 +// Not yet verified on hardware. Offset 0 is right for a true 128-column +// SSD1306; a 132-column SH1106 will sit 2px left of centre, in which case set +// DISPLAY_COL_OFFSET to 2. Blanking spans 132 either way so no uncovered +// column can show power-up garbage. +#define DISPLAY_COL_OFFSET 0 +#define DISPLAY_GDDRAM_WIDTH 132 + +#define DISPLAY_TITLE "TRACKER L1" + //--------------------------------------------------------------------+ // BLE OTA //--------------------------------------------------------------------+ diff --git a/src/boards/wiscore_rak3401/board.h b/src/boards/wiscore_rak3401/board.h index ae5c86bee..0224b920d 100644 --- a/src/boards/wiscore_rak3401/board.h +++ b/src/boards/wiscore_rak3401/board.h @@ -46,6 +46,32 @@ #define BUTTON_2 _PINNUM(0, 8) // change default 0.0 to 0.8, add by Michael. This pin is not connected on 4631. #define BUTTON_PULL NRF_GPIO_PIN_PULLUP +/*------------------------------------------------------------------*/ +/* DISPLAY - optional SSD1306 OLED on a WisBlock IO slot + * + * Pin- and rail-identical to the RAK4631; the panel is a plug-in module, so + * board_display_init() probes for it and a board with an empty slot behaves + * exactly as it did before. + *------------------------------------------------------------------*/ +#define DISPLAY_CONTROLLER_SSD1306 +#define DISPLAY_I2C_ADDR 0x3C + +// WB_I2C1, shared by the sensor and IO slots. +#define DISPLAY_PIN_SDA _PINNUM(0, 13) +#define DISPLAY_PIN_SCL _PINNUM(0, 14) + +// Switchable 3V3_S rail (WB_IO2), off out of reset. +#define DISPLAY_VSENSOR_PIN _PINNUM(0, 34) +#define DISPLAY_VSENSOR_ON 1 + +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 64 +// Untested here, but the RAK4631's WisBlock OLED measured as 132-column. +#define DISPLAY_COL_OFFSET 2 +#define DISPLAY_GDDRAM_WIDTH 132 + +#define DISPLAY_TITLE "RAK3401" + //--------------------------------------------------------------------+ // BLE OTA //--------------------------------------------------------------------+ diff --git a/src/boards/wiscore_rak4631_board/board.h b/src/boards/wiscore_rak4631_board/board.h index 7886cc334..880d27e84 100644 --- a/src/boards/wiscore_rak4631_board/board.h +++ b/src/boards/wiscore_rak4631_board/board.h @@ -46,6 +46,36 @@ #define BUTTON_2 _PINNUM(0, 8) // change default 0.0 to 0.8, add by Michael. This pin is not connected on 4631. #define BUTTON_PULL NRF_GPIO_PIN_PULLUP +/*------------------------------------------------------------------*/ +/* DISPLAY - optional SSD1306 OLED (RAK1921) on a WisBlock IO slot + * + * The panel is a plug-in module, so this is best-effort: board_display_init() + * probes the I2C address and reports absence rather than blocking, and a + * board with an empty slot behaves exactly as it did before. + *------------------------------------------------------------------*/ +#define DISPLAY_CONTROLLER_SSD1306 + +// WB_I2C1, shared by the sensor and IO slots. +#define DISPLAY_PIN_SDA _PINNUM(0, 13) +#define DISPLAY_PIN_SCL _PINNUM(0, 14) +#define DISPLAY_I2C_ADDR 0x3C + +// The slots are fed from the switchable 3V3_S rail (WB_IO2), which is off +// out of reset - the bootloader has to raise it before the panel answers. +#define DISPLAY_VSENSOR_PIN _PINNUM(0, 34) +#define DISPLAY_VSENSOR_ON 1 + +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 64 +// This panel has 132 columns of GDDRAM behind 128px of glass, so the visible +// window starts at column 2. Confirmed on hardware: with an offset of 0 the +// image sat 2px left of the glass and the two uncovered columns on the right +// showed power-up garbage. +#define DISPLAY_COL_OFFSET 2 +#define DISPLAY_GDDRAM_WIDTH 132 + +#define DISPLAY_TITLE "RAK4631" + //--------------------------------------------------------------------+ // BLE OTA //--------------------------------------------------------------------+ diff --git a/src/boards/xiao_nrf52840_ble/board.h b/src/boards/xiao_nrf52840_ble/board.h index 9f4c45e6e..4b9b9c94d 100644 --- a/src/boards/xiao_nrf52840_ble/board.h +++ b/src/boards/xiao_nrf52840_ble/board.h @@ -46,6 +46,30 @@ #define BUTTON_2 _PINNUM(0, 3) #define BUTTON_PULL NRF_GPIO_PIN_PULLUP +/*------------------------------------------------------------------*/ +/* DISPLAY - optional SSD1306 OLED (user-wired) + * + * The XIAO variant offers three I2C pinouts depending on build flags; these + * are the kit default (NFC pins). On a board wired to D4/D5 or D6/D7 the + * probe finds nothing and the screen stays blank, which is harmless. + *------------------------------------------------------------------*/ +#define DISPLAY_CONTROLLER_SSD1306 +#define DISPLAY_I2C_ADDR 0x3C + +#define DISPLAY_PIN_SDA _PINNUM(0, 30) +#define DISPLAY_PIN_SCL _PINNUM(0, 31) + +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 64 +// Not yet verified on hardware. Offset 0 is right for a true 128-column +// SSD1306; a 132-column SH1106 will sit 2px left of centre, in which case set +// DISPLAY_COL_OFFSET to 2. Blanking spans 132 either way so no uncovered +// column can show power-up garbage. +#define DISPLAY_COL_OFFSET 0 +#define DISPLAY_GDDRAM_WIDTH 132 + +#define DISPLAY_TITLE "XIAO" + //--------------------------------------------------------------------+ // BLE OTA //--------------------------------------------------------------------+ diff --git a/src/boards/xiao_nrf52840_ble_sense/board.h b/src/boards/xiao_nrf52840_ble_sense/board.h index e512c1b78..408bf2576 100644 --- a/src/boards/xiao_nrf52840_ble_sense/board.h +++ b/src/boards/xiao_nrf52840_ble_sense/board.h @@ -46,6 +46,30 @@ #define BUTTON_2 _PINNUM(0, 3) #define BUTTON_PULL NRF_GPIO_PIN_PULLUP +/*------------------------------------------------------------------*/ +/* DISPLAY - optional SSD1306 OLED (user-wired) + * + * The XIAO variant offers three I2C pinouts depending on build flags; these + * are the kit default (NFC pins). On a board wired to D4/D5 or D6/D7 the + * probe finds nothing and the screen stays blank, which is harmless. + *------------------------------------------------------------------*/ +#define DISPLAY_CONTROLLER_SSD1306 +#define DISPLAY_I2C_ADDR 0x3C + +#define DISPLAY_PIN_SDA _PINNUM(0, 30) +#define DISPLAY_PIN_SCL _PINNUM(0, 31) + +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 64 +// Not yet verified on hardware. Offset 0 is right for a true 128-column +// SSD1306; a 132-column SH1106 will sit 2px left of centre, in which case set +// DISPLAY_COL_OFFSET to 2. Blanking spans 132 either way so no uncovered +// column can show power-up garbage. +#define DISPLAY_COL_OFFSET 0 +#define DISPLAY_GDDRAM_WIDTH 132 + +#define DISPLAY_TITLE "XIAO Sense" + //--------------------------------------------------------------------+ // BLE OTA //--------------------------------------------------------------------+ diff --git a/src/images.c b/src/images.c index 2f2767fdf..a634f2b2c 100644 --- a/src/images.c +++ b/src/images.c @@ -1,6 +1,6 @@ #include "boards.h" -#if defined(DISPLAY_PIN_SCK) +#ifdef BOARD_HAS_DISPLAY #include diff --git a/src/main.c b/src/main.c index bd9971379..3c8f5a41e 100644 --- a/src/main.c +++ b/src/main.c @@ -293,9 +293,10 @@ static void check_dfu_mode(void) { // Enter DFU mode accordingly to input if (dfu_start || !valid_app) { if (_ota_dfu) { - #ifdef DISPLAY_PIN_SCK - board_display_init(); - screen_draw_ble(); + #ifdef BOARD_HAS_DISPLAY + if (board_display_init()) { + screen_draw_ble(); + } #endif led_state(STATE_BLE_DISCONNECTED); if (!_sd_inited) mbr_init_sd(); diff --git a/src/screen.c b/src/screen.c index a1f82e44f..fb637997b 100644 --- a/src/screen.c +++ b/src/screen.c @@ -24,7 +24,58 @@ #include "boards.h" -#if defined(DISPLAY_PIN_SCK) +#ifdef BOARD_HAS_DISPLAY + +// Defaults for 1bpp mono panels. The stock geometry below is sized for a +// 240x135 TFT and does not fit a typical 128x64 OLED, so give mono boards a +// four-line layout (title / mode / version / banner) out of the box. A board +// only has to declare its bus and title; anything here can still be +// overridden per board. +#ifdef DISPLAY_MONO +#if DISPLAY_HEIGHT < 96 +// Three 32x32 icons need 96x32; that plus four text lines does not fit. +#define DISPLAY_NO_DRAG_ICONS +#define NOLABELS +#endif + +#ifndef FONT_SIZE_LARGE +#define FONT_SIZE_LARGE 2 +#endif +#ifndef BANNER_TEXT +#define BANNER_TEXT "meshtastic.org" +#endif + +// The colour bars are dark in mono, so bar 1 simply spans the panel and the +// other two collapse; this also keeps drawBar inside the frame buffer. +#ifndef SCREEN_BAR1_Y +#define SCREEN_BAR1_Y 0 +#endif +#ifndef SCREEN_BAR1_H +#define SCREEN_BAR1_H DISPLAY_HEIGHT +#endif +#ifndef SCREEN_BAR2_H +#define SCREEN_BAR2_H 0 +#endif +#ifndef SCREEN_BAR3_H +#define SCREEN_BAR3_H 0 +#endif + +#ifndef DISPLAY_TITLE_Y +#define DISPLAY_TITLE_Y 0 +#endif +#ifndef DRAG_TEXT_Y +#define DRAG_TEXT_Y 22 +#endif +#ifndef BLE_OTA_Y +#define BLE_OTA_Y 22 +#endif +#ifndef UF2_VERSION_BASE_Y +#define UF2_VERSION_BASE_Y 44 +#endif +#ifndef BANNER_TEXT_Y +#define BANNER_TEXT_Y 55 +#endif +#endif // DISPLAY_MONO #ifndef BANNER_TEXT #define BANNER_TEXT "meshtastic.org - OTAFIX by oltaco" @@ -121,6 +172,13 @@ const uint16_t palette[] = { #ifndef BLE_OTA_Y #define BLE_OTA_Y 65 #endif +// Mode line on the drag screen, for panels too short for the 32px icon row. +#ifndef DRAG_TEXT_Y +#define DRAG_TEXT_Y BLE_OTA_Y +#endif +#ifndef DRAG_TEXT +#define DRAG_TEXT "USB UF2" +#endif #ifndef FONT_SIZE_LARGE #define FONT_SIZE_LARGE 4 #endif @@ -129,6 +187,11 @@ const uint16_t palette[] = { // TODO only buffer partial screen to save SRAM // ESP32s2 can only statically allocated DRAM up to 160KB. // the remaining 160KB can only be allocated at runtime as heap. +#ifdef DISPLAY_MONO +_Static_assert(DISPLAY_HEIGHT % 8 == 0, + "mono panels are addressed in 8-row pages; DISPLAY_HEIGHT must be a multiple of 8"); +#endif + static uint8_t frame_buf[DISPLAY_WIDTH * DISPLAY_HEIGHT]; //static uint8_t* frame_buf; @@ -155,12 +218,19 @@ static void printch(int x, int y, int color, const uint8_t* fnt, int size) { } } +#ifndef DISPLAY_NO_DRAG_ICONS // print icon static void printicon(int x, int y, int color, const uint8_t* icon) { int w = *icon++; int h = *icon++; int sz = *icon++; + // The decode loop below writes w*h bytes straight into frame_buf with no + // clipping of its own, so anything that would land outside is dropped. + if (x < 0 || y < 0 || x + w > DISPLAY_WIDTH || y + h > DISPLAY_HEIGHT) { + return; + } + uint8_t mask = 0x80; int runlen = 0; int runbit = 0; @@ -201,6 +271,7 @@ static void printicon(int x, int y, int color, const uint8_t* icon) { } } } +#endif // DISPLAY_NO_DRAG_ICONS static void print(int x, int y, int color, const char* text, int size) { // Glyphs are written straight into frame_buf with no bounds check of @@ -231,6 +302,39 @@ static inline void print_centered(int y, int color, const char* text, int size) // //--------------------------------------------------------------------+ +#ifdef DISPLAY_MONO + +// One bit per pixel, so the 16-entry palette collapses to "lights a pixel or +// does not". The layout above only ever draws foreground in white (text, +// icons) and purple (the version string); the three background bars use +// colours that stay dark. That is what lets the colour layout survive the +// conversion without being redrawn for mono. +#define MONO_LIT_MASK ((1u << COLOR_WHITE) | (1u << COLOR_PURPLE)) + +static void draw_screen(uint8_t const* fb) { + // SSD1306/SH1106 GDDRAM is paged: one byte spans 8 rows of a single + // column, LSB topmost. frame_buf is already column-major, so each output + // byte gathers 8 contiguous bytes of it. + for (int page = 0; page < DISPLAY_HEIGHT / 8; ++page) { + uint8_t line[DISPLAY_WIDTH]; + + for (int x = 0; x < DISPLAY_WIDTH; ++x) { + uint8_t const* col = fb + x * DISPLAY_HEIGHT + page * 8; + uint8_t bits = 0; + for (int bit = 0; bit < 8; ++bit) { + if (MONO_LIT_MASK & (1u << (col[bit] & 0xf))) { + bits |= (uint8_t) (1u << bit); + } + } + line[x] = bits; + } + + board_display_draw_page((uint8_t) page, line, sizeof(line)); + } +} + +#else + static void draw_screen(uint8_t const* fb) { uint8_t const* p = fb; for (int y = 0; y < DISPLAY_WIDTH; ++y) { @@ -246,8 +350,23 @@ static void draw_screen(uint8_t const* fb) { } } +#endif + // draw color bar static void drawBar(int y, int h, int color) { + // Clamp to the panel: the default bar geometry is sized for a 240x135 + // screen and would run off the end of a column on a shorter one. + if (y < 0) { + h += y; + y = 0; + } + if (y >= DISPLAY_HEIGHT || h <= 0) { + return; + } + if (y + h > DISPLAY_HEIGHT) { + h = DISPLAY_HEIGHT - y; + } + for (int x = 0; x < DISPLAY_WIDTH; ++x) { memset(frame_buf + x * DISPLAY_HEIGHT + y, color, h); } @@ -264,6 +383,12 @@ void screen_draw_drag(void) { print_centered(UF2_VERSION_BASE_Y, COLOR_PURPLE, UF2_VERSION_BASE, 1); print_centered(BANNER_TEXT_Y, COLOR_WHITE, BANNER_TEXT, 1); +#ifdef DISPLAY_NO_DRAG_ICONS + // The icon row is three 32x32 glyphs, 96px wide and 32px tall before any + // spacing. A 128x64 panel cannot carry that and the four text lines, so + // name the mode instead. + print_centered(DRAG_TEXT_Y, COLOR_WHITE, DRAG_TEXT, FONT_SIZE_LARGE); +#else #ifndef DRAG #define DRAG 70 #endif @@ -277,6 +402,7 @@ void screen_draw_drag(void) { print(22, DRAG - 12, COLOR_WHITE, "firmware.uf2", 1); print(160, DRAG - 12, COLOR_WHITE, UF2_VOLUME_LABEL, 1); #endif // NOLABELS +#endif // DISPLAY_NO_DRAG_ICONS draw_screen(frame_buf); } diff --git a/src/usb/usb.c b/src/usb/usb.c index 30d91c0d4..5fa008077 100644 --- a/src/usb/usb.c +++ b/src/usb/usb.c @@ -97,9 +97,10 @@ void usb_init(bool cdc_only) { uf2_init(); tusb_init(); - #ifdef DISPLAY_PIN_SCK - board_display_init(); - screen_draw_drag(); + #ifdef BOARD_HAS_DISPLAY + if (board_display_init()) { + screen_draw_drag(); + } #endif } From 918387d7773581e9f24966418aaaaf8cc8e57034 Mon Sep 17 00:00:00 2001 From: rcarteraz Date: Mon, 21 Sep 2026 09:36:13 -0700 Subject: [PATCH 2/5] docs: add rendered DFU screens for the mono OLED path The 128x64 layout is otherwise only visible by flashing a board, and five of the six boards this enables are untested. These two are produced by replaying screen.c's mono path -- font table parsed from images.c, and printch/print_centered/drawBar/draw_screen reproduced with the same integer arithmetic and bounds rejections -- so they are the bit pattern the SSD1306 driver is handed rather than a mock-up. RAK4631 is the representative board since it is the one validated on hardware. Every enabled panel is 128x64 and shares the layout, so the title line is the only thing that differs between them. --- docs/dfu_screen_oled_ble.png | Bin 0 -> 2881 bytes docs/dfu_screen_oled_uf2.png | Bin 0 -> 2888 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/dfu_screen_oled_ble.png create mode 100644 docs/dfu_screen_oled_uf2.png diff --git a/docs/dfu_screen_oled_ble.png b/docs/dfu_screen_oled_ble.png new file mode 100644 index 0000000000000000000000000000000000000000..11a8baef266243f20aa997c24659b210e1e237c3 GIT binary patch literal 2881 zcma)83s4hR6kTjV#3CRPK#(GK%16<5rV0p(G6bPj5K4}0by_SUqjfts3&2Xr@WYGmm@9#0h~9&SsVvc$n!0xN zq2|>~OYD7HK4#6wmMBaxwHa#lPH0R|Ep8kb>pa!Sb4Yb;72m0sU)q|ZFs+VMH1HH$ zF6wRg^*lv!rAX$Z@+R1=#0?FlXu364(cq*GLEGsT4N}yc(ocqqW)V_c1plc0*DSIZ z{}r?5Y=c*O!^x7{s!4TQrt#GrZL4=epMth*!vm?q$pcjbXKeY@MoLV+44tR77TKU@ z&OrW8Yy39n?On9)gQ4s;ldA#gU2(U1tini}B;Le66UKPbT}lCHmY0({-95CPQ-=c% z`)OzJm-HG91~L0&4r5pz{ygygVpMowD_m)$LBq@AXKJ2Lb;~R z={H9=+U5D4yKYtMBg=(Yg|u)vKJI+ z_klAqypUwKdtzh`I+sjceZNpK!a+JDQlSa(B27jzAtY9<&8Nm2tF*ut>wsNh2VntV z|8548lI)>IfOQLPkr*Jb;NVd&D0QH;qY!kp%_T^bw}0%XO6h7S(%`YO6QRj!&gisw zP2LWoEgPHy;|$w=W?EM^sp3iYOf@xLB9XV$ZXZNhlA+WU!I0U=g`q!X$Z!MsHzk9? zR(-v?>uP+)pi-j_Zh4s6;%P7LKRU0by|yk^R54N}?*1aP_>Mx{nm#!ezkUVpUOzLd zrBYO#z))`H_bm)$L97B?1{V=kV@6arIj;8+2@AbeRj`S7Wvo_4r-@+kFP{AI1x=MZn9%6@EWH@Y^Fg=?2DuA_!1U+n>Y9 z4t8oYpb^}5A>2nzNw9*FCGJKtNbW%uYQHG&iX(^c8rf3YSH819@SCBib>;Wf-WyhL z=#)fPemc@B?jc0+2hIx_HZs)=`AfcD460*@qFPcZ>{%r4Kg}r{t0-TTl~Q_sbF0O= zmY9D!v)w9f1dbimx`41tf-z#j`iwD+GBeLKR=wro(_YE;%c1VIQ{>$F{=3etr89u! zFdG1+Z;XigM;l?LbzI~jyuQ!_aQczl+IFn2bLsMjdm;o58b_>jpC&os^p4}lLnjj=FpcyL+J Hcf0=wuxGCJ literal 0 HcmV?d00001 diff --git a/docs/dfu_screen_oled_uf2.png b/docs/dfu_screen_oled_uf2.png new file mode 100644 index 0000000000000000000000000000000000000000..97047d40185f295da8e28e23e8df4bab84af27fd GIT binary patch literal 2888 zcmbVO4NwzD6h5GWMH#?CP$G^NMG8_>gftCQi2;mADwGxw82^T#qjGhyAyg5oAc07u z1*@Xy4DweCDuSXClc@wmJHTK^p+*A)lsZb`L_vx|clUCagu$P2*a>&L?|u8;_r15b znX3gsR+f&I7=~H#SMb6x%rqCnOu{Fdf*JdA4j03wSo3-Q;o^I}_x%LU{x*9;H}E7r z_Yz0eDGs&>nNg0#HKYIWJ>#acykD>vj=p^L$`o@P{{ACuwg%fHHp8@5a7bA2T{ort zd1&aA5?i*AFvq}9F)3oq(koQ zSHh*=mP&&Hit0F1YeMy&B#wIde2s*1pgmC(XgWh+w4r*IElx0S5G(ngfvcGCog9~f z5@Oj~WNFuGr}x{Jm?@H;izMHfQiCLlotSWX#qj;Pw5;^HfoAlak@`9@^>ehD zk?Whv8dkaoRpfg>Wzkhn_W)4NjB1K^nCjbw_C`|z8@gfWUIFE8LNyPAYNchMLon5G zOxE)xJ1;`u)x@ETik5yTK=3J6y35!vLc!jyETZOk5q8bbM40Tnu zcw;Cq)|Gh5HWyMtkCkM7shH9*IeI~r?FF2&0djytTw92+yz;NR|8#miE@x}A0h}-l z_U$ECfY4Y75xW3}^!BPOura-iS_8s(XvCny3zp;vrV;f8b~p)*WIARCTWG;bfYn%- z!;`aL6A8o*zbLYvm5OC!iN_<60}K$AzE2JSlT^cm$naszFb(n$fm}Fi5CgddC9Eg9*2ux@V(e&yIdI1UhTFdN4sr99i*(BYY^|d zmji(VNX+FsD5D+z`)UKiGXr4J!Wz|`|L;@I<4q^}aKHxm5UlZC)R#J|xs~ZHtMx0n z-(9p*vE#S&jLQCXLk|WN!Rh&THssVoI>}Vz%3;c3j z^`EAHqp0I=M0=Al>~*=vk`>h*?-_fhA>^dU(W(9GBzbm9YHm6nXg&5sTD=p=i(t7! z3-mZT@ZNKt8a-t6mh&WSS@2`chygWn+MiERJUf9 zv1WF)rylLe{TpbdPZB==30nx(`Gmuwp5q=-63c!XZOz$zV60#4FuY<{o6@&RY#!~q z!&`ZA$rilo@&!X@6?t|cICl!P7#(mrGc#-sC_=Ih!8xisN{y#Jpil*bSTV}#aB$lF zv}uNlX>V&!n1cjW;QGP@f9eWOybT`C13dsr&lV5e?ih2*>h!0Y+2rM=R1(9sf;Sf? zA$oe$3U~spz-iXkVqJSZD6W0^%h6j$cDGy)PLI9P72I(3=J}pd;l{!If$P_{#~&<9 z((_7xbP<>_-OwsfUXyYmsnaX@aoCiI)V3QZYkk7+`o*W8e{`55Drnf6_F(^u&-K-3 zNZt6W7qkNF8L6Y9PW9e0ado6<;H#@Kg`cdqP#5GmExqHZ$01FjBGetn=oSYJ?IP2i zlrfbXr>{AYC|%^fIL)aYzt|=;jDS)UYDq<7w9djNjc%Sqn4`l6rVE~IaSSk Date: Mon, 21 Sep 2026 09:53:17 -0700 Subject: [PATCH 3/5] docs: re-render the DFU screens at the current release version The first pair were stamped 0.9.2-OTAFIX2.3-BP1.6-4-g43df87d, which is two releases stale now that master carries OTAFIX 2.5. Re-rendered at the 0.9.2-OTAFIX2.5 tag rather than at this branch's describe output. A release build stamps the bare tag, 15 characters, which centres with room to spare; an untagged build stamps 26 characters and print() drops everything past the 21st, so the branch's own 0.9.2-OTAFIX2.5-4-g2b1c66a would render as 0.9.2-OTAFIX2.5-4-g2b and lose the rest of the hash. The tagged form is what ships, so that is what these show. --- docs/dfu_screen_oled_ble.png | Bin 2881 -> 2831 bytes docs/dfu_screen_oled_uf2.png | Bin 2888 -> 2837 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/dfu_screen_oled_ble.png b/docs/dfu_screen_oled_ble.png index 11a8baef266243f20aa997c24659b210e1e237c3..89f82e4cbc72a652dd13ff14c4ce5a185f43318f 100644 GIT binary patch literal 2831 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 Hcf0=wuxGCJ diff --git a/docs/dfu_screen_oled_uf2.png b/docs/dfu_screen_oled_uf2.png index 97047d40185f295da8e28e23e8df4bab84af27fd..b9a1d80988ddfaed3ce45ff5132f4d492cee80bb 100644 GIT binary patch literal 2837 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 Date: Mon, 21 Sep 2026 15:07:10 -0500 Subject: [PATCH 4/5] refactor(rak): spell DISPLAY_VSENSOR_PIN as the P1.02 pin it is _PINNUM(port, pin) is port*32 + pin, so _PINNUM(0, 34) and _PINNUM(1, 2) are both 34 and both WB_IO2. Port 0 has no pin 34; the second form reads correctly beside the other pin defines. No change to any binary. --- src/boards/wiscore_rak3401/board.h | 2 +- src/boards/wiscore_rak4631_board/board.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/boards/wiscore_rak3401/board.h b/src/boards/wiscore_rak3401/board.h index 0224b920d..a96748415 100644 --- a/src/boards/wiscore_rak3401/board.h +++ b/src/boards/wiscore_rak3401/board.h @@ -61,7 +61,7 @@ #define DISPLAY_PIN_SCL _PINNUM(0, 14) // Switchable 3V3_S rail (WB_IO2), off out of reset. -#define DISPLAY_VSENSOR_PIN _PINNUM(0, 34) +#define DISPLAY_VSENSOR_PIN _PINNUM(1, 2) #define DISPLAY_VSENSOR_ON 1 #define DISPLAY_WIDTH 128 diff --git a/src/boards/wiscore_rak4631_board/board.h b/src/boards/wiscore_rak4631_board/board.h index 880d27e84..c5fe614c6 100644 --- a/src/boards/wiscore_rak4631_board/board.h +++ b/src/boards/wiscore_rak4631_board/board.h @@ -62,7 +62,7 @@ // The slots are fed from the switchable 3V3_S rail (WB_IO2), which is off // out of reset - the bootloader has to raise it before the panel answers. -#define DISPLAY_VSENSOR_PIN _PINNUM(0, 34) +#define DISPLAY_VSENSOR_PIN _PINNUM(1, 2) #define DISPLAY_VSENSOR_ON 1 #define DISPLAY_WIDTH 128 From 1772ef9c4968d7809e3b42c82483c05b74aab614 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:07:10 -0500 Subject: [PATCH 5/5] docs: changelog and AGENTS.md for the mono OLED DFU screens AGENTS.md still said DISPLAY_PIN_SCK gates the display code and named the three Heltec boards as the only ones with it. --- AGENTS.md | 15 ++++++++++----- changelog.md | 2 ++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 73ec5e451..ed69da8f0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,8 +60,9 @@ No lint, no test suite — this is a bootloader; the only correctness signal is ### Board abstraction: `src/boards//` Each board directory can carry: -- `board.h` — pin/peripheral defines (`DISPLAY_PIN_SCK` gates the OLED code - in `src/screen.c` and `src/images.c`; `BLEDIS_MANUFACTURER`/`BLEDIS_MODEL` +- `board.h` — pin/peripheral defines (`DISPLAY_PIN_SCK` or `DISPLAY_PIN_SDA` + gates the display code in `src/screen.c` and `src/images.c`, through the + `BOARD_HAS_DISPLAY` those two set; `BLEDIS_MANUFACTURER`/`BLEDIS_MODEL` are the real vendor name, e.g. RAKWireless — don't rebrand these). Defining `BANNER_TEXT` overrides the on-screen DFU banner default in `src/screen.c`. @@ -82,9 +83,13 @@ keep in sync in either build system (unlike the CI matrix, see below). `main.c` is the entry point (MBR/SoftDevice handoff, DFU state machine dispatch). `dfu_magic.h` holds the `NRF_POWER->GPREGRET` magics an -application (or the bootloader itself) writes to pick the next boot mode. `screen.c`/`images.c` render the OLED UF2/BLE-OTA screens, active -only when the board defines `DISPLAY_PIN_SCK` — today that's `heltec_t096`, -`heltec_t1`, and `heltec_t114`. `dfu_ble_svc.c`/`dfu_init.c` are the BLE DFU +application (or the bootloader itself) writes to pick the next boot mode. `screen.c`/`images.c` render the DFU UF2/BLE-OTA screens, active +only when the board names a display bus: `DISPLAY_PIN_SCK` for an SPI TFT +(`heltec_t096`, `heltec_t1`, `heltec_t114`) or `DISPLAY_PIN_SDA` for an I2C +mono OLED (`wiscore_rak4631_board`, `wiscore_rak3401`, `wio_tracker_l1`, +`promicro_nrf52840`, `xiao_nrf52840_ble`, `xiao_nrf52840_ble_sense`). The two +are mutually exclusive, since TWIM0 and SPIM0 are one peripheral, and +`boards.h` `#error`s on a board that names both. `dfu_ble_svc.c`/`dfu_init.c` are the BLE DFU service; `flash_nrf5x.c` wraps flash writes. `usb/` is the USB MSC (UF2 drive) + CDC stack on top of the vendored `lib/tinyusb`. A UF2 block with family `CFG_UF2_MESHTASTIC_ERASE_ID` (`usb/uf2/uf2cfg.h`) is a factory-erase diff --git a/changelog.md b/changelog.md index d4aa4b319..9c897911a 100644 --- a/changelog.md +++ b/changelog.md @@ -11,6 +11,8 @@ it and is upstream Adafruit history, kept for provenance. - `t1000_e` opts into `UF2_EXIT_ON_EJECT`. The T1000-E exposes one button and wires P0.18 to RESET, so there is no way to reset out of UF2 mode once in it. Measured on a T1000-E running OTAFIX 2.4: a host eject is acknowledged but the drive re-mounts before a 2 s sample can catch it gone and the board never leaves the bootloader, and unplugging leaves it spinning in `wait_for_events()` on battery (LED on the 300 ms `STATE_USB_UNMOUNTED` cycle) until it is plugged back in. Ejecting the drive now leaves DFU and boots the application, matching `mesh_tracker_x1`. - `t1000_e`'s button now works in the bootloader. `BUTTON_PULL` was `NRF_GPIO_PIN_PULLUP`, which makes `button_pressed()` compare against an `active_state` of 0, but P0.06 is active high with an internal pull-down (firmware's `variants/nrf52840/tracker-t1000-e/variant.h` sets `BUTTON_ACTIVE_LOW false` and `BUTTON_SENSE_TYPE 0x5`). The pin can only ever read 1, so the comparison never matched and the board had no button path into DFU at all - confirmed on a T1000-E by watching the application register presses (`LONG PRESS RELEASE AFTER 1540 MILLIS`) on the same pin the bootloader could not see. The board now uses `BUTTON_DFU_HOLD` like `mesh_tracker_x1`: a 3 s hold through boot enters UF2 DFU and a short press stays with the application. That also compiles out the `BUTTON_FRESET` reads, which matters because P0.18 sits high and would otherwise read as permanently pressed; `BUTTON_2` is aliased to `BUTTON_1` rather than pointing at RESET. - Build tooling, no bootloader behaviour change. `tools/build_all.py` called a bare `size`, which on macOS resolves to Apple's and cannot read an ARM ELF, so the script died on the first board that built; it now uses the same `CROSS_COMPILE` prefix the Makefile does. The object rules also gained `$(BUILD)` as an order-only prerequisite - the directory was only a prerequisite of the link rule, so under `make -j` a compile could start before `mkdir` and `-fstack-usage` failed writing its `.su`. Between them `tools/build_all.py`, which CONTRIBUTING tells you to run before opening a PR, goes from unusable on macOS to 19/19. +- DFU screens on I2C mono OLEDs. A board with an SSD1306 or SH1106 panel showed nothing while sitting in DFU mode; `boards.c` gains a paged 1bpp path alongside the existing SPI TFT one, and `screen.c` a four-line layout (title, mode, version, banner) for panels too short to carry the three 32px drag icons. A board opts in by naming `DISPLAY_PIN_SDA` in its `board.h`. That and `DISPLAY_PIN_SCK` are mutually exclusive because TWIM0 and SPIM0 are one peripheral, and `boards.h` now `#error`s on a board that names both. The panel is often a plug-in module (the RAK1921 in a WisBlock IO slot, say), so `board_display_init()` probes the I2C address and returns false when nothing answers, both call sites skip the draw, and every transfer is bounded - a missing or unterminated bus cannot stall the bootloader, and a board with an empty slot behaves exactly as it did before. On for `wiscore_rak4631_board`, `wiscore_rak3401`, `wio_tracker_l1`, `promicro_nrf52840`, `xiao_nrf52840_ble` and `xiao_nrf52840_ble_sense`, at 1,776-1,872 bytes of flash each with the CI-pinned ARM GCC 12.3.Rel1; only the RAK4631 is validated on hardware, the other five carry pin definitions taken from their firmware variants and have never been flashed. The three TFT boards pay +32 bytes for the shared `screen.c` restructuring and the eight with no display are unchanged to the byte. `DISPLAY_COL_OFFSET` is panel-specific and cannot be probed: the RAK4631's WisBlock OLED measured as 132-column and needs 2, the untested boards default to 0 with blanking spanning 132 columns, so a wrong guess shows as a 2px shift rather than power-up garbage. On RAK4631 and RAK3401, entering DFU now also raises WB_IO2 to feed the slots, which on the RAK4631 is `PIN_GPS_EN` as well. +- Fixed an out-of-bounds write in `screen.c`'s `printicon()`, of the same shape as the `print()` fix in 2.3 and likewise predating the work above. On the 160x80 `heltec_t096` and `heltec_t1`, `DRAGX 4` plus the default `pendriveLogo_X 129` put the third drag icon at x=133..164 and wrote 386 bytes past the end of `frame_buf`. Those two boards now set `pendriveLogo_X 124`, which sits the icon flush with the right edge, and `printicon()` drops any blit that would land outside the panel. ## OTAFIX 2.4