Compare commits

...

14 Commits

Author SHA1 Message Date
Razvalyaev
dda98860d1 добавлен субмодуль с зарядкой и мультисборка с ней 2026-08-26 12:59:57 +03:00
Razvalyaev
30264698b8 подправлен разъем для зарядника, но ещше не окончаительно 2026-08-25 18:31:21 +03:00
Razvalyaev
e1fcd0b7fe гербер для завода 2026-08-24 18:50:54 +03:00
Razvalyaev
a2de62f61d Возвращены отверстия для заказа платы, а не чпу
добавлен дюпонт разъем для самодельного аккума
2026-08-23 22:16:51 +03:00
Razvalyaev
bff0294a22 в pcb добавлен потенциометр для настройки мощности оптики 2026-08-23 21:34:40 +03:00
Razvalyaev
8699f8e4eb Сделана настройка мощности передающей оптики 2026-08-23 18:32:47 +03:00
Razvalyaev
3907cbcf77 Добавлено тестировние платы в меню:
- Тест Tx Оптики
- Тест Rx Оптики
- Тест АЦП
2026-08-22 20:03:12 +03:00
Razvalyaev
49332aa028 доработки по интерфейсу 2026-08-14 15:41:09 +03:00
Razvalyaev
035792aedd Доработки по тесту драйвера 2026-08-14 14:42:46 +03:00
Razvalyaev
037bb37e62 добавлена бета проверка драйверов 2026-08-14 12:01:07 +03:00
Razvalyaev
a17e8962b4 Глобальная переделка. тест сделан по длине импульса и заданной частоте шим, а не меандру 2026-08-12 18:10:05 +03:00
Razvalyaev
1db89fca79 доработки всякие 2026-08-12 12:11:55 +03:00
Razvalyaev
862781fa6a Коррекция уровня оптики, вкл по умолчанию, выкл в сне
плюс коррекции по отображению
2026-08-12 08:25:56 +03:00
Razvalyaev
f56595f767 добавлены дефайны для теста разводки оптики
запущено все на s3
скорректированны пины и шим на s3
2026-08-11 21:43:43 +03:00
74 changed files with 16305 additions and 5370 deletions

2
.gitignore vendored
View File

@@ -2,5 +2,7 @@
__Previews/ __Previews/
History History
Project Logs*/ Project Logs*/
Project Outputs*/
/.build/

4
.gitmodules vendored Normal file
View File

@@ -0,0 +1,4 @@
[submodule "PCB_Charger"]
path = PCB_Charger
url = https://git.rd12.ru/Razvalyaev/Charger.git
branch = master

View File

@@ -8,12 +8,18 @@
#include <esp_system.h> #include <esp_system.h>
#include <esp32-hal-cpu.h> #include <esp32-hal-cpu.h>
#include <driver/gpio.h> #include <driver/gpio.h>
#include <Wire.h>
#include <math.h> #include <math.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
namespace { namespace {
constexpr uint8_t MENU_OPTICAL_CALIBRATION_ITEM = 7;
const char *uiFailName(FailReason reason);
const char *appStateName(AppState state) { const char *appStateName(AppState state) {
static const char *names[] = {"IDLE", "MENU", "SOLO_MEASURE", "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", "MASTER_WAIT_READY", "MASTER_WAIT_RESULT", "MASTER_FINALIZE", "SLAVE_READY", "SLAVE_WAIT_START",
"SLAVE_MEASURE", "SLAVE_WAIT_ACK", "FINISHED"}; "SLAVE_MEASURE", "SLAVE_WAIT_ACK", "FINISHED"};
const uint8_t index = static_cast<uint8_t>(state); const uint8_t index = static_cast<uint8_t>(state);
@@ -26,9 +32,59 @@ const char *buttonEventName(ButtonEvent event) {
return index < sizeof(names) / sizeof(names[0]) ? names[index] : "UNKNOWN"; return index < sizeof(names) / sizeof(names[0]) ? names[index] : "UNKNOWN";
} }
void formatErrorDuty(float duty, char *out, size_t size) { uint32_t pulseFromDuty(float hz, float dutyPct) {
if (fabsf(duty - roundf(duty)) < 0.05f) snprintf(out, size, "%.0f%%", duty); return hz > 0.0f ? static_cast<uint32_t>(lroundf(dutyPct * 10000000.0f / hz)) : 0U;
else snprintf(out, size, "%.1f%%", duty); }
float dutyFromPulse(uint32_t hz, uint32_t pulseNs) {
return static_cast<float>(static_cast<double>(hz) * pulseNs / 10000000.0);
}
bool configuredTxPulseLightOn(const Settings &settings) {
return static_cast<TestKind>(settings.testKind) == TestKind::DRIVER
? txActiveLightOn(settings) : true;
}
const char *configuredLevelName(const Settings &settings) {
return static_cast<TestKind>(settings.testKind) == TestKind::DRIVER
? lightCodeName(static_cast<LightCode>(settings.lightCode)) : "AUTO";
}
void formatTarget(uint32_t hz, uint32_t pulseNs, char *out, size_t size) {
char frequency[16], pulse[12];
Display::formatPwmFrequency(hz, frequency, sizeof(frequency));
Display::formatPulse(pulseNs, pulse, sizeof(pulse));
snprintf(out, size, UiText::TEST_FORMAT, frequency, pulse);
}
void formatMeasured(float hz, uint32_t pulseNs, char *out, size_t size) {
char frequency[12], pulse[12];
Display::formatFrequency(hz, frequency, sizeof(frequency));
Display::formatPulse(pulseNs, pulse, sizeof(pulse), true);
snprintf(out, size, "F:%s, P:%s", frequency, pulse);
}
void formatTestTarget(uint32_t hz, uint32_t pulseNs, char *out, size_t size) {
char target[32];
formatTarget(hz, pulseNs, target, sizeof(target));
snprintf(out, size, UiText::TEST_TARGET_FORMAT, target);
}
void formatFailure(FailReason reason, uint32_t hz, uint32_t pulseNs,
char *out, size_t size) {
(void)reason;
char target[32];
formatTarget(hz, pulseNs, target, sizeof(target));
snprintf(out, size, UiText::FAIL_TARGET_FORMAT, target);
}
void formatElapsedNs(uint64_t ns, char *out, size_t size) {
// Compact form keeps error timing within 21 OLED columns.
// Exact nanoseconds remain available in the Serial diagnostic.
if (ns < 1000ULL) snprintf(out, size, "%llun", ns);
else if (ns < 1000000ULL) snprintf(out, size, "%.1fu", ns / 1000.0);
else if (ns < 1000000000ULL) snprintf(out, size, "%.0fm", ns / 1000000.0);
else snprintf(out, size, "%.2fs", ns / 1000000000.0);
} }
size_t utf8CharacterCount(const char *text) { size_t utf8CharacterCount(const char *text) {
@@ -53,6 +109,12 @@ const char *uiFailName(FailReason reason) {
? UiText::FAIL_NAMES[index] : "UNKNOWN"; ? UiText::FAIL_NAMES[index] : "UNKNOWN";
} }
const char *roleCorner(Role role) {
static const char *markers[] = {"O", "M", "S"};
const uint8_t index = static_cast<uint8_t>(role);
return index < sizeof(markers) / sizeof(markers[0]) ? markers[index] : "?";
}
void formatMenuLine(const char *label, const char *value, char *out, size_t size) { void formatMenuLine(const char *label, const char *value, char *out, size_t size) {
constexpr size_t OLED_TEXT_COLUMNS = 21; constexpr size_t OLED_TEXT_COLUMNS = 21;
const size_t labelLength = utf8CharacterCount(label); const size_t labelLength = utf8CharacterCount(label);
@@ -63,24 +125,178 @@ void formatMenuLine(const char *label, const char *value, char *out, size_t size
snprintf(out, size, "%s%*s%s", label, padding, "", value); snprintf(out, size, "%s%*s%s", label, padding, "", value);
} }
uint32_t overallProgress(uint32_t stageIndex, uint8_t step) { uint32_t overallProgress(uint32_t stageIndex, uint8_t step,
if (step > MEASUREMENT_PROGRESS_STEPS) step = MEASUREMENT_PROGRESS_STEPS; uint8_t stepsPerStage = MEASUREMENT_PROGRESS_STEPS) {
return stageIndex * MEASUREMENT_PROGRESS_STEPS + step; if (step > stepsPerStage) step = stepsPerStage;
return stageIndex * stepsPerStage + step;
} }
uint32_t overallProgressTotal(uint32_t stageCount) { uint32_t overallProgressTotal(
return stageCount * MEASUREMENT_PROGRESS_STEPS; uint32_t stageCount,
uint8_t stepsPerStage = MEASUREMENT_PROGRESS_STEPS) {
return stageCount * stepsPerStage;
} }
uint32_t stageWallTimeMs(uint32_t testTimeMs, uint32_t frequencyHz) { uint32_t stageWallTimeMs(uint32_t testTimeMs, uint32_t frequencyHz) {
return static_cast<uint32_t>((nominalStageUs(frequencyHz, testTimeMs, PWM_SETTLE_CYCLES) + 999ULL) / 1000ULL); return static_cast<uint32_t>((nominalStageUs(frequencyHz, testTimeMs, PWM_SETTLE_CYCLES) + 999ULL) / 1000ULL);
} }
const char *uiTestName(TestKind kind) {
const uint8_t index = static_cast<uint8_t>(kind);
return index < sizeof(UiText::TEST_NAMES) / sizeof(UiText::TEST_NAMES[0])
? UiText::TEST_NAMES[index] : "?";
} }
App::App() : startButton_(GPIO_BUTTON_START), modeButton_(GPIO_BUTTON_MODE), measurement_(receiver_) {} 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)
--last;
return last;
}
uint8_t firstMaxPulseIndexAtLeast(uint32_t pulseNs, uint8_t last) {
for (uint8_t i = 0; i <= last; ++i)
if (MAX_PULSE_OPTIONS_NS[i] >= pulseNs) return i;
return last;
}
uint8_t lastMinPulseIndexAtMost(uint32_t pulseNs) {
for (size_t i = countOf(MIN_PULSE_OPTIONS_NS); i > 0; --i)
if (MIN_PULSE_OPTIONS_NS[i - 1U] <= pulseNs) return static_cast<uint8_t>(i - 1U);
return 0;
}
uint32_t plannedPulseCaptureHz(uint32_t frequencyHz, float dutyPct) {
if (!frequencyHz || dutyPct <= 0.0f || dutyPct >= 100.0f) return 0;
return MCPWM_CAPTURE_RESOLUTION_HZ;
}
uint32_t plannedCaptureHz(uint32_t frequencyHz, float dutyPct) {
// One 32-bit S3 MCPWM capture timer measures period and pulse at 80 MHz.
return plannedPulseCaptureHz(frequencyHz, dutyPct);
}
bool pulsePointHasResolution(uint32_t hz, uint32_t pulseNs, float accuracyPct) {
uint32_t actualHz = 0, actualPulseNs = 0;
uint8_t bits = 0;
if (TARGET_IS_C3) {
IntegerPwmConfig config = {};
if (!choosePwmConfig(hz, pulseNs, LEDC_SOURCE_CLOCK_HZ, LEDC_MAX_BITS, config)) return false;
actualHz = config.actualHz;
actualPulseNs = config.actualPulseNs;
bits = config.bits;
} else {
if (!hz || MCPWM_RESOLUTION_HZ % hz) return false;
const uint32_t periodTicks = MCPWM_RESOLUTION_HZ / hz;
if (periodTicks < 2U || periodTicks > MCPWM_MAX_PERIOD_TICKS) return false;
uint32_t activeTicks = static_cast<uint32_t>(
(static_cast<uint64_t>(pulseNs) * MCPWM_RESOLUTION_HZ + 500000000ULL) / 1000000000ULL);
if (!activeTicks || activeTicks >= periodTicks) return false;
actualHz = hz;
actualPulseNs = static_cast<uint32_t>(
(static_cast<uint64_t>(activeTicks) * 1000000000ULL + MCPWM_RESOLUTION_HZ / 2U) /
MCPWM_RESOLUTION_HZ);
bits = 1;
for (uint32_t ticks = periodTicks; ticks > 1U; ticks >>= 1U) ++bits;
}
if (!periodWithin(actualHz, hz, accuracyPct) ||
!periodWithin(actualPulseNs, pulseNs, accuracyPct)) return false;
const float dutyPct = dutyFromPulse(actualHz, actualPulseNs);
const uint32_t captureHz = plannedCaptureHz(actualHz, dutyPct);
const uint32_t pulseCaptureHz = plannedPulseCaptureHz(actualHz, dutyPct);
return captureHz && pulseCaptureHz && validateResolution(actualHz, dutyPct, accuracyPct,
captureHz, pulseCaptureHz, bits, MEASUREMENT_AVERAGING_PERIODS) == FailReason::NONE;
}
uint32_t minimumPulseForAccuracy(uint32_t frequencyHz, float accuracyPct) {
for (uint32_t pulseNs : TEST_PULSE_WIDTHS_NS)
if (pulsePointHasResolution(frequencyHz, pulseNs, accuracyPct)) return pulseNs;
return UINT32_MAX;
}
uint8_t firstMinPulseIndexAtLeast(uint32_t pulseNs, uint8_t last) {
for (uint8_t i = 0; i <= last; ++i)
if (MIN_PULSE_OPTIONS_NS[i] >= pulseNs) return i;
return last;
}
uint8_t cycleIndex(uint8_t value, uint8_t first, uint8_t last, int direction) {
if (first >= last) return first;
if (direction > 0) return value >= last ? first : static_cast<uint8_t>(value + 1U);
return value <= first ? last : static_cast<uint8_t>(value - 1U);
}
uint8_t nextMenuItem(uint8_t current, TestGroup group) {
if (group == TestGroup::BOARD)
return current == 0U ? MENU_OPTICAL_CALIBRATION_ITEM : 0U;
return current >= MENU_OPTICAL_CALIBRATION_ITEM ? 0U
: static_cast<uint8_t>(current + 1U);
}
bool parseUnsigned(const char *text, uint32_t &value) {
if (!text || !*text || *text == '-') return false;
char *end = nullptr;
const unsigned long parsed = strtoul(text, &end, 10);
if (!end || *end) return false;
value = static_cast<uint32_t>(parsed);
return true;
}
template <size_t N>
int optionIndex(const uint32_t (&options)[N], uint32_t value) {
for (size_t i = 0; i < N; ++i)
if (options[i] == value) return static_cast<int>(i);
return -1;
}
int accuracyOptionIndex(const char *text) {
if (!text || !*text) return -1;
char *end = nullptr;
const float value = strtof(text, &end);
if (!end || *end) return -1;
for (size_t i = 0; i < countOf(ACCURACY_OPTIONS_PCT); ++i)
if (fabsf(ACCURACY_OPTIONS_PCT[i] - value) < 0.001f) return static_cast<int>(i);
return -1;
}
void lowerAscii(char *text) {
for (; text && *text; ++text)
if (*text >= 'A' && *text <= 'Z') *text = static_cast<char>(*text - 'A' + 'a');
}
}
App::App() : startButton_(GPIO_BUTTON_START), modeButton_(GPIO_BUTTON_MODE),
measurement_(receiver_), driverTest_(receiver_) {}
void App::begin() { void App::begin() {
Serial.begin(SERIAL_BAUD); Serial.begin(SERIAL_BAUD);
#if ARDUINO_USB_CDC_ON_BOOT
Serial.setTxTimeoutMs(SERIAL_TX_TIMEOUT_MS);
#endif
Log::printf("BOOT", "firmware start, Serial=%lu baud", SERIAL_BAUD); Log::printf("BOOT", "firmware start, Serial=%lu baud", SERIAL_BAUD);
startButton_.begin(); modeButton_.begin(); pwm_.begin(); startButton_.begin(); modeButton_.begin(); pwm_.begin();
bootCheckStartedMs_ = millis(); bootCheckStartedMs_ = millis();
@@ -97,19 +313,23 @@ void App::finishInitialization(bool factoryReset) {
} else if (!store_.load(settings_)) { } else if (!store_.load(settings_)) {
store_.save(settings_); Log::event("BOOT", "NVS invalid/missing: defaults loaded"); store_.save(settings_); Log::event("BOOT", "NVS invalid/missing: defaults loaded");
} }
sanitizeRange();
params_ = store_.params(settings_); params_ = store_.params(settings_);
pwm_.configureActiveLight(configuredTxPulseLightOn(settings_));
if (!display_.begin()) Log::event("BOOT", "OLED unavailable; Serial UI remains fully operational"); if (!display_.begin()) Log::event("BOOT", "OLED unavailable; Serial UI remains fully operational");
initialized_ = true; initialized_ = true;
if (!receiver_.begin()) { Log::event("BOOT", "FATAL: capture peripheral init failed"); finish(false, FailReason::UNSUPPORTED); return; } if (!receiver_.begin()) { Log::event("BOOT", "FATAL: capture peripheral init failed"); finish(false, FailReason::UNSUPPORTED); return; }
Log::printf("BOOT", "capture initialized: %s", receiver_.highRateBackend() ? "RMT DMA" : "RMT ping-pong"); Log::printf("BOOT", "capture initialized: %s", receiver_.highRateBackend() ? "MCPWM 80MHz" : "GPIO cycle counter");
lastUserActivityMs_ = millis(); lastUserActivityMs_ = millis();
setActivePerformance(false); setActivePerformance(false);
printConfiguration(); 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 showIdle();
} }
void App::update() { void App::update() {
serviceSerialConsole();
serviceIdlePowerSave(); serviceIdlePowerSave();
const uint32_t now = millis(); const uint32_t now = millis();
const ButtonEvent startEvent = startButton_.update(now); const ButtonEvent startEvent = startButton_.update(now);
@@ -127,6 +347,15 @@ void App::update() {
else if (now - bootCheckStartedMs_ >= FACTORY_RESET_HOLD_MS) finishInitialization(true); else if (now - bootCheckStartedMs_ >= FACTORY_RESET_HOLD_MS) finishInitialization(true);
return; return;
} }
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 && if (state_ != AppState::IDLE && state_ != AppState::MENU && state_ != AppState::FINISHED &&
startEvent == ButtonEvent::LONG) { abortTest(); return; } startEvent == ButtonEvent::LONG) { abortTest(); return; }
if (state_ != AppState::IDLE && state_ != AppState::MENU && state_ != AppState::FINISHED && if (state_ != AppState::IDLE && state_ != AppState::MENU && state_ != AppState::FINISHED &&
@@ -134,11 +363,18 @@ void App::update() {
if (state_ == AppState::IDLE || state_ == AppState::FINISHED) { if (state_ == AppState::IDLE || state_ == AppState::FINISHED) {
if (modeEvent == ButtonEvent::SHORT) { if (modeEvent == ButtonEvent::SHORT) {
settings_.role = (settings_.role + 1U) % 3U; const bool saved = store_.save(settings_); cycleRunMode(); sanitizeRange(); const bool saved = store_.save(settings_);
params_ = store_.params(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(); else showIdle();
Log::printf("ACTION", "role changed to %s, NVS=%s", roleName(static_cast<Role>(settings_.role)), 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) { } else if (modeEvent == ButtonEvent::LONG) {
state_ = AppState::MENU; menuItem_ = 0; Log::event("ACTION", "settings menu entered"); showMenu(); state_ = AppState::MENU; menuItem_ = 0; Log::event("ACTION", "settings menu entered"); showMenu();
} else if (startEvent == ButtonEvent::SHORT) { Log::event("ACTION", "test start requested"); startTest(); } } else if (startEvent == ButtonEvent::SHORT) { Log::event("ACTION", "test start requested"); startTest(); }
@@ -149,7 +385,9 @@ void App::update() {
if (state_ == AppState::SLAVE_READY && modeEvent != ButtonEvent::NONE) { if (state_ == AppState::SLAVE_READY && modeEvent != ButtonEvent::NONE) {
radio_.end(); havePeer_ = false; radio_.end(); havePeer_ = false;
if (modeEvent == ButtonEvent::SHORT) { if (modeEvent == ButtonEvent::SHORT) {
settings_.role = static_cast<uint8_t>(Role::SOLO); const bool saved = store_.save(settings_); settings_.role = static_cast<uint8_t>(Role::SOLO);
settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
const bool saved = store_.save(settings_);
params_ = store_.params(settings_); state_ = AppState::IDLE; showIdle(); params_ = store_.params(settings_); state_ = AppState::IDLE; showIdle();
Log::printf("ACTION", "role changed to SOLO, NVS=%s", saved ? "OK" : "FAILED"); Log::printf("ACTION", "role changed to SOLO, NVS=%s", saved ? "OK" : "FAILED");
} else if (modeEvent == ButtonEvent::LONG) { } else if (modeEvent == ButtonEvent::LONG) {
@@ -159,33 +397,80 @@ void App::update() {
} }
if (state_ == AppState::MENU) { if (state_ == AppState::MENU) {
if (modeEvent == ButtonEvent::SHORT) { if (modeEvent == ButtonEvent::SHORT) {
menuItem_ = (menuItem_ + 1U) % 5U; Log::printf("ACTION", "menu item selected index=%u", menuItem_); showMenu(); leaveOpticalCalibration();
menuItem_ = nextMenuItem(menuItem_,
static_cast<TestGroup>(settings_.testGroup));
Log::printf("ACTION", "menu item selected index=%u", menuItem_);
showMenu();
} }
else if (modeEvent == ButtonEvent::LONG) { else if (modeEvent == ButtonEvent::LONG) {
leaveOpticalCalibration();
sanitizeRange(); const bool saved = store_.save(settings_); params_ = store_.params(settings_); sanitizeRange(); const bool saved = store_.save(settings_); params_ = store_.params(settings_);
Log::printf("ACTION", "settings menu saved and closed, NVS=%s", saved ? "OK" : "FAILED"); Log::printf("ACTION", "settings menu saved and closed, NVS=%s", saved ? "OK" : "FAILED");
state_ = AppState::IDLE; printConfiguration(); 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 showIdle();
} else if (startEvent == ButtonEvent::SHORT) changeMenu(+1); } else if (menuItem_ != MENU_OPTICAL_CALIBRATION_ITEM &&
else if (startEvent == ButtonEvent::LONG || startEvent == ButtonEvent::REPEAT) changeMenu(-1); startEvent == ButtonEvent::SHORT) changeMenu(+1);
else if (menuItem_ != MENU_OPTICAL_CALIBRATION_ITEM &&
(startEvent == ButtonEvent::LONG || startEvent == ButtonEvent::REPEAT)) changeMenu(-1);
updateOpticalCalibration(now);
return; return;
} }
if (state_ == AppState::SOLO_MEASURE) { if (state_ == AppState::SOLO_MEASURE) {
if (static_cast<int32_t>(now - localMeasurementDeadlineMs_) >= 0) {
Log::event("MEASURE", "local stage watchdog expired");
measurement_.forceFail(FailReason::LOST_EDGE);
}
const MeasureState ms = measurement_.update(); const MeasureState ms = measurement_.update();
if (ms == MeasureState::FAIL) { if (ms == MeasureState::FAIL) {
pwm_.stop();
printStageStats(measurement_.stats(), actual_.actualHz); printStageStats(measurement_.stats(), actual_.actualHz);
showStageResult(measurement_.stats()); showStageResult(measurement_.stats());
finish(false, measurement_.reason(), true); finish(false, measurement_.reason(), true);
} }
else if (ms == MeasureState::PASS) { else if (ms == MeasureState::PASS) {
pwm_.stop();
printStageStats(measurement_.stats(), actual_.actualHz); printStageStats(measurement_.stats(), actual_.actualHz);
showStageResult(measurement_.stats()); showStageResult(measurement_.stats());
stagePassed(); stagePassed();
} else if (ms == MeasureState::STEP_READY) { } else if (measurement_.takeProgressUpdate()) {
StageStats live = {}; StageStats live = {};
if (measurement_.statsSnapshot(live)) showStageResult(live); if (measurement_.statsSnapshot(live)) showStageResult(live);
measurement_.continueAfterDisplay(); }
} else if (state_ == AppState::SOLO_DRIVER) {
if (static_cast<int32_t>(now - localMeasurementDeadlineMs_) >= 0)
driverTest_.forceFail(FailReason::LOST_EDGE);
const DriverState ds = driverTest_.update();
if (ds == DriverState::SUBSAMPLE_DONE) {
// Capture is already stopped. Update the OLED only in this quiet gap,
// then restart the same PWM point and arm the next tenth of the sample.
pwm_.stop();
driverTest_.takeProgressUpdate();
showDriverResult(driverTest_.stats());
ActualPwm resumed = {};
if (!pwm_.start(requestedHz_, requestedPulseNs_, resumed)) {
driverTest_.forceFail(FailReason::RESOLUTION);
} else {
actual_ = resumed;
if (!driverTest_.resumeSubsample()) {
pwm_.stop();
driverTest_.forceFail(FailReason::DATA_LOSS);
}
}
} else if (ds == DriverState::FAIL) {
pwm_.stop(); receiver_.stop();
driverTest_.printSummary();
driverTest_.printTrace();
showDriverResult(driverTest_.stats());
finish(false, driverTest_.stats().reason, true);
} else if (ds == DriverState::PASS) {
pwm_.stop();
driverTest_.printSummary();
const bool finalPoint = stageIndex_ + 1U >= stageCount_;
showDriverResult(driverTest_.stats(), finalPoint);
stagePassed();
} }
} else if (state_ == AppState::MASTER_DISCOVER || state_ == AppState::MASTER_WAIT_READY || } else if (state_ == AppState::MASTER_DISCOVER || state_ == AppState::MASTER_WAIT_READY ||
state_ == AppState::MASTER_WAIT_RESULT || state_ == AppState::MASTER_FINALIZE) { state_ == AppState::MASTER_WAIT_RESULT || state_ == AppState::MASTER_FINALIZE) {
@@ -197,30 +482,317 @@ void App::update() {
void App::showIdle() { void App::showIdle() {
setActivePerformance(false); setActivePerformance(false);
setStandbyOpticalOutput();
lastUserActivityMs_ = millis(); lastUserActivityMs_ = millis();
char one[64]; snprintf(one, sizeof(one), "%s%s", UiText::MODE_PREFIX, char one[64];
uiRoleName(static_cast<Role>(settings_.role))); if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::BOARD) {
display_.show(one, UiText::START_RUN); 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() {
while (Serial.available() > 0) {
const int raw = Serial.read();
if (raw < 0) break;
const char c = static_cast<char>(raw);
lastUserActivityMs_ = millis();
leaveIdlePowerSave();
if (c == '\r') continue;
if (c == '\n') {
if (serialLineOverflow_) Serial.println("ERR command too long");
else if (serialLineLength_) {
serialLine_[serialLineLength_] = '\0';
handleSerialCommand(serialLine_);
}
serialLineLength_ = 0;
serialLineOverflow_ = false;
continue;
}
if (c < ' ' || c > '~') continue;
if (serialLineLength_ + 1U < sizeof(serialLine_))
serialLine_[serialLineLength_++] = c;
else serialLineOverflow_ = true;
}
}
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");
Serial.println(" set max 2000|5000|10000|20000|50000|100000|200000|500000");
Serial.println(" set min 250|500|1000|2000|5000|10000|50000");
Serial.println(" set accuracy 1|2|5|10");
Serial.println(" set time 100|250|500|1000|2000|5000 (ms)");
Serial.println(" set light HH|HL|LH|LL (DRIVER only)");
}
void App::printSerialStatus() {
if (!initialized_) {
Serial.println("STATUS initializing");
return;
}
params_ = store_.params(settings_);
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_),
usbHostPresent() ? "connected" : "disconnected");
}
bool App::serialSettingsMutable() const {
return initialized_ && (state_ == AppState::IDLE || state_ == AppState::FINISHED ||
state_ == AppState::MENU || state_ == AppState::SLAVE_READY);
}
void App::finishSerialSettingsChange() {
if (state_ == AppState::SLAVE_READY) radio_.end();
leaveOpticalCalibration();
state_ = AppState::IDLE;
sanitizeRange();
params_ = store_.params(settings_);
pwm_.configureActiveLight(configuredTxPulseLightOn(settings_));
const bool saved = store_.save(settings_);
Serial.printf("OK settings saved=%s\n", saved ? "yes" : "no");
if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::OPTICS &&
static_cast<Role>(settings_.role) == Role::SLAVE) armSlave();
else showIdle();
printSerialStatus();
}
void App::handleSerialCommand(char *line) {
lowerAscii(line);
char *save = nullptr;
char *command = strtok_r(line, " \t", &save);
char *name = strtok_r(nullptr, " \t", &save);
char *value = strtok_r(nullptr, " \t", &save);
char *extra = strtok_r(nullptr, " \t", &save);
if (!command) return;
if ((!strcmp(command, "help") || !strcmp(command, "?")) && !name) {
printSerialHelp();
return;
}
if ((!strcmp(command, "status") || !strcmp(command, "get")) && !name) {
printSerialStatus();
return;
}
if (!strcmp(command, "start") && !name) {
if (!initialized_) Serial.println("ERR still initializing");
else if (state_ == AppState::IDLE || state_ == AppState::FINISHED) {
Serial.println("OK test start requested");
startTest();
} else if (state_ == AppState::SLAVE_READY) Serial.println("OK slave already armed");
else Serial.printf("ERR busy state=%s\n", appStateName(state_));
return;
}
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::BOARD_TEST) {
Serial.println("OK board test stopped");
stopBoardTest();
} else if (state_ == AppState::MENU) {
leaveOpticalCalibration();
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 {
Serial.println("OK abort requested");
abortTest();
}
return;
}
if (!strcmp(command, "defaults") && !name) {
if (!serialSettingsMutable()) {
Serial.printf("ERR settings locked state=%s\n", appStateName(state_));
return;
}
store_.defaults(settings_);
finishSerialSettingsChange();
return;
}
if (strcmp(command, "set") || !name || !value || extra) {
Serial.println("ERR unknown command; send 'help'");
return;
}
if (!serialSettingsMutable()) {
Serial.printf("ERR settings locked state=%s; stop the test first\n", appStateName(state_));
return;
}
bool accepted = false;
uint32_t numeric = 0;
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; }
} else if (!strcmp(name, "test")) {
if (!strcmp(value, "optical")) { settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL); accepted = true; }
else if (!strcmp(value, "driver") && !TARGET_IS_C3 &&
static_cast<Role>(settings_.role) == Role::SOLO) {
settings_.testKind = static_cast<uint8_t>(TestKind::DRIVER); accepted = true;
}
} else if ((!strcmp(name, "frequency") || !strcmp(name, "freq")) && parseUnsigned(value, numeric)) {
const int index = optionIndex(PWM_FREQUENCY_OPTIONS_HZ, numeric);
if (index >= 0) { settings_.frequencyIndex = static_cast<uint8_t>(index); accepted = true; }
} else if ((!strcmp(name, "max") || !strcmp(name, "maxpulse")) && parseUnsigned(value, numeric)) {
const int index = optionIndex(MAX_PULSE_OPTIONS_NS, numeric);
if (index >= 0) { settings_.maxPulseIndex = static_cast<uint8_t>(index); accepted = true; }
} else if ((!strcmp(name, "min") || !strcmp(name, "minpulse")) && parseUnsigned(value, numeric)) {
const int index = optionIndex(MIN_PULSE_OPTIONS_NS, numeric);
if (index >= 0) { settings_.minPulseIndex = static_cast<uint8_t>(index); accepted = true; }
} else if (!strcmp(name, "accuracy")) {
const int index = accuracyOptionIndex(value);
if (index >= 0) { settings_.accuracyIndex = static_cast<uint8_t>(index); accepted = true; }
} else if ((!strcmp(name, "time") || !strcmp(name, "duration")) && parseUnsigned(value, numeric)) {
const int index = optionIndex(TEST_TIME_OPTIONS_MS, numeric);
if (index >= 0) { settings_.timeIndex = static_cast<uint8_t>(index); accepted = true; }
} else if (!strcmp(name, "light")) {
if (static_cast<TestKind>(settings_.testKind) != TestKind::DRIVER) {
Serial.println("ERR level setting is available only in DRIVER test");
return;
}
if (!strcmp(value, "hh")) { settings_.lightCode = static_cast<uint8_t>(LightCode::HH); accepted = true; }
else if (!strcmp(value, "hl")) { settings_.lightCode = static_cast<uint8_t>(LightCode::HL); accepted = true; }
else if (!strcmp(value, "lh")) { settings_.lightCode = static_cast<uint8_t>(LightCode::LH); accepted = true; }
else if (!strcmp(value, "ll")) { settings_.lightCode = static_cast<uint8_t>(LightCode::LL); accepted = true; }
}
if (!accepted) {
Serial.println("ERR invalid setting or value; send 'help'");
return;
}
finishSerialSettingsChange();
}
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) {
settings_.testKind = static_cast<uint8_t>(TestKind::DRIVER);
} else if (role == Role::SOLO) {
settings_.role = static_cast<uint8_t>(Role::MASTER);
settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
} else if (role == Role::MASTER) {
settings_.role = static_cast<uint8_t>(Role::SLAVE);
settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
} else {
settings_.role = static_cast<uint8_t>(Role::SOLO);
settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
}
} }
void App::sanitizeRange() { void App::sanitizeRange() {
settings_.startIndex %= countOf(START_FREQ_OPTIONS_HZ); if (settings_.testGroup > static_cast<uint8_t>(TestGroup::BOARD))
settings_.endIndex %= countOf(END_FREQ_OPTIONS_HZ); 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))
settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
if (settings_.lightCode > static_cast<uint8_t>(LightCode::LL))
settings_.lightCode = static_cast<uint8_t>(LightCode::HH);
if (settings_.role != static_cast<uint8_t>(Role::SOLO) ||
(TARGET_IS_C3 && settings_.testKind == static_cast<uint8_t>(TestKind::DRIVER)))
settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
settings_.frequencyIndex %= countOf(PWM_FREQUENCY_OPTIONS_HZ);
settings_.maxPulseIndex %= countOf(MAX_PULSE_OPTIONS_NS);
settings_.minPulseIndex %= countOf(MIN_PULSE_OPTIONS_NS);
settings_.accuracyIndex %= countOf(ACCURACY_OPTIONS_PCT);
settings_.timeIndex %= countOf(TEST_TIME_OPTIONS_MS);
const uint32_t hz = PWM_FREQUENCY_OPTIONS_HZ[settings_.frequencyIndex];
const uint8_t lastValid = lastValidMaxPulseIndex(hz);
if (settings_.maxPulseIndex > lastValid) settings_.maxPulseIndex = lastValid;
const uint8_t lastMin = lastMinPulseIndexAtMost(MAX_PULSE_OPTIONS_NS[settings_.maxPulseIndex]);
if (settings_.minPulseIndex > lastMin) settings_.minPulseIndex = lastMin;
if (settings_.testKind == static_cast<uint8_t>(TestKind::DRIVER)) {
const uint8_t driverLastMin = lastMinPulseIndexAtMost(
MAX_PULSE_OPTIONS_NS[settings_.maxPulseIndex]);
const uint8_t firstDriverMin = firstMinPulseIndexAtLeast(
DRIVER_MIN_INPUT_PULSE_NS, driverLastMin);
if (settings_.minPulseIndex < firstDriverMin)
settings_.minPulseIndex = firstDriverMin;
}
} }
void App::changeMenu(int d) { void App::changeMenu(int d) {
sanitizeRange(); sanitizeRange();
uint8_t *value = nullptr; size_t count = 0; if (menuItem_ == 0) {
switch (menuItem_) { settings_.testGroup = cycleIndex(settings_.testGroup, 0,
case 0: value = &settings_.startIndex; count = countOf(START_FREQ_OPTIONS_HZ); break; static_cast<uint8_t>(TestGroup::BOARD), d);
case 1: value = &settings_.endIndex; count = countOf(END_FREQ_OPTIONS_HZ); break; } else if (menuItem_ == 2) {
case 2: value = &settings_.accuracyIndex; count = countOf(ACCURACY_OPTIONS_PCT); break; const uint8_t last = lastValidMaxPulseIndex(
case 3: value = &settings_.timeIndex; count = countOf(TEST_TIME_OPTIONS_MS); break; PWM_FREQUENCY_OPTIONS_HZ[settings_.frequencyIndex]);
default: value = &settings_.dutyIndex; count = countOf(DUTY_OPTIONS_PCT); break; const uint8_t first = firstMaxPulseIndexAtLeast(
MIN_PULSE_OPTIONS_NS[settings_.minPulseIndex], last);
settings_.maxPulseIndex = cycleIndex(settings_.maxPulseIndex,
first, last, d);
} 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
? firstMinPulseIndexAtLeast(DRIVER_MIN_INPUT_PULSE_NS, last) : 0U;
settings_.minPulseIndex = cycleIndex(settings_.minPulseIndex, first, last, d);
} else {
uint8_t *value = nullptr; size_t count = 0;
switch (menuItem_) {
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;
}
value = &settings_.lightCode; count = 4; break;
default: return;
}
*value = cycleIndex(*value, 0, static_cast<uint8_t>(count - 1U), d);
} }
*value = static_cast<uint8_t>((*value + count + d) % count); sanitizeRange(); params_ = store_.params(settings_);
Log::printf("ACTION", "menu item=%u changed direction=%+d new-index=%u", menuItem_, d, *value); pwm_.configureActiveLight(configuredTxPulseLightOn(settings_));
sanitizeRange(); params_ = store_.params(settings_); showMenu(); Log::printf("ACTION", "menu item=%u changed direction=%+d frequency=%u max-pulse=%u min-pulse=%u accuracy=%u time=%u light=%s",
menuItem_, d, settings_.frequencyIndex, settings_.maxPulseIndex,
settings_.minPulseIndex, settings_.accuracyIndex, settings_.timeIndex,
configuredLevelName(settings_));
showMenu();
} }
void App::showMenu() { void App::showMenu() {
@@ -229,70 +801,237 @@ void App::showMenu() {
Display::formatDuration(actualNominalTotalUs(), all, sizeof(all)); Display::formatDuration(actualNominalTotalUs(), all, sizeof(all));
switch (menuItem_) { switch (menuItem_) {
case 0: case 0:
Display::formatTestFrequency(params_.startHz, value, sizeof(value)); snprintf(value, sizeof(value), "%s",
strncat(value, UiText::FREQUENCY_UNIT, sizeof(value) - strlen(value) - 1U); uiTestGroupName(static_cast<TestGroup>(settings_.testGroup)));
label = UiText::MENU_START_FREQUENCY; label = UiText::MENU_TEST_GROUP;
break; break;
case 1: case 1:
Display::formatTestFrequency(params_.endHz, value, sizeof(value)); Display::formatPwmFrequency(params_.frequencyHz, value, sizeof(value));
strncat(value, UiText::FREQUENCY_UNIT, sizeof(value) - strlen(value) - 1U); label = UiText::MENU_FREQUENCY;
label = UiText::MENU_END_FREQUENCY;
break; break;
case 2: case 2:
Display::formatPulse(params_.maxPulseNs, value, sizeof(value));
label = UiText::MENU_MAX_PULSE;
break;
case 3:
Display::formatPulse(params_.minPulseNs, value, sizeof(value));
label = UiText::MENU_MIN_PULSE;
break;
case 4:
snprintf(value, sizeof(value), "+/-%g%%", params_.accuracyPct); snprintf(value, sizeof(value), "+/-%g%%", params_.accuracyPct);
label = UiText::MENU_ACCURACY; label = UiText::MENU_ACCURACY;
break; break;
case 3: case 5:
snprintf(value, sizeof(value), "%.1fs", params_.testTimeMs / 1000.0f); snprintf(value, sizeof(value), "%.1fs", params_.testTimeMs / 1000.0f);
label = UiText::MENU_TEST_TIME; label = UiText::MENU_TEST_TIME;
break; break;
default: case 6: {
snprintf(value, sizeof(value), "%u%%", params_.dutyPct); if (static_cast<TestKind>(settings_.testKind) != TestKind::DRIVER) {
label = UiText::MENU_PWM_DUTY; snprintf(value, sizeof(value), "%s", UiText::LIGHT_AUTO);
break; label = UiText::MENU_LIGHT_CODE;
formatMenuLine(label, value, one, sizeof(one));
display_.show(one, UiText::LIGHT_AUTO_FORMAT);
return;
}
const char *code = lightCodeName(static_cast<LightCode>(settings_.lightCode));
snprintf(value, sizeof(value), "%s", code);
label = UiText::MENU_LIGHT_CODE;
formatMenuLine(label, value, one, sizeof(one));
snprintf(total, sizeof(total), UiText::LIGHT_CODE_FORMAT, code[0], code[1]);
display_.show(one, total);
return;
}
case MENU_OPTICAL_CALIBRATION_ITEM:
enterOpticalCalibration(millis());
return;
default: return;
} }
formatMenuLine(label, value, one, sizeof(one)); 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); display_.show(one, total);
} }
void App::enterOpticalCalibration(uint32_t now) {
if (opticalCalibrationActive_) return;
optical_current_begin();
pwm_.lightOn();
opticalCalibrationActive_ = true;
opticalCalibrationStartedMs_ = now;
opticalCalibrationUpdatedMs_ = 0;
display_.show("Idiode: --.- mA", "Vcc: --.-- V");
Log::printf("CALIB", "optical LED ON; sense GPIO=%u, VCC GPIO=%u, settling %lums",
GPIO_OPTICAL_CURRENT, GPIO_OPTICAL_VCC, OPTICAL_CURRENT_SETTLE_MS);
}
void App::updateOpticalCalibration(uint32_t now, bool force) {
if (!opticalCalibrationActive_ ||
now - opticalCalibrationStartedMs_ < OPTICAL_CURRENT_SETTLE_MS) return;
(void)force;
OpticalCurrentMeasurement reading = {};
if (!optical_current_poll(reading)) return;
opticalCalibrationUpdatedMs_ = now;
char current[32], vcc[24];
snprintf(current, sizeof(current), "Idiode: %.1f mA", reading.currentMa);
snprintf(vcc, sizeof(vcc), "Vcc: %.2f V", reading.vccVoltage);
display_.show(current, vcc);
Log::printf("CALIB",
"raw sense=%u vcc=%u | ADC sense=%.3fV vcc=%.3fV | Usense=%.3fV Vcc=%.3fV | Idiode=%.1fmA",
reading.senseRaw, reading.vccRaw, reading.senseAdcVoltage,
reading.vccAdcVoltage, reading.senseVoltage, reading.vccVoltage,
reading.currentMa);
}
void App::leaveOpticalCalibration() {
if (!opticalCalibrationActive_) return;
pwm_.stop();
opticalCalibrationActive_ = false;
Log::event("CALIB", "optical LED OFF; calibration menu left");
}
void App::startTest() { void App::startTest() {
leaveIdlePowerSave(); leaveIdlePowerSave();
pwm_.stop();
setActivePerformance(true); setActivePerformance(true);
params_ = store_.params(settings_); stageCount_ = frequencyPointCount(params_.startHz, params_.endHz); sanitizeRange();
stageIndex_ = 0; requestedHz_ = 0; pendingReason_ = FailReason::NONE; 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; havePeer_ = false; lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0;
if (!stageCount_) { finish(false, FailReason::UNSUPPORTED); return; } if (!stageCount_) { finish(false, FailReason::UNSUPPORTED); return; }
Log::printf("TEST", "starting role=%s stages=%lu", roleName(static_cast<Role>(settings_.role)), stageCount_); Log::printf("TEST", "starting role=%s test=%s light=%s stages=%lu",
roleName(static_cast<Role>(settings_.role)),
testKindName(static_cast<TestKind>(settings_.testKind)),
configuredLevelName(settings_), stageCount_);
if (SERIAL_MINIMAL_LOG) { if (SERIAL_MINIMAL_LOG) {
char startText[12], endText[12]; Log::printf("CONFIG", "mode=%s/%s frequency=%luHz pulse=%lu..%luns accuracy=%.2f%% time=%lums LIGHT=%s stages=%lu",
Display::formatFrequency(params_.startHz, startText, sizeof(startText)); roleName(static_cast<Role>(settings_.role)),
Display::formatFrequency(params_.endHz, endText, sizeof(endText)); testKindName(static_cast<TestKind>(settings_.testKind)), params_.frequencyHz,
Log::printf("CONFIG", "mode=%s range=%s..%s adjacent accuracy=%.2f%% time=%lums duty=%u%% stages=%lu", params_.maxPulseNs, params_.minPulseNs, params_.accuracyPct, params_.testTimeMs,
roleName(static_cast<Role>(settings_.role)), startText, endText, configuredLevelName(settings_),
params_.accuracyPct, params_.testTimeMs, params_.dutyPct, stageCount_); stageCount_);
} }
printConfiguration(); printConfiguration();
const Role role = static_cast<Role>(settings_.role); const Role role = static_cast<Role>(settings_.role);
if (role == Role::SOLO) { if (role == Role::SOLO) {
if (!prepareStage()) return; if (!prepareStage()) return;
state_ = AppState::SOLO_MEASURE; state_ = static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER
? AppState::SOLO_DRIVER : AppState::SOLO_MEASURE;
} else if (!radio_.begin()) finish(false, FailReason::LINK_LOST); } else if (!radio_.begin()) finish(false, FailReason::LINK_LOST);
else if (role == Role::MASTER) startMasterDiscovery(); else if (role == Role::MASTER) startMasterDiscovery();
else { state_ = AppState::SLAVE_READY; Log::event("TEST", "Slave armed and waiting for Master"); display_.show(UiText::SLAVE_READY, UiText::WAIT_MASTER); } 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) { bool App::armSlave(bool preserveDisplay) {
setActivePerformance(false); setActivePerformance(false);
pwm_.stop();
lastUserActivityMs_ = millis(); lastUserActivityMs_ = millis();
params_ = store_.params(settings_); params_ = store_.params(settings_);
stageIndex_ = 0; stageCount_ = frequencyPointCount(params_.startHz, params_.endHz); stageIndex_ = 0; stageCount_ = pulseWidthPointCount(params_.maxPulseNs, params_.minPulseNs);
requestedHz_ = 0; session_ = 0; sequence_ = 0; havePeer_ = false; requestedHz_ = params_.frequencyHz; requestedPulseNs_ = 0; session_ = 0; sequence_ = 0; havePeer_ = false;
lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0; retries_ = 0; slaveRearmAtMs_ = 0; lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0; retries_ = 0; slaveRearmAtMs_ = 0;
if (!radio_.begin()) { if (!radio_.begin()) {
state_ = AppState::FINISHED; pendingReason_ = FailReason::LINK_LOST; state_ = AppState::FINISHED; pendingReason_ = FailReason::LINK_LOST;
slaveRearmAtMs_ = millis() + LINK_HEARTBEAT_TIMEOUT_MS; slaveRearmAtMs_ = millis() + LINK_HEARTBEAT_TIMEOUT_MS;
display_.show(UiText::LINK_FAILED, UiText::RADIO_ERROR); display_.show(UiText::LINK_FAILED, UiText::RADIO_ERROR,
0, 0, roleCorner(Role::SLAVE));
return false; return false;
} }
radio_.setWindowedReceive(true); radio_.setWindowedReceive(true);
@@ -303,52 +1042,102 @@ bool App::armSlave(bool preserveDisplay) {
} }
bool App::prepareStage(bool showProgress) { bool App::prepareStage(bool showProgress) {
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, stageIndex_); requestedHz_ = params_.frequencyHz;
requestedPulseNs_ = pulseWidthAt(params_.maxPulseNs, params_.minPulseNs, stageIndex_);
actual_ = {}; actual_ = {};
const uint32_t maxHz = TARGET_IS_C3 ? C3_STRICT_MAX_HZ : const uint32_t maxHz = TARGET_IS_C3 ? C3_STRICT_MAX_HZ :
(receiver_.highRateBackend() ? S3_STRICT_MAX_HZ : C3_STRICT_MAX_HZ); (receiver_.highRateBackend() ? S3_STRICT_MAX_HZ : C3_STRICT_MAX_HZ);
if (requestedHz_ > maxHz) { finish(false, FailReason::UNSUPPORTED); return false; } if (requestedHz_ > maxHz) { finish(false, FailReason::UNSUPPORTED); return false; }
Log::printf("PWM", "starting GPIO=%u requested=%luHz duty=%u%%", GPIO_PWM, requestedHz_, params_.dutyPct); Log::printf("PWM", "starting GPIO=%u requested=%luHz pulse=%luns", GPIO_PWM, requestedHz_, requestedPulseNs_);
if (!pwm_.start(requestedHz_, params_.dutyPct, actual_)) { if (!pwm_.start(requestedHz_, requestedPulseNs_, actual_)) {
Log::printf("PWM", "START FAILED GPIO=%u requested=%luHz; LEDC attach/write/read failed", Log::printf("PWM", "START FAILED GPIO=%u requested=%luHz pulse=%luns; PWM setup failed",
GPIO_PWM, requestedHz_); GPIO_PWM, requestedHz_, requestedPulseNs_);
finish(false, FailReason::RESOLUTION); return false; finish(false, FailReason::RESOLUTION); return false;
} }
const uint32_t plannedRxHz = receiver_.plannedTickHz(actual_.actualHz, actual_.actualDutyPct); if (!periodWithin(actual_.actualHz, requestedHz_, params_.accuracyPct) ||
const FailReason resolution = validateResolution(actual_.actualHz, actual_.actualDutyPct, params_.accuracyPct, !periodWithin(actual_.actualPulseNs, requestedPulseNs_, params_.accuracyPct)) {
plannedRxHz, actual_.bits); Log::printf("PWM", "requested point cannot be generated within tolerance: requested=%luHz/%luns actual=%luHz/%luns tolerance=%.2f%%",
if (resolution != FailReason::NONE) { requestedHz_, requestedPulseNs_, actual_.actualHz, actual_.actualPulseNs,
Log::printf("PWM", "resolution rejected: actual=%luHz duty=%.3f%% bits=%u RXclock=%luHz tolerance=%.3f%%", params_.accuracyPct);
actual_.actualHz, actual_.actualDutyPct, actual_.bits, plannedRxHz, finish(false, FailReason::RESOLUTION); return false;
effectiveTolerancePct(params_.accuracyPct));
finish(false, resolution); return false;
} }
Log::printf("PWM", "stage=%lu/%lu requested=%luHz actual=%luHz duty=%.2f%% bits=%u STARTED", const bool driverMode = static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER;
stageIndex_ + 1, stageCount_, requestedHz_, actual_.actualHz, actual_.actualDutyPct, actual_.bits); if (!driverMode) {
if (showProgress) showStageProgress(); const uint32_t plannedRxHz = receiver_.plannedTickHz(actual_.actualHz, actual_.actualDutyPct);
if (static_cast<Role>(settings_.role) == Role::SOLO && !startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct)) { const uint32_t plannedPulseRxHz = receiver_.plannedPulseTickHz(
actual_.actualHz, actual_.actualDutyPct);
const FailReason resolution = validateResolution(actual_.actualHz, actual_.actualDutyPct,
params_.accuracyPct, plannedRxHz, plannedPulseRxHz, actual_.bits,
MEASUREMENT_AVERAGING_PERIODS);
if (resolution != FailReason::NONE) {
Log::printf("PWM", "resolution rejected: actual=%luHz duty=%.3f%% bits=%u period-capture=%luHz pulse-capture=%luHz tolerance=%.3f%%",
actual_.actualHz, actual_.actualDutyPct, actual_.bits, plannedRxHz,
plannedPulseRxHz, effectiveTolerancePct(params_.accuracyPct));
finish(false, resolution); return false;
}
} else if (!receiver_.highRateBackend()) {
finish(false, FailReason::UNSUPPORTED); return false; finish(false, FailReason::UNSUPPORTED); return false;
} }
Log::printf("PWM", "stage=%lu/%lu requested=%luHz/%luns actual=%luHz/%luns duty=%.3f%% bits=%u STARTED",
stageIndex_ + 1, stageCount_, requestedHz_, requestedPulseNs_, actual_.actualHz,
actual_.actualPulseNs, actual_.actualDutyPct, actual_.bits);
if (showProgress) showStageProgress();
if (static_cast<Role>(settings_.role) == Role::SOLO) {
const bool started = driverMode ? startDriverMeasurement() :
startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct);
if (!started) { finish(false, FailReason::UNSUPPORTED); return false; }
}
return true; return true;
} }
bool App::startLocalMeasurement(float hz, float duty) { bool App::startLocalMeasurement(float hz, float duty) {
Log::printf("MEASURE", "arming expected=%.3fHz duty=%.3f%% tolerance=%.3f%% RX=%luHz settle=%u cycles window=%lums; per-pulse logging suspended", Log::printf("MEASURE", "arming expected=%.3fHz duty=%.3f%% tolerance=%.3f%% period-capture=%luHz pulse-capture=%luHz settle=%u cycles window=%lums; every pulse validated",
hz, duty, effectiveTolerancePct(params_.accuracyPct), receiver_.plannedTickHz(static_cast<uint32_t>(hz + 0.5f), duty), hz, duty, effectiveTolerancePct(params_.accuracyPct), receiver_.plannedTickHz(static_cast<uint32_t>(hz + 0.5f), duty),
receiver_.plannedPulseTickHz(static_cast<uint32_t>(hz + 0.5f), duty),
PWM_SETTLE_CYCLES, params_.testTimeMs); PWM_SETTLE_CYCLES, params_.testTimeMs);
const bool ok = measurement_.start(hz, duty, params_.accuracyPct, params_.testTimeMs, PWM_SETTLE_CYCLES); const bool ok = measurement_.start(hz, duty, params_.accuracyPct, params_.testTimeMs,
Log::printf("MEASURE", "receiver start %s, RMT chunk=%u symbols", ok ? "OK" : "FAILED", MEASUREMENT_AVERAGING_PERIODS, PWM_SETTLE_CYCLES, true);
receiver_.receiveChunkSymbols()); const uint32_t nominalMs = stageWallTimeMs(params_.testTimeMs,
static_cast<uint32_t>(hz + 0.5f));
const uint64_t watchdogMs = static_cast<uint64_t>(nominalMs) * 2ULL + 2000ULL;
localMeasurementDeadlineMs_ = millis() + static_cast<uint32_t>(
watchdogMs > UINT32_MAX ? UINT32_MAX : watchdogMs);
Log::printf("MEASURE", "receiver start %s, continuous edge capture", ok ? "OK" : "FAILED");
return ok;
}
bool App::startDriverMeasurement() {
const bool ok = driverTest_.start(actual_.actualHz, actual_.actualPulseNs,
params_.accuracyPct, params_.testTimeMs, PWM_SETTLE_CYCLES,
txActiveLightOn(settings_), rxActiveLightOn(settings_));
const uint32_t nominalMs = stageWallTimeMs(params_.testTimeMs, actual_.actualHz);
const uint64_t watchdogMs = static_cast<uint64_t>(nominalMs) * 2ULL + 2000ULL;
localMeasurementDeadlineMs_ = millis() + static_cast<uint32_t>(
watchdogMs > UINT32_MAX ? UINT32_MAX : watchdogMs);
if (!ok) Log::event("DRIVER", "response test start FAILED");
return ok; return ok;
} }
void App::stagePassed() { void App::stagePassed() {
Log::printf("TEST", "stage %lu/%lu PASS; PWM stopping", stageIndex_ + 1, stageCount_); Log::printf("TEST", "stage %lu/%lu PASS; PWM stopping", stageIndex_ + 1, stageCount_);
pwm_.stop(); pwm_.stop();
if (++stageIndex_ >= stageCount_) { finish(true, FailReason::NONE); return; } // With PWM already quiet it is safe to stop capture before clearing its
if (static_cast<Role>(settings_.role) == Role::SOLO) { if (prepareStage()) state_ = AppState::SOLO_MEASURE; } // queue for the next pulse width. Never reset a FreeRTOS queue concurrently
// with the capture ISR.
if (static_cast<Role>(settings_.role) == Role::SOLO) receiver_.stop();
if (++stageIndex_ >= stageCount_) {
const bool preserveDriverMeasurements =
static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER;
finish(true, FailReason::NONE, preserveDriverMeasurements);
return;
}
if (static_cast<Role>(settings_.role) == Role::SOLO) {
if (prepareStage()) state_ = static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER
? AppState::SOLO_DRIVER : AppState::SOLO_MEASURE;
}
else if (static_cast<Role>(settings_.role) == Role::MASTER) { else if (static_cast<Role>(settings_.role) == Role::MASTER) {
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, stageIndex_); requestedHz_ = params_.frequencyHz;
requestedPulseNs_ = pulseWidthAt(params_.maxPulseNs, params_.minPulseNs, stageIndex_);
actual_ = {}; actual_ = {};
stageStartConfirmed_ = false; stageStartConfirmed_ = false;
pendingPacket_ = makePacket(MessageType::PREPARE); sendCurrent(MessageType::PREPARE); pendingPacket_ = makePacket(MessageType::PREPARE); sendCurrent(MessageType::PREPARE);
@@ -358,7 +1147,11 @@ void App::stagePassed() {
void App::startMasterDiscovery() { void App::startMasterDiscovery() {
session_ = esp_random(); if (!session_) session_ = 1; session_ = esp_random(); if (!session_) session_ = 1;
sequence_ = 1; stageIndex_ = 0; requestedHz_ = 0; havePeer_ = false; radio_.flush(); sequence_ = 1; stageIndex_ = 0; requestedHz_ = params_.frequencyHz;
requestedPulseNs_ = 0; havePeer_ = false; radio_.flush();
opticalWakeActive_ = true;
lastOpticalWakeToggleMs_ = millis();
pwm_.lightOn();
pendingPacket_ = makePacket(MessageType::DISCOVER); radio_.sendBroadcast(pendingPacket_); pendingPacket_ = makePacket(MessageType::DISCOVER); radio_.sendBroadcast(pendingPacket_);
lastSendMs_ = millis(); retries_ = 0; lastSendMs_ = millis(); retries_ = 0;
state_ = AppState::MASTER_DISCOVER; Log::printf("ESP-NOW", "discovery started session=%08lX", session_); state_ = AppState::MASTER_DISCOVER; Log::printf("ESP-NOW", "discovery started session=%08lX", session_);
@@ -369,11 +1162,11 @@ ProtocolPacket App::makePacket(MessageType type) const {
ProtocolPacket p = {}; ProtocolPacket p = {};
p.type = static_cast<uint8_t>(type); p.session = session_; p.stage = stageIndex_; p.type = static_cast<uint8_t>(type); p.session = session_; p.stage = stageIndex_;
p.stageCount = static_cast<uint16_t>(stageCount_); p.sequence = sequence_; p.stageCount = static_cast<uint16_t>(stageCount_); p.sequence = sequence_;
p.requestedHz = requestedHz_; p.actualHz = actual_.actualHz; p.requestedHz = requestedHz_; p.requestedPulseNs = requestedPulseNs_;
const float packetDuty = actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct; p.actualHz = actual_.actualHz; p.actualPulseNs = actual_.actualPulseNs;
p.actualDutyX100 = static_cast<uint16_t>(packetDuty * 100.0f + 0.5f);
p.testTimeMs = params_.testTimeMs; p.testTimeMs = params_.testTimeMs;
p.accuracyX100 = static_cast<uint16_t>(params_.accuracyPct * 100.0f + 0.5f); p.settleCycles = PWM_SETTLE_CYCLES; p.accuracyX100 = static_cast<uint16_t>(params_.accuracyPct * 100.0f + 0.5f);
p.lightCode = settings_.lightCode;
return p; return p;
} }
@@ -421,6 +1214,7 @@ void App::handleRadio() {
if ((state_ == AppState::SLAVE_READY || state_ == AppState::SLAVE_WAIT_START) && if ((state_ == AppState::SLAVE_READY || state_ == AppState::SLAVE_WAIT_START) &&
type == MessageType::DISCOVER && (!havePeer_ || !memcmp(peer_, r.mac, 6))) { type == MessageType::DISCOVER && (!havePeer_ || !memcmp(peer_, r.mac, 6))) {
leaveIdlePowerSave(); leaveIdlePowerSave();
pwm_.stop();
setActivePerformance(true); setActivePerformance(true);
radio_.setWindowedReceive(false); radio_.setWindowedReceive(false);
memcpy(peer_, r.mac, 6); havePeer_ = true; session_ = r.packet.session; stageIndex_ = 0; sequence_ = r.packet.sequence; memcpy(peer_, r.mac, 6); havePeer_ = true; session_ = r.packet.session; stageIndex_ = 0; sequence_ = r.packet.sequence;
@@ -429,8 +1223,11 @@ void App::handleRadio() {
state_ = AppState::SLAVE_WAIT_START; display_.show(UiText::MASTER_SEEN, UiText::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_) { if (state_ == AppState::MASTER_DISCOVER && type == MessageType::DISCOVER_ACK && r.packet.session == session_) {
opticalWakeActive_ = false;
pwm_.stop();
memcpy(peer_, r.mac, 6); havePeer_ = true; lastPeerSeenMs_ = lastHeartbeatMs_ = millis(); memcpy(peer_, r.mac, 6); havePeer_ = true; lastPeerSeenMs_ = lastHeartbeatMs_ = millis();
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, stageIndex_); requestedHz_ = params_.frequencyHz;
requestedPulseNs_ = pulseWidthAt(params_.maxPulseNs, params_.minPulseNs, stageIndex_);
sendCurrent(MessageType::PREPARE); state_ = AppState::MASTER_WAIT_READY; retries_ = 0; deadlineMs_ = millis() + LINK_REPLY_TIMEOUT_MS; sendCurrent(MessageType::PREPARE); state_ = AppState::MASTER_WAIT_READY; retries_ = 0; deadlineMs_ = millis() + LINK_REPLY_TIMEOUT_MS;
char mac[20]; Radio::macText(peer_, mac, sizeof(mac)); Log::printf("ESP-NOW", "Slave selected %s", mac); continue; char mac[20]; Radio::macText(peer_, mac, sizeof(mac)); Log::printf("ESP-NOW", "Slave selected %s", mac); continue;
} }
@@ -456,7 +1253,10 @@ void App::handleRadio() {
sequence_ = r.packet.sequence; sequence_ = r.packet.sequence;
state_ = AppState::SLAVE_WAIT_START; state_ = AppState::SLAVE_WAIT_START;
params_.testTimeMs = r.packet.testTimeMs; params_.testTimeMs = r.packet.testTimeMs;
params_.accuracyPct = r.packet.accuracyX100 / 100.0f; requestedHz_ = r.packet.requestedHz; params_.accuracyPct = r.packet.accuracyX100 / 100.0f;
if (r.packet.lightCode <= static_cast<uint8_t>(LightCode::LL))
settings_.lightCode = r.packet.lightCode;
requestedHz_ = r.packet.requestedHz; requestedPulseNs_ = r.packet.requestedPulseNs;
stageCount_ = r.packet.stageCount; stageCount_ = r.packet.stageCount;
actual_ = {}; actual_ = {};
ProtocolPacket ready = makePacket(MessageType::READY); ProtocolPacket ready = makePacket(MessageType::READY);
@@ -470,8 +1270,10 @@ void App::handleRadio() {
r.packet.reason <= static_cast<uint8_t>(FailReason::ABORTED) r.packet.reason <= static_cast<uint8_t>(FailReason::ABORTED)
? static_cast<FailReason>(r.packet.reason) : FailReason::ABORTED; ? static_cast<FailReason>(r.packet.reason) : FailReason::ABORTED;
if (r.packet.requestedHz) requestedHz_ = r.packet.requestedHz; if (r.packet.requestedHz) requestedHz_ = r.packet.requestedHz;
if (r.packet.requestedPulseNs) requestedPulseNs_ = r.packet.requestedPulseNs;
actual_.actualHz = r.packet.actualHz ? r.packet.actualHz : requestedHz_; actual_.actualHz = r.packet.actualHz ? r.packet.actualHz : requestedHz_;
actual_.actualDutyPct = r.packet.actualDutyX100 ? r.packet.actualDutyX100 / 100.0f : params_.dutyPct; actual_.actualPulseNs = r.packet.actualPulseNs ? r.packet.actualPulseNs : requestedPulseNs_;
actual_.actualDutyPct = dutyFromPulse(actual_.actualHz, actual_.actualPulseNs);
measurement_.abort(); finish(false, reason); continue; measurement_.abort(); finish(false, reason); continue;
} }
if (state_ == AppState::MASTER_WAIT_READY && type == MessageType::READY) { if (state_ == AppState::MASTER_WAIT_READY && type == MessageType::READY) {
@@ -507,7 +1309,8 @@ void App::handleRadio() {
sendLinked(pendingPacket_); sendLinked(pendingPacket_);
} else if (state_ == AppState::SLAVE_WAIT_START && type == MessageType::START_STAGE) { } else if (state_ == AppState::SLAVE_WAIT_START && type == MessageType::START_STAGE) {
sequence_ = r.packet.sequence; sequence_ = r.packet.sequence;
actual_.actualHz = r.packet.actualHz; actual_.actualDutyPct = r.packet.actualDutyX100 / 100.0f; actual_.actualHz = r.packet.actualHz; actual_.actualPulseNs = r.packet.actualPulseNs;
actual_.actualDutyPct = dutyFromPulse(actual_.actualHz, actual_.actualPulseNs);
if (!startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct)) { finish(false, FailReason::UNSUPPORTED); continue; } if (!startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct)) { finish(false, FailReason::UNSUPPORTED); continue; }
showStageProgress(); showStageProgress();
state_ = AppState::SLAVE_MEASURE; state_ = AppState::SLAVE_MEASURE;
@@ -521,6 +1324,10 @@ void App::handleRadio() {
started.sequence = r.packet.sequence; sendLinked(started); started.sequence = r.packet.sequence; sendLinked(started);
} else if (state_ == AppState::SLAVE_WAIT_ACK && type == MessageType::ACK && r.packet.sequence == pendingPacket_.sequence) { } else if (state_ == AppState::SLAVE_WAIT_ACK && type == MessageType::ACK && r.packet.sequence == pendingPacket_.sequence) {
if (pendingPacket_.passed) { if (pendingPacket_.passed) {
// Master sends ACK only after stopping its PWM. Disable capture for
// every completed stage, so the next start can clear its queue without
// racing the ISR (not only after the final stage).
receiver_.stop();
if (r.packet.passed) { if (r.packet.passed) {
radio_.end(); pendingReason_ = FailReason::NONE; radio_.end(); pendingReason_ = FailReason::NONE;
if (armSlave(true)) display_.show(UiText::PASS_WORD, UiText::WAIT_MASTER); if (armSlave(true)) display_.show(UiText::PASS_WORD, UiText::WAIT_MASTER);
@@ -537,6 +1344,12 @@ void App::handleRadio() {
void App::updateMaster() { void App::updateMaster() {
const uint32_t now = millis(); const uint32_t now = millis();
if (state_ == AppState::MASTER_DISCOVER) { if (state_ == AppState::MASTER_DISCOVER) {
if (now - lastOpticalWakeToggleMs_ >= OPTICAL_WAKE_HALF_PERIOD_MS) {
opticalWakeActive_ = !opticalWakeActive_;
if (opticalWakeActive_) pwm_.lightOn();
else pwm_.stop();
lastOpticalWakeToggleMs_ = now;
}
if (now - lastSendMs_ >= DISCOVERY_RETRY_INTERVAL_MS) { if (now - lastSendMs_ >= DISCOVERY_RETRY_INTERVAL_MS) {
if (++retries_ % 50U == 0U) Log::event("ESP-NOW", "DISCOVER burst continues"); if (++retries_ % 50U == 0U) Log::event("ESP-NOW", "DISCOVER burst continues");
radio_.sendBroadcast(pendingPacket_); radio_.sendBroadcast(pendingPacket_);
@@ -567,8 +1380,12 @@ void App::updateSlave() {
updateHeartbeat(); updateHeartbeat();
if (state_ == AppState::FINISHED) return; if (state_ == AppState::FINISHED) return;
if (state_ == AppState::SLAVE_MEASURE) { if (state_ == AppState::SLAVE_MEASURE) {
if (static_cast<int32_t>(millis() - localMeasurementDeadlineMs_) >= 0) {
Log::event("MEASURE", "Slave local stage watchdog expired");
measurement_.forceFail(FailReason::LOST_EDGE);
}
const MeasureState ms = measurement_.update(); const MeasureState ms = measurement_.update();
if (ms == MeasureState::STEP_READY) { if (measurement_.takeProgressUpdate()) {
StageStats live = {}; StageStats live = {};
if (measurement_.statsSnapshot(live)) { if (measurement_.statsSnapshot(live)) {
ProtocolPacket progress = makePacket(MessageType::PROGRESS); ProtocolPacket progress = makePacket(MessageType::PROGRESS);
@@ -577,7 +1394,6 @@ void App::updateSlave() {
progress.sequence = sequence_; sendLinked(progress); progress.sequence = sequence_; sendLinked(progress);
showStageResult(live); showStageResult(live);
} }
measurement_.continueAfterDisplay();
return; return;
} }
if (ms != MeasureState::PASS && ms != MeasureState::FAIL) { if (ms != MeasureState::PASS && ms != MeasureState::FAIL) {
@@ -607,13 +1423,16 @@ void App::sendAbort(FailReason reason) {
++sequence_; ++sequence_;
ProtocolPacket packet = makePacket(MessageType::ABORT); ProtocolPacket packet = makePacket(MessageType::ABORT);
packet.reason = static_cast<uint8_t>(reason); packet.reason = static_cast<uint8_t>(reason);
if (!packet.actualDutyX100) packet.actualDutyX100 = params_.dutyPct * 100U; if (!packet.actualPulseNs) packet.actualPulseNs = requestedPulseNs_;
sendLinked(packet); sendLinked(packet);
} }
void App::abortTest() { void App::abortTest() {
Log::event("ACTION", "abort requested: sending ABORT, stopping receiver and PWM"); Log::event("ACTION", "abort requested: sending ABORT, stopping receiver and PWM");
sendAbort(FailReason::ABORTED); measurement_.abort(); finish(false, FailReason::ABORTED); sendAbort(FailReason::ABORTED);
measurement_.abort();
driverTest_.abort();
finish(false, FailReason::ABORTED);
} }
void App::finish(bool pass, FailReason reason, bool preserveDisplay) { void App::finish(bool pass, FailReason reason, bool preserveDisplay) {
@@ -637,18 +1456,26 @@ void App::finish(bool pass, FailReason reason, bool preserveDisplay) {
if (!pass && masterActive && havePeer_ && reason != FailReason::ABORTED) sendAbort(reason); if (!pass && masterActive && havePeer_ && reason != FailReason::ABORTED) sendAbort(reason);
if (state_ != AppState::IDLE && state_ != AppState::MENU) radio_.end(); if (state_ != AppState::IDLE && state_ != AppState::MENU) radio_.end();
state_ = AppState::FINISHED; pendingReason_ = reason; state_ = AppState::FINISHED; pendingReason_ = reason;
setStandbyOpticalOutput();
setActivePerformance(false); setActivePerformance(false);
lastUserActivityMs_ = millis(); lastUserActivityMs_ = millis();
if (slaveLinkLost) { if (slaveLinkLost) {
char target[12], one[64]; char target[32], one[64];
Display::formatTestFrequency(actual_.actualHz ? actual_.actualHz : requestedHz_, target, sizeof(target)); formatTarget(requestedHz_, requestedPulseNs_, target, sizeof(target));
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target, snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target);
actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct); display_.show(one, uiFailName(reason), stageIndex_ + 1, stageCount_,
display_.show(one, uiFailName(reason), stageIndex_ + 1, stageCount_); roleCorner(Role::SLAVE));
armSlave(true);
return;
}
// The result has already been acknowledged before a normal measurement
// failure reaches here. Re-arm ESP-NOW immediately so a quick retry from
// Master is not hidden behind the former two-second delay; preserve the
// failure screen while listening.
if (static_cast<Role>(settings_.role) == Role::SLAVE) {
armSlave(true); armSlave(true);
return; return;
} }
if (static_cast<Role>(settings_.role) == Role::SLAVE) slaveRearmAtMs_ = millis() + 2000;
if (preserveDisplay) return; if (preserveDisplay) return;
char one[64]; char one[64];
if (pass) { if (pass) {
@@ -657,23 +1484,53 @@ void App::finish(bool pass, FailReason reason, bool preserveDisplay) {
display_.show(one, role == Role::SLAVE ? UiText::WAIT_MASTER : UiText::START_AGAIN); display_.show(one, role == Role::SLAVE ? UiText::WAIT_MASTER : UiText::START_AGAIN);
} }
else if (requestedHz_) { else if (requestedHz_) {
char frequency[12]; char target[32];
Display::formatTestFrequency(actual_.actualHz ? actual_.actualHz : requestedHz_, frequency, sizeof(frequency)); formatTarget(requestedHz_, requestedPulseNs_, target, sizeof(target));
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, frequency, snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target);
actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct); display_.show(one, uiFailName(reason), stageIndex_ + 1, stageCount_,
display_.show(one, uiFailName(reason), stageIndex_ + 1, stageCount_); roleCorner(static_cast<Role>(settings_.role)));
} else { } else {
display_.show(UiText::TEST_FAILED, uiFailName(reason)); display_.show(UiText::TEST_FAILED, uiFailName(reason), 0, 0,
roleCorner(static_cast<Role>(settings_.role)));
} }
} }
bool App::idlePowerSaveAllowed() const { bool App::idlePowerSaveAllowed() const {
return initialized_ && (state_ == AppState::IDLE || state_ == AppState::MENU || // Never enter blocking light sleep while the settings screen is open. A
// wake-up press is deliberately consumed by the button state machine, which
// is useful in IDLE but makes menu navigation appear frozen.
return !usbHostPresent() && initialized_ && (state_ == AppState::IDLE ||
state_ == AppState::FINISHED || state_ == AppState::SLAVE_READY); state_ == AppState::FINISHED || state_ == AppState::SLAVE_READY);
} }
bool App::usbHostPresent() const {
#if ARDUINO_USB_MODE && ARDUINO_USB_CDC_ON_BOOT && SOC_USB_SERIAL_JTAG_SUPPORTED
// This is driven by USB SOF packets, not by CDC traffic: an enumerated host
// keeps the board awake even if COM is closed and no bytes are exchanged.
// Retain the state across short SOF/driver glitches.
const uint32_t now = millis();
if (Serial.isPlugged()) {
lastUsbHostSeenMs_ = now ? now : 1U;
return true;
}
return lastUsbHostSeenMs_ &&
now - lastUsbHostSeenMs_ <= USB_HOST_DISCONNECT_GRACE_MS;
#else
return false;
#endif
}
void App::setStandbyOpticalOutput() {
// Calibration and tests may hold the transmitter active. Idle/result
// screens must always detach PWM and restore the physical light-OFF level.
pwm_.stop();
}
void App::setActivePerformance(bool active) { void App::setActivePerformance(bool active) {
const uint32_t targetMhz = active ? 160U : 80U; 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)) if (getCpuFrequencyMhz() != targetMhz && !setCpuFrequencyMhz(targetMhz))
Log::printf("POWER", "CPU frequency change to %luMHz FAILED", targetMhz); Log::printf("POWER", "CPU frequency change to %luMHz FAILED", targetMhz);
} }
@@ -685,6 +1542,15 @@ void App::leaveIdlePowerSave(bool wakeDisplay) {
} }
idlePowerSave_ = false; idlePowerSave_ = false;
lastUserActivityMs_ = millis(); lastUserActivityMs_ = millis();
setStandbyOpticalOutput();
if (idleSleepRadioStopped_) {
idleSleepRadioStopped_ = false;
if (radio_.begin()) {
radio_.setWindowedReceive(true);
radio_.flush();
Log::event("POWER", "Slave ESP-NOW restored after external wake");
} else Log::event("POWER", "Slave ESP-NOW restore FAILED after external wake");
}
if (wakeDisplay) display_.setPower(true); if (wakeDisplay) display_.setPower(true);
Log::event("POWER", "idle light sleep ended"); Log::event("POWER", "idle light sleep ended");
} }
@@ -702,46 +1568,107 @@ void App::serviceIdlePowerSave() {
return; return;
} }
idlePowerSave_ = true; idlePowerSave_ = true;
pwm_.stop();
if (state_ == AppState::SLAVE_READY) {
radio_.end();
idleSleepRadioStopped_ = true;
}
display_.setPower(false); display_.setPower(false);
Log::event("POWER", "idle timeout; OLED off and light sleep started"); Log::event("POWER", "idle timeout; preparing light sleep");
Serial.flush();
delay(2);
} }
gpio_wakeup_enable(static_cast<gpio_num_t>(GPIO_BUTTON_START), gpio_wakeup_enable(static_cast<gpio_num_t>(GPIO_BUTTON_START),
BUTTON_ACTIVE_LEVEL == LOW ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL); BUTTON_ACTIVE_LEVEL == LOW ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL);
gpio_wakeup_enable(static_cast<gpio_num_t>(GPIO_BUTTON_MODE), gpio_wakeup_enable(static_cast<gpio_num_t>(GPIO_BUTTON_MODE),
BUTTON_ACTIVE_LEVEL == LOW ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL); BUTTON_ACTIVE_LEVEL == LOW ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL);
if (static_cast<Role>(settings_.role) == Role::SLAVE) {
const bool currentRxHigh = gpio_get_level(static_cast<gpio_num_t>(GPIO_RX)) != 0;
gpio_wakeup_enable(static_cast<gpio_num_t>(GPIO_RX),
currentRxHigh ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL);
} else gpio_wakeup_disable(static_cast<gpio_num_t>(GPIO_RX));
esp_sleep_enable_gpio_wakeup(); esp_sleep_enable_gpio_wakeup();
esp_sleep_enable_timer_wakeup(IDLE_LIGHT_SLEEP_SLICE_US);
const esp_err_t result = esp_light_sleep_start(); const esp_err_t result = esp_light_sleep_start();
if (result != ESP_OK) { if (result != ESP_OK) {
leaveIdlePowerSave(true);
delay(1); delay(1);
return; return;
} }
if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_GPIO) { const esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
// The wake-up press is deliberately consumed. Holding or releasing it const bool buttonWake = digitalRead(GPIO_BUTTON_START) == BUTTON_ACTIVE_LEVEL ||
// must not later turn into a SHORT, LONG, or REPEAT event. digitalRead(GPIO_BUTTON_MODE) == BUTTON_ACTIVE_LEVEL;
if (buttonWake) {
startButton_.suppressUntilRelease(); startButton_.suppressUntilRelease();
modeButton_.suppressUntilRelease(); modeButton_.suppressUntilRelease();
leaveIdlePowerSave();
Log::event("POWER", "button wake consumed; next press will perform the action");
} }
// GPIO wake worked, so disarm all level sources before peripherals and the
// button state machines are brought back up.
gpio_wakeup_disable(static_cast<gpio_num_t>(GPIO_BUTTON_START));
gpio_wakeup_disable(static_cast<gpio_num_t>(GPIO_BUTTON_MODE));
gpio_wakeup_disable(static_cast<gpio_num_t>(GPIO_RX));
// Native USB and I2C can retain stale driver state across light sleep even
// though their clocks have stopped. A full end/begin cycle prevents the
// several-second button stalls and restores Serial output after wake.
setActivePerformance(false);
Serial.end();
delay(2);
Serial.begin(SERIAL_BAUD);
#if ARDUINO_USB_CDC_ON_BOOT
Serial.setTxTimeoutMs(SERIAL_TX_TIMEOUT_MS);
#endif
Wire.end();
Wire.begin(GPIO_SDA, GPIO_SCL);
Wire.setClock(400000);
Wire.setTimeOut(30);
// This also restores ESP-NOW when Slave stopped it before sleeping.
leaveIdlePowerSave(true);
Log::printf("POWER", "light sleep wake cause=%u button=%s; peripherals restored",
static_cast<unsigned>(cause), buttonWake ? "YES" : "NO");
if (buttonWake)
Log::event("POWER", "wake button consumed; next press will perform the action");
} }
void App::printConfiguration() { void App::printConfiguration() {
if (SERIAL_MINIMAL_LOG) return; if (SERIAL_MINIMAL_LOG) return;
const char *board = TARGET_IS_C3 ? "ESP32-C3" : "ESP32-S3"; const char *board = TARGET_IS_C3 ? "ESP32-C3" : "ESP32-S3";
uint8_t mac[6] = {}; esp_read_mac(mac, ESP_MAC_WIFI_STA); uint8_t mac[6] = {}; esp_read_mac(mac, ESP_MAC_WIFI_STA);
Serial.printf("\nOptical Channel Tester | %s | mode=%s\n", board, roleName(static_cast<Role>(settings_.role))); 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_));
Serial.printf("MAC=%02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); Serial.printf("MAC=%02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
Serial.printf("GPIO PWM=%u RX=%u START=%u MODE=%u SDA=%u SCL=%u\n", GPIO_PWM, GPIO_RX, Serial.printf("GPIO PWM=%u RX=%u START=%u MODE=%u SDA=%u SCL=%u\n", GPIO_PWM, GPIO_RX,
GPIO_BUTTON_START, GPIO_BUTTON_MODE, GPIO_SDA, GPIO_SCL); GPIO_BUTTON_START, GPIO_BUTTON_MODE, GPIO_SDA, GPIO_SCL);
Serial.printf("Test %lu..%lu Hz (adjacent exact frequencies), accuracy %.2f%%, %lums, duty %u%%\n", if (static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER) {
params_.startHz, params_.endHz, params_.accuracyPct, params_.testTimeMs, params_.dutyPct); const char *code = lightCodeName(static_cast<LightCode>(settings_.lightCode));
stageCount_ = frequencyPointCount(params_.startHz, params_.endHz); Serial.printf("Test %lu Hz, pulse %lu..%lu ns, accuracy %.2f%%, %lums, TX light=%c RX active light=%c\n",
Serial.printf("Frequencies (%lu): ", stageCount_); params_.frequencyHz, params_.maxPulseNs, params_.minPulseNs,
for (uint32_t i = 0; i < stageCount_; ++i) Serial.printf("%lu%s", frequencyAt(params_.startHz, params_.endHz, i), i + 1 == stageCount_ ? "\n" : ","); params_.accuracyPct, params_.testTimeMs, code[0], code[1]);
Serial.printf("ALL nominal: %llu us | RX=%s\n", actualNominalTotalUs(), receiver_.highRateBackend() ? "RMT DMA" : "RMT ping-pong"); } else {
Serial.printf("Test %lu Hz, pulse %lu..%lu ns, accuracy %.2f%%, %lums, optical polarity=AUTO\n",
params_.frequencyHz, params_.maxPulseNs, params_.minPulseNs,
params_.accuracyPct, params_.testTimeMs);
}
stageCount_ = pulseWidthPointCount(params_.maxPulseNs, params_.minPulseNs);
Serial.printf("Pulse widths descending (%lu): ", stageCount_);
for (uint32_t i = 0; i < stageCount_; ++i)
Serial.printf("%lu%s", pulseWidthAt(params_.maxPulseNs, params_.minPulseNs, i),
i + 1 == stageCount_ ? " ns\n" : ",");
Serial.printf("ALL nominal: %llu us | RX=%s\n", actualNominalTotalUs(),
receiver_.highRateBackend() ? "MCPWM 80MHz" : "GPIO cycle counter");
} }
uint64_t App::actualNominalTotalUs() { uint64_t App::actualNominalTotalUs() {
@@ -754,80 +1681,137 @@ uint64_t App::actualNominalTotalUs() {
void App::printStageStats(const StageStats &s, uint32_t hz) { void App::printStageStats(const StageStats &s, uint32_t hz) {
if (!s.periods) return; if (!s.periods) return;
const float measuredHz = static_cast<float>(receiver_.tickHz()) * s.periods / s.periodSum; const float measuredHz = static_cast<float>(receiver_.tickHz()) * s.periods / s.periodSum;
const float measuredDuty = 100.0f * s.activeSum / s.periodSum; const uint32_t measuredPulseNs = static_cast<uint32_t>(lround(
static_cast<double>(s.activeSum) * 1000000000.0 /
(static_cast<uint64_t>(receiver_.pulseTickHz()) * s.periods)));
char requestedText[12], measuredText[12]; char requestedText[12], measuredText[12];
Display::formatFrequency(hz, requestedText, sizeof(requestedText)); Display::formatFrequency(hz, requestedText, sizeof(requestedText));
Display::formatFrequency(measuredHz, measuredText, sizeof(measuredText)); Display::formatFrequency(measuredHz, measuredText, sizeof(measuredText));
const char *status = s.reason == FailReason::NONE ? "PASS" : "FAIL"; const char *status = s.reason == FailReason::NONE ? "PASS" : "FAIL";
Log::printf("RESULT", "%s %s periods=%lu measured=%s duty=%.2f%% skipped=%lu%s%s", Log::printf("RESULT", "%s/%luns %s periods=%lu measured=%s/%luns skipped=%lu%s%s",
requestedText, status, s.periods, measuredText, measuredDuty, s.droppedItems, requestedText, requestedPulseNs_, status, s.periods, measuredText, measuredPulseNs, s.droppedItems,
s.reason == FailReason::NONE ? "" : " reason=", s.reason == FailReason::NONE ? "" : failName(s.reason)); s.reason == FailReason::NONE ? "" : " reason=", s.reason == FailReason::NONE ? "" : failName(s.reason));
} }
void App::showStageResult(const StageStats &s) { void App::showStageResult(const StageStats &s) {
char one[64], two[64]; char one[64], two[64];
char target[12]; Display::formatTestFrequency(actual_.actualHz, target, sizeof(target)); formatTestTarget(requestedHz_, requestedPulseNs_, one, sizeof(one));
if (s.reason != FailReason::NONE) { if (s.reason != FailReason::NONE) {
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target, actual_.actualDutyPct); formatFailure(s.reason, requestedHz_, requestedPulseNs_, one, sizeof(one));
if (s.reason == FailReason::PERIOD_OUT && s.badFrequency > 0.0f) { if (s.reason == FailReason::PERIOD_OUT && s.badFrequency > 0.0f) {
char frequency[12]; char frequency[12];
Display::formatTestFrequency(static_cast<uint32_t>(lroundf(s.badFrequency)), frequency, sizeof(frequency)); Display::formatFrequency(s.badFrequency, frequency, sizeof(frequency));
snprintf(two, sizeof(two), UiText::PERIOD_OUT_FORMAT, frequency); snprintf(two, sizeof(two), UiText::PERIOD_OUT_FORMAT, frequency);
} else if (s.reason == FailReason::DUTY_OUT && s.badFrequency > 0.0f) { } else if (s.reason == FailReason::DUTY_OUT && s.badFrequency > 0.0f) {
char duty[10]; formatErrorDuty(s.badDuty, duty, sizeof(duty)); char pulse[12];
snprintf(two, sizeof(two), UiText::DUTY_OUT_FORMAT, duty); Display::formatPulse(pulseFromDuty(s.badFrequency, s.badDuty), pulse,
sizeof(pulse), true);
snprintf(two, sizeof(two), UiText::DUTY_OUT_FORMAT, pulse);
} else { } else {
snprintf(two, sizeof(two), "%s", uiFailName(s.reason)); snprintf(two, sizeof(two), "%s", uiFailName(s.reason));
} }
display_.show(one, two, overallProgress(stageIndex_, measurement_.progressStep()), display_.show(one, two, overallProgress(stageIndex_, measurement_.progressStep()),
overallProgressTotal(stageCount_)); overallProgressTotal(stageCount_), roleCorner(static_cast<Role>(settings_.role)));
return; return;
} }
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) { if (!s.periods || !s.periodSum) {
display_.show(one, UiText::NO_MEASUREMENT, overallProgress(stageIndex_, measurement_.progressStep()), display_.show(one, UiText::NO_MEASUREMENT, overallProgress(stageIndex_, measurement_.progressStep()),
overallProgressTotal(stageCount_)); overallProgressTotal(stageCount_));
return; return;
} }
const float measuredHz = static_cast<float>(receiver_.tickHz()) * s.periods / s.periodSum; const float measuredHz = static_cast<float>(receiver_.tickHz()) * s.periods / s.periodSum;
const float measuredDuty = 100.0f * s.activeSum / s.periodSum; const uint32_t measuredPulseNs = static_cast<uint32_t>(lround(
char frequency[12]; Display::formatFrequency(measuredHz, frequency, sizeof(frequency)); static_cast<double>(s.activeSum) * 1000000000.0 /
snprintf(two, sizeof(two), "F:%-8s D:%4.1f%%", frequency, measuredDuty); (static_cast<uint64_t>(receiver_.pulseTickHz()) * s.periods)));
formatMeasured(measuredHz, measuredPulseNs, two, sizeof(two));
display_.show(one, two, overallProgress(stageIndex_, measurement_.progressStep()), display_.show(one, two, overallProgress(stageIndex_, measurement_.progressStep()),
overallProgressTotal(stageCount_)); overallProgressTotal(stageCount_));
} }
void App::showDriverResult(const DriverStats &s, bool testPassed) {
char one[64], two[64];
const bool haveResponse = s.responses || s.lastResponseTicks;
const uint64_t delaySumTicks =
s.turnOn.delaySumTicks + s.turnOff.delaySumTicks;
const uint64_t responseSumTicks =
s.turnOn.responseSumTicks + s.turnOff.responseSumTicks;
const uint64_t displayedDelayTicks = s.responses ?
delaySumTicks / s.responses : s.lastDelayTicks;
const uint64_t displayedResponseTicks = s.responses ?
responseSumTicks / s.responses : s.lastResponseTicks;
const uint32_t delayNs = static_cast<uint32_t>(
(displayedDelayTicks * 1000000000ULL +
driverTest_.tickHz() / 2U) / driverTest_.tickHz());
const uint32_t responseNs = static_cast<uint32_t>(
(displayedResponseTicks * 1000000000ULL +
driverTest_.tickHz() / 2U) / driverTest_.tickHz());
char delay[12] = "---", response[12] = "---";
if (haveResponse) {
Display::formatPulse(delayNs, delay, sizeof(delay));
Display::formatPulse(responseNs, response, sizeof(response));
}
if (testPassed) {
snprintf(one, sizeof(one), "%s", UiText::PASS_WORD);
snprintf(two, sizeof(two), UiText::DRIVER_MEASUREMENT_FORMAT,
delay, response);
} else if (s.reason != FailReason::NONE) {
snprintf(one, sizeof(one), "%s", uiFailName(s.reason));
char elapsed[12] = "---", errorPulse[12] = "---";
if (s.errorTriggerValid) {
const uint64_t elapsedNs =
(static_cast<uint64_t>(s.errorTriggerTicks) * 1000000000ULL +
driverTest_.tickHz() / 2U) /
driverTest_.tickHz();
formatElapsedNs(elapsedNs, elapsed, sizeof(elapsed));
}
if (s.errorPulseValid) {
const uint64_t errorPulseNs =
(static_cast<uint64_t>(s.errorPulseTicks) * 1000000000ULL +
driverTest_.tickHz() / 2U) / driverTest_.tickHz();
formatElapsedNs(errorPulseNs, errorPulse, sizeof(errorPulse));
}
if (s.errorPulseValid)
snprintf(two, sizeof(two), "T:%s P:%s", elapsed, errorPulse);
else snprintf(two, sizeof(two), "T:%s", elapsed);
} else {
formatTestTarget(requestedHz_, requestedPulseNs_, one, sizeof(one));
snprintf(two, sizeof(two), UiText::DRIVER_MEASUREMENT_FORMAT,
delay, response);
}
const bool finished = testPassed || s.reason != FailReason::NONE;
const uint8_t progressSteps = driverTest_.progressSteps();
display_.show(one, two,
finished ? 0U : overallProgress(
stageIndex_, driverTest_.progressStep(), progressSteps),
finished ? 0U : overallProgressTotal(stageCount_, progressSteps),
s.reason == FailReason::NONE ? nullptr : roleCorner(Role::SOLO));
}
void App::showRemoteResult(const ProtocolPacket &packet) { void App::showRemoteResult(const ProtocolPacket &packet) {
const FailReason reason = packet.reason <= static_cast<uint8_t>(FailReason::ABORTED) const FailReason reason = packet.reason <= static_cast<uint8_t>(FailReason::ABORTED)
? static_cast<FailReason>(packet.reason) : FailReason::UNSUPPORTED; ? static_cast<FailReason>(packet.reason) : FailReason::UNSUPPORTED;
char target[12], one[64], two[64]; char one[64], two[64];
Display::formatTestFrequency(packet.actualHz ? packet.actualHz : packet.requestedHz, formatTestTarget(packet.requestedHz, packet.requestedPulseNs, one, sizeof(one));
target, sizeof(target));
if (reason == FailReason::NONE) { if (reason == FailReason::NONE) {
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) { if (packet.measuredHzX10) {
char measured[12]; formatMeasured(packet.measuredHzX10 / 10.0f, packet.measuredPulseNs, two, sizeof(two));
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), "%s", UiText::NO_MEASUREMENT); } else snprintf(two, sizeof(two), "%s", UiText::NO_MEASUREMENT);
} else if (reason == FailReason::PERIOD_OUT && packet.measuredHzX10) { } else if (reason == FailReason::PERIOD_OUT && packet.measuredHzX10) {
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target, packet.actualDutyX100 / 100.0f); char frequency[12];
char measured[12]; Display::formatFrequency(packet.measuredHzX10 / 10.0f, frequency, sizeof(frequency));
Display::formatTestFrequency((packet.measuredHzX10 + 5U) / 10U, measured, sizeof(measured)); snprintf(two, sizeof(two), UiText::PERIOD_OUT_FORMAT, frequency);
snprintf(two, sizeof(two), UiText::PERIOD_OUT_FORMAT, measured); } else if (reason == FailReason::DUTY_OUT && packet.measuredPulseNs) {
} else if (reason == FailReason::DUTY_OUT && packet.measuredDutyX10) { char pulse[12];
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target, packet.actualDutyX100 / 100.0f); Display::formatPulse(packet.measuredPulseNs, pulse, sizeof(pulse), true);
char duty[10]; formatErrorDuty(packet.measuredDutyX10 / 10.0f, duty, sizeof(duty)); snprintf(two, sizeof(two), UiText::DUTY_OUT_FORMAT, pulse);
snprintf(two, sizeof(two), UiText::DUTY_OUT_FORMAT, duty);
} else { } else {
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target, packet.actualDutyX100 / 100.0f);
snprintf(two, sizeof(two), "%s", uiFailName(reason)); snprintf(two, sizeof(two), "%s", uiFailName(reason));
} }
if (reason != FailReason::NONE)
formatFailure(reason, packet.requestedHz, packet.requestedPulseNs, one, sizeof(one));
display_.show(one, two, overallProgress(stageIndex_, packet.progressStep), display_.show(one, two, overallProgress(stageIndex_, packet.progressStep),
overallProgressTotal(stageCount_)); overallProgressTotal(stageCount_), reason == FailReason::NONE ? nullptr :
roleCorner(static_cast<Role>(settings_.role)));
} }
void App::fillMeasuredResult(ProtocolPacket &packet, const StageStats &stats) const { void App::fillMeasuredResult(ProtocolPacket &packet, const StageStats &stats) const {
@@ -838,16 +1822,24 @@ void App::fillMeasuredResult(ProtocolPacket &packet, const StageStats &stats) co
stats.badFrequency > 0.0f; stats.badFrequency > 0.0f;
const float measuredHz = badPeriod ? stats.badFrequency : const float measuredHz = badPeriod ? stats.badFrequency :
static_cast<float>(receiver_.tickHz()) * stats.periods / stats.periodSum; static_cast<float>(receiver_.tickHz()) * stats.periods / stats.periodSum;
const float measuredDuty = badPeriod ? stats.badDuty : 100.0f * stats.activeSum / stats.periodSum;
packet.measuredHzX10 = static_cast<uint32_t>(lroundf(measuredHz * 10.0f)); packet.measuredHzX10 = static_cast<uint32_t>(lroundf(measuredHz * 10.0f));
packet.measuredDutyX10 = static_cast<uint16_t>(lroundf(measuredDuty * 10.0f)); packet.measuredPulseNs = badPeriod ? pulseFromDuty(measuredHz, stats.badDuty) :
static_cast<uint32_t>(lround(static_cast<double>(stats.activeSum) * 1000000000.0 /
(static_cast<uint64_t>(receiver_.pulseTickHz()) * stats.periods)));
} }
void App::showStageProgress() { void App::showStageProgress() {
char target[12], one[64], stage[12]; if (static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER) {
Display::formatTestFrequency(actual_.actualHz, target, sizeof(target)); char one[64], two[64];
snprintf(stage, sizeof(stage), "%lu/%lu", stageIndex_ + 1, stageCount_); formatTestTarget(requestedHz_, requestedPulseNs_, one, sizeof(one));
snprintf(one, sizeof(one), UiText::TEST_FORMAT, target, actual_.actualDutyPct, stage); snprintf(two, sizeof(two), UiText::DRIVER_MEASUREMENT_FORMAT,
"---", "---");
display_.show(one, two, overallProgress(stageIndex_, 0),
overallProgressTotal(stageCount_));
return;
}
char one[64];
formatTestTarget(requestedHz_, requestedPulseNs_, one, sizeof(one));
display_.show(one, UiText::NO_MEASUREMENT, overallProgress(stageIndex_, 0), display_.show(one, UiText::NO_MEASUREMENT, overallProgress(stageIndex_, 0),
overallProgressTotal(stageCount_)); overallProgressTotal(stageCount_));
} }

View File

@@ -1,13 +1,15 @@
#pragma once #pragma once
#include "Buttons.h" #include "Buttons.h"
#include "Display.h" #include "Display.h"
#include "DriverTest.h"
#include "Measurement.h" #include "Measurement.h"
#include "OpticalCurrent.h"
#include "Pwm.h" #include "Pwm.h"
#include "Radio.h" #include "Radio.h"
#include "SettingsStore.h" #include "SettingsStore.h"
enum class AppState : uint8_t { enum class AppState : uint8_t {
IDLE, MENU, SOLO_MEASURE, 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, MASTER_WAIT_RESULT, MASTER_FINALIZE, SLAVE_READY, SLAVE_WAIT_START, SLAVE_MEASURE,
SLAVE_WAIT_ACK, FINISHED SLAVE_WAIT_ACK, FINISHED
}; };
@@ -22,11 +24,19 @@ class App {
void finishInitialization(bool factoryReset); void finishInitialization(bool factoryReset);
void showMenu(); void showMenu();
void changeMenu(int direction); void changeMenu(int direction);
void enterOpticalCalibration(uint32_t now);
void updateOpticalCalibration(uint32_t now, bool force = false);
void leaveOpticalCalibration();
void cycleRunMode();
void sanitizeRange(); void sanitizeRange();
void startTest(); void startTest();
void startBoardTest();
void updateBoardTest(uint32_t now);
void stopBoardTest();
bool armSlave(bool preserveDisplay = false); bool armSlave(bool preserveDisplay = false);
bool prepareStage(bool showProgress = true); bool prepareStage(bool showProgress = true);
bool startLocalMeasurement(float hz, float duty); bool startLocalMeasurement(float hz, float duty);
bool startDriverMeasurement();
void startMasterDiscovery(); void startMasterDiscovery();
void handleRadio(); void handleRadio();
void updateMaster(); void updateMaster();
@@ -38,6 +48,7 @@ class App {
void printConfiguration(); void printConfiguration();
void printStageStats(const StageStats &s, uint32_t hz); void printStageStats(const StageStats &s, uint32_t hz);
void showStageResult(const StageStats &s); void showStageResult(const StageStats &s);
void showDriverResult(const DriverStats &s, bool testPassed = false);
void showRemoteResult(const ProtocolPacket &packet); void showRemoteResult(const ProtocolPacket &packet);
void fillMeasuredResult(ProtocolPacket &packet, const StageStats &stats) const; void fillMeasuredResult(ProtocolPacket &packet, const StageStats &stats) const;
void showStageProgress(); void showStageProgress();
@@ -48,8 +59,16 @@ class App {
void updateHeartbeat(); void updateHeartbeat();
bool packetForCurrent(const ProtocolPacket &p) const; bool packetForCurrent(const ProtocolPacket &p) const;
void serviceIdlePowerSave(); void serviceIdlePowerSave();
void serviceSerialConsole();
void handleSerialCommand(char *line);
void printSerialHelp();
void printSerialStatus();
bool serialSettingsMutable() const;
void finishSerialSettingsChange();
void leaveIdlePowerSave(bool wakeDisplay = true); void leaveIdlePowerSave(bool wakeDisplay = true);
bool idlePowerSaveAllowed() const; bool idlePowerSaveAllowed() const;
bool usbHostPresent() const;
void setStandbyOpticalOutput();
void setActivePerformance(bool active); void setActivePerformance(bool active);
Button startButton_, modeButton_; Button startButton_, modeButton_;
@@ -60,11 +79,12 @@ class App {
PwmGenerator pwm_; PwmGenerator pwm_;
PulseReceiver receiver_; PulseReceiver receiver_;
Measurement measurement_; Measurement measurement_;
DriverTest driverTest_;
Radio radio_; Radio radio_;
AppState state_ = AppState::IDLE; AppState state_ = AppState::IDLE;
uint8_t menuItem_ = 0; uint8_t menuItem_ = 0;
uint32_t stageIndex_ = 0, stageCount_ = 0; uint32_t stageIndex_ = 0, stageCount_ = 0;
uint32_t requestedHz_ = 0; uint32_t requestedHz_ = 0, requestedPulseNs_ = 0;
ActualPwm actual_ = {}; ActualPwm actual_ = {};
FailReason pendingReason_ = FailReason::NONE; FailReason pendingReason_ = FailReason::NONE;
uint32_t session_ = 0; uint32_t session_ = 0;
@@ -72,6 +92,7 @@ class App {
uint8_t peer_[6] = {}; uint8_t peer_[6] = {};
bool havePeer_ = false; bool havePeer_ = false;
uint32_t deadlineMs_ = 0, lastSendMs_ = 0; uint32_t deadlineMs_ = 0, lastSendMs_ = 0;
uint32_t localMeasurementDeadlineMs_ = 0;
uint32_t lastHeartbeatMs_ = 0, lastPeerSeenMs_ = 0; uint32_t lastHeartbeatMs_ = 0, lastPeerSeenMs_ = 0;
uint8_t retries_ = 0; uint8_t retries_ = 0;
ProtocolPacket pendingPacket_ = {}; ProtocolPacket pendingPacket_ = {};
@@ -81,4 +102,18 @@ class App {
bool stageStartConfirmed_ = false; bool stageStartConfirmed_ = false;
uint32_t lastUserActivityMs_ = 0; uint32_t lastUserActivityMs_ = 0;
bool idlePowerSave_ = false; bool idlePowerSave_ = false;
bool idleSleepRadioStopped_ = false;
uint32_t lastOpticalWakeToggleMs_ = 0;
bool opticalWakeActive_ = false;
bool rxPinStateKnown_ = false, rxPinState_ = false;
uint32_t boardTestUpdatedMs_ = 0;
uint32_t boardDisplayUpdatedMs_ = 0;
bool boardPwmLightOn_ = false;
bool opticalCalibrationActive_ = false;
uint32_t opticalCalibrationStartedMs_ = 0;
uint32_t opticalCalibrationUpdatedMs_ = 0;
mutable uint32_t lastUsbHostSeenMs_ = 0;
char serialLine_[96] = {};
uint8_t serialLineLength_ = 0;
bool serialLineOverflow_ = false;
}; };

View File

@@ -3,20 +3,53 @@
#include <Arduino.h> #include <Arduino.h>
// ------------------------- Hardware configuration ------------------------- // ------------------------- Hardware configuration -------------------------
// Uncomment for the hand-wired prototype. The production PCB assignments // Enabled for the hand-wired prototype. Comment out for the production PCB.
// below follow the physical header positions shown in the schematic. // Both profiles map S3 signals by physical header position with 5V/GND aligned.
// #define MAKETKA #define MAKETKA
// 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
#ifndef BOARD_RX_INTERFACE
#define BOARD_RX_INTERFACE BOARD_RX_INTERFACE_DIGITAL
#endif
#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 #if CONFIG_IDF_TARGET_ESP32C3
constexpr bool TARGET_IS_C3 = true; constexpr bool TARGET_IS_C3 = true;
constexpr uint8_t GPIO_PWM = 3; constexpr uint8_t GPIO_PWM = 3;
constexpr uint8_t GPIO_RX = 4; constexpr uint8_t GPIO_RX = 4;
#ifdef MAKETKA #ifdef MAKETKA
constexpr bool BOARD_ADC_AVAILABLE = false;
constexpr uint8_t GPIO_BUTTON_MODE = 0; constexpr uint8_t GPIO_BUTTON_MODE = 0;
constexpr uint8_t GPIO_BUTTON_START = 1; constexpr uint8_t GPIO_BUTTON_START = 1;
constexpr uint8_t GPIO_OPTICAL_CURRENT = 2;
constexpr uint8_t GPIO_OPTICAL_VCC = 5;
constexpr uint8_t GPIO_VBAT = UINT8_MAX;
constexpr uint8_t GPIO_ANALOG_RX = UINT8_MAX;
#else #else
constexpr bool BOARD_ADC_AVAILABLE = true;
constexpr uint8_t GPIO_BUTTON_MODE = 20; constexpr uint8_t GPIO_BUTTON_MODE = 20;
constexpr uint8_t GPIO_BUTTON_START = 10; constexpr uint8_t GPIO_BUTTON_START = 10;
constexpr uint8_t GPIO_OPTICAL_CURRENT = 1;
constexpr uint8_t GPIO_OPTICAL_VCC = 5;
constexpr uint8_t GPIO_VBAT = 2; constexpr uint8_t GPIO_VBAT = 2;
constexpr uint8_t GPIO_ANALOG_RX = 0; constexpr uint8_t GPIO_ANALOG_RX = 0;
#endif #endif
@@ -25,19 +58,28 @@ constexpr uint8_t GPIO_SCL = 7;
#elif CONFIG_IDF_TARGET_ESP32S3 #elif CONFIG_IDF_TARGET_ESP32S3
constexpr bool TARGET_IS_C3 = false; constexpr bool TARGET_IS_C3 = false;
#ifdef MAKETKA #ifdef MAKETKA
constexpr uint8_t GPIO_PWM = 4; constexpr bool BOARD_ADC_AVAILABLE = false;
constexpr uint8_t GPIO_RX = 5; // Same physical header contacts as the C3 MAKETKA profile when 5V/GND align.
constexpr uint8_t GPIO_BUTTON_MODE = 0; constexpr uint8_t GPIO_PWM = 12;
constexpr uint8_t GPIO_BUTTON_START = 1; constexpr uint8_t GPIO_RX = 13;
constexpr uint8_t GPIO_SDA = 8; constexpr uint8_t GPIO_BUTTON_MODE = 9;
constexpr uint8_t GPIO_SCL = 9; constexpr uint8_t GPIO_BUTTON_START = 10;
constexpr uint8_t GPIO_OPTICAL_CURRENT = 2;
constexpr uint8_t GPIO_OPTICAL_VCC = 6;
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 #else
constexpr bool BOARD_ADC_AVAILABLE = true;
// The S3 SuperMini is fitted so its 5V and GND pins occupy the same PCB // 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. // contacts as on the C3 SuperMini. Signals therefore follow header position.
constexpr uint8_t GPIO_PWM = 12; constexpr uint8_t GPIO_PWM = 12;
constexpr uint8_t GPIO_RX = 13; constexpr uint8_t GPIO_RX = 13;
constexpr uint8_t GPIO_BUTTON_MODE = 5; constexpr uint8_t GPIO_BUTTON_MODE = 5;
constexpr uint8_t GPIO_BUTTON_START = 4; constexpr uint8_t GPIO_BUTTON_START = 4;
constexpr uint8_t GPIO_OPTICAL_CURRENT = 10;
constexpr uint8_t GPIO_OPTICAL_VCC = 6;
constexpr uint8_t GPIO_SDA = 44; constexpr uint8_t GPIO_SDA = 44;
constexpr uint8_t GPIO_SCL = 1; constexpr uint8_t GPIO_SCL = 1;
constexpr uint8_t GPIO_VBAT = 11; constexpr uint8_t GPIO_VBAT = 11;
@@ -47,18 +89,48 @@ constexpr uint8_t GPIO_ANALOG_RX = 9;
#error "Only ESP32-C3 and ESP32-S3 are supported" #error "Only ESP32-C3 and ESP32-S3 are supported"
#endif #endif
static_assert(GPIO_OPTICAL_CURRENT != GPIO_PWM &&
GPIO_OPTICAL_CURRENT != GPIO_RX &&
GPIO_OPTICAL_CURRENT != GPIO_BUTTON_MODE &&
GPIO_OPTICAL_CURRENT != GPIO_BUTTON_START &&
GPIO_OPTICAL_CURRENT != GPIO_SDA &&
GPIO_OPTICAL_CURRENT != GPIO_SCL,
"Optical-current ADC GPIO conflicts with another board signal");
static_assert(GPIO_OPTICAL_VCC != GPIO_OPTICAL_CURRENT &&
GPIO_OPTICAL_VCC != GPIO_PWM &&
GPIO_OPTICAL_VCC != GPIO_RX &&
GPIO_OPTICAL_VCC != GPIO_BUTTON_MODE &&
GPIO_OPTICAL_VCC != GPIO_BUTTON_START &&
GPIO_OPTICAL_VCC != GPIO_SDA &&
GPIO_OPTICAL_VCC != GPIO_SCL,
"Optical-VCC ADC GPIO conflicts with another board signal");
constexpr uint8_t OLED_ROTATION = 0; constexpr uint8_t OLED_ROTATION = 0;
constexpr uint8_t OLED_ADDRESS = 0x3C; constexpr uint8_t OLED_ADDRESS = 0x3C;
constexpr uint8_t ESPNOW_WIFI_CHANNEL = 6; constexpr uint8_t ESPNOW_WIFI_CHANNEL = 6;
constexpr uint32_t SERIAL_BAUD = 115200; 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 float OPTICAL_VCC = 5.1f;
constexpr float OPTICAL_SENSE_R = 47.0f;
constexpr float OPTICAL_DIVIDER_RATIO = 2.0f;
constexpr uint32_t OPTICAL_CURRENT_SETTLE_MS = 20;
constexpr uint32_t OPTICAL_CURRENT_AVERAGING_MS = 200;
constexpr bool SERIAL_ACTION_LOG = true; constexpr bool SERIAL_ACTION_LOG = true;
constexpr bool SERIAL_LOG_TIMESTAMPS = true; constexpr bool SERIAL_LOG_TIMESTAMPS = true;
constexpr bool SERIAL_MINIMAL_LOG = true; constexpr bool SERIAL_MINIMAL_LOG = true;
#define BUTTON_ACTIVE_LEVEL LOW #define BUTTON_ACTIVE_LEVEL LOW
#define RX_SIGNAL_INVERTED false // Fixed PCB conversion between electrical GPIO levels and actual optical
#define PWM_SAFE_LEVEL LOW // light. User settings HH/HL/LH/LL operate only in the optical domain and
// never change these hardware facts.
#define TX_LIGHT_ON_GPIO_LEVEL LOW
#define RX_LIGHT_ON_GPIO_LEVEL LOW
#define TX_LIGHT_OFF_GPIO_LEVEL (TX_LIGHT_ON_GPIO_LEVEL == HIGH ? LOW : HIGH)
#define PWM_SETTLE_CYCLES 5U #define PWM_SETTLE_CYCLES 5U
constexpr uint32_t BUTTON_DEBOUNCE_MS = 30; constexpr uint32_t BUTTON_DEBOUNCE_MS = 30;
@@ -71,21 +143,30 @@ constexpr uint32_t LINK_REPLY_TIMEOUT_MS = 1500;
constexpr uint8_t LINK_PACKET_RETRIES = 10; constexpr uint8_t LINK_PACKET_RETRIES = 10;
constexpr uint32_t LINK_RETRY_INTERVAL_MS = 1000; constexpr uint32_t LINK_RETRY_INTERVAL_MS = 1000;
constexpr uint32_t DISCOVERY_RETRY_INTERVAL_MS = 20; constexpr uint32_t DISCOVERY_RETRY_INTERVAL_MS = 20;
// During discovery Master alternates actual optical light ON and OFF to wake
// a sleeping Slave through the optical channel.
constexpr uint32_t OPTICAL_WAKE_HALF_PERIOD_MS = 50;
constexpr uint32_t LINK_HEARTBEAT_INTERVAL_MS = 500; constexpr uint32_t LINK_HEARTBEAT_INTERVAL_MS = 500;
constexpr uint32_t LINK_HEARTBEAT_TIMEOUT_MS = 2500; constexpr uint32_t LINK_HEARTBEAT_TIMEOUT_MS = 2500;
constexpr uint32_t FINAL_ACK_RETRY_INTERVAL_MS = 50; constexpr uint32_t FINAL_ACK_RETRY_INTERVAL_MS = 50;
constexpr uint8_t FINAL_ACK_RETRIES = 2; constexpr uint8_t FINAL_ACK_RETRIES = 2;
constexpr uint8_t NO_SIGNAL_TIMEOUT_PERIODS = 8; constexpr uint8_t NO_SIGNAL_TIMEOUT_PERIODS = 8;
constexpr uint16_t RMT_MIN_RECEIVE_SYMBOLS = 48;
constexpr uint16_t RMT_MAX_RECEIVE_SYMBOLS = 512;
constexpr uint32_t RMT_TARGET_CHUNK_US = 5000;
constexpr uint8_t RMT_QUEUE_BLOCKS = 8;
constexpr uint16_t PERIOD_BATCH_SIZE = 128; constexpr uint16_t PERIOD_BATCH_SIZE = 128;
// Retained as the minimum statistical depth used by the hardware-resolution
// calculation and diagnostics. PASS/FAIL is evaluated for every complete
// pulse independently; accumulated values are used only for display.
constexpr uint16_t MEASUREMENT_AVERAGING_PERIODS = 100;
static_assert(MEASUREMENT_AVERAGING_PERIODS > 0,
"Averaging window must contain at least one period");
constexpr uint8_t MEASUREMENT_PROGRESS_STEPS = 10; constexpr uint8_t MEASUREMENT_PROGRESS_STEPS = 10;
constexpr uint32_t OLED_PROGRESS_UPDATE_MS = 15; constexpr uint32_t OLED_PROGRESS_UPDATE_MS = 15;
constexpr uint8_t DRIVER_SHORT_SAMPLE_PROGRESS_STEPS = 10;
constexpr uint32_t DRIVER_PROGRESS_INTERVAL_MS = 100;
constexpr uint32_t IDLE_POWER_SAVE_TIMEOUT_MS = 60000; constexpr uint32_t IDLE_POWER_SAVE_TIMEOUT_MS = 60000;
constexpr uint32_t IDLE_LIGHT_SLEEP_SLICE_US = 10000; // usb_serial_jtag_is_connected() needs no open COM port or CDC traffic, but a
// short SOF detection gap must not send the board to sleep.
constexpr uint32_t USB_HOST_DISCONNECT_GRACE_MS = 5000;
constexpr uint16_t SLAVE_LISTEN_INTERVAL_MS = 100; constexpr uint16_t SLAVE_LISTEN_INTERVAL_MS = 100;
constexpr uint16_t SLAVE_LISTEN_WINDOW_MS = 20; constexpr uint16_t SLAVE_LISTEN_WINDOW_MS = 20;
static_assert(SLAVE_LISTEN_WINDOW_MS < SLAVE_LISTEN_INTERVAL_MS, static_assert(SLAVE_LISTEN_WINDOW_MS < SLAVE_LISTEN_INTERVAL_MS,
@@ -95,38 +176,58 @@ constexpr uint32_t RX_PROCESSING_PERIODS_PER_SECOND = 300000;
constexpr uint32_t C3_STRICT_MAX_HZ = 1000000; constexpr uint32_t C3_STRICT_MAX_HZ = 1000000;
constexpr uint32_t S3_STRICT_MAX_HZ = 1000000; constexpr uint32_t S3_STRICT_MAX_HZ = 1000000;
// RMT stores each HIGH/LOW duration in 15 bits. Select the fastest clock that // S3 MCPWM Capture uses one 32-bit 80 MHz timer for both edges. Unlike RMT,
// still fits both levels of the current PWM signal: 20, 40 or 80 MHz. // its width does not constrain long LOW/HIGH intervals, so capture precision
constexpr uint32_t CAPTURE_RESOLUTION_OPTIONS_HZ[] = {20000000, 40000000, 80000000}; // stays at 12.5 ns for every selectable PWM frequency and pulse length.
constexpr uint32_t RMT_MAX_LEVEL_TICKS = 32766; constexpr uint32_t MCPWM_CAPTURE_RESOLUTION_HZ = 80000000;
// C3 uses the 40 MHz crystal as the LEDC clock. // C3 uses the 40 MHz crystal as the LEDC clock.
// Keep this explicit so the resolution calculation never asks LEDC for an // Keep this explicit so the resolution calculation never asks LEDC for an
// impossible frequency/resolution combination. // impossible frequency/resolution combination.
constexpr uint32_t LEDC_SOURCE_CLOCK_HZ = 40000000; constexpr uint32_t LEDC_SOURCE_CLOCK_HZ = 40000000;
constexpr uint8_t LEDC_CHANNEL = 0; constexpr uint8_t LEDC_CHANNEL = 0;
constexpr uint8_t LEDC_MAX_BITS = 14; constexpr uint8_t LEDC_MAX_BITS = 14;
// S3 uses the dedicated MCPWM peripheral. A 40 MHz timer clock keeps the // S3 uses the dedicated MCPWM peripheral. A 20 MHz timer clock keeps the
// longest 1 kHz period within the S3's 16-bit MCPWM counter and makes every // selectable 500 Hz period within the S3's 16-bit counter while retaining
// frequency in TEST_FREQUENCIES_HZ exact. // 50 ns pulse resolution and exact periods for every menu frequency.
constexpr uint32_t MCPWM_RESOLUTION_HZ = 40000000; constexpr uint32_t MCPWM_RESOLUTION_HZ = 20000000;
constexpr uint32_t MCPWM_MAX_PERIOD_TICKS = 65535; constexpr uint32_t MCPWM_MAX_PERIOD_TICKS = 65535;
// -------------------------- Menu value arrays ----------------------------- // Concept 1SP0635 status acknowledgement, expressed in the optical domain.
// START and END deliberately have separate, independently cycling menu lists. constexpr uint32_t DRIVER_MIN_INPUT_PULSE_NS = 2000;
// Every value is exactly achievable from a 40 MHz timer clock. The test walks constexpr uint32_t DRIVER_ACK_DELAY_NS = 250;
// TEST_FREQUENCIES_HZ between the selected endpoints, so there is no constexpr uint32_t DRIVER_ACK_WIDTH_NS = 700;
// separately configurable step. constexpr uint32_t DRIVER_ACK_START_MAX_NS = 2000;
constexpr uint32_t START_FREQ_OPTIONS_HZ[] = {1000, 10000, 100000}; constexpr uint32_t DRIVER_ACK_MERGE_MARGIN_NS = 250;
constexpr uint32_t END_FREQ_OPTIONS_HZ[] = {100000, 500000, 1000000}; // The first MCPWM TX end may belong to a pulse that was already active when
// capture was enabled. The following period also drains capture events that
// were pending independently in the rising/falling channels. Validation
// therefore begins at the third TX period.
constexpr uint8_t DRIVER_CAPTURE_SYNC_CYCLES = 2;
// Any response this long is a fault, not a normal acknowledgement.
constexpr uint32_t DRIVER_FAULT_MIN_NS = 1500;
constexpr uint32_t DRIVER_RX_STUCK_MIN_NS = 20000;
// Retained by the generic receiver backend; the driver test itself uses the
// stricter ACK start deadline above.
constexpr uint32_t DRIVER_RESPONSE_TIMEOUT_NS = 10000;
// All achievable whole-number frequencies in the supported 1 kHz..1 MHz // -------------------------- Menu value arrays -----------------------------
// range, used for adjacent test stages rather than direct menu selection. // The test uses one selected PWM frequency and walks the pulse-width list from
constexpr uint32_t TEST_FREQUENCIES_HZ[] = { // the selected maximum down to the selected minimum. Widths are stored in
1000, 2000, 5000, 10000, 25000, 50000, // nanoseconds so sub-microsecond pulses remain representable without floats.
100000, 200000, 312500, 400000, 500000, 625000, 800000, 1000000 constexpr uint32_t PWM_FREQUENCY_OPTIONS_HZ[] = {
500, 1000, 2000, 5000, 10000,
};
constexpr uint32_t MAX_PULSE_OPTIONS_NS[] = {
2000, 5000, 10000, 50000, 100000, 500000
};
constexpr uint32_t MIN_PULSE_OPTIONS_NS[] = {
250, 500, 1000, 2000, 5000, 10000, 50000
};
constexpr uint32_t TEST_PULSE_WIDTHS_NS[] = {
50, 100, 150, 200, 250, 500, 1000, 2000, 5000, 10000, 20000, 50000,
100000, 200000, 500000, 1000000
}; };
constexpr float ACCURACY_OPTIONS_PCT[] = {1.0f, 2.0f, 5.0f, 10.0f}; constexpr float ACCURACY_OPTIONS_PCT[] = {1.0f, 2.0f, 5.0f, 10.0f};
constexpr uint32_t TEST_TIME_OPTIONS_MS[] = {100, 250, 500, 1000, 2000, 5000}; constexpr uint32_t TEST_TIME_OPTIONS_MS[] = {100, 250, 500, 1000, 2000, 5000, 60000};
constexpr uint8_t DUTY_OPTIONS_PCT[] = {10, 25, 50, 75, 90};
template <typename T, size_t N> constexpr size_t countOf(const T (&)[N]) { return N; } template <typename T, size_t N> constexpr size_t countOf(const T (&)[N]) { return N; }

View File

@@ -17,11 +17,23 @@ constexpr const char *ROLE_NAMES[] = {
"СОЛО", "МАСТЕР", "СЛЕЙВ" "СОЛО", "МАСТЕР", "СЛЕЙВ"
}; };
constexpr const char *TEST_NAMES[] = {
"ОПТИКА", "ДРАЙВЕР"
};
constexpr const char *TEST_GROUP_NAMES[] = {
"ОПТИКА", "ПЛАТА"
};
constexpr const char *BOARD_TEST_NAMES[] = {
"АЦП", "ШИМ ВЫХОД", "RX ВХОД"
};
constexpr const char *FAIL_NAMES[] = { constexpr const char *FAIL_NAMES[] = {
"НЕТ ОШИБКИ", "НЕТ ОШИБКИ",
"НЕТ СИГНАЛА", "НЕТ СИГНАЛА",
"ПЕРИОД ВНЕ ДОПУСКА", "ПЕРИОД ВНЕ ДОПУСКА",
"ЗАПОЛН. ВНЕ ДОПУСКА", "ИМПУЛЬС ВНЕ ДОПУСКА",
"ЛИШНИЙ ФРОНТ", "ЛИШНИЙ ФРОНТ",
"ИМПУЛЬСНАЯ ПОМЕХА", "ИМПУЛЬСНАЯ ПОМЕХА",
"ПРОПУЩЕН ФРОНТ", "ПРОПУЩЕН ФРОНТ",
@@ -29,18 +41,31 @@ constexpr const char *FAIL_NAMES[] = {
"СВЯЗЬ ПОТЕРЯНА", "СВЯЗЬ ПОТЕРЯНА",
"РЕЖИМ НЕ ПОДДЕРЖИВ.", "РЕЖИМ НЕ ПОДДЕРЖИВ.",
"НЕ ХВАТАЕТ ТОЧНОСТИ", "НЕ ХВАТАЕТ ТОЧНОСТИ",
"ТЕСТ ОСТАНОВЛЕН" "ТЕСТ ОСТАНОВЛЕН",
"НЕТ ОТВЕТА ACK",
"ТАЙМИНГ ACK",
"АВАРИЯ ДРАЙВЕРА",
"ОТВЕТЫ ACK СЛИЛИСЬ"
}; };
constexpr const char *MODE_PREFIX = "РЕЖИМ: "; constexpr const char *MODE_PREFIX = "РЕЖИМ: ";
constexpr const char *START_RUN = "ГОТОВ К ЗАПУСКУ"; constexpr const char *START_RUN = "ГОТОВ К ЗАПУСКУ";
constexpr const char *MENU_START_FREQUENCY = "ЧАСТОТА ОТ:"; constexpr const char *MENU_FREQUENCY = "ЧАСТОТА ШИМ:";
constexpr const char *MENU_END_FREQUENCY = "ЧАСТОТА ДО:"; constexpr const char *MENU_TEST_GROUP = "ГРУППА ТЕСТОВ:";
constexpr const char *MENU_MAX_PULSE = "МАКС. ИМПУЛЬС:";
constexpr const char *MENU_MIN_PULSE = "МИН. ИМПУЛЬС:";
constexpr const char *MENU_ACCURACY = "ТОЧНОСТЬ:"; constexpr const char *MENU_ACCURACY = "ТОЧНОСТЬ:";
constexpr const char *MENU_TEST_TIME = "ВРЕМЯ ВЫБОРКИ:"; constexpr const char *MENU_TEST_TIME = "ВРЕМЯ ВЫБОРКИ:";
constexpr const char *MENU_PWM_DUTY = "ЗАПОЛНЕНИЕ:"; constexpr const char *MENU_LIGHT_CODE = "АКТ. УРОВЕНЬ:";
constexpr const char *MENU_OPTICAL_CALIBRATION = "КАЛИБР. ОПТИКИ";
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 *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 *FREQUENCY_UNIT = " Гц";
constexpr const char *SLAVE_READY = "СЛЕЙВ ГОТОВ"; constexpr const char *SLAVE_READY = "СЛЕЙВ ГОТОВ";
@@ -48,18 +73,22 @@ constexpr const char *WAIT_MASTER = "ОЖИДАНИЕ МАСТЕРА";
constexpr const char *LINK_FAILED = "СВЯЗЬ НЕ УСТАНОВЛЕНА"; constexpr const char *LINK_FAILED = "СВЯЗЬ НЕ УСТАНОВЛЕНА";
constexpr const char *RADIO_ERROR = "ОШИБКА СВЯЗИ"; constexpr const char *RADIO_ERROR = "ОШИБКА СВЯЗИ";
constexpr const char *MASTER_SEARCH = "ПОИСК СЛЕЙВА"; constexpr const char *MASTER_SEARCH = "ПОИСК СЛЕЙВА";
constexpr const char *HOLD_START_STOP = "УДЕРЖ. START ДЛЯ СТОП"; constexpr const char *HOLD_START_STOP = "УДЕРЖ. ПУСК ДЛЯ СТОП";
constexpr const char *MASTER_SEEN = "МАСТЕР ОБНАРУЖЕН"; constexpr const char *MASTER_SEEN = "МАСТЕР ОБНАРУЖЕН";
constexpr const char *ACK_SENT = "ОТВЕТ ОТПРАВЛЕН"; constexpr const char *ACK_SENT = "ОТВЕТ ОТПРАВЛЕН";
constexpr const char *START_AGAIN = "ГОТОВ К ЗАПУСКУ"; constexpr const char *START_AGAIN = "ГОТОВ К ЗАПУСКУ";
constexpr const char *TEST_FAILED = "ТЕСТ НЕ ПРОЙДЕН"; constexpr const char *TEST_FAILED = "ТЕСТ НЕ ПРОЙДЕН";
constexpr const char *PASS_WORD = "ТЕСТ ПРОЙДЕН"; constexpr const char *PASS_WORD = "ТЕСТ ПРОЙДЕН";
constexpr const char *FAIL_FORMAT = "СБОЙ %s %.0f%%"; constexpr const char *FAIL_FORMAT = "СБОЙ %s";
constexpr const char *TEST_FORMAT = "Тест:%-6s %2.0f%% %5s"; constexpr const char *TEST_FORMAT = "%s, %s";
constexpr const char *PERIOD_OUT_FORMAT = "ОШИБКА ЧАСТОТЫ %s"; constexpr const char *TEST_TARGET_FORMAT = "ТЕСТ: %s";
constexpr const char *DUTY_OUT_FORMAT = "ОШИБКА ЗАПОЛН. %s"; constexpr const char *FAIL_TARGET_FORMAT = "СБОЙ: %s";
constexpr const char *NO_MEASUREMENT = "F:--- D:---%"; constexpr const char *PERIOD_OUT_FORMAT = "ЧАСТОТА: %s";
constexpr const char *DUTY_OUT_FORMAT = "ИМПУЛЬС: %s";
constexpr const char *NO_MEASUREMENT = "F:---, P:---";
constexpr const char *DRIVER_RESPONSE_FORMAT = "ACK:%lu D:%luns";
constexpr const char *DRIVER_MEASUREMENT_FORMAT = "D: %s, P: %s";
#elif UI_LANGUAGE == UI_LANGUAGE_EN #elif UI_LANGUAGE == UI_LANGUAGE_EN
@@ -67,11 +96,23 @@ constexpr const char *ROLE_NAMES[] = {
"SOLO", "MASTER", "SLAVE" "SOLO", "MASTER", "SLAVE"
}; };
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[] = { constexpr const char *FAIL_NAMES[] = {
"NONE", "NONE",
"NO SIGNAL", "NO SIGNAL",
"PERIOD OUT", "PERIOD OUT",
"DUTY OUT", "PULSE OUT",
"EXTRA EDGE", "EXTRA EDGE",
"GLITCH", "GLITCH",
"LOST EDGE", "LOST EDGE",
@@ -79,18 +120,31 @@ constexpr const char *FAIL_NAMES[] = {
"LINK LOST", "LINK LOST",
"UNSUPPORTED", "UNSUPPORTED",
"RESOLUTION", "RESOLUTION",
"ABORTED" "ABORTED",
"ACK MISSING",
"ACK TIMING",
"DRIVER FAULT",
"ACK MERGED"
}; };
constexpr const char *MODE_PREFIX = "MODE: "; constexpr const char *MODE_PREFIX = "MODE: ";
constexpr const char *START_RUN = "READY TO START"; constexpr const char *START_RUN = "READY TO START";
constexpr const char *MENU_START_FREQUENCY = "START FREQ:"; constexpr const char *MENU_FREQUENCY = "PWM FREQUENCY:";
constexpr const char *MENU_END_FREQUENCY = "END FREQ:"; 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:"; constexpr const char *MENU_ACCURACY = "ACCURACY:";
constexpr const char *MENU_TEST_TIME = "TEST TIME:"; constexpr const char *MENU_TEST_TIME = "TEST TIME:";
constexpr const char *MENU_PWM_DUTY = "PWM DUTY:"; constexpr const char *MENU_LIGHT_CODE = "ACTIVE LEVEL:";
constexpr const char *MENU_OPTICAL_CALIBRATION = "OPTICAL CALIBRATION";
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 *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 *FREQUENCY_UNIT = " Hz";
constexpr const char *SLAVE_READY = "SLAVE READY"; constexpr const char *SLAVE_READY = "SLAVE READY";
@@ -105,11 +159,15 @@ constexpr const char *START_AGAIN = "READY TO START";
constexpr const char *TEST_FAILED = "TEST FAILED"; constexpr const char *TEST_FAILED = "TEST FAILED";
constexpr const char *PASS_WORD = "TEST PASS"; constexpr const char *PASS_WORD = "TEST PASS";
constexpr const char *FAIL_FORMAT = "FAIL %s %.0f%%"; constexpr const char *FAIL_FORMAT = "FAIL %s";
constexpr const char *TEST_FORMAT = "Test:%-6s %2.0f%% %5s"; constexpr const char *TEST_FORMAT = "%s, %s";
constexpr const char *PERIOD_OUT_FORMAT = "PERIOD OUT %s"; constexpr const char *TEST_TARGET_FORMAT = "TEST: %s";
constexpr const char *DUTY_OUT_FORMAT = "DUTY OUT %s"; constexpr const char *FAIL_TARGET_FORMAT = "FAIL AT %s";
constexpr const char *NO_MEASUREMENT = "F:--- D:---%"; constexpr const char *PERIOD_OUT_FORMAT = "FREQ OUT %s";
constexpr const char *DUTY_OUT_FORMAT = "PULSE OUT %s";
constexpr const char *NO_MEASUREMENT = "F:---, P:---";
constexpr const char *DRIVER_RESPONSE_FORMAT = "ACK:%lu D:%luns";
constexpr const char *DRIVER_MEASUREMENT_FORMAT = "D: %s, P: %s";
#else #else
#error "UI_LANGUAGE must be UI_LANGUAGE_EN or UI_LANGUAGE_RU" #error "UI_LANGUAGE must be UI_LANGUAGE_EN or UI_LANGUAGE_RU"

View File

@@ -9,10 +9,35 @@ const char *roleName(Role r) {
return i < 3 ? names[i] : "?"; 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);
return i < 4 ? names[i] : "??";
}
const char *failName(FailReason r) { const char *failName(FailReason r) {
static const char *names[] = {"NONE", "NO SIGNAL", "PERIOD OUT", "DUTY OUT", static const char *names[] = {"NONE", "NO SIGNAL", "PERIOD OUT", "PULSE OUT",
"EXTRA EDGE", "GLITCH", "LOST EDGE", "DATA LOSS ERROR", "LINK LOST", "EXTRA EDGE", "GLITCH", "LOST EDGE", "DATA LOSS ERROR", "LINK LOST",
"UNSUPPORTED", "RESOLUTION", "ABORTED"}; "UNSUPPORTED", "RESOLUTION", "ABORTED", "ACK MISSING", "ACK TIMING",
"DRIVER FAULT", "ACK MERGED"};
const uint8_t i = static_cast<uint8_t>(r); const uint8_t i = static_cast<uint8_t>(r);
return i < (sizeof(names) / sizeof(names[0])) ? names[i] : "UNKNOWN"; return i < (sizeof(names) / sizeof(names[0])) ? names[i] : "UNKNOWN";
} }
@@ -31,19 +56,27 @@ uint32_t settingsChecksum(const Settings &s) {
return hash; return hash;
} }
uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz) { bool txActiveLightOn(const Settings &s) {
if (!startHz || endHz <= startHz) return 0; return static_cast<uint8_t>(s.lightCode) < static_cast<uint8_t>(LightCode::LH);
}
bool rxActiveLightOn(const Settings &s) {
return (static_cast<uint8_t>(s.lightCode) & 1U) == 0U;
}
uint32_t pulseWidthPointCount(uint32_t maxPulseNs, uint32_t minPulseNs) {
if (!minPulseNs || maxPulseNs < minPulseNs) return 0;
uint32_t count = 0; uint32_t count = 0;
for (size_t i = 0; i < countOf(TEST_FREQUENCIES_HZ); ++i) for (size_t i = 0; i < countOf(TEST_PULSE_WIDTHS_NS); ++i)
if (TEST_FREQUENCIES_HZ[i] >= startHz && TEST_FREQUENCIES_HZ[i] <= endHz) ++count; if (TEST_PULSE_WIDTHS_NS[i] >= minPulseNs && TEST_PULSE_WIDTHS_NS[i] <= maxPulseNs) ++count;
return count; return count;
} }
uint32_t frequencyAt(uint32_t startHz, uint32_t endHz, uint32_t index) { uint32_t pulseWidthAt(uint32_t maxPulseNs, uint32_t minPulseNs, uint32_t index) {
for (size_t i = 0; i < countOf(TEST_FREQUENCIES_HZ); ++i) { for (size_t i = countOf(TEST_PULSE_WIDTHS_NS); i > 0; --i) {
const uint32_t frequency = TEST_FREQUENCIES_HZ[i]; const uint32_t pulseNs = TEST_PULSE_WIDTHS_NS[i - 1U];
if (frequency < startHz || frequency > endHz) continue; if (pulseNs < minPulseNs || pulseNs > maxPulseNs) continue;
if (!index--) return frequency; if (!index--) return pulseNs;
} }
return 0; return 0;
} }
@@ -58,26 +91,17 @@ uint64_t nominalStageUs(uint32_t frequencyHz, uint32_t sampleTimeMs, uint32_t se
RX_PROCESSING_PERIODS_PER_SECOND - 1U) / RX_PROCESSING_PERIODS_PER_SECOND; RX_PROCESSING_PERIODS_PER_SECOND - 1U) / RX_PROCESSING_PERIODS_PER_SECOND;
const uint64_t samplingWallUs = processingUs > sampleUs ? processingUs : sampleUs; const uint64_t samplingWallUs = processingUs > sampleUs ? processingUs : sampleUs;
uint64_t chunkSymbols =
(static_cast<uint64_t>(frequencyHz) * RMT_TARGET_CHUNK_US + 999999ULL) / 1000000ULL;
if (chunkSymbols < RMT_MIN_RECEIVE_SYMBOLS) chunkSymbols = RMT_MIN_RECEIVE_SYMBOLS;
if (chunkSymbols > RMT_MAX_RECEIVE_SYMBOLS) chunkSymbols = RMT_MAX_RECEIVE_SYMBOLS;
const uint64_t batchWaitUs =
((chunkSymbols * 1000000ULL + frequencyHz - 1U) / frequencyHz) * MEASUREMENT_PROGRESS_STEPS;
const uint64_t settleUs = const uint64_t settleUs =
(1000000ULL * settleCycles * MEASUREMENT_PROGRESS_STEPS + frequencyHz - 1U) / frequencyHz; (1000000ULL * settleCycles * MEASUREMENT_PROGRESS_STEPS + frequencyHz - 1U) / frequencyHz;
// Initial stage screen, nine intermediate screens and the final result. // Initial stage screen, nine intermediate screens and the final result.
const uint64_t displayUs = static_cast<uint64_t>(OLED_PROGRESS_UPDATE_MS) * 1000ULL * const uint64_t displayUs = static_cast<uint64_t>(OLED_PROGRESS_UPDATE_MS) * 1000ULL *
(MEASUREMENT_PROGRESS_STEPS + 1U); (MEASUREMENT_PROGRESS_STEPS + 1U);
return samplingWallUs + batchWaitUs + settleUs + displayUs; return samplingWallUs + settleUs + displayUs;
} }
uint64_t nominalTotalUs(const TestParams &p, uint32_t settleCycles) { uint64_t nominalTotalUs(const TestParams &p, uint32_t settleCycles) {
uint64_t total = 0; return static_cast<uint64_t>(pulseWidthPointCount(p.maxPulseNs, p.minPulseNs)) *
const uint32_t count = frequencyPointCount(p.startHz, p.endHz); nominalStageUs(p.frequencyHz, p.testTimeMs, settleCycles);
for (uint32_t i = 0; i < count; ++i)
total += nominalStageUs(frequencyAt(p.startHz, p.endHz, i), p.testTimeMs, settleCycles);
return total;
} }
bool periodWithin(float measured, float expected, float tolerance) { bool periodWithin(float measured, float expected, float tolerance) {
@@ -89,7 +113,7 @@ bool dutyWithin(float measured, float expected, float tolerance) {
} }
float effectiveTolerancePct(float configured) { float effectiveTolerancePct(float configured) {
return configured > 0.0f && configured <= 1.0001f ? 1.25f : configured; return configured;
} }
uint8_t choosePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz, uint8_t choosePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz,
@@ -116,49 +140,54 @@ uint8_t chooseStablePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz,
return fallback; return fallback;
} }
bool chooseIntegerPwmConfig(uint32_t requestedHz, uint32_t sourceClockHz, bool choosePwmConfig(uint32_t requestedHz, uint32_t requestedPulseNs,
uint8_t maxBits, uint8_t dutyPct, uint32_t sourceClockHz, uint8_t maxBits,
IntegerPwmConfig &config) { IntegerPwmConfig &config) {
if (!requestedHz || !sourceClockHz || !maxBits || dutyPct > 100U) return false; if (!requestedHz || !requestedPulseNs || !sourceClockHz || !maxBits) return false;
constexpr uint32_t FRACTION_SCALE = 256U;
constexpr uint32_t MAX_DIVIDER_RAW = 1024U * FRACTION_SCALE - 1U;
bool found = false; bool found = false;
uint32_t bestErrorHz = 0; uint32_t bestFrequencyError = UINT32_MAX;
uint32_t bestDutyError = 0; uint32_t bestPulseError = UINT32_MAX;
uint32_t bestLevels = 1;
for (uint8_t bits = 1; bits <= maxBits && bits < 31; ++bits) { for (uint8_t bits = 1; bits <= maxBits && bits < 31; ++bits) {
const uint32_t levels = 1UL << bits; const uint32_t levels = 1UL << bits;
for (uint32_t divider = 1; divider <= 1023U; ++divider) { const uint64_t dividerNumerator = static_cast<uint64_t>(sourceClockHz) * FRACTION_SCALE;
const uint32_t denominator = levels * divider; const uint64_t dividerDenominator = static_cast<uint64_t>(requestedHz) * levels;
// A fixed integer divider gives identical PWM periods. Requiring an const uint32_t dividerFloor = static_cast<uint32_t>(dividerNumerator / dividerDenominator);
// exact division also guarantees that the physical frequency is a const uint32_t candidates[] = {dividerFloor, dividerFloor + 1U};
// whole number of hertz rather than a rounded value. for (uint32_t dividerRaw : candidates) {
if (sourceClockHz % denominator) continue; if (dividerRaw < FRACTION_SCALE || dividerRaw > MAX_DIVIDER_RAW) continue;
const uint32_t actualHz = sourceClockHz / denominator; const uint64_t frequencyDenominator = static_cast<uint64_t>(levels) * dividerRaw;
const uint32_t errorHz = actualHz > requestedHz const uint32_t actualHz = static_cast<uint32_t>(
? actualHz - requestedHz : requestedHz - actualHz; (dividerNumerator + frequencyDenominator / 2U) / frequencyDenominator);
const uint32_t dutyCount = (static_cast<uint64_t>(levels) * dutyPct + 50U) / 100U; if (!actualHz) continue;
const uint32_t representedDuty = dutyCount * 100U;
const uint32_t requestedDuty = levels * dutyPct;
const uint32_t dutyError = representedDuty > requestedDuty
? representedDuty - requestedDuty : requestedDuty - representedDuty;
const bool frequencyBetter = !found || errorHz < bestErrorHz; const uint64_t dutyNumerator = static_cast<uint64_t>(requestedPulseNs) *
const bool frequencyEqual = found && errorHz == bestErrorHz; sourceClockHz * FRACTION_SCALE;
const bool dutyBetter = frequencyEqual && const uint64_t dutyDenominator = static_cast<uint64_t>(dividerRaw) * 1000000000ULL;
static_cast<uint64_t>(dutyError) * bestLevels < uint32_t dutyCount = static_cast<uint32_t>((dutyNumerator + dutyDenominator / 2U) /
static_cast<uint64_t>(bestDutyError) * levels; dutyDenominator);
const bool dutyEqual = frequencyEqual && if (!dutyCount) dutyCount = 1U;
static_cast<uint64_t>(dutyError) * bestLevels == if (dutyCount >= levels) dutyCount = levels - 1U;
static_cast<uint64_t>(bestDutyError) * levels; if (!dutyCount) continue;
if (!frequencyBetter && !dutyBetter && !(dutyEqual && bits > config.bits)) continue;
config.actualHz = actualHz; const uint32_t actualPulseNs = static_cast<uint32_t>(
config.divider = static_cast<uint16_t>(divider); (static_cast<uint64_t>(dutyCount) * dividerRaw * 1000000000ULL +
config.bits = bits; static_cast<uint64_t>(sourceClockHz) * FRACTION_SCALE / 2U) /
bestErrorHz = errorHz; (static_cast<uint64_t>(sourceClockHz) * FRACTION_SCALE));
bestDutyError = dutyError; const uint32_t frequencyError = actualHz > requestedHz ? actualHz - requestedHz : requestedHz - actualHz;
bestLevels = levels; const uint32_t pulseError = actualPulseNs > requestedPulseNs
? actualPulseNs - requestedPulseNs : requestedPulseNs - actualPulseNs;
if (found && (frequencyError > bestFrequencyError ||
(frequencyError == bestFrequencyError && pulseError > bestPulseError) ||
(frequencyError == bestFrequencyError && pulseError == bestPulseError && bits <= config.bits)))
continue;
config = {actualHz, dividerRaw, dutyCount, actualPulseNs, bits};
bestFrequencyError = frequencyError;
bestPulseError = pulseError;
found = true; found = true;
} }
} }
@@ -166,37 +195,70 @@ bool chooseIntegerPwmConfig(uint32_t requestedHz, uint32_t sourceClockHz,
} }
FailReason validateResolution(uint32_t frequencyHz, float dutyPct, float accuracyPct, FailReason validateResolution(uint32_t frequencyHz, float dutyPct, float accuracyPct,
uint32_t captureHz, uint8_t pwmBits) { uint32_t periodCaptureHz, uint32_t pulseCaptureHz,
if (!frequencyHz || !captureHz || !pwmBits) return FailReason::RESOLUTION; uint8_t pwmBits,
const float periodTicks = static_cast<float>(captureHz) / frequencyHz; uint16_t averagingPeriods) {
const float activeTicks = periodTicks * dutyPct / 100.0f; if (!frequencyHz || !periodCaptureHz || !pulseCaptureHz || !pwmBits || !averagingPeriods)
const float inactiveTicks = periodTicks - activeTicks; return FailReason::RESOLUTION;
if (periodTicks < 4.0f || activeTicks < 2.0f || inactiveTicks < 2.0f) return FailReason::RESOLUTION; const float periodTicks = static_cast<float>(periodCaptureHz) / frequencyHz;
const float activeTicks = static_cast<float>(pulseCaptureHz) * dutyPct /
(100.0f * frequencyHz);
if (periodTicks < 4.0f || activeTicks < 2.0f) return FailReason::RESOLUTION;
const float timerPeriodError = 100.0f / periodTicks; const float timerPeriodError = 100.0f / periodTicks;
const float timerDutyError = 100.0f / periodTicks; const float timerPulseError = 100.0f / activeTicks;
// Measurement uses the duty actually programmed into LEDC. A coarse PWM // Measurement uses the duty actually programmed into LEDC. A coarse PWM
// step is not itself an error when the requested value (e.g. 50%) is exactly // step is not itself an error when the requested value (e.g. 50%) is exactly
// representable; only the selected value's actual quantization matters. // representable; only the selected value's actual quantization matters.
const float effectiveAccuracy = effectiveTolerancePct(accuracyPct); const float effectiveAccuracy = effectiveTolerancePct(accuracyPct);
return (timerPeriodError > effectiveAccuracy || timerDutyError > effectiveAccuracy) return (timerPeriodError > effectiveAccuracy || timerPulseError > effectiveAccuracy)
? FailReason::RESOLUTION : FailReason::NONE; ? FailReason::RESOLUTION : FailReason::NONE;
} }
FailReason evaluatePeriod(const PulsePeriod &p, uint32_t tickHz, float expectedHz, FailReason evaluatePeriod(const PulsePeriod &p, uint32_t tickHz, float expectedHz,
float expectedDuty, float tolerance, uint8_t repeat, float expectedDuty, float tolerance, uint8_t repeat,
StageStats &s) { StageStats &s) {
if (!p.periodTicks || p.activeTicks >= p.periodTicks) return FailReason::EXTRA_EDGE; const uint32_t pulseTickHz = p.activeTickHz ? p.activeTickHz : tickHz;
if (!p.periodTicks || !p.activeTicks || !tickHz || !pulseTickHz || expectedHz <= 0.0f)
return FailReason::EXTRA_EDGE;
const float hz = static_cast<float>(tickHz) / p.periodTicks; const float hz = static_cast<float>(tickHz) / p.periodTicks;
const float duty = 100.0f * p.activeTicks / p.periodTicks; const float duty = static_cast<float>(100.0 * p.activeTicks * tickHz /
(static_cast<double>(pulseTickHz) * p.periodTicks));
++s.periods; ++s.periods;
s.periodSum += p.periodTicks; s.activeSum += p.activeTicks; s.periodSum += p.periodTicks; s.activeSum += p.activeTicks;
if (p.periodTicks < s.minPeriod) s.minPeriod = p.periodTicks; if (p.periodTicks < s.minPeriod) s.minPeriod = p.periodTicks;
if (p.periodTicks > s.maxPeriod) s.maxPeriod = p.periodTicks; if (p.periodTicks > s.maxPeriod) s.maxPeriod = p.periodTicks;
if (p.activeTicks < s.minActive) s.minActive = p.activeTicks; if (p.activeTicks < s.minActive) s.minActive = p.activeTicks;
if (p.activeTicks > s.maxActive) s.maxActive = p.activeTicks; if (p.activeTicks > s.maxActive) s.maxActive = p.activeTicks;
FailReason reason = FailReason::NONE; // Validate every complete period independently. A single capture tick is the
if (!periodWithin(hz, expectedHz, tolerance)) reason = FailReason::PERIOD_OUT; // unavoidable endpoint uncertainty, so only that one tick may be corrected
else if (!dutyWithin(duty, expectedDuty, tolerance)) reason = FailReason::DUTY_OUT; // toward the expected value. It cannot hide a larger isolated distortion.
bool frequencyOk = periodWithin(hz, expectedHz, tolerance);
if (!frequencyOk) {
uint32_t correctedPeriod = p.periodTicks;
if (hz > expectedHz) ++correctedPeriod;
else if (correctedPeriod > 1U) --correctedPeriod;
frequencyOk = periodWithin(static_cast<float>(tickHz) / correctedPeriod,
expectedHz, tolerance);
}
const double expectedPulseTicks = static_cast<double>(pulseTickHz) * expectedDuty /
(100.0 * expectedHz);
bool pulseOk = expectedPulseTicks > 0.0 &&
fabs(static_cast<double>(p.activeTicks) - expectedPulseTicks) * 100.0 /
expectedPulseTicks <= tolerance + 0.0001;
if (!pulseOk) {
uint32_t correctedActive = p.activeTicks;
if (correctedActive > expectedPulseTicks) {
if (correctedActive) --correctedActive;
} else {
++correctedActive;
}
pulseOk = fabs(static_cast<double>(correctedActive) - expectedPulseTicks) *
100.0 / expectedPulseTicks <= tolerance + 0.0001;
}
const FailReason reason = !frequencyOk ? FailReason::PERIOD_OUT :
(!pulseOk ? FailReason::DUTY_OUT : FailReason::NONE);
if (reason != FailReason::NONE && s.reason == FailReason::NONE) { if (reason != FailReason::NONE && s.reason == FailReason::NONE) {
s.reason = reason; s.firstBadPeriod = s.periods; s.firstBadRepeat = repeat; s.reason = reason; s.firstBadPeriod = s.periods; s.firstBadRepeat = repeat;
s.badFrequency = hz; s.badDuty = duty; s.badFrequency = hz; s.badDuty = duty;
@@ -256,3 +318,64 @@ FailReason evaluatePeriodFast(const PulsePeriod &p, uint32_t tickHz,
} }
return reason; return reason;
} }
FailReason evaluatePeriodWindow(uint64_t periodSum, uint64_t activeSum,
uint32_t periodCount, uint32_t tickHz,
float expectedHz, float expectedDuty,
float tolerance,
uint32_t minPeriod, uint32_t maxPeriod,
uint8_t repeat,
StageStats &s) {
if (!periodSum || !periodCount || activeSum >= periodSum || !tickHz)
return FailReason::EXTRA_EDGE;
const float hz = static_cast<float>(
static_cast<double>(tickHz) * periodCount / periodSum);
const float duty = static_cast<float>(
100.0 * static_cast<double>(activeSum) / periodSum);
bool frequencyOk = periodWithin(hz, expectedHz, tolerance);
if (!frequencyOk && maxPeriod == minPeriod + 1U) {
// At a tolerance boundary, alternating adjacent capture counts prove that the
// result is quantization-limited. Accept only when a one-tick correction
// toward the expected value returns the averaged frequency into tolerance.
// Consecutive periods telescope into one first-to-last edge interval, so
// the whole window has a one-tick endpoint uncertainty, not one tick per
// period.
uint64_t correctedPeriodSum = periodSum;
if (hz > expectedHz) ++correctedPeriodSum;
else if (periodSum > 1U) --correctedPeriodSum;
const float correctedHz = static_cast<float>(
static_cast<double>(tickHz) * periodCount / correctedPeriodSum);
frequencyOk = periodWithin(correctedHz, expectedHz, tolerance);
}
const double expectedPulseTicks = static_cast<double>(tickHz) * expectedDuty /
(100.0 * expectedHz);
const double measuredPulseTicks = static_cast<double>(activeSum) / periodCount;
bool pulseOk = expectedPulseTicks > 0.0 &&
fabs(measuredPulseTicks - expectedPulseTicks) * 100.0 / expectedPulseTicks <= tolerance + 0.0001;
if (!pulseOk) {
// Unlike full periods, active intervals do not telescope: every pulse is
// bounded by a different rising/falling edge pair. With slowly drifting
// asynchronous clocks an entire short window can therefore quantize to
// the same adjacent count (e.g. 41/80 for a true 50% duty). Apply one tick
// per active interval even when minActive == maxActive.
const bool dutyHigh = measuredPulseTicks > expectedPulseTicks;
const uint64_t correctedActive = dutyHigh
? (activeSum > periodCount ? activeSum - periodCount : 0U)
: activeSum + periodCount;
const double correctedPulseTicks = static_cast<double>(correctedActive) / periodCount;
pulseOk = fabs(correctedPulseTicks - expectedPulseTicks) * 100.0 /
expectedPulseTicks <= tolerance + 0.0001;
}
FailReason reason = !frequencyOk ? FailReason::PERIOD_OUT :
(!pulseOk ? FailReason::DUTY_OUT : FailReason::NONE);
if (reason != FailReason::NONE && s.reason == FailReason::NONE) {
s.reason = reason;
s.firstBadPeriod = s.periods >= periodCount ? s.periods - periodCount + 1U : 1U;
s.firstBadRepeat = repeat;
s.badFrequency = hz;
s.badDuty = duty;
}
return reason;
}

View File

@@ -4,37 +4,51 @@
#include <stddef.h> #include <stddef.h>
enum class Role : uint8_t { SOLO, MASTER, SLAVE }; 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 { enum class FailReason : uint8_t {
NONE, NO_SIGNAL, PERIOD_OUT, DUTY_OUT, EXTRA_EDGE, GLITCH, LOST_EDGE, NONE, NO_SIGNAL, PERIOD_OUT, DUTY_OUT, EXTRA_EDGE, GLITCH, LOST_EDGE,
DATA_LOSS, LINK_LOST, UNSUPPORTED, RESOLUTION, ABORTED DATA_LOSS, LINK_LOST, UNSUPPORTED, RESOLUTION, ABORTED,
ACK_MISSING, ACK_TIMING, DRIVER_FAULT, ACK_MERGED
}; };
const char *roleName(Role role); 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); const char *failName(FailReason reason);
struct Settings { struct Settings {
uint16_t version; uint16_t version;
uint8_t role; uint8_t role;
uint8_t startIndex; uint8_t testKind;
uint8_t endIndex; uint8_t lightCode;
uint8_t frequencyIndex;
uint8_t maxPulseIndex;
uint8_t minPulseIndex;
uint8_t accuracyIndex; uint8_t accuracyIndex;
uint8_t timeIndex; uint8_t timeIndex;
uint8_t dutyIndex; uint8_t testGroup;
uint8_t boardTest;
uint32_t checksum; uint32_t checksum;
}; };
struct TestParams { struct TestParams {
uint32_t startHz; uint32_t frequencyHz;
uint32_t endHz; uint32_t maxPulseNs;
uint32_t minPulseNs;
float accuracyPct; float accuracyPct;
uint32_t testTimeMs; uint32_t testTimeMs;
uint8_t dutyPct;
}; };
struct PulsePeriod { struct PulsePeriod {
uint64_t startTick; uint64_t startTick;
uint32_t periodTicks; uint32_t periodTicks;
uint32_t activeTicks; uint32_t activeTicks;
uint32_t activeTickHz;
}; };
struct StageStats { struct StageStats {
@@ -63,13 +77,17 @@ struct PeriodLimits {
struct IntegerPwmConfig { struct IntegerPwmConfig {
uint32_t actualHz; uint32_t actualHz;
uint16_t divider; uint32_t dividerRaw;
uint32_t dutyCount;
uint32_t actualPulseNs;
uint8_t bits; uint8_t bits;
}; };
uint32_t settingsChecksum(const Settings &s); uint32_t settingsChecksum(const Settings &s);
uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz); bool txActiveLightOn(const Settings &s);
uint32_t frequencyAt(uint32_t startHz, uint32_t endHz, uint32_t index); bool rxActiveLightOn(const Settings &s);
uint32_t pulseWidthPointCount(uint32_t maxPulseNs, uint32_t minPulseNs);
uint32_t pulseWidthAt(uint32_t maxPulseNs, uint32_t minPulseNs, uint32_t index);
uint64_t nominalStageUs(uint32_t frequencyHz, uint32_t sampleTimeMs, uint32_t settleCycles); uint64_t nominalStageUs(uint32_t frequencyHz, uint32_t sampleTimeMs, uint32_t settleCycles);
uint64_t nominalTotalUs(const TestParams &p, uint32_t settleCycles); uint64_t nominalTotalUs(const TestParams &p, uint32_t settleCycles);
bool periodWithin(float measuredHz, float expectedHz, float tolerancePct); bool periodWithin(float measuredHz, float expectedHz, float tolerancePct);
@@ -79,11 +97,13 @@ uint8_t choosePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz,
uint8_t maxBits); uint8_t maxBits);
uint8_t chooseStablePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz, uint8_t chooseStablePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz,
uint8_t maxBits, uint8_t dutyPct); uint8_t maxBits, uint8_t dutyPct);
bool chooseIntegerPwmConfig(uint32_t requestedHz, uint32_t sourceClockHz, bool choosePwmConfig(uint32_t requestedHz, uint32_t requestedPulseNs,
uint8_t maxBits, uint8_t dutyPct, uint32_t sourceClockHz, uint8_t maxBits,
IntegerPwmConfig &config); IntegerPwmConfig &config);
FailReason validateResolution(uint32_t frequencyHz, float dutyPct, float accuracyPct, FailReason validateResolution(uint32_t frequencyHz, float dutyPct, float accuracyPct,
uint32_t captureResolutionHz, uint8_t pwmBits); uint32_t periodResolutionHz, uint32_t pulseResolutionHz,
uint8_t pwmBits,
uint16_t averagingPeriods);
FailReason evaluatePeriod(const PulsePeriod &period, uint32_t tickHz, float expectedHz, FailReason evaluatePeriod(const PulsePeriod &period, uint32_t tickHz, float expectedHz,
float expectedDuty, float tolerancePct, uint8_t repeat, float expectedDuty, float tolerancePct, uint8_t repeat,
StageStats &stats); StageStats &stats);
@@ -92,3 +112,10 @@ bool makePeriodLimits(uint32_t expectedHz, float expectedDuty, float tolerancePc
FailReason evaluatePeriodFast(const PulsePeriod &period, uint32_t tickHz, FailReason evaluatePeriodFast(const PulsePeriod &period, uint32_t tickHz,
const PeriodLimits &limits, uint8_t repeat, const PeriodLimits &limits, uint8_t repeat,
StageStats &stats); StageStats &stats);
FailReason evaluatePeriodWindow(uint64_t periodSum, uint64_t activeSum,
uint32_t periodCount, uint32_t tickHz,
float expectedHz, float expectedDuty,
float tolerancePct,
uint32_t minPeriod, uint32_t maxPeriod,
uint8_t repeat,
StageStats &stats);

View File

@@ -26,6 +26,7 @@ Display::Display() : oled_(128, 32, &Wire, -1) {}
bool Display::begin() { bool Display::begin() {
Wire.begin(GPIO_SDA, GPIO_SCL); Wire.begin(GPIO_SDA, GPIO_SCL);
Wire.setClock(400000); // keeps a full 128x32 framebuffer update near 15 ms Wire.setClock(400000); // keeps a full 128x32 framebuffer update near 15 ms
Wire.setTimeOut(30); // a faulty/stretched I2C bus must not stall button polling for seconds
// An absent optional OLED produces a large burst of ESP-IDF NACK messages. // An absent optional OLED produces a large burst of ESP-IDF NACK messages.
// Probe it once and keep the I2C driver quiet when no display is connected. // Probe it once and keep the I2C driver quiet when no display is connected.
esp_log_level_set("i2c.master", ESP_LOG_NONE); esp_log_level_set("i2c.master", ESP_LOG_NONE);
@@ -53,8 +54,8 @@ void Display::setPower(bool enabled) {
Log::printf("OLED", "display power %s", enabled ? "ON" : "OFF"); Log::printf("OLED", "display power %s", enabled ? "ON" : "OFF");
} }
void Display::drawTextLine(const char *text, int16_t y) { void Display::drawTextLine(const char *text, int16_t y, int16_t startX) {
int16_t x = 0; int16_t x = startX;
while (text && *text && x + CyrillicFont::WIDTH <= oled_.width()) { while (text && *text && x + CyrillicFont::WIDTH <= oled_.width()) {
const uint32_t codepoint = nextUtf8Codepoint(text); const uint32_t codepoint = nextUtf8Codepoint(text);
const uint8_t *glyph = CyrillicFont::glyph(codepoint); const uint8_t *glyph = CyrillicFont::glyph(codepoint);
@@ -75,7 +76,8 @@ void Display::drawTextLine(const char *text, int16_t y) {
} }
} }
void Display::show(const char *a, const char *b, uint32_t progress, uint32_t progressTotal) { void Display::show(const char *a, const char *b, uint32_t progress,
uint32_t progressTotal, const char *topRight) {
char one[64], two[64]; char one[64], two[64];
snprintf(one, sizeof(one), "%s", a ? a : ""); snprintf(two, sizeof(two), "%s", b ? b : ""); 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. // Serial is the primary UI mirror and remains available when OLED is absent.
@@ -85,6 +87,11 @@ void Display::show(const char *a, const char *b, uint32_t progress, uint32_t pro
oled_.clearDisplay(); oled_.clearDisplay();
drawTextLine(one, 3); drawTextLine(one, 3);
drawTextLine(two, 19); drawTextLine(two, 19);
if (topRight && *topRight) {
oled_.fillRect(oled_.width() - CyrillicFont::ADVANCE, 0,
CyrillicFont::ADVANCE, CyrillicFont::HEIGHT + 3, SSD1306_BLACK);
drawTextLine(topRight, 3, oled_.width() - CyrillicFont::ADVANCE);
}
if (progressTotal) { if (progressTotal) {
if (progress > progressTotal) progress = progressTotal; if (progress > progressTotal) progress = progressTotal;
const uint16_t width = static_cast<uint16_t>( const uint16_t width = static_cast<uint16_t>(
@@ -97,7 +104,7 @@ void Display::show(const char *a, const char *b, uint32_t progress, uint32_t pro
void Display::formatFrequency(float hz, char *out, size_t n) { void Display::formatFrequency(float hz, char *out, size_t n) {
float value = hz; const char *suffix = "Hz"; float value = hz; const char *suffix = "Hz";
if (hz >= 999950.0f) { value = hz / 1000000.0f; suffix = "M"; } if (hz >= 999950.0f) { value = hz / 1000000.0f; suffix = "M"; }
else if (hz >= 1000.0f) { value = hz / 1000.0f; suffix = "k"; } else if (hz >= 999.5f) { value = hz / 1000.0f; suffix = "k"; }
if (suffix[0] == 'M' && fabsf(value - roundf(value)) < 0.0005f) if (suffix[0] == 'M' && fabsf(value - roundf(value)) < 0.0005f)
snprintf(out, n, "%.0f%s", value, suffix); snprintf(out, n, "%.0f%s", value, suffix);
else if (value >= 100.0f) snprintf(out, n, "%.1f%s", value, suffix); else if (value >= 100.0f) snprintf(out, n, "%.1f%s", value, suffix);
@@ -119,6 +126,30 @@ void Display::formatTestFrequency(uint32_t hz, char *out, size_t n) {
snprintf(out, n, "%lu", hz); snprintf(out, n, "%lu", hz);
} }
void Display::formatPwmFrequency(uint32_t hz, char *out, size_t n) {
if (hz >= 1000000U && hz % 1000000U == 0U)
snprintf(out, n, "%luMHz", hz / 1000000U);
else if (hz >= 1000U && hz % 1000U == 0U)
snprintf(out, n, "%lukHz", hz / 1000U);
else if (hz >= 1000U)
snprintf(out, n, "%.3gkHz", hz / 1000.0f);
else
snprintf(out, n, "%luHz", hz);
}
void Display::formatPulse(uint32_t pulseNs, char *out, size_t n, bool measured) {
if (pulseNs >= 1000U) {
const float us = pulseNs / 1000.0f;
if (measured) snprintf(out, n, "%.2fu", us);
else if (pulseNs % 1000U == 0U) snprintf(out, n, "%luus", pulseNs / 1000U);
else snprintf(out, n, "%.2fus", us);
} else if (measured) {
snprintf(out, n, "%.3fu", pulseNs / 1000.0f);
} else {
snprintf(out, n, "%luns", pulseNs);
}
}
void Display::formatDuration(uint64_t us, char *out, size_t n) { void Display::formatDuration(uint64_t us, char *out, size_t n) {
const uint64_t totalSeconds = (us + 999999ULL) / 1000000ULL; const uint64_t totalSeconds = (us + 999999ULL) / 1000000ULL;
const uint64_t minutes = totalSeconds / 60ULL; const uint64_t minutes = totalSeconds / 60ULL;

View File

@@ -8,15 +8,18 @@ class Display {
Display(); Display();
bool begin(); bool begin();
void show(const char *line1, const char *line2, void show(const char *line1, const char *line2,
uint32_t progress = 0, uint32_t progressTotal = 0); uint32_t progress = 0, uint32_t progressTotal = 0,
const char *topRight = nullptr);
void setPower(bool enabled); void setPower(bool enabled);
bool available() const { return ok_; } bool available() const { return ok_; }
bool powered() const { return powered_; } bool powered() const { return powered_; }
static void formatFrequency(float hz, char *out, size_t size); static void formatFrequency(float hz, char *out, size_t size);
static void formatTestFrequency(uint32_t 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 formatPwmFrequency(uint32_t hz, char *out, size_t size);
static void formatPulse(uint32_t pulseNs, char *out, size_t size, bool measured = false);
static void formatDuration(uint64_t us, char *out, size_t size);
private: private:
void drawTextLine(const char *text, int16_t y); void drawTextLine(const char *text, int16_t y, int16_t startX = 0);
Adafruit_SSD1306 oled_; Adafruit_SSD1306 oled_;
bool ok_ = false; bool ok_ = false;
bool powered_ = false; bool powered_ = false;

View File

@@ -0,0 +1,736 @@
#include "DriverTest.h"
#include "Config.h"
#include "Log.h"
#include <driver/gpio.h>
#include <esp_cpu.h>
#include <esp_task_wdt.h>
#include <esp_timer.h>
#include <esp32-hal-cpu.h>
#include <soc/gpio_struct.h>
#include <string.h>
static inline uint32_t IRAM_ATTR maskAllInterrupts() {
uint32_t state;
asm volatile("rsil %0, 15" : "=a"(state) :: "memory");
return state;
}
static inline void IRAM_ATTR restoreInterrupts(uint32_t state) {
asm volatile("wsr %0, ps\nrsync" :: "a"(state) : "memory");
}
void DriverEdgeStats::reset() {
memset(this, 0, sizeof(*this));
minDelayTicks = minResponseTicks = UINT32_MAX;
}
void DriverStats::reset() {
memset(this, 0, sizeof(*this));
minDelayTicks = minResponseTicks = UINT32_MAX;
turnOn.reset();
turnOff.reset();
reason = FailReason::NONE;
}
uint64_t DriverTest::nsToTicks(uint32_t ns) const {
return (static_cast<uint64_t>(ns) * captureHz_ + 999999999ULL) /
1000000000ULL;
}
uint64_t DriverTest::ticksToNs(uint64_t ticks) const {
return (ticks * 1000000000ULL + captureHz_ / 2U) / captureHz_;
}
bool DriverTest::start(uint32_t frequencyHz, uint32_t pulseNs,
float tolerancePct, uint32_t testTimeMs,
uint8_t settleCycles, bool activeTxLightOn,
bool activeRxLightOn) {
(void)tolerancePct;
(void)settleCycles;
if (!receiver_.highRateBackend() || !frequencyHz || !pulseNs ||
!testTimeMs || GPIO_PWM >= 32U || GPIO_RX >= 32U) return false;
requestCaptureStop();
if (!waitCaptureStopped(25U)) return false;
if (!pollTask_ && xTaskCreatePinnedToCore(pollTaskEntry, "driver-poll",
3072, this, configMAX_PRIORITIES - 1U, &pollTask_, 0) != pdPASS)
return false;
if (!analyzerTask_ && xTaskCreatePinnedToCore(analyzerTaskEntry,
"driver-analyze", 4096, this, 4, &analyzerTask_, 1) != pdPASS)
return false;
// The ACK edges can be less than 1 us apart. MCPWM capture delivers all
// channels through one group ISR and can overwrite an earlier channel
// timestamp before that ISR reaches it. During DRIVER test dedicate core 0
// to direct GPIO sampling; PWM itself remains fully hardware-generated.
captureHz_ = getCpuFrequencyMhz() * 1000000UL;
if (!captureHz_ || captureHz_ % frequencyHz) return false;
captureFrequencyHz_ = frequencyHz;
capturePulseNs_ = pulseNs;
captureTxLightOn_ = activeTxLightOn;
txPulseLightOn_ = activeTxLightOn;
pollPeriodCycles_ = captureHz_ / frequencyHz;
pollWindowBeforeCycles_ = captureHz_ / 200000U; // 5 us
const uint64_t periodNs = 1000000000ULL / frequencyHz;
uint64_t windowNs = pulseNs + 50000ULL;
const uint64_t maximumWindowNs = periodNs * 3ULL / 4ULL;
if (windowNs > maximumWindowNs) windowNs = maximumWindowNs;
pollWindowAfterCycles_ = static_cast<uint32_t>(
windowNs * captureHz_ / 1000000000ULL);
const uint8_t activeTxRaw = activeTxLightOn ? TX_LIGHT_ON_GPIO_LEVEL :
TX_LIGHT_OFF_GPIO_LEVEL;
pollTxStartRawHigh_ = activeTxRaw == HIGH;
rxActiveRawHigh_ =
((RX_LIGHT_ON_GPIO_LEVEL == HIGH) == activeRxLightOn);
ackStartMaxTicks_ = nsToTicks(DRIVER_ACK_START_MAX_NS);
faultLongTicks_ = nsToTicks(DRIVER_FAULT_MIN_NS);
stuckTicks_ = nsToTicks(DRIVER_RX_STUCK_MIN_NS);
testTicks_ = static_cast<uint64_t>(captureHz_) * testTimeMs / 1000ULL;
const uint32_t requestedSubsamples = testTimeMs < 1000U ?
DRIVER_SHORT_SAMPLE_PROGRESS_STEPS :
(testTimeMs + DRIVER_PROGRESS_INTERVAL_MS - 1U) /
DRIVER_PROGRESS_INTERVAL_MS;
subsampleCount_ = static_cast<uint8_t>(
requestedSubsamples > UINT8_MAX ? UINT8_MAX : requestedSubsamples);
subsampleTicks_ = testTicks_ / subsampleCount_;
if (!pollPeriodCycles_ || !pollWindowAfterCycles_ ||
!ackStartMaxTicks_ || !faultLongTicks_ || !stuckTicks_ ||
!testTicks_ || !subsampleTicks_)
return false;
clearCapture();
stats_.reset();
publishStats();
pendingCount_ = 0;
response_ = {};
measurementStartTick_ = deadlineTick_ = 0;
pointOriginTick_ = lastEventTick_ = lastActiveTxTick_ = 0;
// Skip two complete periods after the polling task synchronizes with TX.
settleCycles_ = DRIVER_CAPTURE_SYNC_CYCLES;
Log::printf("DRIVER", "capture=GPIO-%luMHz sync-periods=%u ACK-timeout=%luns",
static_cast<unsigned long>(captureHz_ / 1000000UL),
static_cast<unsigned>(settleCycles_),
static_cast<unsigned long>(DRIVER_ACK_START_MAX_NS));
const uint64_t periodUs =
(1000000ULL + frequencyHz - 1ULL) / frequencyHz;
settlingTimeoutUs_ = periodUs *
(static_cast<uint64_t>(DRIVER_CAPTURE_SYNC_CYCLES) + 2ULL) + 1000ULL;
settlingDeadlineUs_ = 0;
settledCycles_ = 0;
completedSubsamples_ = 0;
measurementClosed_ = false;
havePointOrigin_ = false;
haveLastActiveTx_ = false;
rxActive_ = (gpio_get_level(static_cast<gpio_num_t>(GPIO_RX)) != 0) ==
rxActiveRawHigh_;
currentStep_ = 0;
traceWrite_ = traceCount_ = 0;
__atomic_store_n(&progressUpdatePending_, false, __ATOMIC_RELEASE);
state_ = DriverState::SETTLING;
return armCapture();
}
bool DriverTest::armCapture() {
// Never wait for USB/Serial here: a disconnected or slow host must not
// delay a subsample or consume the test's global timeout.
if (!__atomic_load_n(&core0WdtDisabled_, __ATOMIC_ACQUIRE)) {
TaskHandle_t idle0 = xTaskGetIdleTaskHandleForCore(0);
const bool watched = idle0 && esp_task_wdt_status(idle0) == ESP_OK;
const bool disabled = watched && disableCore0WDT();
__atomic_store_n(&core0WdtDisabled_, disabled, __ATOMIC_RELEASE);
// If IDLE0 is not watched there is nothing to remove or restore.
}
__atomic_store_n(&captureReady_, false, __ATOMIC_RELEASE);
__atomic_store_n(&captureActive_, true, __ATOMIC_RELEASE);
xTaskNotifyGive(pollTask_);
const uint32_t readyDeadline = millis() + 25U;
while (!__atomic_load_n(&captureReady_, __ATOMIC_ACQUIRE) &&
static_cast<int32_t>(millis() - readyDeadline) < 0) delay(0);
if (!__atomic_load_n(&captureReady_, __ATOMIC_ACQUIRE)) {
requestCaptureStop();
waitCaptureStopped(25U);
state_ = DriverState::IDLE;
return false;
}
settlingDeadlineUs_ = static_cast<uint64_t>(esp_timer_get_time()) +
settlingTimeoutUs_;
xTaskNotifyGive(analyzerTask_);
return true;
}
bool DriverTest::resumeSubsample() {
if (state_ != DriverState::SUBSAMPLE_DONE) return false;
if (!waitCaptureStopped(25U)) {
fail(FailReason::DATA_LOSS, lastEventTick_);
return false;
}
clearCapture();
pendingCount_ = 0;
response_ = {};
measurementStartTick_ = deadlineTick_ = 0;
lastActiveTxTick_ = 0;
settlingDeadlineUs_ = 0;
settledCycles_ = 0;
measurementClosed_ = false;
haveLastActiveTx_ = false;
rxActive_ = (gpio_get_level(static_cast<gpio_num_t>(GPIO_RX)) != 0) ==
rxActiveRawHigh_;
state_ = DriverState::SETTLING;
if (armCapture()) return true;
fail(FailReason::DATA_LOSS, lastEventTick_);
return false;
}
void DriverTest::pollTaskEntry(void *context) {
static_cast<DriverTest *>(context)->pollTaskLoop();
}
void IRAM_ATTR DriverTest::pollTaskLoop() {
constexpr uint32_t PIN_MASK = (1UL << GPIO_PWM) | (1UL << GPIO_RX);
for (;;) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
uint32_t levels = GPIO.in & PIN_MASK;
uint32_t nextStart = 0;
uint32_t windowEnd = 0;
uint32_t lastTxStart = 0;
RawEvent hotEvents[32] = {};
uint8_t hotCount = 0;
bool sawTxStart = false;
bool critical = false;
bool allInterruptsMasked = false;
uint32_t interruptState = 0;
auto sampleOnce = [&]() {
const uint32_t current = GPIO.in & PIN_MASK;
if (current == levels) return;
const uint32_t now = esp_cpu_get_cycle_count();
const uint32_t changed = current ^ levels;
if ((changed & (1UL << GPIO_PWM)) && hotCount < 32U)
hotEvents[hotCount++] = {now,
(current & (1UL << GPIO_PWM)) != 0U, Source::TX};
if ((changed & (1UL << GPIO_RX)) && hotCount < 32U)
hotEvents[hotCount++] = {now,
(current & (1UL << GPIO_RX)) != 0U, Source::RX};
if ((changed & (1UL << GPIO_PWM)) &&
((current & (1UL << GPIO_PWM)) != 0U) == pollTxStartRawHigh_) {
lastTxStart = now;
sawTxStart = true;
}
levels = current;
};
auto flushHot = [&]() {
for (uint8_t i = 0; i < hotCount; ++i)
recordRaw(hotEvents[i].tick, hotEvents[i].rising,
hotEvents[i].source);
hotCount = 0;
};
portENTER_CRITICAL(&pollMux_);
critical = true;
__atomic_store_n(&captureReady_, true, __ATOMIC_RELEASE);
while (__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE) && !sawTxStart)
for (uint8_t i = 0; i < 16U; ++i) sampleOnce();
if (sawTxStart) windowEnd = lastTxStart + pollWindowAfterCycles_;
const bool synchronized = sawTxStart;
if (synchronized) {
interruptState = maskAllInterrupts();
allInterruptsMasked = true;
}
while (__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE) && synchronized) {
while (__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE) &&
static_cast<int32_t>(esp_cpu_get_cycle_count() - windowEnd) < 0)
for (uint8_t i = 0; i < 16U; ++i) sampleOnce();
restoreInterrupts(interruptState);
allInterruptsMasked = false;
portEXIT_CRITICAL(&pollMux_);
critical = false;
flushHot();
// This marker is written only after every TX/RX edge from the completed
// sampling window. The analyzer may now safely decide that an ACK was
// absent without racing the producer that writes those edges.
recordRaw(esp_cpu_get_cycle_count(), false, Source::WINDOW_END);
if (!__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE)) break;
nextStart = lastTxStart + pollPeriodCycles_;
sawTxStart = false;
uint32_t outsideSpins = 0;
while (__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE) &&
static_cast<int32_t>(esp_cpu_get_cycle_count() -
(nextStart - pollWindowBeforeCycles_)) < 0) {
for (uint8_t i = 0; i < 16U; ++i) sampleOnce();
if (++outsideSpins >= 256U) {
outsideSpins = 0;
taskYIELD();
}
}
if (!__atomic_load_n(&captureActive_, __ATOMIC_ACQUIRE)) break;
portENTER_CRITICAL(&pollMux_);
critical = true;
interruptState = maskAllInterrupts();
allInterruptsMasked = true;
windowEnd = nextStart + pollWindowAfterCycles_;
}
if (allInterruptsMasked) restoreInterrupts(interruptState);
if (critical) portEXIT_CRITICAL(&pollMux_);
flushHot();
if (__atomic_exchange_n(&core0WdtDisabled_, false,
__ATOMIC_ACQ_REL)) enableCore0WDT();
__atomic_store_n(&captureReady_, false, __ATOMIC_RELEASE);
}
}
void DriverTest::analyzerTaskEntry(void *context) {
static_cast<DriverTest *>(context)->analyzerTaskLoop();
}
void DriverTest::analyzerTaskLoop() {
TimedEvent events[64] = {};
for (;;) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
while (state_ == DriverState::SETTLING || state_ == DriverState::RUNNING) {
const size_t count = readRaw(events, 64, pdMS_TO_TICKS(1));
for (size_t i = 0; i < count &&
(state_ == DriverState::SETTLING || state_ == DriverState::RUNNING);
++i) processEvent(events[i]);
const uint32_t dropped = takeDropped();
if (dropped) {
stats_.droppedItems += dropped;
fail(FailReason::DATA_LOSS, lastEventTick_);
}
if (!count && state_ == DriverState::SETTLING &&
static_cast<uint64_t>(esp_timer_get_time()) >=
settlingDeadlineUs_) {
fail(FailReason::ACK_MISSING, lastEventTick_);
}
}
}
}
void DriverTest::processEvent(const TimedEvent &event) {
lastEventTick_ = event.tick;
if (event.source == Source::WINDOW_END) {
if (state_ == DriverState::RUNNING) {
expirePending(event.tick);
if (state_ == DriverState::RUNNING) completeIfPossible(event.tick);
}
return;
}
if (!havePointOrigin_) {
pointOriginTick_ = event.tick;
havePointOrigin_ = true;
}
rememberTrace(event);
if (state_ == DriverState::SETTLING) processSettling(event);
else if (state_ == DriverState::RUNNING) processRunning(event);
}
void DriverTest::processSettling(const TimedEvent &event) {
if (event.source == Source::RX) {
rxActive_ = event.rising == rxActiveRawHigh_;
return;
}
const uint8_t rawLevel = event.rising ? HIGH : LOW;
const bool lightOn = rawLevel == TX_LIGHT_ON_GPIO_LEVEL;
if (lightOn != txPulseLightOn_) return;
if (settledCycles_ < settleCycles_) {
++settledCycles_;
return;
}
if (rxActive_) {
fail(FailReason::DRIVER_FAULT, event.tick);
return;
}
state_ = DriverState::RUNNING;
measurementStartTick_ = event.tick;
const uint64_t measuredBefore =
static_cast<uint64_t>(completedSubsamples_) * subsampleTicks_;
const uint64_t thisSubsampleTicks =
completedSubsamples_ + 1U == subsampleCount_ ?
testTicks_ - measuredBefore : subsampleTicks_;
deadlineTick_ = event.tick + thisSubsampleTicks;
processTx(event, lightOn);
}
void DriverTest::processRunning(const TimedEvent &event) {
if (response_.active && event.tick - response_.startTick >= stuckTicks_) {
const uint64_t delay = response_.associated ?
response_.startTick - response_.tx.tick : 0;
const uint64_t trigger = haveLastActiveTx_ &&
response_.startTick >= lastActiveTxTick_ ?
response_.startTick - lastActiveTxTick_ : delay;
fail(FailReason::DRIVER_FAULT, event.tick, delay,
event.tick - response_.startTick, trigger);
return;
}
if (event.source == Source::TX) {
expirePending(event.tick);
if (state_ != DriverState::RUNNING) return;
const uint8_t rawLevel = event.rising ? HIGH : LOW;
const bool lightOn = rawLevel == TX_LIGHT_ON_GPIO_LEVEL;
if (event.tick < deadlineTick_) processTx(event, lightOn);
else measurementClosed_ = true;
} else {
// A delayed fault indication can start after the normal ACK deadline.
// Measure the RX pulse before expiring its possible causal TX edge.
processRx(event, event.rising == rxActiveRawHigh_);
if (state_ != DriverState::RUNNING) return;
if (!response_.active) expirePending(event.tick);
}
if (state_ != DriverState::RUNNING) return;
completeIfPossible(event.tick);
}
bool DriverTest::addPending(uint64_t tick, bool lightOn) {
if (pendingCount_ >= MAX_PENDING) {
fail(FailReason::DATA_LOSS, tick);
return false;
}
pending_[pendingCount_++] = {tick, lightOn};
return true;
}
void DriverTest::processTx(const TimedEvent &event, bool lightOn) {
if (!addPending(event.tick, lightOn)) return;
if (lightOn == txPulseLightOn_) {
lastActiveTxTick_ = event.tick;
haveLastActiveTx_ = true;
}
++stats_.inputEdges;
}
int8_t DriverTest::matchingPending(uint64_t rxTick) const {
for (uint8_t i = 0; i < pendingCount_; ++i)
if (rxTick >= pending_[i].tick &&
rxTick - pending_[i].tick <= ackStartMaxTicks_)
return static_cast<int8_t>(i);
return -1;
}
void DriverTest::removePending(uint8_t index) {
if (index >= pendingCount_) return;
for (uint8_t i = index + 1U; i < pendingCount_; ++i)
pending_[i - 1U] = pending_[i];
--pendingCount_;
}
void DriverTest::processRx(const TimedEvent &event, bool activeNow) {
rxActive_ = activeNow;
if (activeNow) {
if (response_.active) {
fail(FailReason::DATA_LOSS, event.tick);
return;
}
response_ = {};
response_.active = true;
response_.startTick = event.tick;
const int8_t index = matchingPending(event.tick);
if (index >= 0) {
response_.associated = true;
response_.tx = pending_[index];
removePending(static_cast<uint8_t>(index));
} else ++stats_.unexpectedResponses;
return;
}
if (!response_.active) return;
const uint64_t width = event.tick - response_.startTick;
if (!response_.associated) {
const uint64_t trigger = haveLastActiveTx_ &&
response_.startTick >= lastActiveTxTick_ ?
response_.startTick - lastActiveTxTick_ : 0;
response_ = {};
fail(FailReason::DRIVER_FAULT, event.tick, trigger, width, trigger);
return;
}
const uint64_t guard = mergeGuardTicks();
for (uint8_t i = 0; i < pendingCount_; ++i) {
if (pending_[i].tick > response_.startTick &&
event.tick - pending_[i].tick >= guard) {
const uint64_t delay = response_.startTick - response_.tx.tick;
const uint64_t trigger = event.tick - pending_[i].tick;
response_ = {};
fail(FailReason::ACK_MERGED, event.tick, delay, width, trigger);
return;
}
}
if (width >= faultLongTicks_) {
const uint64_t delay = response_.startTick - response_.tx.tick;
const uint64_t trigger = haveLastActiveTx_ &&
response_.startTick >= lastActiveTxTick_ ?
response_.startTick - lastActiveTxTick_ : delay;
response_ = {};
fail(FailReason::DRIVER_FAULT, event.tick, delay, width, trigger);
return;
}
const uint64_t delay = response_.startTick - response_.tx.tick;
const bool lightOn = response_.tx.lightOn;
response_ = {};
acceptAcknowledgement(delay, width, lightOn);
}
uint64_t DriverTest::mergeGuardTicks() const {
uint32_t observedMax = stats_.maxDelayTicks;
const uint64_t baseline = observedMax ? observedMax :
nsToTicks(DRIVER_ACK_DELAY_NS + 500U);
return baseline + nsToTicks(DRIVER_ACK_MERGE_MARGIN_NS);
}
void DriverTest::acceptAcknowledgement(uint64_t delay, uint64_t width,
bool lightOn) {
const uint32_t delay32 = delay > UINT32_MAX ? UINT32_MAX :
static_cast<uint32_t>(delay);
const uint32_t width32 = width > UINT32_MAX ? UINT32_MAX :
static_cast<uint32_t>(width);
stats_.lastDelayTicks = delay32;
stats_.lastResponseTicks = width32;
++stats_.responses;
if (delay32 < stats_.minDelayTicks) stats_.minDelayTicks = delay32;
if (delay32 > stats_.maxDelayTicks) stats_.maxDelayTicks = delay32;
if (width32 < stats_.minResponseTicks) stats_.minResponseTicks = width32;
if (width32 > stats_.maxResponseTicks) stats_.maxResponseTicks = width32;
DriverEdgeStats &edge = lightOn ? stats_.turnOn : stats_.turnOff;
++edge.responses;
edge.delaySumTicks += delay32;
edge.responseSumTicks += width32;
if (delay32 < edge.minDelayTicks) edge.minDelayTicks = delay32;
if (delay32 > edge.maxDelayTicks) edge.maxDelayTicks = delay32;
if (width32 < edge.minResponseTicks) edge.minResponseTicks = width32;
if (width32 > edge.maxResponseTicks) edge.maxResponseTicks = width32;
publishStats();
}
void DriverTest::expirePending(uint64_t now) {
for (uint8_t i = 0; i < pendingCount_; ++i) {
if (now < pending_[i].tick + ackStartMaxTicks_) continue;
// The failure belongs to the ACK deadline itself. A later TX edge or the
// end-of-window marker is only the safe moment when absence is confirmed.
fail(FailReason::ACK_MISSING,
pending_[i].tick + ackStartMaxTicks_, ackStartMaxTicks_);
return;
}
}
void DriverTest::completeIfPossible(uint64_t now) {
if (state_ != DriverState::RUNNING) return;
if (!measurementClosed_ && now >= deadlineTick_) measurementClosed_ = true;
if (!measurementClosed_ || response_.active || pendingCount_) return;
if (!stats_.turnOn.responses || !stats_.turnOff.responses) {
fail(FailReason::ACK_MISSING, now);
return;
}
++completedSubsamples_;
currentStep_ = completedSubsamples_;
publishStats();
requestCaptureStop();
if (!waitCaptureStopped(25U)) {
fail(FailReason::DATA_LOSS, lastEventTick_);
return;
}
if (completedSubsamples_ >= subsampleCount_) {
__atomic_store_n(&progressUpdatePending_, false, __ATOMIC_RELEASE);
state_ = DriverState::PASS;
} else {
__atomic_store_n(&progressUpdatePending_, true, __ATOMIC_RELEASE);
state_ = DriverState::SUBSAMPLE_DONE;
}
}
void DriverTest::fail(FailReason reason, uint64_t tick, uint64_t delay,
uint64_t pulseWidth, uint64_t triggerAfterTx) {
if (state_ == DriverState::FAIL || state_ == DriverState::PASS) return;
if (stats_.reason == FailReason::NONE) {
stats_.reason = reason;
if (tick && havePointOrigin_ && tick >= pointOriginTick_)
stats_.errorElapsedTicks = tick - pointOriginTick_;
const uint64_t trigger = triggerAfterTx ? triggerAfterTx : delay;
if (trigger) {
stats_.errorTriggerTicks = trigger > UINT32_MAX ? UINT32_MAX :
static_cast<uint32_t>(trigger);
stats_.errorTriggerValid = true;
}
if (delay) {
stats_.errorDelayTicks = delay > UINT32_MAX ? UINT32_MAX :
static_cast<uint32_t>(delay);
stats_.errorDelayValid = true;
}
if (pulseWidth) {
stats_.errorPulseTicks = pulseWidth > UINT32_MAX ? UINT32_MAX :
static_cast<uint32_t>(pulseWidth);
stats_.errorPulseValid = true;
}
}
publishStats();
__atomic_store_n(&progressUpdatePending_, false, __ATOMIC_RELEASE);
requestCaptureStop();
state_ = DriverState::FAIL;
}
void DriverTest::publishStats() {
portENTER_CRITICAL(&statsMux_);
publishedStats_ = stats_;
portEXIT_CRITICAL(&statsMux_);
}
void DriverTest::forceFail(FailReason reason) {
if (state_ == DriverState::SETTLING || state_ == DriverState::RUNNING ||
state_ == DriverState::SUBSAMPLE_DONE)
fail(reason, lastEventTick_);
}
void DriverTest::abort() {
if (state_ == DriverState::SETTLING || state_ == DriverState::RUNNING ||
state_ == DriverState::SUBSAMPLE_DONE)
fail(FailReason::ABORTED, lastEventTick_);
else {
requestCaptureStop();
state_ = DriverState::IDLE;
}
}
bool DriverTest::takeProgressUpdate() {
return __atomic_exchange_n(&progressUpdatePending_, false,
__ATOMIC_ACQ_REL);
}
void DriverTest::requestCaptureStop() {
__atomic_store_n(&captureActive_, false, __ATOMIC_RELEASE);
}
bool DriverTest::waitCaptureStopped(uint32_t timeoutMs) {
const uint32_t deadline = millis() + timeoutMs;
while (__atomic_load_n(&captureReady_, __ATOMIC_ACQUIRE) &&
static_cast<int32_t>(millis() - deadline) < 0) delay(0);
if (__atomic_load_n(&captureReady_, __ATOMIC_ACQUIRE)) return false;
if (__atomic_exchange_n(&core0WdtDisabled_, false,
__ATOMIC_ACQ_REL)) enableCore0WDT();
return true;
}
void DriverTest::clearCapture() {
const uint16_t write = __atomic_load_n(&ringWrite_, __ATOMIC_ACQUIRE);
__atomic_store_n(&ringRead_, write, __ATOMIC_RELEASE);
__atomic_store_n(&droppedItems_, 0U, __ATOMIC_RELEASE);
haveRawTick_ = false;
lastRawTick_ = 0;
tickEpoch_ = 0;
}
void IRAM_ATTR DriverTest::recordRaw(uint32_t tick, bool rising,
Source source) {
const uint16_t write = ringWrite_;
const uint16_t next = static_cast<uint16_t>(
(write + 1U) & (RING_CAPACITY - 1U));
if (next == ringRead_) {
++droppedItems_;
return;
}
ring_[write] = {tick, rising, source};
asm volatile("memw" ::: "memory");
ringWrite_ = next;
}
size_t DriverTest::readRaw(TimedEvent *events, size_t capacity,
TickType_t waitTicks) {
if (!events || !capacity) return 0;
uint16_t read = __atomic_load_n(&ringRead_, __ATOMIC_RELAXED);
if (read == __atomic_load_n(&ringWrite_, __ATOMIC_ACQUIRE) && waitTicks) {
vTaskDelay(waitTicks);
read = __atomic_load_n(&ringRead_, __ATOMIC_RELAXED);
}
const uint16_t write = __atomic_load_n(&ringWrite_, __ATOMIC_ACQUIRE);
size_t count = 0;
while (read != write && count < capacity) {
const RawEvent raw = ring_[read];
read = static_cast<uint16_t>((read + 1U) & (RING_CAPACITY - 1U));
if (haveRawTick_ && raw.tick < lastRawTick_ &&
lastRawTick_ - raw.tick > 0x80000000UL) tickEpoch_ += 1ULL << 32U;
lastRawTick_ = raw.tick;
haveRawTick_ = true;
events[count++] = {tickEpoch_ + raw.tick, raw.rising, raw.source};
}
__atomic_store_n(&ringRead_, read, __ATOMIC_RELEASE);
return count;
}
uint32_t DriverTest::takeDropped() {
return __atomic_exchange_n(&droppedItems_, 0U, __ATOMIC_ACQ_REL);
}
void DriverTest::rememberTrace(const TimedEvent &event) {
trace_[traceWrite_] = {event.tick, static_cast<uint8_t>(event.source),
static_cast<uint8_t>(event.rising), static_cast<uint8_t>(state_),
pendingCount_};
traceWrite_ = static_cast<uint8_t>((traceWrite_ + 1U) % TRACE_CAPACITY);
if (traceCount_ < TRACE_CAPACITY) ++traceCount_;
}
void DriverTest::printSummary() const {
auto printEdge = [&](const char *name, const DriverEdgeStats &edge) {
if (!edge.responses) {
Log::printf("DRIVER", "%s ACK=0", name);
return;
}
Log::printf("DRIVER",
"%s ACK=%lu D=%lluns/%lluns/%lluns P=%lluns/%lluns/%lluns",
name, static_cast<unsigned long>(edge.responses),
static_cast<unsigned long long>(ticksToNs(edge.minDelayTicks)),
static_cast<unsigned long long>(ticksToNs(
edge.delaySumTicks / edge.responses)),
static_cast<unsigned long long>(ticksToNs(edge.maxDelayTicks)),
static_cast<unsigned long long>(ticksToNs(edge.minResponseTicks)),
static_cast<unsigned long long>(ticksToNs(
edge.responseSumTicks / edge.responses)),
static_cast<unsigned long long>(ticksToNs(edge.maxResponseTicks)));
};
Log::printf("DRIVER", "TX edges=%lu responses=%lu dropped=%lu unexpected=%lu result=%s",
static_cast<unsigned long>(publishedStats_.inputEdges),
static_cast<unsigned long>(publishedStats_.responses),
static_cast<unsigned long>(publishedStats_.droppedItems),
static_cast<unsigned long>(publishedStats_.unexpectedResponses),
failName(publishedStats_.reason));
if (publishedStats_.reason != FailReason::NONE) {
auto formatOptional = [&](bool valid, uint32_t ticks,
char *out, size_t size) {
if (!valid) snprintf(out, size, "---");
else snprintf(out, size, "%lluns",
static_cast<unsigned long long>(ticksToNs(ticks)));
};
char trigger[24], pulse[24];
formatOptional(publishedStats_.errorTriggerValid,
publishedStats_.errorTriggerTicks, trigger, sizeof(trigger));
formatOptional(publishedStats_.errorPulseValid,
publishedStats_.errorPulseTicks, pulse, sizeof(pulse));
if (publishedStats_.errorPulseValid)
Log::printf("DRIVER", "error timing: T=%s P=%s", trigger, pulse);
else Log::printf("DRIVER", "error timing: T=%s", trigger);
}
printEdge("ON", publishedStats_.turnOn);
printEdge("OFF", publishedStats_.turnOff);
}
void DriverTest::printTrace() const {
if (!traceCount_) return;
const uint8_t first = static_cast<uint8_t>(
(traceWrite_ + TRACE_CAPACITY - traceCount_) % TRACE_CAPACITY);
const uint64_t origin = trace_[first].tick;
Log::printf("DRIVER", "RAM trace: %u events, tick=%luHz", traceCount_,
static_cast<unsigned long>(captureHz_));
for (uint8_t i = 0; i < traceCount_; ++i) {
const TraceEvent &event = trace_[(first + i) % TRACE_CAPACITY];
Log::printf("DRIVER", "E%02u +%lluns %s/%s state=%u pending=%u", i,
static_cast<unsigned long long>(ticksToNs(event.tick - origin)),
event.source == static_cast<uint8_t>(Source::TX) ? "TX" : "RX",
event.rising ? "rise" : "fall", event.state, event.pending);
}
}

View File

@@ -0,0 +1,180 @@
#pragma once
#include <Arduino.h>
#include "Receiver.h"
enum class DriverState : uint8_t {
IDLE, SETTLING, RUNNING, SUBSAMPLE_DONE, PASS, FAIL
};
struct DriverEdgeStats {
uint32_t responses;
uint32_t minDelayTicks;
uint32_t maxDelayTicks;
uint64_t delaySumTicks;
uint32_t minResponseTicks;
uint32_t maxResponseTicks;
uint64_t responseSumTicks;
void reset();
};
struct DriverStats {
uint32_t inputEdges;
uint32_t responses;
uint32_t minDelayTicks;
uint32_t maxDelayTicks;
uint32_t minResponseTicks;
uint32_t maxResponseTicks;
uint32_t lastDelayTicks;
uint32_t lastResponseTicks;
uint32_t droppedItems;
uint32_t unexpectedResponses;
uint64_t errorElapsedTicks;
uint32_t errorTriggerTicks;
uint32_t errorDelayTicks;
uint32_t errorPulseTicks;
bool errorTriggerValid;
bool errorDelayValid;
bool errorPulseValid;
DriverEdgeStats turnOn;
DriverEdgeStats turnOff;
FailReason reason;
void reset();
};
class DriverTest {
public:
explicit DriverTest(PulseReceiver &receiver) : receiver_(receiver) {}
bool start(uint32_t frequencyHz, uint32_t pulseNs, float tolerancePct,
uint32_t testTimeMs, uint8_t settleCycles,
bool activeTxLightOn, bool activeRxLightOn);
DriverState update() const { return state_; }
void abort();
void forceFail(FailReason reason);
bool resumeSubsample();
bool takeProgressUpdate();
void printSummary() const;
void printTrace() const;
uint8_t progressStep() const { return currentStep_; }
uint8_t progressSteps() const { return subsampleCount_; }
uint32_t tickHz() const { return captureHz_; }
const DriverStats &stats() const { return publishedStats_; }
private:
enum class Source : uint8_t { TX, RX, WINDOW_END };
struct RawEvent { uint32_t tick; bool rising; Source source; };
struct TimedEvent { uint64_t tick; bool rising; Source source; };
struct PendingTx {
uint64_t tick;
bool lightOn;
};
struct Response {
bool active;
bool associated;
uint64_t startTick;
PendingTx tx;
};
struct TraceEvent {
uint64_t tick;
uint8_t source;
uint8_t rising;
uint8_t state;
uint8_t pending;
};
static void analyzerTaskEntry(void *context);
static void pollTaskEntry(void *context);
void analyzerTaskLoop();
void IRAM_ATTR pollTaskLoop();
void processEvent(const TimedEvent &event);
void processSettling(const TimedEvent &event);
void processRunning(const TimedEvent &event);
void processTx(const TimedEvent &event, bool lightOn);
void processRx(const TimedEvent &event, bool activeNow);
void expirePending(uint64_t now);
void completeIfPossible(uint64_t now);
bool addPending(uint64_t tick, bool lightOn);
int8_t matchingPending(uint64_t rxTick) const;
void removePending(uint8_t index);
uint64_t mergeGuardTicks() const;
void acceptAcknowledgement(uint64_t delay, uint64_t width, bool lightOn);
void fail(FailReason reason, uint64_t tick = 0, uint64_t delay = 0,
uint64_t pulseWidth = 0, uint64_t triggerAfterTx = 0);
void publishStats();
void rememberTrace(const TimedEvent &event);
bool armCapture();
void requestCaptureStop();
bool waitCaptureStopped(uint32_t timeoutMs);
void clearCapture();
void recordRaw(uint32_t tick, bool rising, Source source);
size_t readRaw(TimedEvent *events, size_t capacity, TickType_t waitTicks);
uint32_t takeDropped();
uint64_t nsToTicks(uint32_t ns) const;
uint64_t ticksToNs(uint64_t ticks) const;
static constexpr uint8_t MAX_PENDING = 8;
static constexpr uint16_t RING_CAPACITY = 2048;
static constexpr uint8_t TRACE_CAPACITY = 32;
static_assert((RING_CAPACITY & (RING_CAPACITY - 1U)) == 0,
"driver ring capacity must be a power of two");
PulseReceiver &receiver_;
TaskHandle_t analyzerTask_ = nullptr;
TaskHandle_t pollTask_ = nullptr;
volatile DriverState state_ = DriverState::IDLE;
DriverStats stats_ = {};
DriverStats publishedStats_ = {};
mutable portMUX_TYPE statsMux_ = portMUX_INITIALIZER_UNLOCKED;
RawEvent ring_[RING_CAPACITY] = {};
volatile uint16_t ringWrite_ = 0;
volatile uint16_t ringRead_ = 0;
volatile uint32_t droppedItems_ = 0;
volatile bool captureActive_ = false;
volatile bool captureReady_ = false;
volatile bool core0WdtDisabled_ = false;
uint32_t captureHz_ = 0;
uint32_t captureFrequencyHz_ = 0;
uint32_t capturePulseNs_ = 0;
uint32_t pollPeriodCycles_ = 0;
uint32_t pollWindowBeforeCycles_ = 0;
uint32_t pollWindowAfterCycles_ = 0;
bool pollTxStartRawHigh_ = false;
bool captureTxLightOn_ = true;
portMUX_TYPE pollMux_ = portMUX_INITIALIZER_UNLOCKED;
PendingTx pending_[MAX_PENDING] = {};
uint8_t pendingCount_ = 0;
Response response_ = {};
uint64_t ackStartMaxTicks_ = 0;
uint64_t faultLongTicks_ = 0;
uint64_t stuckTicks_ = 0;
uint64_t testTicks_ = 0;
uint64_t subsampleTicks_ = 0;
uint64_t settlingTimeoutUs_ = 0;
uint64_t settlingDeadlineUs_ = 0;
uint64_t measurementStartTick_ = 0;
uint64_t deadlineTick_ = 0;
uint64_t pointOriginTick_ = 0;
uint64_t lastEventTick_ = 0;
uint64_t lastActiveTxTick_ = 0;
uint8_t settleCycles_ = 0;
uint8_t settledCycles_ = 0;
uint8_t subsampleCount_ = DRIVER_SHORT_SAMPLE_PROGRESS_STEPS;
uint8_t completedSubsamples_ = 0;
bool rxActiveRawHigh_ = true;
bool txPulseLightOn_ = true;
bool rxActive_ = false;
bool measurementClosed_ = false;
bool havePointOrigin_ = false;
bool haveLastActiveTx_ = false;
bool haveRawTick_ = false;
uint32_t lastRawTick_ = 0;
uint64_t tickEpoch_ = 0;
volatile uint8_t currentStep_ = 0;
volatile bool progressUpdatePending_ = false;
TraceEvent trace_[TRACE_CAPACITY] = {};
uint8_t traceWrite_ = 0;
uint8_t traceCount_ = 0;
};

View File

@@ -7,7 +7,8 @@ namespace Log {
void event(const char *component, const char *message) { void event(const char *component, const char *message) {
if (!SERIAL_ACTION_LOG) return; if (!SERIAL_ACTION_LOG) return;
if (SERIAL_MINIMAL_LOG && strcmp(component, "INPUT") && strcmp(component, "UI") && if (SERIAL_MINIMAL_LOG && strcmp(component, "INPUT") && strcmp(component, "UI") &&
strcmp(component, "CONFIG") && strcmp(component, "RESULT") && strcmp(component, "ESP-NOW")) return; strcmp(component, "CONFIG") && strcmp(component, "RESULT") && strcmp(component, "CAPTURE") &&
strcmp(component, "DRIVER") && strcmp(component, "ESP-NOW")) return;
if (SERIAL_LOG_TIMESTAMPS) Serial.printf("[%10lu][%-8s] %s\n", millis(), component, message); if (SERIAL_LOG_TIMESTAMPS) Serial.printf("[%10lu][%-8s] %s\n", millis(), component, message);
else Serial.printf("[%-8s] %s\n", component, message); else Serial.printf("[%-8s] %s\n", component, message);
} }

View File

@@ -3,22 +3,22 @@
#include <string.h> #include <string.h>
bool Measurement::start(float hz, float duty, float tolerance, uint32_t timeMs, bool Measurement::start(float hz, float duty, float tolerance, uint32_t timeMs,
uint8_t settleCycles) { uint16_t averagingPeriods, uint8_t settleCycles,
bool activeRxLightOn) {
if (!task_ && xTaskCreate(taskEntry, "optical-rx", 4096, this, 4, &task_) != pdPASS) return false; if (!task_ && xTaskCreate(taskEntry, "optical-rx", 4096, this, 4, &task_) != pdPASS) return false;
expectedHz_ = static_cast<uint32_t>(hz + 0.5f); expectedHz_ = static_cast<uint32_t>(hz + 0.5f);
expectedDutyPct_ = duty; expectedDutyPct_ = duty;
tolerance = effectiveTolerancePct(tolerance); tolerance = effectiveTolerancePct(tolerance);
if (!expectedHz_ || !timeMs || if (!expectedHz_ || !timeMs || !averagingPeriods ||
!receiver_.start(expectedHz_, expectedDutyPct_)) return false; !receiver_.start(expectedHz_, expectedDutyPct_, activeRxLightOn)) return false;
if (!makePeriodLimits(expectedHz_, duty, tolerance, receiver_.tickHz(), limits_)) {
receiver_.stop(); return false;
}
settleCycles_ = settleCycles; settleLeft_ = settleCycles; settleCycles_ = settleCycles; settleLeft_ = settleCycles;
tolerancePct_ = tolerance;
stepTimeMs_ = (timeMs + MEASUREMENT_PROGRESS_STEPS - 1U) / MEASUREMENT_PROGRESS_STEPS; stepTimeMs_ = (timeMs + MEASUREMENT_PROGRESS_STEPS - 1U) / MEASUREMENT_PROGRESS_STEPS;
stepTicks_ = static_cast<uint64_t>(receiver_.tickHz()) * timeMs / stepTicks_ = static_cast<uint64_t>(receiver_.tickHz()) * timeMs /
(1000ULL * MEASUREMENT_PROGRESS_STEPS); (1000ULL * MEASUREMENT_PROGRESS_STEPS);
if (!stepTicks_) stepTicks_ = 1; if (!stepTicks_) stepTicks_ = 1;
currentStep_ = 0; currentStep_ = 0;
__atomic_store_n(&progressUpdatePending_, false, __ATOMIC_RELEASE);
stats_.reset(); stats_.reset();
publishStats(); publishStats();
measurementStartTick_ = deadlineTick_ = 0; startedMs_ = millis(); measurementStartTick_ = deadlineTick_ = 0; startedMs_ = millis();
@@ -43,22 +43,33 @@ void Measurement::taskLoop() {
void Measurement::fail(FailReason reason) { void Measurement::fail(FailReason reason) {
if (stats_.reason == FailReason::NONE) stats_.reason = reason; if (stats_.reason == FailReason::NONE) stats_.reason = reason;
__atomic_store_n(&progressUpdatePending_, false, __ATOMIC_RELEASE);
publishStats(); publishStats();
receiver_.stop(); state_ = MeasureState::FAIL; // App stops PWM first and only then disables capture. Disabling MCPWM from
// this RX task while input edges are still arriving can race its ISR.
state_ = MeasureState::FAIL;
} }
void Measurement::completeMeasurement() { void Measurement::completeMeasurement() {
receiver_.stop(); const uint32_t dropped = receiver_.takeDroppedItems();
stats_.droppedItems += receiver_.takeDroppedItems(); stats_.droppedItems += dropped;
if (receiver_.overflowed()) { fail(FailReason::GLITCH); return; } if (dropped) { fail(FailReason::DATA_LOSS); return; }
publishStats(); publishStats();
if (++currentStep_ < MEASUREMENT_PROGRESS_STEPS) { if (++currentStep_ < MEASUREMENT_PROGRESS_STEPS) {
state_ = MeasureState::STEP_READY; // Capture and validation continue while the main task draws OLED. Pausing
// here would overflow the edge queue at higher PWM frequencies; stopping
// MCPWM Capture can race an edge ISR. Publish a snapshot, then advance the
// edge-based window without interrupting the RX pipeline.
__atomic_store_n(&progressUpdatePending_, true, __ATOMIC_RELEASE);
measurementStartTick_ = deadlineTick_;
deadlineTick_ += stepTicks_;
measurementStartMs_ = lastPeriodMs_ = millis();
return; return;
} }
if (!stats_.periods) { if (!stats_.periods) {
fail(FailReason::DATA_LOSS); return; fail(FailReason::DATA_LOSS); return;
} }
__atomic_store_n(&progressUpdatePending_, false, __ATOMIC_RELEASE);
state_ = MeasureState::PASS; state_ = MeasureState::PASS;
} }
@@ -77,11 +88,12 @@ bool Measurement::statsSnapshot(StageStats &out) const {
MeasureState Measurement::processOnce() { MeasureState Measurement::processOnce() {
if (state_ != MeasureState::SETTLING && state_ != MeasureState::RUNNING) return state_; if (state_ != MeasureState::SETTLING && state_ != MeasureState::RUNNING) return state_;
if (receiver_.overflowed()) { fail(FailReason::GLITCH); return state_; }
bool receivedPeriod = false; bool receivedPeriod = false;
for (;;) { for (;;) {
const size_t periodCount = receiver_.readPeriods(periodBatch_, PERIOD_BATCH_SIZE, pdMS_TO_TICKS(2)); const size_t periodCount = receiver_.readPeriods(periodBatch_, PERIOD_BATCH_SIZE, pdMS_TO_TICKS(2));
stats_.droppedItems += receiver_.takeDroppedItems(); const uint32_t dropped = receiver_.takeDroppedItems();
stats_.droppedItems += dropped;
if (dropped) { fail(FailReason::DATA_LOSS); return state_; }
if (!periodCount) break; if (!periodCount) break;
receivedPeriod = true; receivedPeriod = true;
for (size_t periodIndex = 0; periodIndex < periodCount; ++periodIndex) { for (size_t periodIndex = 0; periodIndex < periodCount; ++periodIndex) {
@@ -96,31 +108,32 @@ MeasureState Measurement::processOnce() {
} }
continue; continue;
} }
const uint64_t endTick = period.startTick + period.periodTicks;
if (period.startTick < measurementStartTick_) continue; // leading incomplete period if (period.startTick < measurementStartTick_) continue; // leading incomplete period
if (endTick > deadlineTick_) { completeMeasurement(); return state_; } // trailing incomplete period while (period.startTick >= deadlineTick_) {
const FailReason r = evaluatePeriodFast(period, receiver_.tickHz(), limits_, 1, stats_); // Progress boundaries never discard a pulse. A complete period is
if (r != FailReason::NONE) { fail(r); return state_; } // assigned by its start edge, then validated exactly once. The nine
// intermediate boundaries only publish UI snapshots.
completeMeasurement();
if (state_ != MeasureState::RUNNING) return state_;
}
if (!period.periodTicks || !period.activeTicks || !period.activeTickHz) {
fail(FailReason::EXTRA_EDGE); return state_;
}
const FailReason result = evaluatePeriod(period, receiver_.tickHz(),
expectedHz_, expectedDutyPct_, tolerancePct_, currentStep_ + 1U, stats_);
if (result != FailReason::NONE) { fail(result); return state_; }
} }
} }
if (receivedPeriod && state_ == MeasureState::RUNNING) lastPeriodMs_ = millis(); if (receivedPeriod && state_ == MeasureState::RUNNING) lastPeriodMs_ = millis();
const uint64_t edgeBasedTimeout = const uint64_t edgeBasedTimeout =
static_cast<uint64_t>(PWM_SETTLE_CYCLES + NO_SIGNAL_TIMEOUT_PERIODS) * expectedPeriodMs_ + 20; static_cast<uint64_t>(PWM_SETTLE_CYCLES + NO_SIGNAL_TIMEOUT_PERIODS) * expectedPeriodMs_ + 20;
const uint64_t rmtBatchTimeout = const uint64_t settleTimeout = edgeBasedTimeout;
static_cast<uint64_t>(RMT_MIN_RECEIVE_SYMBOLS + NO_SIGNAL_TIMEOUT_PERIODS) * expectedPeriodMs_ + 20;
const uint64_t settleTimeout = edgeBasedTimeout > rmtBatchTimeout ? edgeBasedTimeout : rmtBatchTimeout;
if (state_ == MeasureState::SETTLING && millis() - startedMs_ > settleTimeout) fail(FailReason::NO_SIGNAL); if (state_ == MeasureState::SETTLING && millis() - startedMs_ > settleTimeout) fail(FailReason::NO_SIGNAL);
if (state_ == MeasureState::RUNNING && measurementStartTick_) { if (state_ == MeasureState::RUNNING && measurementStartTick_) {
const uint32_t now = millis(); const uint32_t now = millis();
// RMT reports a block only after its user buffer has filled. At 1 kHz the
// minimum 48-symbol C3 block contains roughly 48 PWM periods and therefore
// arrives much later than the old 8-period timeout. Do not call that
// normal batching delay a lost edge.
const uint32_t batchPeriods = receiver_.receiveChunkSymbols();
const uint32_t batchTimeoutMs = expectedPeriodMs_ * (batchPeriods + NO_SIGNAL_TIMEOUT_PERIODS) + 2U;
const uint32_t edgeTimeoutMs = expectedPeriodMs_ * NO_SIGNAL_TIMEOUT_PERIODS + 2U; const uint32_t edgeTimeoutMs = expectedPeriodMs_ * NO_SIGNAL_TIMEOUT_PERIODS + 2U;
const uint32_t receiveTimeoutMs = batchTimeoutMs > edgeTimeoutMs ? batchTimeoutMs : edgeTimeoutMs; if (now - measurementStartMs_ < stepTimeMs_ && now - lastPeriodMs_ > edgeTimeoutMs) {
if (now - measurementStartMs_ < stepTimeMs_ && now - lastPeriodMs_ > receiveTimeoutMs) {
fail(FailReason::LOST_EDGE); return state_; fail(FailReason::LOST_EDGE); return state_;
} }
if (now - measurementStartMs_ > stepTimeMs_ + expectedPeriodMs_ + 2) completeMeasurement(); if (now - measurementStartMs_ > stepTimeMs_ + expectedPeriodMs_ + 2) completeMeasurement();
@@ -130,21 +143,16 @@ MeasureState Measurement::processOnce() {
MeasureState Measurement::update() { return state_; } MeasureState Measurement::update() { return state_; }
bool Measurement::continueAfterDisplay() { bool Measurement::takeProgressUpdate() {
if (state_ != MeasureState::STEP_READY) return false; return __atomic_exchange_n(&progressUpdatePending_, false, __ATOMIC_ACQ_REL);
if (!receiver_.start(expectedHz_, expectedDutyPct_)) {
fail(FailReason::UNSUPPORTED);
return false;
}
settleLeft_ = settleCycles_;
measurementStartTick_ = deadlineTick_ = 0;
startedMs_ = millis(); measurementStartMs_ = lastPeriodMs_ = 0;
state_ = MeasureState::SETTLING;
xTaskNotifyGive(task_);
return true;
} }
void Measurement::abort() { void Measurement::abort() {
if (state_ == MeasureState::SETTLING || state_ == MeasureState::RUNNING || if (state_ == MeasureState::SETTLING || state_ == MeasureState::RUNNING)
state_ == MeasureState::STEP_READY) fail(FailReason::ABORTED); fail(FailReason::ABORTED);
}
void Measurement::forceFail(FailReason reason) {
if (state_ == MeasureState::SETTLING || state_ == MeasureState::RUNNING)
fail(reason);
} }

View File

@@ -1,16 +1,18 @@
#pragma once #pragma once
#include "Receiver.h" #include "Receiver.h"
enum class MeasureState : uint8_t { IDLE, SETTLING, RUNNING, STEP_READY, PASS, FAIL }; enum class MeasureState : uint8_t { IDLE, SETTLING, RUNNING, PASS, FAIL };
class Measurement { class Measurement {
public: public:
explicit Measurement(PulseReceiver &receiver) : receiver_(receiver) {} explicit Measurement(PulseReceiver &receiver) : receiver_(receiver) {}
bool start(float expectedHz, float expectedDuty, float tolerancePct, bool start(float expectedHz, float expectedDuty, float tolerancePct,
uint32_t testTimeMs, uint8_t settleCycles); uint32_t testTimeMs, uint16_t averagingPeriods,
uint8_t settleCycles, bool activeRxLightOn);
MeasureState update(); MeasureState update();
bool continueAfterDisplay(); bool takeProgressUpdate();
void abort(); void abort();
void forceFail(FailReason reason);
MeasureState state() const { return state_; } MeasureState state() const { return state_; }
FailReason reason() const { return stats_.reason; } FailReason reason() const { return stats_.reason; }
const StageStats &stats() const { return stats_; } const StageStats &stats() const { return stats_; }
@@ -29,13 +31,13 @@ class Measurement {
StageStats stats_ = {}; StageStats stats_ = {};
StageStats publishedStats_ = {}; StageStats publishedStats_ = {};
mutable portMUX_TYPE statsMux_ = portMUX_INITIALIZER_UNLOCKED; mutable portMUX_TYPE statsMux_ = portMUX_INITIALIZER_UNLOCKED;
PeriodLimits limits_ = {};
uint32_t expectedHz_ = 0; uint32_t expectedHz_ = 0;
float expectedDutyPct_ = 0.0f; float expectedDutyPct_ = 0.0f, tolerancePct_ = 0.0f;
uint8_t settleCycles_ = 0, settleLeft_ = 0; uint8_t settleCycles_ = 0, settleLeft_ = 0;
uint64_t measurementStartTick_ = 0, deadlineTick_ = 0, stepTicks_ = 0; uint64_t measurementStartTick_ = 0, deadlineTick_ = 0, stepTicks_ = 0;
uint32_t startedMs_ = 0, measurementStartMs_ = 0, lastPeriodMs_ = 0; uint32_t startedMs_ = 0, measurementStartMs_ = 0, lastPeriodMs_ = 0;
uint32_t stepTimeMs_ = 1, expectedPeriodMs_ = 1; uint32_t stepTimeMs_ = 1, expectedPeriodMs_ = 1;
volatile uint8_t currentStep_ = 0; volatile uint8_t currentStep_ = 0;
volatile bool progressUpdatePending_ = false;
PulsePeriod periodBatch_[PERIOD_BATCH_SIZE] = {}; PulsePeriod periodBatch_[PERIOD_BATCH_SIZE] = {};
}; };

View File

@@ -1,5 +1,4 @@
#include "App.h" #include "App.h"
App app; App app;
void setup() { app.begin(); } void setup() { app.begin(); }

View File

@@ -0,0 +1,74 @@
#include "OpticalCurrent.h"
#include "Config.h"
namespace {
struct AdcAccumulator {
uint64_t senseRaw = 0;
uint64_t vccRaw = 0;
uint64_t senseMillivolts = 0;
uint64_t vccMillivolts = 0;
uint32_t samples = 0;
uint32_t startedMs = 0;
};
AdcAccumulator accumulator;
void resetAccumulator(uint32_t now) {
accumulator = {};
accumulator.startedMs = now;
}
OpticalCurrentMeasurement finishMeasurement() {
const float senseAdcVoltage =
accumulator.senseMillivolts / (1000.0f * accumulator.samples);
const float vccAdcVoltage =
accumulator.vccMillivolts / (1000.0f * accumulator.samples);
const float senseVoltage = senseAdcVoltage * OPTICAL_DIVIDER_RATIO;
const float vccVoltage = vccAdcVoltage * OPTICAL_DIVIDER_RATIO;
const float resistorVoltage = vccVoltage - senseVoltage;
const float currentMa = resistorVoltage * 1000.0f / OPTICAL_SENSE_R;
return {
static_cast<uint16_t>(accumulator.senseRaw / accumulator.samples),
static_cast<uint16_t>(accumulator.vccRaw / accumulator.samples),
senseAdcVoltage, vccAdcVoltage, senseVoltage, vccVoltage,
currentMa > 0.0f ? currentMa : 0.0f
};
}
}
void optical_current_begin(void) {
analogReadResolution(12);
pinMode(GPIO_OPTICAL_CURRENT, INPUT);
pinMode(GPIO_OPTICAL_VCC, INPUT);
// Both 10k/10k dividers can present about 2.5 V to their ADC inputs.
analogSetPinAttenuation(GPIO_OPTICAL_CURRENT, ADC_11db);
analogSetPinAttenuation(GPIO_OPTICAL_VCC, ADC_11db);
resetAccumulator(0);
}
bool optical_current_poll(OpticalCurrentMeasurement &measurement) {
const uint32_t now = millis();
if (!accumulator.startedMs) resetAccumulator(now ? now : 1U);
accumulator.senseRaw += analogRead(GPIO_OPTICAL_CURRENT);
accumulator.senseMillivolts += analogReadMilliVolts(GPIO_OPTICAL_CURRENT);
accumulator.vccRaw += analogRead(GPIO_OPTICAL_VCC);
accumulator.vccMillivolts += analogReadMilliVolts(GPIO_OPTICAL_VCC);
++accumulator.samples;
if (now - accumulator.startedMs < OPTICAL_CURRENT_AVERAGING_MS) return false;
measurement = finishMeasurement();
resetAccumulator(now);
return true;
}
OpticalCurrentMeasurement optical_get_led_measurement(void) {
OpticalCurrentMeasurement measurement = {};
resetAccumulator(millis());
while (!optical_current_poll(measurement)) delay(1);
return measurement;
}
float optical_get_led_current_ma(void) {
return optical_get_led_measurement().currentMa;
}

View File

@@ -0,0 +1,18 @@
#pragma once
#include <stdint.h>
struct OpticalCurrentMeasurement {
uint16_t senseRaw;
uint16_t vccRaw;
float senseAdcVoltage;
float vccAdcVoltage;
float senseVoltage;
float vccVoltage;
float currentMa;
};
void optical_current_begin(void);
bool optical_current_poll(OpticalCurrentMeasurement &measurement);
OpticalCurrentMeasurement optical_get_led_measurement(void);
float optical_get_led_current_ma(void);

View File

@@ -2,7 +2,7 @@
#include "Core.h" #include "Core.h"
constexpr uint16_t PROTOCOL_MAGIC = 0x4F43; constexpr uint16_t PROTOCOL_MAGIC = 0x4F43;
constexpr uint8_t PROTOCOL_VERSION = 8; constexpr uint8_t PROTOCOL_VERSION = 12;
enum class MessageType : uint8_t { enum class MessageType : uint8_t {
DISCOVER, DISCOVER_ACK, PREPARE, READY, START_STAGE, RESULT, ACK, ABORT, DISCOVER, DISCOVER_ACK, PREPARE, READY, START_STAGE, RESULT, ACK, ABORT,
@@ -21,24 +21,25 @@ struct ProtocolPacket {
uint16_t stageCount; uint16_t stageCount;
uint16_t sequence; uint16_t sequence;
uint32_t requestedHz; uint32_t requestedHz;
uint32_t requestedPulseNs;
uint32_t actualHz; uint32_t actualHz;
uint16_t actualDutyX100; uint32_t actualPulseNs;
uint32_t testTimeMs; uint32_t testTimeMs;
uint16_t accuracyX100; uint16_t accuracyX100;
uint8_t settleCycles; uint8_t lightCode;
uint8_t progressStep; uint8_t progressStep;
uint8_t passed; uint8_t passed;
uint8_t reason; uint8_t reason;
uint32_t periods; uint32_t periods;
uint32_t measuredHzX10; uint32_t measuredHzX10;
uint16_t measuredDutyX10; uint32_t measuredPulseNs;
uint32_t minPeriodTicks; uint32_t minPeriodTicks;
uint32_t maxPeriodTicks; uint32_t maxPeriodTicks;
uint16_t crc; uint16_t crc;
}; };
#pragma pack(pop) #pragma pack(pop)
static_assert(sizeof(ProtocolPacket) == 54, "Protocol layout changed"); static_assert(sizeof(ProtocolPacket) == 62, "Protocol layout changed");
uint16_t packetCrc(const ProtocolPacket &packet); uint16_t packetCrc(const ProtocolPacket &packet);
void finalizePacket(ProtocolPacket &packet); void finalizePacket(ProtocolPacket &packet);

View File

@@ -12,20 +12,20 @@ namespace {
constexpr ledc_mode_t PWM_SPEED_MODE = LEDC_LOW_SPEED_MODE; constexpr ledc_mode_t PWM_SPEED_MODE = LEDC_LOW_SPEED_MODE;
constexpr ledc_timer_t PWM_TIMER = LEDC_TIMER_0; constexpr ledc_timer_t PWM_TIMER = LEDC_TIMER_0;
void setIntegerDivider(uint16_t divider) { void setDivider(uint32_t dividerRaw) {
ledc_dev_t *hardware = LEDC_LL_GET_HW(); ledc_dev_t *hardware = LEDC_LL_GET_HW();
ledc_ll_timer_pause(hardware, PWM_SPEED_MODE, PWM_TIMER); ledc_ll_timer_pause(hardware, PWM_SPEED_MODE, PWM_TIMER);
ledc_ll_set_clock_divider(hardware, PWM_SPEED_MODE, PWM_TIMER, ledc_ll_set_clock_divider(hardware, PWM_SPEED_MODE, PWM_TIMER,
static_cast<uint32_t>(divider) << LEDC_LL_FRACTIONAL_BITS); dividerRaw);
ledc_ll_timer_rst(hardware, PWM_SPEED_MODE, PWM_TIMER); ledc_ll_timer_rst(hardware, PWM_SPEED_MODE, PWM_TIMER);
ledc_ll_ls_timer_update(hardware, PWM_SPEED_MODE, PWM_TIMER); ledc_ll_ls_timer_update(hardware, PWM_SPEED_MODE, PWM_TIMER);
ledc_ll_timer_resume(hardware, PWM_SPEED_MODE, PWM_TIMER); ledc_ll_timer_resume(hardware, PWM_SPEED_MODE, PWM_TIMER);
} }
bool integerDividerIsSet(uint16_t expected) { bool dividerIsSet(uint32_t expectedRaw) {
uint32_t rawDivider = 0; uint32_t rawDivider = 0;
ledc_ll_get_clock_divider(LEDC_LL_GET_HW(), PWM_SPEED_MODE, PWM_TIMER, &rawDivider); ledc_ll_get_clock_divider(LEDC_LL_GET_HW(), PWM_SPEED_MODE, PWM_TIMER, &rawDivider);
return rawDivider == (static_cast<uint32_t>(expected) << LEDC_LL_FRACTIONAL_BITS); return rawDivider == expectedRaw;
} }
#elif CONFIG_IDF_TARGET_ESP32S3 #elif CONFIG_IDF_TARGET_ESP32S3
mcpwm_timer_handle_t mcpwmTimer = nullptr; mcpwm_timer_handle_t mcpwmTimer = nullptr;
@@ -94,38 +94,49 @@ void PwmGenerator::begin() {
timerConfig.period_ticks / 2U) == ESP_OK; timerConfig.period_ticks / 2U) == ESP_OK;
ok = ok && mcpwm_generator_set_action_on_timer_event(mcpwmGenerator, ok = ok && mcpwm_generator_set_action_on_timer_event(mcpwmGenerator,
MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP,
MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH)) == ESP_OK; MCPWM_TIMER_EVENT_EMPTY, TX_LIGHT_ON_GPIO_LEVEL == HIGH ?
MCPWM_GEN_ACTION_HIGH : MCPWM_GEN_ACTION_LOW)) == ESP_OK;
ok = ok && mcpwm_generator_set_action_on_compare_event(mcpwmGenerator, ok = ok && mcpwm_generator_set_action_on_compare_event(mcpwmGenerator,
MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP,
mcpwmComparator, MCPWM_GEN_ACTION_LOW)) == ESP_OK; mcpwmComparator, TX_LIGHT_OFF_GPIO_LEVEL == HIGH ?
MCPWM_GEN_ACTION_HIGH : MCPWM_GEN_ACTION_LOW)) == ESP_OK;
ok = ok && mcpwm_timer_enable(mcpwmTimer) == ESP_OK; ok = ok && mcpwm_timer_enable(mcpwmTimer) == ESP_OK;
if (!ok) { if (!ok) {
releaseMcpwm(); releaseMcpwm();
pinMode(GPIO_PWM, OUTPUT); pinMode(GPIO_PWM, OUTPUT);
digitalWrite(GPIO_PWM, PWM_SAFE_LEVEL); digitalWrite(GPIO_PWM, TX_LIGHT_OFF_GPIO_LEVEL);
return; return;
} }
mcpwm_generator_set_force_level(mcpwmGenerator, PWM_SAFE_LEVEL, true); mcpwm_generator_set_force_level(mcpwmGenerator, TX_LIGHT_OFF_GPIO_LEVEL, true);
#endif #endif
} }
bool PwmGenerator::start(uint32_t hz, uint8_t dutyPct, ActualPwm &a) { bool PwmGenerator::start(uint32_t hz, uint32_t pulseNs, ActualPwm &a) {
const uint8_t activeLevel = activeLightOn_ ? TX_LIGHT_ON_GPIO_LEVEL :
TX_LIGHT_OFF_GPIO_LEVEL;
const uint8_t inactiveLevel = activeLevel == HIGH ? LOW : HIGH;
#if CONFIG_IDF_TARGET_ESP32C3 #if CONFIG_IDF_TARGET_ESP32C3
IntegerPwmConfig config = {}; IntegerPwmConfig config = {};
if (!chooseIntegerPwmConfig(hz, LEDC_SOURCE_CLOCK_HZ, LEDC_MAX_BITS, dutyPct, config)) return false; if (!choosePwmConfig(hz, pulseNs, LEDC_SOURCE_CLOCK_HZ, LEDC_MAX_BITS, config)) return false;
const uint8_t bits = config.bits; const uint8_t bits = config.bits;
const uint32_t levels = 1UL << bits; const uint32_t levels = 1UL << bits;
const uint32_t duty = (static_cast<uint64_t>(levels) * dutyPct + 50U) / 100U; const uint32_t duty = config.dutyCount;
for (uint8_t attempt = 0; attempt < 2; ++attempt) { for (uint8_t attempt = 0; attempt < 2; ++attempt) {
stop(); stop();
const bool attached = ledcAttachChannel(GPIO_PWM, config.actualHz, bits, LEDC_CHANNEL); const bool attached = ledcAttachChannel(GPIO_PWM, config.actualHz, bits, LEDC_CHANNEL);
if (attached) { if (attached) {
// Arduino's LEDC API normally chooses an 8-bit fractional divider. // Native LEDC produces a HIGH pulse. Invert the GPIO matrix output when
// Force the fractional byte to zero so every PWM period contains the // the configured active pulse level is LOW.
// same integer number of 40 MHz source-clock ticks. if (!ledcOutputInvert(GPIO_PWM, activeLevel == LOW)) {
setIntegerDivider(config.divider); ledcDetach(GPIO_PWM);
delay(2);
continue;
}
// Apply the selected Q10.8 divider explicitly. This lets pulse width,
// rather than duty percentage, drive the hardware quantization.
setDivider(config.dividerRaw);
} }
if (attached && integerDividerIsSet(config.divider) && ledcWriteChannel(LEDC_CHANNEL, duty)) { if (attached && dividerIsSet(config.dividerRaw) && ledcWriteChannel(LEDC_CHANNEL, duty)) {
// On the first configuration after power-up the duty update is latched // On the first configuration after power-up the duty update is latched
// on a timer edge. Reading immediately can therefore return zero. // on a timer edge. Reading immediately can therefore return zero.
uint32_t settleUs = static_cast<uint32_t>((2000000ULL + hz - 1U) / hz); uint32_t settleUs = static_cast<uint32_t>((2000000ULL + hz - 1U) / hz);
@@ -133,7 +144,8 @@ bool PwmGenerator::start(uint32_t hz, uint8_t dutyPct, ActualPwm &a) {
delayMicroseconds(settleUs); delayMicroseconds(settleUs);
const uint32_t actualHz = ledcReadFreq(GPIO_PWM); const uint32_t actualHz = ledcReadFreq(GPIO_PWM);
if (actualHz == config.actualHz) { if (actualHz == config.actualHz) {
a = {hz, actualHz, 100.0f * duty / levels, bits}; a = {hz, actualHz, pulseNs, config.actualPulseNs,
100.0f * duty / levels, bits};
running_ = true; running_ = true;
return true; return true;
} }
@@ -141,28 +153,45 @@ bool PwmGenerator::start(uint32_t hz, uint8_t dutyPct, ActualPwm &a) {
if (attached) ledcDetach(GPIO_PWM); if (attached) ledcDetach(GPIO_PWM);
delay(2); delay(2);
} }
pinMode(GPIO_PWM, OUTPUT); digitalWrite(GPIO_PWM, PWM_SAFE_LEVEL); pinMode(GPIO_PWM, OUTPUT); digitalWrite(GPIO_PWM, TX_LIGHT_OFF_GPIO_LEVEL);
return false; return false;
#elif CONFIG_IDF_TARGET_ESP32S3 #elif CONFIG_IDF_TARGET_ESP32S3
if (!mcpwmTimer || !mcpwmComparator || !mcpwmGenerator || !hz || dutyPct > 100U || if (!mcpwmTimer || !mcpwmComparator || !mcpwmGenerator || !hz || !pulseNs ||
MCPWM_RESOLUTION_HZ % hz) return false; MCPWM_RESOLUTION_HZ % hz) return false;
const uint32_t periodTicks = MCPWM_RESOLUTION_HZ / hz; const uint32_t periodTicks = MCPWM_RESOLUTION_HZ / hz;
if (periodTicks < 2U || periodTicks > MCPWM_MAX_PERIOD_TICKS) return false; if (periodTicks < 2U || periodTicks > MCPWM_MAX_PERIOD_TICKS) return false;
uint32_t activeTicks = (static_cast<uint64_t>(periodTicks) * dutyPct + 50U) / 100U; uint32_t activeTicks = static_cast<uint32_t>(
(static_cast<uint64_t>(pulseNs) * MCPWM_RESOLUTION_HZ + 500000000ULL) /
1000000000ULL);
if (activeTicks == 0U) activeTicks = 1U; if (activeTicks == 0U) activeTicks = 1U;
if (activeTicks >= periodTicks) activeTicks = periodTicks - 1U; if (activeTicks >= periodTicks) activeTicks = periodTicks - 1U;
stop(); stop();
bool ok = mcpwm_timer_set_period(mcpwmTimer, periodTicks) == ESP_OK; bool ok = mcpwm_timer_set_period(mcpwmTimer, periodTicks) == ESP_OK;
ok = ok && mcpwm_comparator_set_compare_value(mcpwmComparator, activeTicks) == ESP_OK; ok = ok && mcpwm_comparator_set_compare_value(mcpwmComparator, activeTicks) == ESP_OK;
ok = ok && mcpwm_generator_set_force_level(mcpwmGenerator, -1, false) == ESP_OK; ok = ok && mcpwm_generator_set_action_on_timer_event(mcpwmGenerator,
MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP,
MCPWM_TIMER_EVENT_EMPTY, activeLevel == HIGH ?
MCPWM_GEN_ACTION_HIGH : MCPWM_GEN_ACTION_LOW)) == ESP_OK;
ok = ok && mcpwm_generator_set_action_on_compare_event(mcpwmGenerator,
MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP,
mcpwmComparator, inactiveLevel == HIGH ?
MCPWM_GEN_ACTION_HIGH : MCPWM_GEN_ACTION_LOW)) == ESP_OK;
// stop() applies a continuous force level (hold_on=true). Remove that same
// continuous-force action; hold_on=false addresses a different, one-shot
// force mechanism and would leave the safe level permanently active.
ok = ok && mcpwm_generator_set_force_level(mcpwmGenerator, -1, true) == ESP_OK;
ok = ok && mcpwm_timer_start_stop(mcpwmTimer, MCPWM_TIMER_START_NO_STOP) == ESP_OK; ok = ok && mcpwm_timer_start_stop(mcpwmTimer, MCPWM_TIMER_START_NO_STOP) == ESP_OK;
if (!ok) { if (!ok) {
mcpwm_generator_set_force_level(mcpwmGenerator, PWM_SAFE_LEVEL, true); mcpwm_generator_set_force_level(mcpwmGenerator, TX_LIGHT_OFF_GPIO_LEVEL, true);
return false; return false;
} }
a = {hz, hz, 100.0f * activeTicks / periodTicks, periodResolutionBits(periodTicks)}; const uint32_t actualPulseNs = static_cast<uint32_t>(
(static_cast<uint64_t>(activeTicks) * 1000000000ULL + MCPWM_RESOLUTION_HZ / 2U) /
MCPWM_RESOLUTION_HZ);
a = {hz, hz, pulseNs, actualPulseNs, 100.0f * activeTicks / periodTicks,
periodResolutionBits(periodTicks)};
mcpwmFrequencyHz = hz; mcpwmFrequencyHz = hz;
running_ = true; running_ = true;
return true; return true;
@@ -172,9 +201,10 @@ bool PwmGenerator::start(uint32_t hz, uint8_t dutyPct, ActualPwm &a) {
void PwmGenerator::stop() { void PwmGenerator::stop() {
#if CONFIG_IDF_TARGET_ESP32C3 #if CONFIG_IDF_TARGET_ESP32C3
if (running_) ledcDetach(GPIO_PWM); if (running_) ledcDetach(GPIO_PWM);
pinMode(GPIO_PWM, OUTPUT); digitalWrite(GPIO_PWM, PWM_SAFE_LEVEL); pinMode(GPIO_PWM, OUTPUT); digitalWrite(GPIO_PWM, TX_LIGHT_OFF_GPIO_LEVEL);
#elif CONFIG_IDF_TARGET_ESP32S3 #elif CONFIG_IDF_TARGET_ESP32S3
if (mcpwmGenerator) mcpwm_generator_set_force_level(mcpwmGenerator, PWM_SAFE_LEVEL, true); if (mcpwmGenerator) mcpwm_generator_set_force_level(
mcpwmGenerator, TX_LIGHT_OFF_GPIO_LEVEL, true);
if (running_ && mcpwmTimer) { if (running_ && mcpwmTimer) {
mcpwm_timer_start_stop(mcpwmTimer, MCPWM_TIMER_STOP_EMPTY); mcpwm_timer_start_stop(mcpwmTimer, MCPWM_TIMER_STOP_EMPTY);
const uint32_t waitUs = mcpwmFrequencyHz ? (1000000U / mcpwmFrequencyHz + 2U) : 2U; const uint32_t waitUs = mcpwmFrequencyHz ? (1000000U / mcpwmFrequencyHz + 2U) : 2U;
@@ -184,3 +214,35 @@ void PwmGenerator::stop() {
#endif #endif
running_ = false; running_ = false;
} }
void PwmGenerator::active() {
// First detach/stop the PWM peripheral, then apply the same active level
// that denotes the pulse during a running test.
stop();
#if CONFIG_IDF_TARGET_ESP32C3
digitalWrite(GPIO_PWM, activeLightOn_ ? TX_LIGHT_ON_GPIO_LEVEL :
TX_LIGHT_OFF_GPIO_LEVEL);
#elif CONFIG_IDF_TARGET_ESP32S3
const uint8_t level = activeLightOn_ ? TX_LIGHT_ON_GPIO_LEVEL :
TX_LIGHT_OFF_GPIO_LEVEL;
if (mcpwmGenerator) mcpwm_generator_set_force_level(mcpwmGenerator, level, true);
else {
pinMode(GPIO_PWM, OUTPUT);
digitalWrite(GPIO_PWM, level);
}
#endif
}
void PwmGenerator::lightOn() {
stop();
#if CONFIG_IDF_TARGET_ESP32C3
digitalWrite(GPIO_PWM, TX_LIGHT_ON_GPIO_LEVEL);
#elif CONFIG_IDF_TARGET_ESP32S3
if (mcpwmGenerator)
mcpwm_generator_set_force_level(mcpwmGenerator, TX_LIGHT_ON_GPIO_LEVEL, true);
else {
pinMode(GPIO_PWM, OUTPUT);
digitalWrite(GPIO_PWM, TX_LIGHT_ON_GPIO_LEVEL);
}
#endif
}

View File

@@ -1,14 +1,27 @@
#pragma once #pragma once
#include <Arduino.h> #include <Arduino.h>
struct ActualPwm { uint32_t requestedHz; uint32_t actualHz; float actualDutyPct; uint8_t bits; }; struct ActualPwm {
uint32_t requestedHz;
uint32_t actualHz;
uint32_t requestedPulseNs;
uint32_t actualPulseNs;
float actualDutyPct;
uint8_t bits;
};
class PwmGenerator { class PwmGenerator {
public: public:
void begin(); void begin();
bool start(uint32_t frequencyHz, uint8_t dutyPct, ActualPwm &actual); void configureActiveLight(bool lightOn) { activeLightOn_ = lightOn; }
bool start(uint32_t frequencyHz, uint32_t pulseNs, ActualPwm &actual);
// Hold the optical level selected as the active TX pulse.
void active();
// Hold actual optical light ON, independently of HH/HL/LH/LL.
void lightOn();
void stop(); void stop();
bool running() const { return running_; } bool running() const { return running_; }
private: private:
bool running_ = false; bool running_ = false;
bool activeLightOn_ = true;
}; };

View File

@@ -56,6 +56,7 @@ void Radio::end() {
if (heartbeatQueue_) xQueueReset(heartbeatQueue_); if (heartbeatQueue_) xQueueReset(heartbeatQueue_);
if (instance_ == this) instance_ = nullptr; if (instance_ == this) instance_ = nullptr;
windowedReceive_ = false; windowedReceive_ = false;
if (wasActive) WiFi.mode(WIFI_OFF);
Log::event("ESP-NOW", "stopped"); Log::event("ESP-NOW", "stopped");
} }

View File

@@ -1,13 +1,13 @@
#include "Receiver.h" #include "Receiver.h"
#include "Config.h" #include "Log.h"
#include <string.h> #include <math.h>
#if !OPTICAL_USE_RMT_DMA #if !OPTICAL_USE_MCPWM_CAPTURE
#include <esp_cpu.h> #include <esp_cpu.h>
#include <esp32-hal-cpu.h> #include <esp32-hal-cpu.h>
#endif #endif
uint32_t PulseReceiver::tickHz() const { uint32_t PulseReceiver::tickHz() const {
#if OPTICAL_USE_RMT_DMA #if OPTICAL_USE_MCPWM_CAPTURE
return captureResolutionHz_; return captureResolutionHz_;
#else #else
return cpuTickHz_; return cpuTickHz_;
@@ -15,31 +15,57 @@ uint32_t PulseReceiver::tickHz() const {
} }
uint32_t PulseReceiver::plannedTickHz(uint32_t expectedHz, float expectedDutyPct) const { uint32_t PulseReceiver::plannedTickHz(uint32_t expectedHz, float expectedDutyPct) const {
#if OPTICAL_USE_RMT_DMA if (!expectedHz || expectedDutyPct <= 0.0f || expectedDutyPct >= 100.0f) return 0;
if (!expectedHz || expectedDutyPct <= 0.0f || expectedDutyPct >= 100.0f) #if OPTICAL_USE_MCPWM_CAPTURE
return CAPTURE_RESOLUTION_OPTIONS_HZ[0]; return captureResolutionHz_ ? captureResolutionHz_ :
uint32_t dutyX100 = static_cast<uint32_t>(expectedDutyPct * 100.0f + 0.5f); MCPWM_CAPTURE_RESOLUTION_HZ;
if (dutyX100 < 5000U) dutyX100 = 10000U - dutyX100;
for (int i = static_cast<int>(countOf(CAPTURE_RESOLUTION_OPTIONS_HZ)) - 1; i >= 0; --i) {
const uint32_t resolution = CAPTURE_RESOLUTION_OPTIONS_HZ[i];
const uint64_t levelTicksX100 = static_cast<uint64_t>(resolution) * dutyX100;
const uint64_t limitX100 = static_cast<uint64_t>(expectedHz) * 10000ULL * RMT_MAX_LEVEL_TICKS;
if (levelTicksX100 <= limitX100) return resolution;
}
return CAPTURE_RESOLUTION_OPTIONS_HZ[0];
#else #else
(void)expectedHz; (void)expectedDutyPct;
return cpuTickHz_; return cpuTickHz_;
#endif #endif
} }
bool PulseReceiver::begin() { bool PulseReceiver::begin() {
#if OPTICAL_USE_RMT_DMA queue_ = xQueueCreate(512, sizeof(Edge));
queue_ = xQueueCreate(RMT_QUEUE_BLOCKS, sizeof(SymbolBlock));
return queue_ && configureRmt(CAPTURE_RESOLUTION_OPTIONS_HZ[0]);
#else
queue_ = xQueueCreate(256, sizeof(Edge));
if (!queue_) return false; if (!queue_) return false;
#if OPTICAL_USE_MCPWM_CAPTURE
// PWM generation uses MCPWM group 0. Group 1 is dedicated to input capture,
// so RX cannot exhaust or conflict with the generator's resources.
mcpwm_capture_timer_config_t timerConfig = {};
timerConfig.group_id = 1;
timerConfig.clk_src = MCPWM_CAPTURE_CLK_SRC_DEFAULT;
timerConfig.resolution_hz = MCPWM_CAPTURE_RESOLUTION_HZ;
if (mcpwm_new_capture_timer(&timerConfig, &captureTimer_) != ESP_OK) return false;
if (mcpwm_capture_timer_get_resolution(captureTimer_, &captureResolutionHz_) != ESP_OK ||
!captureResolutionHz_) return false;
mcpwm_capture_channel_config_t channelConfig = {};
channelConfig.gpio_num = GPIO_RX;
// ACK pulses are sub-microsecond and consecutive acknowledgements can be
// only 1 us apart. A low-priority capture interrupt can leave the channel
// status pending long enough for the next timestamp to overwrite it.
channelConfig.intr_priority = 3;
channelConfig.prescale = 1;
channelConfig.flags.pos_edge = true;
channelConfig.flags.neg_edge = false;
if (mcpwm_new_capture_channel(captureTimer_, &channelConfig, &risingChannel_) != ESP_OK)
return false;
mcpwm_capture_event_callbacks_t callbacks = {};
callbacks.on_cap = onCapture;
if (mcpwm_capture_channel_register_event_callbacks(
risingChannel_, &callbacks, this) != ESP_OK) return false;
channelConfig.flags.pos_edge = false;
channelConfig.flags.neg_edge = true;
if (mcpwm_new_capture_channel(captureTimer_, &channelConfig, &fallingChannel_) != ESP_OK)
return false;
if (mcpwm_capture_channel_register_event_callbacks(
fallingChannel_, &callbacks, this) != ESP_OK) return false;
// The TX channel is created immediately before a driver test. Only the end
// of the active PWM pulse is armed; handling its start here would occupy the
// shared MCPWM ISR during the RX acknowledgement only ~300 ns later.
return true;
#else
pinMode(GPIO_RX, INPUT); pinMode(GPIO_RX, INPUT);
cpuTickHz_ = getCpuFrequencyMhz() * 1000000UL; cpuTickHz_ = getCpuFrequencyMhz() * 1000000UL;
attachInterruptArg(GPIO_RX, onGpio, this, CHANGE); attachInterruptArg(GPIO_RX, onGpio, this, CHANGE);
@@ -47,177 +73,402 @@ bool PulseReceiver::begin() {
#endif #endif
} }
#if OPTICAL_USE_RMT_DMA bool PulseReceiver::start(uint32_t expectedHz, float expectedDutyPct,
bool PulseReceiver::configureRmt(uint32_t resolutionHz) { bool activeLightOn) {
if (channel_ && captureResolutionHz_ == resolutionHz) return true; (void)activeLightOn;
stop(); if (!plannedTickHz(expectedHz, expectedDutyPct)) return false;
if (channel_) { expectedHz_ = expectedHz;
if (rmt_del_channel(channel_) != ESP_OK) return false; expectedDutyPct_ = expectedDutyPct;
channel_ = nullptr; #if !OPTICAL_USE_MCPWM_CAPTURE
} cpuTickHz_ = getCpuFrequencyMhz() * 1000000UL;
rmt_rx_channel_config_t cfg = {}; if (!cpuTickHz_) return false;
cfg.clk_src = RMT_CLK_SRC_DEFAULT; cfg.resolution_hz = resolutionHz;
cfg.gpio_num = static_cast<gpio_num_t>(GPIO_RX);
cfg.flags.invert_in = RX_SIGNAL_INVERTED;
#if CONFIG_IDF_TARGET_ESP32S3
cfg.mem_block_symbols = 512;
cfg.flags.with_dma = true;
#else
// C3 has 48 RMT symbols per channel and no RMT DMA. A request for 512
// consumes all available blocks and fails with "no free rx channels".
cfg.mem_block_symbols = RMT_MIN_RECEIVE_SYMBOLS;
cfg.flags.with_dma = false; // C3 uses hardware RMT ping-pong partial reception
#endif #endif
if (rmt_new_rx_channel(&cfg, &channel_) != ESP_OK) return false; resetStream();
rmt_rx_event_callbacks_t callbacks = {}; callbacks.on_recv_done = onRmt; Log::event("CAPTURE", "RX optical polarity will be detected automatically");
if (rmt_rx_register_event_callbacks(channel_, &callbacks, this) != ESP_OK) { return startCapture(false);
rmt_del_channel(channel_); channel_ = nullptr; return false; }
#if OPTICAL_USE_MCPWM_CAPTURE
bool PulseReceiver::configureDriverTxCapture(bool risingEdge) {
if (running_) return false;
if (txChannel_) {
if (mcpwm_del_capture_channel(txChannel_) != ESP_OK) return false;
txChannel_ = nullptr;
} }
captureResolutionHz_ = resolutionHz; mcpwm_capture_channel_config_t config = {};
return true; config.gpio_num = GPIO_PWM;
config.intr_priority = 3;
config.prescale = 1;
config.flags.pos_edge = risingEdge;
config.flags.neg_edge = !risingEdge;
config.flags.io_loop_back = true;
if (mcpwm_new_capture_channel(captureTimer_, &config, &txChannel_) != ESP_OK)
return false;
mcpwm_capture_event_callbacks_t callbacks = {};
callbacks.on_cap = onCapture;
return mcpwm_capture_channel_register_event_callbacks(
txChannel_, &callbacks, this) == ESP_OK;
} }
#endif #endif
bool PulseReceiver::start(uint32_t expectedHz, float expectedDutyPct) { bool PulseReceiver::startDriver(uint32_t frequencyHz, uint32_t pulseNs,
#if OPTICAL_USE_RMT_DMA bool activeTxLightOn) {
const uint32_t resolutionHz = plannedTickHz(expectedHz, expectedDutyPct); #if OPTICAL_USE_MCPWM_CAPTURE
if (!configureRmt(resolutionHz)) return false; if (!frequencyHz || !pulseNs || !tickHz() || running_) return false;
#else
(void)expectedHz; (void)expectedDutyPct;
#endif
resetStream(); resetStream();
#if OPTICAL_USE_RMT_DMA const uint64_t pulseTicks =
// In partial RX mode the callback is delivered when this user buffer fills. (static_cast<uint64_t>(pulseNs) * tickHz() + 500000000ULL) /
// Keep chunks near 5 ms so low-frequency input is reported before NO SIGNAL. 1000000000ULL;
uint64_t symbols = (static_cast<uint64_t>(expectedHz) * RMT_TARGET_CHUNK_US + 999999ULL) / 1000000ULL; const uint32_t periodTicks = tickHz() / frequencyHz;
if (symbols < RMT_MIN_RECEIVE_SYMBOLS) symbols = RMT_MIN_RECEIVE_SYMBOLS; if (!pulseTicks || pulseTicks >= periodTicks || pulseTicks > UINT32_MAX)
if (symbols > RMT_MAX_RECEIVE_SYMBOLS) symbols = RMT_MAX_RECEIVE_SYMBOLS; return false;
receiveChunkSymbols_ = static_cast<uint16_t>(symbols); driverPulseTicks_ = static_cast<uint32_t>(pulseTicks);
if (rmt_enable(channel_) != ESP_OK) return false; driverReleaseSlackTicks_ = static_cast<uint32_t>(
rmt_receive_config_t cfg = {}; (static_cast<uint64_t>(DRIVER_RESPONSE_TIMEOUT_NS) * tickHz() +
cfg.signal_range_min_ns = 1000000000UL / captureResolutionHz_; 999999999ULL) / 1000000000ULL);
const uint64_t maxNs = 4000000000ULL / (expectedHz ? expectedHz : 1); const uint8_t activeRawLevel = activeTxLightOn ?
// A duration field is 15 bits. Keep the driver's end-of-signal threshold TX_LIGHT_ON_GPIO_LEVEL : TX_LIGHT_OFF_GPIO_LEVEL;
// strictly below that hardware limit (IDF rejects larger values). const bool pulseEndIsRising = activeRawLevel == LOW;
const uint64_t hardwareMaxNs = static_cast<uint64_t>(RMT_MAX_LEVEL_TICKS) * 1000000000ULL / captureResolutionHz_; if (!driverReleaseSlackTicks_ ||
cfg.signal_range_max_ns = static_cast<uint32_t>(maxNs > hardwareMaxNs ? hardwareMaxNs : maxNs); !configureDriverTxCapture(pulseEndIsRising)) return false;
cfg.flags.en_partial_rx = true; return startCapture(true);
if (rmt_receive(channel_, receiveBuffer_, #else
receiveChunkSymbols_ * sizeof(receiveBuffer_[0]), &cfg) != ESP_OK) { return false;
rmt_disable(channel_); return false;
}
#endif #endif
running_ = true; return true; }
bool PulseReceiver::startCapture(bool withTx) {
#if OPTICAL_USE_MCPWM_CAPTURE
// Progress updates keep one capture session alive. Pulse-width stages stop
// capture only after PWM is quiet, so resetStream never races the ISR.
if (running_) return true;
if (mcpwm_capture_timer_enable(captureTimer_) != ESP_OK) return false;
if (mcpwm_capture_channel_enable(risingChannel_) != ESP_OK) {
mcpwm_capture_timer_disable(captureTimer_);
return false;
}
if (mcpwm_capture_channel_enable(fallingChannel_) != ESP_OK) {
mcpwm_capture_channel_disable(risingChannel_);
mcpwm_capture_timer_disable(captureTimer_);
return false;
}
if (withTx && mcpwm_capture_channel_enable(txChannel_) != ESP_OK) {
mcpwm_capture_channel_disable(fallingChannel_);
mcpwm_capture_channel_disable(risingChannel_);
mcpwm_capture_timer_disable(captureTimer_);
return false;
}
txCaptureEnabled_ = withTx;
running_ = true;
if (mcpwm_capture_timer_start(captureTimer_) != ESP_OK) {
running_ = false;
if (txCaptureEnabled_) mcpwm_capture_channel_disable(txChannel_);
txCaptureEnabled_ = false;
mcpwm_capture_channel_disable(fallingChannel_);
mcpwm_capture_channel_disable(risingChannel_);
mcpwm_capture_timer_disable(captureTimer_);
return false;
}
#else
if (withTx) return false;
running_ = true;
#endif
return true;
} }
void PulseReceiver::stop() { void PulseReceiver::stop() {
#if OPTICAL_USE_RMT_DMA const bool wasRunning = running_;
if (running_) rmt_disable(channel_);
#endif
running_ = false; running_ = false;
#if OPTICAL_USE_MCPWM_CAPTURE
if (wasRunning) {
// Mask capture interrupts before stopping the shared capture timer.
if (txCaptureEnabled_) mcpwm_capture_channel_disable(txChannel_);
txCaptureEnabled_ = false;
mcpwm_capture_channel_disable(fallingChannel_);
mcpwm_capture_channel_disable(risingChannel_);
mcpwm_capture_timer_stop(captureTimer_);
mcpwm_capture_timer_disable(captureTimer_);
}
#else
(void)wasRunning;
#endif
} }
void PulseReceiver::resetStream() { void PulseReceiver::resetStream() {
if (queue_) xQueueReset(queue_); if (queue_) xQueueReset(queue_);
overflow_ = false; droppedItems_ = 0; haveRise_ = haveFall_ = haveRawTick_ = false; haveReorderEdge_ = false;
lastRawTick_ = 0; tickEpoch_ = rise_ = fall_ = 0; __atomic_store_n(&driverRingWrite_, 0U, __ATOMIC_RELEASE);
#if OPTICAL_USE_RMT_DMA __atomic_store_n(&driverRingRead_, 0U, __ATOMIC_RELEASE);
block_ = {}; blockIndex_ = 0; phase_ = 0; haveLevel_ = false; level_ = false; rmtTick_ = 0; haveLastDriverEdge_ = false;
#endif lastDriverEdge_ = {};
driverPulseTicks_ = 0;
driverReleaseSlackTicks_ = 0;
droppedItems_ = 0;
polarityKnown_ = false;
activeStartRising_ = false;
polarityEdgeCount_ = 0;
memset(polarityEdges_, 0, sizeof(polarityEdges_));
waitingForActiveEnd_ = true;
activeStart_ = activeEnd_ = 0;
haveRawTick_ = false;
lastRawTick_ = 0;
tickEpoch_ = 0;
} }
bool PulseReceiver::consumeEdge(const Edge &e, PulsePeriod &out) { PulseReceiver::TimedEdge PulseReceiver::extendEdge(const Edge &e) {
if (haveRawTick_ && e.tick < lastRawTick_ && lastRawTick_ - e.tick > 0x80000000UL) if (haveRawTick_ && e.tick < lastRawTick_ && lastRawTick_ - e.tick > 0x80000000UL)
tickEpoch_ += 0x100000000ULL; tickEpoch_ += 0x100000000ULL;
haveRawTick_ = true; lastRawTick_ = e.tick; haveRawTick_ = true; lastRawTick_ = e.tick;
const uint64_t tick = tickEpoch_ + e.tick; return {tickEpoch_ + e.tick, e.rising != 0};
if (e.rising) {
if (!haveRise_) { rise_ = tick; haveRise_ = true; haveFall_ = false; return false; }
if (!haveFall_) { overflow_ = true; rise_ = tick; return false; }
const uint32_t period = static_cast<uint32_t>(tick - rise_);
const uint32_t active = fall_ - rise_;
out = {rise_, period, active}; rise_ = tick; haveFall_ = false;
return true;
}
// Reception can begin in the middle of a HIGH pulse. In that case the first
// observable edge is falling and there is no complete period to validate.
// Ignore only this leading partial pulse and synchronize on the next rise.
if (!haveRise_) return false;
if (haveFall_) { overflow_ = true; return false; }
fall_ = tick; haveFall_ = true; return false;
} }
bool PulseReceiver::overflowed() { bool PulseReceiver::consumeEdge(const Edge &rawEdge, PulsePeriod &out) {
const bool value = overflow_; overflow_ = false; return value; const TimedEdge edge = extendEdge(rawEdge);
if (polarityKnown_) {
// Deliberately ignore edge type after synchronization. A PWM waveform is
// just alternating intervals: active, inactive, active, inactive. An
// extra or missing edge therefore becomes a concrete wrong pulse/period
// instead of an ambiguous GLITCH state.
if (waitingForActiveEnd_) {
activeEnd_ = edge.tick;
waitingForActiveEnd_ = false;
return false;
}
const uint64_t periodTicks = edge.tick - activeStart_;
const uint64_t activeTicks = activeEnd_ - activeStart_;
out = {activeStart_, static_cast<uint32_t>(periodTicks),
static_cast<uint32_t>(activeTicks), tickHz()};
activeStart_ = edge.tick;
waitingForActiveEnd_ = true;
return true;
}
// Optical mode does not use the DRIVER level setting. Compare the first two
// alternating intervals with the configured active duration and select the
// level that is actually present at RX. Three edges are enough to determine
// polarity and, when the first interval is active, produce the first period.
polarityEdges_[polarityEdgeCount_++] = edge;
if (polarityEdgeCount_ < 3U) return false;
const uint64_t firstInterval =
polarityEdges_[1].tick - polarityEdges_[0].tick;
const uint64_t secondInterval =
polarityEdges_[2].tick - polarityEdges_[1].tick;
const uint64_t expectedPeriod = tickHz() / expectedHz_;
const uint64_t expectedActive = static_cast<uint64_t>(
expectedPeriod * expectedDutyPct_ / 100.0f + 0.5f);
const uint64_t firstError = firstInterval > expectedActive ?
firstInterval - expectedActive : expectedActive - firstInterval;
const uint64_t secondError = secondInterval > expectedActive ?
secondInterval - expectedActive : expectedActive - secondInterval;
const bool firstIntervalIsActive = firstError <= secondError;
activeStartRising_ = firstIntervalIsActive ? polarityEdges_[0].rising :
polarityEdges_[1].rising;
polarityKnown_ = true;
polarityEdgeCount_ = 0;
if (firstIntervalIsActive) {
out = {polarityEdges_[0].tick,
static_cast<uint32_t>(polarityEdges_[2].tick - polarityEdges_[0].tick),
static_cast<uint32_t>(firstInterval), tickHz()};
activeStart_ = polarityEdges_[2].tick;
waitingForActiveEnd_ = true;
return true;
}
activeStart_ = polarityEdges_[1].tick;
activeEnd_ = polarityEdges_[2].tick;
waitingForActiveEnd_ = false;
return false;
} }
uint32_t PulseReceiver::takeDroppedItems() { uint32_t PulseReceiver::takeDroppedItems() {
return __atomic_exchange_n(&droppedItems_, 0, __ATOMIC_RELAXED); return __atomic_exchange_n(&droppedItems_, 0, __ATOMIC_RELAXED);
} }
#if OPTICAL_USE_RMT_DMA #if OPTICAL_USE_MCPWM_CAPTURE
bool IRAM_ATTR PulseReceiver::onRmt(rmt_channel_handle_t, const rmt_rx_done_event_data_t *data, void *ctx) { bool IRAM_ATTR PulseReceiver::onCapture(mcpwm_cap_channel_handle_t channel,
const mcpwm_capture_event_data_t *data,
void *ctx) {
PulseReceiver *self = static_cast<PulseReceiver *>(ctx); PulseReceiver *self = static_cast<PulseReceiver *>(ctx);
if (!self->running_) return false;
const bool rawRising = data->cap_edge == MCPWM_CAP_EDGE_POS;
const Edge edge = {data->cap_value, static_cast<uint8_t>(rawRising),
static_cast<uint8_t>(channel == self->txChannel_ ? CaptureSource::TX :
CaptureSource::RX)};
if (self->txCaptureEnabled_) {
// All channels in one MCPWM group are dispatched serially by the same
// group ISR. Keep this callback shorter than the minimum interval between
// equal RX edges (about 2.1 us at W=2 us): a spinlock and several atomic
// RMW operations here can leave the channel pending until its capture
// register is overwritten by the next edge.
if (self->haveLastDriverEdge_ &&
self->lastDriverEdge_.tick == edge.tick &&
self->lastDriverEdge_.rising == edge.rising &&
self->lastDriverEdge_.source == edge.source) {
// The same channel callback can be delivered twice while several MCPWM
// capture status bits are pending. Two physical edges cannot have the
// same source, direction and 12.5 ns hardware timestamp.
return false;
}
const uint16_t write = self->driverRingWrite_;
const uint16_t next = static_cast<uint16_t>(
(write + 1U) & (DRIVER_RING_CAPACITY - 1U));
if (next == self->driverRingRead_) {
++self->droppedItems_;
} else {
self->driverRing_[write] = edge;
self->lastDriverEdge_ = edge;
self->haveLastDriverEdge_ = true;
asm volatile("memw" ::: "memory");
self->driverRingWrite_ = next;
}
return false;
}
BaseType_t wake = pdFALSE; BaseType_t wake = pdFALSE;
size_t offset = 0; if (xQueueSendFromISR(self->queue_, &edge, &wake) != pdTRUE)
while (offset < data->num_symbols) { __atomic_fetch_add(&self->droppedItems_, 1U, __ATOMIC_RELAXED);
SymbolBlock &b = self->isrBlock_;
b.count = static_cast<uint16_t>((data->num_symbols - offset) > BLOCK_SYMBOLS ?
BLOCK_SYMBOLS : (data->num_symbols - offset));
memcpy(b.symbols, data->received_symbols + offset, b.count * sizeof(rmt_symbol_word_t));
if (xQueueSendFromISR(self->queue_, &b, &wake) != pdTRUE)
__atomic_fetch_add(&self->droppedItems_, b.count, __ATOMIC_RELAXED);
offset += b.count;
}
return wake == pdTRUE; return wake == pdTRUE;
} }
bool PulseReceiver::nextRmtEdge(Edge &edge, TickType_t waitTicks) {
for (;;) {
if (blockIndex_ >= block_.count) {
if (xQueueReceive(queue_, &block_, waitTicks) != pdTRUE) return false;
blockIndex_ = 0; phase_ = 0;
waitTicks = 0;
}
const rmt_symbol_word_t &s = block_.symbols[blockIndex_];
const bool nextLevel = phase_ == 0 ? s.level0 : s.level1;
const uint32_t duration = phase_ == 0 ? s.duration0 : s.duration1;
phase_ ^= 1;
if (phase_ == 0) ++blockIndex_;
if (!duration) continue;
if (!haveLevel_) { haveLevel_ = true; level_ = nextLevel; rmtTick_ += duration; continue; }
if (nextLevel != level_) {
level_ = nextLevel; edge = {rmtTick_, static_cast<uint8_t>(nextLevel)};
rmtTick_ += duration; return true;
}
rmtTick_ += duration;
}
}
size_t PulseReceiver::readPeriods(PulsePeriod *periods, size_t capacity, TickType_t waitTicks) {
size_t count = 0;
Edge e;
while (count < capacity && nextRmtEdge(e, count ? 0 : waitTicks))
if (consumeEdge(e, periods[count])) ++count;
return count;
}
#else #else
void IRAM_ATTR PulseReceiver::onGpio(void *ctx) { void IRAM_ATTR PulseReceiver::onGpio(void *ctx) {
PulseReceiver *self = static_cast<PulseReceiver *>(ctx); PulseReceiver *self = static_cast<PulseReceiver *>(ctx);
bool level = gpio_get_level(static_cast<gpio_num_t>(GPIO_RX)); if (!self->running_) return;
if (RX_SIGNAL_INVERTED) level = !level; const bool level = gpio_get_level(static_cast<gpio_num_t>(GPIO_RX));
Edge e = {esp_cpu_get_cycle_count(), static_cast<uint8_t>(level)}; const Edge edge = {esp_cpu_get_cycle_count(), static_cast<uint8_t>(level),
static_cast<uint8_t>(CaptureSource::RX)};
BaseType_t wake = pdFALSE; BaseType_t wake = pdFALSE;
if (xQueueSendFromISR(self->queue_, &e, &wake) != pdTRUE) if (xQueueSendFromISR(self->queue_, &edge, &wake) != pdTRUE)
__atomic_fetch_add(&self->droppedItems_, 1U, __ATOMIC_RELAXED); __atomic_fetch_add(&self->droppedItems_, 1U, __ATOMIC_RELAXED);
if (wake) portYIELD_FROM_ISR(); if (wake) portYIELD_FROM_ISR();
} }
#endif
size_t PulseReceiver::readPeriods(PulsePeriod *periods, size_t capacity, TickType_t waitTicks) { bool PulseReceiver::nextOrderedEdge(Edge &edge, TickType_t waitTicks) {
if (!haveReorderEdge_) {
if (xQueueReceive(queue_, &reorderEdge_, waitTicks) != pdTRUE) return false;
haveReorderEdge_ = true;
}
Edge next = {};
// Keep one-event look-ahead. If both channel interrupts were pending while
// OLED/I2C ran, the MCPWM driver may dispatch them by channel number rather
// than timestamp. The signed modular comparison restores their real order.
if (xQueueReceive(queue_, &next, waitTicks) != pdTRUE) return false;
if (static_cast<int32_t>(next.tick - reorderEdge_.tick) < 0) {
edge = next;
} else {
edge = reorderEdge_;
reorderEdge_ = next;
}
return true;
}
size_t PulseReceiver::readPeriods(PulsePeriod *periods, size_t capacity,
TickType_t waitTicks) {
size_t count = 0; size_t count = 0;
Edge e; Edge edge = {};
while (count < capacity && xQueueReceive(queue_, &e, count ? 0 : waitTicks) == pdTRUE) while (count < capacity && nextOrderedEdge(edge, count ? 0 : waitTicks)) {
if (consumeEdge(e, periods[count])) ++count; if (consumeEdge(edge, periods[count])) {
periods[count].activeTickHz = tickHz();
++count;
}
}
return count;
}
size_t PulseReceiver::readEvents(CaptureEvent *events, size_t capacity,
TickType_t waitTicks) {
if (!events || capacity < 3U || !txCaptureEnabled_ || !driverPulseTicks_ ||
!driverReleaseSlackTicks_) return 0;
constexpr size_t MAX_BATCH = 64;
const size_t limit = capacity < MAX_BATCH ? capacity : MAX_BATCH;
Edge ordered[MAX_BATCH] = {};
uint16_t read = __atomic_load_n(&driverRingRead_, __ATOMIC_RELAXED);
if (read == __atomic_load_n(&driverRingWrite_, __ATOMIC_ACQUIRE) && waitTicks) {
vTaskDelay(waitTicks);
read = __atomic_load_n(&driverRingRead_, __ATOMIC_RELAXED);
}
// Work on one immutable producer snapshot. RX belonging to a pulse start is
// deliberately retained until that pulse's captured end arrives: only then
// can the missing start interrupt be reconstructed and sorted before RX.
const uint16_t write = __atomic_load_n(&driverRingWrite_, __ATOMIC_ACQUIRE);
uint16_t scan = read;
bool haveLatestTxEnd = false;
bool haveReleaseTxEnd = false;
uint32_t latestTxEnd = 0;
uint32_t releaseTxEnd = 0;
size_t projectedCount = 0;
while (scan != write) {
const Edge &edge = driverRing_[scan];
const size_t needed = edge.source == static_cast<uint8_t>(CaptureSource::TX)
? 2U : 1U;
// Reserve one output slot for WINDOW_END.
if (projectedCount + needed + 1U > limit) break;
projectedCount += needed;
if (edge.source == static_cast<uint8_t>(CaptureSource::TX)) {
if (haveLatestTxEnd) {
releaseTxEnd = latestTxEnd;
haveReleaseTxEnd = true;
}
latestTxEnd = edge.tick;
haveLatestTxEnd = true;
}
scan = static_cast<uint16_t>((scan + 1U) % DRIVER_RING_CAPACITY);
}
// Keep the newest TX period in the ring. Arrival of the following TX end
// proves that the previous end's ACK/fault window has completely elapsed.
if (!haveReleaseTxEnd) {
if (waitTicks) vTaskDelay(waitTicks);
return 0;
}
const uint32_t releaseThrough = releaseTxEnd + driverReleaseSlackTicks_;
size_t count = 0;
while (read != write) {
const Edge edge = driverRing_[read];
if (edge.source == static_cast<uint8_t>(CaptureSource::TX) &&
static_cast<int32_t>(edge.tick - releaseTxEnd) > 0)
break;
if (edge.source == static_cast<uint8_t>(CaptureSource::RX) &&
static_cast<int32_t>(edge.tick - releaseThrough) > 0)
break;
const size_t needed = edge.source == static_cast<uint8_t>(CaptureSource::TX)
? 2U : 1U;
if (count + needed > limit) break;
read = static_cast<uint16_t>((read + 1U) % DRIVER_RING_CAPACITY);
if (edge.source == static_cast<uint8_t>(CaptureSource::TX)) {
Edge pulseStart = edge;
pulseStart.tick -= driverPulseTicks_;
pulseStart.rising = !edge.rising;
ordered[count++] = pulseStart;
}
ordered[count++] = edge;
}
__atomic_store_n(&driverRingRead_, read, __ATOMIC_RELEASE);
// MCPWM channels share one timer but their callbacks can be dispatched in
// channel order when several interrupts are pending. Restore the hardware
// order inside the captured batch using the common timestamp.
for (size_t i = 1; i < count; ++i) {
const Edge key = ordered[i];
size_t j = i;
while (j && static_cast<int32_t>(ordered[j - 1].tick - key.tick) > 0) {
ordered[j] = ordered[j - 1];
--j;
}
ordered[j] = key;
}
for (size_t i = 0; i < count; ++i) {
const TimedEdge timed = extendEdge(ordered[i]);
events[i] = {timed.tick, timed.rising,
static_cast<CaptureSource>(ordered[i].source)};
}
const Edge marker = {releaseThrough, 0,
static_cast<uint8_t>(CaptureSource::WINDOW_END)};
const TimedEdge timedMarker = extendEdge(marker);
events[count++] = {timedMarker.tick, false, CaptureSource::WINDOW_END};
return count; return count;
} }
#endif

View File

@@ -1,66 +1,90 @@
#pragma once #pragma once
#include <Arduino.h> #include <Arduino.h>
#include <esp_idf_version.h> #include <esp_idf_version.h>
#include <driver/gpio.h>
#include "Config.h" #include "Config.h"
#include "Core.h" #include "Core.h"
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) #if CONFIG_IDF_TARGET_ESP32S3 && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
#define OPTICAL_USE_RMT_DMA 1 #define OPTICAL_USE_MCPWM_CAPTURE 1
#include <driver/rmt_rx.h> #include <driver/mcpwm_cap.h>
#else #else
#define OPTICAL_USE_RMT_DMA 0 #define OPTICAL_USE_MCPWM_CAPTURE 0
#include <driver/gpio.h>
#endif #endif
enum class CaptureSource : uint8_t { RX, TX, WINDOW_END };
struct CaptureEvent {
uint64_t tick;
bool rising;
CaptureSource source;
};
class PulseReceiver { class PulseReceiver {
public: public:
bool begin(); bool begin();
bool start(uint32_t expectedHz, float expectedDutyPct); bool start(uint32_t expectedHz, float expectedDutyPct, bool activeLightOn);
bool startDriver(uint32_t frequencyHz, uint32_t pulseNs,
bool activeTxLightOn);
void stop(); void stop();
void resetStream(); void resetStream();
size_t readPeriods(PulsePeriod *periods, size_t capacity, TickType_t waitTicks = 0); size_t readPeriods(PulsePeriod *periods, size_t capacity, TickType_t waitTicks = 0);
bool overflowed(); size_t readEvents(CaptureEvent *events, size_t capacity, TickType_t waitTicks = 0);
uint32_t takeDroppedItems(); uint32_t takeDroppedItems();
uint32_t tickHz() const; uint32_t tickHz() const;
uint32_t pulseTickHz() const { return tickHz(); }
uint32_t plannedTickHz(uint32_t expectedHz, float expectedDutyPct) const; uint32_t plannedTickHz(uint32_t expectedHz, float expectedDutyPct) const;
uint16_t receiveChunkSymbols() const { return receiveChunkSymbols_; } uint32_t plannedPulseTickHz(uint32_t expectedHz, float expectedDutyPct) const {
bool highRateBackend() const { return plannedTickHz(expectedHz, expectedDutyPct);
#if OPTICAL_USE_RMT_DMA && CONFIG_IDF_TARGET_ESP32S3
return true;
#else
return false;
#endif
} }
private: uint16_t receiveChunkSymbols() const { return 1; }
struct Edge { uint32_t tick; uint8_t rising; }; bool highRateBackend() const { return OPTICAL_USE_MCPWM_CAPTURE; }
bool consumeEdge(const Edge &edge, PulsePeriod &period);
#if OPTICAL_USE_RMT_DMA private:
static constexpr size_t BLOCK_SYMBOLS = RMT_MAX_RECEIVE_SYMBOLS; struct Edge { uint32_t tick; uint8_t rising; uint8_t source; };
struct SymbolBlock { uint16_t count; rmt_symbol_word_t symbols[BLOCK_SYMBOLS]; }; static constexpr uint16_t DRIVER_RING_CAPACITY = 512;
static bool IRAM_ATTR onRmt(rmt_channel_handle_t, const rmt_rx_done_event_data_t *, void *); static_assert((DRIVER_RING_CAPACITY & (DRIVER_RING_CAPACITY - 1U)) == 0,
bool configureRmt(uint32_t resolutionHz); "driver capture ring must be a power of two");
bool nextRmtEdge(Edge &edge, TickType_t waitTicks); struct TimedEdge { uint64_t tick; bool rising; };
rmt_channel_handle_t channel_ = nullptr; bool startCapture(bool withTx);
bool consumeEdge(const Edge &edge, PulsePeriod &period);
bool nextOrderedEdge(Edge &edge, TickType_t waitTicks);
TimedEdge extendEdge(const Edge &edge);
#if OPTICAL_USE_MCPWM_CAPTURE
bool configureDriverTxCapture(bool risingEdge);
static bool IRAM_ATTR onCapture(mcpwm_cap_channel_handle_t,
const mcpwm_capture_event_data_t *, void *);
mcpwm_cap_timer_handle_t captureTimer_ = nullptr;
mcpwm_cap_channel_handle_t risingChannel_ = nullptr;
mcpwm_cap_channel_handle_t fallingChannel_ = nullptr;
mcpwm_cap_channel_handle_t txChannel_ = nullptr;
uint32_t captureResolutionHz_ = 0; uint32_t captureResolutionHz_ = 0;
rmt_symbol_word_t receiveBuffer_[RMT_MAX_RECEIVE_SYMBOLS];
uint16_t receiveChunkSymbols_ = 0;
SymbolBlock isrBlock_ = {};
SymbolBlock block_ = {};
uint16_t blockIndex_ = 0;
uint8_t phase_ = 0;
bool haveLevel_ = false;
bool level_ = false;
uint32_t rmtTick_ = 0;
#else #else
static void IRAM_ATTR onGpio(void *ctx); static void IRAM_ATTR onGpio(void *ctx);
uint32_t cpuTickHz_ = 0; uint32_t cpuTickHz_ = 0;
#endif #endif
QueueHandle_t queue_ = nullptr; QueueHandle_t queue_ = nullptr;
volatile bool overflow_ = false; Edge driverRing_[DRIVER_RING_CAPACITY] = {};
volatile uint16_t driverRingWrite_ = 0;
volatile uint16_t driverRingRead_ = 0;
portMUX_TYPE driverRingMux_ = portMUX_INITIALIZER_UNLOCKED;
Edge lastDriverEdge_ = {};
bool haveLastDriverEdge_ = false;
uint32_t driverPulseTicks_ = 0;
uint32_t driverReleaseSlackTicks_ = 0;
Edge reorderEdge_ = {};
bool haveReorderEdge_ = false;
volatile uint32_t droppedItems_ = 0; volatile uint32_t droppedItems_ = 0;
bool running_ = false; volatile bool running_ = false;
bool haveRise_ = false, haveFall_ = false, haveRawTick_ = false; volatile bool txCaptureEnabled_ = false;
uint32_t expectedHz_ = 0;
float expectedDutyPct_ = 50.0f;
bool polarityKnown_ = false, activeStartRising_ = false;
TimedEdge polarityEdges_[3] = {};
uint8_t polarityEdgeCount_ = 0;
bool waitingForActiveEnd_ = true;
uint64_t activeStart_ = 0, activeEnd_ = 0;
bool haveRawTick_ = false;
uint32_t lastRawTick_ = 0; uint32_t lastRawTick_ = 0;
uint64_t tickEpoch_ = 0, rise_ = 0, fall_ = 0; uint64_t tickEpoch_ = 0;
}; };

View File

@@ -3,20 +3,36 @@
#include "Log.h" #include "Log.h"
#include <Preferences.h> #include <Preferences.h>
namespace { constexpr uint16_t SETTINGS_VERSION = 4; constexpr char NAMESPACE[] = "opt-test"; } // 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 { void SettingsStore::defaults(Settings &s) const {
s = {SETTINGS_VERSION, static_cast<uint8_t>(Role::SOLO), 0, 4, 2, 3, 2, 0}; // 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, static_cast<uint8_t>(TestGroup::OPTICS),
static_cast<uint8_t>(BoardTest::ADC), 0};
s.checksum = settingsChecksum(s); s.checksum = settingsChecksum(s);
} }
bool SettingsStore::valid(const Settings &s) const { bool SettingsStore::valid(const Settings &s) const {
return s.version == SETTINGS_VERSION && s.role <= static_cast<uint8_t>(Role::SLAVE) && return s.version == SETTINGS_VERSION && s.role <= static_cast<uint8_t>(Role::SLAVE) &&
s.startIndex < countOf(START_FREQ_OPTIONS_HZ) && s.endIndex < countOf(END_FREQ_OPTIONS_HZ) && 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) ||
s.role == static_cast<uint8_t>(Role::SOLO)) &&
s.frequencyIndex < countOf(PWM_FREQUENCY_OPTIONS_HZ) &&
s.maxPulseIndex < countOf(MAX_PULSE_OPTIONS_NS) &&
s.minPulseIndex < countOf(MIN_PULSE_OPTIONS_NS) &&
MAX_PULSE_OPTIONS_NS[s.maxPulseIndex] >= MIN_PULSE_OPTIONS_NS[s.minPulseIndex] &&
static_cast<uint64_t>(MAX_PULSE_OPTIONS_NS[s.maxPulseIndex]) *
PWM_FREQUENCY_OPTIONS_HZ[s.frequencyIndex] < 1000000000ULL &&
s.accuracyIndex < countOf(ACCURACY_OPTIONS_PCT) && s.accuracyIndex < countOf(ACCURACY_OPTIONS_PCT) &&
s.timeIndex < countOf(TEST_TIME_OPTIONS_MS) && s.dutyIndex < countOf(DUTY_OPTIONS_PCT) && s.timeIndex < countOf(TEST_TIME_OPTIONS_MS) &&
s.checksum == settingsChecksum(s) && s.checksum == settingsChecksum(s);
END_FREQ_OPTIONS_HZ[s.endIndex] > START_FREQ_OPTIONS_HZ[s.startIndex];
} }
bool SettingsStore::load(Settings &s) { bool SettingsStore::load(Settings &s) {
@@ -39,7 +55,7 @@ bool SettingsStore::save(Settings &s) {
} }
TestParams SettingsStore::params(const Settings &s) const { TestParams SettingsStore::params(const Settings &s) const {
return {START_FREQ_OPTIONS_HZ[s.startIndex], END_FREQ_OPTIONS_HZ[s.endIndex], return {PWM_FREQUENCY_OPTIONS_HZ[s.frequencyIndex],
ACCURACY_OPTIONS_PCT[s.accuracyIndex], TEST_TIME_OPTIONS_MS[s.timeIndex], MAX_PULSE_OPTIONS_NS[s.maxPulseIndex], MIN_PULSE_OPTIONS_NS[s.minPulseIndex],
DUTY_OPTIONS_PCT[s.dutyIndex]}; ACCURACY_OPTIONS_PCT[s.accuracyIndex], TEST_TIME_OPTIONS_MS[s.timeIndex]};
} }

View File

@@ -0,0 +1,1866 @@
G04*
G04 #@! TF.GenerationSoftware,Altium Limited,Altium Designer,19.1.9 (167)*
G04*
G04 Layer_Physical_Order=3*
G04 Layer_Color=16711680*
%FSLAX44Y44*%
%MOMM*%
G71*
G01*
G75*
%ADD19R,1.2000X1.4000*%
%ADD20R,1.3000X1.5000*%
%ADD21R,1.5000X1.3000*%
%ADD22R,1.4000X1.2000*%
%ADD43C,0.8000*%
%ADD44C,1.5240*%
%ADD45C,1.7000*%
%ADD46O,2.4000X1.8000*%
%ADD47C,1.5000*%
%ADD48R,1.5000X1.5000*%
%ADD49R,1.5000X1.5000*%
%ADD50C,3.0000*%
%ADD51C,2.0000*%
%ADD52C,1.8000*%
%ADD53R,1.8000X1.8000*%
%ADD54C,1.6000*%
%ADD55R,1.6000X1.6000*%
%ADD56R,1.7000X1.7000*%
%ADD57R,1.6000X1.6000*%
%ADD58C,1.6000*%
G04:AMPARAMS|DCode=59|XSize=2.3mm|YSize=1.8mm|CornerRadius=0.225mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=0.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
%AMROUNDEDRECTD59*
21,1,2.3000,1.3500,0,0,0.0*
21,1,1.8500,1.8000,0,0,0.0*
1,1,0.4500,0.9250,-0.6750*
1,1,0.4500,-0.9250,-0.6750*
1,1,0.4500,-0.9250,0.6750*
1,1,0.4500,0.9250,0.6750*
%
%ADD59ROUNDEDRECTD59*%
G36*
X164097Y490000D02*
X167924Y480761D01*
X133581Y446419D01*
X132139Y444539D01*
X131232Y442350D01*
X130922Y440000D01*
Y403760D01*
X93581Y366419D01*
X92139Y364539D01*
X91232Y362349D01*
X90922Y360000D01*
Y320166D01*
X88190Y317602D01*
X80922Y313286D01*
X80000Y313408D01*
X76737Y312978D01*
X73696Y311719D01*
X71085Y309715D01*
X69081Y307104D01*
X67822Y304063D01*
X67392Y300800D01*
X67822Y297537D01*
X69081Y294496D01*
X71085Y290946D01*
Y285254D01*
X69081Y281704D01*
X67822Y278663D01*
X67392Y275400D01*
X67822Y272137D01*
X69001Y269289D01*
X69081Y269096D01*
X71085Y263368D01*
Y262032D01*
X69081Y256304D01*
X69001Y256110D01*
X67822Y253263D01*
X67392Y250000D01*
X67822Y246737D01*
X69001Y243890D01*
X69081Y243696D01*
X71085Y237968D01*
Y236632D01*
X69081Y230904D01*
X69001Y230711D01*
X67822Y227863D01*
X67392Y224600D01*
X67774Y221700D01*
X67803Y220779D01*
X67500Y211700D01*
X67500Y205831D01*
Y202700D01*
X80000D01*
X92500D01*
Y205831D01*
X92500Y211700D01*
X92197Y220779D01*
X92226Y221700D01*
X92608Y224600D01*
X92178Y227863D01*
X90999Y230711D01*
X90919Y230904D01*
X88915Y236632D01*
Y237968D01*
X90919Y243696D01*
X90999Y243890D01*
X99504Y246871D01*
X102289Y246922D01*
X105950Y244321D01*
X108864Y235075D01*
X107468Y233374D01*
X106075Y230768D01*
X105217Y227941D01*
X104927Y225000D01*
X105217Y222059D01*
X106075Y219232D01*
X107468Y216626D01*
X109342Y214342D01*
X111626Y212468D01*
X114232Y211075D01*
X117059Y210217D01*
X120000Y209927D01*
X122941Y210217D01*
X125768Y211075D01*
X128374Y212468D01*
X130658Y214342D01*
X132532Y216626D01*
X133925Y219232D01*
X134783Y222059D01*
X135073Y225000D01*
X134783Y227941D01*
X133925Y230768D01*
X132532Y233374D01*
X130658Y235658D01*
Y239342D01*
X132532Y241626D01*
X133925Y244232D01*
X134783Y247059D01*
X135073Y250000D01*
X134783Y252941D01*
X133925Y255768D01*
X132532Y258374D01*
X130658Y260658D01*
Y264342D01*
X132532Y266626D01*
X133925Y269232D01*
X134783Y272059D01*
X135073Y275000D01*
X134783Y277941D01*
X133925Y280768D01*
X133814Y280976D01*
X163040Y310203D01*
X171433Y304691D01*
X170289Y300921D01*
X169903Y297000D01*
X170289Y293079D01*
X171433Y289309D01*
X173290Y285835D01*
X175789Y282789D01*
X178835Y280290D01*
X182309Y278433D01*
X186079Y277289D01*
X190000Y276903D01*
X193921Y277289D01*
X197691Y278433D01*
X201165Y280290D01*
X204211Y282789D01*
X206710Y285835D01*
X208567Y289309D01*
X209711Y293079D01*
X210097Y297000D01*
X209711Y300921D01*
X208567Y304691D01*
X206710Y308165D01*
X204211Y311211D01*
X201165Y313710D01*
X197691Y315567D01*
X193921Y316711D01*
X190000Y317097D01*
X186079Y316711D01*
X182309Y315567D01*
X176798Y323960D01*
X251419Y398581D01*
X252861Y400461D01*
X253768Y402650D01*
X254078Y405000D01*
X254078Y405000D01*
Y422710D01*
X255700Y423794D01*
X265700Y418449D01*
Y416500D01*
X275700D01*
Y430000D01*
X282700D01*
Y416500D01*
X292700D01*
Y416500D01*
X302700Y416634D01*
X302990Y416596D01*
X399804Y319781D01*
X401684Y318339D01*
X403873Y317432D01*
X406223Y317122D01*
X406223Y317122D01*
X460219D01*
X464046Y307884D01*
X462081Y305919D01*
X460639Y304039D01*
X459732Y301849D01*
X459422Y299500D01*
Y295947D01*
X458384Y294083D01*
X451000Y287617D01*
X447476Y287153D01*
X444192Y285792D01*
X441372Y283628D01*
X439208Y280808D01*
X437848Y277524D01*
X437384Y274000D01*
X437474Y273312D01*
X343240Y179078D01*
X255051D01*
X254628Y179628D01*
X251808Y181792D01*
X248524Y183153D01*
X245000Y183616D01*
X241476Y183153D01*
X238192Y181792D01*
X235372Y179628D01*
X233208Y176808D01*
X231847Y173524D01*
X231383Y170000D01*
X231847Y166476D01*
X233208Y163192D01*
X235372Y160372D01*
X238192Y158208D01*
X241476Y156847D01*
X245000Y156383D01*
X248524Y156847D01*
X251808Y158208D01*
X254628Y160372D01*
X255051Y160922D01*
X347000D01*
X349350Y161232D01*
X351539Y162139D01*
X353419Y163581D01*
X449422Y259585D01*
X452330Y259219D01*
X458384Y253917D01*
X459422Y252052D01*
Y229017D01*
X458732Y227349D01*
X458422Y225000D01*
Y151500D01*
Y111983D01*
X440579Y94140D01*
X430295Y95964D01*
X427979Y103612D01*
X428256Y104044D01*
X429565Y107206D01*
X430012Y110600D01*
X429565Y113994D01*
X428256Y117156D01*
X426172Y119872D01*
X423456Y121955D01*
X420294Y123265D01*
X416900Y123712D01*
X413506Y123265D01*
X410344Y121955D01*
X407628Y119872D01*
X405751Y117425D01*
X405545Y117156D01*
X405545Y117156D01*
X404235Y113994D01*
X404096Y113873D01*
X394626Y115381D01*
X394100Y117005D01*
Y131500D01*
X374969D01*
X369100Y131500D01*
X360021Y131197D01*
X359100Y131226D01*
X356200Y131608D01*
X352937Y131178D01*
X350089Y129999D01*
X349896Y129919D01*
X344168Y127915D01*
X342832D01*
X337104Y129919D01*
X336911Y129999D01*
X334063Y131178D01*
X330800Y131608D01*
X327537Y131178D01*
X324689Y129999D01*
X324496Y129919D01*
X318768Y127915D01*
X317432D01*
X311704Y129919D01*
X311511Y129999D01*
X308663Y131178D01*
X305400Y131608D01*
X302137Y131178D01*
X299289Y129999D01*
X299096Y129919D01*
X293368Y127915D01*
X292032D01*
X286304Y129919D01*
X286110Y129999D01*
X283263Y131178D01*
X280000Y131608D01*
X276737Y131178D01*
X273890Y129999D01*
X273696Y129919D01*
X267968Y127915D01*
X266632D01*
X260904Y129919D01*
X260711Y129999D01*
X257863Y131178D01*
X254600Y131608D01*
X251337Y131178D01*
X248296Y129919D01*
X245685Y127915D01*
X243681Y125304D01*
X242422Y122263D01*
X241992Y119000D01*
X242422Y115737D01*
X243681Y112696D01*
X245685Y110085D01*
X248296Y108081D01*
X251337Y106822D01*
X254600Y106392D01*
X257863Y106822D01*
X260711Y108001D01*
X260904Y108081D01*
X266632Y110085D01*
X267968D01*
X273696Y108081D01*
X273890Y108001D01*
X276737Y106822D01*
X280000Y106392D01*
X283263Y106822D01*
X286110Y108001D01*
X286304Y108081D01*
X292032Y110085D01*
X293368D01*
X299096Y108081D01*
X299289Y108001D01*
X302137Y106822D01*
X305400Y106392D01*
X308663Y106822D01*
X311511Y108001D01*
X311704Y108081D01*
X317432Y110085D01*
X318768D01*
X324496Y108081D01*
X324689Y108001D01*
X327537Y106822D01*
X330800Y106392D01*
X334063Y106822D01*
X336911Y108001D01*
X337104Y108081D01*
X342832Y110085D01*
X344168D01*
X349896Y108081D01*
X350089Y108001D01*
X352937Y106822D01*
X356200Y106392D01*
X359100Y106774D01*
X360021Y106803D01*
X369100Y106500D01*
X374969Y106500D01*
X387162Y106500D01*
X394100Y106500D01*
X401250Y101782D01*
X400661Y93062D01*
X398781Y91619D01*
X388581Y81419D01*
X387138Y79539D01*
X386232Y77350D01*
X385922Y75000D01*
Y20000D01*
X380924Y11588D01*
X380250Y10817D01*
X379037Y10000D01*
X78018D01*
X70541Y18662D01*
X70270Y20000D01*
X70598Y25000D01*
X70459Y27122D01*
X70599Y27572D01*
X74176Y32213D01*
X76301Y34210D01*
X76500Y34316D01*
Y50000D01*
Y64055D01*
X73345Y63640D01*
X69940Y62229D01*
X67015Y59985D01*
X64164Y59185D01*
X57318Y58759D01*
X54880Y59314D01*
X52758Y61175D01*
X47799Y64489D01*
X42449Y67127D01*
X36802Y69044D01*
X30952Y70208D01*
X25000Y70598D01*
X20000Y70270D01*
X18662Y70541D01*
X10000Y78018D01*
X10000Y421982D01*
X18662Y429459D01*
X20000Y429730D01*
X25000Y429402D01*
X30952Y429793D01*
X36802Y430956D01*
X42449Y432873D01*
X47799Y435511D01*
X52758Y438825D01*
X54880Y440686D01*
X57318Y441241D01*
X64164Y440815D01*
X67015Y440015D01*
X69940Y437771D01*
X73345Y436360D01*
X77000Y435879D01*
X83000D01*
X86655Y436360D01*
X90060Y437771D01*
X92985Y440015D01*
X95229Y442940D01*
X96640Y446345D01*
X97121Y450000D01*
X96640Y453655D01*
X95229Y457060D01*
X92985Y459985D01*
X90060Y462229D01*
X86655Y463640D01*
X83000Y464121D01*
X79423D01*
X76301Y465790D01*
X74176Y467786D01*
X70599Y472428D01*
X70459Y472878D01*
X70598Y475000D01*
X70270Y480000D01*
X70541Y481338D01*
X78018Y490000D01*
X164097D01*
D02*
G37*
G36*
X411300Y477685D02*
Y451685D01*
X411300D01*
X411502Y441685D01*
X411188Y439300D01*
X411635Y435906D01*
X412944Y432744D01*
X415028Y429492D01*
Y423707D01*
X412944Y420456D01*
X411635Y417294D01*
X411188Y413900D01*
X411635Y410506D01*
X412944Y407344D01*
X415028Y404092D01*
Y398307D01*
X413883Y396520D01*
X403618Y394286D01*
X397078Y399210D01*
Y431000D01*
X396768Y433349D01*
X395862Y435539D01*
X394419Y437419D01*
X374419Y457419D01*
X372539Y458862D01*
X370349Y459768D01*
X368000Y460078D01*
X261000D01*
X258650Y459768D01*
X256461Y458862D01*
X254581Y457419D01*
X238581Y441419D01*
X237139Y439539D01*
X236232Y437350D01*
X235922Y435000D01*
Y411411D01*
X228717Y404247D01*
X219362Y406739D01*
X219229Y407060D01*
X216985Y409985D01*
X214060Y412229D01*
X210655Y413640D01*
X207500Y414055D01*
Y400000D01*
X204000D01*
Y396500D01*
X187340D01*
X187360Y396345D01*
X188771Y392940D01*
X191015Y390015D01*
X193940Y387771D01*
X197345Y386360D01*
X199144Y386124D01*
X201909Y381424D01*
X203041Y375879D01*
X143178Y316016D01*
X135139Y319366D01*
X133547Y320527D01*
X133153Y323524D01*
X131792Y326808D01*
X131437Y327271D01*
X129628Y330568D01*
Y336932D01*
X131437Y340229D01*
X131792Y340692D01*
X133153Y343976D01*
X133616Y347500D01*
X133153Y351024D01*
X131792Y354308D01*
X129628Y357128D01*
X126808Y359292D01*
X126429Y359449D01*
X123556Y367558D01*
X123570Y370733D01*
X146419Y393581D01*
X147861Y395461D01*
X148768Y397650D01*
X149078Y400000D01*
Y436240D01*
X191260Y478422D01*
X401606D01*
X411300Y477685D01*
D02*
G37*
G36*
X484344Y476097D02*
X487971Y468422D01*
X487835Y468093D01*
X487388Y464700D01*
X487835Y461306D01*
X489145Y458144D01*
X491228Y454892D01*
Y449107D01*
X489145Y445856D01*
X487835Y442693D01*
X487388Y439300D01*
X487835Y435906D01*
X489145Y432744D01*
X491228Y429492D01*
Y423707D01*
X489145Y420456D01*
X487835Y417294D01*
X487388Y413900D01*
X487835Y410506D01*
X489145Y407344D01*
X491228Y404092D01*
Y398307D01*
X489145Y395056D01*
X487835Y391894D01*
X487388Y388500D01*
X487835Y385106D01*
X489145Y381944D01*
X491228Y379228D01*
X491566Y378969D01*
X493557Y370432D01*
X493335Y366596D01*
X485817Y359078D01*
X435760D01*
X428927Y365910D01*
X428642Y368208D01*
X430774Y376812D01*
X431410Y377570D01*
X433572Y379228D01*
X435655Y381944D01*
X436921Y385000D01*
X424300D01*
Y392000D01*
X436921D01*
X435655Y395056D01*
X433572Y398307D01*
Y404092D01*
X435655Y407344D01*
X436965Y410506D01*
X437412Y413900D01*
X436965Y417294D01*
X435655Y420456D01*
X433572Y423707D01*
Y429492D01*
X435655Y432744D01*
X436965Y435906D01*
X437412Y439300D01*
X437098Y441685D01*
X437300Y451685D01*
X437300Y451685D01*
Y477685D01*
X446994Y478422D01*
X482039D01*
X484344Y476097D01*
D02*
G37*
G36*
X688391Y490000D02*
X689849Y480000D01*
X688841Y478646D01*
X680904Y473792D01*
X680500Y473763D01*
Y460000D01*
Y446237D01*
X680904Y446208D01*
X688841Y441354D01*
X690000Y439798D01*
Y404002D01*
X688841Y402446D01*
X680904Y397592D01*
X680500Y397562D01*
Y383800D01*
Y370037D01*
X680904Y370008D01*
X688841Y365154D01*
X690000Y363598D01*
Y347455D01*
X687030Y344417D01*
X680000Y340176D01*
X678900Y340321D01*
X675245Y339840D01*
X671840Y338429D01*
X668915Y336185D01*
X663485D01*
X660560Y338429D01*
X657155Y339840D01*
X654671Y340167D01*
X640000Y354838D01*
Y366000D01*
X623226D01*
X614800Y369800D01*
Y380300D01*
X600800D01*
Y387300D01*
X614800D01*
Y397800D01*
X614800Y397800D01*
X614750Y405700D01*
X600800D01*
Y412700D01*
X614460D01*
X614440Y412855D01*
X613029Y416260D01*
X610785Y419185D01*
Y424615D01*
X613029Y427540D01*
X614440Y430945D01*
X614921Y434600D01*
X614440Y438255D01*
X613029Y441660D01*
X610785Y444585D01*
Y450015D01*
X613029Y452940D01*
X614440Y456345D01*
X614921Y460000D01*
X614440Y463655D01*
X613029Y467060D01*
X610785Y469985D01*
X607860Y472229D01*
X604455Y473640D01*
X600800Y474121D01*
X597145Y473640D01*
X596475Y473362D01*
X589076Y480761D01*
X592903Y490000D01*
X688391D01*
D02*
G37*
G36*
X671840Y161571D02*
X675245Y160160D01*
X678900Y159679D01*
X680000Y159824D01*
X687030Y155583D01*
X690000Y152545D01*
Y136402D01*
X688841Y134846D01*
X680904Y129992D01*
X680500Y129963D01*
Y116200D01*
Y102437D01*
X680904Y102408D01*
X688841Y97554D01*
X690000Y95998D01*
Y60202D01*
X688841Y58646D01*
X680904Y53792D01*
X680500Y53762D01*
Y40000D01*
Y26237D01*
X680904Y26208D01*
X688841Y21354D01*
X689849Y20000D01*
X688391Y10000D01*
X590472D01*
X587271Y19010D01*
X593995Y26000D01*
X614800D01*
Y37422D01*
X618500D01*
X620849Y37732D01*
X623039Y38639D01*
X624919Y40081D01*
X625838Y41000D01*
X637000D01*
Y59000D01*
Y81000D01*
X625838D01*
X614751Y92086D01*
X614440Y94455D01*
X613029Y97860D01*
X610785Y100785D01*
Y106215D01*
X613029Y109140D01*
X614440Y112545D01*
X614921Y116200D01*
X614440Y119855D01*
X613029Y123260D01*
X610785Y126185D01*
X607860Y128429D01*
X604455Y129840D01*
X600800Y130321D01*
X597145Y129840D01*
X593740Y128429D01*
X590815Y126185D01*
X590119Y125278D01*
X555060D01*
X530251Y150087D01*
X530221Y153207D01*
X532930Y160900D01*
X542233Y163571D01*
X544840Y161571D01*
X548245Y160160D01*
X551900Y159679D01*
X555555Y160160D01*
X558960Y161571D01*
X561885Y163815D01*
X567315D01*
X570240Y161571D01*
X573645Y160160D01*
X577300Y159679D01*
X580955Y160160D01*
X584360Y161571D01*
X587285Y163815D01*
X592715D01*
X595640Y161571D01*
X599045Y160160D01*
X602700Y159679D01*
X606355Y160160D01*
X609760Y161571D01*
X612685Y163815D01*
X618115D01*
X621040Y161571D01*
X624445Y160160D01*
X628100Y159679D01*
X631755Y160160D01*
X635160Y161571D01*
X638085Y163815D01*
X643515D01*
X646440Y161571D01*
X649845Y160160D01*
X653500Y159679D01*
X657155Y160160D01*
X660560Y161571D01*
X663485Y163815D01*
X668915D01*
X671840Y161571D01*
D02*
G37*
G36*
X528146Y99016D02*
X528372Y97958D01*
X528270Y95509D01*
X527125Y88971D01*
X526120Y87198D01*
X525977Y87043D01*
X520581Y81647D01*
X519138Y79767D01*
X518232Y77577D01*
X518090Y76500D01*
X514000D01*
Y76500D01*
X507668Y77098D01*
X505281Y80637D01*
X505721Y81700D01*
X493100D01*
X480479D01*
X481745Y78644D01*
X477582Y69405D01*
X477173Y68978D01*
X465000D01*
X462650Y68668D01*
X461919Y68365D01*
X456073Y69816D01*
X452700Y80586D01*
X468694Y96579D01*
X480087Y94622D01*
X481698Y91837D01*
X481629Y91477D01*
X480479Y88700D01*
X493100D01*
X505721D01*
X504455Y91756D01*
X502372Y95007D01*
Y100793D01*
X504455Y104044D01*
X505765Y107206D01*
X505771Y107249D01*
X514258Y111112D01*
X515943Y111219D01*
X528146Y99016D01*
D02*
G37*
%LPC*%
G36*
X83500Y414055D02*
Y403500D01*
X96660D01*
X96640Y403655D01*
X95229Y407060D01*
X92985Y409985D01*
X90060Y412229D01*
X86655Y413640D01*
X83500Y414055D01*
D02*
G37*
G36*
X76500Y414055D02*
X73345Y413640D01*
X69940Y412229D01*
X67015Y409985D01*
X64771Y407060D01*
X63360Y403655D01*
X63340Y403500D01*
X76500D01*
Y414055D01*
D02*
G37*
G36*
X96660Y396500D02*
X83500D01*
Y385945D01*
X86655Y386360D01*
X90060Y387771D01*
X92985Y390015D01*
X95229Y392940D01*
X96640Y396345D01*
X96660Y396500D01*
D02*
G37*
G36*
X76500D02*
X63340D01*
X63360Y396345D01*
X64771Y392940D01*
X67015Y390015D01*
X69940Y387771D01*
X73345Y386360D01*
X76500Y385945D01*
Y396500D01*
D02*
G37*
G36*
X92500Y195700D02*
X83500D01*
Y186700D01*
X92500D01*
Y195700D01*
D02*
G37*
G36*
X76500D02*
X67500D01*
Y186700D01*
X76500D01*
Y195700D01*
D02*
G37*
G36*
X190000Y223097D02*
X186079Y222711D01*
X182309Y221567D01*
X178835Y219710D01*
X175789Y217211D01*
X173290Y214165D01*
X171433Y210691D01*
X170289Y206921D01*
X169903Y203000D01*
X170289Y199079D01*
X171433Y195309D01*
X173290Y191835D01*
X175789Y188789D01*
X178835Y186290D01*
X182309Y184433D01*
X186079Y183289D01*
X190000Y182903D01*
X193921Y183289D01*
X197691Y184433D01*
X201165Y186290D01*
X204211Y188789D01*
X206710Y191835D01*
X208567Y195309D01*
X209711Y199079D01*
X210097Y203000D01*
X209711Y206921D01*
X208567Y210691D01*
X206710Y214165D01*
X204211Y217211D01*
X201165Y219710D01*
X197691Y221567D01*
X193921Y222711D01*
X190000Y223097D01*
D02*
G37*
G36*
X83000Y114121D02*
X77000D01*
X73345Y113640D01*
X69940Y112229D01*
X67015Y109985D01*
X64771Y107060D01*
X63360Y103655D01*
X62879Y100000D01*
X63360Y96345D01*
X64771Y92940D01*
X67015Y90015D01*
X69940Y87771D01*
X73345Y86360D01*
X77000Y85879D01*
X83000D01*
X86655Y86360D01*
X90060Y87771D01*
X92985Y90015D01*
X95229Y92940D01*
X96640Y96345D01*
X97121Y100000D01*
X96640Y103655D01*
X95229Y107060D01*
X92985Y109985D01*
X90060Y112229D01*
X86655Y113640D01*
X83000Y114121D01*
D02*
G37*
G36*
X340000Y75000D02*
X330000D01*
Y66000D01*
X340000D01*
Y75000D01*
D02*
G37*
G36*
X207500Y64055D02*
Y53500D01*
X220660D01*
X220640Y53655D01*
X219229Y57060D01*
X216985Y59985D01*
X214060Y62229D01*
X210655Y63640D01*
X207500Y64055D01*
D02*
G37*
G36*
X83500D02*
Y53500D01*
X96660D01*
X96640Y53655D01*
X95229Y57060D01*
X92985Y59985D01*
X90060Y62229D01*
X86655Y63640D01*
X83500Y64055D01*
D02*
G37*
G36*
X200500D02*
X197345Y63640D01*
X193940Y62229D01*
X191015Y59985D01*
X188771Y57060D01*
X187360Y53655D01*
X187340Y53500D01*
X200500D01*
Y64055D01*
D02*
G37*
G36*
X220660Y46500D02*
X207500D01*
Y35945D01*
X210655Y36360D01*
X214060Y37771D01*
X216985Y40015D01*
X219229Y42940D01*
X220640Y46345D01*
X220660Y46500D01*
D02*
G37*
G36*
X96660D02*
X83500D01*
Y35945D01*
X86655Y36360D01*
X90060Y37771D01*
X92985Y40015D01*
X95229Y42940D01*
X96640Y46345D01*
X96660Y46500D01*
D02*
G37*
G36*
X200500D02*
X187340D01*
X187360Y46345D01*
X188771Y42940D01*
X191015Y40015D01*
X193940Y37771D01*
X197345Y36360D01*
X200500Y35945D01*
Y46500D01*
D02*
G37*
G36*
X207000Y114121D02*
X201000D01*
X197345Y113640D01*
X193940Y112229D01*
X191015Y109985D01*
X188771Y107060D01*
X187360Y103655D01*
X186879Y100000D01*
X187360Y96345D01*
X188771Y92940D01*
X191015Y90015D01*
X193940Y87771D01*
X197345Y86360D01*
X201000Y85879D01*
X207000D01*
X210655Y86360D01*
X212609Y87170D01*
X216265Y85727D01*
X221499Y80874D01*
X221384Y80000D01*
X221847Y76476D01*
X223208Y73192D01*
X225372Y70372D01*
X226553Y69465D01*
X227139Y68051D01*
X228581Y66171D01*
X249671Y45081D01*
X251433Y43729D01*
X253437Y41725D01*
Y33750D01*
X253687Y31857D01*
X254417Y30094D01*
X255579Y28579D01*
X257094Y27417D01*
X258857Y26687D01*
X260750Y26437D01*
X279250D01*
X281143Y26687D01*
X282906Y27417D01*
X284421Y28579D01*
X285583Y30094D01*
X285926Y30922D01*
X295000D01*
X297349Y31232D01*
X299539Y32139D01*
X301419Y33581D01*
X302838Y35000D01*
X315000Y35000D01*
Y35000D01*
X315000D01*
Y35000D01*
X340000D01*
Y52000D01*
Y61000D01*
X327500D01*
Y63500D01*
X325000D01*
Y75000D01*
X325000D01*
X315000Y75000D01*
Y75000D01*
X315000D01*
Y75000D01*
X305000D01*
Y63500D01*
X300000D01*
Y75000D01*
X290000D01*
X284421Y81421D01*
X282906Y82583D01*
X281143Y83313D01*
X279250Y83562D01*
X272500D01*
Y69500D01*
X267500D01*
Y83562D01*
X260750D01*
X258857Y83313D01*
X258297Y83081D01*
X254986Y83226D01*
X249883Y84782D01*
X246903Y86541D01*
X246792Y86808D01*
X244628Y89628D01*
X241808Y91792D01*
X238524Y93153D01*
X235000Y93616D01*
X231476Y93153D01*
X229865Y92485D01*
X225703Y94227D01*
X220970Y98856D01*
X221121Y100000D01*
X220640Y103655D01*
X219229Y107060D01*
X216985Y109985D01*
X214060Y112229D01*
X210655Y113640D01*
X207000Y114121D01*
D02*
G37*
G36*
Y464121D02*
X201000D01*
X197345Y463640D01*
X193940Y462229D01*
X191015Y459985D01*
X188771Y457060D01*
X187360Y453655D01*
X186879Y450000D01*
X187360Y446345D01*
X188771Y442940D01*
X191015Y440015D01*
X193940Y437771D01*
X197345Y436360D01*
X201000Y435879D01*
X207000D01*
X210655Y436360D01*
X214060Y437771D01*
X216985Y440015D01*
X219229Y442940D01*
X220640Y446345D01*
X221121Y450000D01*
X220640Y453655D01*
X219229Y457060D01*
X216985Y459985D01*
X214060Y462229D01*
X210655Y463640D01*
X207000Y464121D01*
D02*
G37*
G36*
X200500Y414055D02*
X197345Y413640D01*
X193940Y412229D01*
X191015Y409985D01*
X188771Y407060D01*
X187360Y403655D01*
X187340Y403500D01*
X200500D01*
Y414055D01*
D02*
G37*
G36*
X673500Y473660D02*
X673345Y473640D01*
X669940Y472229D01*
X667015Y469985D01*
X664771Y467060D01*
X663360Y463655D01*
X663340Y463500D01*
X673500D01*
Y473660D01*
D02*
G37*
G36*
Y456500D02*
X663340D01*
X663360Y456345D01*
X664771Y452940D01*
X667015Y450015D01*
X669940Y447771D01*
X673345Y446360D01*
X673500Y446340D01*
Y456500D01*
D02*
G37*
G36*
Y397460D02*
X673345Y397440D01*
X669940Y396029D01*
X667015Y393785D01*
X664771Y390860D01*
X663360Y387455D01*
X663340Y387300D01*
X673500D01*
Y397460D01*
D02*
G37*
G36*
Y380300D02*
X663340D01*
X663360Y380145D01*
X664771Y376740D01*
X667015Y373815D01*
X669940Y371571D01*
X673345Y370160D01*
X673500Y370140D01*
Y380300D01*
D02*
G37*
G36*
X673500Y129860D02*
X673345Y129840D01*
X669940Y128429D01*
X667015Y126185D01*
X664771Y123260D01*
X663360Y119855D01*
X663340Y119700D01*
X673500D01*
Y129860D01*
D02*
G37*
G36*
Y112700D02*
X663340D01*
X663360Y112545D01*
X664771Y109140D01*
X667015Y106215D01*
X669940Y103971D01*
X673345Y102560D01*
X673500Y102540D01*
Y112700D01*
D02*
G37*
G36*
X673500Y53660D02*
X673345Y53640D01*
X669940Y52229D01*
X667015Y49985D01*
X664771Y47060D01*
X663360Y43655D01*
X663340Y43500D01*
X673500D01*
Y53660D01*
D02*
G37*
G36*
Y36500D02*
X663340D01*
X663360Y36345D01*
X664771Y32940D01*
X667015Y30015D01*
X669940Y27771D01*
X673345Y26360D01*
X673500Y26340D01*
Y36500D01*
D02*
G37*
%LPD*%
D19*
X537000Y435000D02*
D03*
X555000D02*
D03*
X537000Y470000D02*
D03*
X555000D02*
D03*
X549000Y97000D02*
D03*
X567000D02*
D03*
X517000Y256000D02*
D03*
X499000D02*
D03*
X537000Y256000D02*
D03*
X555000D02*
D03*
X611000Y354000D02*
D03*
X629000D02*
D03*
X447500Y35000D02*
D03*
X465500D02*
D03*
D20*
X555000Y380000D02*
D03*
X572000D02*
D03*
X555000Y405000D02*
D03*
X572000D02*
D03*
X542500Y64000D02*
D03*
X525500D02*
D03*
X465000Y55000D02*
D03*
X448000D02*
D03*
D21*
X327500Y63500D02*
D03*
Y46500D02*
D03*
X302500Y63500D02*
D03*
Y46500D02*
D03*
D22*
X625000Y52000D02*
D03*
Y70000D02*
D03*
D43*
X535437Y470750D02*
X537000Y469187D01*
X546000Y94000D02*
X550000D01*
X507500Y132500D02*
X546000Y94000D01*
X500000Y255000D02*
Y285000D01*
X499000Y252000D02*
X501000Y254000D01*
X235000Y72590D02*
X256090Y51500D01*
X235000Y72590D02*
Y80000D01*
X256090Y51500D02*
X256500D01*
X267500Y40500D01*
X270000D01*
X270500Y40000D02*
X295000D01*
X270000Y40500D02*
X270500Y40000D01*
X295000D02*
X301500Y46500D01*
X302500D01*
X327500D01*
X245000Y170000D02*
X347000D01*
X451000Y274000D01*
X467500Y225000D02*
X468500Y226000D01*
X467500Y151500D02*
Y225000D01*
X468500Y226000D02*
Y299500D01*
X571000Y405000D02*
X572000Y404000D01*
X100000Y360000D02*
X140000Y400000D01*
X100000Y268130D02*
Y360000D01*
Y268130D02*
X118130Y250000D01*
X120000D01*
Y225000D02*
Y250000D01*
X593800Y460000D02*
Y463200D01*
X574999Y198000D02*
Y247999D01*
X663000Y305000D02*
X678900Y320900D01*
X618000Y305000D02*
X663000D01*
X575000Y262000D02*
X618000Y305000D01*
X575000Y248000D02*
Y262000D01*
X140000Y400000D02*
Y440000D01*
X611000Y354000D02*
Y359000D01*
X600000Y370000D02*
X611000Y359000D01*
X600000Y370000D02*
Y381000D01*
X586000Y356000D02*
X600000Y370000D01*
X554000Y356000D02*
X586000D01*
X526500Y328500D02*
X554000Y356000D01*
X569500Y487500D02*
X593800Y463200D01*
X187500Y487500D02*
X569500D01*
X499000Y386000D02*
X502000Y389000D01*
X561400Y467000D02*
X593800Y434600D01*
X557000Y467000D02*
X561400D01*
X588200Y409200D02*
X600800D01*
X584000Y405000D02*
X588200Y409200D01*
X572000Y405000D02*
X584000D01*
X542500Y63000D02*
X566000D01*
X593800Y90800D01*
X587913Y62913D02*
X591313D01*
X572500Y47500D02*
X587913Y62913D01*
X532272Y47500D02*
X572500D01*
X579800Y26000D02*
X597300Y43500D01*
X624000Y52000D02*
X625000D01*
X618500Y46500D02*
X624000Y52000D01*
X607300Y46500D02*
X618500D01*
X600800Y40000D02*
X607300Y46500D01*
X624000Y70000D02*
X625000D01*
X603200Y90800D02*
X624000Y70000D01*
X600800Y90800D02*
X603200D01*
X596287Y88313D02*
X600687D01*
X591900Y381900D02*
X592000Y382000D01*
X526500Y326200D02*
Y328500D01*
X495200Y326200D02*
X501100D01*
X577300Y331144D02*
Y331200D01*
X555000Y271000D02*
X580000Y296000D01*
X555000Y256000D02*
Y271000D01*
X611000Y354000D02*
X613000Y356000D01*
X573900Y381900D02*
X591900D01*
X572000Y380000D02*
X573900Y381900D01*
X629000Y353000D02*
Y354000D01*
Y353000D02*
X653500Y328500D01*
Y326200D02*
Y328500D01*
X527000Y388000D02*
Y425000D01*
X526577Y387000D02*
X528000D01*
X432000Y350000D02*
X489577D01*
X527000Y425000D02*
X537000Y435000D01*
X527000Y388000D02*
X528000Y387000D01*
X535617Y432383D02*
X535999Y432000D01*
X489577Y350000D02*
X526577Y387000D01*
X481500Y291500D02*
X495000Y305000D01*
X500000Y255000D02*
X501000Y254000D01*
X481500Y165823D02*
Y291500D01*
X495000Y305000D02*
X558277D01*
X468500Y299500D02*
X495200Y326200D01*
X499000Y252000D02*
X499000Y252000D01*
X543999Y188217D02*
Y231999D01*
X542500Y186718D02*
X543999Y188217D01*
X542500Y186718D02*
X551900Y177318D01*
X368000Y451000D02*
X388000Y431000D01*
X261000Y451000D02*
X368000D01*
X245000Y435000D02*
X261000Y451000D01*
X388000Y394000D02*
Y431000D01*
Y394000D02*
X432000Y350000D01*
X525000Y175000D02*
X526500Y176500D01*
X520000Y219563D02*
Y224000D01*
Y219563D02*
X526500Y213063D01*
X500499Y174401D02*
X501100Y173800D01*
X500499Y174401D02*
Y231248D01*
X515000Y245749D01*
Y254000D01*
X517000Y256000D01*
X526500Y176500D02*
Y213063D01*
X551900Y173800D02*
Y177318D01*
X537000Y256000D02*
X537000Y256000D01*
X517000Y256000D02*
X537000D01*
X497323Y150000D02*
X517500D01*
X551300Y116200D01*
X481500Y165823D02*
X497323Y150000D01*
X551300Y116200D02*
X593800D01*
X571000Y405000D02*
X572000D01*
Y380000D02*
Y404000D01*
X555000Y379000D02*
Y380000D01*
X552500Y376500D02*
X555000Y379000D01*
X548415Y376500D02*
X552500D01*
X501100Y329185D02*
X548415Y376500D01*
X501100Y326200D02*
Y329185D01*
X140000Y440000D02*
X187500Y487500D01*
X245000Y405000D02*
Y435000D01*
X120000Y280000D02*
X245000Y405000D01*
X120000Y275000D02*
Y280000D01*
X555000Y469000D02*
Y470000D01*
Y469000D02*
X557000Y467000D01*
X406223Y326200D02*
X495200D01*
X304600Y427823D02*
X406223Y326200D01*
X304600Y427823D02*
Y430000D01*
X555000Y405000D02*
Y435000D01*
X506550Y470750D02*
X535437D01*
X500500Y464700D02*
X506550Y470750D01*
X537000Y469187D02*
Y470000D01*
Y460728D02*
Y469187D01*
Y460728D02*
X553000Y444728D01*
Y437000D02*
Y444728D01*
Y437000D02*
X555000Y435000D01*
X554000Y384000D02*
Y405000D01*
Y405000D02*
X554000Y405000D01*
X554000Y405000D02*
X554000Y405000D01*
X465000Y30000D02*
Y55000D01*
X448000Y35000D02*
Y55000D01*
X467500Y108223D02*
Y151500D01*
X419077Y59800D02*
X467500Y108223D01*
X416900Y59800D02*
X419077D01*
X432900Y14900D02*
X447000Y29000D01*
X400100Y14900D02*
X432900D01*
X395000Y20000D02*
X400100Y14900D01*
X395000Y20000D02*
Y75000D01*
X405200Y85200D01*
X416900D01*
X447000Y29000D02*
Y30000D01*
X448000Y31000D01*
Y35000D01*
X465000Y59900D02*
X493000D01*
X495587Y57313D01*
X467500Y151500D02*
X486500Y132500D01*
X574999Y247999D02*
X575000Y248000D01*
X678900Y320900D02*
Y326200D01*
X504075Y57313D02*
X535387Y26000D01*
X579800D01*
X527000Y52772D02*
X532272Y47500D01*
X527000Y52772D02*
Y62500D01*
X525500Y64000D02*
X527000Y62500D01*
X495587Y57313D02*
X504075D01*
X567000Y96000D02*
Y97000D01*
X564000Y93000D02*
X567000Y96000D01*
X564000Y86272D02*
Y93000D01*
X558478Y80750D02*
X564000Y86272D01*
X532522Y80750D02*
X558478D01*
X527000Y75228D02*
X532522Y80750D01*
X527000Y65500D02*
Y75228D01*
X525500Y64000D02*
X527000Y65500D01*
X593800Y90800D02*
X596287Y88313D01*
X591313Y62913D02*
X593800Y65400D01*
X486500Y132500D02*
X507500D01*
X500500Y388500D02*
X501000Y389000D01*
X499000Y386000D02*
X499000D01*
X502000Y389000D02*
X506000D01*
X558277Y305000D02*
X577300Y324023D01*
Y326200D01*
X571000Y405000D02*
X571000Y405000D01*
X501000Y389000D02*
X502000D01*
D44*
X650000Y250000D02*
D03*
X25000Y475000D02*
D03*
Y25000D02*
D03*
D45*
X120000Y347500D02*
D03*
Y320000D02*
D03*
X355400Y430000D02*
D03*
X330000D02*
D03*
X304600D02*
D03*
X587000Y299000D02*
D03*
X235000Y80000D02*
D03*
X245000Y170000D02*
D03*
X574999Y198000D02*
D03*
X528000Y387000D02*
D03*
X500000Y285000D02*
D03*
X543999Y231999D02*
D03*
X451000Y274000D02*
D03*
X520000Y224000D02*
D03*
D46*
X80000Y450000D02*
D03*
X204000D02*
D03*
X80000Y400000D02*
D03*
X204000D02*
D03*
X80000Y100000D02*
D03*
X204000D02*
D03*
X80000Y50000D02*
D03*
X204000D02*
D03*
D47*
X80000Y300800D02*
D03*
Y275400D02*
D03*
Y250000D02*
D03*
Y224600D02*
D03*
X254600Y119000D02*
D03*
X356200D02*
D03*
X330800D02*
D03*
X305400D02*
D03*
X280000D02*
D03*
D48*
X80000Y199200D02*
D03*
D49*
X381600Y119000D02*
D03*
D50*
X190000Y203000D02*
D03*
Y297000D02*
D03*
D51*
X120000Y225000D02*
D03*
Y250000D02*
D03*
Y275000D02*
D03*
D52*
X677000Y383800D02*
D03*
Y460000D02*
D03*
X600800D02*
D03*
Y434600D02*
D03*
Y409200D02*
D03*
X677000Y40000D02*
D03*
Y116200D02*
D03*
X600800D02*
D03*
Y90800D02*
D03*
Y65400D02*
D03*
X678900Y326200D02*
D03*
X653500D02*
D03*
X628100D02*
D03*
X602700D02*
D03*
X577300D02*
D03*
X551900D02*
D03*
X526500D02*
D03*
X501100D02*
D03*
X678900Y173800D02*
D03*
X653500D02*
D03*
X628100D02*
D03*
X602700D02*
D03*
X577300D02*
D03*
X551900D02*
D03*
X526500D02*
D03*
X501100D02*
D03*
D53*
X600800Y383800D02*
D03*
Y40000D02*
D03*
D54*
X416900Y34400D02*
D03*
Y59800D02*
D03*
Y85200D02*
D03*
Y110600D02*
D03*
X493100D02*
D03*
Y85200D02*
D03*
Y59800D02*
D03*
D55*
Y34400D02*
D03*
D56*
X279200Y430000D02*
D03*
D57*
X424300Y464684D02*
D03*
D58*
Y439300D02*
D03*
Y413900D02*
D03*
Y388500D02*
D03*
X500500D02*
D03*
Y413900D02*
D03*
Y439300D02*
D03*
Y464700D02*
D03*
D59*
X270000Y69500D02*
D03*
Y40500D02*
D03*
M02*

View File

@@ -0,0 +1,456 @@
G04*
G04 #@! TF.GenerationSoftware,Altium Limited,Altium Designer,19.1.9 (167)*
G04*
G04 Layer_Color=32896*
%FSLAX44Y44*%
%MOMM*%
G71*
G01*
G75*
%ADD11C,0.2540*%
%ADD12C,0.2000*%
%ADD14C,0.1000*%
%ADD61C,0.1270*%
D11*
X269843Y100196D02*
X272383Y102735D01*
X277461D01*
X280000Y100196D01*
Y90039D01*
X277461Y87500D01*
X272383D01*
X269843Y90039D01*
X264765Y87500D02*
X259687D01*
X262226D01*
Y102735D01*
X264765Y100196D01*
X679000Y347000D02*
Y362235D01*
X671383D01*
X668843Y359696D01*
Y354618D01*
X671383Y352078D01*
X679000D01*
X673922D02*
X668843Y347000D01*
X663765D02*
X658687D01*
X661226D01*
Y362235D01*
X663765Y359696D01*
X651069D02*
X648530Y362235D01*
X643452D01*
X640912Y359696D01*
Y349539D01*
X643452Y347000D01*
X648530D01*
X651069Y349539D01*
Y359696D01*
X567000Y274000D02*
Y289235D01*
X559383D01*
X556843Y286696D01*
Y281618D01*
X559383Y279078D01*
X567000D01*
X561922D02*
X556843Y274000D01*
X551765Y276539D02*
X549226Y274000D01*
X544147D01*
X541608Y276539D01*
Y286696D01*
X544147Y289235D01*
X549226D01*
X551765Y286696D01*
Y284157D01*
X549226Y281618D01*
X541608D01*
X534000Y274000D02*
Y289235D01*
X526382D01*
X523843Y286696D01*
Y281618D01*
X526382Y279078D01*
X534000D01*
X528922D02*
X523843Y274000D01*
X518765Y286696D02*
X516226Y289235D01*
X511147D01*
X508608Y286696D01*
Y284157D01*
X511147Y281618D01*
X508608Y279078D01*
Y276539D01*
X511147Y274000D01*
X516226D01*
X518765Y276539D01*
Y279078D01*
X516226Y281618D01*
X518765Y284157D01*
Y286696D01*
X516226Y281618D02*
X511147D01*
X569843Y68696D02*
X572383Y71235D01*
X577461D01*
X580000Y68696D01*
Y58539D01*
X577461Y56000D01*
X572383D01*
X569843Y58539D01*
X564765Y68696D02*
X562226Y71235D01*
X557147D01*
X554608Y68696D01*
Y66157D01*
X557147Y63618D01*
X554608Y61078D01*
Y58539D01*
X557147Y56000D01*
X562226D01*
X564765Y58539D01*
Y61078D01*
X562226Y63618D01*
X564765Y66157D01*
Y68696D01*
X562226Y63618D02*
X557147D01*
X648000Y65696D02*
X645461Y68235D01*
X640383D01*
X637843Y65696D01*
Y55539D01*
X640383Y53000D01*
X645461D01*
X648000Y55539D01*
Y65696D01*
X469000Y7500D02*
Y22735D01*
X461382D01*
X458843Y20196D01*
Y15117D01*
X461382Y12578D01*
X469000D01*
X463922D02*
X458843Y7500D01*
X443608Y22735D02*
X453765D01*
Y15117D01*
X448687D01*
X453765D01*
Y7500D01*
X456343Y80196D02*
X458882Y82735D01*
X463961D01*
X466500Y80196D01*
Y70039D01*
X463961Y67500D01*
X458882D01*
X456343Y70039D01*
X441108Y82735D02*
X451265D01*
Y75118D01*
X446187D01*
X451265D01*
Y67500D01*
X535500Y89000D02*
Y104235D01*
X527882D01*
X525343Y101696D01*
Y96618D01*
X527882Y94078D01*
X535500D01*
X530422D02*
X525343Y89000D01*
X520265Y101696D02*
X517726Y104235D01*
X512647D01*
X510108Y101696D01*
Y99157D01*
X512647Y96618D01*
X515187D01*
X512647D01*
X510108Y94078D01*
Y91539D01*
X512647Y89000D01*
X517726D01*
X520265Y91539D01*
X529843Y412696D02*
X532383Y415235D01*
X537461D01*
X540000Y412696D01*
Y402539D01*
X537461Y400000D01*
X532383D01*
X529843Y402539D01*
X514608Y415235D02*
X524765D01*
Y407617D01*
X519687Y410157D01*
X517147D01*
X514608Y407617D01*
Y402539D01*
X517147Y400000D01*
X522226D01*
X524765Y402539D01*
X529843Y387696D02*
X532383Y390235D01*
X537461D01*
X540000Y387696D01*
Y377539D01*
X537461Y375000D01*
X532383D01*
X529843Y377539D01*
X517147Y375000D02*
Y390235D01*
X524765Y382617D01*
X514608D01*
D12*
X619000Y61000D02*
X631000D01*
X546000Y429000D02*
Y441000D01*
Y464000D02*
Y476000D01*
X558000Y91000D02*
Y103000D01*
X508000Y250000D02*
Y262000D01*
X546000Y250000D02*
Y262000D01*
X620000Y348000D02*
Y360000D01*
X456500Y29000D02*
Y41000D01*
X328335Y88331D02*
X330002Y89997D01*
X333334D01*
X335000Y88331D01*
Y81666D01*
X333334Y80000D01*
X330002D01*
X328335Y81666D01*
X325003Y89997D02*
X318339D01*
Y88331D01*
X325003Y81666D01*
Y80000D01*
X303335Y88331D02*
X305002Y89997D01*
X308334D01*
X310000Y88331D01*
Y81666D01*
X308334Y80000D01*
X305002D01*
X303335Y81666D01*
X293339Y89997D02*
X296671Y88331D01*
X300003Y84998D01*
Y81666D01*
X298337Y80000D01*
X295005D01*
X293339Y81666D01*
Y83332D01*
X295005Y84998D01*
X300003D01*
X583000Y473000D02*
Y482997D01*
X578002D01*
X576335Y481331D01*
Y477998D01*
X578002Y476332D01*
X583000D01*
X579668D02*
X576335Y473000D01*
X566339D02*
X573003D01*
X566339Y479664D01*
Y481331D01*
X568005Y482997D01*
X571337D01*
X573003Y481331D01*
X580000Y443000D02*
Y452997D01*
X575002D01*
X573335Y451331D01*
Y447998D01*
X575002Y446332D01*
X580000D01*
X576668D02*
X573335Y443000D01*
X570003D02*
X566671D01*
X568337D01*
Y452997D01*
X570003Y451331D01*
D14*
X337660Y37474D02*
Y72526D01*
X330040D02*
X337660D01*
X330040Y37474D02*
X337660D01*
X317340Y72526D02*
X324960D01*
X317340Y37474D02*
X324960D01*
X317340D02*
Y72526D01*
X312660Y37474D02*
Y72526D01*
X305040D02*
X312660D01*
X305040Y37474D02*
X312660D01*
X292340Y72526D02*
X299960D01*
X292340Y37474D02*
X299960D01*
X292340D02*
Y72526D01*
X634652Y43474D02*
X634652Y57190D01*
X615348Y43474D02*
X634652D01*
X615348Y57190D02*
X615348Y43474D01*
X634652Y64810D02*
Y78526D01*
X615348D02*
X634652D01*
X615348Y64810D02*
Y78526D01*
X545974Y390160D02*
X581026D01*
X545974Y382540D02*
Y390160D01*
X581026Y382540D02*
Y390160D01*
X545974Y369840D02*
Y377460D01*
X581026Y369840D02*
Y377460D01*
X545974Y369840D02*
X581026D01*
X545974Y415160D02*
X581026D01*
X545974Y407540D02*
Y415160D01*
X581026Y407540D02*
Y415160D01*
X545974Y394840D02*
Y402460D01*
X581026Y394840D02*
Y402460D01*
X545974Y394840D02*
X581026D01*
X516474Y53840D02*
X551526D01*
Y61460D01*
X516474Y53840D02*
Y61460D01*
X551526Y66540D02*
Y74160D01*
X516474Y66540D02*
Y74160D01*
X551526D01*
X438974Y44840D02*
X474026D01*
Y52460D01*
X438974Y44840D02*
Y52460D01*
X474026Y57540D02*
Y65160D01*
X438974Y57540D02*
Y65160D01*
X474026D01*
X528474Y425348D02*
X542190Y425348D01*
X528474Y425348D02*
Y444652D01*
X542190Y444652D01*
X549810Y425348D02*
X563526D01*
Y444652D01*
X549810D02*
X563526D01*
X528474Y460348D02*
X542190Y460348D01*
X528474Y460348D02*
Y479652D01*
X542190Y479652D01*
X549810Y460348D02*
X563526D01*
Y479652D01*
X549810D02*
X563526D01*
X540474Y87348D02*
X554190Y87348D01*
X540474Y87348D02*
Y106652D01*
X554190Y106652D01*
X561810Y87348D02*
X575526D01*
Y106652D01*
X561810D02*
X575526D01*
X511810Y265652D02*
X525526Y265652D01*
Y246348D02*
Y265652D01*
X511810Y246348D02*
X525526Y246348D01*
X490474Y265652D02*
X504190D01*
X490474Y246348D02*
Y265652D01*
Y246348D02*
X504190D01*
X528474Y246348D02*
X542190Y246348D01*
X528474Y246348D02*
Y265652D01*
X542190Y265652D01*
X549810Y246348D02*
X563526D01*
Y265652D01*
X549810D02*
X563526D01*
X602474Y344348D02*
X616190Y344348D01*
X602474Y344348D02*
Y363652D01*
X616190Y363652D01*
X623810Y344348D02*
X637526D01*
Y363652D01*
X623810D02*
X637526D01*
X438974Y25348D02*
X452690Y25348D01*
X438974Y25348D02*
Y44652D01*
X452690Y44652D01*
X460310Y25348D02*
X474026D01*
Y44652D01*
X460310D02*
X474026D01*
D61*
X255000Y40000D02*
Y66000D01*
X285000Y42000D02*
X288000D01*
Y40000D02*
Y42000D01*
X285000Y40000D02*
X288000D01*
X285000Y41000D02*
X288000D01*
X285000Y40000D02*
Y66000D01*
M02*

View File

@@ -0,0 +1,89 @@
G04*
G04 #@! TF.GenerationSoftware,Altium Limited,Altium Designer,19.1.9 (167)*
G04*
G04 Layer_Color=128*
%FSLAX44Y44*%
%MOMM*%
G71*
G01*
G75*
%ADD19R,1.2000X1.4000*%
%ADD20R,1.3000X1.5000*%
%ADD21R,1.5000X1.3000*%
%ADD22R,1.4000X1.2000*%
G04:AMPARAMS|DCode=59|XSize=2.3mm|YSize=1.8mm|CornerRadius=0.225mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=0.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
%AMROUNDEDRECTD59*
21,1,2.3000,1.3500,0,0,0.0*
21,1,1.8500,1.8000,0,0,0.0*
1,1,0.4500,0.9250,-0.6750*
1,1,0.4500,-0.9250,-0.6750*
1,1,0.4500,-0.9250,0.6750*
1,1,0.4500,0.9250,0.6750*
%
%ADD59ROUNDEDRECTD59*%
D19*
X537000Y435000D02*
D03*
X555000D02*
D03*
X537000Y470000D02*
D03*
X555000D02*
D03*
X549000Y97000D02*
D03*
X567000D02*
D03*
X517000Y256000D02*
D03*
X499000D02*
D03*
X537000Y256000D02*
D03*
X555000D02*
D03*
X611000Y354000D02*
D03*
X629000D02*
D03*
X447500Y35000D02*
D03*
X465500D02*
D03*
D20*
X555000Y380000D02*
D03*
X572000D02*
D03*
X555000Y405000D02*
D03*
X572000D02*
D03*
X542500Y64000D02*
D03*
X525500D02*
D03*
X465000Y55000D02*
D03*
X448000D02*
D03*
D21*
X327500Y63500D02*
D03*
Y46500D02*
D03*
X302500Y63500D02*
D03*
Y46500D02*
D03*
D22*
X625000Y52000D02*
D03*
Y70000D02*
D03*
D59*
X270000Y69500D02*
D03*
Y40500D02*
D03*
M02*

View File

@@ -0,0 +1,291 @@
G04*
G04 #@! TF.GenerationSoftware,Altium Limited,Altium Designer,19.1.9 (167)*
G04*
G04 Layer_Color=16711935*
%FSLAX44Y44*%
%MOMM*%
G71*
G01*
G75*
%ADD24R,1.3500X1.5500*%
%ADD25R,1.4500X1.6500*%
%ADD26R,1.6500X1.4500*%
%ADD27R,1.5500X1.3500*%
%ADD28C,1.6740*%
%ADD29C,1.8500*%
%ADD30O,2.6000X2.0000*%
%ADD31C,1.6500*%
%ADD32R,1.6500X1.6500*%
%ADD33R,1.6500X1.6500*%
%ADD34C,3.1500*%
%ADD35C,2.1500*%
%ADD36C,1.9500*%
%ADD37R,1.9500X1.9500*%
%ADD38C,1.7500*%
%ADD39R,1.7500X1.7500*%
%ADD40R,1.8500X1.8500*%
%ADD41R,1.7500X1.7500*%
%ADD42C,1.7500*%
G04:AMPARAMS|DCode=60|XSize=2.45mm|YSize=1.95mm|CornerRadius=0.3mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=0.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
%AMROUNDEDRECTD60*
21,1,2.4500,1.3500,0,0,0.0*
21,1,1.8500,1.9500,0,0,0.0*
1,1,0.6000,0.9250,-0.6750*
1,1,0.6000,-0.9250,-0.6750*
1,1,0.6000,-0.9250,0.6750*
1,1,0.6000,0.9250,0.6750*
%
%ADD60ROUNDEDRECTD60*%
D24*
X537000Y435000D02*
D03*
X555000D02*
D03*
X537000Y470000D02*
D03*
X555000D02*
D03*
X549000Y97000D02*
D03*
X567000D02*
D03*
X517000Y256000D02*
D03*
X499000D02*
D03*
X537000Y256000D02*
D03*
X555000D02*
D03*
X611000Y354000D02*
D03*
X629000D02*
D03*
X447500Y35000D02*
D03*
X465500D02*
D03*
D25*
X555000Y380000D02*
D03*
X572000D02*
D03*
X555000Y405000D02*
D03*
X572000D02*
D03*
X542500Y64000D02*
D03*
X525500D02*
D03*
X465000Y55000D02*
D03*
X448000D02*
D03*
D26*
X327500Y63500D02*
D03*
Y46500D02*
D03*
X302500Y63500D02*
D03*
Y46500D02*
D03*
D27*
X625000Y52000D02*
D03*
Y70000D02*
D03*
D28*
X650000Y250000D02*
D03*
X25000Y475000D02*
D03*
Y25000D02*
D03*
D29*
X120000Y347500D02*
D03*
Y320000D02*
D03*
X355400Y430000D02*
D03*
X330000D02*
D03*
X304600D02*
D03*
X587000Y299000D02*
D03*
X235000Y80000D02*
D03*
X245000Y170000D02*
D03*
X574999Y198000D02*
D03*
X528000Y387000D02*
D03*
X500000Y285000D02*
D03*
X543999Y231999D02*
D03*
X451000Y274000D02*
D03*
X520000Y224000D02*
D03*
D30*
X80000Y450000D02*
D03*
X204000D02*
D03*
X80000Y400000D02*
D03*
X204000D02*
D03*
X80000Y100000D02*
D03*
X204000D02*
D03*
X80000Y50000D02*
D03*
X204000D02*
D03*
D31*
X80000Y300800D02*
D03*
Y275400D02*
D03*
Y250000D02*
D03*
Y224600D02*
D03*
X254600Y119000D02*
D03*
X356200D02*
D03*
X330800D02*
D03*
X305400D02*
D03*
X280000D02*
D03*
D32*
X80000Y199200D02*
D03*
D33*
X381600Y119000D02*
D03*
D34*
X190000Y203000D02*
D03*
Y297000D02*
D03*
D35*
X120000Y225000D02*
D03*
Y250000D02*
D03*
Y275000D02*
D03*
D36*
X677000Y383800D02*
D03*
Y460000D02*
D03*
X600800D02*
D03*
Y434600D02*
D03*
Y409200D02*
D03*
X677000Y40000D02*
D03*
Y116200D02*
D03*
X600800D02*
D03*
Y90800D02*
D03*
Y65400D02*
D03*
X678900Y326200D02*
D03*
X653500D02*
D03*
X628100D02*
D03*
X602700D02*
D03*
X577300D02*
D03*
X551900D02*
D03*
X526500D02*
D03*
X501100D02*
D03*
X678900Y173800D02*
D03*
X653500D02*
D03*
X628100D02*
D03*
X602700D02*
D03*
X577300D02*
D03*
X551900D02*
D03*
X526500D02*
D03*
X501100D02*
D03*
D37*
X600800Y383800D02*
D03*
Y40000D02*
D03*
D38*
X416900Y34400D02*
D03*
Y59800D02*
D03*
Y85200D02*
D03*
Y110600D02*
D03*
X493100D02*
D03*
Y85200D02*
D03*
Y59800D02*
D03*
D39*
Y34400D02*
D03*
D40*
X279200Y430000D02*
D03*
D41*
X424300Y464684D02*
D03*
D42*
Y439300D02*
D03*
Y413900D02*
D03*
Y388500D02*
D03*
X500500D02*
D03*
Y413900D02*
D03*
Y439300D02*
D03*
Y464700D02*
D03*
D60*
X270000Y69500D02*
D03*
Y40500D02*
D03*
M02*

View File

@@ -0,0 +1,1965 @@
G04*
G04 #@! TF.GenerationSoftware,Altium Limited,Altium Designer,19.1.9 (167)*
G04*
G04 Layer_Physical_Order=1*
G04 Layer_Color=255*
%FSLAX44Y44*%
%MOMM*%
G71*
G01*
G75*
%ADD18R,2.5000X2.2000*%
%ADD19R,1.2000X1.4000*%
%ADD20R,1.3000X1.5000*%
%ADD21R,1.5000X1.3000*%
%ADD22R,1.4000X1.2000*%
%ADD43C,0.8000*%
%ADD44C,1.5240*%
%ADD45C,1.7000*%
%ADD46O,2.4000X1.8000*%
%ADD47C,1.5000*%
%ADD48R,1.5000X1.5000*%
%ADD49R,1.5000X1.5000*%
%ADD50C,3.0000*%
%ADD51C,2.0000*%
%ADD52C,1.8000*%
%ADD53R,1.8000X1.8000*%
%ADD54C,1.6000*%
%ADD55R,1.6000X1.6000*%
%ADD56R,1.7000X1.7000*%
%ADD57R,1.6000X1.6000*%
%ADD58C,1.6000*%
G36*
X692500Y471011D02*
X687500Y469314D01*
X686985Y469985D01*
X684060Y472229D01*
X680655Y473640D01*
X680500Y473660D01*
Y460000D01*
Y446340D01*
X680655Y446360D01*
X684060Y447771D01*
X686985Y450015D01*
X687500Y450686D01*
X692500Y448989D01*
Y394811D01*
X687500Y393114D01*
X686985Y393785D01*
X684060Y396029D01*
X680655Y397440D01*
X680500Y397460D01*
Y383800D01*
Y370140D01*
X680655Y370160D01*
X684060Y371571D01*
X686985Y373815D01*
X687500Y374486D01*
X692500Y372789D01*
Y339432D01*
X691977Y338962D01*
X687500Y337248D01*
X685960Y338429D01*
X682555Y339840D01*
X678900Y340321D01*
X675245Y339840D01*
X671840Y338429D01*
X668915Y336185D01*
X663485D01*
X660560Y338429D01*
X657155Y339840D01*
X654671Y340167D01*
X640000Y354838D01*
Y366000D01*
X617697D01*
X614800Y369800D01*
Y380300D01*
X600800D01*
X586800D01*
Y371078D01*
X578760D01*
X556419Y393419D01*
X554539Y394861D01*
X552350Y395768D01*
X550000Y396078D01*
X538051D01*
X537628Y396628D01*
X534808Y398792D01*
X533444Y399357D01*
X532003Y404067D01*
X532131Y405116D01*
X552537Y425522D01*
X590119D01*
X590815Y424615D01*
Y419185D01*
X588571Y416260D01*
X587160Y412855D01*
X587140Y412700D01*
X600800D01*
X614460D01*
X614440Y412855D01*
X613029Y416260D01*
X610785Y419185D01*
Y424615D01*
X613029Y427540D01*
X614440Y430945D01*
X614921Y434600D01*
X614440Y438255D01*
X613029Y441660D01*
X610785Y444585D01*
Y450015D01*
X613029Y452940D01*
X614440Y456345D01*
X614921Y460000D01*
X614440Y463655D01*
X613029Y467060D01*
X610785Y469985D01*
X607860Y472229D01*
X604455Y473640D01*
X600800Y474121D01*
X597145Y473640D01*
X593740Y472229D01*
X590815Y469985D01*
X588571Y467060D01*
X587160Y463655D01*
X586679Y460000D01*
X587160Y456345D01*
X588571Y452940D01*
X590815Y450015D01*
Y448389D01*
X586191Y443678D01*
X548777D01*
X546428Y443368D01*
X544239Y442461D01*
X542359Y441019D01*
X534078Y432738D01*
X529078Y434809D01*
Y447377D01*
X528768Y449726D01*
X527861Y451916D01*
X526419Y453796D01*
X526419Y453796D01*
X513324Y466891D01*
X513165Y468093D01*
X511855Y471256D01*
X509772Y473971D01*
X507056Y476055D01*
X503894Y477365D01*
X500500Y477812D01*
X497106Y477365D01*
X493944Y476055D01*
X491228Y473971D01*
X490125Y472534D01*
X488281Y471119D01*
X459581Y442419D01*
X458139Y440539D01*
X457232Y438349D01*
X456922Y436000D01*
Y362301D01*
X457232Y359952D01*
X458139Y357763D01*
X459581Y355883D01*
X487245Y328219D01*
X486979Y326200D01*
X487460Y322545D01*
X488871Y319140D01*
X491115Y316215D01*
X494040Y313971D01*
X494807Y313653D01*
X494819Y313563D01*
X495726Y311374D01*
X497169Y309494D01*
X503458Y303204D01*
X501123Y298468D01*
X500000Y298616D01*
X496476Y298152D01*
X493192Y296792D01*
X490372Y294628D01*
X488208Y291808D01*
X486847Y288524D01*
X486504Y285917D01*
X486159Y285401D01*
X485130Y284407D01*
X481636Y282754D01*
X481601Y282768D01*
X479251Y283078D01*
X461051D01*
X460628Y283628D01*
X457808Y285792D01*
X454524Y287153D01*
X451000Y287617D01*
X447476Y287153D01*
X444192Y285792D01*
X441372Y283628D01*
X439208Y280808D01*
X437848Y277524D01*
X437735Y276673D01*
X432456Y274881D01*
X410578Y296759D01*
Y376814D01*
X413332Y378308D01*
X415578Y378806D01*
X417744Y377144D01*
X420800Y375878D01*
Y388500D01*
X424300D01*
Y392000D01*
X436921D01*
X435655Y395056D01*
X433572Y397771D01*
X433031Y398186D01*
X432878Y398493D01*
Y403907D01*
X433031Y404213D01*
X433572Y404628D01*
X435655Y407344D01*
X436965Y410506D01*
X437412Y413900D01*
X436965Y417294D01*
X435655Y420456D01*
X433572Y423172D01*
X433031Y423586D01*
X432878Y423893D01*
Y429307D01*
X433031Y429613D01*
X433572Y430028D01*
X435655Y432744D01*
X436965Y435906D01*
X437412Y439300D01*
X436965Y442693D01*
X435655Y445856D01*
X435019Y446685D01*
X437300Y451685D01*
X437300D01*
Y477685D01*
X411300D01*
Y458609D01*
X406300Y456538D01*
X391419Y471419D01*
X389539Y472862D01*
X387349Y473768D01*
X385000Y474078D01*
X225000D01*
X225000Y474078D01*
X222651Y473768D01*
X220461Y472862D01*
X218581Y471419D01*
X210759Y463596D01*
X210655Y463640D01*
X207000Y464121D01*
X201000D01*
X197345Y463640D01*
X193940Y462229D01*
X191015Y459985D01*
X188771Y457060D01*
X187360Y453655D01*
X186879Y450000D01*
X187360Y446345D01*
X188771Y442940D01*
X191015Y440015D01*
X193940Y437771D01*
X197345Y436360D01*
X201000Y435879D01*
X207000D01*
X210655Y436360D01*
X214060Y437771D01*
X216985Y440015D01*
X219229Y442940D01*
X220640Y446345D01*
X220861Y448023D01*
X228760Y455922D01*
X331552D01*
X333465Y451303D01*
X324494Y442332D01*
X323192Y441792D01*
X320372Y439628D01*
X314228D01*
X311408Y441792D01*
X308124Y443153D01*
X304600Y443616D01*
X301076Y443153D01*
X297792Y441792D01*
X297700Y441722D01*
X292700Y443500D01*
Y443500D01*
X282700D01*
Y430000D01*
Y416500D01*
X292700D01*
Y416500D01*
X297700Y418278D01*
X297792Y418208D01*
X301076Y416847D01*
X304600Y416384D01*
X308124Y416847D01*
X311408Y418208D01*
X314228Y420372D01*
X320372D01*
X323192Y418208D01*
X326476Y416847D01*
X330000Y416384D01*
X333524Y416847D01*
X336808Y418208D01*
X339628Y420372D01*
X345772D01*
X348592Y418208D01*
X351876Y416847D01*
X355400Y416384D01*
X358924Y416847D01*
X359907Y417255D01*
X365922Y411240D01*
Y276615D01*
X366232Y274266D01*
X367139Y272076D01*
X368581Y270196D01*
X418196Y220581D01*
X420076Y219139D01*
X422266Y218232D01*
X424615Y217922D01*
X424615Y217922D01*
X478449D01*
X478969Y217181D01*
X480146Y212922D01*
X479881Y212719D01*
X479881Y212719D01*
X416240Y149078D01*
X245000D01*
X242651Y148768D01*
X240461Y147861D01*
X238581Y146419D01*
X206283Y114121D01*
X201000D01*
X197345Y113640D01*
X193940Y112229D01*
X191015Y109985D01*
X188771Y107060D01*
X187360Y103655D01*
X186879Y100000D01*
X187360Y96345D01*
X188771Y92940D01*
X191015Y90015D01*
X193940Y87771D01*
X197345Y86360D01*
X201000Y85879D01*
X207000D01*
X210655Y86360D01*
X214060Y87771D01*
X216985Y90015D01*
X219229Y92940D01*
X220640Y96345D01*
X221121Y100000D01*
X220739Y102901D01*
X237131Y119293D01*
X237927Y119245D01*
X242215Y117306D01*
X242422Y115737D01*
X243681Y112696D01*
X245685Y110085D01*
X248296Y108081D01*
X251337Y106822D01*
X254600Y106392D01*
X257863Y106822D01*
X260904Y108081D01*
X263515Y110085D01*
X264318Y111131D01*
X265495Y111518D01*
X269104D01*
X270282Y111131D01*
X271085Y110085D01*
X273696Y108081D01*
X276737Y106822D01*
X280000Y106392D01*
X283263Y106822D01*
X286304Y108081D01*
X288915Y110085D01*
X289718Y111131D01*
X290896Y111518D01*
X294505D01*
X295682Y111131D01*
X296485Y110085D01*
X299096Y108081D01*
X302137Y106822D01*
X305400Y106392D01*
X308663Y106822D01*
X311704Y108081D01*
X314315Y110085D01*
X315118Y111131D01*
X316296Y111518D01*
X319904D01*
X321082Y111131D01*
X321885Y110085D01*
X324496Y108081D01*
X327537Y106822D01*
X330800Y106392D01*
X334063Y106822D01*
X337104Y108081D01*
X339715Y110085D01*
X340518Y111131D01*
X341696Y111518D01*
X345304D01*
X346482Y111131D01*
X347285Y110085D01*
X349896Y108081D01*
X352937Y106822D01*
X356200Y106392D01*
X359463Y106822D01*
X362504Y108081D01*
X364100Y109306D01*
X367569Y108225D01*
X369100Y107065D01*
Y106500D01*
X394100D01*
Y130922D01*
X420000D01*
X422350Y131232D01*
X424539Y132139D01*
X426419Y133581D01*
X460803Y167965D01*
X465422Y166052D01*
Y151000D01*
X463500D01*
Y139338D01*
X448162Y124000D01*
X437000D01*
Y102000D01*
Y96838D01*
X435640Y95478D01*
X427568D01*
X425374Y99904D01*
X425449Y100550D01*
X425631Y100913D01*
X426172Y101328D01*
X428256Y104044D01*
X429565Y107206D01*
X430012Y110600D01*
X429565Y113994D01*
X428256Y117156D01*
X426172Y119872D01*
X423456Y121955D01*
X420294Y123265D01*
X416900Y123712D01*
X413506Y123265D01*
X410344Y121955D01*
X407628Y119872D01*
X405545Y117156D01*
X404235Y113994D01*
X403788Y110600D01*
X404235Y107206D01*
X405545Y104044D01*
X407628Y101328D01*
X408169Y100913D01*
X408322Y100607D01*
Y95193D01*
X408169Y94887D01*
X407628Y94472D01*
X405545Y91756D01*
X405263Y91076D01*
X404267Y90281D01*
X400019Y88686D01*
X399206Y88703D01*
X399049Y88768D01*
X396700Y89078D01*
X245051D01*
X244628Y89628D01*
X241808Y91792D01*
X238524Y93153D01*
X235000Y93616D01*
X231476Y93153D01*
X228192Y91792D01*
X225372Y89628D01*
X223208Y86808D01*
X221847Y83524D01*
X221384Y80000D01*
X221847Y76476D01*
X223208Y73192D01*
X225372Y70372D01*
X225922Y69949D01*
Y61000D01*
X220000D01*
X220000Y61000D01*
X215160Y61385D01*
X214060Y62229D01*
X210655Y63640D01*
X207500Y64055D01*
Y50000D01*
X200500D01*
Y64055D01*
X197345Y63640D01*
X193940Y62229D01*
X191015Y59985D01*
X188771Y57060D01*
X188501Y56408D01*
X182741Y55097D01*
X72578Y165260D01*
Y186700D01*
X76500D01*
Y199200D01*
X80000D01*
Y202700D01*
X92500D01*
Y205721D01*
X97500Y207792D01*
X141500Y163792D01*
Y158000D01*
X181500D01*
Y160922D01*
X189000D01*
Y158000D01*
X215500D01*
Y170000D01*
X220500D01*
Y158000D01*
X229000D01*
Y160867D01*
X229548Y161169D01*
X234000Y162159D01*
X235372Y160372D01*
X238192Y158208D01*
X241476Y156847D01*
X245000Y156383D01*
X248524Y156847D01*
X251808Y158208D01*
X254628Y160372D01*
X256792Y163192D01*
X258153Y166476D01*
X258616Y170000D01*
X258153Y173524D01*
X256792Y176808D01*
X254628Y179628D01*
X251808Y181792D01*
X248524Y183153D01*
X245000Y183616D01*
X244312Y183526D01*
X235419Y192419D01*
X233539Y193862D01*
X231349Y194768D01*
X229000Y195078D01*
X213722D01*
X211016Y197777D01*
X209809Y200078D01*
X210097Y203000D01*
X209711Y206921D01*
X208567Y210691D01*
X206710Y214165D01*
X204211Y217211D01*
X201165Y219710D01*
X197691Y221567D01*
X193921Y222711D01*
X190000Y223097D01*
X186079Y222711D01*
X182309Y221567D01*
X178835Y219710D01*
X175789Y217211D01*
X173290Y214165D01*
X171433Y210691D01*
X170289Y206921D01*
X169903Y203000D01*
X170289Y199079D01*
X171433Y195309D01*
X173290Y191835D01*
X175789Y188789D01*
X177970Y187000D01*
X177381Y183750D01*
X176380Y182000D01*
X148968D01*
X125269Y205699D01*
X125557Y208800D01*
X126547Y211491D01*
X128374Y212468D01*
X130658Y214342D01*
X132532Y216626D01*
X133925Y219232D01*
X134783Y222059D01*
X135073Y225000D01*
X134783Y227941D01*
X133925Y230768D01*
X132532Y233374D01*
X130658Y235658D01*
Y239342D01*
X132532Y241626D01*
X133925Y244232D01*
X134783Y247059D01*
X135073Y250000D01*
X134783Y252941D01*
X133925Y255768D01*
X132532Y258374D01*
X130658Y260658D01*
Y264342D01*
X132532Y266626D01*
X133925Y269232D01*
X134783Y272059D01*
X135073Y275000D01*
X134783Y277941D01*
X133925Y280768D01*
X132532Y283374D01*
X130658Y285658D01*
X128374Y287532D01*
X125768Y288925D01*
X122941Y289783D01*
X120000Y290073D01*
X117059Y289783D01*
X114232Y288925D01*
X111626Y287532D01*
X109342Y285658D01*
X107468Y283374D01*
X106075Y280768D01*
X105217Y277941D01*
X104927Y275000D01*
X105217Y272059D01*
X106075Y269232D01*
X107468Y266626D01*
X109342Y264342D01*
Y260658D01*
X107468Y258374D01*
X106075Y255768D01*
X105217Y252941D01*
X104927Y250000D01*
X105217Y247059D01*
X106075Y244232D01*
X106532Y243377D01*
X102472Y240366D01*
X92573Y250265D01*
X92178Y253263D01*
X90919Y256304D01*
X88915Y258915D01*
X87869Y259718D01*
X87482Y260896D01*
Y264505D01*
X87869Y265682D01*
X88915Y266485D01*
X90919Y269096D01*
X92178Y272137D01*
X92608Y275400D01*
X92390Y277053D01*
X93105Y277767D01*
X93253Y277881D01*
X122022Y306650D01*
X123524Y306847D01*
X126808Y308208D01*
X129628Y310372D01*
X131792Y313192D01*
X133153Y316476D01*
X133616Y320000D01*
X133153Y323524D01*
X131792Y326808D01*
X129628Y329628D01*
X128075Y330820D01*
X127646Y332477D01*
Y335023D01*
X128075Y336680D01*
X129628Y337872D01*
X131792Y340692D01*
X133153Y343976D01*
X133616Y347500D01*
X133153Y351024D01*
X131792Y354308D01*
X129628Y357128D01*
X126808Y359292D01*
X123524Y360653D01*
X120000Y361116D01*
X116476Y360653D01*
X113192Y359292D01*
X110372Y357128D01*
X108208Y354308D01*
X106847Y351024D01*
X106383Y347500D01*
X106474Y346812D01*
X80581Y320919D01*
X79139Y319039D01*
X78232Y316849D01*
X77922Y314500D01*
Y313454D01*
X77554Y313086D01*
X76737Y312978D01*
X73696Y311719D01*
X71085Y309715D01*
X69081Y307104D01*
X67822Y304063D01*
X67392Y300800D01*
X67822Y297537D01*
X69081Y294496D01*
X71085Y291885D01*
X72131Y291082D01*
X72518Y289904D01*
Y286296D01*
X72131Y285118D01*
X71085Y284315D01*
X69081Y281704D01*
X67822Y278663D01*
X67392Y275400D01*
X67822Y272137D01*
X69081Y269096D01*
X71085Y266485D01*
X72131Y265682D01*
X72518Y264505D01*
Y260896D01*
X72131Y259718D01*
X71085Y258915D01*
X69081Y256304D01*
X67822Y253263D01*
X67392Y250000D01*
X67822Y246737D01*
X69081Y243696D01*
X71085Y241085D01*
X72131Y240282D01*
X72518Y239104D01*
Y235495D01*
X72131Y234318D01*
X71085Y233515D01*
X69081Y230904D01*
X67822Y227863D01*
X67714Y227046D01*
X64602Y223934D01*
X62738Y222504D01*
X57081Y216847D01*
X55638Y214967D01*
X54732Y212777D01*
X54422Y210428D01*
Y161500D01*
X54732Y159151D01*
X55638Y156961D01*
X57081Y155081D01*
X193581Y18581D01*
X195461Y17139D01*
X197651Y16232D01*
X200000Y15922D01*
X200000Y15922D01*
X367500D01*
X369850Y16232D01*
X372039Y17139D01*
X373919Y18581D01*
X384338Y29000D01*
X400000D01*
Y29000D01*
X405000Y29159D01*
X405545Y27844D01*
X407628Y25128D01*
X410344Y23045D01*
X413506Y21735D01*
X416900Y21288D01*
X420294Y21735D01*
X423456Y23045D01*
X426172Y25128D01*
X428256Y27844D01*
X429565Y31006D01*
X432000Y34246D01*
X432500Y34206D01*
X437000Y33000D01*
Y33000D01*
X480100D01*
Y21400D01*
X506100D01*
Y47400D01*
X506100D01*
X503808Y52400D01*
X504455Y53244D01*
X505765Y56406D01*
X506212Y59800D01*
X505765Y63194D01*
X504455Y66356D01*
X502372Y69072D01*
X501831Y69487D01*
X501678Y69793D01*
Y75207D01*
X501831Y75513D01*
X502372Y75928D01*
X504455Y78644D01*
X505721Y81700D01*
X493100D01*
X480479D01*
X481745Y78644D01*
X482239Y78000D01*
X479773Y73000D01*
X472500D01*
Y61500D01*
X467500D01*
Y73000D01*
X452000D01*
Y61500D01*
X447000D01*
Y73000D01*
X442966D01*
X441749Y77632D01*
X443939Y78539D01*
X445819Y79981D01*
X449838Y84000D01*
X461000D01*
Y102000D01*
Y111162D01*
X476838Y127000D01*
X485657D01*
X486651Y122000D01*
X486544Y121955D01*
X483828Y119872D01*
X481745Y117156D01*
X480479Y114100D01*
X493100D01*
X505721D01*
X504455Y117156D01*
X502372Y119872D01*
X499656Y121955D01*
X499548Y122000D01*
X500543Y127000D01*
X503500D01*
Y136500D01*
X492500D01*
Y139000D01*
X490000D01*
Y151000D01*
X483578D01*
Y166453D01*
X488578Y167448D01*
X488871Y166740D01*
X491115Y163815D01*
X494040Y161571D01*
X497445Y160160D01*
X501100Y159679D01*
X504755Y160160D01*
X508160Y161571D01*
X511085Y163815D01*
X516515D01*
X519440Y161571D01*
X522845Y160160D01*
X526500Y159679D01*
X530155Y160160D01*
X533560Y161571D01*
X536485Y163815D01*
X541915D01*
X544840Y161571D01*
X548245Y160160D01*
X551900Y159679D01*
X555555Y160160D01*
X558960Y161571D01*
X561885Y163815D01*
X567315D01*
X570240Y161571D01*
X573645Y160160D01*
X577300Y159679D01*
X580955Y160160D01*
X584360Y161571D01*
X587285Y163815D01*
X592715D01*
X595640Y161571D01*
X599045Y160160D01*
X602700Y159679D01*
X606355Y160160D01*
X609760Y161571D01*
X612685Y163815D01*
X618115D01*
X621040Y161571D01*
X624445Y160160D01*
X628100Y159679D01*
X631755Y160160D01*
X635160Y161571D01*
X638085Y163815D01*
X643515D01*
X646440Y161571D01*
X649845Y160160D01*
X653500Y159679D01*
X657155Y160160D01*
X660560Y161571D01*
X663485Y163815D01*
X668915D01*
X671840Y161571D01*
X675245Y160160D01*
X678900Y159679D01*
X682555Y160160D01*
X685960Y161571D01*
X687500Y162752D01*
X691977Y161038D01*
X692500Y160568D01*
Y127211D01*
X687500Y125514D01*
X686985Y126185D01*
X684060Y128429D01*
X680655Y129840D01*
X680500Y129860D01*
Y116200D01*
Y102540D01*
X680655Y102560D01*
X684060Y103971D01*
X686985Y106215D01*
X687500Y106886D01*
X692500Y105189D01*
Y51011D01*
X687500Y49314D01*
X686985Y49985D01*
X684060Y52229D01*
X680655Y53640D01*
X680500Y53660D01*
Y40000D01*
Y26340D01*
X680655Y26360D01*
X684060Y27771D01*
X686985Y30015D01*
X687500Y30686D01*
X692500Y28989D01*
Y9092D01*
X71942D01*
X71617Y9546D01*
X69222Y14092D01*
X70208Y19048D01*
X70598Y25000D01*
X70208Y30952D01*
X69927Y32364D01*
X71698Y34436D01*
X74253Y36241D01*
X76500Y35945D01*
Y50000D01*
Y64055D01*
X73345Y63640D01*
X69940Y62229D01*
X67015Y59985D01*
X64771Y57060D01*
X64541Y56504D01*
X58741Y55534D01*
X57242Y57242D01*
X52758Y61175D01*
X47799Y64489D01*
X42449Y67127D01*
X36802Y69044D01*
X30952Y70208D01*
X25000Y70598D01*
X19048Y70208D01*
X17500Y69900D01*
X12500Y74003D01*
Y425997D01*
X17500Y430100D01*
X19048Y429793D01*
X25000Y429402D01*
X30952Y429793D01*
X36802Y430956D01*
X42449Y432873D01*
X47799Y435511D01*
X52758Y438825D01*
X57242Y442758D01*
X58741Y444466D01*
X64541Y443496D01*
X64771Y442940D01*
X67015Y440015D01*
X69940Y437771D01*
X73345Y436360D01*
X77000Y435879D01*
X83000D01*
X86655Y436360D01*
X90060Y437771D01*
X92985Y440015D01*
X95229Y442940D01*
X96640Y446345D01*
X97121Y450000D01*
X96640Y453655D01*
X95229Y457060D01*
X92985Y459985D01*
X90060Y462229D01*
X86655Y463640D01*
X83000Y464121D01*
X77000D01*
X74253Y463759D01*
X71698Y465564D01*
X69927Y467635D01*
X70208Y469048D01*
X70598Y475000D01*
X70208Y480952D01*
X69583Y484092D01*
X73069Y489092D01*
X692500D01*
Y471011D01*
D02*
G37*
G36*
X570240Y313971D02*
X573645Y312560D01*
X574261Y312479D01*
X574930Y311651D01*
X576255Y307174D01*
X575208Y305808D01*
X573848Y302524D01*
X573844Y302500D01*
X587000D01*
Y295500D01*
X573844D01*
X573848Y295476D01*
X575208Y292192D01*
X577372Y289372D01*
X580110Y287270D01*
X580193Y287201D01*
X581263Y281352D01*
X576489Y276578D01*
X542364D01*
X517824Y276578D01*
X517640Y276793D01*
X515915Y281577D01*
X520838Y286500D01*
X534000D01*
Y299000D01*
X536500D01*
Y301500D01*
X548000D01*
Y307549D01*
X551889Y312066D01*
X552716Y312187D01*
X555555Y312560D01*
X558960Y313971D01*
X561885Y316215D01*
X567315D01*
X570240Y313971D01*
D02*
G37*
%LPC*%
G36*
X673500Y473660D02*
X673345Y473640D01*
X669940Y472229D01*
X667015Y469985D01*
X664771Y467060D01*
X663360Y463655D01*
X663340Y463500D01*
X673500D01*
Y473660D01*
D02*
G37*
G36*
Y456500D02*
X663340D01*
X663360Y456345D01*
X664771Y452940D01*
X667015Y450015D01*
X669940Y447771D01*
X673345Y446360D01*
X673500Y446340D01*
Y456500D01*
D02*
G37*
G36*
X275700Y443500D02*
X265700D01*
Y433500D01*
X275700D01*
Y443500D01*
D02*
G37*
G36*
Y426500D02*
X265700D01*
Y416500D01*
X275700D01*
Y426500D01*
D02*
G37*
G36*
X207500Y414055D02*
Y403500D01*
X220660D01*
X220640Y403655D01*
X219229Y407060D01*
X216985Y409985D01*
X214060Y412229D01*
X210655Y413640D01*
X207500Y414055D01*
D02*
G37*
G36*
X83500D02*
Y403500D01*
X96660D01*
X96640Y403655D01*
X95229Y407060D01*
X92985Y409985D01*
X90060Y412229D01*
X86655Y413640D01*
X83500Y414055D01*
D02*
G37*
G36*
X200500Y414055D02*
X197345Y413640D01*
X193940Y412229D01*
X191015Y409985D01*
X188771Y407060D01*
X187360Y403655D01*
X187340Y403500D01*
X200500D01*
Y414055D01*
D02*
G37*
G36*
X76500D02*
X73345Y413640D01*
X69940Y412229D01*
X67015Y409985D01*
X64771Y407060D01*
X63360Y403655D01*
X63340Y403500D01*
X76500D01*
Y414055D01*
D02*
G37*
G36*
X673500Y397460D02*
X673345Y397440D01*
X669940Y396029D01*
X667015Y393785D01*
X664771Y390860D01*
X663360Y387455D01*
X663340Y387300D01*
X673500D01*
Y397460D01*
D02*
G37*
G36*
X614460Y405700D02*
X600800D01*
X587140D01*
X587160Y405545D01*
X588298Y402800D01*
X586800Y397800D01*
X586800D01*
Y387300D01*
X600800D01*
X614800D01*
Y397800D01*
X614800Y397800D01*
X613302Y402800D01*
X614440Y405545D01*
X614460Y405700D01*
D02*
G37*
G36*
X220660Y396500D02*
X207500D01*
Y385945D01*
X210655Y386360D01*
X214060Y387771D01*
X216985Y390015D01*
X219229Y392940D01*
X220640Y396345D01*
X220660Y396500D01*
D02*
G37*
G36*
X96660D02*
X83500D01*
Y385945D01*
X86655Y386360D01*
X90060Y387771D01*
X92985Y390015D01*
X95229Y392940D01*
X96640Y396345D01*
X96660Y396500D01*
D02*
G37*
G36*
X200500D02*
X187340D01*
X187360Y396345D01*
X188771Y392940D01*
X191015Y390015D01*
X193940Y387771D01*
X197345Y386360D01*
X200500Y385945D01*
Y396500D01*
D02*
G37*
G36*
X76500D02*
X63340D01*
X63360Y396345D01*
X64771Y392940D01*
X67015Y390015D01*
X69940Y387771D01*
X73345Y386360D01*
X76500Y385945D01*
Y396500D01*
D02*
G37*
G36*
X436921Y385000D02*
X427800D01*
Y375878D01*
X430856Y377144D01*
X433572Y379228D01*
X435655Y381944D01*
X436921Y385000D01*
D02*
G37*
G36*
X673500Y380300D02*
X663340D01*
X663360Y380145D01*
X664771Y376740D01*
X667015Y373815D01*
X669940Y371571D01*
X673345Y370160D01*
X673500Y370140D01*
Y380300D01*
D02*
G37*
G36*
X190000Y317097D02*
X186079Y316711D01*
X182309Y315567D01*
X178835Y313710D01*
X175789Y311211D01*
X173290Y308165D01*
X171433Y304691D01*
X170289Y300921D01*
X169903Y297000D01*
X170289Y293079D01*
X171433Y289309D01*
X173290Y285835D01*
X175789Y282789D01*
X178835Y280290D01*
X182309Y278433D01*
X186079Y277289D01*
X190000Y276903D01*
X193921Y277289D01*
X197691Y278433D01*
X201165Y280290D01*
X204211Y282789D01*
X206710Y285835D01*
X208567Y289309D01*
X209711Y293079D01*
X210097Y297000D01*
X209711Y300921D01*
X208567Y304691D01*
X206710Y308165D01*
X204211Y311211D01*
X201165Y313710D01*
X197691Y315567D01*
X193921Y316711D01*
X190000Y317097D01*
D02*
G37*
G36*
X92500Y195700D02*
X83500D01*
Y186700D01*
X92500D01*
Y195700D01*
D02*
G37*
G36*
X503500Y151000D02*
X495000D01*
Y141500D01*
X503500D01*
Y151000D01*
D02*
G37*
G36*
X673500Y129860D02*
X673345Y129840D01*
X669940Y128429D01*
X667015Y126185D01*
X664771Y123260D01*
X663360Y119855D01*
X663340Y119700D01*
X673500D01*
Y129860D01*
D02*
G37*
G36*
Y112700D02*
X663340D01*
X663360Y112545D01*
X664771Y109140D01*
X667015Y106215D01*
X669940Y103971D01*
X673345Y102560D01*
X673500Y102540D01*
Y112700D01*
D02*
G37*
G36*
X505721Y107100D02*
X493100D01*
X480479D01*
X481745Y104044D01*
X483828Y101328D01*
X484369Y100913D01*
X484522Y100607D01*
Y95193D01*
X484369Y94887D01*
X483828Y94472D01*
X481745Y91756D01*
X480479Y88700D01*
X493100D01*
X505721D01*
X504455Y91756D01*
X502372Y94472D01*
X501831Y94887D01*
X501678Y95193D01*
Y100607D01*
X501831Y100913D01*
X502372Y101328D01*
X504455Y104044D01*
X505721Y107100D01*
D02*
G37*
G36*
X83000Y114121D02*
X77000D01*
X73345Y113640D01*
X69940Y112229D01*
X67015Y109985D01*
X64771Y107060D01*
X63360Y103655D01*
X62879Y100000D01*
X63360Y96345D01*
X64771Y92940D01*
X67015Y90015D01*
X69940Y87771D01*
X73345Y86360D01*
X77000Y85879D01*
X83000D01*
X86655Y86360D01*
X90060Y87771D01*
X92985Y90015D01*
X95229Y92940D01*
X96640Y96345D01*
X97121Y100000D01*
X96640Y103655D01*
X95229Y107060D01*
X92985Y109985D01*
X90060Y112229D01*
X86655Y113640D01*
X83000Y114121D01*
D02*
G37*
G36*
X83500Y64055D02*
Y53500D01*
X96660D01*
X96640Y53655D01*
X95229Y57060D01*
X92985Y59985D01*
X90060Y62229D01*
X86655Y63640D01*
X83500Y64055D01*
D02*
G37*
G36*
X673500Y53660D02*
X673345Y53640D01*
X669940Y52229D01*
X667015Y49985D01*
X664771Y47060D01*
X663360Y43655D01*
X663340Y43500D01*
X673500D01*
Y53660D01*
D02*
G37*
G36*
X96660Y46500D02*
X83500D01*
Y35945D01*
X86655Y36360D01*
X90060Y37771D01*
X92985Y40015D01*
X95229Y42940D01*
X96640Y46345D01*
X96660Y46500D01*
D02*
G37*
G36*
X673500Y36500D02*
X663340D01*
X663360Y36345D01*
X664771Y32940D01*
X667015Y30015D01*
X669940Y27771D01*
X673345Y26360D01*
X673500Y26340D01*
Y36500D01*
D02*
G37*
G36*
X600800Y130321D02*
X597145Y129840D01*
X593740Y128429D01*
X590815Y126185D01*
X588571Y123260D01*
X587160Y119855D01*
X586679Y116200D01*
X587160Y112545D01*
X588571Y109140D01*
X590815Y106215D01*
Y100785D01*
X588571Y97860D01*
X587160Y94455D01*
X586679Y90800D01*
X587160Y87145D01*
X588571Y83740D01*
X590815Y80815D01*
Y75385D01*
X588571Y72460D01*
X587160Y69055D01*
X586679Y65400D01*
X587160Y61745D01*
X588298Y59000D01*
X586800Y54000D01*
X586800D01*
Y26000D01*
X614800D01*
Y54000D01*
X614800Y54000D01*
X613302Y59000D01*
X614440Y61745D01*
X614921Y65400D01*
X614440Y69055D01*
X613029Y72460D01*
X610785Y75385D01*
Y80815D01*
X613029Y83740D01*
X614440Y87145D01*
X614921Y90800D01*
X614440Y94455D01*
X613029Y97860D01*
X610785Y100785D01*
Y106215D01*
X613029Y109140D01*
X614440Y112545D01*
X614921Y116200D01*
X614440Y119855D01*
X613029Y123260D01*
X610785Y126185D01*
X607860Y128429D01*
X604455Y129840D01*
X600800Y130321D01*
D02*
G37*
G36*
X548000Y296500D02*
X539000D01*
Y286500D01*
X548000D01*
Y296500D01*
D02*
G37*
%LPD*%
D18*
X382500Y45000D02*
D03*
X237500D02*
D03*
D19*
X200000Y170000D02*
D03*
X218000D02*
D03*
X170500D02*
D03*
X152500D02*
D03*
X474500Y139000D02*
D03*
X492500D02*
D03*
X629000Y354000D02*
D03*
X611000D02*
D03*
D20*
X536500Y299000D02*
D03*
X519500D02*
D03*
X523500Y352000D02*
D03*
X506500D02*
D03*
D21*
X449500Y44500D02*
D03*
Y61500D02*
D03*
X470000Y44500D02*
D03*
Y61500D02*
D03*
D22*
X449000Y113000D02*
D03*
Y95000D02*
D03*
D43*
X367500Y25000D02*
X385000Y42500D01*
X200000Y25000D02*
X367500D01*
X63500Y161500D02*
X200000Y25000D01*
X432884Y44500D02*
X449500D01*
X417584Y59800D02*
X432884Y44500D01*
X449500D02*
X470000D01*
X416900Y59800D02*
X417584D01*
X396700Y80000D02*
X416900Y59800D01*
X235000Y80000D02*
X396700D01*
X63500Y161500D02*
Y210428D01*
X148130Y170000D02*
X152500D01*
X101000Y217130D02*
X148130Y170000D01*
X63500Y210428D02*
X69157Y216085D01*
X69591D01*
X78106Y224600D01*
X80000D01*
X101000Y217130D02*
Y229000D01*
X235000Y50000D02*
Y80000D01*
X118278Y315743D02*
Y318278D01*
X86834Y284300D02*
X118278Y315743D01*
X86800Y284300D02*
X86834D01*
X80000Y277500D02*
X86800Y284300D01*
X80000Y275400D02*
Y277500D01*
X137500Y318278D02*
X140778Y315000D01*
X170000D01*
X118278Y318278D02*
X120000Y320000D01*
X87000Y309694D02*
Y314500D01*
X80000Y302694D02*
X87000Y309694D01*
X80000Y300800D02*
Y302694D01*
X87000Y314500D02*
X120000Y347500D01*
X200000Y177728D02*
X208272Y186000D01*
X200000Y170000D02*
Y177728D01*
X229000Y186000D02*
X245000Y170000D01*
X208272Y186000D02*
X229000D01*
X170500Y170000D02*
X200000D01*
X78661Y223261D02*
X80000Y224600D01*
X524350Y328350D02*
X525000Y329000D01*
X523500Y327500D02*
X524350Y328350D01*
X523000Y332700D02*
Y354500D01*
X80000Y250000D02*
X101000Y229000D01*
X628100Y315351D02*
Y326200D01*
X580249Y267500D02*
X628100Y315351D01*
X542364Y267500D02*
X580249D01*
X598477Y231000D02*
X653500Y175977D01*
X571249Y231000D02*
X598477D01*
X653500Y173800D02*
Y175977D01*
X628100Y173800D02*
Y175977D01*
X588577Y215500D02*
X628100Y175977D01*
X552248Y215500D02*
X588577D01*
X570299Y193300D02*
X574999Y198000D01*
X653250Y326450D02*
X653500Y326200D01*
X500500Y388500D02*
X502677D01*
X548777Y434600D01*
X593800D01*
X533728Y368500D02*
X555228Y347000D01*
X492923Y368500D02*
X533728D01*
X528000Y387000D02*
X550000D01*
X575000Y362000D01*
X591000D01*
X520000Y431223D02*
Y447377D01*
X502677Y413900D02*
X520000Y431223D01*
X653500Y326200D02*
Y328500D01*
X629000Y353000D02*
X653500Y328500D01*
X629000Y353000D02*
Y354000D01*
X599000D02*
X611000D01*
X591000Y362000D02*
X599000Y354000D01*
X555228Y347000D02*
X584077D01*
X602700Y328377D01*
Y326200D02*
Y328377D01*
X479000Y382423D02*
X492923Y368500D01*
X500000Y285000D02*
X501722Y286722D01*
X508222D01*
X519500Y298000D01*
Y299000D01*
X526500Y322682D02*
Y326200D01*
Y322682D02*
X535000Y314182D01*
Y300500D02*
Y314182D01*
Y300500D02*
X536500Y299000D01*
X503587Y315913D02*
Y323713D01*
X501100Y326200D02*
X503587Y323713D01*
Y315913D02*
X519500Y300000D01*
Y299000D02*
Y300000D01*
X440000Y254500D02*
X547749Y254500D01*
X401500Y292999D02*
X440000Y254500D01*
X547749Y254500D02*
X571249Y231000D01*
X543048Y206300D02*
X552248Y215500D01*
X486300Y206300D02*
X543048D01*
X543999Y231999D02*
Y233501D01*
X536000Y241500D02*
X543999Y233501D01*
X479251Y274000D02*
X485751Y267500D01*
X451000Y274000D02*
X479251D01*
X485751Y267500D02*
X542364Y267500D01*
X431500Y241500D02*
X512751D01*
X388500Y284500D02*
X431500Y241500D01*
X512751D02*
X512751Y241500D01*
X388500Y284500D02*
Y431500D01*
X517000Y227000D02*
X520000Y224000D01*
X424615Y227000D02*
X517000D01*
X375000Y276615D02*
X424615Y227000D01*
X512751Y241500D02*
X536000D01*
X493023Y193300D02*
X570299D01*
X474500Y174777D02*
X493023Y193300D01*
X523000Y354500D02*
X523500Y355000D01*
X524350Y328350D02*
X526500Y326200D01*
X523000Y329700D02*
X524350Y328350D01*
X225000Y465000D02*
X385000D01*
X210000Y450000D02*
X225000Y465000D01*
X204000Y450000D02*
X210000D01*
X204000Y100000D02*
X205000D01*
X245000Y140000D02*
X420000D01*
X205000Y100000D02*
X245000Y140000D01*
X401500Y292999D02*
Y448500D01*
X385000Y465000D02*
X401500Y448500D01*
X368000Y452000D02*
X388500Y431500D01*
X347000Y452000D02*
X368000D01*
X375000Y276615D02*
Y415000D01*
X360000Y430000D02*
X375000Y415000D01*
X355400Y430000D02*
X360000D01*
X330000Y435000D02*
X347000Y452000D01*
X330000Y430000D02*
Y435000D01*
X420000Y140000D02*
X486300Y206300D01*
X448000Y95000D02*
X449000D01*
X439400Y86400D02*
X448000Y95000D01*
X418100Y86400D02*
X439400D01*
X416900Y85200D02*
X418100Y86400D01*
X479000Y382423D02*
Y419977D01*
X498323Y439300D01*
X500500D01*
X506000Y328000D02*
Y351500D01*
X501100Y326200D02*
X504200D01*
X470000Y61500D02*
X472000D01*
X449500D02*
X470000D01*
X506000Y354500D02*
X506500Y355000D01*
X474500Y139000D02*
Y174777D01*
Y137500D02*
Y139000D01*
X450000Y113000D02*
X474500Y137500D01*
X449000Y113000D02*
X450000D01*
X470000Y61500D02*
Y65000D01*
X490200Y85200D02*
X493100D01*
X470000Y65000D02*
X490200Y85200D01*
X492800Y110900D02*
X493100Y110600D01*
X492800Y110900D02*
Y134700D01*
X492500Y135000D02*
X492800Y134700D01*
X500500Y464700D02*
X502677D01*
X466000Y362301D02*
X496151Y332151D01*
X466000Y436000D02*
X494700Y464700D01*
X500500D01*
X502677D02*
X520000Y447377D01*
X499600Y413000D02*
X500000D01*
X502000Y463200D02*
Y464000D01*
X498000Y414000D02*
Y415041D01*
X466000Y362301D02*
Y436000D01*
X500500Y413900D02*
X502677D01*
D44*
X650000Y250000D02*
D03*
X25000Y475000D02*
D03*
Y25000D02*
D03*
D45*
X120000Y347500D02*
D03*
Y320000D02*
D03*
X355400Y430000D02*
D03*
X330000D02*
D03*
X304600D02*
D03*
X587000Y299000D02*
D03*
X235000Y80000D02*
D03*
X245000Y170000D02*
D03*
X574999Y198000D02*
D03*
X528000Y387000D02*
D03*
X500000Y285000D02*
D03*
X543999Y231999D02*
D03*
X451000Y274000D02*
D03*
X520000Y224000D02*
D03*
D46*
X80000Y450000D02*
D03*
X204000D02*
D03*
X80000Y400000D02*
D03*
X204000D02*
D03*
X80000Y100000D02*
D03*
X204000D02*
D03*
X80000Y50000D02*
D03*
X204000D02*
D03*
D47*
X80000Y300800D02*
D03*
Y275400D02*
D03*
Y250000D02*
D03*
Y224600D02*
D03*
X254600Y119000D02*
D03*
X356200D02*
D03*
X330800D02*
D03*
X305400D02*
D03*
X280000D02*
D03*
D48*
X80000Y199200D02*
D03*
D49*
X381600Y119000D02*
D03*
D50*
X190000Y203000D02*
D03*
Y297000D02*
D03*
D51*
X120000Y225000D02*
D03*
Y250000D02*
D03*
Y275000D02*
D03*
D52*
X677000Y383800D02*
D03*
Y460000D02*
D03*
X600800D02*
D03*
Y434600D02*
D03*
Y409200D02*
D03*
X677000Y40000D02*
D03*
Y116200D02*
D03*
X600800D02*
D03*
Y90800D02*
D03*
Y65400D02*
D03*
X678900Y326200D02*
D03*
X653500D02*
D03*
X628100D02*
D03*
X602700D02*
D03*
X577300D02*
D03*
X551900D02*
D03*
X526500D02*
D03*
X501100D02*
D03*
X678900Y173800D02*
D03*
X653500D02*
D03*
X628100D02*
D03*
X602700D02*
D03*
X577300D02*
D03*
X551900D02*
D03*
X526500D02*
D03*
X501100D02*
D03*
D53*
X600800Y383800D02*
D03*
Y40000D02*
D03*
D54*
X416900Y34400D02*
D03*
Y59800D02*
D03*
Y85200D02*
D03*
Y110600D02*
D03*
X493100D02*
D03*
Y85200D02*
D03*
Y59800D02*
D03*
D55*
Y34400D02*
D03*
D56*
X279200Y430000D02*
D03*
D57*
X424300Y464684D02*
D03*
D58*
Y439300D02*
D03*
Y413900D02*
D03*
Y388500D02*
D03*
X500500D02*
D03*
Y413900D02*
D03*
Y439300D02*
D03*
Y464700D02*
D03*
M02*

View File

@@ -0,0 +1,1099 @@
G04*
G04 #@! TF.GenerationSoftware,Altium Limited,Altium Designer,19.1.9 (167)*
G04*
G04 Layer_Color=65535*
%FSLAX44Y44*%
%MOMM*%
G71*
G01*
G75*
%ADD10C,0.1524*%
%ADD11C,0.2540*%
%ADD12C,0.2000*%
%ADD13C,0.1500*%
%ADD14C,0.1000*%
%ADD15C,0.1800*%
%ADD16C,0.2500*%
%ADD17C,0.1540*%
G36*
X125490Y58490D02*
Y91510D01*
X158510D01*
Y58490D01*
X125490D01*
D02*
G37*
G36*
Y408490D02*
Y441510D01*
X158510D01*
Y408490D01*
X125490D01*
D02*
G37*
D10*
X202960Y475800D02*
G03*
X192800Y485960I-10160J0D01*
G01*
X154617Y375792D02*
G03*
X154929Y474127I-12617J49208D01*
G01*
X192800Y364040D02*
G03*
X202960Y374200I0J10160D01*
G01*
X154477Y386323D02*
G03*
X154477Y463677I-12477J38677D01*
G01*
X129523D02*
G03*
X129523Y386323I12477J-38677D01*
G01*
X91200Y485960D02*
G03*
X81040Y475800I0J-10160D01*
G01*
X129382Y474208D02*
G03*
X129072Y375873I12617J-49208D01*
G01*
X81040Y374200D02*
G03*
X91200Y364040I10160J0D01*
G01*
X81040Y24200D02*
G03*
X91200Y14040I10160J0D01*
G01*
X129382Y124208D02*
G03*
X129072Y25873I12617J-49208D01*
G01*
X91200Y135960D02*
G03*
X81040Y125800I0J-10160D01*
G01*
X129523Y113677D02*
G03*
X129523Y36323I12477J-38677D01*
G01*
X154477D02*
G03*
X154477Y113677I-12477J38677D01*
G01*
X192800Y14040D02*
G03*
X202960Y24200I0J10160D01*
G01*
X154617Y25792D02*
G03*
X154929Y124127I-12617J49208D01*
G01*
X202960Y125800D02*
G03*
X192800Y135960I-10160J0D01*
G01*
D11*
X411900Y477400D02*
G03*
X411900Y477400I-300J0D01*
G01*
X449700D02*
G03*
X475100Y477400I12700J0D01*
G01*
X67300Y186500D02*
X92700D01*
Y313500D01*
X67300Y186500D02*
Y313500D01*
X92700D01*
X407000Y373000D02*
Y482000D01*
Y373000D02*
X516000D01*
X407000Y482000D02*
X516000D01*
Y373000D02*
Y482000D01*
X332153Y304294D02*
X321996Y299216D01*
X332153Y294137D01*
X321996Y281441D02*
Y286520D01*
X324536Y289059D01*
X329614D01*
X332153Y286520D01*
Y281441D01*
X329614Y278902D01*
X327075D01*
Y289059D01*
X332153Y273824D02*
X321996D01*
X327075D01*
X329614Y271285D01*
X332153Y268745D01*
Y266206D01*
X321996Y258589D02*
X324536D01*
Y256050D01*
X321996D01*
Y258589D01*
Y230658D02*
Y225579D01*
Y228119D01*
X337231D01*
X334692Y230658D01*
X321996Y217962D02*
X324536D01*
Y215423D01*
X321996D01*
Y217962D01*
X334692Y205266D02*
X337231Y202727D01*
Y197649D01*
X334692Y195109D01*
X324536D01*
X321996Y197649D01*
Y202727D01*
X324536Y205266D01*
X334692D01*
X312853Y337303D02*
X297618D01*
Y344921D01*
X300158Y347460D01*
X305236D01*
X307775Y344921D01*
Y337303D01*
X297618Y332225D02*
Y327147D01*
Y329686D01*
X307775D01*
Y332225D01*
X292540Y314451D02*
Y311912D01*
X295079Y309372D01*
X307775D01*
Y316990D01*
X305236Y319529D01*
X300158D01*
X297618Y316990D01*
Y309372D01*
Y304294D02*
Y299216D01*
Y301755D01*
X307775D01*
Y304294D01*
X310314Y289059D02*
X307775D01*
Y291598D01*
Y286520D01*
Y289059D01*
X300158D01*
X297618Y286520D01*
X307775Y276363D02*
Y271285D01*
X305236Y268745D01*
X297618D01*
Y276363D01*
X300158Y278902D01*
X302697Y276363D01*
Y268745D01*
X297618Y263667D02*
Y258589D01*
Y261128D01*
X312853D01*
Y263667D01*
X297618Y250971D02*
X307775Y240814D01*
Y233197D02*
Y228119D01*
X305236Y225579D01*
X297618D01*
Y233197D01*
X300158Y235736D01*
X302697Y233197D01*
Y225579D01*
X297618Y220501D02*
X307775D01*
Y212884D01*
X305236Y210344D01*
X297618D01*
X307775Y202727D02*
Y197649D01*
X305236Y195109D01*
X297618D01*
Y202727D01*
X300158Y205266D01*
X302697Y202727D01*
Y195109D01*
X297618Y190031D02*
Y184953D01*
Y187492D01*
X312853D01*
Y190031D01*
X297618Y174796D02*
Y169718D01*
X300158Y167178D01*
X305236D01*
X307775Y169718D01*
Y174796D01*
X305236Y177335D01*
X300158D01*
X297618Y174796D01*
X292540Y157022D02*
Y154482D01*
X295079Y151943D01*
X307775D01*
Y159561D01*
X305236Y162100D01*
X300158D01*
X297618Y159561D01*
Y151943D01*
X257657Y105196D02*
X255117Y107735D01*
X250039D01*
X247500Y105196D01*
Y102657D01*
X250039Y100118D01*
X255117D01*
X257657Y97578D01*
Y95039D01*
X255117Y92500D01*
X250039D01*
X247500Y95039D01*
X262735Y107735D02*
Y92500D01*
X267813Y97578D01*
X272892Y92500D01*
Y107735D01*
X277970Y89961D02*
X288127D01*
X293205Y92500D02*
Y107735D01*
X300823D01*
X303362Y105196D01*
Y100118D01*
X300823Y97578D01*
X293205D01*
X316058Y107735D02*
X310979D01*
X308440Y105196D01*
Y95039D01*
X310979Y92500D01*
X316058D01*
X318597Y95039D01*
Y105196D01*
X316058Y107735D01*
X323675D02*
Y92500D01*
X328754Y97578D01*
X333832Y92500D01*
Y107735D01*
X349067D02*
X338910D01*
Y92500D01*
X349067D01*
X338910Y100118D02*
X343989D01*
X354146Y92500D02*
Y107735D01*
X361763D01*
X364302Y105196D01*
Y100118D01*
X361763Y97578D01*
X354146D01*
X359224D02*
X364302Y92500D01*
X45000Y260000D02*
X60235D01*
Y252383D01*
X57696Y249843D01*
X52618D01*
X50078Y252383D01*
Y260000D01*
X45000Y244765D02*
Y239687D01*
Y242226D01*
X60235D01*
X57696Y244765D01*
X560157Y303196D02*
X557617Y305735D01*
X552539D01*
X550000Y303196D01*
Y293039D01*
X552539Y290500D01*
X557617D01*
X560157Y293039D01*
X575392Y290500D02*
X565235D01*
X575392Y300657D01*
Y303196D01*
X572853Y305735D01*
X567774D01*
X565235Y303196D01*
X546157Y357196D02*
X543618Y359735D01*
X538539D01*
X536000Y357196D01*
Y347039D01*
X538539Y344500D01*
X543618D01*
X546157Y347039D01*
X551235Y357196D02*
X553774Y359735D01*
X558853D01*
X561392Y357196D01*
Y354657D01*
X558853Y352118D01*
X556313D01*
X558853D01*
X561392Y349578D01*
Y347039D01*
X558853Y344500D01*
X553774D01*
X551235Y347039D01*
X148000Y140000D02*
Y155235D01*
X155618D01*
X158157Y152696D01*
Y147618D01*
X155618Y145078D01*
X148000D01*
X153078D02*
X158157Y140000D01*
X173392Y155235D02*
X168313Y152696D01*
X163235Y147618D01*
Y142539D01*
X165774Y140000D01*
X170853D01*
X173392Y142539D01*
Y145078D01*
X170853Y147618D01*
X163235D01*
X195000Y140000D02*
Y155235D01*
X202617D01*
X205157Y152696D01*
Y147618D01*
X202617Y145078D01*
X195000D01*
X200078D02*
X205157Y140000D01*
X210235Y155235D02*
X220392D01*
Y152696D01*
X210235Y142539D01*
Y140000D01*
X641000Y347000D02*
Y362235D01*
X648617D01*
X651157Y359696D01*
Y354618D01*
X648617Y352078D01*
X641000D01*
X646078D02*
X651157Y347000D01*
X656235D02*
X661313D01*
X658774D01*
Y362235D01*
X656235Y359696D01*
X668931Y347000D02*
X674009D01*
X671470D01*
Y362235D01*
X668931Y359696D01*
X150000Y295000D02*
X165235D01*
Y287383D01*
X162696Y284843D01*
X157618D01*
X155078Y287383D01*
Y295000D01*
X165235Y272147D02*
Y277226D01*
X162696Y279765D01*
X152539D01*
X150000Y277226D01*
Y272147D01*
X152539Y269608D01*
X162696D01*
X165235Y272147D01*
Y264530D02*
Y254373D01*
Y259452D01*
X150000D01*
X157618Y249295D02*
Y239138D01*
X165235Y234060D02*
X150000D01*
X157618D01*
Y223903D01*
X165235D01*
X150000D01*
X162696Y208668D02*
X165235Y211207D01*
Y216285D01*
X162696Y218825D01*
X152539D01*
X150000Y216285D01*
Y211207D01*
X152539Y208668D01*
X157618D01*
Y213746D01*
X614235Y274843D02*
Y285000D01*
X599000D01*
Y274843D01*
X606618Y285000D02*
Y279922D01*
X611696Y259608D02*
X614235Y262147D01*
Y267226D01*
X611696Y269765D01*
X609157D01*
X606618Y267226D01*
Y262147D01*
X604078Y259608D01*
X601539D01*
X599000Y262147D01*
Y267226D01*
X601539Y269765D01*
X599000Y254530D02*
X614235D01*
Y246912D01*
X611696Y244373D01*
X606618D01*
X604078Y246912D01*
Y254530D01*
X611696Y239295D02*
X614235Y236756D01*
Y231677D01*
X611696Y229138D01*
X609157D01*
X606618Y231677D01*
Y234216D01*
Y231677D01*
X604078Y229138D01*
X601539D01*
X599000Y231677D01*
Y236756D01*
X601539Y239295D01*
X599000Y213903D02*
Y224060D01*
X609157Y213903D01*
X611696D01*
X614235Y216442D01*
Y221521D01*
X611696Y224060D01*
X526235Y105382D02*
Y110461D01*
X523696Y113000D01*
X513539D01*
X511000Y110461D01*
Y105382D01*
X513539Y102843D01*
X523696D01*
X526235Y105382D01*
X511000Y97765D02*
X526235D01*
Y90147D01*
X523696Y87608D01*
X518618D01*
X516078Y90147D01*
Y97765D01*
X511000Y82530D02*
X521157D01*
X526235Y77452D01*
X521157Y72373D01*
X511000D01*
X518618D01*
Y82530D01*
X523696Y67295D02*
X526235Y64756D01*
Y59677D01*
X523696Y57138D01*
X521157D01*
X518618Y59677D01*
Y62216D01*
Y59677D01*
X516078Y57138D01*
X513539D01*
X511000Y59677D01*
Y64756D01*
X513539Y67295D01*
X523696Y52060D02*
X526235Y49521D01*
Y44442D01*
X523696Y41903D01*
X521157D01*
X518618Y44442D01*
X516078Y41903D01*
X513539D01*
X511000Y44442D01*
Y49521D01*
X513539Y52060D01*
X516078D01*
X518618Y49521D01*
X521157Y52060D01*
X523696D01*
X518618Y49521D02*
Y44442D01*
X523696Y36825D02*
X526235Y34286D01*
Y29207D01*
X523696Y26668D01*
X513539D01*
X511000Y29207D01*
Y34286D01*
X513539Y36825D01*
X523696D01*
X435000Y132000D02*
Y147235D01*
X442617D01*
X445157Y144696D01*
Y139618D01*
X442617Y137078D01*
X435000D01*
X440078D02*
X445157Y132000D01*
X460392Y147235D02*
X450235D01*
Y139618D01*
X455313Y142157D01*
X457853D01*
X460392Y139618D01*
Y134539D01*
X457853Y132000D01*
X452774D01*
X450235Y134539D01*
X562235Y456183D02*
Y461261D01*
X559696Y463800D01*
X549539D01*
X547000Y461261D01*
Y456183D01*
X549539Y453643D01*
X559696D01*
X562235Y456183D01*
X547000Y448565D02*
X562235D01*
Y440947D01*
X559696Y438408D01*
X554618D01*
X552078Y440947D01*
Y448565D01*
X562235Y433330D02*
Y423173D01*
Y428251D01*
X547000D01*
X544461Y418095D02*
Y407938D01*
X562235Y402860D02*
Y392703D01*
Y397781D01*
X547000D01*
X562235Y387625D02*
X547000Y377468D01*
X562235D02*
X547000Y387625D01*
X470235Y455000D02*
Y444843D01*
X467696D01*
X457539Y455000D01*
X455000D01*
X470235Y429608D02*
Y439765D01*
X462617D01*
X465157Y434687D01*
Y432147D01*
X462617Y429608D01*
X457539D01*
X455000Y432147D01*
Y437226D01*
X457539Y439765D01*
X455000Y416912D02*
X470235D01*
X462617Y424530D01*
Y414373D01*
X470235Y399138D02*
Y409295D01*
X462617D01*
X465157Y404216D01*
Y401677D01*
X462617Y399138D01*
X457539D01*
X455000Y401677D01*
Y406756D01*
X457539Y409295D01*
X455000Y394060D02*
Y388981D01*
Y391521D01*
X470235D01*
X467696Y394060D01*
X562235Y112382D02*
Y117461D01*
X559696Y120000D01*
X549539D01*
X547000Y117461D01*
Y112382D01*
X549539Y109843D01*
X559696D01*
X562235Y112382D01*
X547000Y104765D02*
X562235D01*
Y97147D01*
X559696Y94608D01*
X554618D01*
X552078Y97147D01*
Y104765D01*
X562235Y89530D02*
Y79373D01*
Y84452D01*
X547000D01*
X544461Y74295D02*
Y64138D01*
X547000Y59060D02*
X562235D01*
Y51442D01*
X559696Y48903D01*
X554618D01*
X552078Y51442D01*
Y59060D01*
Y53981D02*
X547000Y48903D01*
X562235Y43825D02*
X547000Y33668D01*
X562235D02*
X547000Y43825D01*
X297617Y405235D02*
X292539D01*
X290000Y402696D01*
Y392539D01*
X292539Y390000D01*
X297617D01*
X300157Y392539D01*
Y402696D01*
X297617Y405235D01*
X305235D02*
Y390000D01*
X315392D01*
X330627Y405235D02*
X320470D01*
Y390000D01*
X330627D01*
X320470Y397617D02*
X325549D01*
X335705Y405235D02*
Y390000D01*
X343323D01*
X345862Y392539D01*
Y402696D01*
X343323Y405235D01*
X335705D01*
X57696Y162343D02*
X60235Y164883D01*
Y169961D01*
X57696Y172500D01*
X55157D01*
X52618Y169961D01*
Y164883D01*
X50078Y162343D01*
X47539D01*
X45000Y164883D01*
Y169961D01*
X47539Y172500D01*
X60235Y157265D02*
X45000D01*
X50078Y152187D01*
X45000Y147108D01*
X60235D01*
X42461Y142030D02*
Y131873D01*
X57696Y116638D02*
X60235Y119177D01*
Y124256D01*
X57696Y126795D01*
X55157D01*
X52618Y124256D01*
Y119177D01*
X50078Y116638D01*
X47539D01*
X45000Y119177D01*
Y124256D01*
X47539Y126795D01*
X60235Y111560D02*
Y101403D01*
Y106481D01*
X45000D01*
Y96325D02*
X55157D01*
X60235Y91246D01*
X55157Y86168D01*
X45000D01*
X52618D01*
Y96325D01*
X45000Y81090D02*
X60235D01*
Y73472D01*
X57696Y70933D01*
X52618D01*
X50078Y73472D01*
Y81090D01*
Y76011D02*
X45000Y70933D01*
X60235Y65855D02*
Y55698D01*
Y60776D01*
X45000D01*
X62696Y434843D02*
X65235Y437383D01*
Y442461D01*
X62696Y445000D01*
X60157D01*
X57618Y442461D01*
Y437383D01*
X55078Y434843D01*
X52539D01*
X50000Y437383D01*
Y442461D01*
X52539Y445000D01*
X65235Y429765D02*
X50000D01*
X55078Y424687D01*
X50000Y419608D01*
X65235D01*
X47461Y414530D02*
Y404373D01*
X50000Y399295D02*
X65235D01*
X60157Y394216D01*
X65235Y389138D01*
X50000D01*
X65235Y376442D02*
Y381521D01*
X62696Y384060D01*
X52539D01*
X50000Y381521D01*
Y376442D01*
X52539Y373903D01*
X62696D01*
X65235Y376442D01*
Y368825D02*
X50000D01*
Y361207D01*
X52539Y358668D01*
X62696D01*
X65235Y361207D01*
Y368825D01*
Y343433D02*
Y353590D01*
X50000D01*
Y343433D01*
X57618Y353590D02*
Y348511D01*
D12*
X466000Y20500D02*
G03*
X444000Y20500I-11000J0D01*
G01*
X310000Y18000D02*
Y72000D01*
X283000Y45000D02*
X337000D01*
X365000Y5000D02*
Y85000D01*
X255000Y5000D02*
X365000D01*
X255000D02*
Y85000D01*
X365000D01*
X125490Y408490D02*
X158510D01*
X125490Y441510D02*
X158510D01*
Y408490D02*
Y441510D01*
X125490Y408490D02*
Y441510D01*
X126760Y364040D02*
X192800D01*
X161050Y405950D02*
Y444050D01*
X126760Y364040D02*
Y370390D01*
X91200Y485960D02*
X192800D01*
X122950Y405950D02*
X161050D01*
X122950Y444050D02*
X161050D01*
X122950Y405950D02*
Y444050D01*
X106440Y370390D02*
X126760D01*
X106440Y364040D02*
Y370390D01*
X91200Y364040D02*
X106440D01*
X209000Y164000D02*
Y176000D01*
X161500Y164000D02*
Y176000D01*
X403000Y20500D02*
Y124500D01*
Y20500D02*
X507000D01*
X403000Y124500D02*
X507000D01*
Y20500D02*
Y124500D01*
X135000Y201000D02*
Y299000D01*
X244999Y201000D02*
Y299000D01*
X91200Y14040D02*
X106440D01*
Y20390D01*
X126760D01*
X122950Y55950D02*
Y94050D01*
X161050D01*
X122950Y55950D02*
X161050D01*
X91200Y135960D02*
X192800D01*
X126760Y14040D02*
Y20390D01*
X161050Y55950D02*
Y94050D01*
X126760Y14040D02*
X192800D01*
X125490Y58490D02*
Y91510D01*
X158510Y58490D02*
Y91510D01*
X125490D02*
X158510D01*
X125490Y58490D02*
X158510D01*
X443000Y104000D02*
X455000D01*
X483500Y133000D02*
Y145000D01*
X620000Y348000D02*
Y360000D01*
D13*
X130800Y285000D02*
G03*
X130800Y285000I-800J0D01*
G01*
X256200Y140000D02*
Y444000D01*
Y140000D02*
X378200D01*
X256200Y444000D02*
X378200D01*
Y140000D02*
Y444000D01*
X141497Y354635D02*
X131500D01*
Y349636D01*
X133166Y347970D01*
X139831D01*
X141497Y349636D01*
Y354635D01*
X136498Y344638D02*
Y337974D01*
X141497Y327135D02*
X131500D01*
Y322136D01*
X133166Y320470D01*
X139831D01*
X141497Y322136D01*
Y327135D01*
X136498Y317138D02*
Y310473D01*
X139831Y313806D02*
X133166D01*
D14*
X191474Y160348D02*
X205190D01*
X212810Y160348D02*
X226526Y160348D01*
X191474D02*
Y179652D01*
X205190D01*
X226526Y160348D02*
Y179652D01*
X212810Y179652D02*
X226526Y179652D01*
X165310Y160348D02*
X179026Y160348D01*
Y179652D01*
X165310Y179652D02*
X179026Y179652D01*
X143974Y160348D02*
X157690D01*
X143974Y179652D02*
X157690D01*
X143974Y160348D02*
Y179652D01*
X545526Y301540D02*
Y309160D01*
Y288840D02*
Y296460D01*
X510474Y309160D02*
X545526D01*
X510474Y301540D02*
Y309160D01*
Y288840D02*
X545526D01*
X510474D02*
Y296460D01*
X532526Y354540D02*
Y362160D01*
Y341840D02*
Y349460D01*
X497474Y362160D02*
X532526D01*
X497474Y354540D02*
Y362160D01*
Y341840D02*
X532526D01*
X497474D02*
Y349460D01*
X459660Y35474D02*
Y70526D01*
X452040Y35474D02*
X459660D01*
X452040Y70526D02*
X459660D01*
X439340Y35474D02*
X446960D01*
X439340Y70526D02*
X446960D01*
X439340Y35474D02*
Y70526D01*
X480160Y35474D02*
Y70526D01*
X472540Y35474D02*
X480160D01*
X472540Y70526D02*
X480160D01*
X459840Y35474D02*
X467460D01*
X459840Y70526D02*
X467460D01*
X459840Y35474D02*
Y70526D01*
X458652Y121526D02*
X458652Y107810D01*
X439348Y121526D02*
X458652D01*
X439348Y107810D02*
X439348Y121526D01*
X458652Y86474D02*
Y100190D01*
X439348Y86474D02*
X458652D01*
X439348D02*
Y100190D01*
X465974Y148652D02*
X479690Y148652D01*
X465974Y129348D02*
Y148652D01*
Y129348D02*
X479690Y129348D01*
X487310Y148652D02*
X501026D01*
Y129348D02*
Y148652D01*
X487310Y129348D02*
X501026D01*
X623810Y344348D02*
X637526Y344348D01*
Y363652D01*
X623810Y363652D02*
X637526Y363652D01*
X602474Y344348D02*
X616190D01*
X602474D02*
Y363652D01*
X616190D01*
D15*
X485000Y158000D02*
Y190000D01*
Y158000D02*
X694000D01*
X485000Y190000D02*
X694000D01*
X484000Y310000D02*
Y342000D01*
Y310000D02*
X694000D01*
X484000Y342000D02*
X694000D01*
Y158000D02*
Y190000D01*
Y310000D02*
Y342000D01*
D16*
X572250Y21000D02*
Y134000D01*
Y21000D02*
X696000D01*
X572250Y134000D02*
X696000D01*
Y21000D02*
Y134000D01*
X572250Y366000D02*
Y477000D01*
Y366000D02*
X696000D01*
X572250Y477000D02*
X696000D01*
Y366000D02*
Y477000D01*
D17*
X477000Y91000D02*
X461765D01*
Y98617D01*
X464304Y101157D01*
X469383D01*
X471922Y98617D01*
Y91000D01*
Y96078D02*
X477000Y101157D01*
Y113853D02*
X461765D01*
X469383Y106235D01*
Y116392D01*
X440664Y82331D02*
X438998Y83997D01*
X435666D01*
X434000Y82331D01*
Y75666D01*
X435666Y74000D01*
X438998D01*
X440664Y75666D01*
X443997Y74000D02*
X447329D01*
X445663D01*
Y83997D01*
X443997Y82331D01*
X452327D02*
X453994Y83997D01*
X457326D01*
X458992Y82331D01*
Y75666D01*
X457326Y74000D01*
X453994D01*
X452327Y75666D01*
Y82331D01*
M02*

View File

@@ -7,28 +7,43 @@ G04 Layer_Color=8421504*
G71* G71*
G01* G01*
G75* G75*
%ADD18R,1.3000X1.5000*% %ADD18R,2.5000X2.2000*%
%ADD19R,1.5000X1.3000*% %ADD19R,1.2000X1.4000*%
%ADD20R,1.4000X1.2000*% %ADD20R,1.3000X1.5000*%
%ADD21R,1.2000X1.4000*% %ADD21R,1.5000X1.3000*%
%ADD22R,1.4000X1.2000*%
D18* D18*
X523500Y299500D02* X382500Y45000D02*
D03*
X237500D02*
D03*
D19*
X200000Y170000D02*
D03*
X218000D02*
D03*
X170500D02*
D03*
X152500D02*
D03*
X474500Y139000D02*
D03*
X492500D02*
D03*
X629000Y354000D02*
D03*
X611000D02*
D03*
D20*
X536500Y299000D02*
D03*
X519500D02*
D03*
X523500Y352000D02*
D03* D03*
X506500D02* X506500D02*
D03* D03*
Y352000D02* D21*
D03*
X523500D02*
D03*
X108500Y375000D02*
D03*
X91500D02*
D03*
X108500Y400000D02*
D03*
X91500D02*
D03*
D19*
X449500Y44500D02* X449500Y44500D02*
D03* D03*
Y61500D02* Y61500D02*
@@ -37,14 +52,9 @@ X470000Y44500D02*
D03* D03*
Y61500D02* Y61500D02*
D03* D03*
D20* D22*
X449000Y113000D02* X449000Y113000D02*
D03* D03*
Y95000D02* Y95000D02*
D03* D03*
D21*
X474500Y139000D02*
D03*
X492500D02*
D03*
M02* M02*

View File

@@ -0,0 +1,262 @@
G04*
G04 #@! TF.GenerationSoftware,Altium Limited,Altium Designer,19.1.9 (167)*
G04*
G04 Layer_Color=8388736*
%FSLAX44Y44*%
%MOMM*%
G71*
G01*
G75*
%ADD23R,2.6500X2.3500*%
%ADD24R,1.3500X1.5500*%
%ADD25R,1.4500X1.6500*%
%ADD26R,1.6500X1.4500*%
%ADD27R,1.5500X1.3500*%
%ADD28C,1.6740*%
%ADD29C,1.8500*%
%ADD30O,2.6000X2.0000*%
%ADD31C,1.6500*%
%ADD32R,1.6500X1.6500*%
%ADD33R,1.6500X1.6500*%
%ADD34C,3.1500*%
%ADD35C,2.1500*%
%ADD36C,1.9500*%
%ADD37R,1.9500X1.9500*%
%ADD38C,1.7500*%
%ADD39R,1.7500X1.7500*%
%ADD40R,1.8500X1.8500*%
%ADD41R,1.7500X1.7500*%
%ADD42C,1.7500*%
D23*
X382500Y45000D02*
D03*
X237500D02*
D03*
D24*
X200000Y170000D02*
D03*
X218000D02*
D03*
X170500D02*
D03*
X152500D02*
D03*
X474500Y139000D02*
D03*
X492500D02*
D03*
X629000Y354000D02*
D03*
X611000D02*
D03*
D25*
X536500Y299000D02*
D03*
X519500D02*
D03*
X523500Y352000D02*
D03*
X506500D02*
D03*
D26*
X449500Y44500D02*
D03*
Y61500D02*
D03*
X470000Y44500D02*
D03*
Y61500D02*
D03*
D27*
X449000Y113000D02*
D03*
Y95000D02*
D03*
D28*
X650000Y250000D02*
D03*
X25000Y475000D02*
D03*
Y25000D02*
D03*
D29*
X120000Y347500D02*
D03*
Y320000D02*
D03*
X355400Y430000D02*
D03*
X330000D02*
D03*
X304600D02*
D03*
X587000Y299000D02*
D03*
X235000Y80000D02*
D03*
X245000Y170000D02*
D03*
X574999Y198000D02*
D03*
X528000Y387000D02*
D03*
X500000Y285000D02*
D03*
X543999Y231999D02*
D03*
X451000Y274000D02*
D03*
X520000Y224000D02*
D03*
D30*
X80000Y450000D02*
D03*
X204000D02*
D03*
X80000Y400000D02*
D03*
X204000D02*
D03*
X80000Y100000D02*
D03*
X204000D02*
D03*
X80000Y50000D02*
D03*
X204000D02*
D03*
D31*
X80000Y300800D02*
D03*
Y275400D02*
D03*
Y250000D02*
D03*
Y224600D02*
D03*
X254600Y119000D02*
D03*
X356200D02*
D03*
X330800D02*
D03*
X305400D02*
D03*
X280000D02*
D03*
D32*
X80000Y199200D02*
D03*
D33*
X381600Y119000D02*
D03*
D34*
X190000Y203000D02*
D03*
Y297000D02*
D03*
D35*
X120000Y225000D02*
D03*
Y250000D02*
D03*
Y275000D02*
D03*
D36*
X677000Y383800D02*
D03*
Y460000D02*
D03*
X600800D02*
D03*
Y434600D02*
D03*
Y409200D02*
D03*
X677000Y40000D02*
D03*
Y116200D02*
D03*
X600800D02*
D03*
Y90800D02*
D03*
Y65400D02*
D03*
X678900Y326200D02*
D03*
X653500D02*
D03*
X628100D02*
D03*
X602700D02*
D03*
X577300D02*
D03*
X551900D02*
D03*
X526500D02*
D03*
X501100D02*
D03*
X678900Y173800D02*
D03*
X653500D02*
D03*
X628100D02*
D03*
X602700D02*
D03*
X577300D02*
D03*
X551900D02*
D03*
X526500D02*
D03*
X501100D02*
D03*
D37*
X600800Y383800D02*
D03*
Y40000D02*
D03*
D38*
X416900Y34400D02*
D03*
Y59800D02*
D03*
Y85200D02*
D03*
Y110600D02*
D03*
X493100D02*
D03*
Y85200D02*
D03*
Y59800D02*
D03*
D39*
Y34400D02*
D03*
D40*
X279200Y430000D02*
D03*
D41*
X424300Y464684D02*
D03*
D42*
Y439300D02*
D03*
Y413900D02*
D03*
Y388500D02*
D03*
X500500D02*
D03*
Y413900D02*
D03*
Y439300D02*
D03*
Y464700D02*
D03*
M02*

View File

@@ -0,0 +1,108 @@
M48
;Layer_Color=9474304
;FILE_FORMAT=4:4
METRIC,LZ
;TYPE=PLATED
T1F00S00C0.8000
T2F00S00C0.9000
T3F00S00C1.0000
T4F00S00C1.1000
T5F00S00C1.2000
T6F00S00C2.2000
;TYPE=NON_PLATED
T7F00S00C3.1000
%
T01
X004169Y000344
Y000598
X004931
Y000344
Y000852
Y001106
X004169
Y000852
T02
X00574999Y00198
X00587Y00299
X005Y00285
X00451Y00274
X0052Y00224
X00543999Y00231999
X00528Y00387
X003816Y00119
X003562
X003308
X003054
X0028
X002546
X00235Y0008
X00245Y0017
X0008Y001992
Y002246
Y0025
Y002754
Y003008
X0012Y0032
Y003475
T03
X005773Y001738
X006027
X006281
X006535
X006789
Y003262
X006535
X006281
X006027
X005773
X005519
X005265
X005011
X005005Y003885
Y004139
Y004393
X004243
Y004139
Y003885
X003554Y0043
X0033
X003046
X002792
X005011Y001738
X005265
X005519
X0012Y00225
Y0025
Y00275
X004243Y00464684
X005005Y004647
T04
X006008Y0004
X00677
Y001162
X006008
Y000908
Y000654
Y003838
Y004092
Y004346
X00677Y003838
Y0046
X006008
T05
X0008Y0005
X00204
Y001
Y004
Y0045
X0008
Y004
Y001
T06
X0019Y00203
Y00297
T07
X00025Y00025
X0065Y0025
X00025Y00475
M30

Binary file not shown.

BIN
PCB/Libs/1208YD.PcbLib Normal file

Binary file not shown.

BIN
PCB/Libs/1208YD.SchLib Normal file

Binary file not shown.

BIN
PCB/Libs/MSK12C02.PcbLib Normal file

Binary file not shown.

BIN
PCB/Libs/MSK12C02.SchLib Normal file

Binary file not shown.

5352
PCB/Libs/MSK12C02.step Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

BIN
PCB/Libs/POT.SchLib Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -38,23 +38,6 @@ PrefsVaultGUID=
PrefsRevisionGUID= PrefsRevisionGUID=
[Document1] [Document1]
DocumentPath=..\..\..\..\SSSAlDataBaseLib\SssAccesDatbase.DbLib
AnnotationEnabled=1
AnnotateStartValue=1
AnnotationIndexControlEnabled=0
AnnotateSuffix=
AnnotateScope=All
AnnotateOrder=-1
DoLibraryUpdate=1
DoDatabaseUpdate=1
ClassGenCCAutoEnabled=1
ClassGenCCAutoRoomEnabled=1
ClassGenNCAutoScope=None
DItemRevisionGUID=
GenerateClassCluster=0
DocumentUniqueId=
[Document2]
DocumentPath=Libs\Sensor Light.SchLib DocumentPath=Libs\Sensor Light.SchLib
AnnotationEnabled=1 AnnotationEnabled=1
AnnotateStartValue=1 AnnotateStartValue=1
@@ -71,7 +54,7 @@ DItemRevisionGUID=
GenerateClassCluster=0 GenerateClassCluster=0
DocumentUniqueId=XHSRCRCV DocumentUniqueId=XHSRCRCV
[Document3] [Document2]
DocumentPath=Libs\ESP32-C3-SuperMini.SchLib DocumentPath=Libs\ESP32-C3-SuperMini.SchLib
AnnotationEnabled=1 AnnotationEnabled=1
AnnotateStartValue=1 AnnotateStartValue=1
@@ -86,9 +69,9 @@ ClassGenCCAutoRoomEnabled=1
ClassGenNCAutoScope=None ClassGenNCAutoScope=None
DItemRevisionGUID= DItemRevisionGUID=
GenerateClassCluster=0 GenerateClassCluster=0
DocumentUniqueId=FNFLLWRF DocumentUniqueId=URAFKFNP
[Document4] [Document3]
DocumentPath=Libs\ESP32-S3-SuperMini.SchLib DocumentPath=Libs\ESP32-S3-SuperMini.SchLib
AnnotationEnabled=1 AnnotationEnabled=1
AnnotateStartValue=1 AnnotateStartValue=1
@@ -103,9 +86,9 @@ ClassGenCCAutoRoomEnabled=1
ClassGenNCAutoScope=None ClassGenNCAutoScope=None
DItemRevisionGUID= DItemRevisionGUID=
GenerateClassCluster=0 GenerateClassCluster=0
DocumentUniqueId=IWYWSUPY DocumentUniqueId=KNRSSPNM
[Document5] [Document4]
DocumentPath=Libs\HFBR-1528Z_HFBR-2528Z.PcbLib DocumentPath=Libs\HFBR-1528Z_HFBR-2528Z.PcbLib
AnnotationEnabled=1 AnnotationEnabled=1
AnnotateStartValue=1 AnnotateStartValue=1
@@ -122,7 +105,7 @@ DItemRevisionGUID=
GenerateClassCluster=0 GenerateClassCluster=0
DocumentUniqueId= DocumentUniqueId=
[Document6] [Document5]
DocumentPath=OptoTest.PcbDoc DocumentPath=OptoTest.PcbDoc
AnnotationEnabled=1 AnnotationEnabled=1
AnnotateStartValue=1 AnnotateStartValue=1
@@ -139,7 +122,7 @@ DItemRevisionGUID=
GenerateClassCluster=0 GenerateClassCluster=0
DocumentUniqueId=VJUIGDUR DocumentUniqueId=VJUIGDUR
[Document7] [Document6]
DocumentPath=OptoTest.SchDoc DocumentPath=OptoTest.SchDoc
AnnotationEnabled=1 AnnotationEnabled=1
AnnotateStartValue=1 AnnotateStartValue=1
@@ -156,7 +139,7 @@ DItemRevisionGUID=
GenerateClassCluster=0 GenerateClassCluster=0
DocumentUniqueId=OSZVBCBN DocumentUniqueId=OSZVBCBN
[Document8] [Document7]
DocumentPath=Libs\ESP32-C3-SuperMini.PcbLib DocumentPath=Libs\ESP32-C3-SuperMini.PcbLib
AnnotationEnabled=1 AnnotationEnabled=1
AnnotateStartValue=1 AnnotateStartValue=1
@@ -173,7 +156,7 @@ DItemRevisionGUID=
GenerateClassCluster=0 GenerateClassCluster=0
DocumentUniqueId=SBGUKPNI DocumentUniqueId=SBGUKPNI
[Document9] [Document8]
DocumentPath=Libs\ESP32-S3-SuperMini.PcbLib DocumentPath=Libs\ESP32-S3-SuperMini.PcbLib
AnnotationEnabled=1 AnnotationEnabled=1
AnnotateStartValue=1 AnnotateStartValue=1
@@ -190,7 +173,7 @@ DItemRevisionGUID=
GenerateClassCluster=0 GenerateClassCluster=0
DocumentUniqueId=BAPDEFTM DocumentUniqueId=BAPDEFTM
[Document10] [Document9]
DocumentPath=Libs\Switch Push Botton.PcbLib DocumentPath=Libs\Switch Push Botton.PcbLib
AnnotationEnabled=1 AnnotationEnabled=1
AnnotateStartValue=1 AnnotateStartValue=1
@@ -207,7 +190,7 @@ DItemRevisionGUID=
GenerateClassCluster=0 GenerateClassCluster=0
DocumentUniqueId= DocumentUniqueId=
[Document11] [Document10]
DocumentPath=Libs\Switch Push Botton.SchLib DocumentPath=Libs\Switch Push Botton.SchLib
AnnotationEnabled=1 AnnotationEnabled=1
AnnotateStartValue=1 AnnotateStartValue=1
@@ -224,7 +207,7 @@ DItemRevisionGUID=
GenerateClassCluster=0 GenerateClassCluster=0
DocumentUniqueId=ITMPIRQI DocumentUniqueId=ITMPIRQI
[Document12] [Document11]
DocumentPath=Libs\MOD_OLED_0.91_128x32.PcbLib DocumentPath=Libs\MOD_OLED_0.91_128x32.PcbLib
AnnotationEnabled=1 AnnotationEnabled=1
AnnotateStartValue=1 AnnotateStartValue=1
@@ -241,7 +224,7 @@ DItemRevisionGUID=
GenerateClassCluster=0 GenerateClassCluster=0
DocumentUniqueId= DocumentUniqueId=
[Document13] [Document12]
DocumentPath=Libs\MOD_OLED_0.91_128x32.SchLib DocumentPath=Libs\MOD_OLED_0.91_128x32.SchLib
AnnotationEnabled=1 AnnotationEnabled=1
AnnotateStartValue=1 AnnotateStartValue=1
@@ -258,7 +241,7 @@ DItemRevisionGUID=
GenerateClassCluster=0 GenerateClassCluster=0
DocumentUniqueId=UNSXUFGW DocumentUniqueId=UNSXUFGW
[Document14] [Document13]
DocumentPath=Libs\SN75451BP.SchLib DocumentPath=Libs\SN75451BP.SchLib
AnnotationEnabled=1 AnnotationEnabled=1
AnnotateStartValue=1 AnnotateStartValue=1
@@ -273,9 +256,9 @@ ClassGenCCAutoRoomEnabled=1
ClassGenNCAutoScope=None ClassGenNCAutoScope=None
DItemRevisionGUID= DItemRevisionGUID=
GenerateClassCluster=0 GenerateClassCluster=0
DocumentUniqueId=LAFSSKWH DocumentUniqueId=A8G5624N
[Document15] [Document14]
DocumentPath=Libs\SN75451BP.PcbLib DocumentPath=Libs\SN75451BP.PcbLib
AnnotationEnabled=1 AnnotationEnabled=1
AnnotateStartValue=1 AnnotateStartValue=1
@@ -292,7 +275,7 @@ DItemRevisionGUID=
GenerateClassCluster=0 GenerateClassCluster=0
DocumentUniqueId= DocumentUniqueId=
[Document16] [Document15]
DocumentPath=Libs\J_ARK2.Schlib DocumentPath=Libs\J_ARK2.Schlib
AnnotationEnabled=1 AnnotationEnabled=1
AnnotateStartValue=1 AnnotateStartValue=1
@@ -309,7 +292,7 @@ DItemRevisionGUID=
GenerateClassCluster=0 GenerateClassCluster=0
DocumentUniqueId=HIEGOULK DocumentUniqueId=HIEGOULK
[Document17] [Document16]
DocumentPath=Libs\J_ARK2_2.54.PcbLib DocumentPath=Libs\J_ARK2_2.54.PcbLib
AnnotationEnabled=1 AnnotationEnabled=1
AnnotateStartValue=1 AnnotateStartValue=1
@@ -326,7 +309,7 @@ DItemRevisionGUID=
GenerateClassCluster=0 GenerateClassCluster=0
DocumentUniqueId= DocumentUniqueId=
[Document18] [Document17]
DocumentPath=Libs\Sensor.PcbLib DocumentPath=Libs\Sensor.PcbLib
AnnotationEnabled=1 AnnotationEnabled=1
AnnotateStartValue=1 AnnotateStartValue=1
@@ -343,7 +326,7 @@ DItemRevisionGUID=
GenerateClassCluster=0 GenerateClassCluster=0
DocumentUniqueId=SCOMYVDB DocumentUniqueId=SCOMYVDB
[Document19] [Document18]
DocumentPath=Libs\BPW34.PcbLib DocumentPath=Libs\BPW34.PcbLib
AnnotationEnabled=1 AnnotationEnabled=1
AnnotateStartValue=1 AnnotateStartValue=1
@@ -360,7 +343,7 @@ DItemRevisionGUID=
GenerateClassCluster=0 GenerateClassCluster=0
DocumentUniqueId=DEJBNXAE DocumentUniqueId=DEJBNXAE
[Document20] [Document19]
DocumentPath=Libs\OPA380.PcbLib DocumentPath=Libs\OPA380.PcbLib
AnnotationEnabled=1 AnnotationEnabled=1
AnnotateStartValue=1 AnnotateStartValue=1
@@ -377,7 +360,7 @@ DItemRevisionGUID=
GenerateClassCluster=0 GenerateClassCluster=0
DocumentUniqueId=TPLFYPML DocumentUniqueId=TPLFYPML
[Document21] [Document20]
DocumentPath=Libs\OPA380.SchLib DocumentPath=Libs\OPA380.SchLib
AnnotationEnabled=1 AnnotationEnabled=1
AnnotateStartValue=1 AnnotateStartValue=1
@@ -392,9 +375,9 @@ ClassGenCCAutoRoomEnabled=1
ClassGenNCAutoScope=None ClassGenNCAutoScope=None
DItemRevisionGUID= DItemRevisionGUID=
GenerateClassCluster=0 GenerateClassCluster=0
DocumentUniqueId=A8G5624N DocumentUniqueId=MBMSOHVF
[Document22] [Document21]
DocumentPath=Libs\J_ARK3_2.54.PcbLib DocumentPath=Libs\J_ARK3_2.54.PcbLib
AnnotationEnabled=1 AnnotationEnabled=1
AnnotateStartValue=1 AnnotateStartValue=1
@@ -411,7 +394,7 @@ DItemRevisionGUID=
GenerateClassCluster=0 GenerateClassCluster=0
DocumentUniqueId= DocumentUniqueId=
[Document23] [Document22]
DocumentPath=Libs\J_ARK3.SchLib DocumentPath=Libs\J_ARK3.SchLib
AnnotationEnabled=1 AnnotationEnabled=1
AnnotateStartValue=1 AnnotateStartValue=1
@@ -426,10 +409,27 @@ ClassGenCCAutoRoomEnabled=1
ClassGenNCAutoScope=None ClassGenNCAutoScope=None
DItemRevisionGUID= DItemRevisionGUID=
GenerateClassCluster=0 GenerateClassCluster=0
DocumentUniqueId=WYDOVXHH DocumentUniqueId=GMSJVYGO
[Document23]
DocumentPath=Libs\POT.SchLib
AnnotationEnabled=1
AnnotateStartValue=1
AnnotationIndexControlEnabled=0
AnnotateSuffix=
AnnotateScope=All
AnnotateOrder=-1
DoLibraryUpdate=1
DoDatabaseUpdate=1
ClassGenCCAutoEnabled=1
ClassGenCCAutoRoomEnabled=1
ClassGenNCAutoScope=None
DItemRevisionGUID=
GenerateClassCluster=0
DocumentUniqueId=JKPGHLQK
[Document24] [Document24]
DocumentPath=gerber.Cam DocumentPath=Libs\POT_RK09D1130C2P.PcbLib
AnnotationEnabled=1 AnnotationEnabled=1
AnnotateStartValue=1 AnnotateStartValue=1
AnnotationIndexControlEnabled=0 AnnotationIndexControlEnabled=0
@@ -446,7 +446,24 @@ GenerateClassCluster=0
DocumentUniqueId= DocumentUniqueId=
[Document25] [Document25]
DocumentPath=nc_drills.Cam DocumentPath=Libs\P_HEADER_1x5.SchLib
AnnotationEnabled=1
AnnotateStartValue=1
AnnotationIndexControlEnabled=0
AnnotateSuffix=
AnnotateScope=All
AnnotateOrder=-1
DoLibraryUpdate=1
DoDatabaseUpdate=1
ClassGenCCAutoEnabled=1
ClassGenCCAutoRoomEnabled=1
ClassGenNCAutoScope=None
DItemRevisionGUID=
GenerateClassCluster=0
DocumentUniqueId=KQQWVKPH
[Document26]
DocumentPath=Libs\P_HEADER_1x5_2.54_ANGLE.PcbLib
AnnotationEnabled=1 AnnotationEnabled=1
AnnotateStartValue=1 AnnotateStartValue=1
AnnotationIndexControlEnabled=0 AnnotationIndexControlEnabled=0
@@ -462,65 +479,39 @@ DItemRevisionGUID=
GenerateClassCluster=0 GenerateClassCluster=0
DocumentUniqueId= DocumentUniqueId=
[GeneratedDocument1] [Document27]
DocumentPath=Project Outputs for OptoTest\OptoTest.DRR DocumentPath=Libs\1208YD.PcbLib
AnnotationEnabled=1
AnnotateStartValue=1
AnnotationIndexControlEnabled=0
AnnotateSuffix=
AnnotateScope=All
AnnotateOrder=-1
DoLibraryUpdate=1
DoDatabaseUpdate=1
ClassGenCCAutoEnabled=1
ClassGenCCAutoRoomEnabled=1
ClassGenNCAutoScope=None
DItemRevisionGUID= DItemRevisionGUID=
GenerateClassCluster=0
DocumentUniqueId=
[GeneratedDocument2] [Document28]
DocumentPath=Project Outputs for OptoTest\OptoTest.EXTREP DocumentPath=Libs\1208YD.SchLib
DItemRevisionGUID= AnnotationEnabled=1
AnnotateStartValue=1
[GeneratedDocument3] AnnotationIndexControlEnabled=0
DocumentPath=Project Outputs for OptoTest\OptoTest.GBL AnnotateSuffix=
DItemRevisionGUID= AnnotateScope=All
AnnotateOrder=-1
[GeneratedDocument4] DoLibraryUpdate=1
DocumentPath=Project Outputs for OptoTest\OptoTest.GBO DoDatabaseUpdate=1
DItemRevisionGUID= ClassGenCCAutoEnabled=1
ClassGenCCAutoRoomEnabled=1
[GeneratedDocument5] ClassGenNCAutoScope=None
DocumentPath=Project Outputs for OptoTest\OptoTest.GBP
DItemRevisionGUID=
[GeneratedDocument6]
DocumentPath=Project Outputs for OptoTest\OptoTest.GBS
DItemRevisionGUID=
[GeneratedDocument7]
DocumentPath=Project Outputs for OptoTest\OptoTest.GKO
DItemRevisionGUID=
[GeneratedDocument8]
DocumentPath=Project Outputs for OptoTest\OptoTest.GTL
DItemRevisionGUID=
[GeneratedDocument9]
DocumentPath=Project Outputs for OptoTest\OptoTest.GTO
DItemRevisionGUID=
[GeneratedDocument10]
DocumentPath=Project Outputs for OptoTest\OptoTest.GTP
DItemRevisionGUID=
[GeneratedDocument11]
DocumentPath=Project Outputs for OptoTest\OptoTest.GTS
DItemRevisionGUID=
[GeneratedDocument12]
DocumentPath=Project Outputs for OptoTest\OptoTest.LDP
DItemRevisionGUID=
[GeneratedDocument13]
DocumentPath=Project Outputs for OptoTest\OptoTest.REP
DItemRevisionGUID=
[GeneratedDocument14]
DocumentPath=Project Outputs for OptoTest\OptoTest.RUL
DItemRevisionGUID=
[GeneratedDocument15]
DocumentPath=Project Outputs for OptoTest\OptoTest.TXT
DItemRevisionGUID= DItemRevisionGUID=
GenerateClassCluster=0
DocumentUniqueId=GVWJKGKK
[Configuration1] [Configuration1]
Name=Sources Name=Sources
@@ -641,7 +632,7 @@ OutputDefault20=0
[OutputGroup2] [OutputGroup2]
Name=Simulator Outputs Name=Simulator Outputs
Description= Description=
TargetPrinter=Microsoft Print to PDF TargetPrinter=Adobe PDF
PrinterOptions=Record=PrinterOptions|Copies=1|Duplex=1|TrueTypeOptions=3|Collate=1|PrintJobKind=1|PrintWhat=1 PrinterOptions=Record=PrinterOptions|Copies=1|Duplex=1|TrueTypeOptions=3|Collate=1|PrintJobKind=1|PrintWhat=1
[OutputGroup3] [OutputGroup3]

Binary file not shown.

View File

@@ -1,18 +0,0 @@
G04:AMPARAMS|DCode=41|XSize=2.3mm|YSize=1.8mm|CornerRadius=0.225mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=90.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
%AMROUNDEDRECTD41*
21,1,2.3000,1.3500,0,0,90.0*
21,1,1.8500,1.8000,0,0,90.0*
1,1,0.4500,0.6750,0.9250*
1,1,0.4500,0.6750,-0.9250*
1,1,0.4500,-0.6750,-0.9250*
1,1,0.4500,-0.6750,0.9250*
%
G04:AMPARAMS|DCode=42|XSize=2.45mm|YSize=1.95mm|CornerRadius=0.3mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=90.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
%AMROUNDEDRECTD42*
21,1,2.4500,1.3500,0,0,90.0*
21,1,1.8500,1.9500,0,0,90.0*
1,1,0.6000,0.6750,0.9250*
1,1,0.6000,0.6750,-0.9250*
1,1,0.6000,-0.6750,-0.9250*
1,1,0.6000,-0.6750,0.9250*
%

Some files were not shown because too many files have changed in this diff Show More