Сделан русский текст

This commit is contained in:
2026-08-08 11:15:20 +03:00
parent c952c165f8
commit 70fe3566b4
9 changed files with 300 additions and 69 deletions

View File

@@ -1,5 +1,6 @@
#include "App.h"
#include "Config.h"
#include "Config_Text.h"
#include "Log.h"
#include <WiFi.h>
#include <esp_mac.h>
@@ -30,12 +31,36 @@ void formatErrorDuty(float duty, char *out, size_t size) {
else snprintf(out, size, "%.1f%%", duty);
}
size_t utf8CharacterCount(const char *text) {
size_t count = 0;
while (text && *text) {
const uint8_t byte = static_cast<uint8_t>(*text++);
// Continuation bytes (10xxxxxx) belong to the preceding UTF-8 character.
if ((byte & 0xC0U) != 0x80U) ++count;
}
return count;
}
const char *uiRoleName(Role role) {
const uint8_t index = static_cast<uint8_t>(role);
return index < sizeof(UiText::ROLE_NAMES) / sizeof(UiText::ROLE_NAMES[0])
? UiText::ROLE_NAMES[index] : "?";
}
const char *uiFailName(FailReason reason) {
const uint8_t index = static_cast<uint8_t>(reason);
return index < sizeof(UiText::FAIL_NAMES) / sizeof(UiText::FAIL_NAMES[0])
? UiText::FAIL_NAMES[index] : "UNKNOWN";
}
void formatMenuLine(const char *label, const char *value, char *out, size_t size) {
constexpr size_t OLED_TEXT_COLUMNS = 21;
const size_t valueLength = strlen(value);
const int labelWidth = static_cast<int>(
valueLength < OLED_TEXT_COLUMNS ? OLED_TEXT_COLUMNS - valueLength : 1U);
snprintf(out, size, "%-*s%s", labelWidth, label, value);
const size_t labelLength = utf8CharacterCount(label);
const size_t valueLength = utf8CharacterCount(value);
const size_t usedColumns = labelLength + valueLength;
const int padding = static_cast<int>(
usedColumns < OLED_TEXT_COLUMNS ? OLED_TEXT_COLUMNS - usedColumns : 0U);
snprintf(out, size, "%s%*s%s", label, padding, "", value);
}
uint32_t overallProgress(uint32_t stageIndex, uint8_t step) {
@@ -173,8 +198,9 @@ void App::update() {
void App::showIdle() {
setActivePerformance(false);
lastUserActivityMs_ = millis();
char one[24]; snprintf(one, sizeof(one), "MODE: %s", roleName(static_cast<Role>(settings_.role)));
display_.show(one, "START=RUN");
char one[64]; snprintf(one, sizeof(one), "%s%s", UiText::MODE_PREFIX,
uiRoleName(static_cast<Role>(settings_.role)));
display_.show(one, UiText::START_RUN);
}
void App::sanitizeRange() {
@@ -198,35 +224,35 @@ void App::changeMenu(int d) {
}
void App::showMenu() {
char one[22], value[12], total[22], all[12];
char one[64], value[24], total[64], all[12];
const char *label = nullptr;
Display::formatDuration(actualNominalTotalUs(), all, sizeof(all));
switch (menuItem_) {
case 0:
Display::formatTestFrequency(params_.startHz, value, sizeof(value));
strncat(value, " Hz", sizeof(value) - strlen(value) - 1U);
label = "START FREQ:";
strncat(value, UiText::FREQUENCY_UNIT, sizeof(value) - strlen(value) - 1U);
label = UiText::MENU_START_FREQUENCY;
break;
case 1:
Display::formatTestFrequency(params_.endHz, value, sizeof(value));
strncat(value, " Hz", sizeof(value) - strlen(value) - 1U);
label = "END FREQ:";
strncat(value, UiText::FREQUENCY_UNIT, sizeof(value) - strlen(value) - 1U);
label = UiText::MENU_END_FREQUENCY;
break;
case 2:
snprintf(value, sizeof(value), "+/-%g%%", params_.accuracyPct);
label = "ACCURACY:";
label = UiText::MENU_ACCURACY;
break;
case 3:
snprintf(value, sizeof(value), "%.1fs", params_.testTimeMs / 1000.0f);
label = "TEST TIME:";
label = UiText::MENU_TEST_TIME;
break;
default:
snprintf(value, sizeof(value), "%u%%", params_.dutyPct);
label = "PWM DUTY:";
label = UiText::MENU_PWM_DUTY;
break;
}
formatMenuLine(label, value, one, sizeof(one));
formatMenuLine("TOTAL TIME:", all, total, sizeof(total));
formatMenuLine(UiText::MENU_TOTAL_TIME, all, total, sizeof(total));
display_.show(one, total);
}
@@ -253,7 +279,7 @@ void App::startTest() {
state_ = AppState::SOLO_MEASURE;
} else if (!radio_.begin()) finish(false, FailReason::LINK_LOST);
else if (role == Role::MASTER) startMasterDiscovery();
else { state_ = AppState::SLAVE_READY; Log::event("TEST", "Slave armed and waiting for Master"); display_.show("SLAVE READY", "WAIT MASTER"); }
else { state_ = AppState::SLAVE_READY; Log::event("TEST", "Slave armed and waiting for Master"); display_.show(UiText::SLAVE_READY, UiText::WAIT_MASTER); }
}
bool App::armSlave(bool preserveDisplay) {
@@ -266,13 +292,13 @@ bool App::armSlave(bool preserveDisplay) {
if (!radio_.begin()) {
state_ = AppState::FINISHED; pendingReason_ = FailReason::LINK_LOST;
slaveRearmAtMs_ = millis() + LINK_HEARTBEAT_TIMEOUT_MS;
display_.show("LINK FAILED", "RADIO ERROR");
display_.show(UiText::LINK_FAILED, UiText::RADIO_ERROR);
return false;
}
radio_.setWindowedReceive(true);
radio_.flush(); state_ = AppState::SLAVE_READY;
Log::event("TEST", "Slave automatically armed and waiting for Master");
if (!preserveDisplay) display_.show("SLAVE READY", "WAIT MASTER");
if (!preserveDisplay) display_.show(UiText::SLAVE_READY, UiText::WAIT_MASTER);
return true;
}
@@ -336,7 +362,7 @@ void App::startMasterDiscovery() {
pendingPacket_ = makePacket(MessageType::DISCOVER); radio_.sendBroadcast(pendingPacket_);
lastSendMs_ = millis(); retries_ = 0;
state_ = AppState::MASTER_DISCOVER; Log::printf("ESP-NOW", "discovery started session=%08lX", session_);
display_.show("MASTER SEARCH", "HOLD START=STOP");
display_.show(UiText::MASTER_SEARCH, UiText::HOLD_START_STOP);
}
ProtocolPacket App::makePacket(MessageType type) const {
@@ -400,7 +426,7 @@ void App::handleRadio() {
memcpy(peer_, r.mac, 6); havePeer_ = true; session_ = r.packet.session; stageIndex_ = 0; sequence_ = r.packet.sequence;
lastPeerSeenMs_ = millis();
ProtocolPacket ack = makePacket(MessageType::DISCOVER_ACK); ack.sequence = r.packet.sequence; sendLinked(ack);
state_ = AppState::SLAVE_WAIT_START; display_.show("MASTER SEEN", "ACK SENT"); continue;
state_ = AppState::SLAVE_WAIT_START; display_.show(UiText::MASTER_SEEN, UiText::ACK_SENT); continue;
}
if (state_ == AppState::MASTER_DISCOVER && type == MessageType::DISCOVER_ACK && r.packet.session == session_) {
memcpy(peer_, r.mac, 6); havePeer_ = true; lastPeerSeenMs_ = lastHeartbeatMs_ = millis();
@@ -497,7 +523,7 @@ void App::handleRadio() {
if (pendingPacket_.passed) {
if (r.packet.passed) {
radio_.end(); pendingReason_ = FailReason::NONE;
if (armSlave(true)) display_.show("SLAVE PASS", "WAIT MASTER");
if (armSlave(true)) display_.show(UiText::PASS_WORD, UiText::WAIT_MASTER);
} else {
stageIndex_ = static_cast<uint32_t>(r.packet.stage) + 1U;
state_ = AppState::SLAVE_WAIT_START;
@@ -614,30 +640,30 @@ void App::finish(bool pass, FailReason reason, bool preserveDisplay) {
setActivePerformance(false);
lastUserActivityMs_ = millis();
if (slaveLinkLost) {
char target[12], one[24];
char target[12], one[64];
Display::formatTestFrequency(actual_.actualHz ? actual_.actualHz : requestedHz_, target, sizeof(target));
snprintf(one, sizeof(one), "FAIL %s %.0f%%", target,
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target,
actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct);
display_.show(one, failName(reason), stageIndex_ + 1, stageCount_);
display_.show(one, uiFailName(reason), stageIndex_ + 1, stageCount_);
armSlave(true);
return;
}
if (static_cast<Role>(settings_.role) == Role::SLAVE) slaveRearmAtMs_ = millis() + 2000;
if (preserveDisplay) return;
char one[24];
char one[64];
if (pass) {
const Role role = static_cast<Role>(settings_.role);
snprintf(one, sizeof(one), "%s PASS", roleName(role));
display_.show(one, role == Role::SLAVE ? "WAIT MASTER" : "START=AGAIN");
snprintf(one, sizeof(one), "%s %s", uiRoleName(role), UiText::PASS_WORD);
display_.show(one, role == Role::SLAVE ? UiText::WAIT_MASTER : UiText::START_AGAIN);
}
else if (requestedHz_) {
char frequency[12];
Display::formatTestFrequency(actual_.actualHz ? actual_.actualHz : requestedHz_, frequency, sizeof(frequency));
snprintf(one, sizeof(one), "FAIL %s %.0f%%", frequency,
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, frequency,
actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct);
display_.show(one, failName(reason), stageIndex_ + 1, stageCount_);
display_.show(one, uiFailName(reason), stageIndex_ + 1, stageCount_);
} else {
display_.show("TEST FAILED", failName(reason));
display_.show(UiText::TEST_FAILED, uiFailName(reason));
}
}
@@ -739,28 +765,28 @@ void App::printStageStats(const StageStats &s, uint32_t hz) {
}
void App::showStageResult(const StageStats &s) {
char one[24], two[24];
char one[64], two[64];
char target[12]; Display::formatTestFrequency(actual_.actualHz, target, sizeof(target));
if (s.reason != FailReason::NONE) {
snprintf(one, sizeof(one), "FAIL %s %.0f%%", target, actual_.actualDutyPct);
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target, actual_.actualDutyPct);
if (s.reason == FailReason::PERIOD_OUT && s.badFrequency > 0.0f) {
char frequency[12];
Display::formatTestFrequency(static_cast<uint32_t>(lroundf(s.badFrequency)), frequency, sizeof(frequency));
snprintf(two, sizeof(two), "PERIOD OUT %s", frequency);
snprintf(two, sizeof(two), UiText::PERIOD_OUT_FORMAT, frequency);
} else if (s.reason == FailReason::DUTY_OUT && s.badFrequency > 0.0f) {
char duty[10]; formatErrorDuty(s.badDuty, duty, sizeof(duty));
snprintf(two, sizeof(two), "DUTY OUT %s", duty);
snprintf(two, sizeof(two), UiText::DUTY_OUT_FORMAT, duty);
} else {
snprintf(two, sizeof(two), "%s", failName(s.reason));
snprintf(two, sizeof(two), "%s", uiFailName(s.reason));
}
display_.show(one, two, overallProgress(stageIndex_, measurement_.progressStep()),
overallProgressTotal(stageCount_));
return;
}
snprintf(one, sizeof(one), "Test:%-6s %2.0f%% %2lu/%2lu",
target, actual_.actualDutyPct, stageIndex_ + 1, stageCount_);
char stage[12]; snprintf(stage, sizeof(stage), "%lu/%lu", stageIndex_ + 1, stageCount_);
snprintf(one, sizeof(one), UiText::TEST_FORMAT, target, actual_.actualDutyPct, stage);
if (!s.periods || !s.periodSum) {
display_.show(one, "F:--- D:---%", overallProgress(stageIndex_, measurement_.progressStep()),
display_.show(one, UiText::NO_MEASUREMENT, overallProgress(stageIndex_, measurement_.progressStep()),
overallProgressTotal(stageCount_));
return;
}
@@ -775,29 +801,30 @@ void App::showStageResult(const StageStats &s) {
void App::showRemoteResult(const ProtocolPacket &packet) {
const FailReason reason = packet.reason <= static_cast<uint8_t>(FailReason::ABORTED)
? static_cast<FailReason>(packet.reason) : FailReason::UNSUPPORTED;
char target[12], one[24], two[24];
char target[12], one[64], two[64];
Display::formatTestFrequency(packet.actualHz ? packet.actualHz : packet.requestedHz,
target, sizeof(target));
if (reason == FailReason::NONE) {
snprintf(one, sizeof(one), "Test:%-6s %2.0f%% %2lu/%2lu",
target, packet.actualDutyX100 / 100.0f, stageIndex_ + 1, stageCount_);
char stage[12]; snprintf(stage, sizeof(stage), "%lu/%lu", stageIndex_ + 1, stageCount_);
snprintf(one, sizeof(one), UiText::TEST_FORMAT,
target, packet.actualDutyX100 / 100.0f, stage);
if (packet.measuredHzX10) {
char measured[12];
Display::formatFrequency(packet.measuredHzX10 / 10.0f, measured, sizeof(measured));
snprintf(two, sizeof(two), "F:%-8s D:%4.1f%%", measured, packet.measuredDutyX10 / 10.0f);
} else snprintf(two, sizeof(two), "F:--- D:---%%");
} else snprintf(two, sizeof(two), "%s", UiText::NO_MEASUREMENT);
} else if (reason == FailReason::PERIOD_OUT && packet.measuredHzX10) {
snprintf(one, sizeof(one), "FAIL %s %.0f%%", target, packet.actualDutyX100 / 100.0f);
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target, packet.actualDutyX100 / 100.0f);
char measured[12];
Display::formatTestFrequency((packet.measuredHzX10 + 5U) / 10U, measured, sizeof(measured));
snprintf(two, sizeof(two), "PERIOD OUT %s", measured);
snprintf(two, sizeof(two), UiText::PERIOD_OUT_FORMAT, measured);
} else if (reason == FailReason::DUTY_OUT && packet.measuredDutyX10) {
snprintf(one, sizeof(one), "FAIL %s %.0f%%", target, packet.actualDutyX100 / 100.0f);
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target, packet.actualDutyX100 / 100.0f);
char duty[10]; formatErrorDuty(packet.measuredDutyX10 / 10.0f, duty, sizeof(duty));
snprintf(two, sizeof(two), "DUTY OUT %s", duty);
snprintf(two, sizeof(two), UiText::DUTY_OUT_FORMAT, duty);
} else {
snprintf(one, sizeof(one), "FAIL %s %.0f%%", target, packet.actualDutyX100 / 100.0f);
snprintf(two, sizeof(two), "%s", failName(reason));
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target, packet.actualDutyX100 / 100.0f);
snprintf(two, sizeof(two), "%s", uiFailName(reason));
}
display_.show(one, two, overallProgress(stageIndex_, packet.progressStep),
overallProgressTotal(stageCount_));
@@ -817,10 +844,10 @@ void App::fillMeasuredResult(ProtocolPacket &packet, const StageStats &stats) co
}
void App::showStageProgress() {
char target[12], one[24];
char target[12], one[64], stage[12];
Display::formatTestFrequency(actual_.actualHz, target, sizeof(target));
snprintf(one, sizeof(one), "Test:%-6s %2.0f%% %2lu/%2lu",
target, actual_.actualDutyPct, stageIndex_ + 1, stageCount_);
display_.show(one, "F:--- D:---%", overallProgress(stageIndex_, 0),
snprintf(stage, sizeof(stage), "%lu/%lu", stageIndex_ + 1, stageCount_);
snprintf(one, sizeof(one), UiText::TEST_FORMAT, target, actual_.actualDutyPct, stage);
display_.show(one, UiText::NO_MEASUREMENT, overallProgress(stageIndex_, 0),
overallProgressTotal(stageCount_));
}

View File

@@ -0,0 +1,118 @@
#pragma once
// Select the language used by the OLED interface.
// Build with UI_LANGUAGE_EN for English or UI_LANGUAGE_RU for Russian.
#define UI_LANGUAGE_EN 0
#define UI_LANGUAGE_RU 1
#ifndef UI_LANGUAGE
#define UI_LANGUAGE UI_LANGUAGE_RU
#endif
namespace UiText {
#if UI_LANGUAGE == UI_LANGUAGE_RU
constexpr const char *ROLE_NAMES[] = {
"СОЛО", "МАСТЕР", "СЛЕЙВ"
};
constexpr const char *FAIL_NAMES[] = {
"НЕТ ОШИБКИ",
"НЕТ СИГНАЛА",
"ПЕРИОД ВНЕ ДОПУСКА",
"ЗАПОЛН. ВНЕ ДОПУСКА",
"ЛИШНИЙ ФРОНТ",
"ИМПУЛЬСНАЯ ПОМЕХА",
"ПРОПУЩЕН ФРОНТ",
"ОШИБКА ПОТЕРИ ДАННЫХ",
"СВЯЗЬ ПОТЕРЯНА",
"РЕЖИМ НЕ ПОДДЕРЖИВ.",
"НЕ ХВАТАЕТ ТОЧНОСТИ",
"ТЕСТ ОСТАНОВЛЕН"
};
constexpr const char *MODE_PREFIX = "РЕЖИМ: ";
constexpr const char *START_RUN = "ГОТОВ К ЗАПУСКУ";
constexpr const char *MENU_START_FREQUENCY = "ЧАСТОТА ОТ:";
constexpr const char *MENU_END_FREQUENCY = "ЧАСТОТА ДО:";
constexpr const char *MENU_ACCURACY = "ТОЧНОСТЬ:";
constexpr const char *MENU_TEST_TIME = "ВРЕМЯ ВЫБОРКИ:";
constexpr const char *MENU_PWM_DUTY = "ЗАПОЛНЕНИЕ:";
constexpr const char *MENU_TOTAL_TIME = "ОБЩЕЕ ВРЕМЯ:";
constexpr const char *FREQUENCY_UNIT = " Гц";
constexpr const char *SLAVE_READY = "СЛЕЙВ ГОТОВ";
constexpr const char *WAIT_MASTER = "ОЖИДАНИЕ МАСТЕРА";
constexpr const char *LINK_FAILED = "СВЯЗЬ НЕ УСТАНОВЛЕНА";
constexpr const char *RADIO_ERROR = "ОШИБКА СВЯЗИ";
constexpr const char *MASTER_SEARCH = "ПОИСК СЛЕЙВА";
constexpr const char *HOLD_START_STOP = "УДЕРЖ. START ДЛЯ СТОП";
constexpr const char *MASTER_SEEN = "МАСТЕР ОБНАРУЖЕН";
constexpr const char *ACK_SENT = "ОТВЕТ ОТПРАВЛЕН";
constexpr const char *START_AGAIN = "ГОТОВ К ЗАПУСКУ";
constexpr const char *TEST_FAILED = "ТЕСТ НЕ ПРОЙДЕН";
constexpr const char *PASS_WORD = "ТЕСТ ПРОЙДЕН";
constexpr const char *FAIL_FORMAT = "СБОЙ %s %.0f%%";
constexpr const char *TEST_FORMAT = "Тест:%-6s %2.0f%% %5s";
constexpr const char *PERIOD_OUT_FORMAT = "ОШИБКА ЧАСТОТЫ %s";
constexpr const char *DUTY_OUT_FORMAT = "ОШИБКА ЗАПОЛН. %s";
constexpr const char *NO_MEASUREMENT = "F:--- D:---%";
#elif UI_LANGUAGE == UI_LANGUAGE_EN
constexpr const char *ROLE_NAMES[] = {
"SOLO", "MASTER", "SLAVE"
};
constexpr const char *FAIL_NAMES[] = {
"NONE",
"NO SIGNAL",
"PERIOD OUT",
"DUTY OUT",
"EXTRA EDGE",
"GLITCH",
"LOST EDGE",
"DATA LOSS ERROR",
"LINK LOST",
"UNSUPPORTED",
"RESOLUTION",
"ABORTED"
};
constexpr const char *MODE_PREFIX = "MODE: ";
constexpr const char *START_RUN = "READY TO START";
constexpr const char *MENU_START_FREQUENCY = "START FREQ:";
constexpr const char *MENU_END_FREQUENCY = "END FREQ:";
constexpr const char *MENU_ACCURACY = "ACCURACY:";
constexpr const char *MENU_TEST_TIME = "TEST TIME:";
constexpr const char *MENU_PWM_DUTY = "PWM DUTY:";
constexpr const char *MENU_TOTAL_TIME = "TOTAL TIME:";
constexpr const char *FREQUENCY_UNIT = " Hz";
constexpr const char *SLAVE_READY = "SLAVE READY";
constexpr const char *WAIT_MASTER = "WAIT MASTER";
constexpr const char *LINK_FAILED = "LINK FAILED";
constexpr const char *RADIO_ERROR = "RADIO ERROR";
constexpr const char *MASTER_SEARCH = "MASTER SEARCH";
constexpr const char *HOLD_START_STOP = "HOLD START TO STOP";
constexpr const char *MASTER_SEEN = "MASTER SEEN";
constexpr const char *ACK_SENT = "ACK SENT";
constexpr const char *START_AGAIN = "READY TO START";
constexpr const char *TEST_FAILED = "TEST FAILED";
constexpr const char *PASS_WORD = "TEST PASS";
constexpr const char *FAIL_FORMAT = "FAIL %s %.0f%%";
constexpr const char *TEST_FORMAT = "Test:%-6s %2.0f%% %5s";
constexpr const char *PERIOD_OUT_FORMAT = "PERIOD OUT %s";
constexpr const char *DUTY_OUT_FORMAT = "DUTY OUT %s";
constexpr const char *NO_MEASUREMENT = "F:--- D:---%";
#else
#error "UI_LANGUAGE must be UI_LANGUAGE_EN or UI_LANGUAGE_RU"
#endif
} // namespace UiText

View File

@@ -11,7 +11,7 @@ const char *roleName(Role r) {
const char *failName(FailReason r) {
static const char *names[] = {"NONE", "NO SIGNAL", "PERIOD OUT", "DUTY OUT",
"EXTRA EDGE", "GLITCH", "LOST EDGE", "TOO FEW PERIODS", "LINK LOST",
"EXTRA EDGE", "GLITCH", "LOST EDGE", "DATA LOSS ERROR", "LINK LOST",
"UNSUPPORTED", "RESOLUTION", "ABORTED"};
const uint8_t i = static_cast<uint8_t>(r);
return i < (sizeof(names) / sizeof(names[0])) ? names[i] : "UNKNOWN";

View File

@@ -6,7 +6,7 @@
enum class Role : uint8_t { SOLO, MASTER, SLAVE };
enum class FailReason : uint8_t {
NONE, NO_SIGNAL, PERIOD_OUT, DUTY_OUT, EXTRA_EDGE, GLITCH, LOST_EDGE,
TOO_FEW_PERIODS, LINK_LOST, UNSUPPORTED, RESOLUTION, ABORTED
DATA_LOSS, LINK_LOST, UNSUPPORTED, RESOLUTION, ABORTED
};
const char *roleName(Role role);

View File

@@ -1,10 +1,26 @@
#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() {
@@ -37,25 +53,38 @@ void Display::setPower(bool enabled) {
Log::printf("OLED", "display power %s", enabled ? "ON" : "OFF");
}
void Display::fit(char *s) {
int16_t x, y; uint16_t w, h;
while (*s) {
oled_.getTextBounds(s, 0, 0, &x, &y, &w, &h);
if (w <= 128) break;
s[strlen(s) - 1] = '\0';
void Display::drawTextLine(const char *text, int16_t y) {
int16_t x = 0;
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) {
char one[32], two[32];
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);
fit(one); fit(two);
oled_.clearDisplay(); oled_.setCursor(0, 3); oled_.print(one);
oled_.setCursor(0, 19); oled_.print(two);
oled_.clearDisplay();
drawTextLine(one, 3);
drawTextLine(two, 19);
if (progressTotal) {
if (progress > progressTotal) progress = progressTotal;
const uint16_t width = static_cast<uint16_t>(

View File

@@ -16,7 +16,7 @@ class Display {
static void formatTestFrequency(uint32_t hz, char *out, size_t size);
static void formatDuration(uint64_t us, char *out, size_t size);
private:
void fit(char *text);
void drawTextLine(const char *text, int16_t y);
Adafruit_SSD1306 oled_;
bool ok_ = false;
bool powered_ = false;

View File

@@ -0,0 +1,57 @@
#pragma once
#include <Arduino.h>
// Compact 5x7 uppercase Cyrillic font. Lowercase UTF-8 letters are deliberately
// rendered with the same uppercase glyphs: at 5x7 pixels this is considerably
// clearer than trying to preserve lowercase shapes.
namespace CyrillicFont {
constexpr uint8_t WIDTH = 5;
constexpr uint8_t HEIGHT = 7;
constexpr uint8_t ADVANCE = 6;
// Rows are stored top-to-bottom; the low five bits are pixels left-to-right.
const uint8_t GLYPHS[][HEIGHT] PROGMEM = {
{0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11}, // А
{0x1F, 0x10, 0x10, 0x1E, 0x11, 0x11, 0x1E}, // Б
{0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E}, // В
{0x1F, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10}, // Г
{0x0E, 0x0A, 0x0A, 0x0A, 0x11, 0x1F, 0x11}, // Д
{0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x1F}, // Е
{0x15, 0x15, 0x0E, 0x04, 0x0E, 0x15, 0x15}, // Ж
{0x0E, 0x11, 0x01, 0x06, 0x01, 0x11, 0x0E}, // З
{0x11, 0x13, 0x15, 0x15, 0x19, 0x11, 0x11}, // И
{0x0A, 0x11, 0x13, 0x15, 0x19, 0x11, 0x11}, // Й
{0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11}, // К
{0x07, 0x09, 0x09, 0x09, 0x11, 0x11, 0x11}, // Л
{0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11}, // М
{0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11}, // Н
{0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E}, // О
{0x1F, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11}, // П
{0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10}, // Р
{0x0F, 0x10, 0x10, 0x10, 0x10, 0x10, 0x0F}, // С
{0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04}, // Т
{0x11, 0x11, 0x11, 0x0F, 0x01, 0x11, 0x0E}, // У
{0x04, 0x0E, 0x15, 0x15, 0x0E, 0x04, 0x04}, // Ф
{0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11}, // Х
{0x12, 0x12, 0x12, 0x12, 0x12, 0x1E, 0x01}, // Ц
{0x11, 0x11, 0x11, 0x0F, 0x01, 0x01, 0x01}, // Ч
{0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x1F}, // Ш
{0x15, 0x15, 0x15, 0x15, 0x15, 0x1E, 0x01}, // Щ
{0x18, 0x08, 0x08, 0x0E, 0x09, 0x09, 0x0E}, // Ъ
{0x11, 0x11, 0x11, 0x1D, 0x15, 0x15, 0x1D}, // Ы
{0x10, 0x10, 0x10, 0x1E, 0x11, 0x11, 0x1E}, // Ь
{0x0E, 0x11, 0x01, 0x07, 0x01, 0x11, 0x0E}, // Э
{0x12, 0x15, 0x15, 0x1D, 0x15, 0x15, 0x12}, // Ю
{0x0F, 0x11, 0x11, 0x0F, 0x05, 0x09, 0x11} // Я
};
inline const uint8_t *glyph(uint32_t codepoint) {
if (codepoint >= 0x0430U && codepoint <= 0x044FU) codepoint -= 0x20U;
if (codepoint == 0x0401U || codepoint == 0x0451U) codepoint = 0x0415U; // Ё -> Е
if (codepoint < 0x0410U || codepoint > 0x042FU) return nullptr;
return GLYPHS[codepoint - 0x0410U];
}
} // namespace CyrillicFont

View File

@@ -57,7 +57,7 @@ void Measurement::completeMeasurement() {
return;
}
if (!stats_.periods) {
fail(FailReason::TOO_FEW_PERIODS); return;
fail(FailReason::DATA_LOSS); return;
}
state_ = MeasureState::PASS;
}

File diff suppressed because one or more lines are too long