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.
This commit is contained in:
mikecarper committed 2026-10-20 09:43:25 -07:00
1 parent 499a4f850c
commit a1ec96fcab
8 files changed
+729

No files matched your search

+1
View File
@@ -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
+13
View File
@@ -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
+102
View File
@@ -0,0 +1,102 @@
#pragma once
#include <Adafruit_SSD1306.h>
#include <stdlib.h>
#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<uint8_t*>(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);
}
};
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <stdint.h>
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 <typename WireBus>
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
+7
View File
@@ -6,6 +6,9 @@
#define SSD1306_NO_SPLASH
#include <Adafruit_SSD1306.h>
#include <helpers/RefCountedDigitalPin.h>
#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;
+571
View File
@@ -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 <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()
+1
View File
@@ -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
+1
View File
@@ -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