#include "Display.h" #include "Config.h" #include "Font_Cyrillic.h" #include "Log.h" #include #include #include namespace { uint32_t nextUtf8Codepoint(const char *&text) { const uint8_t first = static_cast(*text++); if (first < 0x80U) return first; if ((first & 0xE0U) == 0xC0U) { const uint8_t second = static_cast(*text); if ((second & 0xC0U) == 0x80U) { ++text; return ((first & 0x1FU) << 6) | (second & 0x3FU); } } return '?'; } } Display::Display() : oled_(128, 32, &Wire, -1) {} bool Display::begin() { Wire.begin(GPIO_SDA, GPIO_SCL); Wire.setClock(400000); // keeps a full 128x32 framebuffer update near 15 ms Wire.setTimeOut(30); // a faulty/stretched I2C bus must not stall button polling for seconds // An absent optional OLED produces a large burst of ESP-IDF NACK messages. // Probe it once and keep the I2C driver quiet when no display is connected. esp_log_level_set("i2c.master", ESP_LOG_NONE); Wire.beginTransmission(OLED_ADDRESS); if (Wire.endTransmission() != 0) { ok_ = false; Log::printf("OLED", "not detected at I2C address=0x%02X", OLED_ADDRESS); return false; } ok_ = oled_.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS); if (ok_) { oled_.setRotation(OLED_ROTATION); oled_.setTextColor(SSD1306_WHITE); oled_.setTextSize(1); powered_ = true; } Log::printf("OLED", "initialization %s, I2C address=0x%02X", ok_ ? "OK" : "FAILED", OLED_ADDRESS); return ok_; } void Display::setPower(bool enabled) { if (!ok_ || powered_ == enabled) return; oled_.ssd1306_command(enabled ? SSD1306_DISPLAYON : SSD1306_DISPLAYOFF); powered_ = enabled; Log::printf("OLED", "display power %s", enabled ? "ON" : "OFF"); } void Display::drawTextLine(const char *text, int16_t y, int16_t startX) { int16_t x = startX; while (text && *text && x + CyrillicFont::WIDTH <= oled_.width()) { const uint32_t codepoint = nextUtf8Codepoint(text); const uint8_t *glyph = CyrillicFont::glyph(codepoint); if (glyph) { for (uint8_t row = 0; row < CyrillicFont::HEIGHT; ++row) { const uint8_t pixels = pgm_read_byte(glyph + row); for (uint8_t column = 0; column < CyrillicFont::WIDTH; ++column) if (pixels & (1U << (CyrillicFont::WIDTH - 1U - column))) oled_.drawPixel(x + column, y + row, SSD1306_WHITE); } } else if (codepoint < 0x100U) { oled_.drawChar(x, y, static_cast(codepoint), SSD1306_WHITE, SSD1306_BLACK, 1); } else { oled_.drawChar(x, y, '?', SSD1306_WHITE, SSD1306_BLACK, 1); } x += CyrillicFont::ADVANCE; } } void Display::show(const char *a, const char *b, uint32_t progress, uint32_t progressTotal, const char *topRight) { char one[64], two[64]; snprintf(one, sizeof(one), "%s", a ? a : ""); snprintf(two, sizeof(two), "%s", b ? b : ""); // Serial is the primary UI mirror and remains available when OLED is absent. Log::printf("UI", "%s | %s", one, two); if (!ok_) return; setPower(true); oled_.clearDisplay(); drawTextLine(one, 3); drawTextLine(two, 19); if (topRight && *topRight) { oled_.fillRect(oled_.width() - CyrillicFont::ADVANCE, 0, CyrillicFont::ADVANCE, CyrillicFont::HEIGHT + 3, SSD1306_BLACK); drawTextLine(topRight, 3, oled_.width() - CyrillicFont::ADVANCE); } if (progressTotal) { if (progress > progressTotal) progress = progressTotal; const uint16_t width = static_cast( (static_cast(progress) * 128U + progressTotal - 1U) / progressTotal); if (width) oled_.drawFastHLine(0, 31, width, SSD1306_WHITE); } oled_.display(); } void Display::formatFrequency(float hz, char *out, size_t n) { float value = hz; const char *suffix = "Hz"; if (hz >= 999950.0f) { value = hz / 1000000.0f; suffix = "M"; } else if (hz >= 999.5f) { value = hz / 1000.0f; suffix = "k"; } if (suffix[0] == 'M' && fabsf(value - roundf(value)) < 0.0005f) snprintf(out, n, "%.0f%s", value, suffix); else if (value >= 100.0f) snprintf(out, n, "%.1f%s", value, suffix); else if (value >= 10.0f) snprintf(out, n, "%.2f%s", value, suffix); else snprintf(out, n, "%.3f%s", value, suffix); } void Display::formatTestFrequency(uint32_t hz, char *out, size_t n) { if (hz >= 1000000U && hz % 1000000U == 0) snprintf(out, n, "%luM", hz / 1000000U); else if (hz >= 1000U && hz % 1000U == 0) snprintf(out, n, "%luk", hz / 1000U); else if (hz >= 1000U) { const float khz = hz / 1000.0f; if (khz >= 100.0f) snprintf(out, n, "%.1fk", khz); else if (khz >= 10.0f) snprintf(out, n, "%.2fk", khz); else snprintf(out, n, "%.3fk", khz); } else snprintf(out, n, "%lu", hz); } void Display::formatPwmFrequency(uint32_t hz, char *out, size_t n) { if (hz >= 1000000U && hz % 1000000U == 0U) snprintf(out, n, "%luMHz", hz / 1000000U); else if (hz >= 1000U && hz % 1000U == 0U) snprintf(out, n, "%lukHz", hz / 1000U); else if (hz >= 1000U) snprintf(out, n, "%.3gkHz", hz / 1000.0f); else snprintf(out, n, "%luHz", hz); } void Display::formatPulse(uint32_t pulseNs, char *out, size_t n, bool measured) { if (pulseNs >= 1000U) { const float us = pulseNs / 1000.0f; if (measured) snprintf(out, n, "%.2fu", us); else if (pulseNs % 1000U == 0U) snprintf(out, n, "%luus", pulseNs / 1000U); else snprintf(out, n, "%.2fus", us); } else if (measured) { snprintf(out, n, "%.3fu", pulseNs / 1000.0f); } else { snprintf(out, n, "%luns", pulseNs); } } void Display::formatDuration(uint64_t us, char *out, size_t n) { const uint64_t totalSeconds = (us + 999999ULL) / 1000000ULL; const uint64_t minutes = totalSeconds / 60ULL; if (minutes < 60) snprintf(out, n, "%02llu:%02llu", minutes, totalSeconds % 60ULL); else snprintf(out, n, "%llu:%02llu", minutes / 60ULL, minutes % 60ULL); }