Сделан русский текст
This commit is contained in:
@@ -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_));
|
||||
}
|
||||
|
||||
118
OpticalChannelTester/Config_Text.h
Normal file
118
OpticalChannelTester/Config_Text.h
Normal 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
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -14,9 +14,9 @@ class Display {
|
||||
bool powered() const { return powered_; }
|
||||
static void formatFrequency(float hz, char *out, size_t size);
|
||||
static void formatTestFrequency(uint32_t hz, char *out, size_t size);
|
||||
static void formatDuration(uint64_t us, 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;
|
||||
|
||||
57
OpticalChannelTester/Font_Cyrillic.h
Normal file
57
OpticalChannelTester/Font_Cyrillic.h
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user