diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index 8fb846fc7..ce0f76d59 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -996,6 +996,7 @@ jobs: python3 -B test/test_legacy_ui_version_bounds.py -v python3 -B test/test_infrastructure_ui_version.py -v python3 -B test/test_ssd1306_power.py -v + python3 -B test/test_oled_display_probe.py -v python3 -B test/test_companion_inbox.py python3 -B test/test_heltec_r8_external_power.py python3 -B test/test_bluetooth_mac_contract.py diff --git a/docs/cli_commands.md b/docs/cli_commands.md index d38a72f7d..e4b55a5fc 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1059,6 +1059,19 @@ set display.rotation 270 The values are clockwise degrees. `0` clears the override and restores the board's compiled default. Unsupported display drivers return an error. +RAK4631 and RAK3401 images detect SSD1306 or SH1106 OLED controllers when +initializing the screen. This lets the same RAK4631 image serve WisMesh Pocket +v2 and v3. Both retain rotation support and use one 1 KiB framebuffer. + +Detection checks the configured `DISPLAY_ADDRESS` (default `0x3C`). Only +repeated SH1106 status readings select that controller; ambiguous responses +keep SSD1306, preserving compatibility with older RAK1921 panels. For panels +that cannot identify themselves, build with `OLED_CONTROLLER_OVERRIDE=1` +(SSD1306) or `=2` (SH1106); `=0` restores automatic detection. + +See [the Pocket v3 issue](https://github.com/meshcore-dev/MeshCore/issues/3296) +and [the RAK1921 detection caveat](https://github.com/meshtastic/firmware/pull/11265). + --- ## Show touchscreen touch areas diff --git a/src/helpers/ui/AutoOLEDBackend.h b/src/helpers/ui/AutoOLEDBackend.h new file mode 100644 index 000000000..cff55bd20 --- /dev/null +++ b/src/helpers/ui/AutoOLEDBackend.h @@ -0,0 +1,102 @@ +#pragma once + +#include +#include +#include "OLEDDisplayProbe.h" + +// Keep Adafruit's drawing, rotation and single 1 KiB framebuffer. SH1106 uses +// page writes into 132-column RAM, with the visible 128 columns starting at 2. +class AutoOLEDBackend : public Adafruit_SSD1306 { + mesh::ui::OLEDController _controller = mesh::ui::OLEDController::Absent; + bool _ready = false; + + bool writeCommands(const uint8_t* commands, uint8_t count) { + wire->beginTransmission(i2caddr); + const bool control = wire->write(uint8_t(0x00)) == 1; + const bool payload = wire->write(commands, count) == count; + return wire->endTransmission() == 0 && control && payload; + } + +public: + using Adafruit_SSD1306::Adafruit_SSD1306; + + bool begin(uint8_t vcc, uint8_t address, bool reset, bool periphBegin) { + _ready = false; + i2caddr = address; + if (periphBegin) wire->begin(); + if (reset && rstPin >= 0) { + pinMode(rstPin, OUTPUT); + digitalWrite(rstPin, HIGH); + delay(1); + digitalWrite(rstPin, LOW); + delay(10); + digitalWrite(rstPin, HIGH); + } + +#if defined(OLED_CONTROLLER_OVERRIDE) && OLED_CONTROLLER_OVERRIDE != 0 + static_assert(OLED_CONTROLLER_OVERRIDE == 1 || OLED_CONTROLLER_OVERRIDE == 2, + "OLED_CONTROLLER_OVERRIDE must be 0 (auto), 1 (SSD1306) or 2 (SH1106)"); + wire->beginTransmission(address); + _controller = wire->endTransmission() != 0 ? mesh::ui::OLEDController::Absent + : OLED_CONTROLLER_OVERRIDE == 2 ? mesh::ui::OLEDController::SH1106 + : mesh::ui::OLEDController::SSD1306; +#else + _controller = mesh::ui::detectOLEDController(*wire, address); +#endif + if (_controller == mesh::ui::OLEDController::Absent) return false; + if (_controller == mesh::ui::OLEDController::SSD1306) { + _ready = Adafruit_SSD1306::begin(vcc, address, false, false); + return _ready; + } + + if (!buffer && !(buffer = static_cast(malloc(WIDTH * ((HEIGHT + 7) / 8))))) + return false; + vccstate = vcc; + clearDisplay(); + // SH1106G initialization, matching Adafruit_SH1106G::begin(). + static const uint8_t init[] = { + 0xAE, 0xD5, 0x80, 0xA8, 0x3F, 0xD3, 0x00, 0x40, + 0xAD, 0x8B, 0xA1, 0xC8, 0xDA, 0x12, 0x81, 0xFF, + 0xD9, 0x1F, 0xDB, 0x40, 0x33, 0xA6, 0x20, 0x10, 0xA4 + }; + wire->setClock(wireClk); + const bool initialized = writeCommands(init, sizeof(init)); + wire->setClock(restoreClk); + if (!initialized) return false; + delay(100); + const uint8_t on = 0xAF; + wire->setClock(wireClk); + _ready = writeCommands(&on, 1); + wire->setClock(restoreClk); + return _ready; + } + + mesh::ui::OLEDController controller() const { return _controller; } + + void ssd1306_command(uint8_t command) { + if (_ready) Adafruit_SSD1306::ssd1306_command(command); + } + + void display() { + if (!_ready) return; + if (_controller == mesh::ui::OLEDController::SSD1306) { + Adafruit_SSD1306::display(); + return; + } + wire->setClock(wireClk); + bool ok = true; + for (uint8_t page = 0; ok && page < (HEIGHT + 7) / 8; ++page) { + const uint8_t commands[] = {uint8_t(0xB0 | page), 0x02, 0x10}; + ok = writeCommands(commands, sizeof(commands)); + for (uint16_t column = 0; ok && column < WIDTH;) { + const uint8_t count = WIDTH - column > 31 ? 31 : WIDTH - column; + wire->beginTransmission(i2caddr); + const bool control = wire->write(uint8_t(0x40)) == 1; + const bool payload = wire->write(buffer + page * WIDTH + column, count) == count; + ok = wire->endTransmission() == 0 && control && payload; + column += count; + } + } + wire->setClock(restoreClk); + } +}; diff --git a/src/helpers/ui/OLEDDisplayProbe.h b/src/helpers/ui/OLEDDisplayProbe.h new file mode 100644 index 000000000..fc46ea840 --- /dev/null +++ b/src/helpers/ui/OLEDDisplayProbe.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +namespace mesh { namespace ui { + +enum class OLEDController : uint8_t { Absent, SSD1306, SH1106 }; + +// Status-byte detection is used by ss_oled and Meshtastic. Only 0x08 is +// evidence of SH1106: some RAK1921 SSD1306 panels return the ambiguous 0x00. +// See https://github.com/meshtastic/firmware/pull/11265. +template +OLEDController detectOLEDController(WireBus& wire, uint8_t address) { + wire.beginTransmission(address); + if (wire.endTransmission() != 0) return OLEDController::Absent; + + int previous = -1; + for (unsigned attempt = 0; attempt < 4; ++attempt) { + wire.beginTransmission(address); + const bool written = wire.write(uint8_t(0x00)) == 1; + if (wire.endTransmission() != 0 || !written) break; + if (wire.requestFrom(address, uint8_t(1)) != 1 || wire.available() != 1) break; + const int status = wire.read(); + if (status < 0) break; + const int nibble = status & 0x0f; + if (nibble == previous) + return nibble == 0x08 ? OLEDController::SH1106 : OLEDController::SSD1306; + previous = nibble; + } + return OLEDController::SSD1306; +} + +}} // namespace mesh::ui diff --git a/src/helpers/ui/SSD1306Display.h b/src/helpers/ui/SSD1306Display.h index 0c387b9b5..5df001706 100644 --- a/src/helpers/ui/SSD1306Display.h +++ b/src/helpers/ui/SSD1306Display.h @@ -6,6 +6,9 @@ #define SSD1306_NO_SPLASH #include #include +#if defined(OLED_AUTO_DETECT) && OLED_AUTO_DETECT +#include "AutoOLEDBackend.h" +#endif #ifndef PIN_OLED_RESET #define PIN_OLED_RESET 21 // Reset pin # (or -1 if sharing Arduino reset pin) @@ -21,7 +24,11 @@ public: static constexpr int16_t PANEL_HEIGHT = 64; static constexpr size_t FRAMEBUFFER_BYTES = PANEL_WIDTH * ((PANEL_HEIGHT + 7) / 8); private: +#if defined(OLED_AUTO_DETECT) && OLED_AUTO_DETECT + AutoOLEDBackend display; +#else Adafruit_SSD1306 display; +#endif bool _isOn; bool _panel_ready = false; bool _flipped = false; diff --git a/test/test_oled_display_probe.py b/test/test_oled_display_probe.py new file mode 100644 index 000000000..0bcfaa320 --- /dev/null +++ b/test/test_oled_display_probe.py @@ -0,0 +1,571 @@ +#!/usr/bin/env python3 +"""Run the real SSD1306/SH1106 status probe against scripted I2C replies.""" + +from pathlib import Path +import os +import shutil +import subprocess +import sys +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[1] + +HARNESS = r''' +#include +#include +#include +#include +#include +#include +#include + +struct Sample { + uint8_t status; + uint8_t count = 1; + bool available = true; + int read = -2; // -2 supplies the status byte, -1 simulates a failed read. +}; + +struct ScriptedWire { + uint8_t expected_address = 0x3C; + uint8_t probe_error = 0; + std::vector samples; + std::vector status_errors; + std::vector> writes; + unsigned requests = 0, reads = 0, ends = 0, write_calls = 0; + unsigned failed_write = std::numeric_limits::max(); + Sample current{}; + + void beginTransmission(uint8_t address) { + assert(address == expected_address); + writes.push_back({}); + } + size_t write(uint8_t byte) { + assert(!writes.empty()); + writes.back().push_back(byte); + return write_calls++ == failed_write ? 0 : 1; + } + uint8_t endTransmission(bool = true) { + const unsigned transaction = ends++; + if (transaction == 0) return probe_error; + const unsigned sample = transaction - 1; + return sample < status_errors.size() ? status_errors[sample] : 0; + } + uint8_t requestFrom(uint8_t address, uint8_t count) { + assert(address == expected_address); + assert(count == 1); + assert(requests < 4); + current = requests < samples.size() ? samples[requests] : Sample{0}; + ++requests; + return current.count; + } + int available() { return current.available ? current.count : 0; } + int read() { + ++reads; + return current.read == -2 ? current.status : current.read; + } + + void checkBus() const { + assert(ends <= 5); + assert(requests <= 4); + assert(reads <= requests); + assert(!writes.empty()); + assert(writes.front().empty()); // Address probe does not alter the panel. + for (size_t i = 1; i < writes.size(); ++i) { + assert(writes[i] == std::vector{0x00}); + } + } +}; + +using mesh::ui::OLEDController; + +static OLEDController detect(ScriptedWire& wire) { + const auto controller = mesh::ui::detectOLEDController(wire, wire.expected_address); + wire.checkBus(); + return controller; +} + +static ScriptedWire status(std::initializer_list values) { + ScriptedWire wire; + for (uint8_t value : values) wire.samples.push_back({value}); + return wire; +} + +int main() { + for (uint8_t address : {0x3C, 0x3D}) { + for (uint8_t error : {1, 2, 3, 4, 5}) { + ScriptedWire wire; + wire.expected_address = address; + wire.probe_error = error; + assert(detect(wire) == OLEDController::Absent); + assert(wire.requests == 0 && wire.reads == 0 && wire.ends == 1); + } + auto wire = status({0x08, 0x08}); + wire.expected_address = address; + assert(detect(wire) == OLEDController::SH1106); + assert(wire.requests == 2); + } + + // Only the status low nibble identifies the controller. Busy bit 0x80 + // alone is also seen on SSD1306 hardware and must not select SH1106. + for (unsigned value = 0; value <= 255; ++value) { + auto wire = status({uint8_t(value), uint8_t(value), + uint8_t(value), uint8_t(value)}); + const auto expected = (value & 0x0F) == 0x08 + ? OLEDController::SH1106 : OLEDController::SSD1306; + assert(detect(wire) == expected); + assert(wire.requests == 2); + } + for (auto values : { + std::initializer_list{8, 0, 8, 0}, + {0, 8, 0, 8}, {8, 0x80, 8, 0x80}, {0xFF, 8, 0xFF, 8}}) { + auto wire = status(values); + assert(detect(wire) == OLEDController::SSD1306); + assert(wire.requests == 4); + } + for (auto values : { + std::initializer_list{0, 8, 0x88}, + {0, 1, 8, 8}, {8, 0, 8, 8}}) { + auto wire = status(values); + assert(detect(wire) == OLEDController::SH1106); + assert(wire.requests >= 3 && wire.requests <= 4); + } + // Two ordinary stable statuses identify SSD1306 without wasting all four + // reads, and must not be overturned by later unrelated reply bytes. + auto stable_ssd = status({0x00, 0x80, 8, 8}); + assert(detect(stable_ssd) == OLEDController::SSD1306); + assert(stable_ssd.requests == 2); + + // Incomplete and failed samples may never complete a matching pair. + for (unsigned failure = 0; failure < 4; ++failure) { + auto wire = status({8, 8, 0, 0}); + if (failure == 0) wire.samples[1].count = 0; + if (failure == 1) wire.samples[1].available = false; + if (failure == 2) wire.samples[1].read = -1; + if (failure == 3) wire.samples[1].count = 2; + assert(detect(wire) == OLEDController::SSD1306); + assert(wire.requests == 2); + } + for (unsigned failed_sample = 0; failed_sample < 4; ++failed_sample) { + auto wire = status({0, 1, 2, 3}); + wire.status_errors.resize(4, 0); + wire.status_errors[failed_sample] = 2; + assert(detect(wire) == OLEDController::SSD1306); + assert(wire.requests == failed_sample); + assert(wire.ends == failed_sample + 2); + } + for (unsigned failed_sample = 0; failed_sample < 4; ++failed_sample) { + auto wire = status({0, 1, 2, 3}); + wire.failed_write = failed_sample; + assert(detect(wire) == OLEDController::SSD1306); + assert(wire.requests == failed_sample); + } + + // No detection state carries between boots, addresses, or missing panels. + auto sh = status({8, 8}); + auto ssd = status({0x80, 0x80, 0x80, 0x80}); + ScriptedWire missing; + missing.probe_error = 2; + assert(detect(sh) == OLEDController::SH1106); + assert(detect(ssd) == OLEDController::SSD1306); + assert(detect(missing) == OLEDController::Absent); + std::puts("PASS: OLED controller status detection, errors and sample bounds"); +} +''' + +ARDUINO = r''' +#pragma once +#include +#include +#include +#define OUTPUT 1 +#define HIGH 1 +#define LOW 0 +inline std::vector> pin_modes, pin_levels; +inline std::vector delays; +inline void pinMode(int pin, int mode) { pin_modes.push_back({pin, mode}); } +inline void digitalWrite(int pin, int level) { pin_levels.push_back({pin, level}); } +inline void delay(unsigned ms) { delays.push_back(ms); } +''' + +WIRE = r''' +#pragma once +#include +#include +#include +#include +#include +#include +struct TwoWire { + struct Transaction { uint8_t address; std::vector data; }; + std::vector transactions; + std::vector clocks; + std::map present{{0x3C, true}, {0x3D, false}}; + std::map> status{{0x3C, {8, 8, 8, 8}}}; + std::map requests; + unsigned begun = 0, byte_writes = 0, block_writes = 0; + unsigned failed_tx = std::numeric_limits::max(); + unsigned failed_byte = std::numeric_limits::max(); + unsigned failed_block = std::numeric_limits::max(); + int current_read = 0; + void begin() { ++begun; } + void setClock(uint32_t value) { clocks.push_back(value); } + void beginTransmission(uint8_t address) { transactions.push_back({address, {}}); } + size_t write(uint8_t byte) { + assert(!transactions.empty()); + transactions.back().data.push_back(byte); + return byte_writes++ == failed_byte ? 0 : 1; + } + size_t write(const uint8_t* bytes, size_t count) { + assert(!transactions.empty()); + transactions.back().data.insert(transactions.back().data.end(), bytes, bytes + count); + assert(transactions.back().data.size() <= 32); + return block_writes++ == failed_block ? count - 1 : count; + } + uint8_t endTransmission(bool = true) { + const auto& tx = transactions.back(); + return !present[tx.address] || transactions.size() - 1 == failed_tx ? 2 : 0; + } + uint8_t requestFrom(uint8_t address, uint8_t count) { + assert(count == 1); + const unsigned index = requests[address]++; + current_read = index < status[address].size() ? status[address][index] : 0; + return present[address] ? 1 : 0; + } + int available() { return 1; } + int read() { return current_read; } +}; +inline TwoWire Wire; +''' + +VENDOR = r''' +#pragma once +#include +#include +#include +#include +#define SSD1306_SWITCHCAPVCC 2 +void* test_malloc(size_t count); +void test_free(void* pointer); +class Adafruit_SSD1306 { +protected: + const uint16_t WIDTH, HEIGHT; + TwoWire* wire; + uint8_t* buffer = nullptr; + int8_t rstPin, i2caddr = 0, vccstate = 0; + uint32_t wireClk, restoreClk; +public: + unsigned base_begin_calls = 0, base_display_calls = 0; + bool fail_begin = false, last_reset = true, last_periph_begin = true; + Adafruit_SSD1306(uint8_t width, uint8_t height, TwoWire* bus = &Wire, + int8_t reset = -1, uint32_t during = 400000, uint32_t after = 100000) + : WIDTH(width), HEIGHT(height), wire(bus), rstPin(reset), + wireClk(during), restoreClk(after) {} + ~Adafruit_SSD1306() { test_free(buffer); } + bool begin(uint8_t vcc, uint8_t address, bool reset, bool periph_begin) { + ++base_begin_calls; + last_reset = reset; + last_periph_begin = periph_begin; + if (fail_begin) return false; + if (!buffer) buffer = static_cast(test_malloc(WIDTH * ((HEIGHT + 7) / 8))); + if (!buffer) return false; + i2caddr = address; + vccstate = vcc; + clearDisplay(); + return true; + } + void clearDisplay() { std::memset(buffer, 0, WIDTH * ((HEIGHT + 7) / 8)); } + uint8_t* getBuffer() { return buffer; } + void display() { ++base_display_calls; } + void ssd1306_command(uint8_t command) { + wire->beginTransmission(uint8_t(i2caddr)); + wire->write(uint8_t(0)); + wire->write(command); + wire->endTransmission(); + } +}; +''' + +BACKEND_HARNESS = r''' +#include +#include +#include +#include +#include +#include +static unsigned active_allocations = 0, total_allocations = 0; +static bool allocation_fails = false; +void* test_malloc(size_t count) { + assert(count == 1024); + if (allocation_fails) return nullptr; + void* result = std::malloc(count); + assert(result); + ++active_allocations; + ++total_allocations; + return result; +} +void test_free(void* pointer) { + if (!pointer) return; + assert(active_allocations == 1); + --active_allocations; + std::free(pointer); +} +#define malloc test_malloc +#include +#undef malloc +using mesh::ui::OLEDController; + +static void resetPins() { pin_modes.clear(); pin_levels.clear(); delays.clear(); } +static unsigned totalRequests(const TwoWire& wire) { + unsigned count = 0; + for (const auto& entry : wire.requests) count += entry.second; + return count; +} +[[maybe_unused]] static void beginSH(AutoOLEDBackend& display, TwoWire& wire) { + assert(display.begin(SSD1306_SWITCHCAPVCC, 0x3C, true, false)); + assert(display.controller() == OLEDController::SH1106); + assert(display.base_begin_calls == 0); + assert(display.getBuffer() != nullptr); + assert(active_allocations == 1); + assert(wire.begun == 0); + assert(wire.clocks.back() == 100000); +} +[[maybe_unused]] static void verifyFrame(AutoOLEDBackend& display, TwoWire& wire) { + uint8_t* buffer = display.getBuffer(); + for (unsigned i = 0; i < 1024; ++i) buffer[i] = uint8_t((i * 37 + i / 128) & 255); + const size_t first = wire.transactions.size(); + display.display(); + assert(display.base_display_calls == 0); + size_t index = first; + for (unsigned page = 0; page < 8; ++page) { + const auto& command = wire.transactions.at(index++); + assert(command.address == 0x3C); + assert(command.data == (std::vector{0x00, uint8_t(0xB0 | page), 0x02, 0x10})); + unsigned column = 0; + while (column < 128) { + const auto& data = wire.transactions.at(index++); + assert(data.address == 0x3C); + const unsigned count = std::min(31u, 128u - column); + assert(data.data.size() == count + 1); + assert(data.data[0] == 0x40); + for (unsigned i = 0; i < count; ++i) + assert(data.data[i + 1] == buffer[page * 128 + column + i]); + column += count; + } + } + assert(index == wire.transactions.size()); + assert(wire.clocks.back() == 100000); +} + +int main() { + resetPins(); +#if defined(OLED_CONTROLLER_OVERRIDE) && OLED_CONTROLLER_OVERRIDE == 1 + { + TwoWire wire; + AutoOLEDBackend display(128, 64, &wire, -1); + assert(display.begin(2, 0x3C, true, true)); + assert(display.controller() == OLEDController::SSD1306); + assert(display.base_begin_calls == 1); + assert(!display.last_reset && !display.last_periph_begin); + assert(wire.begun == 1 && totalRequests(wire) == 0); + display.display(); + assert(display.base_display_calls == 1); + } +#else + { + TwoWire wire; +#if defined(OLED_CONTROLLER_OVERRIDE) && OLED_CONTROLLER_OVERRIDE == 2 + wire.status[0x3C] = {0, 0}; +#endif + AutoOLEDBackend display(128, 64, &wire, 9); + beginSH(display, wire); + assert(pin_modes == (std::vector>{{9, OUTPUT}})); + assert(pin_levels == (std::vector>{{9, HIGH}, {9, LOW}, {9, HIGH}})); + assert(delays == (std::vector{1, 10, 100})); + assert(totalRequests(wire) == +#if defined(OLED_CONTROLLER_OVERRIDE) && OLED_CONTROLLER_OVERRIDE == 2 + 0 +#else + 2 +#endif + ); + const std::vector init = { + 0x00, 0xAE, 0xD5, 0x80, 0xA8, 0x3F, 0xD3, 0x00, 0x40, + 0xAD, 0x8B, 0xA1, 0xC8, 0xDA, 0x12, 0x81, 0xFF, + 0xD9, 0x1F, 0xDB, 0x40, 0x33, 0xA6, 0x20, 0x10, 0xA4 + }; + assert(wire.transactions[wire.transactions.size() - 2].data == init); + assert(wire.transactions.back().data == (std::vector{0, 0xAF})); + verifyFrame(display, wire); + const auto* original = display.getBuffer(); + const auto allocations = total_allocations; + resetPins(); + assert(display.begin(2, 0x3C, false, false)); + assert(display.getBuffer() == original && total_allocations == allocations); + assert(pin_modes.empty() && pin_levels.empty()); + verifyFrame(display, wire); + } + assert(active_allocations == 0); + + // Initialization and frame errors stop further writes, restore the bus + // clock, and leave a bounded framebuffer available for a later retry. + for (unsigned failure = 0; failure < 8; ++failure) { + TwoWire wire; + AutoOLEDBackend display(128, 64, &wire, -1); + const unsigned init = +#if defined(OLED_CONTROLLER_OVERRIDE) && OLED_CONTROLLER_OVERRIDE == 2 + 1; +#else + 3; +#endif + if (failure < 2) wire.failed_tx = init + failure; + if (failure == 2) wire.failed_byte = init - 1; + if (failure == 3) wire.failed_block = 0; + if (failure < 4) { + assert(!display.begin(2, 0x3C, false, false)); + assert(wire.clocks.back() == 100000); + const auto transfers = wire.transactions.size(); + display.display(); + display.ssd1306_command(0xAF); + assert(wire.transactions.size() == transfers); + } else { + beginSH(display, wire); + const auto transfers = wire.transactions.size(); + if (failure == 4) wire.failed_tx = transfers; + if (failure == 5) wire.failed_block = wire.block_writes + 1; + if (failure == 6) wire.failed_tx = transfers + 1; + if (failure == 7) wire.failed_byte = wire.byte_writes + 1; + display.display(); + assert(wire.transactions.size() == transfers + (failure == 4 ? 1 : 2)); + assert(wire.clocks.back() == 100000); + } + wire.failed_tx = wire.failed_byte = wire.failed_block = ~0u; + wire.requests.clear(); + const auto* original = display.getBuffer(); + const auto allocations = total_allocations; + assert(display.begin(2, 0x3C, false, false)); + assert(display.getBuffer() == original && total_allocations == allocations); + verifyFrame(display, wire); + } + assert(active_allocations == 0); + { + TwoWire wire; + AutoOLEDBackend display(128, 64, &wire, -1); + allocation_fails = true; + assert(!display.begin(2, 0x3C, false, false)); + assert(display.getBuffer() == nullptr && active_allocations == 0); + const auto transfers = wire.transactions.size(); + display.display(); + display.ssd1306_command(0xAF); + assert(wire.transactions.size() == transfers); + allocation_fails = false; + wire.requests.clear(); + beginSH(display, wire); + } +#endif + assert(active_allocations == 0); + +#if !defined(OLED_CONTROLLER_OVERRIDE) || OLED_CONTROLLER_OVERRIDE == 0 + { + TwoWire wire; + wire.status[0x3C] = {0, 0}; + AutoOLEDBackend display(128, 64, &wire, -1); + assert(display.begin(2, 0x3C, false, false)); + const auto* original = display.getBuffer(); + const auto allocations = total_allocations; + wire = TwoWire{}; + assert(display.begin(2, 0x3C, false, false)); + assert(display.controller() == OLEDController::SH1106); + assert(display.getBuffer() == original && total_allocations == allocations); + verifyFrame(display, wire); + wire = TwoWire{}; + wire.present[0x3C] = false; + assert(!display.begin(2, 0x3C, false, false)); + assert(display.controller() == OLEDController::Absent); + const auto transfers = wire.transactions.size(); + display.display(); + assert(wire.transactions.size() == transfers); + wire = TwoWire{}; + wire.status[0x3C] = {0x80, 0x80}; + assert(display.begin(2, 0x3C, false, false)); + assert(display.controller() == OLEDController::SSD1306); + assert(display.getBuffer() == original && total_allocations == allocations); + display.display(); + assert(display.base_display_calls == 1 && display.base_begin_calls == 2); + } + assert(active_allocations == 0); + for (bool fail : {false, true}) { + TwoWire wire; + wire.status[0x3C] = {0x00, 0x80}; + AutoOLEDBackend display(128, 64, &wire, -1); + display.fail_begin = fail; + assert(display.begin(2, 0x3C, true, true) == !fail); + assert(display.controller() == OLEDController::SSD1306); + assert(display.base_begin_calls == 1); + assert(!display.last_reset && !display.last_periph_begin); + assert(wire.begun == 1); + display.display(); + assert(display.base_display_calls == unsigned(!fail)); + } +#endif + { + TwoWire wire; + wire.present[0x3C] = false; + AutoOLEDBackend display(128, 64, &wire, -1); + display.ssd1306_command(0xAE); + assert(wire.transactions.empty()); + assert(!display.begin(2, 0x3C, true, false)); + assert(display.controller() == OLEDController::Absent); + assert(display.getBuffer() == nullptr); + assert(totalRequests(wire) == 0); + const auto transfers = wire.transactions.size(); + display.display(); + display.ssd1306_command(0xAE); + assert(wire.transactions.size() == transfers); + } + assert(active_allocations == 0); + std::puts("PASS: OLED backend selection, single buffer, page writes and retry"); +} +''' + + +class OledDisplayProbeTests(unittest.TestCase): + def compile_run(self, source_text, definitions=(), mocks=None): + compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++") + if compiler is None: + self.skipTest("a host C++17 compiler is required") + with tempfile.TemporaryDirectory(prefix="meshcore-oled-probe-") as directory: + work = Path(directory) + source = work / "probe.cpp" + source.write_text(source_text, encoding="ascii") + for name, contents in (mocks or {}).items(): + (work / name).write_text(contents, encoding="ascii") + binary = work / "probe" + sanitizer = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all", + "-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else []) + built = subprocess.run([ + compiler, "-std=c++17", "-O1", "-Wall", "-Wextra", "-Werror", + *sanitizer, *definitions, "-I", str(work), + "-I", str(ROOT / "src"), str(source), "-o", str(binary), + ], text=True, capture_output=True, timeout=60) + self.assertEqual(built.returncode, 0, built.stdout + built.stderr) + checked = subprocess.run([str(binary)], text=True, capture_output=True, timeout=15) + self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr) + return checked.stdout + + def test_real_detector_selects_only_repeated_sh1106_status(self): + self.assertIn("PASS: OLED controller status detection", self.compile_run(HARNESS)) + + def test_real_backend_transfers_and_failure_recovery(self): + mocks = {"Arduino.h": ARDUINO, "Wire.h": WIRE, "Adafruit_SSD1306.h": VENDOR} + for definitions in ((), ("-DOLED_CONTROLLER_OVERRIDE=1",), + ("-DOLED_CONTROLLER_OVERRIDE=2",)): + with self.subTest(definitions=definitions): + output = self.compile_run(BACKEND_HARNESS, definitions, mocks) + self.assertIn("PASS: OLED backend selection", output) + + +if __name__ == "__main__": + unittest.main() diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 5c2f868db..ccb776f2b 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -16,6 +16,7 @@ build_flags = ${nrf52_base.build_flags} ; UICR and reset once before the external SX1262 can be reached. -D CONFIG_NFCT_PINS_AS_GPIOS -D NRF52_POWER_MANAGEMENT + -D OLED_AUTO_DETECT=1 ; shared WisBlock SSD1306 / SH1106 screen support -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 08932eb15..ffc2c54eb 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -19,6 +19,7 @@ build_flags = ${nrf52_base.build_flags} ; The boot power policy automatically protects Ethernet/PoE and external NOR. -D PIN_GPS_EN=-1 -D PIN_OLED_RESET=-1 + -D OLED_AUTO_DETECT=1 ; SSD1306 / SH1106, including WisMesh Pocket v2 and v3 -D USE_SX1262 -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper