159 lines
5.8 KiB
C++
159 lines
5.8 KiB
C++
#include "Display.h"
|
|
#include "Config.h"
|
|
#include "Font_Cyrillic.h"
|
|
#include "Log.h"
|
|
#include <Wire.h>
|
|
#include <esp_log.h>
|
|
#include <string.h>
|
|
|
|
namespace {
|
|
uint32_t nextUtf8Codepoint(const char *&text) {
|
|
const uint8_t first = static_cast<uint8_t>(*text++);
|
|
if (first < 0x80U) return first;
|
|
if ((first & 0xE0U) == 0xC0U) {
|
|
const uint8_t second = static_cast<uint8_t>(*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<unsigned char>(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<uint16_t>(
|
|
(static_cast<uint64_t>(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);
|
|
}
|