diff --git a/OpticalChannelTester/App.cpp b/OpticalChannelTester/App.cpp index 393ab23..d273212 100644 --- a/OpticalChannelTester/App.cpp +++ b/OpticalChannelTester/App.cpp @@ -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(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(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(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(countOf(MAX_PULSE_OPTIONS_NS) - 1U); while (last && static_cast(MAX_PULSE_OPTIONS_NS[last]) * frequencyHz >= 1000000000ULL) @@ -290,7 +314,8 @@ void App::finishInitialization(bool factoryReset) { lastUserActivityMs_ = millis(); setActivePerformance(false); printConfiguration(); - if (static_cast(settings_.role) == Role::SLAVE) armSlave(); + if (static_cast(settings_.testGroup) == TestGroup::OPTICS && + static_cast(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(settings_.role) == Role::SLAVE) armSlave(); + if (static_cast(settings_.testGroup) == TestGroup::OPTICS && + static_cast(settings_.role) == Role::SLAVE) armSlave(); else showIdle(); - Log::printf("ACTION", "mode changed to %s/%s, NVS=%s", - roleName(static_cast(settings_.role)), - testKindName(static_cast(settings_.testKind)), - saved ? "OK" : "FAILED"); + if (static_cast(settings_.testGroup) == TestGroup::BOARD) + Log::printf("ACTION", "board test changed to %s, NVS=%s", + boardTestName(static_cast(settings_.boardTest)), saved ? "OK" : "FAILED"); + else + Log::printf("ACTION", "mode changed to %s/%s, NVS=%s", + roleName(static_cast(settings_.role)), + testKindName(static_cast(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(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(settings_.role) == Role::SLAVE) armSlave(); + if (static_cast(settings_.testGroup) == TestGroup::OPTICS && + static_cast(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(settings_.role)), - uiTestName(static_cast(settings_.testKind))); - display_.show(one, UiText::START_RUN); + char one[64]; + if (static_cast(settings_.testGroup) == TestGroup::BOARD) { + snprintf(one, sizeof(one), "%s: %s", uiTestGroupName(TestGroup::BOARD), + uiBoardTestName(static_cast(settings_.boardTest))); + display_.show(one, UiText::BOARD_READY); + } else { + snprintf(one, sizeof(one), "%s: %s", + uiRoleName(static_cast(settings_.role)), + uiTestName(static_cast(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(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(settings_.testGroup)), + boardTestName(static_cast(settings_.boardTest)), roleName(static_cast(settings_.role)), testKindName(static_cast(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(settings_.role) == Role::SLAVE) armSlave(); + if (static_cast(settings_.testGroup) == TestGroup::OPTICS && + static_cast(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(TestGroup::OPTICS); accepted = true; + } else if (!strcmp(value, "board")) { + settings_.testGroup = static_cast(TestGroup::BOARD); accepted = true; + } + } else if (!strcmp(name, "boardtest") || !strcmp(name, "board")) { + BoardTest selected = static_cast(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(selected); + accepted = true; + } + } else if (!strcmp(name, "role")) { if (!strcmp(value, "solo")) { settings_.role = static_cast(Role::SOLO); accepted = true; } else if (!strcmp(value, "master")) { settings_.role = static_cast(Role::MASTER); accepted = true; } else if (!strcmp(value, "slave")) { settings_.role = static_cast(Role::SLAVE); accepted = true; } @@ -608,6 +676,13 @@ void App::handleSerialCommand(char *line) { } void App::cycleRunMode() { + if (static_cast(settings_.testGroup) == TestGroup::BOARD) { + do { + settings_.boardTest = (settings_.boardTest + 1U) % + (static_cast(BoardTest::RX_INPUT) + 1U); + } while (!boardTestAvailable(static_cast(settings_.boardTest))); + return; + } const Role role = static_cast(settings_.role); const TestKind kind = static_cast(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(TestGroup::BOARD)) + settings_.testGroup = static_cast(TestGroup::OPTICS); + if (settings_.boardTest > static_cast(BoardTest::RX_INPUT) || + !boardTestAvailable(static_cast(settings_.boardTest))) + settings_.boardTest = static_cast(defaultBoardTest()); if (settings_.role > static_cast(Role::SLAVE)) settings_.role = static_cast(Role::SOLO); if (settings_.testKind > static_cast(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(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(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(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(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(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(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(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(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(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(test)], UiText::BOARD_STOP); + } else { + pinMode(GPIO_RX, INPUT); + display_.show(UiText::BOARD_TEST_NAMES[static_cast(test)], UiText::BOARD_STOP); + } +} + +void App::updateBoardTest(uint32_t now) { + const BoardTest test = static_cast(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(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(test)], two); + } +} + +void App::stopBoardTest() { + Log::printf("BOARD", "stopping test=%s", boardTestName(static_cast(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(settings_.role) == Role::SLAVE || + if (static_cast(settings_.testGroup) == TestGroup::BOARD || + static_cast(settings_.role) == Role::SLAVE || static_cast(settings_.testKind) == TestKind::DRIVER) pwm_.stop(); else pwm_.active(); } void App::setActivePerformance(bool active) { const bool driverMode = initialized_ && + static_cast(settings_.testGroup) == TestGroup::OPTICS && static_cast(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(settings_.testGroup) == TestGroup::BOARD) { + Serial.printf("\nOptical Channel Tester | %s | group=BOARD | test=%s\n", board, + boardTestName(static_cast(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(settings_.role)), testKindName(static_cast(settings_.testKind)), configuredLevelName(settings_)); diff --git a/OpticalChannelTester/App.h b/OpticalChannelTester/App.h index b3da243..674d1ac 100644 --- a/OpticalChannelTester/App.h +++ b/OpticalChannelTester/App.h @@ -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; diff --git a/OpticalChannelTester/Config.h b/OpticalChannelTester/Config.h index c2efc6b..63fb912 100644 --- a/OpticalChannelTester/Config.h +++ b/OpticalChannelTester/Config.h @@ -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; diff --git a/OpticalChannelTester/Config_Text.h b/OpticalChannelTester/Config_Text.h index 1d06d5c..7bd5b15 100644 --- a/OpticalChannelTester/Config_Text.h +++ b/OpticalChannelTester/Config_Text.h @@ -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"; diff --git a/OpticalChannelTester/Core.cpp b/OpticalChannelTester/Core.cpp index 90733d6..51534f7 100644 --- a/OpticalChannelTester/Core.cpp +++ b/OpticalChannelTester/Core.cpp @@ -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(group); + return i < 2 ? names[i] : "?"; +} + const char *testKindName(TestKind kind) { static const char *names[] = {"OPTICAL", "DRIVER"}; const uint8_t i = static_cast(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(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(code); diff --git a/OpticalChannelTester/Core.h b/OpticalChannelTester/Core.h index ec93ff2..d1d5017 100644 --- a/OpticalChannelTester/Core.h +++ b/OpticalChannelTester/Core.h @@ -4,7 +4,9 @@ #include 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; }; diff --git a/OpticalChannelTester/OpticalChannelTester.ino b/OpticalChannelTester/OpticalChannelTester.ino index 6fd658a..3518534 100644 --- a/OpticalChannelTester/OpticalChannelTester.ino +++ b/OpticalChannelTester/OpticalChannelTester.ino @@ -1,57 +1,6 @@ #include "App.h" -#include "Config.h" -#include - -#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(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 diff --git a/OpticalChannelTester/SettingsStore.cpp b/OpticalChannelTester/SettingsStore.cpp index 15b5e23..f4f8fe5 100644 --- a/OpticalChannelTester/SettingsStore.cpp +++ b/OpticalChannelTester/SettingsStore.cpp @@ -3,18 +3,23 @@ #include "Log.h" #include +// 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(Role::SOLO), static_cast(TestKind::OPTICAL), static_cast(LightCode::HH), - 2, 6, 3, 2, 3, 0, 0}; + 2, 6, 3, 2, 3, static_cast(TestGroup::OPTICS), + static_cast(BoardTest::ADC), 0}; s.checksum = settingsChecksum(s); } bool SettingsStore::valid(const Settings &s) const { return s.version == SETTINGS_VERSION && s.role <= static_cast(Role::SLAVE) && + s.testGroup <= static_cast(TestGroup::BOARD) && + s.boardTest <= static_cast(BoardTest::RX_INPUT) && s.testKind <= static_cast(TestKind::DRIVER) && s.lightCode <= static_cast(LightCode::LL) && (s.testKind != static_cast(TestKind::DRIVER) || diff --git a/README.md b/README.md index 920292d..baaf0d9 100644 --- a/README.md +++ b/README.md @@ -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`;