#include "SH1106Display.h" #include #include #include "Adafruit_SH110X.h" #include "MiscFixedRenderer.h" bool SH1106Display::i2c_probe(TwoWire &wire, uint8_t addr) { wire.beginTransmission(addr); uint8_t error = wire.endTransmission(); return (error == 0); } // Color scheme ColorVal UIColor::window_bkg = SH110X_BLACK; ColorVal UIColor::title_bkg = SH110X_BLACK; ColorVal UIColor::title_txt = SH110X_WHITE; ColorVal UIColor::primary_txt = SH110X_WHITE; ColorVal UIColor::secondary_txt = SH110X_WHITE; ColorVal UIColor::warning_txt = SH110X_WHITE; ColorVal UIColor::popup_bkg = SH110X_BLACK; ColorVal UIColor::popup_txt = SH110X_WHITE; ColorVal UIColor::corp_blue = SH110X_WHITE; bool SH1106Display::begin() { // Wire must already be initialised by board.begin() before this is called. // Boards with non-standard SH1106 addresses should define DISPLAY_ADDRESS // in their variant/platformio configuration. The SA0 strap selects 0x3C or // 0x3D and differs between revisions of the same board (e.g. T-Beam // Supreme), so fall back to the other address of the pair. uint8_t addr = 0; if (i2c_probe(Wire, DISPLAY_ADDRESS)) { addr = DISPLAY_ADDRESS; } else if (i2c_probe(Wire, DISPLAY_ADDRESS ^ 1)) { addr = DISPLAY_ADDRESS ^ 1; } // Run the Adafruit init even when no panel answered: it is what allocates // the frame buffer and the I2C device. Skipping it leaves i2c_dev and // spi_dev NULL, and UITask::begin() calls turnOn() regardless of our // return value, which then dereferences the null spi_dev. bool ok = display.begin(addr ? addr : DISPLAY_ADDRESS, true); #ifdef DISPLAY_ROTATION display.setRotation(DISPLAY_ROTATION); #endif return addr != 0 && ok; } void SH1106Display::turnOn() { display.oled_command(SH110X_DISPLAYON); uint8_t pre[] = { 0xD9, _precharge }; display.oled_commandList(pre, 2); display.setContrast(_contrast); _isOn = true; _force_redraw = true; // panel was off — guarantee the next endFrame() flushes } void SH1106Display::turnOff() { display.oled_command(SH110X_DISPLAYOFF); _isOn = false; } void SH1106Display::clear() { display.clearDisplay(); display.display(); _force_redraw = true; // next endFrame() must flush even if its CRC matches a pre-clear frame } void SH1106Display::startFrame(ColorVal bkg) { display.clearDisplay(); // TODO: apply 'bkg' _color = SH110X_WHITE; display.setTextColor(_color); display.setTextSize(1); _text_sz = 1; display.cp437(true); // Use full 256 char 'Code Page 437' font } // The GFX buffer still holds the last frame sent until the next startFrame(). bool SH1106Display::slideBegin(int y0) { _slide_y0 = y0 < 0 ? 0 : (y0 > height() ? height() : y0); memcpy(_slide_old, display.getBuffer(), sizeof(_slide_old)); return true; } // Buffer layout: one byte per column per 8-row page, bit n = row page*8+n. void SH1106Display::slideCompose(int dx) { uint8_t* buf = display.getBuffer(); const int W = width(); int off = dx < 0 ? -dx : dx; if (off > W) off = W; uint8_t fresh[128]; for (int p = _slide_y0 / 8; p < height() / 8; p++) { const uint8_t keep = (p == _slide_y0 / 8) ? (uint8_t)~(0xFF << (_slide_y0 & 7)) : 0; uint8_t* cur = buf + p * W; const uint8_t* old = _slide_old + p * W; memcpy(fresh, cur, W); for (int x = 0; x < W; x++) { uint8_t v; if (dx >= 0) v = x < W - off ? old[x + off] : fresh[x - (W - off)]; else v = x >= off ? old[x - off] : fresh[x + (W - off)]; cur[x] = (cur[x] & keep) | (v & ~keep); } } } void SH1106Display::setTextSize(int sz) { _text_sz = sz; _vw_dirty = true; display.setTextSize(sz); } void SH1106Display::setColor(ColorVal c) { _color = c; display.setTextColor(_color); } void SH1106Display::setCursor(int x, int y) { display.setCursor(x, y); } uint8_t SH1106Display::glyphXAdvance(uint32_t cp) { return miscFixedXAdvance(cp, _text_sz); } void SH1106Display::translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) { if (_single_font) { size_t n = strlen(src); if (n >= dest_size) n = dest_size - 1; memcpy(dest, src, n); dest[n] = '\0'; } else { DisplayDriver::translateUTF8ToBlocks(dest, src, dest_size); } } void SH1106Display::print(const char *str) { if (!_single_font) { display.print(str); return; } miscFixedPrint(display, str, _text_sz, _color); } void SH1106Display::fillRect(int x, int y, int w, int h) { display.fillRect(x, y, w, h, _color); } void SH1106Display::drawRect(int x, int y, int w, int h) { display.drawRect(x, y, w, h, _color); } void SH1106Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) { display.drawBitmap(x, y, bits, w, h, SH110X_WHITE); } uint16_t SH1106Display::getTextWidth(const char *str) { if (_single_font) return miscFixedTextWidth(str, _text_sz); int16_t x1, y1; uint16_t w, h; display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h); return w; } void SH1106Display::setBrightness(uint8_t level) { // Contrast alone has limited effect on some OLED panels; combining with // pre-charge period (0xD9) gives a wider perceptible dimming range. // Pre-charge 0x11 = phase1=1,phase2=1 (minimum drive); 0x1F = default. static const uint8_t contrast_values[] = { 0, 25, 60, 150, 255 }; static const uint8_t precharge_values[] = { 0x11, 0x15, 0x1F, 0x1F, 0x1F }; uint8_t idx = level < 5 ? level : 4; _contrast = contrast_values[idx]; _precharge = precharge_values[idx]; uint8_t pre[] = { 0xD9, _precharge }; display.oled_commandList(pre, 2); display.setContrast(_contrast); } #ifdef UI_PERF_L1 uint32_t g_disp_flushes = 0; #endif void SH1106Paged::sendBlock(uint8_t page, uint8_t col, uint8_t len) { uint8_t c = col + _page_start_offset; uint8_t cmd[] = { 0x00, (uint8_t)(SH110X_SETPAGEADDR + page), (uint8_t)(0x10 + (c >> 4)), (uint8_t)(c & 0xF) }; uint8_t dc_byte = 0x40; const uint8_t* ptr = buffer + (uint16_t)page * WIDTH + col; uint16_t maxbuff = i2c_dev->maxBufferSize() - 1; i2c_dev->setSpeed(i2c_preclk); i2c_dev->write(cmd, 4); while (len) { uint8_t n = len < maxbuff ? len : (uint8_t)maxbuff; i2c_dev->write(ptr, n, true, &dc_byte, 1); ptr += n; len -= n; } i2c_dev->setSpeed(i2c_postclk); } void SH1106Display::endFrame() { const uint8_t* buf = display.getBuffer(); const uint8_t per_page = 128 / BLOCK_W; bool any = false; bool dirty[BLOCKS]; for (uint8_t i = 0; i < BLOCKS; i++) { const uint8_t* q = buf + (uint16_t)i * BLOCK_W; // blocks run along each page in buffer order uint32_t h = 2166136261u; for (uint8_t j = 0; j < BLOCK_W; j++) { h ^= q[j]; h *= 16777619u; } dirty[i] = _force_redraw || h != _block_hash[i]; _block_hash[i] = h; any |= dirty[i]; } if (!any) return; #ifdef UI_PERF_L1 g_disp_flushes++; #endif if (_force_redraw || !display.canSendBlocks()) { _force_redraw = false; display.display(); return; } for (uint8_t page = 0; page < 64 / 8; page++) { // runs of neighbouring dirty blocks go out as one transfer for (uint8_t b = 0; b < per_page; ) { if (!dirty[page * per_page + b]) { b++; continue; } uint8_t e = b; while (e + 1 < per_page && dirty[page * per_page + e + 1]) e++; display.sendBlock(page, b * BLOCK_W, (e - b + 1) * BLOCK_W); b = e + 1; } } }