Files
MeshCore/test/test_oled_display_probe.py
T
mikecarper d5a6df6283 Detect SSD1306 and SH1106 screens on RAK boards
Use repeated status reads at screen initialization and preserve SSD1306 for ambiguous responses. Share the existing framebuffer, drawing, rotation and power handling; use SH1106 page transfers when detected.

Enable this for RAK4631 and RAK3401, including WisMesh Pocket v2/v3. Add controller overrides, protocol tests and CI coverage. Addresses meshcore-dev/MeshCore#3296.
2026-10-20 09:43:25 -07:00

572 lines
20 KiB
Python

#!/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 <cassert>
#include <cstdint>
#include <cstdio>
#include <initializer_list>
#include <limits>
#include <vector>
#include <helpers/ui/OLEDDisplayProbe.h>
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<Sample> samples;
std::vector<uint8_t> status_errors;
std::vector<std::vector<uint8_t>> writes;
unsigned requests = 0, reads = 0, ends = 0, write_calls = 0;
unsigned failed_write = std::numeric_limits<unsigned>::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<uint8_t>{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<uint8_t> 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<uint8_t>{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<uint8_t>{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 <cstdint>
#include <utility>
#include <vector>
#define OUTPUT 1
#define HIGH 1
#define LOW 0
inline std::vector<std::pair<int, int>> pin_modes, pin_levels;
inline std::vector<unsigned> 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 <cassert>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <map>
#include <vector>
struct TwoWire {
struct Transaction { uint8_t address; std::vector<uint8_t> data; };
std::vector<Transaction> transactions;
std::vector<uint32_t> clocks;
std::map<uint8_t, bool> present{{0x3C, true}, {0x3D, false}};
std::map<uint8_t, std::vector<int>> status{{0x3C, {8, 8, 8, 8}}};
std::map<uint8_t, unsigned> requests;
unsigned begun = 0, byte_writes = 0, block_writes = 0;
unsigned failed_tx = std::numeric_limits<unsigned>::max();
unsigned failed_byte = std::numeric_limits<unsigned>::max();
unsigned failed_block = std::numeric_limits<unsigned>::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 <Arduino.h>
#include <Wire.h>
#include <cstdlib>
#include <cstring>
#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<uint8_t*>(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 <cassert>
#include <cstdlib>
#include <stdlib.h>
#include <cstdio>
#include <algorithm>
#include <Adafruit_SSD1306.h>
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 <helpers/ui/AutoOLEDBackend.h>
#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<uint8_t>{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<std::pair<int, int>>{{9, OUTPUT}}));
assert(pin_levels == (std::vector<std::pair<int, int>>{{9, HIGH}, {9, LOW}, {9, HIGH}}));
assert(delays == (std::vector<unsigned>{1, 10, 100}));
assert(totalRequests(wire) ==
#if defined(OLED_CONTROLLER_OVERRIDE) && OLED_CONTROLLER_OVERRIDE == 2
0
#else
2
#endif
);
const std::vector<uint8_t> 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<uint8_t>{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()