Добавлено тестировние платы в меню:

- Тест Tx Оптики
- Тест Rx Оптики
- Тест АЦП
This commit is contained in:
2026-08-22 20:03:12 +03:00
parent 49332aa028
commit 3907cbcf77
9 changed files with 357 additions and 115 deletions

View File

@@ -17,7 +17,7 @@ namespace {
const char *uiFailName(FailReason reason);
const char *appStateName(AppState state) {
static const char *names[] = {"IDLE", "MENU", "SOLO_MEASURE", "SOLO_DRIVER", "MASTER_DISCOVER",
static const char *names[] = {"IDLE", "MENU", "BOARD_TEST", "SOLO_MEASURE", "SOLO_DRIVER", "MASTER_DISCOVER",
"MASTER_WAIT_READY", "MASTER_WAIT_RESULT", "MASTER_FINALIZE", "SLAVE_READY", "SLAVE_WAIT_START",
"SLAVE_MEASURE", "SLAVE_WAIT_ACK", "FINISHED"};
const uint8_t index = static_cast<uint8_t>(state);
@@ -145,6 +145,30 @@ const char *uiTestName(TestKind kind) {
? UiText::TEST_NAMES[index] : "?";
}
const char *uiTestGroupName(TestGroup group) {
const uint8_t index = static_cast<uint8_t>(group);
return index < sizeof(UiText::TEST_GROUP_NAMES) / sizeof(UiText::TEST_GROUP_NAMES[0])
? UiText::TEST_GROUP_NAMES[index] : "?";
}
const char *uiBoardTestName(BoardTest test) {
const uint8_t index = static_cast<uint8_t>(test);
return index < sizeof(UiText::BOARD_TEST_NAMES) / sizeof(UiText::BOARD_TEST_NAMES[0])
? UiText::BOARD_TEST_NAMES[index] : "?";
}
bool boardTestAvailable(BoardTest test) {
if (test == BoardTest::PWM_OUTPUT) return true;
if (test == BoardTest::ADC) return BOARD_RX_USES_ADC && BOARD_ADC_AVAILABLE;
if (test == BoardTest::RX_INPUT) return !BOARD_RX_USES_ADC;
return false;
}
BoardTest defaultBoardTest() {
return BOARD_RX_USES_ADC && BOARD_ADC_AVAILABLE
? BoardTest::ADC : BoardTest::RX_INPUT;
}
uint8_t lastValidMaxPulseIndex(uint32_t frequencyHz) {
uint8_t last = static_cast<uint8_t>(countOf(MAX_PULSE_OPTIONS_NS) - 1U);
while (last && static_cast<uint64_t>(MAX_PULSE_OPTIONS_NS[last]) * frequencyHz >= 1000000000ULL)
@@ -290,7 +314,8 @@ void App::finishInitialization(bool factoryReset) {
lastUserActivityMs_ = millis();
setActivePerformance(false);
printConfiguration();
if (static_cast<Role>(settings_.role) == Role::SLAVE) armSlave();
if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::OPTICS &&
static_cast<Role>(settings_.role) == Role::SLAVE) armSlave();
else showIdle();
}
@@ -313,7 +338,15 @@ void App::update() {
else if (now - bootCheckStartedMs_ >= FACTORY_RESET_HOLD_MS) finishInitialization(true);
return;
}
serviceRxPinStateLog();
if (state_ == AppState::BOARD_TEST) {
if (startEvent == ButtonEvent::LONG) stopBoardTest();
else {
if (modeEvent != ButtonEvent::NONE)
Log::event("ACTION", "MODE ignored while board test is active");
updateBoardTest(now);
}
return;
}
if (state_ != AppState::IDLE && state_ != AppState::MENU && state_ != AppState::FINISHED &&
startEvent == ButtonEvent::LONG) { abortTest(); return; }
if (state_ != AppState::IDLE && state_ != AppState::MENU && state_ != AppState::FINISHED &&
@@ -323,12 +356,16 @@ void App::update() {
if (modeEvent == ButtonEvent::SHORT) {
cycleRunMode(); sanitizeRange(); const bool saved = store_.save(settings_);
params_ = store_.params(settings_);
if (static_cast<Role>(settings_.role) == Role::SLAVE) armSlave();
if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::OPTICS &&
static_cast<Role>(settings_.role) == Role::SLAVE) armSlave();
else showIdle();
Log::printf("ACTION", "mode changed to %s/%s, NVS=%s",
roleName(static_cast<Role>(settings_.role)),
testKindName(static_cast<TestKind>(settings_.testKind)),
saved ? "OK" : "FAILED");
if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::BOARD)
Log::printf("ACTION", "board test changed to %s, NVS=%s",
boardTestName(static_cast<BoardTest>(settings_.boardTest)), saved ? "OK" : "FAILED");
else
Log::printf("ACTION", "mode changed to %s/%s, NVS=%s",
roleName(static_cast<Role>(settings_.role)),
testKindName(static_cast<TestKind>(settings_.testKind)), saved ? "OK" : "FAILED");
} else if (modeEvent == ButtonEvent::LONG) {
state_ = AppState::MENU; menuItem_ = 0; Log::event("ACTION", "settings menu entered"); showMenu();
} else if (startEvent == ButtonEvent::SHORT) { Log::event("ACTION", "test start requested"); startTest(); }
@@ -351,13 +388,15 @@ void App::update() {
}
if (state_ == AppState::MENU) {
if (modeEvent == ButtonEvent::SHORT) {
menuItem_ = (menuItem_ + 1U) % 6U; Log::printf("ACTION", "menu item selected index=%u", menuItem_); showMenu();
const uint8_t menuCount = static_cast<TestGroup>(settings_.testGroup) == TestGroup::BOARD ? 1U : 7U;
menuItem_ = (menuItem_ + 1U) % menuCount; Log::printf("ACTION", "menu item selected index=%u", menuItem_); showMenu();
}
else if (modeEvent == ButtonEvent::LONG) {
sanitizeRange(); const bool saved = store_.save(settings_); params_ = store_.params(settings_);
Log::printf("ACTION", "settings menu saved and closed, NVS=%s", saved ? "OK" : "FAILED");
state_ = AppState::IDLE; printConfiguration();
if (static_cast<Role>(settings_.role) == Role::SLAVE) armSlave();
if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::OPTICS &&
static_cast<Role>(settings_.role) == Role::SLAVE) armSlave();
else showIdle();
} else if (startEvent == ButtonEvent::SHORT) changeMenu(+1);
else if (startEvent == ButtonEvent::LONG || startEvent == ButtonEvent::REPEAT) changeMenu(-1);
@@ -429,10 +468,17 @@ void App::showIdle() {
setActivePerformance(false);
setStandbyOpticalOutput();
lastUserActivityMs_ = millis();
char one[64]; snprintf(one, sizeof(one), "%s: %s",
uiRoleName(static_cast<Role>(settings_.role)),
uiTestName(static_cast<TestKind>(settings_.testKind)));
display_.show(one, UiText::START_RUN);
char one[64];
if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::BOARD) {
snprintf(one, sizeof(one), "%s: %s", uiTestGroupName(TestGroup::BOARD),
uiBoardTestName(static_cast<BoardTest>(settings_.boardTest)));
display_.show(one, UiText::BOARD_READY);
} else {
snprintf(one, sizeof(one), "%s: %s",
uiRoleName(static_cast<Role>(settings_.role)),
uiTestName(static_cast<TestKind>(settings_.testKind)));
display_.show(one, UiText::START_RUN);
}
}
void App::serviceSerialConsole() {
@@ -464,6 +510,8 @@ void App::serviceSerialConsole() {
void App::printSerialHelp() {
Serial.println("COMMANDS (send with newline):");
Serial.println(" help | status | start | stop | defaults");
Serial.println(" set group optics|board");
Serial.println(" set boardtest adc|pwm|rx");
Serial.println(" set role solo|master|slave");
Serial.println(" set test optical|driver");
Serial.println(" set frequency 500|1000|2000|5000|10000|25000");
@@ -480,8 +528,9 @@ void App::printSerialStatus() {
return;
}
params_ = store_.params(settings_);
Serial.printf("STATUS state=%s role=%s test=%s frequency=%luHz max=%luns min=%luns accuracy=%.2f%% time=%lums light=%s usb=%s\n",
appStateName(state_), roleName(static_cast<Role>(settings_.role)),
Serial.printf("STATUS state=%s group=%s boardtest=%s role=%s test=%s frequency=%luHz max=%luns min=%luns accuracy=%.2f%% time=%lums light=%s usb=%s\n",
appStateName(state_), testGroupName(static_cast<TestGroup>(settings_.testGroup)),
boardTestName(static_cast<BoardTest>(settings_.boardTest)), roleName(static_cast<Role>(settings_.role)),
testKindName(static_cast<TestKind>(settings_.testKind)), params_.frequencyHz,
params_.maxPulseNs, params_.minPulseNs, params_.accuracyPct, params_.testTimeMs,
configuredLevelName(settings_),
@@ -501,7 +550,8 @@ void App::finishSerialSettingsChange() {
pwm_.configureActiveLight(configuredTxPulseLightOn(settings_));
const bool saved = store_.save(settings_);
Serial.printf("OK settings saved=%s\n", saved ? "yes" : "no");
if (static_cast<Role>(settings_.role) == Role::SLAVE) armSlave();
if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::OPTICS &&
static_cast<Role>(settings_.role) == Role::SLAVE) armSlave();
else showIdle();
printSerialStatus();
}
@@ -535,7 +585,10 @@ void App::handleSerialCommand(char *line) {
if ((!strcmp(command, "stop") || !strcmp(command, "abort")) && !name) {
if (!initialized_) Serial.println("ERR still initializing");
else if (state_ == AppState::IDLE || state_ == AppState::FINISHED) Serial.println("OK already stopped");
else if (state_ == AppState::MENU) {
else if (state_ == AppState::BOARD_TEST) {
Serial.println("OK board test stopped");
stopBoardTest();
} else if (state_ == AppState::MENU) {
state_ = AppState::IDLE; showIdle(); Serial.println("OK menu closed");
} else if (state_ == AppState::SLAVE_READY) Serial.println("OK slave is armed; no test is running");
else {
@@ -564,7 +617,22 @@ void App::handleSerialCommand(char *line) {
bool accepted = false;
uint32_t numeric = 0;
if (!strcmp(name, "role")) {
if (!strcmp(name, "group")) {
if (!strcmp(value, "optics") || !strcmp(value, "optical")) {
settings_.testGroup = static_cast<uint8_t>(TestGroup::OPTICS); accepted = true;
} else if (!strcmp(value, "board")) {
settings_.testGroup = static_cast<uint8_t>(TestGroup::BOARD); accepted = true;
}
} else if (!strcmp(name, "boardtest") || !strcmp(name, "board")) {
BoardTest selected = static_cast<BoardTest>(UINT8_MAX);
if (!strcmp(value, "adc")) selected = BoardTest::ADC;
else if (!strcmp(value, "pwm")) selected = BoardTest::PWM_OUTPUT;
else if (!strcmp(value, "rx")) selected = BoardTest::RX_INPUT;
if (boardTestAvailable(selected)) {
settings_.boardTest = static_cast<uint8_t>(selected);
accepted = true;
}
} else if (!strcmp(name, "role")) {
if (!strcmp(value, "solo")) { settings_.role = static_cast<uint8_t>(Role::SOLO); accepted = true; }
else if (!strcmp(value, "master")) { settings_.role = static_cast<uint8_t>(Role::MASTER); accepted = true; }
else if (!strcmp(value, "slave")) { settings_.role = static_cast<uint8_t>(Role::SLAVE); accepted = true; }
@@ -608,6 +676,13 @@ void App::handleSerialCommand(char *line) {
}
void App::cycleRunMode() {
if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::BOARD) {
do {
settings_.boardTest = (settings_.boardTest + 1U) %
(static_cast<uint8_t>(BoardTest::RX_INPUT) + 1U);
} while (!boardTestAvailable(static_cast<BoardTest>(settings_.boardTest)));
return;
}
const Role role = static_cast<Role>(settings_.role);
const TestKind kind = static_cast<TestKind>(settings_.testKind);
if (role == Role::SOLO && kind == TestKind::OPTICAL && !TARGET_IS_C3) {
@@ -625,6 +700,11 @@ void App::cycleRunMode() {
}
void App::sanitizeRange() {
if (settings_.testGroup > static_cast<uint8_t>(TestGroup::BOARD))
settings_.testGroup = static_cast<uint8_t>(TestGroup::OPTICS);
if (settings_.boardTest > static_cast<uint8_t>(BoardTest::RX_INPUT) ||
!boardTestAvailable(static_cast<BoardTest>(settings_.boardTest)))
settings_.boardTest = static_cast<uint8_t>(defaultBoardTest());
if (settings_.role > static_cast<uint8_t>(Role::SLAVE))
settings_.role = static_cast<uint8_t>(Role::SOLO);
if (settings_.testKind > static_cast<uint8_t>(TestKind::DRIVER))
@@ -654,29 +734,19 @@ void App::sanitizeRange() {
}
}
void App::serviceRxPinStateLog() {
#ifdef RX_PIN_CHANGE_TEST
const bool level = digitalRead(GPIO_RX) == HIGH;
const bool outsideTest = state_ == AppState::IDLE || state_ == AppState::MENU ||
state_ == AppState::SLAVE_READY || state_ == AppState::FINISHED;
if (rxPinStateKnown_ && level != rxPinState_ && outsideTest)
Log::printf("RX TEST", "GPIO=%u state=%s (%u)", GPIO_RX,
level ? "HIGH" : "LOW", level ? 1U : 0U);
rxPinState_ = level;
rxPinStateKnown_ = true;
#endif
}
void App::changeMenu(int d) {
sanitizeRange();
if (menuItem_ == 1) {
if (menuItem_ == 0) {
settings_.testGroup = cycleIndex(settings_.testGroup, 0,
static_cast<uint8_t>(TestGroup::BOARD), d);
} else if (menuItem_ == 2) {
const uint8_t last = lastValidMaxPulseIndex(
PWM_FREQUENCY_OPTIONS_HZ[settings_.frequencyIndex]);
const uint8_t first = firstMaxPulseIndexAtLeast(
MIN_PULSE_OPTIONS_NS[settings_.minPulseIndex], last);
settings_.maxPulseIndex = cycleIndex(settings_.maxPulseIndex,
first, last, d);
} else if (menuItem_ == 2) {
} else if (menuItem_ == 3) {
const uint8_t last = lastMinPulseIndexAtMost(
MAX_PULSE_OPTIONS_NS[settings_.maxPulseIndex]);
const uint8_t first = static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER
@@ -685,10 +755,10 @@ void App::changeMenu(int d) {
} else {
uint8_t *value = nullptr; size_t count = 0;
switch (menuItem_) {
case 0: value = &settings_.frequencyIndex; count = countOf(PWM_FREQUENCY_OPTIONS_HZ); break;
case 3: value = &settings_.accuracyIndex; count = countOf(ACCURACY_OPTIONS_PCT); break;
case 4: value = &settings_.timeIndex; count = countOf(TEST_TIME_OPTIONS_MS); break;
case 5:
case 1: value = &settings_.frequencyIndex; count = countOf(PWM_FREQUENCY_OPTIONS_HZ); break;
case 4: value = &settings_.accuracyIndex; count = countOf(ACCURACY_OPTIONS_PCT); break;
case 5: value = &settings_.timeIndex; count = countOf(TEST_TIME_OPTIONS_MS); break;
case 6:
if (static_cast<TestKind>(settings_.testKind) != TestKind::DRIVER) {
showMenu();
return;
@@ -713,26 +783,31 @@ void App::showMenu() {
Display::formatDuration(actualNominalTotalUs(), all, sizeof(all));
switch (menuItem_) {
case 0:
snprintf(value, sizeof(value), "%s",
uiTestGroupName(static_cast<TestGroup>(settings_.testGroup)));
label = UiText::MENU_TEST_GROUP;
break;
case 1:
Display::formatPwmFrequency(params_.frequencyHz, value, sizeof(value));
label = UiText::MENU_FREQUENCY;
break;
case 1:
case 2:
Display::formatPulse(params_.maxPulseNs, value, sizeof(value));
label = UiText::MENU_MAX_PULSE;
break;
case 2:
case 3:
Display::formatPulse(params_.minPulseNs, value, sizeof(value));
label = UiText::MENU_MIN_PULSE;
break;
case 3:
case 4:
snprintf(value, sizeof(value), "+/-%g%%", params_.accuracyPct);
label = UiText::MENU_ACCURACY;
break;
case 4:
case 5:
snprintf(value, sizeof(value), "%.1fs", params_.testTimeMs / 1000.0f);
label = UiText::MENU_TEST_TIME;
break;
case 5: {
case 6: {
if (static_cast<TestKind>(settings_.testKind) != TestKind::DRIVER) {
snprintf(value, sizeof(value), "%s", UiText::LIGHT_AUTO);
label = UiText::MENU_LIGHT_CODE;
@@ -751,7 +826,10 @@ void App::showMenu() {
default: return;
}
formatMenuLine(label, value, one, sizeof(one));
formatMenuLine(UiText::MENU_TOTAL_TIME, all, total, sizeof(total));
if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::BOARD)
snprintf(total, sizeof(total), "%s", UiText::BOARD_READY);
else
formatMenuLine(UiText::MENU_TOTAL_TIME, all, total, sizeof(total));
display_.show(one, total);
}
@@ -760,6 +838,10 @@ void App::startTest() {
pwm_.stop();
setActivePerformance(true);
sanitizeRange();
if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::BOARD) {
startBoardTest();
return;
}
params_ = store_.params(settings_); stageCount_ = pulseWidthPointCount(params_.maxPulseNs, params_.minPulseNs);
stageIndex_ = 0; requestedHz_ = params_.frequencyHz; requestedPulseNs_ = 0; pendingReason_ = FailReason::NONE;
havePeer_ = false; lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0;
@@ -787,6 +869,98 @@ void App::startTest() {
else { state_ = AppState::SLAVE_READY; Log::event("TEST", "Slave armed and waiting for Master"); display_.show(UiText::SLAVE_READY, UiText::WAIT_MASTER); }
}
void App::startBoardTest() {
const BoardTest test = static_cast<BoardTest>(settings_.boardTest);
state_ = AppState::BOARD_TEST;
boardTestUpdatedMs_ = boardDisplayUpdatedMs_ = 0;
boardPwmLightOn_ = false;
rxPinStateKnown_ = false;
Log::printf("BOARD", "starting test=%s", boardTestName(test));
// ADC and RX checks need the optical transmitter continuously illuminated.
// The PWM-output check drives the same pin itself and therefore replaces
// the constant active level with its test waveform.
if (test != BoardTest::PWM_OUTPUT) {
pwm_.lightOn();
Log::printf("BOARD", "optical output active GPIO=%u level=%s", GPIO_PWM,
TX_LIGHT_ON_GPIO_LEVEL == HIGH ? "HIGH" : "LOW");
}
if (test == BoardTest::ADC) {
if (!BOARD_ADC_AVAILABLE) {
Serial.println("BOARD ADC: unavailable in MAKETKA profile");
display_.show(UiText::ADC_UNAVAILABLE, UiText::BOARD_STOP);
return;
}
pinMode(GPIO_ANALOG_RX, INPUT);
pinMode(GPIO_VBAT, INPUT);
display_.show(UiText::BOARD_TEST_NAMES[static_cast<uint8_t>(BoardTest::ADC)],
UiText::BOARD_STOP);
} else if (test == BoardTest::PWM_OUTPUT) {
boardPwmLightOn_ = true;
boardTestUpdatedMs_ = millis();
pwm_.lightOn();
Serial.printf("BOARD PWM: GPIO=%u frequency=%luHz duty=50%% light=ON\n",
GPIO_PWM, BOARD_PWM_TEST_FREQUENCY_HZ);
display_.show(UiText::BOARD_TEST_NAMES[static_cast<uint8_t>(test)], UiText::BOARD_STOP);
} else {
pinMode(GPIO_RX, INPUT);
display_.show(UiText::BOARD_TEST_NAMES[static_cast<uint8_t>(test)], UiText::BOARD_STOP);
}
}
void App::updateBoardTest(uint32_t now) {
const BoardTest test = static_cast<BoardTest>(settings_.boardTest);
if (test == BoardTest::ADC) {
if (!BOARD_ADC_AVAILABLE || now - boardTestUpdatedMs_ < BOARD_ADC_PRINT_INTERVAL_MS) return;
boardTestUpdatedMs_ = now;
const uint16_t analogRx = analogRead(GPIO_ANALOG_RX);
const uint16_t vbat = analogRead(GPIO_VBAT);
Serial.printf("ADC analog_rx=%u vbat=%u\n", analogRx, vbat);
if (now - boardDisplayUpdatedMs_ >= BOARD_TEST_DISPLAY_INTERVAL_MS) {
boardDisplayUpdatedMs_ = now;
char one[32], two[32];
snprintf(one, sizeof(one), "ADC RX: %u", analogRx);
snprintf(two, sizeof(two), "ADC VBAT: %u", vbat);
display_.show(one, two);
}
return;
}
if (test == BoardTest::PWM_OUTPUT) {
if (now - boardTestUpdatedMs_ < BOARD_PWM_TEST_HALF_PERIOD_MS) return;
boardTestUpdatedMs_ = now;
boardPwmLightOn_ = !boardPwmLightOn_;
if (boardPwmLightOn_) pwm_.lightOn();
else pwm_.stop();
Serial.printf("BOARD PWM: light=%s\n", boardPwmLightOn_ ? "ON" : "OFF");
char two[32];
snprintf(two, sizeof(two), "%luHz 50%%: %s", BOARD_PWM_TEST_FREQUENCY_HZ,
boardPwmLightOn_ ? "ON" : "OFF");
display_.show(UiText::BOARD_TEST_NAMES[static_cast<uint8_t>(test)], two);
return;
}
const bool level = digitalRead(GPIO_RX) == HIGH;
if (!rxPinStateKnown_ || level != rxPinState_) {
rxPinState_ = level;
rxPinStateKnown_ = true;
Serial.printf("RX GPIO=%u state=%s (%u)\n", GPIO_RX,
level ? "HIGH" : "LOW", level ? 1U : 0U);
char two[32];
snprintf(two, sizeof(two), "GPIO %u: %s", GPIO_RX, level ? "HIGH" : "LOW");
display_.show(UiText::BOARD_TEST_NAMES[static_cast<uint8_t>(test)], two);
}
}
void App::stopBoardTest() {
Log::printf("BOARD", "stopping test=%s", boardTestName(static_cast<BoardTest>(settings_.boardTest)));
pwm_.stop();
state_ = AppState::IDLE;
setActivePerformance(false);
showIdle();
}
bool App::armSlave(bool preserveDisplay) {
setActivePerformance(false);
pwm_.stop();
@@ -1291,13 +1465,15 @@ bool App::usbHostPresent() const {
void App::setStandbyOpticalOutput() {
// A gate driver must never be held enabled while the tester is idle or
// showing a result. DRIVER is SOLO-only, so force real light OFF here.
if (static_cast<Role>(settings_.role) == Role::SLAVE ||
if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::BOARD ||
static_cast<Role>(settings_.role) == Role::SLAVE ||
static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER) pwm_.stop();
else pwm_.active();
}
void App::setActivePerformance(bool active) {
const bool driverMode = initialized_ &&
static_cast<TestGroup>(settings_.testGroup) == TestGroup::OPTICS &&
static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER;
const uint32_t targetMhz = active ? (driverMode ? 240U : 160U) : 80U;
if (getCpuFrequencyMhz() != targetMhz && !setCpuFrequencyMhz(targetMhz))
@@ -1406,7 +1582,15 @@ void App::printConfiguration() {
if (SERIAL_MINIMAL_LOG) return;
const char *board = TARGET_IS_C3 ? "ESP32-C3" : "ESP32-S3";
uint8_t mac[6] = {}; esp_read_mac(mac, ESP_MAC_WIFI_STA);
Serial.printf("\nOptical Channel Tester | %s | mode=%s/%s | light=%s\n", board,
if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::BOARD) {
Serial.printf("\nOptical Channel Tester | %s | group=BOARD | test=%s\n", board,
boardTestName(static_cast<BoardTest>(settings_.boardTest)));
Serial.printf("GPIO PWM=%u RX=%u ANALOG_RX=%u VBAT=%u START=%u MODE=%u SDA=%u SCL=%u\n",
GPIO_PWM, GPIO_RX, GPIO_ANALOG_RX, GPIO_VBAT, GPIO_BUTTON_START,
GPIO_BUTTON_MODE, GPIO_SDA, GPIO_SCL);
return;
}
Serial.printf("\nOptical Channel Tester | %s | group=OPTICS | mode=%s/%s | light=%s\n", board,
roleName(static_cast<Role>(settings_.role)),
testKindName(static_cast<TestKind>(settings_.testKind)),
configuredLevelName(settings_));

View File

@@ -8,7 +8,7 @@
#include "SettingsStore.h"
enum class AppState : uint8_t {
IDLE, MENU, SOLO_MEASURE, SOLO_DRIVER, MASTER_DISCOVER, MASTER_WAIT_READY,
IDLE, MENU, BOARD_TEST, SOLO_MEASURE, SOLO_DRIVER, MASTER_DISCOVER, MASTER_WAIT_READY,
MASTER_WAIT_RESULT, MASTER_FINALIZE, SLAVE_READY, SLAVE_WAIT_START, SLAVE_MEASURE,
SLAVE_WAIT_ACK, FINISHED
};
@@ -26,6 +26,9 @@ class App {
void cycleRunMode();
void sanitizeRange();
void startTest();
void startBoardTest();
void updateBoardTest(uint32_t now);
void stopBoardTest();
bool armSlave(bool preserveDisplay = false);
bool prepareStage(bool showProgress = true);
bool startLocalMeasurement(float hz, float duty);
@@ -52,7 +55,6 @@ class App {
void updateHeartbeat();
bool packetForCurrent(const ProtocolPacket &p) const;
void serviceIdlePowerSave();
void serviceRxPinStateLog();
void serviceSerialConsole();
void handleSerialCommand(char *line);
void printSerialHelp();
@@ -100,6 +102,9 @@ class App {
uint32_t lastOpticalWakeToggleMs_ = 0;
bool opticalWakeActive_ = false;
bool rxPinStateKnown_ = false, rxPinState_ = false;
uint32_t boardTestUpdatedMs_ = 0;
uint32_t boardDisplayUpdatedMs_ = 0;
bool boardPwmLightOn_ = false;
mutable uint32_t lastUsbHostSeenMs_ = 0;
char serialLine_[96] = {};
uint8_t serialLineLength_ = 0;

View File

@@ -5,29 +5,43 @@
// ------------------------- Hardware configuration -------------------------
// Enabled for the hand-wired prototype. Comment out for the production PCB.
// Both profiles map S3 signals by physical header position with 5V/GND aligned.
#define MAKETKA
//#define MAKETKA
// Additionally log raw GPIO_RX level changes while no test is running.
//#define RX_PIN_CHANGE_TEST
// Select exactly one populated receiver circuit. Use
// BOARD_RX_INTERFACE_DIGITAL for MAKETKA and boards fitted with GPIO_RX.
#define BOARD_RX_INTERFACE_ADC 1
#define BOARD_RX_INTERFACE_DIGITAL 2
#define BOARD_RX_INTERFACE BOARD_RX_INTERFACE_ADC
// Standalone PWM output check. While enabled, the normal application is not
// started: GPIO_PWM continuously outputs the frequency and duty below.
// Comment this define out after the hardware check.
//#define PWM_OUTPUT_TEST
constexpr uint32_t PWM_OUTPUT_TEST_FREQUENCY_HZ = 1000;
constexpr uint32_t PWM_OUTPUT_TEST_SWEEP_PERIOD_MS = 2000;
constexpr uint32_t PWM_OUTPUT_TEST_UPDATE_MS = 10;
constexpr uint32_t PWM_OUTPUT_TEST_MIN_PULSE_NS = 1000;
constexpr uint32_t PWM_OUTPUT_TEST_MAX_PULSE_NS = 10000;
#if BOARD_RX_INTERFACE != BOARD_RX_INTERFACE_ADC && \
BOARD_RX_INTERFACE != BOARD_RX_INTERFACE_DIGITAL
#error "BOARD_RX_INTERFACE must select ADC or DIGITAL"
#endif
#if defined(MAKETKA) && BOARD_RX_INTERFACE == BOARD_RX_INTERFACE_ADC
#error "MAKETKA requires BOARD_RX_INTERFACE_DIGITAL"
#endif
constexpr bool BOARD_RX_USES_ADC =
BOARD_RX_INTERFACE == BOARD_RX_INTERFACE_ADC;
// Board-test parameters. These checks are selected at runtime from the UI;
// no special diagnostic firmware build is required.
constexpr uint32_t BOARD_PWM_TEST_FREQUENCY_HZ = 1;
constexpr uint32_t BOARD_PWM_TEST_HALF_PERIOD_MS = 500;
#if CONFIG_IDF_TARGET_ESP32C3
constexpr bool TARGET_IS_C3 = true;
constexpr uint8_t GPIO_PWM = 3;
constexpr uint8_t GPIO_RX = 4;
#ifdef MAKETKA
constexpr bool BOARD_ADC_AVAILABLE = false;
constexpr uint8_t GPIO_BUTTON_MODE = 0;
constexpr uint8_t GPIO_BUTTON_START = 1;
constexpr uint8_t GPIO_VBAT = UINT8_MAX;
constexpr uint8_t GPIO_ANALOG_RX = UINT8_MAX;
#else
constexpr bool BOARD_ADC_AVAILABLE = true;
constexpr uint8_t GPIO_BUTTON_MODE = 20;
constexpr uint8_t GPIO_BUTTON_START = 10;
constexpr uint8_t GPIO_VBAT = 2;
@@ -38,6 +52,7 @@ constexpr uint8_t GPIO_SCL = 7;
#elif CONFIG_IDF_TARGET_ESP32S3
constexpr bool TARGET_IS_C3 = false;
#ifdef MAKETKA
constexpr bool BOARD_ADC_AVAILABLE = false;
// Same physical header contacts as the C3 MAKETKA profile when 5V/GND align.
constexpr uint8_t GPIO_PWM = 12;
constexpr uint8_t GPIO_RX = 13;
@@ -45,7 +60,10 @@ constexpr uint8_t GPIO_BUTTON_MODE = 9;
constexpr uint8_t GPIO_BUTTON_START = 10;
constexpr uint8_t GPIO_SDA = 44;
constexpr uint8_t GPIO_SCL = 1;
constexpr uint8_t GPIO_VBAT = UINT8_MAX;
constexpr uint8_t GPIO_ANALOG_RX = UINT8_MAX;
#else
constexpr bool BOARD_ADC_AVAILABLE = true;
// The S3 SuperMini is fitted so its 5V and GND pins occupy the same PCB
// contacts as on the C3 SuperMini. Signals therefore follow header position.
constexpr uint8_t GPIO_PWM = 12;
@@ -69,6 +87,8 @@ constexpr uint32_t SERIAL_BAUD = 115200;
// Native USB CDC may keep a stale "connected" state after light sleep. Keep
// logging non-blocking so a missing host can never delay button polling.
constexpr uint32_t SERIAL_TX_TIMEOUT_MS = 2;
constexpr uint32_t BOARD_ADC_PRINT_INTERVAL_MS = 100;
constexpr uint32_t BOARD_TEST_DISPLAY_INTERVAL_MS = 250;
constexpr bool SERIAL_ACTION_LOG = true;
constexpr bool SERIAL_LOG_TIMESTAMPS = true;
constexpr bool SERIAL_MINIMAL_LOG = true;

View File

@@ -21,6 +21,14 @@ constexpr const char *TEST_NAMES[] = {
"ОПТИКА", "ДРАЙВЕР"
};
constexpr const char *TEST_GROUP_NAMES[] = {
"ОПТИКА", "ПЛАТА"
};
constexpr const char *BOARD_TEST_NAMES[] = {
"АЦП", "ШИМ ВЫХОД", "RX ВХОД"
};
constexpr const char *FAIL_NAMES[] = {
"НЕТ ОШИБКИ",
"НЕТ СИГНАЛА",
@@ -44,6 +52,7 @@ constexpr const char *MODE_PREFIX = "РЕЖИМ: ";
constexpr const char *START_RUN = "ГОТОВ К ЗАПУСКУ";
constexpr const char *MENU_FREQUENCY = "ЧАСТОТА ШИМ:";
constexpr const char *MENU_TEST_GROUP = "ГРУППА ТЕСТОВ:";
constexpr const char *MENU_MAX_PULSE = "МАКС. ИМПУЛЬС:";
constexpr const char *MENU_MIN_PULSE = "МИН. ИМПУЛЬС:";
constexpr const char *MENU_ACCURACY = "ТОЧНОСТЬ:";
@@ -53,6 +62,9 @@ constexpr const char *LIGHT_CODE_FORMAT = "TX:%c, RX:%c";
constexpr const char *LIGHT_AUTO = "АВТО";
constexpr const char *LIGHT_AUTO_FORMAT = "TX/RX: АВТО";
constexpr const char *MENU_TOTAL_TIME = "ОБЩЕЕ ВРЕМЯ:";
constexpr const char *BOARD_READY = "START: ЗАПУСК";
constexpr const char *BOARD_STOP = "УДЕРЖ START: СТОП";
constexpr const char *ADC_UNAVAILABLE = "АЦП НЕТ НА МАКЕТКЕ";
constexpr const char *FREQUENCY_UNIT = " Гц";
constexpr const char *SLAVE_READY = "СЛЕЙВ ГОТОВ";
@@ -87,6 +99,14 @@ constexpr const char *TEST_NAMES[] = {
"OPTICAL", "DRIVER"
};
constexpr const char *TEST_GROUP_NAMES[] = {
"OPTICS", "BOARD"
};
constexpr const char *BOARD_TEST_NAMES[] = {
"ADC", "PWM OUTPUT", "RX INPUT"
};
constexpr const char *FAIL_NAMES[] = {
"NONE",
"NO SIGNAL",
@@ -110,6 +130,7 @@ constexpr const char *MODE_PREFIX = "MODE: ";
constexpr const char *START_RUN = "READY TO START";
constexpr const char *MENU_FREQUENCY = "PWM FREQUENCY:";
constexpr const char *MENU_TEST_GROUP = "TEST GROUP:";
constexpr const char *MENU_MAX_PULSE = "MAX PULSE:";
constexpr const char *MENU_MIN_PULSE = "MIN PULSE:";
constexpr const char *MENU_ACCURACY = "ACCURACY:";
@@ -119,6 +140,9 @@ constexpr const char *LIGHT_CODE_FORMAT = "TX:%c, RX:%c";
constexpr const char *LIGHT_AUTO = "AUTO";
constexpr const char *LIGHT_AUTO_FORMAT = "TX/RX: AUTO";
constexpr const char *MENU_TOTAL_TIME = "TOTAL TIME:";
constexpr const char *BOARD_READY = "START TO RUN";
constexpr const char *BOARD_STOP = "HOLD START TO STOP";
constexpr const char *ADC_UNAVAILABLE = "ADC ABSENT ON PROTOTYPE";
constexpr const char *FREQUENCY_UNIT = " Hz";
constexpr const char *SLAVE_READY = "SLAVE READY";

View File

@@ -9,12 +9,24 @@ const char *roleName(Role r) {
return i < 3 ? names[i] : "?";
}
const char *testGroupName(TestGroup group) {
static const char *names[] = {"OPTICS", "BOARD"};
const uint8_t i = static_cast<uint8_t>(group);
return i < 2 ? names[i] : "?";
}
const char *testKindName(TestKind kind) {
static const char *names[] = {"OPTICAL", "DRIVER"};
const uint8_t i = static_cast<uint8_t>(kind);
return i < 2 ? names[i] : "?";
}
const char *boardTestName(BoardTest test) {
static const char *names[] = {"ADC", "PWM OUTPUT", "RX INPUT"};
const uint8_t i = static_cast<uint8_t>(test);
return i < 3 ? names[i] : "?";
}
const char *lightCodeName(LightCode code) {
static const char *names[] = {"HH", "HL", "LH", "LL"};
const uint8_t i = static_cast<uint8_t>(code);

View File

@@ -4,7 +4,9 @@
#include <stddef.h>
enum class Role : uint8_t { SOLO, MASTER, SLAVE };
enum class TestGroup : uint8_t { OPTICS, BOARD };
enum class TestKind : uint8_t { OPTICAL, DRIVER };
enum class BoardTest : uint8_t { ADC, PWM_OUTPUT, RX_INPUT };
enum class LightCode : uint8_t { HH, HL, LH, LL };
enum class FailReason : uint8_t {
NONE, NO_SIGNAL, PERIOD_OUT, DUTY_OUT, EXTRA_EDGE, GLITCH, LOST_EDGE,
@@ -13,7 +15,9 @@ enum class FailReason : uint8_t {
};
const char *roleName(Role role);
const char *testGroupName(TestGroup group);
const char *testKindName(TestKind kind);
const char *boardTestName(BoardTest test);
const char *lightCodeName(LightCode code);
const char *failName(FailReason reason);
@@ -27,7 +31,8 @@ struct Settings {
uint8_t minPulseIndex;
uint8_t accuracyIndex;
uint8_t timeIndex;
uint16_t reserved;
uint8_t testGroup;
uint8_t boardTest;
uint32_t checksum;
};

View File

@@ -1,57 +1,6 @@
#include "App.h"
#include "Config.h"
#include <math.h>
#ifdef PWM_OUTPUT_TEST
PwmGenerator pwmOutputTest;
uint32_t pwmOutputTestPulseNs = PWM_OUTPUT_TEST_MIN_PULSE_NS;
uint32_t pwmOutputTestUpdatedMs = 0;
void setup() {
Serial.begin(SERIAL_BAUD);
delay(200);
Serial.printf("\nPWM OUTPUT TEST: GPIO=%u requested=%luHz pulse=%lu..%luns sine=%lums light-off=%s\n",
GPIO_PWM, PWM_OUTPUT_TEST_FREQUENCY_HZ,
PWM_OUTPUT_TEST_MIN_PULSE_NS, PWM_OUTPUT_TEST_MAX_PULSE_NS,
PWM_OUTPUT_TEST_SWEEP_PERIOD_MS,
TX_LIGHT_OFF_GPIO_LEVEL == HIGH ? "HIGH" : "LOW");
pwmOutputTest.begin();
ActualPwm actual = {};
if (pwmOutputTest.start(PWM_OUTPUT_TEST_FREQUENCY_HZ,
pwmOutputTestPulseNs, actual)) {
Serial.printf("PWM OUTPUT TEST STARTED: actual=%luHz pulse=%luns bits=%u\n",
actual.actualHz, actual.actualPulseNs, actual.bits);
} else {
Serial.println("PWM OUTPUT TEST FAILED");
}
}
void loop() {
const uint32_t now = millis();
if (now - pwmOutputTestUpdatedMs < PWM_OUTPUT_TEST_UPDATE_MS) return;
pwmOutputTestUpdatedMs = now;
constexpr float PWM_TWO_PI = 6.28318530718f;
const float phase = PWM_TWO_PI * (now % PWM_OUTPUT_TEST_SWEEP_PERIOD_MS) /
PWM_OUTPUT_TEST_SWEEP_PERIOD_MS;
const float center = (PWM_OUTPUT_TEST_MIN_PULSE_NS + PWM_OUTPUT_TEST_MAX_PULSE_NS) * 0.5f;
const float amplitude = (PWM_OUTPUT_TEST_MAX_PULSE_NS - PWM_OUTPUT_TEST_MIN_PULSE_NS) * 0.5f;
const uint32_t pulseNs = static_cast<uint32_t>(center + amplitude * sinf(phase) + 0.5f);
if (pulseNs == pwmOutputTestPulseNs) return;
ActualPwm actual = {};
if (pwmOutputTest.start(PWM_OUTPUT_TEST_FREQUENCY_HZ, pulseNs, actual)) {
pwmOutputTestPulseNs = pulseNs;
} else {
Serial.printf("PWM OUTPUT TEST UPDATE FAILED: pulse=%luns\n", pulseNs);
delay(100);
}
}
#else
App app;
void setup() { app.begin(); }
void loop() { app.update(); }
#endif

View File

@@ -3,18 +3,23 @@
#include "Log.h"
#include <Preferences.h>
// testGroup/boardTest reuse the former zeroed reserved bytes, so version 11
// settings remain binary-compatible and keep the user's optical parameters.
namespace { constexpr uint16_t SETTINGS_VERSION = 11; constexpr char NAMESPACE[] = "opt-test"; }
void SettingsStore::defaults(Settings &s) const {
// 2 kHz, 200 us .. 2 us, 5%, 1 s.
s = {SETTINGS_VERSION, static_cast<uint8_t>(Role::SOLO),
static_cast<uint8_t>(TestKind::OPTICAL), static_cast<uint8_t>(LightCode::HH),
2, 6, 3, 2, 3, 0, 0};
2, 6, 3, 2, 3, static_cast<uint8_t>(TestGroup::OPTICS),
static_cast<uint8_t>(BoardTest::ADC), 0};
s.checksum = settingsChecksum(s);
}
bool SettingsStore::valid(const Settings &s) const {
return s.version == SETTINGS_VERSION && s.role <= static_cast<uint8_t>(Role::SLAVE) &&
s.testGroup <= static_cast<uint8_t>(TestGroup::BOARD) &&
s.boardTest <= static_cast<uint8_t>(BoardTest::RX_INPUT) &&
s.testKind <= static_cast<uint8_t>(TestKind::DRIVER) &&
s.lightCode <= static_cast<uint8_t>(LightCode::LL) &&
(s.testKind != static_cast<uint8_t>(TestKind::DRIVER) ||

View File

@@ -160,13 +160,47 @@ MASTER <~~~~ ESP-NOW Wi-Fi channel 6 ~~~~> SLAVE
## Управление
В ожидании на S3:
В ожидании в группе `ОПТИКА` на S3:
- MODE short: `SOLO ОПТИКА → SOLO ДРАЙВЕР → MASTER ОПТИКА → SLAVE ОПТИКА`;
- MODE long: открыть настройки;
- START short: начать тест;
- START long во время теста: ABORT, PWM немедленно выключается.
В ожидании в группе `ПЛАТА`:
- MODE short для аналоговой платы: `АЦП ↔ ШИМ ВЫХОД`;
- MODE short для цифровой платы: `RX ВХОД ↔ ШИМ ВЫХОД`;
- MODE long: открыть настройки;
- START short: запустить выбранный тест платы;
- START long: остановить тест платы и вернуться к выбору.
При запуске `АЦП` или `RX ВХОД` оптический передатчик постоянно включается
уровнем `TX_LIGHT_ON_GPIO_LEVEL`. При остановке теста свет выключается. В
`ШИМ ВЫХОД` этот же вывод переключается с частотой 1 Гц и заполнением 50%:
500 мс свет включён, 500 мс выключен, поэтому работу выхода видно глазом.
`АЦП` каждые 100 мс выводит в Serial текущие сырые значения входов
`GPIO_ANALOG_RX` и `GPIO_VBAT`, а также периодически обновляет OLED. Этот тест
доступен в профиле производственной PCB; в профиле `MAKETKA` прошивка явно
сообщает об отсутствии этих входов. `ШИМ ВЫХОД` заменяет прежний
`PWM_OUTPUT_TEST`, а `RX ВХОД` — прежний `RX_PIN_CHANGE_TEST`; отдельная сборка
диагностической прошивки больше не нужна.
Тип распаянного приёмника выбирается в `Config.h` одним взаимоисключающим
значением:
```cpp
#define BOARD_RX_INTERFACE BOARD_RX_INTERFACE_ADC
// или
#define BOARD_RX_INTERFACE BOARD_RX_INTERFACE_DIGITAL
```
По умолчанию выбрана плата с АЦП. Одновременно аналоговый и цифровой тесты
приёмника в меню не появляются. Для `MAKETKA` следует выбрать
`BOARD_RX_INTERFACE_DIGITAL`, поскольку аналоговые выводы в этом профиле
отсутствуют.
В настройках:
- MODE short: следующий параметр;
@@ -184,7 +218,11 @@ START и MODE будят устройство. Первое, пробуждаю
## Настройки теста
Меню содержит шесть общих параметров:
Первый пункт меню выбирает группу `ОПТИКА` или `ПЛАТА`. Для группы `ПЛАТА`
это единственная настройка: конкретный аппаратный тест выбирается коротким
нажатием MODE в экране ожидания.
Для группы `ОПТИКА` меню дополнительно содержит шесть параметров:
1. частота ШИМ из `PWM_FREQUENCY_OPTIONS_HZ`;
2. максимальная длительность импульса из отдельного `MAX_PULSE_OPTIONS_NS`;