Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ssd1306_virtual

CI

An ESPHome display: platform with no bus and no panel: an SSD1306 that exists only as a framebuffer in RAM. Write the usual lambda:, and another component reads the buffer.

Build and debug a display before the hardware arrives, or without ever attaching any.

external_components:
  - source: github://kaboom748/ssd1306_virtual
    components: [ssd1306_virtual]
    refresh: 1d

display:
  - platform: ssd1306_virtual
    id: screen
    model: "SSD1306 128x64"
    update_interval: 33ms
    lambda: |-
      it.printf(0, 0, id(roboto), "%u s", (unsigned) (millis() / 1000));

Pair it with oled_stream to watch it in a browser tab:

oled_stream:
  - display_id: screen
    port: 8080
    contrast_lambda: return id(screen).get_contrast();

oled_stream needs no changes: its display_id accepts any display::DisplayBuffer, and this is one.

See docs/ssd1306_virtual.md for the full option list, and example/ for two working configs.

The idea

Nothing about the display is reimplemented. ssd1306_base::SSD1306 has exactly two pure virtuals:

virtual void command(uint8_t value) = 0;
virtual void write_display_data() = 0;

Everything else - the model table, the geometry, the buffer length, the pixel layout, fill(), contrast, invert - is concrete. So this component overrides those two with empty bodies and inherits the rest. The layout is not a copy of the real driver that can drift from it: it is the real driver.

setup() therefore runs unchanged. It allocates the framebuffer for the configured model, walks the whole init sequence into the void, and clears the buffer for real. What is left is a correctly sized, correctly zeroed SSD1306 framebuffer, with no hardware.

The whole component is about 30 lines of code.

Platforms

There is not one #ifdef in it. No bus means nothing to be platform-specific about: ESP32 (Arduino and ESP-IDF), ESP8266, RP2040, host. RAM is the only constraint, and the largest model needs 2048 bytes.

Models

All 14 of the ssd1306_base family. Sizes are measured, not computed on paper.

model: native framebuffer
SSD1306 96x16 96x16 192
SSD1306 64x32 64x32 256
SSD1306 72x40 72x40 360
SSD1306 64x48 64x48 384
SSD1306 128x32 128x32 512
SSD1306 128x64 128x64 1024
SH1106 128x32 128x32 512
SH1106 128x64 128x64 1024
SH1106 96x16 96x16 192
SH1106 64x48 64x48 384
SH1107 128x64 64x128 1024
SH1107 128x128 128x128 2048
SSD1305 128x32 128x32 512
SSD1305 128x64 128x64 1024

SH1107 128x64 really is 64 wide and 128 tall. That is upstream's naming.

Reading the framebuffer

uint8_t *buf = id(screen).get_buffer();
size_t len = id(screen).get_buffer_length();   // == width * height / 8

buffer_ is an inherited protected member, so these are ordinary C++: no cast, no assumption about anyone's memory layout. They are shaped like the accessors a future display::DisplayBuffer would expose upstream, so the day it does, they can be deleted and callers keep compiling.

Layout, which is the SSD1306's because it is the SSD1306's:

pos = x + (y / 8) * width      bit = 1 << (y % 8)      1 = pixel on

Tests

tests/components/ssd1306_virtual/   upstream ESPHome format, for the eventual PR
tests/ci/                           self-contained, what this repo's CI compiles
tests/bench/                        runtime benches, run on the host

tests/bench/run.sh downloads ESPHome, links the component against the real ssd1306_base, display and display_buffer sources, and runs a live display on your PC. Nothing is stubbed except the two host platform symbols that live in the file owning main().

==> test_virtual
[ OK ] a display with NO hardware sets up without failing
[ OK ] get_native_width/height = 128x64
[ OK ] get_buffer() reaches a real allocation
[ OK ] get_buffer_length() = 1024 bytes
[ OK ] width * height / 8 agrees with the allocation
[ OK ] setup() left the whole 1024-byte buffer cleared
[ OK ] pixel (5,9) lands at page 1, bit 1
[ OK ] a mono_page decoder finds it back
[ OK ] line() of 31 px + the lone pixel = 32 pixels lit
[ OK ] get_contrast() = 0.6 with no bus to write it to
[ OK ] rotation 90 swaps get_width/get_height
[ OK ] ...but get_native_* stays PHYSICAL, so the buffer stays 1024

==> sweep      all 14 models, instantiated and measured
==> mutant     asserts a mono_row layout and is rejected

mutant is the reason the rest is worth anything: it asserts the wrong layout on the same live display and must fail. run.sh treats a passing mutant as an error, so a bench that has stopped checking anything cannot go unnoticed.

Pass a checkout of oled_stream to also build the integration bench, which observes rather than assumes that its downcast lands on the same pointer as get_buffer():

tests/bench/run.sh /path/to/oled_stream/components/oled_stream

Honest limit: the benches run on the host platform with a PC compiler. They catch type, layout and logic mistakes, which is what this component is made of, since it has no platform-specific code. They do not replace flashing a board.

No-ops, inherited on purpose

contrast, invert, flip_x, flip_y, offset_x and offset_y are controller register writes on real hardware: they never touch buffer_. With no bus they keep their state - get_contrast() answers correctly - but they still do not reach the framebuffer. A component reading the framebuffer of a real SSD1306 cannot see them either, so this is fidelity rather than a gap.

reset_pin: is accepted, because rejecting it would break the copy-paste from a working ssd1306_i2c config. If you set it, a real GPIO gets toggled for nothing.

If init_internal_() ever failed to allocate, it logs and returns with buffer_ still null, and the fill() right after it in SSD1306::setup() dereferences that null. That is an upstream fragility shared with every SSD1306 platform, not something this component adds; at 2048 bytes it will not happen.

License

C++ under GPLv3, Python under MIT - ESPHome's model. See LICENSE.

- **It rehearses your lambda, not your bus.** No panel means no panel quirks:
  wiring, address, contrast and controller-init problems of a real SSD1306
  will not reproduce here. The contrast slider is simulated in the viewer.

About

ESPHome display platform: an SSD1306 with no bus and no panel, just its framebuffer in RAM

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages