diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c909cc6..88a8c5ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,10 @@ jobs: run: pip install -U platformio - name: Audio pack tests run: python3 scripts/test_audio_pack.py + - name: PCBA host tests + run: python3 scripts/test_pcba_test.py + - name: PCBA web tests + run: node scripts/test_pcba_web.mjs - name: Laptop screen inserts PDF up to date run: python3 scripts/laptop_screens.py --check - name: Build firmware @@ -59,6 +63,8 @@ jobs: if-no-files-found: error - name: Build OLED expression demo run: pio run -e expression-demo + - name: Build PCBA test image + run: pio run -e pcba-test - name: Host Unity tests run: pio test -e native diff --git a/.gitignore b/.gitignore index 3b388a31..d4d4dac6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ data/*.* data/*/ artifacts/ +.venv + node_modules/ *.bak diff --git a/docs/README.md b/docs/README.md index 37af5a18..21ff8dae 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,6 +16,7 @@ Pick a path. Depth lives in the linked pages. | **Print / parts inventory** | [../3d_models/README.md](../3d_models/README.md) | | **Order printed parts** (no 3D printer) | [3d/order-parts.md](3d/order-parts.md) | | **Order main control board** (PCB / PCBA) | [hardware/order-main-control-board.md](hardware/order-main-control-board.md) | +| **Test a produced board** (PCBA, rev 3) | [hardware/pcba-test.md](hardware/pcba-test.md) | | **Adapt CAD to a different servo** | [3d/parametric-design.md](3d/parametric-design.md) | | **Add a new CAD / printable part** | [3d/adding-parts.md](3d/adding-parts.md) | | **Optional mods** | [../mods/README.md](../mods/README.md) | diff --git a/docs/hardware/pcba-test.md b/docs/hardware/pcba-test.md new file mode 100644 index 00000000..cbd45914 --- /dev/null +++ b/docs/hardware/pcba-test.md @@ -0,0 +1,102 @@ +# PCBA test + +Check a produced main control board rev 3 with a golden ESP32-C3-Zero and one operator. The test is compatible with main control board rev 3. The product firmware cannot do this check. Power is the USB cable through the module. No servo supply. + +The golden module stays with the tester. The ESP32 must already run the test firmware. How to get one is below. Do not ship that module on a board. + +## Get an ESP32 with the test firmware + +The ESP32 must already run the test firmware. The product firmware cannot run this test. The script and the page do not flash the module. + +Seat a Waveshare ESP32-C3-Zero on a main control board. Match the USB mark. Plug a USB-C data cable into the board socket, not the ESP32 socket. From the repo root: + +```bash +pio run -e pcba-test -t upload +``` + +When the test firmware is running, the onboard LED stays steady white. That lamp is not a board pass or fail. Before a module leaves the bench, flash the product image again. + +The module LED stays steady white while the test image has power, including after a failed PCA probe, a verdict, and `q`. Steady white means the test image is running. It is not the board verdict. Judge the board by the ear on J1 and the plug lamps. Unplugging USB is what turns the module LED off. + +## Run the script + +Python 3 and pyserial. From the repo root: + +```bash +python3 -m pip install pyserial +python3 scripts/pcba_test.py +``` + +The script prints a header, the web tester address, and a prepare card. It opens the board USB port. If that port is busy, the script says so and waits. Close the other program using the board. Keys match the page: Enter accepts a step, `n` rejects it. On SPEAKER, `+` is louder and `-` is quieter. After `RESULT`, unplug USB for the next board, or `q` to stop. + +The other path is the web tester: [https://tinyengineer.dev/pcba_test](https://tinyengineer.dev/pcba_test). Chrome or Edge only. If the browser cannot open a serial port, the page says so and names `python3 scripts/pcba_test.py`. + +USB-C data cable. Speaker on J1. Cards stay on screen. Act on the latest one. + +## Testing board + +The testing board seats on one column at a time. The silk-side pin is yellow (SIG). The middle pin is red (5 V). Treat the square pad as GND. + +During `servos`, signal is on all five servo outputs. Take the testing board and plug it into one column at a time: HEAD, NECK, ARM L, ARM R, and BODY. On every column, red stays on and yellow pulses. + +## What you see + +The first card is prepare. Do that, then connect the board. The next card waits until the board is on USB, then waits for Enter before the first test step. + +1. Take the board. +2. Plug in the ESP32. Match the USB mark on the board. +3. Plug the speaker into the JST speaker port. +4. Connect USB to the board. Use the board socket, not the ESP32. + +Power-on is a short beep-beep, then silence. The next card is: + +``` +CONNECTED + +The board is connected. + +Enter = continue +``` + +Enter starts step `1/3`. `n` rejects a step. On a test step that is `Enter = good` and `n = bad`. On SPEAKER, `+` is louder and `-` is quieter. The card shows `+ = louder` and `- = quieter`, plus the level, from `Volume 1/10` through `Volume 10/10`. The tone keeps playing. + +| Card | What you do | Look | +| --- | --- | --- | +| `1/3` `tone` | Speaker on the JST speaker port. `+` louder, `-` quieter. | A repeating tone until Enter or `n`. | +| `2/3` `servos` | Signal is on all five servo outputs. Plug the testing board into one column at a time: HEAD, NECK, ARM L, ARM R, and BODY. Check the LEDs on every column. | Red stays on and yellow pulses. | +| `3/3` OE OUTPUT ENABLE | Look at the testing board on one column. No need to test each servo column. | The red LED stays on. Only the yellow LED goes out. | + +A column whose yellow LED does not pulse is a failure. Judge every column before Enter. + +## Keys + +`n` shows `FAIL` and the menu `r`, `c`, `a`. + +- `r` retries the step and drops that failure. On screen: `r = try this step again`. +- `c` keeps the failure and goes on. On screen: `c = keep the fail, go on`. +- `a` clears the board and returns to CONNECTED without unplugging. Enter starts the test again. On screen: `a = start this board over`. + +After `RESULT PASS` or `RESULT FAIL`, the card says to unplug USB for the next board, or `q` to stop. The host stays open and attaches when the port returns. `q` prints `STOP`. `n` after the verdict does nothing. + +`RESULT PASS` covers the speaker, the five servo columns, and output enable. It does not cover J2, J3, J4, the OLED, Wi-Fi, or a 2 A load. A fault in those can still be present. + +No PCA answer prints `FAIL pca` and `RESULT FAIL pca`. The card says the PCA9685 did not answer and this board fails before the speaker test. There is no `r` / `c` / `a` for that failure. Unplug or `q`, same as any other verdict. + +## Dialogue + +The wire under each card is a short token. All accepted: + +``` +STEP tone +STEP servos +STEP oe +RESULT PASS +``` + +`tone` and `servos` rejected with `c`: + +``` +FAIL tone +FAIL servos +RESULT FAIL tone servos +``` diff --git a/docs/hardware/testing.md b/docs/hardware/testing.md index 22884aa9..239e6458 100644 --- a/docs/hardware/testing.md +++ b/docs/hardware/testing.md @@ -1,5 +1,7 @@ # Hardware testing +A produced main control board is checked with the [PCBA session](pcba-test.md), not this product boot. The golden module stays with the tester. + Firmware is [`src/main.cpp`](../../src/main.cpp). Boot **inits** hardware and starts HTTP. Demos (OLED, tones, servo sweep) run on demand via REST. Helpers: - [`src/hardware/rgb.cpp`](../../src/hardware/rgb.cpp) diff --git a/docs/testing.md b/docs/testing.md index 761d6338..1ccb71f8 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -22,9 +22,12 @@ node scripts/expressions/generate.js --check node scripts/expressions/test-assets.js pio test -e native pio run -e expression-demo +pio run -e pcba-test +python3 scripts/test_pcba_test.py +node scripts/test_pcba_web.mjs ``` -The `expression-demo` environment builds a separate OLED-only application without changing the default robot build. For a later bench test, follow its [wiring and upload notes](../lib/TinyEngineerExpressions/README.md#standalone-oled-demo), leave the separate servo supply off, and observe all 16 faces through a complete 48-second cycle. Report physical display results separately from host tests and compilation. +The `expression-demo` environment builds a separate OLED-only application without changing the default robot build. `pcba-test` builds the PCBA session the same way. Bench steps: [hardware/pcba-test.md](hardware/pcba-test.md). For a later expression-demo bench test, follow the [wiring and upload notes](../lib/TinyEngineerExpressions/README.md#standalone-oled-demo), leave the separate servo supply off, and observe all 16 faces through a complete 48-second cycle. Report physical display results separately from host tests and compilation. ## Web UI diff --git a/examples/pcba-test/main.cpp b/examples/pcba-test/main.cpp new file mode 100644 index 00000000..2d101cbd --- /dev/null +++ b/examples/pcba-test/main.cpp @@ -0,0 +1,451 @@ +#include +#include +#include +#include +#include +#include + +#include "pins.h" +#include "servos.h" + +// PCBA session on the golden module. Product src/ is not linked. + +namespace { + +constexpr uint16_t kFullOff = 4096; +constexpr int kToneLowHz = 500; +constexpr int kToneHighHz = 750; +constexpr int16_t kToneAmplitude = 11000; +// Index 5 is the bench default. Peak of both partials stays inside int16. +constexpr int16_t kToneLevels[] = { + 2000, 4000, 6000, 8000, 9500, 11000, 12500, 14000, 15200, 16300, +}; +constexpr int kToneLevelCount = sizeof(kToneLevels) / sizeof(kToneLevels[0]); +constexpr int kToneLevelDefault = 5; +// 441 frames closes both partials, so the loop junction stays continuous. +constexpr int kToneFrames = 441; + +enum Phase : uint8_t { PhaseStart, PhaseWait, PhaseMenu, PhaseResult, PhaseStop }; + +struct StepDef { + const char* name; + int channel; +}; + +// channel < 0 is the speaker. kAllChannels pulses every servo output together. +// kOeOff releases the PCA9685 outputs while the same pulse keeps running. +constexpr int kAllChannels = -2; +constexpr int kOeOff = -3; + +constexpr StepDef kSteps[] = { + {"tone", -1}, + {"servos", kAllChannels}, + {"oe", kOeOff}, +}; + +constexpr int kStepCount = sizeof(kSteps) / sizeof(kSteps[0]); + +static_assert(kStepCount == 3, "PCBA step list drifted"); +static_assert(kToneLevelCount == 10, "volume card is 1..10"); +static_assert(kToneLevels[kToneLevelDefault] == kToneAmplitude, "default level drifted"); + +I2SClass I2S; +Adafruit_PWMServoDriver pwm(PCA9685_ADDRESS); + +Phase gPhase = PhaseStart; +int gStep = 0; +bool gFailed[kStepCount] = {}; +bool gPcaOk = false; +bool gToneOn = false; +int gToneLevel = kToneLevelDefault; +bool gI2sReady = false; +uint32_t gPulseAt = 0; +uint32_t gPromptAt = 0; +int16_t gTonePcm[kToneFrames * 2]; + +static_assert(kToneFrames * kToneLowHz % SAMPLE_RATE == 0, "low partial must close"); +static_assert(kToneFrames * kToneHighHz % SAMPLE_RATE == 0, "high partial must close"); + +void setOe(bool enabled) { + if (!PCA9685_OE_WIRED) { + return; + } + digitalWrite(PCA9685_OE_PIN, enabled ? LOW : HIGH); +} + +void writeChannel(int channel, bool high) { + if (high) { + pwm.setPWM(channel, kFullOff, 0); + } else { + pwm.setPWM(channel, 0, kFullOff); + } +} + +void allOff() { + for (int channel = 0; channel < SERVO_COUNT; channel++) { + writeChannel(channel, false); + } +} + +void writeDuty(int channel, uint16_t duty) { + if (duty == 0) { + pwm.setPWM(channel, 0, kFullOff); + return; + } + pwm.setPWM(channel, 0, duty); +} + +void servicePulse() { + if (!gPcaOk || (gPhase != PhaseWait && gPhase != PhaseMenu)) { + return; + } + const int channel = kSteps[gStep].channel; + if (channel != kAllChannels && channel != kOeOff) { + return; + } + const uint32_t now = millis(); + if (gPulseAt != 0 && now - gPulseAt < 20) { + return; + } + gPulseAt = now; + constexpr uint32_t kHalf = 500; + const uint32_t t = now % (kHalf * 2); + const uint32_t rise = t < kHalf ? t : (kHalf * 2 - t); + const uint16_t duty = static_cast(rise * 4095 / kHalf); + for (int channel = 0; channel < SERVO_COUNT; channel++) { + writeDuty(channel, duty); + } +} + +bool probePca() { + Wire.beginTransmission(PCA9685_ADDRESS); + return Wire.endTransmission() == 0; +} + +void printResult() { + Serial.print("RESULT "); + bool any = false; + for (int index = 0; index < kStepCount; index++) { + if (gFailed[index]) { + any = true; + break; + } + } + if (!any) { + Serial.println("PASS"); + return; + } + Serial.print("FAIL"); + for (int index = 0; index < kStepCount; index++) { + if (!gFailed[index]) { + continue; + } + Serial.print(' '); + Serial.print(kSteps[index].name); + } + Serial.println(); +} + +void prepareTone(); +void printVolume(); + +void enterStep(int step) { + gStep = step; + gToneOn = false; + const StepDef& def = kSteps[step]; + if (def.channel == kOeOff) { + setOe(false); + gPulseAt = 0; + } else if (def.channel == kAllChannels) { + setOe(true); + gPulseAt = 0; + } else { + setOe(true); + allOff(); + gToneOn = true; + } + Serial.print("STEP "); + Serial.println(def.name); + if (gToneOn) { + printVolume(); + } + gPhase = PhaseWait; +} + +void finishBoard() { + gToneOn = false; + printResult(); + gPhase = PhaseResult; +} + +void acceptStep() { + if (gStep + 1 >= kStepCount) { + finishBoard(); + return; + } + enterStep(gStep + 1); +} + +void failStep() { + gFailed[gStep] = true; + gToneOn = false; + Serial.print("FAIL "); + Serial.println(kSteps[gStep].name); + gPhase = PhaseMenu; +} + +void restartBoard() { + for (int index = 0; index < kStepCount; index++) { + gFailed[index] = false; + } + gToneOn = false; + gStep = 0; + allOff(); + setOe(true); + Serial.println("READY"); + gPhase = PhaseStart; +} + +void printVolume() { + Serial.print("VOL "); + Serial.println(gToneLevel + 1); +} + +void changeVolume(int delta) { + int next = gToneLevel + delta; + if (next < 0) { + next = 0; + } else if (next >= kToneLevelCount) { + next = kToneLevelCount - 1; + } + if (next != gToneLevel) { + gToneLevel = next; + prepareTone(); + } + printVolume(); +} + +void onWaitKey(char key) { + if (key == '\n') { + acceptStep(); + return; + } + if (key == 'n') { + failStep(); + return; + } + if (!gToneOn) { + return; + } + if (key == '+' || key == '=') { + changeVolume(1); + return; + } + if (key == '-') { + changeVolume(-1); + } +} + +void onMenuKey(char key) { + if (key == 'r') { + gFailed[gStep] = false; + enterStep(gStep); + return; + } + if (key == 'c') { + if (gStep + 1 >= kStepCount) { + finishBoard(); + return; + } + enterStep(gStep + 1); + return; + } + if (key == 'a') { + restartBoard(); + } +} + +void onResultKey(char key) { + if (key == '?') { + if (!gPcaOk) { + Serial.println("FAIL pca"); + Serial.println("RESULT FAIL pca"); + } + return; + } + if (key == 'q') { + Serial.println("STOP"); + gPhase = PhaseStop; + } +} + +void onStartKey(char key) { + if (key == '\n') { + enterStep(0); + } +} + +void onKey(char key) { + if (gPhase == PhaseStart) { + onStartKey(key); + } else if (gPhase == PhaseWait) { + onWaitKey(key); + } else if (gPhase == PhaseMenu) { + onMenuKey(key); + } else if (gPhase == PhaseResult) { + onResultKey(key); + } +} + +void pollKeys() { + while (Serial.available() > 0) { + onKey(static_cast(Serial.read())); + } +} + +void prepareTone() { + for (int frame = 0; frame < kToneFrames; frame++) { + const float low = sinf(2.0f * PI * frame * kToneLowHz / SAMPLE_RATE); + const float high = sinf(2.0f * PI * frame * kToneHighHz / SAMPLE_RATE); + float raw = (low + high) * kToneLevels[gToneLevel]; + if (raw > 32767.0f) { + raw = 32767.0f; + } else if (raw < -32767.0f) { + raw = -32767.0f; + } + const int16_t sample = static_cast(raw); + gTonePcm[frame * 2] = sample; + gTonePcm[frame * 2 + 1] = sample; + } +} + +void writeSamples(const int16_t* samples, size_t bytes) { + if (!gI2sReady || bytes == 0) { + return; + } + I2S.write(reinterpret_cast(const_cast(samples)), bytes); +} + +void writeSilenceMs(int ms) { + int16_t zeros[128 * 2] = {}; + int frames = SAMPLE_RATE * ms / 1000; + while (frames > 0) { + const int count = frames > 128 ? 128 : frames; + writeSamples(zeros, static_cast(count) * 2 * sizeof(int16_t)); + frames -= count; + } +} + +void playNote(int hz, int frames) { + constexpr int16_t kChimeAmplitude = 9000; + static int16_t buffer[882 * 2]; + for (int frame = 0; frame < frames; frame++) { + const float raw = sinf(2.0f * PI * frame * hz / SAMPLE_RATE) * kChimeAmplitude; + const int16_t sample = static_cast(raw); + buffer[frame * 2] = sample; + buffer[frame * 2 + 1] = sample; + } + writeSamples(buffer, static_cast(frames) * 2 * sizeof(int16_t)); +} + +void playPowerChime() { + // 40 ms notes a fifth apart. 882 frames closes both. + playNote(800, 882); + writeSilenceMs(20); + playNote(1200, 882); + writeSilenceMs(20); +} + +void serviceTone() { + if (!gToneOn || !gI2sReady || gPhase != PhaseWait) { + return; + } + writeSamples(gTonePcm, sizeof(gTonePcm)); +} + +void startI2s() { + I2S.setPins(I2S_BCLK, I2S_LRC, I2S_DIN); + gI2sReady = I2S.begin( + I2S_MODE_STD, + SAMPLE_RATE, + I2S_DATA_BIT_WIDTH_16BIT, + I2S_SLOT_MODE_STEREO); +} + +void printPrompt() { + if (gPhase == PhaseStart) { + Serial.println("READY"); + } else if (gPhase == PhaseWait) { + Serial.print("STEP "); + Serial.println(kSteps[gStep].name); + } else if (gPhase == PhaseMenu) { + Serial.print("FAIL "); + Serial.println(kSteps[gStep].name); + } +} + +void servicePrompt() { + if (gPhase != PhaseStart && gPhase != PhaseWait && gPhase != PhaseMenu) { + return; + } + const uint32_t now = millis(); + if (gPromptAt != 0 && now - gPromptAt < 200) { + return; + } + gPromptAt = now; + printPrompt(); +} + +void beginSession() { + Serial.println("PCBA-TEST v1"); + Wire.begin(I2C_SDA, I2C_SCL); + gPcaOk = probePca(); + if (!gPcaOk) { + Serial.println("FAIL pca"); + Serial.println("RESULT FAIL pca"); + gPhase = PhaseResult; + return; + } + + pwm.begin(); + pwm.setPWMFreq(200); + allOff(); + setOe(true); + const uint32_t drainUntil = millis() + 50; + while (millis() < drainUntil) { + while (Serial.available() > 0) { + Serial.read(); + } + delay(5); + } + gPhase = PhaseStart; + gToneOn = false; +} + +} // namespace + +void setup() { + // Equal channels stay white for every WS2812 wire order. Leave it on. + constexpr uint8_t kModuleWhite = 64; + rgbLedWrite(RGB_LED_PIN, kModuleWhite, kModuleWhite, kModuleWhite); + + Serial.begin(115200); + if (PCA9685_OE_WIRED) { + pinMode(PCA9685_OE_PIN, OUTPUT); + digitalWrite(PCA9685_OE_PIN, HIGH); + } + startI2s(); + prepareTone(); + playPowerChime(); + beginSession(); +} + +void loop() { + if (gPhase == PhaseStop) { + return; + } + servicePrompt(); + serviceTone(); + servicePulse(); + pollKeys(); +} diff --git a/hardware/boards/main-control-board-servo-tester/main-control-board-servo-tester.kicad_pcb b/hardware/boards/main-control-board-servo-tester/main-control-board-servo-tester.kicad_pcb new file mode 100644 index 00000000..f09d010d --- /dev/null +++ b/hardware/boards/main-control-board-servo-tester/main-control-board-servo-tester.kicad_pcb @@ -0,0 +1,2319 @@ +(kicad_pcb + (version 20260206) + (generator "pcbnew") + (generator_version "10.0") + (general + (thickness 1.6) + (legacy_teardrops no) + ) + (paper "A4") + (layers + (0 "F.Cu" signal) + (2 "B.Cu" signal) + (9 "F.Adhes" user "F.Adhesive") + (11 "B.Adhes" user "B.Adhesive") + (13 "F.Paste" user) + (15 "B.Paste" user) + (5 "F.SilkS" user "F.Silkscreen") + (7 "B.SilkS" user "B.Silkscreen") + (1 "F.Mask" user) + (3 "B.Mask" user) + (17 "Dwgs.User" user "User.Drawings") + (19 "Cmts.User" user "User.Comments") + (21 "Eco1.User" user "User.Eco1") + (23 "Eco2.User" user "User.Eco2") + (25 "Edge.Cuts" user) + (27 "Margin" user) + (31 "F.CrtYd" user "F.Courtyard") + (29 "B.CrtYd" user "B.Courtyard") + (35 "F.Fab" user) + (33 "B.Fab" user) + (39 "User.1" user) + (41 "User.2" user) + (43 "User.3" user) + (45 "User.4" user) + ) + (setup + (stackup + (layer "F.SilkS" + (type "Top Silk Screen") + ) + (layer "F.Paste" + (type "Top Solder Paste") + ) + (layer "F.Mask" + (type "Top Solder Mask") + (thickness 0.01) + ) + (layer "F.Cu" + (type "copper") + (thickness 0.035) + ) + (layer "dielectric 1" + (type "core") + (thickness 1.51) + (material "FR4") + (epsilon_r 4.5) + (loss_tangent 0.02) + ) + (layer "B.Cu" + (type "copper") + (thickness 0.035) + ) + (layer "B.Mask" + (type "Bottom Solder Mask") + (thickness 0.01) + ) + (layer "B.Paste" + (type "Bottom Solder Paste") + ) + (layer "B.SilkS" + (type "Bottom Silk Screen") + ) + (copper_finish "None") + (dielectric_constraints no) + ) + (pad_to_mask_clearance 0) + (allow_soldermask_bridges_in_footprints no) + (tenting + (front yes) + (back yes) + ) + (covering + (front no) + (back no) + ) + (plugging + (front no) + (back no) + ) + (capping no) + (filling no) + (zone_defaults) + (pcbplotparams + (layerselection 0x00000000_00000000_55555555_5755f5ff) + (plot_on_all_layers_selection 0x00000000_00000000_00000000_00000000) + (disableapertmacros no) + (usegerberextensions no) + (usegerberattributes yes) + (usegerberadvancedattributes yes) + (creategerberjobfile yes) + (dashed_line_dash_ratio 12) + (dashed_line_gap_ratio 3) + (svgprecision 4) + (plotframeref no) + (mode 1) + (useauxorigin no) + (pdf_front_fp_property_popups yes) + (pdf_back_fp_property_popups yes) + (pdf_metadata yes) + (pdf_single_document no) + (dxfpolygonmode yes) + (dxfimperialunits yes) + (dxfusepcbnewfont yes) + (psnegative no) + (psa4output no) + (plot_black_and_white yes) + (sketchpadsonfab no) + (plotpadnumbers no) + (hidednponfab no) + (sketchdnponfab yes) + (crossoutdnponfab yes) + (subtractmaskfromsilk no) + (outputformat 1) + (mirror no) + (drillshape 1) + (scaleselection 1) + (outputdirectory "") + ) + ) + (footprint "Connector_PinSocket_2.54mm:PinSocket_1x03_P2.54mm_Horizontal" + (layer "F.Cu") + (uuid "0a66fd44-21b1-4918-b87e-a4f4af4f7a6a") + (at 145.5 88.9) + (descr "Through hole angled socket strip, 1x03, 2.54mm pitch, 8.51mm socket length, single row (from Kicad 4.0.7), script generated") + (tags "Through hole angled socket strip THT 1x03 2.54mm single row") + (property "Reference" "J1" + (at -4.38 -2.77 0) + (layer "F.SilkS") + (hide yes) + (uuid "80ecc9fc-dc4b-4349-b3d4-cb89eaa9e1d6") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property "Value" "Conn_01x03_Socket" + (at -4.38 7.85 0) + (layer "F.Fab") + (hide yes) + (uuid "536bbba6-17b0-4d34-a0bb-f8543c00839e") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (layer "F.Fab") + (hide yes) + (uuid "14ed8221-3ba2-4160-8287-c212f3ca752e") + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Generic connector, single row, 01x03, script generated" + (at 0 0 0) + (layer "F.Fab") + (hide yes) + (uuid "fd152fd0-2c8e-48b7-964d-e56a4ddbc1b8") + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "LCSC Part #" "C2897385" + (at 0 0 0) + (unlocked yes) + (layer "F.Fab") + (hide yes) + (uuid "f1a8d3ec-b713-464a-8ea6-5dccc6531fd4") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property "MPN" "PM254-1-03-W-8.5" + (at 0 0 0) + (unlocked yes) + (layer "F.Fab") + (hide yes) + (uuid "b0461155-7ae4-414e-bd10-8c038fb77da6") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property "Manufacturer" "HCTL" + (at 0 0 0) + (unlocked yes) + (layer "F.Fab") + (hide yes) + (uuid "edb89535-122c-49f2-95d2-f5a097a83f65") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property ki_fp_filters "Connector*:*_1x??_*") + (path "/b0391479-ebcd-4dd9-aa59-b2cdbb297f0c") + (sheetname "/") + (sheetfile "main-control-board-servo-tester.kicad_sch") + (units + (unit + (name "A") + (pins "1" "2" "3") + ) + ) + (attr through_hole) + (duplicate_pad_numbers_are_jumpers no) + (fp_line + (start -1.46 -1.33) + (end -1.46 6.41) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "da4756ee-aa8b-4c69-813c-ee73d58298cf") + ) + (fp_line + (start -1.46 -0.36) + (end -1.11 -0.36) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "30528e24-2fed-427c-84c0-7cbc528d56fb") + ) + (fp_line + (start -1.46 0.36) + (end -1.11 0.36) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "76f160a9-ef37-4db3-8f60-5189ce17e25e") + ) + (fp_line + (start -1.46 2.18) + (end -1.05 2.18) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "3a7cabca-cc41-4232-a3b6-cc92922e7969") + ) + (fp_line + (start -1.46 2.9) + (end -1.05 2.9) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "fbfe81ea-8d52-432b-b8ea-5e7d7a35a2c2") + ) + (fp_line + (start -1.46 4.72) + (end -1.05 4.72) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "091a0544-ba0f-403c-a49a-483096ff21a5") + ) + (fp_line + (start -1.46 5.44) + (end -1.05 5.44) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "020757c9-824e-418f-ba87-784766f52210") + ) + (fp_line + (start 0 -1.33) + (end 1.11 -1.33) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "08d29df6-5592-4d10-87bb-0f0efbf1f0c1") + ) + (fp_line + (start 1.11 -1.33) + (end 1.11 0) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "00195692-7559-4a38-a29f-b7af834d3cb4") + ) + (fp_line + (start -3 -1.75) + (end -3 6.85) + (stroke + (width 0.05) + (type solid) + ) + (layer "F.CrtYd") + (uuid "dc715b8a-3900-42c3-b174-2666741f96b3") + ) + (fp_line + (start -3 6.85) + (end 1.75 6.85) + (stroke + (width 0.05) + (type solid) + ) + (layer "F.CrtYd") + (uuid "f195b774-3723-42fb-b518-e23525f3772a") + ) + (fp_line + (start 1.75 -1.75) + (end -3 -1.75) + (stroke + (width 0.05) + (type solid) + ) + (layer "F.CrtYd") + (uuid "40ef9790-1cbe-4ebe-97d8-e29443b1646d") + ) + (fp_line + (start 1.75 6.85) + (end 1.75 -1.75) + (stroke + (width 0.05) + (type solid) + ) + (layer "F.CrtYd") + (uuid "375f6b8c-2b08-47ed-ad85-dde7ad2d8785") + ) + (fp_line + (start 0 0.3) + (end 0 -0.3) + (stroke + (width 0.1) + (type solid) + ) + (layer "F.Fab") + (uuid "1881f3da-4c31-4e56-9cc2-e7644d2417f7") + ) + (fp_line + (start 0 2.84) + (end 0 2.24) + (stroke + (width 0.1) + (type solid) + ) + (layer "F.Fab") + (uuid "62b7619b-60ed-437c-9ec4-c6270097f596") + ) + (fp_line + (start 0 5.38) + (end 0 4.78) + (stroke + (width 0.1) + (type solid) + ) + (layer "F.Fab") + (uuid "5f5ab410-795c-407c-ad0f-a8c4dc4d6620") + ) + (pad "1" thru_hole rect + (at 0 0) + (size 1.7 1.7) + (drill 1) + (layers "*.Cu" "*.Mask") + (remove_unused_layers no) + (net "+3.3V") + (pinfunction "Pin_1_1") + (pintype "passive") + (uuid "7d4794d6-8739-41aa-9538-76fa47deb67b") + ) + (pad "2" thru_hole circle + (at 0 2.54) + (size 1.7 1.7) + (drill 1) + (layers "*.Cu" "*.Mask") + (remove_unused_layers no) + (net "+5V") + (pinfunction "Pin_2_2") + (pintype "passive") + (uuid "53dd1bbc-d989-48d4-9270-c68dfd667ab0") + ) + (pad "3" thru_hole circle + (at 0 5.08) + (size 1.7 1.7) + (drill 1) + (layers "*.Cu" "*.Mask") + (remove_unused_layers no) + (net "GND") + (pinfunction "Pin_3_3") + (pintype "passive") + (uuid "2c93e3ed-933c-42bd-9f7b-2e66c7a04e0f") + ) + (embedded_fonts no) + (model "${KICAD10_3DMODEL_DIR}/Connector_PinSocket_2.54mm.3dshapes/PinSocket_1x03_P2.54mm_Horizontal.step" + (offset + (xyz 0 0 0) + ) + (scale + (xyz 1 1 1) + ) + (rotate + (xyz 0 0 0) + ) + ) + ) + (footprint "Resistor_SMD:R_0603_1608Metric" + (layer "F.Cu") + (uuid "0d5bf47f-5636-48e8-9324-6aa79691849f") + (at 152.4 88.9 180) + (descr "Resistor SMD 0603 (1608 Metric), square (rectangular) end terminal, IPC-7351 nominal, (Body size source: IPC-SM-782 page 72, https://www.pcb-3d.com/wordpress/wp-content/uploads/ipc-sm-782a_amendment_1_and_2.pdf)") + (tags "resistor") + (property "Reference" "R1" + (at 0 -1.43 0) + (layer "F.SilkS") + (hide yes) + (uuid "ea4020b4-3c15-4c2c-9054-0ae528b737bc") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property "Value" "470" + (at -2.825 0 0) + (layer "F.Fab") + (hide yes) + (uuid "640f7f78-831f-47fd-88ae-2fa423ea8363") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (layer "F.Fab") + (hide yes) + (uuid "aab0cee2-3330-4091-9577-1bb4c5ad1496") + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Resistor" + (at 0 0 0) + (layer "F.Fab") + (hide yes) + (uuid "4fd7b315-2c0e-4832-bc9d-d61388e9c39a") + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "LCSC Part #" "C23179" + (at 0 0 180) + (unlocked yes) + (layer "F.Fab") + (hide yes) + (uuid "c192de9e-ee18-42b0-8ae5-2e21341bd608") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property "MPN" "0603WAF4700T5E" + (at 0 0 180) + (unlocked yes) + (layer "F.Fab") + (hide yes) + (uuid "02082ea1-1916-4130-a885-868f93314904") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property "Manufacturer" "UNI-ROYAL(Uniroyal Elec)" + (at 0 0 180) + (unlocked yes) + (layer "F.Fab") + (hide yes) + (uuid "50aeb438-6016-46bf-987b-239da002c85e") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property ki_fp_filters "R_*") + (path "/e32a9f3e-dc26-4827-9da6-4e2873523622") + (sheetname "/") + (sheetfile "main-control-board-servo-tester.kicad_sch") + (units + (unit + (name "A") + (pins "1" "2") + ) + ) + (attr smd) + (duplicate_pad_numbers_are_jumpers no) + (fp_line + (start -0.237258 0.5225) + (end 0.237258 0.5225) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "185aa34d-0177-4edf-9551-5ab8925e52a3") + ) + (fp_line + (start -0.237258 -0.5225) + (end 0.237258 -0.5225) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "1586615d-01d4-427a-a825-ab7a28422be6") + ) + (fp_rect + (start -1.48 -0.73) + (end 1.48 0.73) + (stroke + (width 0.05) + (type solid) + ) + (fill no) + (layer "F.CrtYd") + (uuid "1402f3d7-2e6d-4d5e-a246-15b3db0b17cb") + ) + (fp_rect + (start -0.8 -0.4125) + (end 0.8 0.4125) + (stroke + (width 0.1) + (type solid) + ) + (fill no) + (layer "F.Fab") + (uuid "7905d220-f658-4440-8fb0-2a914cdaeea1") + ) + (fp_text user "${REFERENCE}" + (at 0 0 0) + (layer "F.Fab") + (uuid "8d80e950-cc8e-492a-b21f-5e5c60697157") + (effects + (font + (size 0.4 0.4) + (thickness 0.06) + ) + ) + ) + (pad "1" smd roundrect + (at -0.825 0 180) + (size 0.8 0.95) + (layers "F.Cu" "F.Mask" "F.Paste") + (roundrect_rratio 0.25) + (net "Net-(D1-A)") + (pintype "passive") + (uuid "b1ee53a5-dad5-49b6-8218-ec4e52ec0309") + ) + (pad "2" smd roundrect + (at 0.825 0 180) + (size 0.8 0.95) + (layers "F.Cu" "F.Mask" "F.Paste") + (roundrect_rratio 0.25) + (net "+3.3V") + (pintype "passive") + (uuid "604d8e15-1497-47ca-a962-0f7458d46503") + ) + (embedded_fonts no) + (model "${KICAD10_3DMODEL_DIR}/Resistor_SMD.3dshapes/R_0603_1608Metric.step" + (offset + (xyz 0 0 0) + ) + (scale + (xyz 1 1 1) + ) + (rotate + (xyz 0 0 0) + ) + ) + ) + (footprint "Resistor_SMD:R_0603_1608Metric" + (layer "F.Cu") + (uuid "244aa7a7-7ed8-4017-b393-56cfc006b448") + (at 152.4 93.98 180) + (descr "Resistor SMD 0603 (1608 Metric), square (rectangular) end terminal, IPC-7351 nominal, (Body size source: IPC-SM-782 page 72, https://www.pcb-3d.com/wordpress/wp-content/uploads/ipc-sm-782a_amendment_1_and_2.pdf)") + (tags "resistor") + (property "Reference" "R2" + (at 0 -1.43 0) + (layer "F.SilkS") + (hide yes) + (uuid "69676ea8-f7e7-47d2-9813-e7b367320dd7") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property "Value" "1K" + (at -3 0 0) + (layer "F.Fab") + (hide yes) + (uuid "ab95a459-b1d4-4f51-b88e-7a45c822cf75") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (layer "F.Fab") + (hide yes) + (uuid "6227a8ce-5adc-473c-ac4a-1dafa5ef6c67") + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Resistor" + (at 0 0 0) + (layer "F.Fab") + (hide yes) + (uuid "dc4f6a0d-a075-421a-8b9f-e5183e301737") + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "LCSC Part #" "0603WAF1001T5E" + (at 0 0 180) + (unlocked yes) + (layer "F.Fab") + (hide yes) + (uuid "5bbb05ee-786c-4214-b2a9-3c0b16498389") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property "MPN" "C21190" + (at 0 0 180) + (unlocked yes) + (layer "F.Fab") + (hide yes) + (uuid "3827ceb3-b9a4-4169-b090-6476ad47ddb3") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property "Manufacturer" "UNI-ROYAL(Uniroyal Elec)" + (at 0 0 180) + (unlocked yes) + (layer "F.Fab") + (hide yes) + (uuid "2521b72f-6fc9-4b78-b7db-55678ec5c4fe") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property ki_fp_filters "R_*") + (path "/4cb34252-ce19-4d2c-aae1-722a53b733c4") + (sheetname "/") + (sheetfile "main-control-board-servo-tester.kicad_sch") + (units + (unit + (name "A") + (pins "1" "2") + ) + ) + (attr smd) + (duplicate_pad_numbers_are_jumpers no) + (fp_line + (start -0.237258 0.5225) + (end 0.237258 0.5225) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "583f4f42-bd40-4a34-869d-2ca0e6ea3693") + ) + (fp_line + (start -0.237258 -0.5225) + (end 0.237258 -0.5225) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "5a194cd3-fe1e-4ef4-9757-bb38b0492f59") + ) + (fp_rect + (start -1.48 -0.73) + (end 1.48 0.73) + (stroke + (width 0.05) + (type solid) + ) + (fill no) + (layer "F.CrtYd") + (uuid "59af1904-296e-44a6-80e6-be75ca3a4ead") + ) + (fp_rect + (start -0.8 -0.4125) + (end 0.8 0.4125) + (stroke + (width 0.1) + (type solid) + ) + (fill no) + (layer "F.Fab") + (uuid "15743d46-6e12-4ac6-83e3-d4dbde943797") + ) + (fp_text user "${REFERENCE}" + (at 0 0 0) + (layer "F.Fab") + (uuid "83f336f6-5edc-49ef-94c4-594a482e29e4") + (effects + (font + (size 0.4 0.4) + (thickness 0.06) + ) + ) + ) + (pad "1" smd roundrect + (at -0.825 0 180) + (size 0.8 0.95) + (layers "F.Cu" "F.Mask" "F.Paste") + (roundrect_rratio 0.25) + (net "Net-(D2-A)") + (pintype "passive") + (uuid "8a193071-8f11-48b6-a035-7dfe708fd5db") + ) + (pad "2" smd roundrect + (at 0.825 0 180) + (size 0.8 0.95) + (layers "F.Cu" "F.Mask" "F.Paste") + (roundrect_rratio 0.25) + (net "+5V") + (pintype "passive") + (uuid "9c93086f-f3ab-4aa4-9aef-2bffe620dcff") + ) + (embedded_fonts no) + (model "${KICAD10_3DMODEL_DIR}/Resistor_SMD.3dshapes/R_0603_1608Metric.step" + (offset + (xyz 0 0 0) + ) + (scale + (xyz 1 1 1) + ) + (rotate + (xyz 0 0 0) + ) + ) + ) + (footprint "LED_SMD:LED_0603_1608Metric" + (layer "F.Cu") + (uuid "44c3341b-2b5d-4504-ad77-052c4568bea8") + (at 170.18 88.9 180) + (descr "LED SMD 0603 (1608 Metric), square (rectangular) end terminal, IPC-7351 nominal, (Body size source: http://www.tortai-tech.com/upload/download/2011102023233369053.pdf)") + (tags "LED") + (property "Reference" "D1" + (at 0 -1.43 0) + (layer "F.SilkS") + (hide yes) + (uuid "0225c3a8-1801-4542-9642-ccb2f4082654") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property "Value" "YELLOW_LED" + (at 0 1.43 0) + (layer "F.Fab") + (hide yes) + (uuid "820408f4-218c-4027-85d6-988d97cc8682") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (layer "F.Fab") + (hide yes) + (uuid "381f5309-d4af-4243-b53a-b7ac4e8fe050") + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Light emitting diode" + (at 0 0 0) + (layer "F.Fab") + (hide yes) + (uuid "04b0f263-48ac-448c-a5e0-ad2e2a8b5441") + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Sim.Pins" "1=K 2=A" + (at 0 0 180) + (unlocked yes) + (layer "F.Fab") + (hide yes) + (uuid "c86afae7-2e0a-474d-a368-d18a0977fdf6") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property "LCSC Part #" "C2287" + (at 0 0 180) + (unlocked yes) + (layer "F.Fab") + (hide yes) + (uuid "3557ca83-abb9-4349-adfe-0fad9e254b60") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property "MPN" "KT-0603Y" + (at 0 0 180) + (unlocked yes) + (layer "F.Fab") + (hide yes) + (uuid "dfdf71c0-3015-4e5b-b67b-459a8b8fa528") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property "Manufacturer" "Hubei KENTO Elec" + (at 0 0 180) + (unlocked yes) + (layer "F.Fab") + (hide yes) + (uuid "00f0b183-b370-409c-a8cc-89da5dc7af53") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property ki_fp_filters "LED* LED_SMD:* LED_THT:*") + (path "/587fb663-5726-425c-ba5f-387c9bee1d97") + (sheetname "/") + (sheetfile "main-control-board-servo-tester.kicad_sch") + (units + (unit + (name "A") + (pins "1" "2") + ) + ) + (attr smd) + (duplicate_pad_numbers_are_jumpers no) + (fp_line + (start 0.8 -0.735) + (end -1.485 -0.735) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "467eb087-7bb8-4ffc-a69a-ed97e4ea82ea") + ) + (fp_line + (start -1.485 0.735) + (end 0.8 0.735) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "d3e45086-e6db-463a-9f35-cb066e360465") + ) + (fp_line + (start -1.485 -0.735) + (end -1.485 0.735) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "4128d305-905a-4d59-928f-4587d71c1649") + ) + (fp_rect + (start -1.48 -0.73) + (end 1.48 0.73) + (stroke + (width 0.05) + (type solid) + ) + (fill no) + (layer "F.CrtYd") + (uuid "34655805-cd4c-4bbf-a294-4570c29ade1d") + ) + (fp_line + (start 0.8 0.4) + (end 0.8 -0.4) + (stroke + (width 0.1) + (type solid) + ) + (layer "F.Fab") + (uuid "07e0b3b2-18e4-4562-a29c-4808ea41d982") + ) + (fp_line + (start 0.8 -0.4) + (end -0.5 -0.4) + (stroke + (width 0.1) + (type solid) + ) + (layer "F.Fab") + (uuid "27fe6c5b-3dd3-48a7-a699-9131174d7029") + ) + (fp_line + (start -0.5 -0.4) + (end -0.8 -0.1) + (stroke + (width 0.1) + (type solid) + ) + (layer "F.Fab") + (uuid "4fc66fab-0ac0-421c-864f-9494431d0e08") + ) + (fp_line + (start -0.8 0.4) + (end 0.8 0.4) + (stroke + (width 0.1) + (type solid) + ) + (layer "F.Fab") + (uuid "d28ab450-083c-464c-8746-a8d9260667e3") + ) + (fp_line + (start -0.8 -0.1) + (end -0.8 0.4) + (stroke + (width 0.1) + (type solid) + ) + (layer "F.Fab") + (uuid "ff813f29-73c6-46af-9803-c1b1d1b19810") + ) + (fp_text user "${REFERENCE}" + (at 0 0 0) + (layer "F.Fab") + (uuid "cde217b5-9902-4178-934b-db5577caff6e") + (effects + (font + (size 0.4 0.4) + (thickness 0.06) + ) + ) + ) + (pad "1" smd roundrect + (at -0.7875 0 180) + (size 0.875 0.95) + (layers "F.Cu" "F.Mask" "F.Paste") + (roundrect_rratio 0.25) + (net "GND") + (pinfunction "K_1") + (pintype "passive") + (uuid "2c008dd8-2a0e-4248-9c34-1320f26b174f") + ) + (pad "2" smd roundrect + (at 0.7875 0 180) + (size 0.875 0.95) + (layers "F.Cu" "F.Mask" "F.Paste") + (roundrect_rratio 0.25) + (net "Net-(D1-A)") + (pinfunction "A_2") + (pintype "passive") + (uuid "c43c0253-e886-4132-99c3-c1b76798f0e6") + ) + (embedded_fonts no) + (model "${KICAD10_3DMODEL_DIR}/LED_SMD.3dshapes/LED_0603_1608Metric.step" + (offset + (xyz 0 0 0) + ) + (scale + (xyz 1 1 1) + ) + (rotate + (xyz 0 0 0) + ) + ) + ) + (footprint "LED_SMD:LED_0603_1608Metric" + (layer "F.Cu") + (uuid "8675a34a-e372-4574-aec8-f96e1eb1fe35") + (at 170.18 94 180) + (descr "LED SMD 0603 (1608 Metric), square (rectangular) end terminal, IPC-7351 nominal, (Body size source: http://www.tortai-tech.com/upload/download/2011102023233369053.pdf)") + (tags "LED") + (property "Reference" "D2" + (at 0 -1.43 0) + (layer "F.SilkS") + (hide yes) + (uuid "e86f7580-7a9d-4f7a-958b-4a454e1ea3df") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property "Value" "RED_LED" + (at 5.7125 0 0) + (layer "F.Fab") + (hide yes) + (uuid "96a83882-01f5-4133-8046-ee15ea3dd276") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (layer "F.Fab") + (hide yes) + (uuid "9c93bafc-305b-48c8-812b-0ac1b8ade3fa") + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Light emitting diode" + (at 0 0 0) + (layer "F.Fab") + (hide yes) + (uuid "884f3aa9-a50f-403a-aa2b-dbaf733f2302") + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Sim.Pins" "1=K 2=A" + (at 0 0 180) + (unlocked yes) + (layer "F.Fab") + (hide yes) + (uuid "9c5a569b-e384-4df5-922e-c40132e317d7") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property "LCSC Part #" "C2286" + (at 0 0 180) + (unlocked yes) + (layer "F.Fab") + (hide yes) + (uuid "f3adde6f-ea1b-4035-a313-743d12fb5837") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property "MPN" "KT-0603R" + (at 0 0 180) + (unlocked yes) + (layer "F.Fab") + (hide yes) + (uuid "d27bb645-218b-4c88-aba1-f0da5e7923d0") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property "Manufacturer" "Hubei KENTO Elec" + (at 0 0 180) + (unlocked yes) + (layer "F.Fab") + (hide yes) + (uuid "4dd015bd-c6ca-4062-964f-8ccc8d08d7cb") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property ki_fp_filters "LED* LED_SMD:* LED_THT:*") + (path "/b6ddd916-9a02-430e-a918-b76f295b3ecd") + (sheetname "/") + (sheetfile "main-control-board-servo-tester.kicad_sch") + (units + (unit + (name "A") + (pins "1" "2") + ) + ) + (attr smd) + (duplicate_pad_numbers_are_jumpers no) + (fp_line + (start 0.8 -0.735) + (end -1.485 -0.735) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "5f29c385-085d-42c2-a3ac-76c378713565") + ) + (fp_line + (start -1.485 0.735) + (end 0.8 0.735) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "427671d2-e398-4efb-8fec-109ffa8ba10a") + ) + (fp_line + (start -1.485 -0.735) + (end -1.485 0.735) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "461743ff-7542-47b5-ac7d-2b37466a64a2") + ) + (fp_rect + (start -1.48 -0.73) + (end 1.48 0.73) + (stroke + (width 0.05) + (type solid) + ) + (fill no) + (layer "F.CrtYd") + (uuid "8af539d9-a4e8-4d11-bfbf-208eccab3a6c") + ) + (fp_line + (start 0.8 0.4) + (end 0.8 -0.4) + (stroke + (width 0.1) + (type solid) + ) + (layer "F.Fab") + (uuid "595a2aa4-5ac4-48f5-b5c5-6a89c9b34216") + ) + (fp_line + (start 0.8 -0.4) + (end -0.5 -0.4) + (stroke + (width 0.1) + (type solid) + ) + (layer "F.Fab") + (uuid "197670fa-5f81-4e4c-883a-64b98f07e2c7") + ) + (fp_line + (start -0.5 -0.4) + (end -0.8 -0.1) + (stroke + (width 0.1) + (type solid) + ) + (layer "F.Fab") + (uuid "f3143c7d-3de6-405f-9f9a-3450e3ea9750") + ) + (fp_line + (start -0.8 0.4) + (end 0.8 0.4) + (stroke + (width 0.1) + (type solid) + ) + (layer "F.Fab") + (uuid "d48a46c8-e448-485f-92ed-41d860585f52") + ) + (fp_line + (start -0.8 -0.1) + (end -0.8 0.4) + (stroke + (width 0.1) + (type solid) + ) + (layer "F.Fab") + (uuid "6d99aa6c-ea53-4995-908d-aa40f7a16932") + ) + (fp_text user "${REFERENCE}" + (at 0 0 0) + (layer "F.Fab") + (uuid "ad323e31-9d8d-4798-9771-66312cbb2ad0") + (effects + (font + (size 0.4 0.4) + (thickness 0.06) + ) + ) + ) + (pad "1" smd roundrect + (at -0.7875 0 180) + (size 0.875 0.95) + (layers "F.Cu" "F.Mask" "F.Paste") + (roundrect_rratio 0.25) + (net "GND") + (pinfunction "K_1") + (pintype "passive") + (uuid "f0585c91-d785-465a-b329-27541f7fce43") + ) + (pad "2" smd roundrect + (at 0.7875 0 180) + (size 0.875 0.95) + (layers "F.Cu" "F.Mask" "F.Paste") + (roundrect_rratio 0.25) + (net "Net-(D2-A)") + (pinfunction "A_2") + (pintype "passive") + (uuid "d842a600-b555-423c-b561-fc0d190151c3") + ) + (embedded_fonts no) + (model "${KICAD10_3DMODEL_DIR}/LED_SMD.3dshapes/LED_0603_1608Metric.step" + (offset + (xyz 0 0 0) + ) + (scale + (xyz 1 1 1) + ) + (rotate + (xyz 0 0 0) + ) + ) + ) + (gr_line + (start 162.814 96.52) + (end 162.814 86.36) + (stroke + (width 0.15) + (type dot) + ) + (layer "F.SilkS") + (uuid "5d883f5f-6412-44a4-a415-f39f08c836a4") + ) + (gr_poly + (pts + (xy 151.658589 90.824184) (xy 151.704839 90.869809) (xy 151.725395 90.889488) (xy 151.744502 90.907273) + (xy 151.762359 90.923311) (xy 151.779161 90.937747) (xy 151.795113 90.95073) (xy 151.81041 90.962402) + (xy 151.82525 90.972911) (xy 151.839832 90.982405) (xy 151.854355 90.991026) (xy 151.869017 90.998922) + (xy 151.884018 91.006243) (xy 151.899555 91.013128) (xy 151.915826 91.019728) (xy 151.933033 91.02619) + (xy 151.986114 91.045456) (xy 152.519637 91.045304) (xy 152.972127 91.04515) (xy 153.020999 90.998612) + (xy 153.070633 90.951617) (xy 153.121341 90.903979) (xy 153.172809 90.855882) (xy 153.158229 90.776088) + (xy 153.143647 90.696295) (xy 152.377713 90.696295) (xy 151.530744 90.696295) + ) + (stroke + (width 0) + (type solid) + ) + (fill yes) + (layer "B.SilkS") + (uuid "9e675410-ae4b-41f6-aa5a-531a91d26fc0") + ) + (gr_poly + (pts + (xy 156.464288 90.69941) (xy 156.463885 90.700119) (xy 156.463337 90.701563) (xy 156.461853 90.706498) + (xy 156.459906 90.713892) (xy 156.457573 90.723432) (xy 156.454935 90.734801) (xy 156.452061 90.74768) + (xy 156.449029 90.761755) (xy 156.445914 90.776705) (xy 156.430731 90.850806) (xy 156.476726 90.893321) + (xy 156.526111 90.939446) (xy 156.58026 90.990643) (xy 156.637802 91.045453) (xy 157.065066 91.044913) + (xy 157.322886 91.044153) (xy 157.493067 91.042286) (xy 157.591196 91.03902) (xy 157.61811 91.036768) + (xy 157.632854 91.034055) (xy 157.651286 91.027785) (xy 157.66942 91.020757) (xy 157.687359 91.012884) + (xy 157.705208 91.004099) (xy 157.723068 90.99432) (xy 157.741045 90.983471) (xy 157.759242 90.971475) + (xy 157.777764 90.95826) (xy 157.796708 90.943743) (xy 157.816185 90.927852) (xy 157.836293 90.910509) + (xy 157.857143 90.891634) (xy 157.87883 90.871154) (xy 157.901461 90.848993) (xy 157.92514 90.825073) + (xy 157.949971 90.799317) (xy 158.044267 90.700356) (xy 157.201853 90.698288) (xy 156.659981 90.697763) + ) + (stroke + (width 0) + (type solid) + ) + (fill yes) + (layer "B.SilkS") + (uuid "65240620-eb5a-4904-8e41-79e56c5096e9") + ) + (gr_poly + (pts + (xy 153.958513 89.927562) (xy 153.958513 90.003316) (xy 154.000542 90.022975) (xy 154.009205 90.027143) + (xy 154.01771 90.031453) (xy 154.025843 90.035785) (xy 154.033384 90.040011) (xy 154.040118 90.044005) + (xy 154.045832 90.047644) (xy 154.050305 90.050806) (xy 154.05201 90.052166) (xy 154.053324 90.053362) + (xy 154.054117 90.054412) (xy 154.054872 90.055966) (xy 154.055591 90.058057) (xy 154.056273 90.060721) + (xy 154.057531 90.067906) (xy 154.058656 90.0778) (xy 154.059653 90.090676) (xy 154.060531 90.106818) + (xy 154.061294 90.1265) (xy 154.061946 90.15) (xy 154.062956 90.209571) (xy 154.063612 90.28775) + (xy 154.063967 90.386764) (xy 154.064075 90.508834) (xy 154.064075 90.953575) (xy 154.013325 90.973446) + (xy 153.962574 90.993316) (xy 153.960261 91.076227) (xy 153.957948 91.159136) (xy 154.226479 91.159136) + (xy 154.495008 91.159136) (xy 154.492718 91.077003) (xy 154.490405 90.99487) (xy 154.439653 90.974069) + (xy 154.388903 90.953269) (xy 154.388903 90.50519) (xy 154.388903 90.057109) (xy 154.441466 90.03363) + (xy 154.494028 90.010151) (xy 154.494232 89.930981) (xy 154.494437 89.851812) (xy 154.226475 89.851812) + (xy 153.958513 89.851812) + ) + (stroke + (width 0) + (type solid) + ) + (fill yes) + (layer "B.SilkS") + (uuid "bb8804a2-f3fc-44d0-9a83-557651f09801") + ) + (gr_poly + (pts + (xy 156.782475 89.724108) (xy 156.621318 89.72728) (xy 156.589917 89.729555) (xy 156.581142 89.732356) + (xy 156.581335 89.733712) (xy 156.581902 89.735635) (xy 156.582822 89.738093) (xy 156.584076 89.741052) + (xy 156.585645 89.744471) (xy 156.587513 89.748317) (xy 156.592062 89.757146) (xy 156.597568 89.767252) + (xy 156.603882 89.778344) (xy 156.61085 89.790138) (xy 156.618317 89.802348) (xy 156.655492 89.862083) + (xy 156.620443 89.899579) (xy 156.601393 89.919743) (xy 156.575438 89.946929) (xy 156.516448 90.008122) + (xy 156.447501 90.079172) (xy 157.499042 90.078626) (xy 158.402367 90.07608) (xy 158.59705 90.072875) + (xy 158.645811 90.070647) (xy 158.668156 90.06795) (xy 158.687184 90.06222) (xy 158.705969 90.05562) + (xy 158.724629 90.048059) (xy 158.743278 90.039458) (xy 158.762029 90.029741) (xy 158.780999 90.018819) + (xy 158.800299 90.006614) (xy 158.820046 89.993046) (xy 158.840355 89.978028) (xy 158.861339 89.961484) + (xy 158.883107 89.943328) (xy 158.905784 89.923482) (xy 158.929478 89.901862) (xy 158.954303 89.878387) + (xy 158.980376 89.852977) (xy 159.007809 89.825547) (xy 159.099801 89.73253) (xy 159.011361 89.727158) + (xy 158.881434 89.725086) (xy 158.593047 89.723412) (xy 157.711514 89.721944) + ) + (stroke + (width 0) + (type solid) + ) + (fill yes) + (layer "B.SilkS") + (uuid "dc605b43-cad9-4477-b2ec-4847166ac7db") + ) + (gr_poly + (pts + (xy 150.874345 89.72476) (xy 150.484451 89.729631) (xy 150.485302 89.731037) (xy 150.487245 89.733532) + (xy 150.494179 89.74154) (xy 150.504788 89.753172) (xy 150.518616 89.767932) (xy 150.535193 89.785338) + (xy 150.554066 89.804902) (xy 150.574772 89.826134) (xy 150.59685 89.848547) (xy 150.636292 89.887736) + (xy 150.671828 89.921565) (xy 150.688332 89.936629) (xy 150.704099 89.950545) (xy 150.719214 89.963375) + (xy 150.733751 89.975187) (xy 150.747795 89.98604) (xy 150.761427 89.996001) (xy 150.774725 90.00513) + (xy 150.78777 90.013497) (xy 150.800641 90.02116) (xy 150.813424 90.028184) (xy 150.826191 90.034635) + (xy 150.839031 90.040576) (xy 150.854646 90.047552) (xy 150.861922 90.050699) (xy 150.869177 90.053628) + (xy 150.876667 90.056349) (xy 150.884643 90.058868) (xy 150.893357 90.061193) (xy 150.903066 90.063331) + (xy 150.914019 90.065293) (xy 150.926469 90.067083) (xy 150.940669 90.068711) (xy 150.956872 90.070184) + (xy 150.97533 90.071509) (xy 150.996298 90.072695) (xy 151.020027 90.073749) (xy 151.046772 90.074681) + (xy 151.07678 90.075495) (xy 151.11031 90.076202) (xy 151.18894 90.077322) (xy 151.284681 90.0781) + (xy 151.399554 90.0786) (xy 151.535582 90.078888) (xy 151.694786 90.07902) (xy 152.090816 90.079075) + (xy 153.142413 90.079178) (xy 153.036967 89.973266) (xy 152.931526 89.867358) (xy 152.966505 89.801861) + (xy 152.973425 89.788597) (xy 152.979676 89.776018) (xy 152.985135 89.764413) (xy 152.989679 89.754074) + (xy 152.993189 89.745287) (xy 152.995541 89.738346) (xy 152.996244 89.735658) (xy 152.996611 89.733539) + (xy 152.996628 89.732026) (xy 152.996501 89.731509) (xy 152.99628 89.731157) (xy 152.599541 89.725808) + (xy 151.735802 89.723582) + ) + (stroke + (width 0) + (type solid) + ) + (fill yes) + (layer "B.SilkS") + (uuid "b90f51b9-e14b-46b2-a785-2deaa37bd193") + ) + (gr_poly + (pts + (xy 154.735816 89.209628) (xy 154.65014 89.21641) (xy 154.564994 89.228368) (xy 154.48061 89.245458) + (xy 154.397211 89.267645) (xy 154.315027 89.294887) (xy 154.234284 89.327149) (xy 154.155213 89.364388) + (xy 154.078037 89.406565) (xy 154.002988 89.453645) (xy 153.930293 89.505585) (xy 153.882374 89.544119) + (xy 153.836055 89.585522) (xy 153.79145 89.629592) (xy 153.748679 89.676135) (xy 153.707859 89.724948) + (xy 153.669112 89.775838) (xy 153.632553 89.828603) (xy 153.598303 89.883049) (xy 153.566481 89.938974) + (xy 153.537202 89.996181) (xy 153.510588 90.05447) (xy 153.486757 90.113648) (xy 153.465827 90.173513) + (xy 153.447918 90.233869) (xy 153.433146 90.294514) (xy 153.421631 90.355253) (xy 153.418802 90.375189) + (xy 153.416404 90.397223) (xy 153.412884 90.446301) (xy 153.41104 90.499898) (xy 153.410845 90.555425) + (xy 153.41227 90.610288) (xy 153.415286 90.661902) (xy 153.41986 90.707673) (xy 153.422725 90.727559) + (xy 153.42597 90.745014) (xy 153.434542 90.783396) (xy 153.443733 90.820922) (xy 153.453501 90.85747) + (xy 153.463812 90.892912) (xy 153.474623 90.92712) (xy 153.4859 90.959972) (xy 153.497602 90.991337) + (xy 153.509694 91.021094) (xy 153.54687 91.101304) (xy 153.588873 91.178337) (xy 153.635526 91.252057) + (xy 153.686647 91.322326) (xy 153.742059 91.389006) (xy 153.801586 91.451964) (xy 153.865047 91.511058) + (xy 153.932263 91.566154) (xy 154.003056 91.617113) (xy 154.077248 91.663799) (xy 154.154662 91.706073) + (xy 154.235117 91.743799) (xy 154.318434 91.776839) (xy 154.404437 91.80506) (xy 154.492946 91.828319) + (xy 154.583781 91.846482) (xy 154.625659 91.852249) (xy 154.674162 91.856724) (xy 154.726954 91.859861) + (xy 154.781701 91.861619) (xy 154.836068 91.86195) (xy 154.887723 91.860806) (xy 154.934324 91.858151) + (xy 154.955001 91.856241) (xy 154.973544 91.853936) (xy 155.036787 91.843504) (xy 155.099361 91.830584) + (xy 155.160975 91.815263) (xy 155.22134 91.797635) (xy 155.280165 91.777789) (xy 155.33716 91.755819) + (xy 155.364881 91.744064) (xy 155.392038 91.731814) (xy 155.418591 91.719076) (xy 155.444504 91.705866) + (xy 155.515811 91.666025) (xy 155.583957 91.623142) (xy 155.648886 91.577298) (xy 155.710536 91.528572) + (xy 155.768847 91.477042) (xy 155.823757 91.42279) (xy 155.875208 91.365897) (xy 155.923137 91.306439) + (xy 155.967484 91.244496) (xy 156.008188 91.18015) (xy 156.045191 91.113478) (xy 156.078427 91.044564) + (xy 156.107841 90.973483) (xy 156.13337 90.900318) (xy 156.154953 90.825148) (xy 156.172531 90.748051) + (xy 156.176349 90.726349) (xy 156.179609 90.70255) (xy 156.182318 90.676954) (xy 156.184476 90.649856) + (xy 156.187152 90.592346) (xy 156.187497 90.551441) (xy 155.971292 90.551441) (xy 155.969822 90.603582) + (xy 155.968282 90.628652) (xy 155.966198 90.652768) (xy 155.963563 90.675718) (xy 155.960368 90.697283) + (xy 155.95266 90.737926) (xy 155.943005 90.77971) (xy 155.931623 90.821942) (xy 155.918748 90.863918) + (xy 155.9046 90.904948) (xy 155.889411 90.944333) (xy 155.873407 90.981376) (xy 155.865168 90.998801) + (xy 155.856812 91.01538) (xy 155.822086 91.081992) (xy 155.819687 91.040923) (xy 155.817287 90.999853) + (xy 155.766906 90.974848) (xy 155.716527 90.949841) (xy 155.670284 90.816976) (xy 155.568161 90.525672) + (xy 155.314585 90.525672) (xy 155.31445 90.526154) (xy 155.314044 90.52662) (xy 155.31337 90.527077) + (xy 155.312432 90.527518) (xy 155.309768 90.528361) (xy 155.306069 90.529151) (xy 155.301359 90.529883) + (xy 155.295655 90.530557) (xy 155.288976 90.531175) (xy 155.281341 90.531733) (xy 155.27277 90.532228) + (xy 155.263281 90.532662) (xy 155.241633 90.533341) (xy 155.216545 90.533755) (xy 155.188178 90.533893) + (xy 155.149285 90.533821) (xy 155.11934 90.533518) (xy 155.097246 90.532855) (xy 155.088802 90.532349) + (xy 155.081908 90.531707) (xy 155.076426 90.530909) (xy 155.072221 90.529941) (xy 155.069157 90.52879) + (xy 155.067091 90.527436) (xy 155.065892 90.525864) (xy 155.065421 90.524058) (xy 155.065538 90.522004) + (xy 155.066106 90.519683) (xy 155.125862 90.325866) (xy 155.164804 90.205106) (xy 155.17771 90.168468) + (xy 155.186896 90.145714) (xy 155.190248 90.139088) (xy 155.192858 90.135382) (xy 155.194785 90.134418) + (xy 155.196091 90.136012) (xy 155.220187 90.211348) (xy 155.260298 90.341956) (xy 155.297932 90.467007) + (xy 155.314585 90.525672) (xy 155.568161 90.525672) (xy 155.513196 90.368885) (xy 155.435737 90.145519) + (xy 155.412814 90.076996) (xy 155.406857 90.058089) (xy 155.405159 90.051149) (xy 155.405665 90.050782) + (xy 155.406592 90.05021) (xy 155.409611 90.048487) (xy 155.41403 90.046081) (xy 155.419663 90.04309) + (xy 155.433822 90.035743) (xy 155.450596 90.02723) (xy 155.493226 90.005815) (xy 155.493226 89.928813) + (xy 155.493226 89.851809) (xy 155.176545 89.851809) (xy 154.859865 89.851809) (xy 154.859865 89.927259) + (xy 154.859865 90.002706) (xy 154.904525 90.024608) (xy 154.913495 90.029185) (xy 154.921866 90.033789) + (xy 154.92945 90.038301) (xy 154.936065 90.042592) (xy 154.938953 90.044615) (xy 154.941531 90.046541) + (xy 154.943774 90.048344) (xy 154.945659 90.050015) (xy 154.947167 90.051541) (xy 154.948272 90.052901) + (xy 154.948954 90.054081) (xy 154.949127 90.0546) (xy 154.949183 90.055066) (xy 154.947346 90.06204) + (xy 154.942077 90.078704) (xy 154.922726 90.136738) (xy 154.894086 90.220442) (xy 154.859111 90.321086) + (xy 154.798719 90.496209) (xy 154.736529 90.680419) (xy 154.684166 90.838934) (xy 154.653256 90.936975) + (xy 154.652909 90.93803) (xy 154.65248 90.939092) (xy 154.651968 90.940165) (xy 154.651372 90.941249) + (xy 154.650697 90.942342) (xy 154.649939 90.943442) (xy 154.649101 90.944552) (xy 154.648183 90.945668) + (xy 154.647188 90.946793) (xy 154.646113 90.947922) (xy 154.644961 90.949058) (xy 154.64373 90.950202) + (xy 154.641042 90.952502) (xy 154.638054 90.95482) (xy 154.634771 90.957152) (xy 154.631198 90.959491) + (xy 154.627339 90.961837) (xy 154.623201 90.964186) (xy 154.618789 90.966532) (xy 154.614106 90.96887) + (xy 154.609161 90.971201) (xy 154.603955 90.973518) (xy 154.5597 90.992673) (xy 154.559546 91.075903) + (xy 154.559393 91.159134) (xy 154.802994 91.159134) (xy 155.046596 91.159134) (xy 155.046596 91.079021) + (xy 155.046596 90.998909) (xy 154.995846 90.980684) (xy 154.985548 90.976917) (xy 154.975742 90.973197) + (xy 154.966658 90.969621) (xy 154.958522 90.966285) (xy 154.951564 90.963288) (xy 154.946013 90.96072) + (xy 154.942099 90.95868) (xy 154.940826 90.957889) (xy 154.940049 90.957265) (xy 154.939936 90.95428) + (xy 154.94108 90.947334) (xy 154.946503 90.923817) (xy 154.955036 90.891205) (xy 154.965398 90.854) + (xy 154.976303 90.816696) (xy 154.986469 90.783786) (xy 154.994613 90.759766) (xy 154.997527 90.752496) + (xy 154.999455 90.749134) (xy 155.002372 90.748463) (xy 155.009331 90.747794) (xy 155.033684 90.746509) + (xy 155.112399 90.744353) (xy 155.20874 90.743175) (xy 155.25513 90.743115) (xy 155.295857 90.74349) + (xy 155.386779 90.745011) (xy 155.420203 90.847743) (xy 155.428897 90.874904) (xy 155.435769 90.897416) + (xy 155.440898 90.915674) (xy 155.444372 90.930068) (xy 155.445513 90.935939) (xy 155.446271 90.94099) + (xy 155.446656 90.94527) (xy 155.446679 90.948829) (xy 155.44635 90.951716) (xy 155.445676 90.953978) + (xy 155.444676 90.955667) (xy 155.443351 90.956828) (xy 155.44204 90.95755) (xy 155.440245 90.958403) + (xy 155.435328 90.96045) (xy 155.428853 90.962881) (xy 155.421074 90.965615) (xy 155.41224 90.96856) + (xy 155.402606 90.971636) (xy 155.392421 90.974752) (xy 155.381937 90.977824) (xy 155.330796 90.992469) + (xy 155.330796 91.075801) (xy 155.330796 91.159132) (xy 155.545978 91.159132) (xy 155.629529 91.159554) + (xy 155.697949 91.160701) (xy 155.744181 91.1624) (xy 155.756767 91.163399) (xy 155.760043 91.163929) + (xy 155.761158 91.164472) (xy 155.76096 91.165811) (xy 155.760377 91.167533) (xy 155.758104 91.17206) + (xy 155.754457 91.177917) (xy 155.749548 91.184974) (xy 155.743491 91.193097) (xy 155.736402 91.202149) + (xy 155.728392 91.211999) (xy 155.719572 91.22251) (xy 155.699974 91.244991) (xy 155.678518 91.268515) + (xy 155.667376 91.280337) (xy 155.656111 91.292018) (xy 155.644835 91.303424) (xy 155.633664 91.314424) + (xy 155.597031 91.348463) (xy 155.55895 91.380943) (xy 155.5195 91.411819) (xy 155.478767 91.441052) + (xy 155.436839 91.468604) (xy 155.393797 91.494434) (xy 155.349727 91.518505) (xy 155.304709 91.540771) + (xy 155.258836 91.561201) (xy 155.212186 91.579747) (xy 155.164845 91.596377) (xy 155.116899 91.611047) + (xy 155.068428 91.623715) (xy 155.019522 91.634346) (xy 154.970263 91.642898) (xy 154.920735 91.649333) + (xy 154.907373 91.650432) (xy 154.891238 91.651179) (xy 154.852187 91.651687) (xy 154.806627 91.650994) + (xy 154.757605 91.649239) (xy 154.708166 91.64656) (xy 154.661357 91.643091) (xy 154.620224 91.638968) + (xy 154.587816 91.634334) (xy 154.587843 91.634337) (xy 154.55961 91.628822) (xy 154.53098 91.622347) + (xy 154.502038 91.614942) (xy 154.472868 91.606639) (xy 154.443549 91.597477) (xy 154.414166 91.587487) + (xy 154.384802 91.576703) (xy 154.355539 91.56516) (xy 154.326458 91.552888) (xy 154.297645 91.539926) + (xy 154.269182 91.526302) (xy 154.24115 91.512056) (xy 154.213634 91.497217) (xy 154.186713 91.481821) + (xy 154.160475 91.4659) (xy 154.134996 91.449492) (xy 154.115618 91.436028) (xy 154.095171 91.42071) + (xy 154.073862 91.403741) (xy 154.051897 91.385331) (xy 154.029481 91.365681) (xy 154.006825 91.345003) + (xy 153.984131 91.323501) (xy 153.961611 91.301381) (xy 153.939465 91.278853) (xy 153.917903 91.25612) + (xy 153.897132 91.23339) (xy 153.87736 91.210868) (xy 153.858792 91.188762) (xy 153.841632 91.16728) + (xy 153.826091 91.146623) (xy 153.812372 91.127004) (xy 153.778766 91.073814) (xy 153.748462 91.019658) + (xy 153.721474 90.964631) (xy 153.697816 90.908826) (xy 153.677501 90.85233) (xy 153.660546 90.795244) + (xy 153.646959 90.737652) (xy 153.636755 90.67965) (xy 153.629952 90.621331) (xy 153.626558 90.562784) + (xy 153.626587 90.504104) (xy 153.630055 90.445382) (xy 153.636975 90.386712) (xy 153.647359 90.328183) + (xy 153.66122 90.26989) (xy 153.678574 90.211924) (xy 153.705632 90.138713) (xy 153.737447 90.068058) + (xy 153.773837 90.000119) (xy 153.81462 89.935054) (xy 153.859608 89.87302) (xy 153.908619 89.814177) + (xy 153.96147 89.758684) (xy 154.017977 89.706698) (xy 154.077954 89.658379) (xy 154.141218 89.613883) + (xy 154.207587 89.57337) (xy 154.276874 89.536998) (xy 154.348894 89.504925) (xy 154.423467 89.477312) + (xy 154.500408 89.454315) (xy 154.579531 89.436094) (xy 154.602418 89.432009) (xy 154.626872 89.428438) + (xy 154.652673 89.42538) (xy 154.679601 89.422835) (xy 154.735947 89.419293) (xy 154.794147 89.417826) + (xy 154.852427 89.418448) (xy 154.909022 89.421173) (xy 154.936133 89.423326) (xy 154.962163 89.42601) + (xy 154.986888 89.429225) (xy 155.010086 89.432975) (xy 155.057156 89.44238) (xy 155.103567 89.453557) + (xy 155.149292 89.466497) (xy 155.194298 89.48118) (xy 155.238558 89.497591) (xy 155.282042 89.515716) + (xy 155.324724 89.535536) (xy 155.366573 89.557039) (xy 155.407561 89.580209) (xy 155.447658 89.605029) + (xy 155.486839 89.631485) (xy 155.52507 89.659559) (xy 155.562326 89.689238) (xy 155.598577 89.720507) + (xy 155.633793 89.753348) (xy 155.667948 89.787747) (xy 155.693404 89.815232) (xy 155.717724 89.843232) + (xy 155.740925 89.871778) (xy 155.763024 89.900901) (xy 155.784034 89.930634) (xy 155.803977 89.961001) + (xy 155.822867 89.992041) (xy 155.840718 90.023779) (xy 155.857552 90.056245) (xy 155.873381 90.089473) + (xy 155.888224 90.123494) (xy 155.902096 90.158336) (xy 155.915013 90.194028) (xy 155.926993 90.230606) + (xy 155.938055 90.268096) (xy 155.94821 90.306529) (xy 155.95269 90.326295) (xy 155.956706 90.347681) + (xy 155.96025 90.370479) (xy 155.963315 90.394467) (xy 155.965896 90.419435) (xy 155.967984 90.445166) + (xy 155.970662 90.498063) (xy 155.971292 90.551441) (xy 156.187497 90.551441) (xy 156.187658 90.5324) + (xy 156.186017 90.472395) (xy 156.182255 90.414708) (xy 156.179582 90.387482) (xy 156.176389 90.361724) + (xy 156.172675 90.337738) (xy 156.168443 90.315816) (xy 156.152922 90.250625) (xy 156.134842 90.186927) + (xy 156.114228 90.124761) (xy 156.091105 90.064171) (xy 156.065499 90.005194) (xy 156.037435 89.947865) + (xy 156.006939 89.892231) (xy 155.974037 89.838327) (xy 155.938753 89.786193) (xy 155.901113 89.735869) + (xy 155.861141 89.687394) (xy 155.818864 89.640807) (xy 155.774304 89.59615) (xy 155.727494 89.553458) + (xy 155.678453 89.512774) (xy 155.627206 89.474137) (xy 155.610399 89.46254) (xy 155.59111 89.450065) + (xy 155.546424 89.423173) (xy 155.495834 89.394841) (xy 155.442024 89.366444) (xy 155.387679 89.339361) + (xy 155.335488 89.314968) (xy 155.288128 89.294642) (xy 155.267099 89.286436) (xy 155.248289 89.279763) + (xy 155.164202 89.254684) (xy 155.079283 89.235014) (xy 154.993759 89.220712) (xy 154.907853 89.21174) + (xy 154.821796 89.208059) + ) + (stroke + (width 0) + (type solid) + ) + (fill yes) + (layer "B.SilkS") + (uuid "7ef55f99-9318-41b8-b222-ed89d130645e") + ) + (gr_poly + (pts + (xy 154.770882 88.757276) (xy 154.743542 88.757706) (xy 154.717648 88.758437) (xy 154.694042 88.759468) + (xy 154.586506 88.765227) (xy 154.547011 88.8883) (xy 154.526352 88.952747) (xy 154.510529 89.000488) + (xy 154.503894 89.018961) (xy 154.497821 89.034299) (xy 154.492093 89.046848) (xy 154.486498 89.056956) + (xy 154.480814 89.064972) (xy 154.474832 89.07124) (xy 154.46833 89.076108) (xy 154.461095 89.079924) + (xy 154.452909 89.083034) (xy 154.443557 89.085786) (xy 154.420495 89.091601) (xy 154.407127 89.09513) + (xy 154.392127 89.099312) (xy 154.359254 89.109019) (xy 154.325951 89.119481) (xy 154.310411 89.124606) + (xy 154.296291 89.129453) (xy 154.232099 89.152036) (xy 154.11131 89.050703) (xy 153.990517 88.949369) + (xy 153.919719 88.991844) (xy 153.880913 89.015585) (xy 153.840606 89.041039) (xy 153.800856 89.06682) + (xy 153.763718 89.091538) (xy 153.731254 89.113803) (xy 153.705518 89.13223) (xy 153.68857 89.145423) + (xy 153.684033 89.149628) (xy 153.68246 89.152003) (xy 153.683289 89.156021) (xy 153.685665 89.16509) + (xy 153.694387 89.19607) (xy 153.707298 89.24031) (xy 153.723063 89.293179) (xy 153.738825 89.346163) + (xy 153.751736 89.39071) (xy 153.760457 89.422144) (xy 153.762831 89.431481) (xy 153.763661 89.435785) + (xy 153.763557 89.436284) (xy 153.763251 89.436981) (xy 153.762046 89.438948) (xy 153.760092 89.441641) + (xy 153.757425 89.445017) (xy 153.754086 89.44903) (xy 153.750118 89.453635) (xy 153.740448 89.464448) + (xy 153.728741 89.477099) (xy 153.715321 89.491233) (xy 153.700512 89.506492) (xy 153.684638 89.522523) + (xy 153.605616 89.601547) (xy 153.459308 89.571778) (xy 153.352233 89.549714) (xy 153.300248 89.538476) + (xy 153.299382 89.538311) (xy 153.298465 89.538287) (xy 153.297499 89.538404) (xy 153.296482 89.538662) + (xy 153.295415 89.539065) (xy 153.294293 89.539613) (xy 153.293117 89.54031) (xy 153.291883 89.541156) + (xy 153.290595 89.54215) (xy 153.289247 89.543297) (xy 153.28784 89.544598) (xy 153.286372 89.546057) + (xy 153.284843 89.547668) (xy 153.283249 89.54944) (xy 153.281592 89.55137) (xy 153.279868 89.553464) + (xy 153.276218 89.558136) (xy 153.272289 89.563476) (xy 153.268074 89.56949) (xy 153.263558 89.576194) + (xy 153.258735 89.583597) (xy 153.253592 89.591716) (xy 153.248121 89.600558) (xy 153.242313 89.610139) + (xy 153.181519 89.712628) (xy 153.157991 89.753432) (xy 153.138992 89.787375) (xy 153.124484 89.81452) + (xy 153.114427 89.834943) (xy 153.108782 89.848713) (xy 153.107603 89.853126) (xy 153.107511 89.855901) + (xy 153.10779 89.856607) (xy 153.108362 89.85761) (xy 153.110358 89.860472) (xy 153.11344 89.864417) + (xy 153.117547 89.869382) (xy 153.128601 89.882108) (xy 153.143032 89.898118) (xy 153.160364 89.916887) + (xy 153.180115 89.937889) (xy 153.2018 89.960594) (xy 153.224939 89.984477) (xy 153.339882 90.102267) + (xy 153.315303 90.186122) (xy 153.288147 90.279494) (xy 153.287761 90.279977) (xy 153.286891 90.280442) + (xy 153.283816 90.28132) (xy 153.279169 90.282125) (xy 153.273201 90.282855) (xy 153.258277 90.284076) + (xy 153.241023 90.284965) (xy 153.223413 90.285498) (xy 153.207427 90.285655) (xy 153.19504 90.285412) + (xy 153.190814 90.285133) (xy 153.188229 90.28475) (xy 153.187086 90.284426) (xy 153.185988 90.28407) + (xy 153.184934 90.283674) (xy 153.183923 90.283239) (xy 153.182957 90.282761) (xy 153.182033 90.282241) + (xy 153.181149 90.281676) (xy 153.180306 90.281063) (xy 153.179502 90.280401) (xy 153.178734 90.279686) + (xy 153.178007 90.278917) (xy 153.177312 90.278093) (xy 153.176654 90.277212) (xy 153.176029 90.27627) + (xy 153.175438 90.275266) (xy 153.174879 90.2742) (xy 153.174352 90.273068) (xy 153.173851 90.271866) + (xy 153.173381 90.270596) (xy 153.172941 90.269255) (xy 153.172526 90.267838) (xy 153.172137 90.266346) + (xy 153.171774 90.264776) (xy 153.171436 90.263126) (xy 153.171118 90.261396) (xy 153.170824 90.25958) + (xy 153.170295 90.255688) (xy 153.169844 90.251438) (xy 153.169463 90.246809) (xy 153.167007 90.213134) + (xy 152.133566 90.211077) (xy 151.346468 90.211485) (xy 151.107032 90.213056) (xy 151.019091 90.215355) + (xy 151.019255 90.21584) (xy 151.019738 90.216633) (xy 151.021633 90.21911) (xy 151.024715 90.222726) + (xy 151.028916 90.22741) (xy 151.040432 90.239729) (xy 151.055668 90.255546) (xy 151.074113 90.274334) + (xy 151.095257 90.295566) (xy 151.118592 90.318723) (xy 151.143602 90.343276) (xy 151.188313 90.386303) + (xy 151.208328 90.404994) (xy 151.227027 90.421977) (xy 151.244591 90.437387) (xy 151.261204 90.451357) + (xy 151.277042 90.464016) (xy 151.292297 90.475503) (xy 151.307145 90.485946) (xy 151.321769 90.49548) + (xy 151.336351 90.504235) (xy 151.351071 90.512346) (xy 151.366119 90.519945) (xy 151.381666 90.527167) + (xy 151.397906 90.53414) (xy 151.415012 90.541001) (xy 151.469754 90.5623) (xy 152.375972 90.5645) + (xy 153.282193 90.5667) (xy 153.286985 90.634631) (xy 153.28961 90.664035) (xy 153.293273 90.692459) + (xy 153.297976 90.719927) (xy 153.30072 90.733309) (xy 153.303726 90.74646) (xy 153.306996 90.759385) + (xy 153.31053 90.772085) (xy 153.314327 90.784563) (xy 153.318391 90.796823) (xy 153.322719 90.808865) + (xy 153.327316 90.820696) (xy 153.332179 90.832316) (xy 153.33731 90.84373) (xy 153.341367 90.852764) + (xy 153.345155 90.861809) (xy 153.348591 90.870626) (xy 153.351593 90.878972) (xy 153.354073 90.886605) + (xy 153.355956 90.893282) (xy 153.356645 90.896186) (xy 153.357151 90.89876) (xy 153.357467 90.900975) + (xy 153.35758 90.9028) (xy 153.357429 90.903835) (xy 153.356976 90.905159) (xy 153.35517 90.908645) + (xy 153.352198 90.913218) (xy 153.348097 90.918835) (xy 153.342903 90.92546) (xy 153.336651 90.933048) + (xy 153.329378 90.941562) (xy 153.321121 90.950956) (xy 153.301798 90.972236) (xy 153.278973 90.996557) + (xy 153.252932 91.023596) (xy 153.223971 91.053027) (xy 153.090282 91.187557) (xy 153.14537 91.284989) + (xy 153.183984 91.352315) (xy 153.226473 91.42505) (xy 153.262212 91.485148) (xy 153.274228 91.504822) + (xy 153.280573 91.514568) (xy 153.281574 91.514641) (xy 153.284178 91.514403) (xy 153.293852 91.513065) + (xy 153.308933 91.510668) (xy 153.328762 91.507325) (xy 153.37999 91.498256) (xy 153.442238 91.486779) + (xy 153.602213 91.456755) (xy 153.682936 91.537177) (xy 153.699151 91.553511) (xy 153.714279 91.569096) + (xy 153.727988 91.583564) (xy 153.739946 91.596554) (xy 153.749824 91.607693) (xy 153.75729 91.616624) + (xy 153.760014 91.620143) (xy 153.762011 91.622974) (xy 153.763241 91.625066) (xy 153.763661 91.626378) + (xy 153.763445 91.628023) (xy 153.762819 91.631097) (xy 153.760409 91.641214) (xy 153.751561 91.675117) + (xy 153.738465 91.723048) (xy 153.722475 91.779968) (xy 153.714343 91.809158) (xy 153.707028 91.836584) + (xy 153.70067 91.861629) (xy 153.695416 91.883682) (xy 153.691399 91.902129) (xy 153.688763 91.916355) + (xy 153.687648 91.925743) (xy 153.687705 91.928434) (xy 153.688196 91.929682) (xy 153.731663 91.9598) + (xy 153.779051 91.991403) (xy 153.827615 92.022798) (xy 153.874604 92.052289) (xy 153.917275 92.07818) + (xy 153.952882 92.098774) (xy 153.978674 92.112372) (xy 153.987034 92.116022) (xy 153.991912 92.117285) + (xy 153.992501 92.117152) (xy 153.993388 92.116755) (xy 153.996019 92.115201) (xy 153.999742 92.112678) + (xy 154.004488 92.109235) (xy 154.016806 92.099798) (xy 154.032447 92.087316) (xy 154.050902 92.072202) + (xy 154.071647 92.054878) (xy 154.094172 92.035761) (xy 154.117954 92.015267) (xy 154.235485 91.913248) + (xy 154.289862 91.931392) (xy 154.302196 91.935393) (xy 154.316423 91.93981) (xy 154.348543 91.949314) + (xy 154.382171 91.958739) (xy 154.413261 91.96692) (xy 154.482281 91.984301) (xy 154.534475 92.143081) + (xy 154.586667 92.30186) (xy 154.652215 92.306964) (xy 154.685843 92.308892) (xy 154.726649 92.310155) + (xy 154.771812 92.310772) (xy 154.818519 92.310766) (xy 154.863952 92.310151) (xy 154.905296 92.308949) + (xy 154.939729 92.307177) (xy 154.96444 92.304856) (xy 154.964441 92.30487) (xy 155 92.3) (xy 155.050646 92.141927) + (xy 155.101295 91.983855) (xy 155.145011 91.973271) (xy 155.168537 91.967151) (xy 155.200283 91.958307) + (xy 155.236087 91.947924) (xy 155.271787 91.937187) (xy 155.354852 91.911683) (xy 155.417949 91.967847) + (xy 155.497324 92.037891) (xy 155.527305 92.063771) (xy 155.551444 92.084078) (xy 155.570211 92.099195) + (xy 155.584074 92.109511) (xy 155.593503 92.115418) (xy 155.596703 92.116836) (xy 155.598972 92.1173) + (xy 155.600277 92.117184) (xy 155.601903 92.116833) (xy 155.60613 92.115429) (xy 155.611662 92.113077) + (xy 155.618514 92.109775) (xy 155.626697 92.105511) (xy 155.636222 92.10028) (xy 155.647103 92.094074) + (xy 155.659355 92.086888) (xy 155.688008 92.069537) (xy 155.722283 92.048169) (xy 155.762276 92.02273) + (xy 155.808088 91.993153) (xy 155.833138 91.976681) (xy 155.854048 91.962464) (xy 155.871024 91.95034) + (xy 155.884267 91.94014) (xy 155.889554 91.93571) (xy 155.893985 91.931698) (xy 155.897585 91.928088) + (xy 155.900381 91.924852) (xy 155.902397 91.921973) (xy 155.903661 91.91943) (xy 155.904196 91.917203) + (xy 155.90403 91.91527) (xy 155.867408 91.783222) (xy 155.854032 91.735452) (xy 155.84192 91.693289) + (xy 155.832403 91.661293) (xy 155.826814 91.644032) (xy 155.826193 91.642315) (xy 155.825659 91.640642) + (xy 155.825221 91.639001) (xy 155.824897 91.637372) (xy 155.82469 91.635746) (xy 155.824612 91.634108) + (xy 155.824674 91.632441) (xy 155.824885 91.630734) (xy 155.825258 91.628971) (xy 155.825798 91.627137) + (xy 155.826523 91.62522) (xy 155.827436 91.623205) (xy 155.828552 91.621076) (xy 155.829878 91.618823) + (xy 155.831428 91.616425) (xy 155.833208 91.613874) (xy 155.835233 91.611154) (xy 155.837507 91.608249) + (xy 155.840045 91.605148) (xy 155.842857 91.601834) (xy 155.845952 91.598293) (xy 155.849342 91.594511) + (xy 155.85704 91.586171) (xy 155.866037 91.576695) (xy 155.876412 91.565975) (xy 155.888251 91.553894) + (xy 155.901635 91.540337) (xy 155.984169 91.456932) (xy 156.144418 91.488309) (xy 156.206911 91.500204) + (xy 156.258607 91.509393) (xy 156.294133 91.514969) (xy 156.304154 91.516119) (xy 156.306935 91.516233) + (xy 156.308118 91.516023) (xy 156.319147 91.498888) (xy 156.341401 91.460901) (xy 156.403829 91.350591) + (xy 156.463877 91.241547) (xy 156.483162 91.20514) (xy 156.488394 91.194553) (xy 156.49003 91.19022) + (xy 156.477041 91.175514) (xy 156.444458 91.141465) (xy 156.346686 91.041784) (xy 156.249061 90.944061) + (xy 156.216661 90.912463) (xy 156.207428 90.903869) (xy 156.203929 90.901183) (xy 156.203171 90.902945) + (xy 156.201556 90.907152) (xy 156.196155 90.9218) (xy 156.188531 90.942921) (xy 156.179484 90.968318) + (xy 156.153376 91.036603) (xy 156.124131 91.102929) (xy 156.091791 91.167252) (xy 156.056394 91.229529) + (xy 156.017983 91.289714) (xy 155.976598 91.347765) (xy 155.93228 91.403635) (xy 155.885071 91.457283) + (xy 155.835009 91.508663) (xy 155.782136 91.557733) (xy 155.726492 91.604445) (xy 155.66812 91.648758) + (xy 155.607061 91.69063) (xy 155.543352 91.730015) (xy 155.477036 91.766866) (xy 155.408155 91.801141) + (xy 155.352 91.82587) (xy 155.294296 91.848033) (xy 155.235209 91.867616) (xy 155.174899 91.884607) + (xy 155.113529 91.898992) (xy 155.051264 91.91076) (xy 154.988261 91.919895) (xy 154.924686 91.926387) + (xy 154.860701 91.930219) (xy 154.796467 91.931382) (xy 154.732146 91.929859) (xy 154.667904 91.925638) + (xy 154.603896 91.918707) (xy 154.54029 91.909055) (xy 154.477248 91.896664) (xy 154.414929 91.881523) + (xy 154.312963 91.850384) (xy 154.214477 91.81246) (xy 154.119741 91.768048) (xy 154.029026 91.717442) + (xy 153.942608 91.660935) (xy 153.860753 91.598819) (xy 153.783735 91.531389) (xy 153.711828 91.45894) + (xy 153.645302 91.381764) (xy 153.584426 91.300153) (xy 153.529475 91.214404) (xy 153.48072 91.124809) + (xy 153.438431 91.031662) (xy 153.402882 90.935255) (xy 153.374344 90.835884) (xy 153.362789 90.785178) + (xy 153.353088 90.733843) (xy 153.34984 90.711657) (xy 153.347161 90.687004) (xy 153.343473 90.63175) + (xy 153.341944 90.570986) (xy 153.34251 90.507618) (xy 153.345096 90.444552) (xy 153.349628 90.384696) + (xy 153.356034 90.330955) (xy 153.359918 90.307285) (xy 153.364243 90.286234) (xy 153.3777 90.232415) + (xy 153.392951 90.179428) (xy 153.409976 90.127312) (xy 153.428753 90.076104) (xy 153.449267 90.025831) + (xy 153.471494 89.976536) (xy 153.495415 89.92825) (xy 153.521011 89.881011) (xy 153.548263 89.83485) + (xy 153.577148 89.789802) (xy 153.607649 89.745908) (xy 153.639746 89.703196) (xy 153.673417 89.661706) + (xy 153.708644 89.62147) (xy 153.745407 89.582524) (xy 153.783689 89.544903) (xy 153.840042 89.494474) + (xy 153.899317 89.447007) (xy 153.961256 89.402596) (xy 154.025601 89.361331) (xy 154.092097 89.323309) + (xy 154.160487 89.288618) (xy 154.230515 89.257353) (xy 154.301926 89.229606) (xy 154.374462 89.205469) + (xy 154.447865 89.185035) (xy 154.521882 89.168395) (xy 154.596255 89.155642) (xy 154.670726 89.146871) + (xy 154.745042 89.142171) (xy 154.818943 89.141636) (xy 154.892176 89.145356) (xy 154.961601 89.152025) + (xy 155.026949 89.160591) (xy 155.058394 89.165658) (xy 155.089179 89.171286) (xy 155.119431 89.177504) + (xy 155.149267 89.184342) (xy 155.178804 89.191829) (xy 155.208166 89.199993) (xy 155.237475 89.208865) + (xy 155.26685 89.218473) (xy 155.326279 89.24001) (xy 155.387417 89.264843) (xy 155.446282 89.291501) + (xy 155.503652 89.32065) (xy 155.559459 89.352218) (xy 155.61363 89.386143) (xy 155.6661 89.422358) + (xy 155.716799 89.460802) (xy 155.765661 89.501405) (xy 155.812615 89.544103) (xy 155.857592 89.588832) + (xy 155.900528 89.635528) (xy 155.941349 89.684123) (xy 155.979989 89.73455) (xy 156.016378 89.78675) + (xy 156.050448 89.840655) (xy 156.082132 89.896196) (xy 156.111359 89.953313) (xy 156.126225 89.98481) + (xy 156.140144 90.015849) (xy 156.153134 90.046517) (xy 156.165229 90.076918) (xy 156.176453 90.107139) + (xy 156.186829 90.137277) (xy 156.196386 90.167425) (xy 156.205147 90.197678) (xy 156.213142 90.228128) + (xy 156.220394 90.25887) (xy 156.226928 90.289999) (xy 156.232774 90.321607) (xy 156.237955 90.353791) + (xy 156.242497 90.38664) (xy 156.246427 90.420254) (xy 156.24977 90.454726) (xy 156.259546 90.566375) + (xy 157.163837 90.566375) (xy 157.740534 90.565824) (xy 157.916436 90.564051) (xy 158.036065 90.560317) + (xy 158.113438 90.5539) (xy 158.162567 90.544079) (xy 158.197476 90.530129) (xy 158.232177 90.511331) + (xy 158.244213 90.504533) (xy 158.256189 90.49731) (xy 158.268214 90.489571) (xy 158.280392 90.481223) + (xy 158.29283 90.472174) (xy 158.305631 90.462335) (xy 158.318903 90.451614) (xy 158.332753 90.43992) + (xy 158.347282 90.427163) (xy 158.362601 90.41325) (xy 158.378812 90.398091) (xy 158.396024 90.381595) + (xy 158.433865 90.344226) (xy 158.476973 90.300416) (xy 158.565927 90.209035) (xy 157.449789 90.211092) + (xy 156.495713 90.213152) (xy 156.498415 90.251719) (xy 156.498409 90.25502) (xy 156.497934 90.258126) + (xy 156.497019 90.261046) (xy 156.495688 90.263784) (xy 156.493971 90.266347) (xy 156.491894 90.268739) + (xy 156.489484 90.270968) (xy 156.486767 90.273038) (xy 156.48377 90.274957) (xy 156.480524 90.276726) + (xy 156.477051 90.278357) (xy 156.47338 90.279853) (xy 156.465556 90.282461) (xy 156.457265 90.284598) + (xy 156.448726 90.286311) (xy 156.440155 90.287645) (xy 156.431769 90.288645) (xy 156.423784 90.289362) + (xy 156.409889 90.29012) (xy 156.400199 90.29029) (xy 156.398651 90.290269) (xy 156.397164 90.2902) + (xy 156.395736 90.290078) (xy 156.394361 90.289901) (xy 156.393036 90.289662) (xy 156.391762 90.289356) + (xy 156.39053 90.288979) (xy 156.389335 90.288528) (xy 156.388178 90.287997) (xy 156.387054 90.287378) + (xy 156.385957 90.286672) (xy 156.384886 90.285869) (xy 156.383835 90.284968) (xy 156.382803 90.283963) + (xy 156.381782 90.282848) (xy 156.380773 90.28162) (xy 156.37977 90.280274) (xy 156.378769 90.278803) + (xy 156.377767 90.277206) (xy 156.376761 90.275474) (xy 156.375745 90.273608) (xy 156.374717 90.271598) + (xy 156.373672 90.269441) (xy 156.372609 90.26713) (xy 156.370408 90.262039) (xy 156.368081 90.256285) + (xy 156.365601 90.249827) (xy 156.362939 90.24263) (xy 156.359035 90.232446) (xy 156.354595 90.22177) + (xy 156.349769 90.210916) (xy 156.344717 90.200205) (xy 156.339592 90.189951) (xy 156.334546 90.180477) + (xy 156.329735 90.172096) (xy 156.325312 90.16513) (xy 156.321244 90.159016) (xy 156.317449 90.153079) + (xy 156.31401 90.14747) (xy 156.31101 90.142331) (xy 156.308531 90.13781) (xy 156.306658 90.134054) + (xy 156.305976 90.132507) (xy 156.305474 90.131208) (xy 156.305167 90.130173) (xy 156.305061 90.129421) + (xy 156.304946 90.129004) (xy 156.304619 90.128361) (xy 156.303413 90.126421) (xy 156.299494 90.120107) + (xy 156.297158 90.115825) (xy 156.295981 90.113425) (xy 156.294828 90.110862) (xy 156.293727 90.10814) + (xy 156.292697 90.105264) (xy 156.291764 90.102242) (xy 156.290952 90.09908) (xy 156.290286 90.095784) + (xy 156.289787 90.092358) (xy 156.28948 90.088813) (xy 156.289392 90.085149) (xy 156.289545 90.081377) + (xy 156.289959 90.077499) (xy 156.290664 90.073527) (xy 156.291681 90.069463) (xy 156.293035 90.065311) + (xy 156.294747 90.06108) (xy 156.296843 90.056777) (xy 156.299348 90.052405) (xy 156.302285 90.047974) + (xy 156.305677 90.043485) (xy 156.309547 90.03895) (xy 156.313923 90.03437) (xy 156.352043 89.996068) + (xy 156.389379 89.957702) (xy 156.421646 89.923731) (xy 156.434537 89.909784) (xy 156.444555 89.898607) + (xy 156.485293 89.852023) (xy 156.403331 89.711844) (xy 156.37039 89.656068) (xy 156.341284 89.607824) + (xy 156.319174 89.572272) (xy 156.311726 89.56087) (xy 156.307209 89.554576) (xy 156.293049 89.537484) + (xy 156.136363 89.567896) (xy 155.979677 89.598308) (xy 155.902785 89.520028) (xy 155.873364 89.490463) + (xy 155.861765 89.47853) (xy 155.856718 89.473068) (xy 155.852165 89.467868) (xy 155.848093 89.462869) + (xy 155.844501 89.458013) (xy 155.841376 89.453242) (xy 155.838715 89.448498) (xy 155.836509 89.443721) + (xy 155.834751 89.438853) (xy 155.833433 89.433837) (xy 155.83255 89.428612) (xy 155.83209 89.423123) + (xy 155.832051 89.417307) (xy 155.832422 89.411108) (xy 155.833197 89.404469) (xy 155.834368 89.397329) + (xy 155.835929 89.38963) (xy 155.837872 89.381315) (xy 155.840188 89.372325) (xy 155.845917 89.352083) + (xy 155.853055 89.328439) (xy 155.87133 89.269068) (xy 155.896767 89.184809) (xy 155.904507 89.158603) + (xy 155.907348 89.148298) (xy 155.905744 89.14644) (xy 155.90113 89.142711) (xy 155.884017 89.1304) + (xy 155.858307 89.112874) (xy 155.826308 89.091649) (xy 155.790322 89.068235) (xy 155.752654 89.044148) + (xy 155.715609 89.020897) (xy 155.681486 88.999997) (xy 155.597723 88.949514) (xy 155.539384 88.996602) + (xy 155.478735 89.045911) (xy 155.414839 89.098339) (xy 155.348633 89.152986) (xy 155.300853 89.133571) + (xy 155.289644 89.129289) (xy 155.276073 89.124551) (xy 155.260659 89.119514) (xy 155.243924 89.114341) + (xy 155.226385 89.109181) (xy 155.208563 89.104195) (xy 155.190978 89.099536) (xy 155.174149 89.095367) + (xy 155.152616 89.090085) (xy 155.135021 89.085383) (xy 155.127583 89.083198) (xy 155.120982 89.081097) + (xy 155.115173 89.07906) (xy 155.110108 89.077064) (xy 155.105737 89.075095) (xy 155.102011 89.073127) + (xy 155.098885 89.07114) (xy 155.096305 89.069113) (xy 155.094225 89.06703) (xy 155.092602 89.064867) + (xy 155.091378 89.062605) (xy 155.090511 89.060223) (xy 155.07427 89.007789) (xy 155.041838 88.904694) + (xy 154.99788 88.765519) (xy 154.89973 88.759623) (xy 154.87782 88.758556) (xy 154.853135 88.757786) + (xy 154.826522 88.757316) (xy 154.798823 88.757148) + ) + (stroke + (width 0) + (type solid) + ) + (fill yes) + (layer "B.SilkS") + (uuid "e765da29-0974-4ae3-afd6-5e758c5afed9") + ) + (gr_line + (start 142.24 87) + (end 142.24 96) + (stroke + (width 0.05) + (type default) + ) + (layer "Edge.Cuts") + (uuid "5857b6c4-4655-403f-8ff8-d50db0a9cf1a") + ) + (gr_arc + (start 142.24 87) + (mid 142.532893 86.292893) + (end 143.24 86) + (stroke + (width 0.05) + (type default) + ) + (layer "Edge.Cuts") + (uuid "65b18074-0f15-4532-885b-0bd23468442c") + ) + (gr_line + (start 143.24 86) + (end 148 86) + (stroke + (width 0.05) + (type solid) + ) + (layer "Edge.Cuts") + (uuid "71c50122-259e-47fe-a520-408994484ccb") + ) + (gr_arc + (start 143.24 97) + (mid 142.532893 96.707107) + (end 142.24 96) + (stroke + (width 0.05) + (type default) + ) + (layer "Edge.Cuts") + (uuid "d3a456ee-4918-49ca-8d0b-3b9eca1f9956") + ) + (gr_line + (start 143.24 97) + (end 148 97) + (stroke + (width 0.05) + (type default) + ) + (layer "Edge.Cuts") + (uuid "6a8ba2aa-ec97-4f7f-bc41-7f87b204d02d") + ) + (gr_arc + (start 148 96.999998) + (mid 155 95.810378) + (end 162 96.999998) + (stroke + (width 0.05) + (type default) + ) + (layer "Edge.Cuts") + (uuid "142108d4-96d9-42c2-92eb-68baa2cd712d") + ) + (gr_arc + (start 162 86) + (mid 155 87.18962) + (end 148 86) + (stroke + (width 0.05) + (type default) + ) + (layer "Edge.Cuts") + (uuid "e59519f4-75b9-452f-8a4a-e5bd10101487") + ) + (gr_line + (start 162 97) + (end 171.25 97) + (stroke + (width 0.05) + (type default) + ) + (layer "Edge.Cuts") + (uuid "5d90bb13-ca5e-441d-83cb-a44eec668702") + ) + (gr_line + (start 171.25 86) + (end 162 86) + (stroke + (width 0.05) + (type default) + ) + (layer "Edge.Cuts") + (uuid "6344c097-3d35-4c5f-88a1-01211211ddb1") + ) + (gr_arc + (start 171.25 86) + (mid 171.957107 86.292893) + (end 172.25 87) + (stroke + (width 0.05) + (type default) + ) + (layer "Edge.Cuts") + (uuid "fb747831-0f27-44e1-a9b0-519b3669fc42") + ) + (gr_arc + (start 172.25 96) + (mid 171.957107 96.707107) + (end 171.25 97) + (stroke + (width 0.05) + (type default) + ) + (layer "Edge.Cuts") + (uuid "3079629b-c567-43b7-a7d0-f46bf6483478") + ) + (gr_line + (start 172.25 96) + (end 172.25 87) + (stroke + (width 0.05) + (type default) + ) + (layer "Edge.Cuts") + (uuid "8f41ce11-c7e6-4ddd-9563-29e472e27f8b") + ) + (gr_text "PWM" + (at 147.32 88.9 0) + (layer "F.SilkS") + (uuid "17a54b51-4d15-4896-8c66-341fe43266d9") + (effects + (font + (size 0.8 0.8) + (thickness 0.15) + ) + (justify left) + ) + ) + (gr_text "+5V" + (at 147.32 91.44 0) + (layer "F.SilkS") + (uuid "039fa39e-7fae-4e33-8238-d9328a682dc3") + (effects + (font + (size 0.8 0.8) + (thickness 0.15) + ) + (justify left) + ) + ) + (gr_text "GND" + (at 147.32 93.98 0) + (layer "F.SilkS") + (uuid "74c62b4a-de4d-492c-81c7-90097ea28713") + (effects + (font + (size 0.8 0.8) + (thickness 0.15) + ) + (justify left) + ) + ) + (gr_text "SIGNAL" + (at 168.402 88.9 0) + (layer "F.SilkS") + (uuid "52abf8db-7b72-46fe-96e4-67e38a3648d1") + (effects + (font + (size 0.8 0.8) + (thickness 0.15) + ) + (justify right) + ) + ) + (gr_text "POWER" + (at 168.402 93.98 0) + (layer "F.SilkS") + (uuid "8e7d6ca6-acaf-43a8-a4d1-58c1bf93e481") + (effects + (font + (size 0.8 0.8) + (thickness 0.15) + ) + (justify right) + ) + ) + (gr_text "SERVO\nTESTER" + (at 161.798 91.44 270) + (layer "F.SilkS") + (uuid "a999aebf-8c5d-4073-b0bc-256ea7d8ef68") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + (justify top) + ) + ) + (gr_text "tinyengineer.dev" + (at 159.2046 93.804948 0) + (layer "B.SilkS") + (uuid "5221eec1-00e6-425c-99a1-56e829a5c6f1") + (effects + (font + (size 0.8 0.8) + (thickness 0.1) + ) + (justify left bottom mirror) + ) + ) + (segment + (start 145.5 88.9) + (end 151.575 88.9) + (width 0.25) + (layer "F.Cu") + (net "+3.3V") + (uuid "9e4a3f8a-32ed-46d5-9a67-e7729c8908a0") + ) + (segment + (start 151.575 93.975) + (end 149.04 91.44) + (width 0.5) + (layer "F.Cu") + (net "+5V") + (uuid "5c1f235d-24c0-414c-b57f-285463933c78") + ) + (segment + (start 151.575 93.98) + (end 151.575 93.975) + (width 0.5) + (layer "F.Cu") + (net "+5V") + (uuid "82730247-8867-4c54-a5d4-f6a12cd9900b") + ) + (segment + (start 149.04 91.44) + (end 145.5 91.44) + (width 0.5) + (layer "F.Cu") + (net "+5V") + (uuid "c8e989f8-b53c-4840-a8cd-d7180ec03efa") + ) + (segment + (start 153.225 88.9) + (end 169.3925 88.9) + (width 0.25) + (layer "F.Cu") + (net "Net-(D1-A)") + (uuid "83b5d58f-124f-4227-82d7-ef0413aac261") + ) + (segment + (start 169.3725 93.98) + (end 169.3925 94) + (width 0.5) + (layer "F.Cu") + (net "Net-(D2-A)") + (uuid "9da4786b-7d20-4e87-ae2d-4e08f58664ba") + ) + (segment + (start 153.225 93.98) + (end 169.3725 93.98) + (width 0.5) + (layer "F.Cu") + (net "Net-(D2-A)") + (uuid "d70c6d1e-815d-4a0f-a81f-2004c73cb62a") + ) + (zone + (net "GND") + (layers "F.Cu" "B.Cu") + (uuid "0b7ed3ca-b66d-4f95-8b78-84366536491a") + (name "GND") + (hatch edge 0.5) + (connect_pads + (clearance 0) + ) + (min_thickness 0.25) + (fill yes + (thermal_gap 0.5) + (thermal_bridge_width 0.5) + (island_removal_mode 0) + ) + (polygon + (pts + (xy 141 85) (xy 173 85) (xy 173 98) (xy 141 98) + ) + ) + (filled_polygon + (layer "F.Cu") + (pts + (xy 147.934841 86.507459) (xy 148.240162 86.614307) (xy 149.061759 86.865817) (xy 149.89267 87.084597) + (xy 150.533057 87.226354) (xy 150.731574 87.270298) (xy 150.731578 87.270298) (xy 150.731593 87.270302) + (xy 151.577212 87.422641) (xy 151.577231 87.422643) (xy 151.577233 87.422644) (xy 152.428178 87.541373) + (xy 152.4282 87.541376) (xy 153.283222 87.62632) (xy 154.140937 87.67734) (xy 155 87.694355) (xy 155.859063 87.67734) + (xy 156.716778 87.62632) (xy 157.5718 87.541376) (xy 158.422788 87.422641) (xy 159.268407 87.270302) + (xy 160.10733 87.084597) (xy 160.938241 86.865817) (xy 161.759838 86.614307) (xy 162.065158 86.507459) + (xy 162.106116 86.5005) (xy 171.184108 86.5005) (xy 171.24304 86.5005) (xy 171.256922 86.50128) + (xy 171.273878 86.50319) (xy 171.347266 86.511458) (xy 171.358636 86.515437) (xy 171.367102 86.515351) + (xy 171.374324 86.517633) (xy 171.453539 86.545351) (xy 171.478549 86.557395) (xy 171.549606 86.602043) + (xy 171.571313 86.619355) (xy 171.630644 86.678686) (xy 171.647957 86.700395) (xy 171.692602 86.771447) + (xy 171.704649 86.796464) (xy 171.732362 86.875662) (xy 171.738541 86.902734) (xy 171.74872 86.993076) + (xy 171.7495 87.006959) (xy 171.7495 87.924513) (xy 171.729815 87.991552) (xy 171.677011 88.037307) + (xy 171.607853 88.047251) (xy 171.560404 88.030052) (xy 171.492081 87.98791) (xy 171.492072 87.987906) + (xy 171.332583 87.935057) (xy 171.23415 87.925) (xy 171.2175 87.925) (xy 171.2175 89.874999) (xy 171.234136 89.874999) + (xy 171.234152 89.874998) (xy 171.332583 89.864943) (xy 171.492072 89.812093) (xy 171.492083 89.812088) + (xy 171.560403 89.769948) (xy 171.627795 89.751507) (xy 171.694459 89.772429) (xy 171.739228 89.826071) + (xy 171.7495 89.875486) (xy 171.7495 93.024513) (xy 171.729815 93.091552) (xy 171.677011 93.137307) + (xy 171.607853 93.147251) (xy 171.560404 93.130052) (xy 171.492081 93.08791) (xy 171.492072 93.087906) + (xy 171.332583 93.035057) (xy 171.23415 93.025) (xy 171.2175 93.025) (xy 171.2175 94.974999) (xy 171.234136 94.974999) + (xy 171.234152 94.974998) (xy 171.332583 94.964943) (xy 171.492072 94.912093) (xy 171.492083 94.912088) + (xy 171.560403 94.869948) (xy 171.627795 94.851507) (xy 171.694459 94.872429) (xy 171.739228 94.926071) + (xy 171.7495 94.975486) (xy 171.7495 95.99304) (xy 171.74872 96.006923) (xy 171.738541 96.097265) + (xy 171.734561 96.108637) (xy 171.734648 96.117102) (xy 171.732362 96.124337) (xy 171.704649 96.203535) + (xy 171.692602 96.228552) (xy 171.647957 96.299604) (xy 171.630644 96.321313) (xy 171.571313 96.380644) + (xy 171.549604 96.397957) (xy 171.478552 96.442602) (xy 171.453535 96.454649) (xy 171.374337 96.482362) + (xy 171.347265 96.488541) (xy 171.256923 96.49872) (xy 171.24304 96.4995) (xy 162.106122 96.4995) + (xy 162.065163 96.49254) (xy 161.978712 96.462286) (xy 161.759845 96.385693) (xy 160.938231 96.134178) + (xy 160.107326 95.9154) (xy 159.268425 95.729699) (xy 158.422766 95.577353) (xy 157.571821 95.458624) + (xy 157.47664 95.449168) (xy 156.716778 95.373678) (xy 156.703486 95.372887) (xy 155.859075 95.322658) + (xy 155 95.305643) (xy 154.140924 95.322658) (xy 153.283238 95.373677) (xy 153.283236 95.373677) + (xy 153.283222 95.373678) (xy 152.632941 95.438281) (xy 152.428178 95.458624) (xy 151.577233 95.577353) + (xy 150.731574 95.729699) (xy 149.892673 95.9154) (xy 149.061768 96.134178) (xy 148.240154 96.385693) + (xy 147.990328 96.47312) (xy 147.934836 96.49254) (xy 147.893878 96.4995) (xy 143.24696 96.4995) + (xy 143.233077 96.49872) (xy 143.142734 96.488541) (xy 143.131361 96.484561) (xy 143.122897 96.484648) + (xy 143.115662 96.482362) (xy 143.036464 96.454649) (xy 143.011447 96.442602) (xy 142.940395 96.397957) + (xy 142.918686 96.380644) (xy 142.859355 96.321313) (xy 142.842042 96.299604) (xy 142.797397 96.228552) + (xy 142.785351 96.203539) (xy 142.757636 96.124333) (xy 142.751458 96.097268) (xy 142.74128 96.006922) + (xy 142.7405 95.99304) (xy 142.7405 93.873753) (xy 144.15 93.873753) (xy 144.15 94.086246) (xy 144.183242 94.296127) + (xy 144.183242 94.29613) (xy 144.248904 94.498217) (xy 144.345375 94.68755) (xy 144.384728 94.741716) + (xy 145.017037 94.109408) (xy 145.034075 94.172993) (xy 145.099901 94.287007) (xy 145.192993 94.380099) + (xy 145.307007 94.445925) (xy 145.37059 94.462962) (xy 144.738282 95.095269) (xy 144.738282 95.09527) + (xy 144.792449 95.134624) (xy 144.981782 95.231095) (xy 145.18387 95.296757) (xy 145.393754 95.33) + (xy 145.606246 95.33) (xy 145.816127 95.296757) (xy 145.81613 95.296757) (xy 146.018217 95.231095) + (xy 146.207554 95.134622) (xy 146.261716 95.09527) (xy 146.261717 95.09527) (xy 145.629408 94.462962) + (xy 145.692993 94.445925) (xy 145.807007 94.380099) (xy 145.900099 94.287007) (xy 145.965925 94.172993) + (xy 145.982962 94.109408) (xy 146.61527 94.741717) (xy 146.61527 94.741716) (xy 146.654622 94.687554) + (xy 146.751095 94.498217) (xy 146.816757 94.29613) (xy 146.816757 94.296127) (xy 146.85 94.086246) + (xy 146.85 93.873753) (xy 146.816757 93.663872) (xy 146.816757 93.663869) (xy 146.751095 93.461782) + (xy 146.654624 93.272449) (xy 146.61527 93.218282) (xy 146.615269 93.218282) (xy 145.982962 93.85059) + (xy 145.965925 93.787007) (xy 145.900099 93.672993) (xy 145.807007 93.579901) (xy 145.692993 93.514075) + (xy 145.629409 93.497037) (xy 146.261716 92.864728) (xy 146.20755 92.825375) (xy 146.018216 92.728904) + (xy 145.842026 92.671656) (xy 145.78435 92.632218) (xy 145.757152 92.56786) (xy 145.769067 92.499013) + (xy 145.816311 92.447538) (xy 145.832882 92.439168) (xy 145.997598 92.370941) (xy 146.169655 92.255977) + (xy 146.315977 92.109655) (xy 146.425589 91.945609) (xy 146.479201 91.900804) (xy 146.528691 91.8905) + (xy 148.802035 91.8905) (xy 148.869074 91.910185) (xy 148.889716 91.926819) (xy 150.938181 93.975284) + (xy 150.971666 94.036607) (xy 150.9745 94.062964) (xy 150.9745 94.286517) (xy 150.976023 94.29613) + (xy 150.989354 94.380304) (xy 151.04695 94.493342) (xy 151.046952 94.493344) (xy 151.046954 94.493347) + (xy 151.136652 94.583045) (xy 151.136654 94.583046) (xy 151.136658 94.58305) (xy 151.249694 94.640645) + (xy 151.249698 94.640647) (xy 151.343475 94.655499) (xy 151.343481 94.6555) (xy 151.806518 94.655499) + (xy 151.900304 94.640646) (xy 152.013342 94.58305) (xy 152.10305 94.493342) (xy 152.160646 94.380304) + (xy 152.160646 94.380302) (xy 152.160647 94.380301) (xy 152.175499 94.286524) (xy 152.1755 94.286519) + (xy 152.175499 93.673482) (xy 152.175498 93.673475) (xy 152.6245 93.673475) (xy 152.6245 94.286517) + (xy 152.626023 94.29613) (xy 152.639354 94.380304) (xy 152.69695 94.493342) (xy 152.696952 94.493344) + (xy 152.696954 94.493347) (xy 152.786652 94.583045) (xy 152.786654 94.583046) (xy 152.786658 94.58305) + (xy 152.899694 94.640645) (xy 152.899698 94.640647) (xy 152.993475 94.655499) (xy 152.993481 94.6555) + (xy 153.456518 94.655499) (xy 153.550304 94.640646) (xy 153.663342 94.58305) (xy 153.693769 94.552623) + (xy 153.75305 94.493343) (xy 153.758786 94.485448) (xy 153.761059 94.4871) (xy 153.798546 94.447409) + (xy 153.861057 94.4305) (xy 168.716012 94.4305) (xy 168.783051 94.450185) (xy 168.826496 94.498204) + (xy 168.830342 94.505752) (xy 168.924243 94.599653) (xy 168.924245 94.599654) (xy 168.924249 94.599658) + (xy 169.04258 94.659951) (xy 169.042581 94.659951) (xy 169.042583 94.659952) (xy 169.042582 94.659952) + (xy 169.140749 94.6755) (xy 169.140754 94.6755) (xy 169.644251 94.6755) (xy 169.742417 94.659952) + (xy 169.742418 94.659951) (xy 169.74242 94.659951) (xy 169.860751 94.599658) (xy 169.907786 94.552623) + (xy 169.969109 94.519137) (xy 170.0388 94.524121) (xy 170.094734 94.565992) (xy 170.101007 94.575206) + (xy 170.181114 94.70508) (xy 170.299919 94.823885) (xy 170.442922 94.912091) (xy 170.442927 94.912093) + (xy 170.602416 94.964942) (xy 170.700856 94.974999) (xy 170.7175 94.974998) (xy 170.7175 93.024999) + (xy 170.700856 93.025) (xy 170.602415 93.035057) (xy 170.442927 93.087906) (xy 170.442922 93.087908) + (xy 170.299919 93.176114) (xy 170.181113 93.29492) (xy 170.18111 93.294924) (xy 170.101006 93.424793) + (xy 170.049058 93.471518) (xy 169.980096 93.482739) (xy 169.916014 93.454896) (xy 169.907787 93.447377) + (xy 169.860756 93.400346) (xy 169.860753 93.400344) (xy 169.860751 93.400342) (xy 169.74242 93.340049) + (xy 169.742419 93.340048) (xy 169.742416 93.340047) (xy 169.742417 93.340047) (xy 169.644251 93.3245) + (xy 169.644246 93.3245) (xy 169.140754 93.3245) (xy 169.140749 93.3245) (xy 169.042582 93.340047) + (xy 168.970158 93.37695) (xy 168.924249 93.400342) (xy 168.924248 93.400343) (xy 168.924247 93.400343) + (xy 168.899798 93.424793) (xy 168.831408 93.493182) (xy 168.770088 93.526666) (xy 168.743729 93.5295) + (xy 153.861057 93.5295) (xy 153.794018 93.509815) (xy 153.760793 93.473093) (xy 153.758786 93.474552) + (xy 153.75305 93.466657) (xy 153.663347 93.376954) (xy 153.663344 93.376952) (xy 153.663342 93.37695) + (xy 153.560404 93.3245) (xy 153.550301 93.319352) (xy 153.456524 93.3045) (xy 152.993482 93.3045) + (xy 152.912519 93.317323) (xy 152.899696 93.319354) (xy 152.786658 93.37695) (xy 152.786657 93.376951) + (xy 152.786652 93.376954) (xy 152.696954 93.466652) (xy 152.696951 93.466657) (xy 152.69695 93.466658) + (xy 152.683436 93.493181) (xy 152.639352 93.579698) (xy 152.6245 93.673475) (xy 152.175498 93.673475) + (xy 152.160646 93.579696) (xy 152.10305 93.466658) (xy 152.103046 93.466654) (xy 152.103045 93.466652) + (xy 152.013347 93.376954) (xy 152.013344 93.376952) (xy 152.013342 93.37695) (xy 151.910404 93.3245) + (xy 151.900301 93.319352) (xy 151.806524 93.3045) (xy 151.806519 93.3045) (xy 151.592965 93.3045) + (xy 151.525926 93.284815) (xy 151.505284 93.268181) (xy 149.316616 91.079513) (xy 149.316614 91.079511) + (xy 149.26525 91.049856) (xy 149.213888 91.020201) (xy 149.20178 91.016957) (xy 149.189673 91.013713) + (xy 149.18967 91.013712) (xy 149.151478 91.003478) (xy 149.099309 90.9895) (xy 149.099308 90.9895) + (xy 146.528691 90.9895) (xy 146.461652 90.969815) (xy 146.425589 90.934391) (xy 146.315977 90.770344) + (xy 146.169657 90.624024) (xy 146.083626 90.566541) (xy 145.997598 90.509059) (xy 145.80642 90.42987) + (xy 145.806412 90.429868) (xy 145.603469 90.3895) (xy 145.603465 90.3895) (xy 145.396535 90.3895) + (xy 145.39653 90.3895) (xy 145.193587 90.429868) (xy 145.193579 90.42987) (xy 145.002403 90.509058) + (xy 144.830342 90.624024) (xy 144.684024 90.770342) (xy 144.569058 90.942403) (xy 144.48987 91.133579) + (xy 144.489868 91.133587) (xy 144.4495 91.33653) (xy 144.4495 91.543469) (xy 144.489868 91.746412) + (xy 144.48987 91.74642) (xy 144.569058 91.937596) (xy 144.684024 92.109657) (xy 144.830342 92.255975) + (xy 145.002405 92.370943) (xy 145.105043 92.413456) (xy 145.167108 92.439164) (xy 145.221511 92.483005) + (xy 145.243576 92.549299) (xy 145.226297 92.616998) (xy 145.17516 92.664609) (xy 145.157974 92.671656) + (xy 144.981781 92.728905) (xy 144.792439 92.82538) (xy 144.738282 92.864727) (xy 144.738282 92.864728) + (xy 145.370591 93.497037) (xy 145.307007 93.514075) (xy 145.192993 93.579901) (xy 145.099901 93.672993) + (xy 145.034075 93.787007) (xy 145.017037 93.850591) (xy 144.384728 93.218282) (xy 144.384727 93.218282) + (xy 144.34538 93.272439) (xy 144.248904 93.461782) (xy 144.183242 93.663869) (xy 144.183242 93.663872) + (xy 144.15 93.873753) (xy 142.7405 93.873753) (xy 142.7405 88.030247) (xy 144.4495 88.030247) (xy 144.4495 89.769752) + (xy 144.461131 89.828229) (xy 144.461132 89.82823) (xy 144.505447 89.894552) (xy 144.571769 89.938867) + (xy 144.57177 89.938868) (xy 144.630247 89.950499) (xy 144.63025 89.9505) (xy 144.630252 89.9505) + (xy 146.36975 89.9505) (xy 146.369751 89.950499) (xy 146.384568 89.947552) (xy 146.428229 89.938868) + (xy 146.428229 89.938867) (xy 146.428231 89.938867) (xy 146.494552 89.894552) (xy 146.538867 89.828231) + (xy 146.538867 89.828229) (xy 146.538868 89.828229) (xy 146.550499 89.769752) (xy 146.5505 89.76975) + (xy 146.5505 89.3495) (xy 146.570185 89.282461) (xy 146.622989 89.236706) (xy 146.6745 89.2255) + (xy 150.875253 89.2255) (xy 150.942292 89.245185) (xy 150.985738 89.293205) (xy 150.989353 89.3003) + (xy 150.989354 89.300304) (xy 151.043085 89.405756) (xy 151.046951 89.413343) (xy 151.046954 89.413347) + (xy 151.136652 89.503045) (xy 151.136654 89.503046) (xy 151.136658 89.50305) (xy 151.248332 89.559951) + (xy 151.249698 89.560647) (xy 151.343475 89.575499) (xy 151.343481 89.5755) (xy 151.806518 89.575499) + (xy 151.900304 89.560646) (xy 152.013342 89.50305) (xy 152.10305 89.413342) (xy 152.160646 89.300304) + (xy 152.160646 89.300302) (xy 152.160647 89.300301) (xy 152.175499 89.206524) (xy 152.1755 89.206519) + (xy 152.175499 88.593482) (xy 152.175498 88.593475) (xy 152.6245 88.593475) (xy 152.6245 89.206517) + (xy 152.635292 89.274657) (xy 152.639354 89.300304) (xy 152.69695 89.413342) (xy 152.696952 89.413344) + (xy 152.696954 89.413347) (xy 152.786652 89.503045) (xy 152.786654 89.503046) (xy 152.786658 89.50305) + (xy 152.898332 89.559951) (xy 152.899698 89.560647) (xy 152.993475 89.575499) (xy 152.993481 89.5755) + (xy 153.456518 89.575499) (xy 153.550304 89.560646) (xy 153.663342 89.50305) (xy 153.75305 89.413342) + (xy 153.810646 89.300304) (xy 153.810646 89.300303) (xy 153.814263 89.293205) (xy 153.862238 89.242409) + (xy 153.924748 89.2255) (xy 168.662512 89.2255) (xy 168.729551 89.245185) (xy 168.772996 89.293204) + (xy 168.830342 89.405751) (xy 168.830344 89.405753) (xy 168.830346 89.405756) (xy 168.924243 89.499653) + (xy 168.924245 89.499654) (xy 168.924249 89.499658) (xy 169.04258 89.559951) (xy 169.042581 89.559951) + (xy 169.042583 89.559952) (xy 169.042582 89.559952) (xy 169.140749 89.5755) (xy 169.140754 89.5755) + (xy 169.644251 89.5755) (xy 169.742417 89.559952) (xy 169.742418 89.559951) (xy 169.74242 89.559951) + (xy 169.860751 89.499658) (xy 169.907786 89.452623) (xy 169.969109 89.419137) (xy 170.0388 89.424121) + (xy 170.094734 89.465992) (xy 170.101007 89.475206) (xy 170.181114 89.60508) (xy 170.299919 89.723885) + (xy 170.442922 89.812091) (xy 170.442927 89.812093) (xy 170.602416 89.864942) (xy 170.700856 89.874999) + (xy 170.7175 89.874998) (xy 170.7175 87.924999) (xy 170.700856 87.925) (xy 170.602415 87.935057) + (xy 170.442927 87.987906) (xy 170.442922 87.987908) (xy 170.299919 88.076114) (xy 170.181113 88.19492) + (xy 170.18111 88.194924) (xy 170.101006 88.324793) (xy 170.049058 88.371518) (xy 169.980096 88.382739) + (xy 169.916014 88.354896) (xy 169.907787 88.347377) (xy 169.860756 88.300346) (xy 169.860753 88.300344) + (xy 169.860751 88.300342) (xy 169.74242 88.240049) (xy 169.742419 88.240048) (xy 169.742416 88.240047) + (xy 169.742417 88.240047) (xy 169.644251 88.2245) (xy 169.644246 88.2245) (xy 169.140754 88.2245) + (xy 169.140749 88.2245) (xy 169.042582 88.240047) (xy 168.963692 88.280244) (xy 168.924249 88.300342) + (xy 168.924248 88.300343) (xy 168.924243 88.300346) (xy 168.830346 88.394243) (xy 168.830343 88.394248) + (xy 168.830342 88.394249) (xy 168.772997 88.506795) (xy 168.725022 88.557591) (xy 168.662512 88.5745) + (xy 153.924747 88.5745) (xy 153.857708 88.554815) (xy 153.814262 88.506795) (xy 153.810646 88.499698) + (xy 153.810646 88.499696) (xy 153.75305 88.386658) (xy 153.753048 88.386656) (xy 153.753045 88.386652) + (xy 153.663347 88.296954) (xy 153.663344 88.296952) (xy 153.663342 88.29695) (xy 153.586517 88.257805) + (xy 153.550301 88.239352) (xy 153.456524 88.2245) (xy 152.993482 88.2245) (xy 152.912519 88.237323) + (xy 152.899696 88.239354) (xy 152.786658 88.29695) (xy 152.786657 88.296951) (xy 152.786652 88.296954) + (xy 152.696954 88.386652) (xy 152.696952 88.386656) (xy 152.69695 88.386658) (xy 152.677751 88.424337) + (xy 152.639352 88.499698) (xy 152.6245 88.593475) (xy 152.175498 88.593475) (xy 152.160646 88.499696) + (xy 152.10305 88.386658) (xy 152.103046 88.386654) (xy 152.103045 88.386652) (xy 152.013347 88.296954) + (xy 152.013344 88.296952) (xy 152.013342 88.29695) (xy 151.936517 88.257805) (xy 151.900301 88.239352) + (xy 151.806524 88.2245) (xy 151.343482 88.2245) (xy 151.262519 88.237323) (xy 151.249696 88.239354) + (xy 151.136658 88.29695) (xy 151.136657 88.296951) (xy 151.136652 88.296954) (xy 151.046954 88.386652) + (xy 151.046952 88.386656) (xy 151.04695 88.386658) (xy 151.014421 88.4505) (xy 150.985737 88.506795) + (xy 150.937762 88.557591) (xy 150.875252 88.5745) (xy 146.6745 88.5745) (xy 146.607461 88.554815) + (xy 146.561706 88.502011) (xy 146.5505 88.4505) (xy 146.5505 88.030249) (xy 146.550499 88.030247) + (xy 146.538868 87.97177) (xy 146.538867 87.971769) (xy 146.494552 87.905447) (xy 146.42823 87.861132) + (xy 146.428229 87.861131) (xy 146.369752 87.8495) (xy 146.369748 87.8495) (xy 144.630252 87.8495) + (xy 144.630247 87.8495) (xy 144.57177 87.861131) (xy 144.571769 87.861132) (xy 144.505447 87.905447) + (xy 144.461132 87.971769) (xy 144.461131 87.97177) (xy 144.4495 88.030247) (xy 142.7405 88.030247) + (xy 142.7405 87.006959) (xy 142.74128 86.993077) (xy 142.74132 86.992717) (xy 142.751458 86.902732) + (xy 142.755437 86.891362) (xy 142.755351 86.882897) (xy 142.757629 86.875684) (xy 142.785353 86.796457) + (xy 142.797393 86.771453) (xy 142.842046 86.700389) (xy 142.859351 86.67869) (xy 142.91869 86.619351) + (xy 142.940389 86.602046) (xy 143.011453 86.557393) (xy 143.036457 86.545353) (xy 143.115668 86.517635) + (xy 143.142729 86.511459) (xy 143.218753 86.502893) (xy 143.233078 86.50128) (xy 143.24696 86.5005) + (xy 143.305892 86.5005) (xy 147.893884 86.5005) + ) + ) + (filled_polygon + (layer "B.Cu") + (pts + (xy 147.934841 86.507459) (xy 148.240162 86.614307) (xy 149.061759 86.865817) (xy 149.89267 87.084597) + (xy 150.533057 87.226354) (xy 150.731574 87.270298) (xy 150.731578 87.270298) (xy 150.731593 87.270302) + (xy 151.577212 87.422641) (xy 151.577231 87.422643) (xy 151.577233 87.422644) (xy 152.428178 87.541373) + (xy 152.4282 87.541376) (xy 153.283222 87.62632) (xy 154.140937 87.67734) (xy 155 87.694355) (xy 155.859063 87.67734) + (xy 156.716778 87.62632) (xy 157.5718 87.541376) (xy 158.422788 87.422641) (xy 159.268407 87.270302) + (xy 160.10733 87.084597) (xy 160.938241 86.865817) (xy 161.759838 86.614307) (xy 162.065158 86.507459) + (xy 162.106116 86.5005) (xy 171.184108 86.5005) (xy 171.24304 86.5005) (xy 171.256922 86.50128) + (xy 171.273878 86.50319) (xy 171.347266 86.511458) (xy 171.358636 86.515437) (xy 171.367102 86.515351) + (xy 171.374324 86.517633) (xy 171.453539 86.545351) (xy 171.478549 86.557395) (xy 171.549606 86.602043) + (xy 171.571313 86.619355) (xy 171.630644 86.678686) (xy 171.647957 86.700395) (xy 171.692602 86.771447) + (xy 171.704649 86.796464) (xy 171.732362 86.875662) (xy 171.738541 86.902734) (xy 171.74872 86.993076) + (xy 171.7495 87.006959) (xy 171.7495 95.99304) (xy 171.74872 96.006923) (xy 171.738541 96.097265) + (xy 171.734561 96.108637) (xy 171.734648 96.117102) (xy 171.732362 96.124337) (xy 171.704649 96.203535) + (xy 171.692602 96.228552) (xy 171.647957 96.299604) (xy 171.630644 96.321313) (xy 171.571313 96.380644) + (xy 171.549604 96.397957) (xy 171.478552 96.442602) (xy 171.453535 96.454649) (xy 171.374337 96.482362) + (xy 171.347265 96.488541) (xy 171.256923 96.49872) (xy 171.24304 96.4995) (xy 162.106122 96.4995) + (xy 162.065163 96.49254) (xy 161.978712 96.462286) (xy 161.759845 96.385693) (xy 160.938231 96.134178) + (xy 160.107326 95.9154) (xy 159.268425 95.729699) (xy 158.422766 95.577353) (xy 157.571821 95.458624) + (xy 157.47664 95.449168) (xy 156.716778 95.373678) (xy 156.703486 95.372887) (xy 155.859075 95.322658) + (xy 155 95.305643) (xy 154.140924 95.322658) (xy 153.283238 95.373677) (xy 153.283236 95.373677) + (xy 153.283222 95.373678) (xy 152.632941 95.438281) (xy 152.428178 95.458624) (xy 151.577233 95.577353) + (xy 150.731574 95.729699) (xy 149.892673 95.9154) (xy 149.061768 96.134178) (xy 148.240154 96.385693) + (xy 147.990328 96.47312) (xy 147.934836 96.49254) (xy 147.893878 96.4995) (xy 143.24696 96.4995) + (xy 143.233077 96.49872) (xy 143.142734 96.488541) (xy 143.131361 96.484561) (xy 143.122897 96.484648) + (xy 143.115662 96.482362) (xy 143.036464 96.454649) (xy 143.011447 96.442602) (xy 142.940395 96.397957) + (xy 142.918686 96.380644) (xy 142.859355 96.321313) (xy 142.842042 96.299604) (xy 142.797397 96.228552) + (xy 142.785351 96.203539) (xy 142.757636 96.124333) (xy 142.751458 96.097268) (xy 142.74128 96.006922) + (xy 142.7405 95.99304) (xy 142.7405 93.873753) (xy 144.15 93.873753) (xy 144.15 94.086246) (xy 144.183242 94.296127) + (xy 144.183242 94.29613) (xy 144.248904 94.498217) (xy 144.345375 94.68755) (xy 144.384728 94.741716) + (xy 145.017037 94.109408) (xy 145.034075 94.172993) (xy 145.099901 94.287007) (xy 145.192993 94.380099) + (xy 145.307007 94.445925) (xy 145.37059 94.462962) (xy 144.738282 95.095269) (xy 144.738282 95.09527) + (xy 144.792449 95.134624) (xy 144.981782 95.231095) (xy 145.18387 95.296757) (xy 145.393754 95.33) + (xy 145.606246 95.33) (xy 145.816127 95.296757) (xy 145.81613 95.296757) (xy 146.018217 95.231095) + (xy 146.207554 95.134622) (xy 146.261716 95.09527) (xy 146.261717 95.09527) (xy 145.629408 94.462962) + (xy 145.692993 94.445925) (xy 145.807007 94.380099) (xy 145.900099 94.287007) (xy 145.965925 94.172993) + (xy 145.982962 94.109408) (xy 146.61527 94.741717) (xy 146.61527 94.741716) (xy 146.654622 94.687554) + (xy 146.751095 94.498217) (xy 146.816757 94.29613) (xy 146.816757 94.296127) (xy 146.85 94.086246) + (xy 146.85 93.873753) (xy 146.816757 93.663872) (xy 146.816757 93.663869) (xy 146.751095 93.461782) + (xy 146.654624 93.272449) (xy 146.61527 93.218282) (xy 146.615269 93.218282) (xy 145.982962 93.85059) + (xy 145.965925 93.787007) (xy 145.900099 93.672993) (xy 145.807007 93.579901) (xy 145.692993 93.514075) + (xy 145.629409 93.497037) (xy 146.261716 92.864728) (xy 146.20755 92.825375) (xy 146.018216 92.728904) + (xy 145.842026 92.671656) (xy 145.78435 92.632218) (xy 145.757152 92.56786) (xy 145.769067 92.499013) + (xy 145.816311 92.447538) (xy 145.832882 92.439168) (xy 145.997598 92.370941) (xy 146.169655 92.255977) + (xy 146.315977 92.109655) (xy 146.430941 91.937598) (xy 146.51013 91.74642) (xy 146.5505 91.543465) + (xy 146.5505 91.336535) (xy 146.51013 91.13358) (xy 146.430941 90.942402) (xy 146.315977 90.770345) + (xy 146.315975 90.770342) (xy 146.169657 90.624024) (xy 146.083626 90.566541) (xy 145.997598 90.509059) + (xy 145.80642 90.42987) (xy 145.806412 90.429868) (xy 145.603469 90.3895) (xy 145.603465 90.3895) + (xy 145.396535 90.3895) (xy 145.39653 90.3895) (xy 145.193587 90.429868) (xy 145.193579 90.42987) + (xy 145.002403 90.509058) (xy 144.830342 90.624024) (xy 144.684024 90.770342) (xy 144.569058 90.942403) + (xy 144.48987 91.133579) (xy 144.489868 91.133587) (xy 144.4495 91.33653) (xy 144.4495 91.543469) + (xy 144.489868 91.746412) (xy 144.48987 91.74642) (xy 144.569058 91.937596) (xy 144.684024 92.109657) + (xy 144.830342 92.255975) (xy 145.002405 92.370943) (xy 145.105043 92.413456) (xy 145.167108 92.439164) + (xy 145.221511 92.483005) (xy 145.243576 92.549299) (xy 145.226297 92.616998) (xy 145.17516 92.664609) + (xy 145.157974 92.671656) (xy 144.981781 92.728905) (xy 144.792439 92.82538) (xy 144.738282 92.864727) + (xy 144.738282 92.864728) (xy 145.370591 93.497037) (xy 145.307007 93.514075) (xy 145.192993 93.579901) + (xy 145.099901 93.672993) (xy 145.034075 93.787007) (xy 145.017037 93.850591) (xy 144.384728 93.218282) + (xy 144.384727 93.218282) (xy 144.34538 93.272439) (xy 144.248904 93.461782) (xy 144.183242 93.663869) + (xy 144.183242 93.663872) (xy 144.15 93.873753) (xy 142.7405 93.873753) (xy 142.7405 88.030247) + (xy 144.4495 88.030247) (xy 144.4495 89.769752) (xy 144.461131 89.828229) (xy 144.461132 89.82823) + (xy 144.505447 89.894552) (xy 144.571769 89.938867) (xy 144.57177 89.938868) (xy 144.630247 89.950499) + (xy 144.63025 89.9505) (xy 144.630252 89.9505) (xy 146.36975 89.9505) (xy 146.369751 89.950499) + (xy 146.384568 89.947552) (xy 146.428229 89.938868) (xy 146.428229 89.938867) (xy 146.428231 89.938867) + (xy 146.494552 89.894552) (xy 146.538867 89.828231) (xy 146.538867 89.828229) (xy 146.538868 89.828229) + (xy 146.550499 89.769752) (xy 146.5505 89.76975) (xy 146.5505 88.030249) (xy 146.550499 88.030247) + (xy 146.538868 87.97177) (xy 146.538867 87.971769) (xy 146.494552 87.905447) (xy 146.42823 87.861132) + (xy 146.428229 87.861131) (xy 146.369752 87.8495) (xy 146.369748 87.8495) (xy 144.630252 87.8495) + (xy 144.630247 87.8495) (xy 144.57177 87.861131) (xy 144.571769 87.861132) (xy 144.505447 87.905447) + (xy 144.461132 87.971769) (xy 144.461131 87.97177) (xy 144.4495 88.030247) (xy 142.7405 88.030247) + (xy 142.7405 87.006959) (xy 142.74128 86.993077) (xy 142.74132 86.992717) (xy 142.751458 86.902732) + (xy 142.755437 86.891362) (xy 142.755351 86.882897) (xy 142.757629 86.875684) (xy 142.785353 86.796457) + (xy 142.797393 86.771453) (xy 142.842046 86.700389) (xy 142.859351 86.67869) (xy 142.91869 86.619351) + (xy 142.940389 86.602046) (xy 143.011453 86.557393) (xy 143.036457 86.545353) (xy 143.115668 86.517635) + (xy 143.142729 86.511459) (xy 143.218753 86.502893) (xy 143.233078 86.50128) (xy 143.24696 86.5005) + (xy 143.305892 86.5005) (xy 147.893884 86.5005) + ) + ) + ) + (group "" + (uuid "e6b525c3-6f00-4643-a81c-a9189b057b5b") + (members "65240620-eb5a-4904-8e41-79e56c5096e9" "7ef55f99-9318-41b8-b222-ed89d130645e" + "9e675410-ae4b-41f6-aa5a-531a91d26fc0" "b90f51b9-e14b-46b2-a785-2deaa37bd193" + "bb8804a2-f3fc-44d0-9a83-557651f09801" "dc605b43-cad9-4477-b2ec-4847166ac7db" + "e765da29-0974-4ae3-afd6-5e758c5afed9" + ) + ) + (embedded_fonts no) +) diff --git a/hardware/boards/main-control-board-servo-tester/main-control-board-servo-tester.kicad_pro b/hardware/boards/main-control-board-servo-tester/main-control-board-servo-tester.kicad_pro new file mode 100644 index 00000000..26543339 --- /dev/null +++ b/hardware/boards/main-control-board-servo-tester/main-control-board-servo-tester.kicad_pro @@ -0,0 +1,695 @@ +{ + "board": { + "3dviewports": [], + "design_settings": { + "defaults": { + "apply_defaults_to_fp_barcodes": false, + "apply_defaults_to_fp_dimensions": false, + "apply_defaults_to_fp_fields": false, + "apply_defaults_to_fp_shapes": false, + "apply_defaults_to_fp_text": false, + "board_outline_line_width": 0.05, + "copper_line_width": 0.2, + "copper_text_italic": false, + "copper_text_size_h": 1.5, + "copper_text_size_v": 1.5, + "copper_text_thickness": 0.3, + "copper_text_upright": false, + "courtyard_line_width": 0.05, + "dimension_precision": 4, + "dimension_units": 3, + "dimensions": { + "arrow_length": 1270000, + "extension_offset": 500000, + "keep_text_aligned": true, + "suppress_zeroes": true, + "text_position": 0, + "units_format": 0 + }, + "fab_line_width": 0.1, + "fab_text_italic": false, + "fab_text_size_h": 1.0, + "fab_text_size_v": 1.0, + "fab_text_thickness": 0.15, + "fab_text_upright": false, + "other_line_width": 0.1, + "other_text_italic": false, + "other_text_size_h": 1.0, + "other_text_size_v": 1.0, + "other_text_thickness": 0.15, + "other_text_upright": false, + "pads": { + "drill": 0.8, + "height": 1.27, + "width": 1.27 + }, + "silk_line_width": 0.1, + "silk_text_italic": false, + "silk_text_size_h": 1.0, + "silk_text_size_v": 1.0, + "silk_text_thickness": 0.1, + "silk_text_upright": false, + "zones": { + "border_display_style": 2, + "border_hatch_pitch": 0.5, + "corner_radius": 0.0, + "corner_smoothing": 0, + "fill_mode": 0, + "hatch_gap": 1.5, + "hatch_orientation": 0.0, + "hatch_smoothing_level": 0, + "hatch_smoothing_value": 0.1, + "hatch_thickness": 1.0, + "min_clearance": 0.0, + "min_island_area": 10.0, + "min_thickness": 0.25, + "pad_connection": 1, + "remove_islands": 0, + "thermal_relief_gap": 0.5, + "thermal_relief_spoke_width": 0.5 + } + }, + "diff_pair_dimensions": [ + { + "gap": 0.0, + "via_gap": 0.0, + "width": 0.0 + } + ], + "drc_exclusions": [], + "meta": { + "version": 2 + }, + "rule_severities": { + "annular_width": "error", + "clearance": "error", + "connection_width": "warning", + "copper_edge_clearance": "error", + "copper_sliver": "warning", + "courtyards_overlap": "error", + "creepage": "error", + "diff_pair_gap_out_of_range": "error", + "diff_pair_uncoupled_length_too_long": "error", + "drill_out_of_range": "error", + "duplicate_footprints": "warning", + "extra_footprint": "warning", + "footprint": "error", + "footprint_filters_mismatch": "ignore", + "footprint_symbol_field_mismatch": "warning", + "footprint_symbol_mismatch": "warning", + "footprint_type_mismatch": "ignore", + "hole_clearance": "error", + "hole_to_hole": "warning", + "holes_co_located": "warning", + "invalid_outline": "error", + "isolated_copper": "warning", + "item_on_disabled_layer": "error", + "items_not_allowed": "error", + "length_out_of_range": "error", + "lib_footprint_issues": "warning", + "lib_footprint_mismatch": "warning", + "malformed_courtyard": "error", + "microvia_drill_out_of_range": "error", + "mirrored_text_on_front_layer": "warning", + "missing_courtyard": "ignore", + "missing_footprint": "warning", + "missing_tuning_profile": "warning", + "net_conflict": "warning", + "nonmirrored_text_on_back_layer": "warning", + "npth_inside_courtyard": "error", + "padstack": "warning", + "pth_inside_courtyard": "error", + "shorting_items": "error", + "silk_edge_clearance": "warning", + "silk_over_copper": "warning", + "silk_overlap": "warning", + "skew_out_of_range": "error", + "solder_mask_bridge": "error", + "starved_thermal": "error", + "text_height": "warning", + "text_on_edge_cuts": "error", + "text_thickness": "warning", + "through_hole_pad_without_hole": "error", + "too_many_vias": "error", + "track_angle": "error", + "track_dangling": "warning", + "track_not_centered_on_via": "ignore", + "track_on_post_machined_layer": "error", + "track_segment_length": "error", + "track_width": "error", + "tracks_crossing": "error", + "tuning_profile_track_geometries": "ignore", + "unconnected_items": "error", + "unresolved_variable": "error", + "via_dangling": "warning", + "zones_intersect": "error" + }, + "rules": { + "max_error": 0.005, + "min_clearance": 0.0, + "min_connection": 0.0, + "min_copper_edge_clearance": 0.5, + "min_groove_width": 0.0, + "min_hole_clearance": 0.25, + "min_hole_to_hole": 0.25, + "min_microvia_diameter": 0.2, + "min_microvia_drill": 0.1, + "min_resolved_spokes": 2, + "min_silk_clearance": 0.0, + "min_text_height": 0.8, + "min_text_thickness": 0.08, + "min_through_hole_diameter": 0.3, + "min_track_width": 0.2, + "min_via_annular_width": 0.1, + "min_via_diameter": 0.5, + "solder_mask_to_copper_clearance": 0.0, + "use_height_for_length_calcs": true + }, + "teardrop_options": [ + { + "td_onpthpad": true, + "td_onroundshapesonly": false, + "td_onsmdpad": true, + "td_ontrackend": false, + "td_onvia": true + } + ], + "teardrop_parameters": [ + { + "td_allow_use_two_tracks": true, + "td_curve_segcount": 0, + "td_height_ratio": 1.0, + "td_length_ratio": 0.5, + "td_maxheight": 2.0, + "td_maxlen": 1.0, + "td_on_pad_in_zone": false, + "td_target_name": "td_round_shape", + "td_width_to_size_filter_ratio": 0.9 + }, + { + "td_allow_use_two_tracks": true, + "td_curve_segcount": 0, + "td_height_ratio": 1.0, + "td_length_ratio": 0.5, + "td_maxheight": 2.0, + "td_maxlen": 1.0, + "td_on_pad_in_zone": false, + "td_target_name": "td_rect_shape", + "td_width_to_size_filter_ratio": 0.9 + }, + { + "td_allow_use_two_tracks": true, + "td_curve_segcount": 0, + "td_height_ratio": 1.0, + "td_length_ratio": 0.5, + "td_maxheight": 2.0, + "td_maxlen": 1.0, + "td_on_pad_in_zone": false, + "td_target_name": "td_track_end", + "td_width_to_size_filter_ratio": 0.9 + } + ], + "track_widths": [ + 0.0, + 0.25, + 0.5 + ], + "tuning_pattern_settings": { + "diff_pair_defaults": { + "corner_radius_percentage": 80, + "corner_style": 1, + "max_amplitude": 1.0, + "min_amplitude": 0.2, + "single_sided": false, + "spacing": 1.0 + }, + "diff_pair_skew_defaults": { + "corner_radius_percentage": 80, + "corner_style": 1, + "max_amplitude": 1.0, + "min_amplitude": 0.2, + "single_sided": false, + "spacing": 0.6 + }, + "single_track_defaults": { + "corner_radius_percentage": 80, + "corner_style": 1, + "max_amplitude": 1.0, + "min_amplitude": 0.2, + "single_sided": false, + "spacing": 0.6 + } + }, + "via_dimensions": [ + { + "diameter": 0.0, + "drill": 0.0 + } + ], + "zones_allow_external_fillets": false + }, + "ipc2581": { + "bom_rev": "", + "dist": "", + "distpn": "", + "internal_id": "", + "mfg": "", + "mpn": "", + "sch_revision": "" + }, + "layer_pairs": [], + "layer_presets": [], + "viewports": [] + }, + "boards": [], + "component_class_settings": { + "assignments": [], + "meta": { + "version": 0 + }, + "sheet_component_classes": { + "enabled": false + } + }, + "cvpcb": { + "equivalence_files": [] + }, + "erc": { + "erc_exclusions": [], + "meta": { + "version": 0 + }, + "pin_map": [ + [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 2 + ], + [ + 0, + 2, + 0, + 1, + 0, + 0, + 1, + 0, + 2, + 2, + 2, + 2 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 2 + ], + [ + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 2, + 1, + 1, + 2 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 2 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2 + ], + [ + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 2 + ], + [ + 0, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 2 + ], + [ + 0, + 2, + 1, + 2, + 0, + 0, + 1, + 0, + 2, + 2, + 2, + 2 + ], + [ + 0, + 2, + 0, + 1, + 0, + 0, + 1, + 0, + 2, + 0, + 0, + 2 + ], + [ + 0, + 2, + 1, + 1, + 0, + 0, + 1, + 0, + 2, + 0, + 0, + 2 + ], + [ + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2 + ] + ], + "rule_severities": { + "bus_definition_conflict": "error", + "bus_entry_needed": "error", + "bus_to_bus_conflict": "error", + "bus_to_net_conflict": "error", + "different_unit_footprint": "error", + "different_unit_net": "error", + "duplicate_reference": "error", + "duplicate_sheet_names": "error", + "endpoint_off_grid": "warning", + "extra_units": "error", + "field_name_whitespace": "warning", + "footprint_filter": "ignore", + "footprint_link_issues": "warning", + "four_way_junction": "ignore", + "ground_pin_not_ground": "warning", + "hier_label_mismatch": "error", + "isolated_pin_label": "warning", + "label_dangling": "error", + "label_multiple_wires": "warning", + "lib_symbol_issues": "warning", + "lib_symbol_mismatch": "warning", + "missing_bidi_pin": "warning", + "missing_input_pin": "warning", + "missing_power_pin": "error", + "missing_unit": "warning", + "multiple_net_names": "warning", + "net_not_bus_member": "warning", + "no_connect_connected": "warning", + "no_connect_dangling": "warning", + "pin_not_connected": "error", + "pin_not_driven": "error", + "pin_to_pin": "warning", + "power_pin_not_driven": "error", + "same_local_global_label": "warning", + "similar_label_and_power": "warning", + "similar_labels": "warning", + "similar_power": "warning", + "simulation_model_issue": "ignore", + "single_global_label": "ignore", + "stacked_pin_name": "warning", + "unannotated": "error", + "unconnected_wire_endpoint": "warning", + "undefined_netclass": "error", + "unit_value_mismatch": "error", + "unresolved_variable": "error", + "wire_dangling": "error" + } + }, + "libraries": { + "pinned_footprint_libs": [], + "pinned_symbol_libs": [] + }, + "meta": { + "filename": "main-control-board-servo-tester.kicad_pro", + "version": 3 + }, + "net_settings": { + "classes": [ + { + "bus_width": 12, + "clearance": 0.2, + "diff_pair_gap": 0.25, + "diff_pair_via_gap": 0.25, + "diff_pair_width": 0.2, + "line_style": 0, + "microvia_diameter": 0.3, + "microvia_drill": 0.1, + "name": "Default", + "pcb_color": "rgba(0, 0, 0, 0.000)", + "priority": 2147483647, + "schematic_color": "rgba(0, 0, 0, 0.000)", + "track_width": 0.2, + "tuning_profile": "", + "via_diameter": 0.6, + "via_drill": 0.3, + "wire_width": 6 + } + ], + "meta": { + "version": 5 + }, + "net_colors": null, + "netclass_assignments": null, + "netclass_patterns": [] + }, + "pcbnew": { + "last_paths": { + "idf": "", + "netlist": "", + "plot": "", + "specctra_dsn": "", + "step": "", + "vrml": "" + }, + "page_layout_descr_file": "" + }, + "schematic": { + "annotate_start_num": 0, + "annotation": { + "method": 0, + "sort_order": 0 + }, + "bom_export_filename": "${PROJECTNAME}.csv", + "bom_fmt_presets": [], + "bom_fmt_settings": { + "field_delimiter": ",", + "keep_line_breaks": false, + "keep_tabs": false, + "name": "CSV", + "ref_delimiter": ",", + "ref_range_delimiter": "", + "string_delimiter": "\"" + }, + "bom_presets": [], + "bom_settings": { + "exclude_dnp": false, + "fields_ordered": [ + { + "group_by": false, + "label": "Reference", + "name": "Reference", + "show": true + }, + { + "group_by": false, + "label": "Qty", + "name": "${QUANTITY}", + "show": true + }, + { + "group_by": true, + "label": "Value", + "name": "Value", + "show": true + }, + { + "group_by": true, + "label": "DNP", + "name": "${DNP}", + "show": true + }, + { + "group_by": true, + "label": "Exclude from BOM", + "name": "${EXCLUDE_FROM_BOM}", + "show": true + }, + { + "group_by": true, + "label": "Exclude from Board", + "name": "${EXCLUDE_FROM_BOARD}", + "show": true + }, + { + "group_by": true, + "label": "Exclude from Simulation", + "name": "${EXCLUDE_FROM_SIM}", + "show": true + }, + { + "group_by": true, + "label": "Exclude from Position Files", + "name": "${EXCLUDE_FROM_POS_FILES}", + "show": true + }, + { + "group_by": true, + "label": "Footprint", + "name": "Footprint", + "show": true + }, + { + "group_by": false, + "label": "Datasheet", + "name": "Datasheet", + "show": true + } + ], + "filter_string": "", + "group_symbols": true, + "include_excluded_from_bom": true, + "name": "Default Editing", + "sort_asc": true, + "sort_field": "Reference" + }, + "bus_aliases": {}, + "connection_grid_size": 50.0, + "drawing": { + "dashed_lines_dash_length_ratio": 12.0, + "dashed_lines_gap_length_ratio": 3.0, + "default_line_thickness": 6.0, + "default_text_size": 50.0, + "field_names": [], + "hop_over_size_choice": 0, + "intersheets_ref_own_page": false, + "intersheets_ref_prefix": "", + "intersheets_ref_short": false, + "intersheets_ref_show": false, + "intersheets_ref_suffix": "", + "junction_size_choice": 3, + "label_size_ratio": 0.375, + "operating_point_overlay_i_precision": 3, + "operating_point_overlay_i_range": "~A", + "operating_point_overlay_v_precision": 3, + "operating_point_overlay_v_range": "~V", + "overbar_offset_ratio": 1.23, + "pin_symbol_size": 25.0, + "text_offset_ratio": 0.15 + }, + "legacy_lib_dir": "", + "legacy_lib_list": [], + "meta": { + "version": 1 + }, + "page_layout_descr_file": "", + "plot_directory": "", + "reuse_designators": true, + "subpart_first_id": 65, + "subpart_id_separator": 0, + "top_level_sheets": [ + { + "filename": "main-control-board-servo-tester.kicad_sch", + "name": "Root", + "uuid": "b2241bbe-4416-4857-ab8d-09689638c7e6" + } + ], + "used_designators": "#FLG1-3,D1-2,R1-2,#PWR1-6,J1", + "variants": [] + }, + "sheets": [ + [ + "b2241bbe-4416-4857-ab8d-09689638c7e6", + "Root" + ] + ], + "text_variables": {}, + "tuning_profiles": { + "meta": { + "version": 0 + }, + "tuning_profiles_impedance_geometric": [] + } +} diff --git a/hardware/boards/main-control-board-servo-tester/main-control-board-servo-tester.kicad_sch b/hardware/boards/main-control-board-servo-tester/main-control-board-servo-tester.kicad_sch new file mode 100644 index 00000000..e5fb4707 --- /dev/null +++ b/hardware/boards/main-control-board-servo-tester/main-control-board-servo-tester.kicad_sch @@ -0,0 +1,2544 @@ +(kicad_sch + (version 20260306) + (generator "eeschema") + (generator_version "10.0") + (uuid "b2241bbe-4416-4857-ab8d-09689638c7e6") + (paper "A4") + (lib_symbols + (symbol "Connector:Conn_01x03_Socket" + (pin_names + (offset 1.016) + (hide yes) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (duplicate_pin_numbers_are_jumpers no) + (property "Reference" "J" + (at 0 5.08 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "Conn_01x03_Socket" + (at 0 -5.08 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Generic connector, single row, 01x03, script generated" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_locked" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_keywords" "connector" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_fp_filters" "Connector*:*_1x??_*" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (symbol "Conn_01x03_Socket_1_1" + (polyline + (pts + (xy -1.27 2.54) (xy -0.508 2.54) + ) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy -1.27 0) (xy -0.508 0) + ) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy -1.27 -2.54) (xy -0.508 -2.54) + ) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (arc + (start 0 2.032) + (mid -0.5058 2.54) + (end 0 3.048) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (arc + (start 0 -0.508) + (mid -0.5058 0) + (end 0 0.508) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (arc + (start 0 -3.048) + (mid -0.5058 -2.54) + (end 0 -2.032) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (pin passive line + (at -5.08 2.54 0) + (length 3.81) + (name "Pin_1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at -5.08 0 0) + (length 3.81) + (name "Pin_2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at -5.08 -2.54 0) + (length 3.81) + (name "Pin_3" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "3" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) + (symbol "Device:LED" + (pin_numbers + (hide yes) + ) + (pin_names + (offset 1.016) + (hide yes) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (duplicate_pin_numbers_are_jumpers no) + (property "Reference" "D" + (at 0 2.54 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "LED" + (at 0 -2.54 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Light emitting diode" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Sim.Pins" "1=K 2=A" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_keywords" "LED diode" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_fp_filters" "LED* LED_SMD:* LED_THT:*" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (symbol "LED_0_1" + (polyline + (pts + (xy -3.048 -0.762) (xy -4.572 -2.286) (xy -3.81 -2.286) (xy -4.572 -2.286) (xy -4.572 -1.524) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy -1.778 -0.762) (xy -3.302 -2.286) (xy -2.54 -2.286) (xy -3.302 -2.286) (xy -3.302 -1.524) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy -1.27 0) (xy 1.27 0) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy -1.27 -1.27) (xy -1.27 1.27) + ) + (stroke + (width 0.254) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 1.27 -1.27) (xy 1.27 1.27) (xy -1.27 0) (xy 1.27 -1.27) + ) + (stroke + (width 0.254) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "LED_1_1" + (pin passive line + (at -3.81 0 0) + (length 2.54) + (name "K" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 3.81 0 180) + (length 2.54) + (name "A" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) + (symbol "Device:R" + (pin_numbers + (hide yes) + ) + (pin_names + (offset 0) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (duplicate_pin_numbers_are_jumpers no) + (property "Reference" "R" + (at 2.032 0 90) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "R" + (at 0 0 90) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at -1.778 0 90) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Resistor" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_keywords" "R res resistor" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_fp_filters" "R_*" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (symbol "R_0_1" + (rectangle + (start -1.016 -2.54) + (end 1.016 2.54) + (stroke + (width 0.254) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "R_1_1" + (pin passive line + (at 0 3.81 270) + (length 1.27) + (name "" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 0 -3.81 90) + (length 1.27) + (name "" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) + (symbol "power:+3.3V" + (power global) + (pin_numbers + (hide yes) + ) + (pin_names + (offset 0) + (hide yes) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (duplicate_pin_numbers_are_jumpers no) + (property "Reference" "#PWR" + (at 0 -3.81 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "+3.3V" + (at 0 3.556 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Power symbol creates a global label with name \"+3.3V\"" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_keywords" "global power" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (symbol "+3.3V_0_1" + (polyline + (pts + (xy -0.762 1.27) (xy 0 2.54) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 2.54) (xy 0.762 1.27) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 0) (xy 0 2.54) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "+3.3V_1_1" + (pin power_in line + (at 0 0 90) + (length 0) + (name "" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) + (symbol "power:+5V" + (power global) + (pin_numbers + (hide yes) + ) + (pin_names + (offset 0) + (hide yes) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (duplicate_pin_numbers_are_jumpers no) + (property "Reference" "#PWR" + (at 0 -3.81 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "+5V" + (at 0 3.556 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Power symbol creates a global label with name \"+5V\"" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_keywords" "global power" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (symbol "+5V_0_1" + (polyline + (pts + (xy -0.762 1.27) (xy 0 2.54) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 2.54) (xy 0.762 1.27) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 0) (xy 0 2.54) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "+5V_1_1" + (pin power_in line + (at 0 0 90) + (length 0) + (name "" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) + (symbol "power:GND" + (power global) + (pin_numbers + (hide yes) + ) + (pin_names + (offset 0) + (hide yes) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (duplicate_pin_numbers_are_jumpers no) + (property "Reference" "#PWR" + (at 0 -6.35 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "GND" + (at 0 -3.81 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_keywords" "global power" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (symbol "GND_0_1" + (polyline + (pts + (xy 0 0) (xy 0 -1.27) (xy 1.27 -1.27) (xy 0 -2.54) (xy -1.27 -1.27) (xy 0 -1.27) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "GND_1_1" + (pin power_in line + (at 0 0 270) + (length 0) + (name "" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) + (symbol "power:PWR_FLAG" + (power global) + (pin_numbers + (hide yes) + ) + (pin_names + (offset 0) + (hide yes) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (duplicate_pin_numbers_are_jumpers no) + (property "Reference" "#FLG" + (at 0 1.905 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "PWR_FLAG" + (at 0 3.81 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Special symbol for telling ERC where power comes from" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_keywords" "flag power" + (at 0 0 0) + (show_name no) + (do_not_autoplace no) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (symbol "PWR_FLAG_0_0" + (pin power_out line + (at 0 0 90) + (length 0) + (name "" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (symbol "PWR_FLAG_0_1" + (polyline + (pts + (xy 0 0) (xy 0 1.27) (xy -1.016 1.905) (xy 0 2.54) (xy 1.016 1.905) (xy 0 1.27) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (embedded_fonts no) + ) + ) + (junction + (at 148.59 106.68) + (diameter 0) + (color 0 0 0 0) + (uuid "58f84d9d-3b89-402e-a931-1579bcf5796b") + ) + (junction + (at 106.68 110.49) + (diameter 0) + (color 0 0 0 0) + (uuid "5a9e4b17-55ae-42f3-b3c5-60f88a94a351") + ) + (junction + (at 148.59 115.57) + (diameter 0) + (color 0 0 0 0) + (uuid "7f460358-8ac2-4fc5-bda5-9d3d26c9e5d2") + ) + (junction + (at 113.03 110.49) + (diameter 0) + (color 0 0 0 0) + (uuid "eeaf9b87-f67a-4190-9e47-c75b53b13dae") + ) + (wire + (pts + (xy 128.27 81.28) (xy 132.08 81.28) + ) + (stroke + (width 0) + (type default) + ) + (uuid "055969ff-444d-4638-bfea-992622f4e86e") + ) + (wire + (pts + (xy 142.24 106.68) (xy 148.59 106.68) + ) + (stroke + (width 0) + (type default) + ) + (uuid "0c707958-0d47-4bd4-b641-37d907a54ea2") + ) + (wire + (pts + (xy 113.03 115.57) (xy 113.03 110.49) + ) + (stroke + (width 0) + (type default) + ) + (uuid "2d1dcd45-0ba4-49ac-9263-1ca65c562557") + ) + (wire + (pts + (xy 113.03 110.49) (xy 113.03 106.68) + ) + (stroke + (width 0) + (type default) + ) + (uuid "440c08e0-2c56-4543-a469-41cb9d12db33") + ) + (wire + (pts + (xy 142.24 115.57) (xy 148.59 115.57) + ) + (stroke + (width 0) + (type default) + ) + (uuid "4ae587ee-faeb-40cc-b8a7-db8d1b6484a3") + ) + (wire + (pts + (xy 99.06 110.49) (xy 113.03 110.49) + ) + (stroke + (width 0) + (type default) + ) + (uuid "6d887668-63c2-4dec-bb75-5ac6637f075b") + ) + (wire + (pts + (xy 113.03 106.68) (xy 119.38 106.68) + ) + (stroke + (width 0) + (type default) + ) + (uuid "7439a9e4-4245-4322-b35f-d716fbd7a7d3") + ) + (wire + (pts + (xy 119.38 115.57) (xy 113.03 115.57) + ) + (stroke + (width 0) + (type default) + ) + (uuid "78c3d8a9-c9f1-462b-88d2-6feaf6646f17") + ) + (wire + (pts + (xy 128.27 86.36) (xy 132.08 86.36) + ) + (stroke + (width 0) + (type default) + ) + (uuid "84cd544c-746b-4253-a79d-86f7c55f56b9") + ) + (wire + (pts + (xy 148.59 115.57) (xy 152.4 115.57) + ) + (stroke + (width 0) + (type default) + ) + (uuid "ade0adaf-cbc3-4fc1-9750-dfaf44a47ac7") + ) + (wire + (pts + (xy 148.59 106.68) (xy 152.4 106.68) + ) + (stroke + (width 0) + (type default) + ) + (uuid "b7394f57-e266-4419-8173-7db97bd15da8") + ) + (wire + (pts + (xy 127 106.68) (xy 134.62 106.68) + ) + (stroke + (width 0) + (type default) + ) + (uuid "c26ce62a-4a71-4cc3-b125-028d0eb61180") + ) + (wire + (pts + (xy 132.08 83.82) (xy 128.27 83.82) + ) + (stroke + (width 0) + (type default) + ) + (uuid "f0a737d8-a1ea-4d9f-a976-074d36541e9d") + ) + (wire + (pts + (xy 127 115.57) (xy 134.62 115.57) + ) + (stroke + (width 0) + (type default) + ) + (uuid "f93bae35-f857-4d48-a350-77328a011acc") + ) + (symbol + (lib_id "power:+5V") + (at 128.27 83.82 90) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (uuid "024c3eae-2eb0-46d4-a6f2-32bb198f27b6") + (property "Reference" "#PWR02" + (at 132.08 83.82 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "+5V" + (at 122.428 83.82 90) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 128.27 83.82 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 128.27 83.82 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Power symbol creates a global label with name \"+5V\"" + (at 128.27 83.82 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (pin "1" + (uuid "f850fb09-4e57-4c90-bdce-0ddd39ca9f03") + ) + (instances + (project "main-control-board-servo-tester" + (path "/b2241bbe-4416-4857-ab8d-09689638c7e6" + (reference "#PWR02") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:PWR_FLAG") + (at 106.68 110.49 0) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (fields_autoplaced yes) + (uuid "230e02fa-846c-43c6-8dab-0185b1493594") + (property "Reference" "#FLG03" + (at 106.68 108.585 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "PWR_FLAG" + (at 106.68 105.41 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 106.68 110.49 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 106.68 110.49 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Special symbol for telling ERC where power comes from" + (at 106.68 110.49 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (pin "1" + (uuid "9ae1db06-994f-410e-b3f8-fcc66f913c19") + ) + (instances + (project "main-control-board-servo-tester" + (path "/b2241bbe-4416-4857-ab8d-09689638c7e6" + (reference "#FLG03") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:GND") + (at 99.06 110.49 270) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (fields_autoplaced yes) + (uuid "305965eb-4879-4cfa-85c1-a87586c72c4b") + (property "Reference" "#PWR03" + (at 92.71 110.49 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "GND" + (at 97.79 106.68 90) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 99.06 110.49 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 99.06 110.49 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 99.06 110.49 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (pin "1" + (uuid "ecfddfc8-49c3-4ea8-8094-cd2852010c3d") + ) + (instances + (project "main-control-board-servo-tester" + (path "/b2241bbe-4416-4857-ab8d-09689638c7e6" + (reference "#PWR03") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:PWR_FLAG") + (at 148.59 106.68 0) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (fields_autoplaced yes) + (uuid "430ae0e2-ec4e-44b4-84fc-1684ab220854") + (property "Reference" "#FLG01" + (at 148.59 104.775 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "PWR_FLAG" + (at 148.59 101.6 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 148.59 106.68 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 148.59 106.68 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Special symbol for telling ERC where power comes from" + (at 148.59 106.68 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (pin "1" + (uuid "888cab68-aae9-4627-a017-b20cce374c1c") + ) + (instances + (project "main-control-board-servo-tester" + (path "/b2241bbe-4416-4857-ab8d-09689638c7e6" + (reference "#FLG01") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:R") + (at 138.43 115.57 90) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (uuid "4cb34252-ce19-4d2c-aae1-722a53b733c4") + (property "Reference" "R2" + (at 138.43 113.284 90) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "1K" + (at 138.43 115.57 90) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "Resistor_SMD:R_0603_1608Metric" + (at 138.43 117.348 90) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 138.43 115.57 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Resistor" + (at 138.43 115.57 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "LCSC Part #" "0603WAF1001T5E" + (at 138.43 115.57 90) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "MPN" "C21190" + (at 138.43 115.57 90) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Manufacturer" "UNI-ROYAL(Uniroyal Elec)" + (at 138.43 115.57 90) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (pin "1" + (uuid "16bb348b-5e01-433c-9c85-80ddfddc4acf") + ) + (pin "2" + (uuid "338cd8c8-7b5c-4f98-b274-75b8fa0499fa") + ) + (instances + (project "main-control-board-servo-tester" + (path "/b2241bbe-4416-4857-ab8d-09689638c7e6" + (reference "R2") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:+3.3V") + (at 152.4 106.68 270) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (fields_autoplaced yes) + (uuid "4ee4922f-7a13-4536-aad2-3f1e81307f4d") + (property "Reference" "#PWR05" + (at 148.59 106.68 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "+3.3V" + (at 156.21 106.6799 90) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "" + (at 152.4 106.68 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 152.4 106.68 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Power symbol creates a global label with name \"+3.3V\"" + (at 152.4 106.68 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (pin "1" + (uuid "92f5a4fb-adb2-4be4-b324-c899f9ff5678") + ) + (instances + (project "main-control-board-servo-tester" + (path "/b2241bbe-4416-4857-ab8d-09689638c7e6" + (reference "#PWR05") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:LED") + (at 123.19 106.68 0) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (uuid "587fb663-5726-425c-ba5f-387c9bee1d97") + (property "Reference" "D1" + (at 122.936 100.33 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "YELLOW_LED" + (at 122.936 102.87 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "LED_SMD:LED_0603_1608Metric" + (at 123.19 106.68 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 123.19 106.68 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Light emitting diode" + (at 123.19 106.68 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Sim.Pins" "1=K 2=A" + (at 123.19 106.68 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "LCSC Part #" "C2287" + (at 123.19 106.68 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "MPN" "KT-0603Y" + (at 123.19 106.68 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Manufacturer" "Hubei KENTO Elec" + (at 123.19 106.68 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (pin "2" + (uuid "f0cadbd5-ff61-4a05-a51a-7c66b708475e") + ) + (pin "1" + (uuid "f915446d-33ea-4f93-b023-1a77b3111ee0") + ) + (instances + (project "main-control-board-servo-tester" + (path "/b2241bbe-4416-4857-ab8d-09689638c7e6" + (reference "D1") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:PWR_FLAG") + (at 148.59 115.57 0) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (fields_autoplaced yes) + (uuid "69d0c288-8fe1-4740-adca-81dacd2a568c") + (property "Reference" "#FLG02" + (at 148.59 113.665 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "PWR_FLAG" + (at 148.59 110.49 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 148.59 115.57 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 148.59 115.57 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Special symbol for telling ERC where power comes from" + (at 148.59 115.57 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (pin "1" + (uuid "3e678f9b-fe96-4e54-a2e3-b96b94f3b31b") + ) + (instances + (project "main-control-board-servo-tester" + (path "/b2241bbe-4416-4857-ab8d-09689638c7e6" + (reference "#FLG02") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:GND") + (at 128.27 86.36 270) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (uuid "a1447770-3a62-40d3-9d84-e5a1feb0a2db") + (property "Reference" "#PWR01" + (at 121.92 86.36 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "GND" + (at 122.174 86.106 90) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 128.27 86.36 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 128.27 86.36 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 128.27 86.36 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (pin "1" + (uuid "a0f32687-233c-45ac-b20b-43725e561e36") + ) + (instances + (project "main-control-board-servo-tester" + (path "/b2241bbe-4416-4857-ab8d-09689638c7e6" + (reference "#PWR01") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Connector:Conn_01x03_Socket") + (at 137.16 83.82 0) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (fields_autoplaced yes) + (uuid "b0391479-ebcd-4dd9-aa59-b2cdbb297f0c") + (property "Reference" "J1" + (at 138.43 82.5499 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "Conn_01x03_Socket" + (at 138.43 85.0899 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "Connector_PinSocket_2.54mm:PinSocket_1x03_P2.54mm_Horizontal" + (at 137.16 83.82 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 137.16 83.82 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Generic connector, single row, 01x03, script generated" + (at 137.16 83.82 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "LCSC Part #" "C2897385" + (at 137.16 83.82 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "MPN" "PM254-1-03-W-8.5" + (at 137.16 83.82 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Manufacturer" "HCTL" + (at 137.16 83.82 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (pin "3" + (uuid "1c770e96-3108-4b23-9c60-ed8d24ee76e6") + ) + (pin "2" + (uuid "de7ec07c-9415-4d1b-b58d-5e29bbd47fb9") + ) + (pin "1" + (uuid "e5249bdb-52dc-4281-afb5-145bb6553cc3") + ) + (instances + (project "main-control-board-servo-tester" + (path "/b2241bbe-4416-4857-ab8d-09689638c7e6" + (reference "J1") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:+5V") + (at 152.4 115.57 270) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (fields_autoplaced yes) + (uuid "b4f583f6-a1a1-4c2a-a95f-208e2271863a") + (property "Reference" "#PWR06" + (at 148.59 115.57 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "+5V" + (at 156.21 115.5699 90) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "" + (at 152.4 115.57 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 152.4 115.57 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Power symbol creates a global label with name \"+5V\"" + (at 152.4 115.57 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (pin "1" + (uuid "c7b95b8e-5d6c-4c0b-8eea-925a4aba9e2b") + ) + (instances + (project "main-control-board-servo-tester" + (path "/b2241bbe-4416-4857-ab8d-09689638c7e6" + (reference "#PWR06") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:LED") + (at 123.19 115.57 0) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (uuid "b6ddd916-9a02-430e-a918-b76f295b3ecd") + (property "Reference" "D2" + (at 123.444 118.872 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "RED_LED" + (at 123.444 121.412 0) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "LED_SMD:LED_0603_1608Metric" + (at 123.19 115.57 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 123.19 115.57 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Light emitting diode" + (at 123.19 115.57 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Sim.Pins" "1=K 2=A" + (at 123.19 115.57 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "LCSC Part #" "C2286" + (at 123.19 115.57 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "MPN" "KT-0603R" + (at 123.19 115.57 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Manufacturer" "Hubei KENTO Elec" + (at 123.19 115.57 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (pin "2" + (uuid "e0a96d34-ed59-4bb9-a7f2-e45527d596bb") + ) + (pin "1" + (uuid "78e98231-492c-405d-a4ad-0e15ef60f9d6") + ) + (instances + (project "main-control-board-servo-tester" + (path "/b2241bbe-4416-4857-ab8d-09689638c7e6" + (reference "D2") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:R") + (at 138.43 106.68 90) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (uuid "e32a9f3e-dc26-4827-9da6-4e2873523622") + (property "Reference" "R1" + (at 138.43 104.394 90) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "470" + (at 138.43 106.68 90) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "Resistor_SMD:R_0603_1608Metric" + (at 138.43 108.458 90) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 138.43 106.68 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Resistor" + (at 138.43 106.68 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "LCSC Part #" "C23179" + (at 138.43 106.68 90) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "MPN" "0603WAF4700T5E" + (at 138.43 106.68 90) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Manufacturer" "UNI-ROYAL(Uniroyal Elec)" + (at 138.43 106.68 90) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (pin "1" + (uuid "65c1f8fe-dfb7-4b60-9f88-c7dcca5394d7") + ) + (pin "2" + (uuid "7d6108d9-29e8-4b9a-a79f-8ab9299f24dd") + ) + (instances + (project "main-control-board-servo-tester" + (path "/b2241bbe-4416-4857-ab8d-09689638c7e6" + (reference "R1") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:+3.3V") + (at 128.27 81.28 90) + (unit 1) + (body_style 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (in_pos_files yes) + (dnp no) + (fields_autoplaced yes) + (uuid "e696bf91-5852-4b79-a1a4-6d0048c88773") + (property "Reference" "#PWR04" + (at 132.08 81.28 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "+3.3V" + (at 124.46 81.2799 90) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "" + (at 128.27 81.28 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 128.27 81.28 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Power symbol creates a global label with name \"+3.3V\"" + (at 128.27 81.28 0) + (hide yes) + (show_name no) + (do_not_autoplace no) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (pin "1" + (uuid "5f4b097d-ef05-4fdb-ab15-66926edfa416") + ) + (instances + (project "main-control-board-servo-tester" + (path "/b2241bbe-4416-4857-ab8d-09689638c7e6" + (reference "#PWR04") + (unit 1) + ) + ) + ) + ) + (sheet_instances + (path "/" + (page "1") + ) + ) + (embedded_fonts no) +) diff --git a/hardware/boards/main-control-board/main-control-board.kicad_pro b/hardware/boards/main-control-board/main-control-board.kicad_pro index 41d2b771..02593f58 100644 --- a/hardware/boards/main-control-board/main-control-board.kicad_pro +++ b/hardware/boards/main-control-board/main-control-board.kicad_pro @@ -853,7 +853,7 @@ "uuid": "82a78d11-58ff-455c-a94e-f7463ce9fc2e" } ], - "used_designators": "#FLG1-2,#FLG4,PCA2,D1-2,#PWR1-40,J1-8,NT1,MAX2,ESP2,JP1,R1-14,H1-3,USB1,C1-7,U1-4,F1,TP1-5", + "used_designators": "TP1-5,F1,U1-4,C1-7,USB1,H1-3,R1-14,JP1,ESP2,MAX2,NT1,J1-8,#PWR1-40,D1-2,PCA2,#FLG1-2,#FLG4", "variants": [] }, "sheets": [ diff --git a/openspec/changes/archive/2026-10-30-main-control-board-tester/design.md b/openspec/changes/archive/2026-10-30-main-control-board-tester/design.md new file mode 100644 index 00000000..74782ce8 --- /dev/null +++ b/openspec/changes/archive/2026-10-30-main-control-board-tester/design.md @@ -0,0 +1,101 @@ +# Design + +## Context + +See proposal.md for why a PCBA test session exists. The product image in `[env:esp32-c3-devkitm-1]` boots Wi-Fi, HTTP, LittleFS, and the COBS serial stack from `src/main.cpp`. `[env:expression-demo]` already shows the opt-in pattern: its own `main`, a `build_src_filter` that drops `src/`, and none of the product `extra_scripts`. Pin numbers live in `include/pins.h`. Servo columns on U2 are head=0, neck=1, arm-l=2, arm-r=3, body=4, in that physical order from the HEAD silkscreen toward BODY. + +Whoever runs the test uses one golden ESP32-C3-Zero, one 3-pin LED plug (red on the column 5 V pin, blue on PWM), one speaker on J1, and a PC whose OS is not known ahead of time. The script and the page say the test is compatible with main control board rev 3. + +## Goals / Non-Goals + +**Goals:** + +- One opt-in env whose image cannot be produced by the default `pio run`. +- A line protocol the host can follow without parsing COBS. +- Operator keys that sit on the same physical keys on a US and a Polish keyboard. +- Both hosts stay up across USB unplug when the cable moves to the next board. + +**Non-Goals:** + +- Judging the plug LEDs or the speaker from firmware. The operator is the sensor. The onboard LED is only an alive lamp. +- Measuring pulse width, eFuse trip current, or J2/J3/J4. +- A runtime switch inside the product image. +- Serving the tester from the robot `ui/` or from the test image. + +## Decisions + +### Separate env, own main + +`[env:pcba-test]` copies the `expression-demo` shape: same platform, board, and USB CDC flags, `build_src_filter` pointing at `examples/pcba-test/main.cpp`, no `embed_ui`, `copy_assets`, `upload_fs_after_upload`, or `park_servos_before_upload`. It links Adafruit's PWM library only. `include/pins.h` is on the include path for GPIO numbers and `PCA9685_ADDRESS`. + +Alternative: `-D PCBA_TEST` inside `src/main.cpp`. Rejected. The product boot would still be one edit away from running on a module that should boot the product, and the test image would still compile Wi-Fi and the UI scripts unless those scripts were also stripped. + +Alternative: a second repository. Rejected. Pin and channel numbers would drift from `pins.h`. + +### Plain lines, not the product serial protocol + +The image prints one token line at a time and reads one operator key. Banner `PCBA-TEST v1`. Steps `STEP tone`, then `STEP servos`, then `STEP oe`. Failures `FAIL `. Menus are the single letters below. Verdict `RESULT PASS` or `RESULT FAIL` plus the failed step names separated by spaces. Idle is `STOP` after `q`. A key that does not apply to the current state is ignored. Enter on the wire is a single line feed. + +Keys: Enter accepts, `n` rejects a step, `r` retries, `c` continues, `a` restarts the board, `q` stops after RESULT. After RESULT the next board is an unplug: USB power loss resets the module, and the host attaches when the port returns. `n` after RESULT is ignored. `y` is avoided because it moves on a Polish keyboard. + +Alternative: COBS frames shared with `src/serial/`. Rejected. The test host would need the product codec, and the product spec forbids raw text on the robot image. This image is not that image. + +### Step behavior + +PCA probe uses `Wire` at `0x40` before any step. Failure prints `FAIL pca` and `RESULT FAIL pca`, with no `r`/`c`/`a` menu. The board ends the same way as any other verdict: unplug for the next one, `q` to stop. + +Tone is a generated sine on the I2S pins (GP2, GP3, GP4), repeated until Enter or `n`. During that step `+` raises the level and `-` lowers it, across ten steps. The middle step is the bench default. Other steps ignore those keys. It does not call `playTone()` in `src/audio/audio.cpp`, which reads NVS volume. + +The servos step sweeps PWM duty on channels 0 through 4 together so every blue LED can pulse, with OE enabled (GP5 low). The operator plugs the testing board into one column at a time during that one step. The last step drives OE high so the pins turn off, while PWM keeps pulsing on channels 0 through 4. The 5 V rail stays on, so only the blue LED goes out. Restarting the board enables OE again. Silk-side pin is blue (SIG), middle pin is red (5 V), square pad is GND. Each column expects red on and blue pulsing. `tone` is the ear on J1; lamps are ignored. + +`r` repeats the current step and drops a fail recorded for that attempt. `c` appends the step name and advances. `a` clears the list and returns to the connected confirmation without a power cycle. Enter starts the tone again. On a step `FAIL` the host shows `r`, `c`, and `a` with the step name. `FAIL pca` does not. + +The onboard WS2812 (`RGB_LED_PIN`, GPIO10) is turned steady white once the image is up and left there through steps, the failure menu, `FAIL pca`, `RESULT`, and `STOP`. The three channels are equal, so `rgb_order` is not read and NVS is not opened. Red and green are not used. The lamp does not blink and does not encode pass or fail. Removing USB power is what turns it off. + +Alternative: blue, to dodge an R/G swap. Rejected. The product already allows `RBG`, `GBR`, `BRG`, and `BGR`, and those put blue on the red or green wire. White survives every order. + +### On-screen coaching + +The wire stays the token lines. Each host adds the same English sentences for the steps. A waiting step does not print the `STEP` token. It shows `Enter = good` and `n = bad` in the box footer, and its place in the three. The page shows the banner sentence. The script does not print it. The procedure card already says the lamp is not the verdict. + +| Token | On screen | +| --- | --- | +| banner | Page only. The module LED stays steady white while the test image has power. That lamp is not the board verdict. | +| `1/3` `tone` | Speaker on the JST speaker port. Expect a repeating tone. `+` louder, `-` quieter. The card shows the level. | +| `2/3` `servos` | Signal is on all five servo outputs. Take the testing board. Plug it into one column at a time. Columns are HEAD, NECK, ARM L, ARM R, and BODY. Check the LEDs on every column. Red stays on and blue pulses. | +| `3/3` OE OUTPUT ENABLE | The red LED stays on. Only the blue LED goes out. No need to test each servo column. | + +A step `FAIL` repeats that step's expected effect and shows `r = try this step again`, `c = keep the fail, go on`, and `a = start this board over`. `FAIL pca` shows `PCA9685 did not answer. This board fails before the speaker test.` and does not show `r`, `c`, or `a`. + +### Two hosts, one line protocol + +`scripts/pcba_test.py` uses Python 3 and `pyserial`. That install exists for Windows, macOS, and Linux. On start it prints `https://tinyengineer.dev/pcba_test` as the alternate tester. The script picks a single CDC port (or `--port`), prints the English coaching as the large line, shows `Enter = good` and `n = bad` in the box footer, shows `r`, `c`, and `a` when the line is a step `FAIL`, and writes keys to the port. On port loss it waits and opens the port that comes back. + +The alternate host is `web/pcba_test/index.html`, published at `https://tinyengineer.dev/pcba_test` the same way `web/flash/` is `/flash/`. A request without the trailing slash redirects to the slash form. The page opens the golden module with Web Serial, clears DTR and RTS, writes `?`, and speaks the same lines and keys. It is not a route in the robot `ui/`. The test image does not start HTTP. + +On load the page checks `isSecureContext` and `navigator.serial`, the same gate as `web/js/flash-status.js`. If either fails, the page says this browser cannot run the test, names `python3 scripts/pcba_test.py`, and shows no connect control. Chrome or Edge on the HTTPS site passes. The first open needs a click, because a serial grant comes only from a user gesture. The page then keeps that grant and reopens it when the cable moves, without a reload. After the port is open the page hides Connect and shows Disconnect. Disconnect closes the port and reloads the page. After the link drops, Connect comes back and the click asks for the port again. The page also names `python3 scripts/pcba_test.py` and links the procedure doc on GitHub. + +No HTML page under `web/` other than this one links to `/pcba_test`. The procedure card `docs/hardware/pcba-test.md` states the bench, the plug facing, that the servos step pulses all five columns while the operator moves the plug with no key between columns, the red/blue look, the steady white module LED, J1, the keys, unplug for the next board, that the golden module stays with the tester, `python3 scripts/pcba_test.py`, and `https://tinyengineer.dev/pcba_test`. + +Alternative: a `file://` HTML page. Rejected. Web Serial is blocked outside a secure context. Alternative: a route on the product panel or on the test image. Rejected. The test image does not start HTTP, and the product panel is the robot UI. Alternative: three native binaries. Deferred. The script covers every OS; the page covers Chromium on the site. + +### Golden module stays with the tester + +The test image is uploaded with `pio run -e pcba-test -t upload` onto the fixture module. That module stays with the person running the test. Both hosts say the ESP32 must already run the test firmware and point at the procedure section for how to get one. This change does not flash the product image onto a produced board. + +## Risks / Trade-offs + +- [Operator skips a column] → The servos step pulses all five outputs at once, and the card says to move the plug across every column before Enter. A missed column still logs as pass if the operator accepts. +- [Plug reversed 180°] → SIG and GND swap. Blue stays dark, so the step fails if the operator follows the card. The 1 kΩ on the red LED limits current into the PWM pin. +- [I2S init succeeds with a dead amplifier] → Pass/fail for `tone` is the operator's ear, not the init return code. +- [Windows COM number changes after replug] → The script treats disappearance as "wait" and attaches to the new CDC port instead of exiting. The page reopens the same granted device, because the golden module does not change. +- [Browser has no Web Serial] → The page names the Python command and does not offer a port connection. +- [Test image flashed onto a module that should run the product] → Default env is unchanged. The pcba-test upload command names `-e pcba-test`. The image banner is `PCBA-TEST`, not `TINY ENGINEER`. + +## Migration Plan + +No product NVS, HTTP, or serial change. Rollback is deleting the env and the example. A module that should run the product is reflashed with the product env before it leaves the test setup. + +## Open Questions + +None that change the session. The on-bench LED wiring drawing stays outside this change. diff --git a/openspec/changes/archive/2026-10-30-main-control-board-tester/proposal.md b/openspec/changes/archive/2026-10-30-main-control-board-tester/proposal.md new file mode 100644 index 00000000..f818ffb3 --- /dev/null +++ b/openspec/changes/archive/2026-10-30-main-control-board-tester/proposal.md @@ -0,0 +1,36 @@ +# Proposal + +## Why + +Anyone who wants to check a produced main control board needs a repeatable pass/fail. The product firmware boots Wi-Fi, centers servos, and treats I2S init as success, so it cannot run this check. A PCBA test image and a small host program should walk one person through the ESP32 socket, the speaker, and each servo column. + +## What Changes + +- Add an opt-in PlatformIO env `pcba-test` with its own `main`. The default robot env stays unchanged. The test image is flashed once onto a golden ESP32-C3-Zero that stays with the person running the test. +- The image talks plain USB CDC lines at 115200. It does not speak the product COBS serial protocol, start Wi-Fi, or mount LittleFS. While it has USB power the onboard LED stays steady white, including after `q`. That lamp only means the test image is running. +- One operator, one keyboard. Silk-side pin is blue, middle pin is red, square pad is GND. The tone step is the speaker. The servos step puts signal on all five servo outputs at once. The operator plugs the testing board into HEAD, NECK, ARM L, ARM R, and BODY, one column at a time. Enter accepts a step. `n` rejects it. +- On every rejected step the image asks: retry the step (`r`), record the failure and continue (`c`), or restart the whole board (`a`). The tone repeats until Enter or `n`. +- After `RESULT PASS` or `RESULT FAIL`, the host stays open. Unplugging USB starts the next board. `q` ends the session. `n` after the verdict does nothing. +- Add a Python 3 host (`pyserial`) that finds the CDC port, shows the current step or the verdict, forwards those keys, and reconnects when the USB cable moves to the next board. On start it prints `https://tinyengineer.dev/pcba_test` as the alternate tester. +- Add that web host at `https://tinyengineer.dev/pcba_test` as an alternate path to the script. It uses the same CDC lines. No other site page links to it. If the browser has no Web Serial, or the page is not a secure context, the page says so and points at the Python script. +- Both hosts, in English, say what the current step tests, what to do, and the expected effect, and still show the wire token. Steps are numbered `1/3` through `3/3`. `FAIL pca` does not show `r`, `c`, or `a`. +- Out of scope: J2, J3, J4, OLED, Wi-Fi, a 2 A load, using the test image as the robot's everyday firmware, and serving this page from the robot UI or from the test image. + +No **BREAKING** product HTTP, serial, or settings changes. The product binary is a different env. + +## Capabilities + +### New Capabilities + +- `pcba-test`: PCBA test session on the golden module and the host: boot checks, tone, all servo columns at once, failure menu, board verdict, next board or stop. + +### Modified Capabilities + +- (none — the product `serial-protocol` stays on the robot image; the PCBA test image uses a separate line protocol) + +## Impact + +- **Firmware**: new env in `platformio.ini` (same pattern as `expression-demo`); new `examples/pcba-test/main.cpp`. Pin numbers come from `include/pins.h`. Product `src/main.cpp` is not linked and not edited for a runtime test mode. +- **Host**: one Python script under `scripts/`, dependency `pyserial`. Launch is `python3` plus that script on Windows, macOS, or Linux. The script prints `https://tinyengineer.dev/pcba_test` as the alternate. That page is `web/pcba_test/` on tinyengineer.dev (same shape as `/flash/`). Not a route in the robot `ui/`. Other pages under `web/` do not link to it. Both hosts print the same English step coaching next to the wire token. +- **Test hardware** (not built in this change): 3-pin female plug with a red LED on 5 V and a blue LED on PWM, plus one test speaker on J1. +- **Docs**: `docs/hardware/pcba-test.md`, the procedure card for the person running the test. It names the bench, the plug facing, the one servos step that pulses every column while the operator moves the plug with no key between columns, the lamp pattern, the steady white module LED, J1, the keys, unplug for the next board, the golden module staying with the tester, the Python command, and `https://tinyengineer.dev/pcba_test`. Product API docs stay as they are. diff --git a/openspec/changes/archive/2026-10-30-main-control-board-tester/specs/pcba-test/spec.md b/openspec/changes/archive/2026-10-30-main-control-board-tester/specs/pcba-test/spec.md new file mode 100644 index 00000000..d0b10a8c --- /dev/null +++ b/openspec/changes/archive/2026-10-30-main-control-board-tester/specs/pcba-test/spec.md @@ -0,0 +1,326 @@ +# Spec Delta + +## Purpose + +Defines the PCBA test session: a golden-module image and a host, either the Python script or the web page, that walk one person through the ESP32 socket, the speaker, and the servo columns together, then record a pass or fail. + +## ADDED Requirements + +### Requirement: PCBA test image is not the product boot +The PCBA test image MUST start the PCBA session on USB connect. It MUST NOT join Wi-Fi, start HTTP, mount the audio filesystem, or speak the product COBS serial protocol. + +#### Scenario: Power-up banner +- **WHEN** the golden module receives USB power through a board under test +- **THEN** it prints PCBA-TEST v1 on USB CDC at 115200 +- **AND** it does not open the product setup AP or HTTP server + +### Requirement: PCA probe gates the session +Before the tone or any servo step, the image MUST probe I2C address 0x40. If nothing acknowledges, it MUST print a PCA failure and a final FAIL result and MUST NOT start the tone or servo steps. + +#### Scenario: PCA missing +- **WHEN** the session starts and address 0x40 does not acknowledge +- **THEN** the image prints FAIL pca and RESULT FAIL pca +- **AND** it does not play a tone or offer retry, continue, or restart + +#### Scenario: PCA present +- **WHEN** address 0x40 acknowledges +- **THEN** the image prints READY +- **AND** the tone has not started until the operator sends Enter + +### Requirement: Fixed step order +A board session MUST run three steps in this order: tone, then servos, then oe. The oe step MUST disable the PCA9685 outputs and MUST keep pulsing PWM on channels 0, 1, 2, 3, and 4. The servos step MUST pulse every servo column at the same time. + +#### Scenario: Happy path order +- **WHEN** the operator accepts every step +- **THEN** the image announces tone, then servos, then oe +- **AND** then prints a RESULT line + +### Requirement: Operator accepts or rejects the current step +Each step MUST hold its outputs until the operator sends Enter or `n`. Enter on the wire MUST be a single line feed and MUST record a pass for that step. `n` MUST record a fail for that step and present the failure menu. Any other key while the step is waiting for Enter or `n` MUST be ignored, except `+` and `-` on the tone step. + +#### Scenario: Accept tone +- **WHEN** the tone step is active and the operator sends Enter +- **THEN** the tone step is recorded as passed +- **AND** the session moves to servos + +#### Scenario: Reject a column +- **WHEN** a servo step is active and the operator sends `n` +- **THEN** that step is recorded as failed +- **AND** the image presents the failure menu before leaving the step + +### Requirement: Failure menu offers retry, continue, or restart +On every failed step the image MUST wait for `r`, `c`, or `a`. Any other key during that wait MUST be ignored. `r` MUST repeat the same step. `c` MUST keep the failure and advance. `a` MUST discard the board's results and return to the connected confirmation without removing USB power. The tone MUST NOT start until Enter. + +#### Scenario: Retry the same step +- **WHEN** the operator sends `r` after failing servos +- **THEN** the image announces servos again +- **AND** the previous fail for servos is not kept + +#### Scenario: Continue after a failure +- **WHEN** the operator sends `c` after failing a step +- **THEN** the session advances to the next step +- **AND** the failed step remains on the board's fail list + +#### Scenario: Restart the board +- **WHEN** the operator sends `a` during a board session +- **THEN** the fail list is cleared +- **AND** the image prints READY +- **AND** the tone has not started until the operator sends Enter + +### Requirement: All servo columns pulse together +During the servos step the image MUST pulse PWM duty on channels 0, 1, 2, 3, and 4 at the same time, with outputs enabled. The mapping MUST be head=0, neck=1, arm-l=2, arm-r=3, body=4. Moving the plug from column to column MUST NOT require a key. + +#### Scenario: Every column pulses +- **WHEN** the servos step is active +- **THEN** channels 0 through 4 are pulsing +- **AND** the outputs are enabled + +### Requirement: Tone is generated on the board +The tone step MUST play an audible tone through the I2S pins to the amplifier and MUST keep playing it until the operator sends Enter or `n`. The tone MUST NOT depend on files in the audio filesystem. + +#### Scenario: Tone uses I2S pins +- **WHEN** the tone step becomes active +- **THEN** the image emits a tone on the I2S clock, word-select, and data pins +- **AND** the tone is still emitting while the operator has not sent Enter or `n` + +### Requirement: Speaker step changes loudness +During the tone step `+` MUST make the tone louder and `-` MUST make it quieter without stopping the tone. At either end of the range that key MUST leave the level unchanged. Other steps MUST ignore `+` and `-`. The speaker card MUST show `+ = louder` and `- = quieter`. + +#### Scenario: Louder on the speaker step +- **WHEN** the tone step is active and the operator sends `+` +- **THEN** the tone keeps playing at a higher level +- **AND** the host shows + = louder + +#### Scenario: Quieter on the speaker step +- **WHEN** the tone step is active and the operator sends `-` +- **THEN** the tone keeps playing at a lower level + +#### Scenario: Other steps ignore volume keys +- **WHEN** the servos step is active and the operator sends `+` +- **THEN** the servo pulse continues unchanged + +### Requirement: One verdict line per board +When the step list finishes, the image MUST print a single RESULT line. RESULT PASS MUST be used only when every completed step was accepted. Otherwise the line MUST be RESULT FAIL followed by the failed step names separated by spaces, in step order. + +#### Scenario: All steps accepted +- **WHEN** the operator has accepted tone, servos, and oe +- **THEN** the image prints RESULT PASS + +#### Scenario: Continue past two failures +- **WHEN** the operator continued past failed tone and failed servos +- **THEN** the image prints RESULT FAIL tone servos + +### Requirement: Next board or stop after the verdict +After RESULT, removing USB power MUST end the session, and the next USB connect MUST start a new session at the banner. `q` MUST print STOP and MUST NOT start another step until power is removed and restored. `n` after RESULT MUST be ignored. + +#### Scenario: Cable leaves after the verdict +- **WHEN** the module loses USB power after RESULT +- **THEN** a later USB connect starts a new session at the banner + +#### Scenario: Operator stops +- **WHEN** the operator sends `q` after RESULT +- **THEN** the image prints STOP +- **AND** it does not announce another step until power is removed and restored + +#### Scenario: Reject key after the verdict +- **WHEN** the operator sends `n` after RESULT +- **THEN** the image does not start another step +- **AND** it does not treat that key as a board failure + +### Requirement: Host shows the step and survives the cable move +Each host MUST open the CDC port, show the current step or the RESULT line, and send Enter, `n`, `r`, `c`, `a`, and `q`. A step FAIL MUST show `r`, `c`, and `a`. FAIL pca MUST NOT. When the port returns, the host MUST attach without a relaunch. + +#### Scenario: Key forwards to the module +- **WHEN** the operator presses Enter while a step is on screen +- **THEN** the host sends a line feed to the module + +#### Scenario: Failure menu is on screen +- **WHEN** the module prints FAIL servos +- **THEN** the host shows `r`, `c`, and `a` + +#### Scenario: PCA failure hides the step menu +- **WHEN** the module prints FAIL pca +- **THEN** the host does not show `r`, `c`, or `a` + +#### Scenario: Cable moves to the next board +- **WHEN** the serial port drops and a CDC port returns +- **THEN** the host shows the new session's banner or step +- **AND** the operator does not start the program again + +### Requirement: Web tester lives at /pcba_test and checks the browser +https://tinyengineer.dev MUST serve the web host at `/pcba_test`. No other page on that site MUST link to it. When Web Serial is unavailable or the page is not a secure context, the page MUST say this browser cannot run the test and MUST name the Python host command, and it MUST NOT offer a port connection. When Web Serial is available in a secure context, the page MUST offer a connection and then run the same session as the script host. + +#### Scenario: Incompatible browser +- **WHEN** the page loads and Web Serial is missing or the context is not secure +- **THEN** the page states that this browser cannot run the test +- **AND** it names the Python host command +- **AND** it does not offer a port connection + +#### Scenario: Compatible browser +- **WHEN** the page loads in a secure context with Web Serial +- **THEN** the page offers a port connection +- **AND** after the port is open it shows steps and forwards the operator keys + +#### Scenario: No link from another site page +- **WHEN** a reader opens any other HTML page on the project site +- **THEN** that page does not link to `/pcba_test` + +### Requirement: Procedure card and script name the web tester +The procedure card MUST describe the bench, the step order, the expected lamps, the keys, and both ways to run the session. The card MUST say the golden module stays with the tester. The card and the script host MUST name the web tester as https://tinyengineer.dev/pcba_test. The script MUST show that address as the alternate to itself. + +#### Scenario: Card names the public page +- **WHEN** the operator reads the procedure card +- **THEN** it includes https://tinyengineer.dev/pcba_test +- **AND** it includes the Python host command +- **AND** it lists the step order and the keys + +#### Scenario: Script offers the web tester +- **WHEN** the script starts +- **THEN** it shows https://tinyengineer.dev/pcba_test as the alternate tester + +#### Scenario: Card keeps the golden module +- **WHEN** the operator reads the bench setup +- **THEN** the card says the golden module stays with the tester + +### Requirement: Plug position and lamp pattern +The procedure card MUST say signal is on all five servo outputs and the operator plugs the testing board into one column at a time: HEAD, NECK, ARM L, ARM R, and BODY. The card MUST name the silk-side pin as blue, the middle pin as red, and the square pad as GND. Each column MUST show red on and blue pulsing. Tone MUST be judged by ear on J1. + +#### Scenario: Servo step is one plug sweep +- **WHEN** the operator reads the servos step on the card +- **THEN** the card says signal is on all five servo outputs +- **AND** it says to plug the testing board into one column at a time +- **AND** the columns are HEAD, NECK, ARM L, ARM R, and BODY +- **AND** the expected look is red stays on and blue pulses + +#### Scenario: Plug faces the square pad +- **WHEN** the operator reads how to seat the plug +- **THEN** the card names the square pad as GND +- **AND** it names the silk-side pin as the blue LED + +### Requirement: Module LED shows the test image is running +While the test image has USB power it MUST hold the onboard LED steady white, including during a step, the failure menu, a PCA failure, RESULT, and STOP. The three channels MUST be equal. The LED MUST NOT blink. The procedure card MUST say this steady white means the test image is running, and that board pass or fail is the ear on J1 and the plug lamps. + +#### Scenario: Session stays white +- **WHEN** the image is in a step, the failure menu, RESULT, or STOP +- **THEN** the onboard LED is steady white + +#### Scenario: PCA failure stays white +- **WHEN** address 0x40 does not acknowledge +- **THEN** the onboard LED is steady white +- **AND** the image still prints RESULT FAIL pca + +#### Scenario: Card names the module lamp +- **WHEN** the operator reads the bench setup +- **THEN** the card says the module LED stays steady white while the test image has power + +### Requirement: Host explains the current step in English +Each host MUST show English text for the current step: what is tested, what to do, and the expected effect. A waiting step MUST NOT show the STEP token. Steps MUST be numbered 1/3 through 3/3. A waiting step MUST show Enter as good and n as bad. A step FAIL MUST show that step's expected effect and words for r, c, and a. FAIL pca MUST NOT show r, c, or a. Both hosts MUST use the same step sentences. + +#### Scenario: Tone step explains the ear +- **WHEN** the module prints STEP tone +- **THEN** the host shows 1/3 +- **AND** it shows Speaker on the JST speaker port. Expect a repeating tone. +- **AND** it does not show the token STEP tone +- **AND** it shows Enter = good and n = bad + +#### Scenario: Servo step is one sweep +- **WHEN** the module prints STEP servos +- **THEN** the host shows 2/3 +- **AND** it shows Signal is on all five servo outputs. Take the testing board. Plug it into one column at a time. Columns are HEAD, NECK, ARM L, ARM R, and BODY. Check the LEDs on every column. Red stays on and blue pulses. +- **AND** it does not show the token STEP servos + +#### Scenario: OE step releases the outputs +- **WHEN** the module prints STEP oe +- **THEN** the host shows 3/3 OE OUTPUT ENABLE +- **AND** it shows The red LED stays on. Only the blue LED goes out. +- **AND** it shows No need to test each servo column. +- **AND** it does not show the token STEP oe + +#### Scenario: Step failure names the keys +- **WHEN** the module prints FAIL servos +- **THEN** the host shows Red stays on and blue pulses. +- **AND** it shows r = try this step again +- **AND** it shows c = keep the fail, go on +- **AND** it shows a = start this board over + +#### Scenario: PCA failure explains the stop +- **WHEN** the module prints FAIL pca +- **THEN** the host shows PCA9685 did not answer. This board fails before the speaker test. +- **AND** it does not show r, c, or a + +### Requirement: Banner lamp sentence is page only +When the banner arrives, the page MUST show that the module LED stays steady white and is not the board verdict. The script MUST NOT show that sentence. + +#### Scenario: Page names the module lamp +- **WHEN** the page reads PCBA-TEST v1 +- **THEN** it shows The module LED stays steady white while the test image has power. That lamp is not the board verdict. + +#### Scenario: Script hides the banner sentence +- **WHEN** the script reads PCBA-TEST v1 +- **THEN** it does not show that lamp sentence + +### Requirement: Hosts name the board revision +The script, the page, and the procedure card MUST say the test is compatible with main control board rev 3. + +#### Scenario: Script names the revision +- **WHEN** the script starts +- **THEN** it shows Compatible with main control board rev 3. + +#### Scenario: Page names the revision +- **WHEN** the page loads +- **THEN** it shows Compatible with main control board rev 3. + +#### Scenario: Card names the revision +- **WHEN** the operator reads the procedure card +- **THEN** the card says the test is compatible with main control board rev 3 + +### Requirement: Web page controls the open port +After the port opens, the page MUST hide Connect and show Disconnect. Disconnect MUST close the port and reload the page. The page MUST keep the serial grant and reopen it when the cable returns, without a reload. After the link drops, Connect MUST return and that click MUST ask for the port again. + +#### Scenario: Port is open +- **WHEN** the operator opens the port +- **THEN** Connect is hidden +- **AND** Disconnect is shown + +#### Scenario: Operator disconnects +- **WHEN** the operator presses Disconnect +- **THEN** the page closes the port and reloads + +#### Scenario: Link drops +- **WHEN** the open port is lost +- **THEN** Connect returns +- **AND** that click asks for the port again + +### Requirement: Page names the script and the procedure +The page MUST name `python3 scripts/pcba_test.py` and MUST link the procedure at `https://github.com/jamro/tiny-engineer/blob/main/docs/hardware/pcba-test.md`. + +#### Scenario: Script path on the page +- **WHEN** the page loads in a browser that can run the test +- **THEN** the page names python3 scripts/pcba_test.py +- **AND** it links the procedure doc on GitHub + +### Requirement: Script waits on a busy port +When the script cannot open the board port because it is busy, it MUST say the port is busy and MUST keep waiting. It MUST NOT exit with a traceback. + +#### Scenario: Another program holds the port +- **WHEN** the board port is busy +- **THEN** the script shows Port is busy. Close the other program using the board. This script will keep waiting. +- **AND** the script stays running + +### Requirement: Hosts require the test firmware +Both hosts MUST say the ESP32 must already run the test firmware. The script MUST print, and the page MUST link, the procedure section on how to get one. The procedure MUST say the product firmware cannot run this test and MUST include the pcba-test upload command. + +#### Scenario: Script names the firmware +- **WHEN** the script starts +- **THEN** it shows The ESP32 must already run the test firmware. +- **AND** it shows https://github.com/jamro/tiny-engineer/blob/main/docs/hardware/pcba-test.md#get-an-esp32-with-the-test-firmware + +#### Scenario: Page names the firmware +- **WHEN** the page loads +- **THEN** it shows The ESP32 must already run the test firmware. +- **AND** it links https://github.com/jamro/tiny-engineer/blob/main/docs/hardware/pcba-test.md#get-an-esp32-with-the-test-firmware + +#### Scenario: Card explains how to get one +- **WHEN** the operator reads how to get the test firmware +- **THEN** the card says the product firmware cannot run this test +- **AND** it includes pio run -e pcba-test -t upload diff --git a/openspec/changes/archive/2026-10-30-main-control-board-tester/tasks.md b/openspec/changes/archive/2026-10-30-main-control-board-tester/tasks.md new file mode 100644 index 00000000..7227a932 --- /dev/null +++ b/openspec/changes/archive/2026-10-30-main-control-board-tester/tasks.md @@ -0,0 +1,25 @@ +# Tasks + +## 1. PCBA test image + +- [x] 1.1 Add `[env:pcba-test]` and `examples/pcba-test/main.cpp` that prints `PCBA-TEST v1` on USB CDC and does not attach product `extra_scripts`. Verify `pio run -e pcba-test` exits 0 and `default_envs` in `platformio.ini` is still `esp32-c3-devkitm-1`. +- [x] 1.2 Implement the session in that `main`: PCA probe at `0x40`, steps `tone`, `servos`, and `oe`, keys Enter/`n`/`r`/`c`/`a`/`q`, repeating tone until Enter or `n`, OE high during `oe` while PWM keeps pulsing on all five channels, stays disabled after Enter or `c` finish the board, and `a` enables it again through restart, `n` after `RESULT` ignored, and lines `PCBA-TEST v1`, `STEP`, `FAIL`, `RESULT PASS`, `RESULT FAIL` with space-separated names, and `STOP`. Verify `pio run -e pcba-test` exits 0 and the example source names `tone`, `servos`, and `oe` in that order. +- [x] 1.3 Hold the onboard LED on GPIO10 steady white with equal channels from boot through STOP, including a PCA failure. Do not blink it and do not read `rgb_order`. Verify `pio run -e pcba-test` exits 0 and the example source writes equal channel values to that pin. + +## 2. Host and operator card + +- [x] 2.1 Add `scripts/pcba_test.py` that reads `STEP`, `FAIL`, `RESULT`, and `STOP` lines, sends Enter as a line feed plus `n`, `r`, `c`, `a`, and `q`, shows `r`, `c`, and `a` when the line is `FAIL`, and on start prints `https://tinyengineer.dev/pcba_test` as the alternate tester. Verify a unit test feeds a scripted accept path and a scripted `n` then `c` path, checks the shown step and the `r`/`c`/`a` prompt, and checks that startup text contains that URL. +- [x] 2.2 On port loss, wait and open the returning CDC port. Verify a unit test drops the fake port, reconnects, and checks the next session banner. +- [x] 2.3 Write `docs/hardware/pcba-test.md` as the procedure card: bench setup, the testing board one column at a time, lamp pattern, J1, keys, unplug for the next board, golden module stays with the tester, `python3 scripts/pcba_test.py`, and `https://tinyengineer.dev/pcba_test`. Verify the doc lists `tone`, `servos`, `oe`, HEAD, NECK, ARM L, ARM R, BODY, keys `r`, `c`, `a`, `n`, `q`, `python3 scripts/pcba_test.py`, `https://tinyengineer.dev/pcba_test`, red stays on and blue pulses, square pad as GND, J1, and that the golden module stays with the tester. +- [x] 2.4 Add `web/pcba_test/index.html` that, when `navigator.serial` is missing or the context is not secure, says this browser cannot run the test, names `python3 scripts/pcba_test.py`, and shows no connect control. Verify a unit test of that gate for the failing cases and the Chrome-on-HTTPS case, and that no other HTML file under `web/` links to `pcba_test`. +- [x] 2.5 On a passing browser, open the golden module with Web Serial, show the current step, forward Enter, `n`, `r`, `c`, `a`, and `q`, and reopen the granted port when it returns without reloading the page. Verify a unit test feeds a scripted `STEP` then `RESULT` and a disconnect-then-connect, and checks the shown step and that the page did not ask for a new grant. +- [x] 2.6 Update `docs/hardware/pcba-test.md` so the bench setup says the module LED stays steady white while the test image has power, and that this lamp is not the board verdict. Verify the doc contains `steady white`, red stays on and blue pulses, and only the blue LED goes out. + +## 3. Session dry run + +- [x] 3.1 Point the host at a fake device that prints the banner and walks an all-accept session, then a session that fails `tone` and `servos` with `c`. Verify the host log shows `RESULT PASS` and `RESULT FAIL tone servos`, and the operator card matches that dialogue. + +## 4. On-screen coaching + +- [x] 4.1 In both hosts, show English coaching for every `STEP`: the index `1/3` through `3/3`, what is tested, what to do, the expected effect, `Enter = good`, `n = bad`, and the wire token. The script and the page use the same three sentences. Verify unit tests for `STEP tone` (`1/3`, speaker, repeating tone), `STEP servos` (`2/3`, testing board one column at a time, red stays on and blue pulses), and `STEP oe` (`3/3`, only the blue LED goes out). +- [x] 4.2 On a step `FAIL`, show that step's expected effect and `r = try this step again`, `c = keep the fail, go on`, `a = start this board over`. On `FAIL pca`, show that the PCA9685 did not answer and do not show `r`, `c`, or `a`. On the banner, say the module LED stays steady white and is not the board verdict. Verify unit tests for `FAIL servos`, `FAIL pca`, and `PCBA-TEST v1`. diff --git a/openspec/specs/pcba-test/spec.md b/openspec/specs/pcba-test/spec.md new file mode 100644 index 00000000..c3942b83 --- /dev/null +++ b/openspec/specs/pcba-test/spec.md @@ -0,0 +1,326 @@ +# pcba-test Specification + +## Purpose + +Defines the PCBA test session: a golden-module image and a host, either the Python script or the web page, that walk one person through the ESP32 socket, the speaker, and the servo columns together, then record a pass or fail. + +## Requirements + +### Requirement: PCBA test image is not the product boot +The PCBA test image MUST start the PCBA session on USB connect. It MUST NOT join Wi-Fi, start HTTP, mount the audio filesystem, or speak the product COBS serial protocol. + +#### Scenario: Power-up banner +- **WHEN** the golden module receives USB power through a board under test +- **THEN** it prints PCBA-TEST v1 on USB CDC at 115200 +- **AND** it does not open the product setup AP or HTTP server + +### Requirement: PCA probe gates the session +Before the tone or any servo step, the image MUST probe I2C address 0x40. If nothing acknowledges, it MUST print a PCA failure and a final FAIL result and MUST NOT start the tone or servo steps. + +#### Scenario: PCA missing +- **WHEN** the session starts and address 0x40 does not acknowledge +- **THEN** the image prints FAIL pca and RESULT FAIL pca +- **AND** it does not play a tone or offer retry, continue, or restart + +#### Scenario: PCA present +- **WHEN** address 0x40 acknowledges +- **THEN** the image prints READY +- **AND** the tone has not started until the operator sends Enter + +### Requirement: Fixed step order +A board session MUST run three steps in this order: tone, then servos, then oe. The oe step MUST disable the PCA9685 outputs and MUST keep pulsing PWM on channels 0, 1, 2, 3, and 4. The servos step MUST pulse every servo column at the same time. + +#### Scenario: Happy path order +- **WHEN** the operator accepts every step +- **THEN** the image announces tone, then servos, then oe +- **AND** then prints a RESULT line + +### Requirement: Operator accepts or rejects the current step +Each step MUST hold its outputs until the operator sends Enter or `n`. Enter on the wire MUST be a single line feed and MUST record a pass for that step. `n` MUST record a fail for that step and present the failure menu. Any other key while the step is waiting for Enter or `n` MUST be ignored, except `+` and `-` on the tone step. + +#### Scenario: Accept tone +- **WHEN** the tone step is active and the operator sends Enter +- **THEN** the tone step is recorded as passed +- **AND** the session moves to servos + +#### Scenario: Reject a column +- **WHEN** a servo step is active and the operator sends `n` +- **THEN** that step is recorded as failed +- **AND** the image presents the failure menu before leaving the step + +### Requirement: Failure menu offers retry, continue, or restart +On every failed step the image MUST wait for `r`, `c`, or `a`. Any other key during that wait MUST be ignored. `r` MUST repeat the same step. `c` MUST keep the failure and advance. `a` MUST discard the board's results and return to the connected confirmation without removing USB power. The tone MUST NOT start until Enter. + +#### Scenario: Retry the same step +- **WHEN** the operator sends `r` after failing servos +- **THEN** the image announces servos again +- **AND** the previous fail for servos is not kept + +#### Scenario: Continue after a failure +- **WHEN** the operator sends `c` after failing a step +- **THEN** the session advances to the next step +- **AND** the failed step remains on the board's fail list + +#### Scenario: Restart the board +- **WHEN** the operator sends `a` during a board session +- **THEN** the fail list is cleared +- **AND** the image prints READY +- **AND** the tone has not started until the operator sends Enter + +### Requirement: All servo columns pulse together +During the servos step the image MUST pulse PWM duty on channels 0, 1, 2, 3, and 4 at the same time, with outputs enabled. The mapping MUST be head=0, neck=1, arm-l=2, arm-r=3, body=4. Moving the plug from column to column MUST NOT require a key. + +#### Scenario: Every column pulses +- **WHEN** the servos step is active +- **THEN** channels 0 through 4 are pulsing +- **AND** the outputs are enabled + +### Requirement: Tone is generated on the board +The tone step MUST play an audible tone through the I2S pins to the amplifier and MUST keep playing it until the operator sends Enter or `n`. The tone MUST NOT depend on files in the audio filesystem. + +#### Scenario: Tone uses I2S pins +- **WHEN** the tone step becomes active +- **THEN** the image emits a tone on the I2S clock, word-select, and data pins +- **AND** the tone is still emitting while the operator has not sent Enter or `n` + +### Requirement: Speaker step changes loudness +During the tone step `+` MUST make the tone louder and `-` MUST make it quieter without stopping the tone. At either end of the range that key MUST leave the level unchanged. Other steps MUST ignore `+` and `-`. The speaker card MUST show `+ = louder` and `- = quieter`. + +#### Scenario: Louder on the speaker step +- **WHEN** the tone step is active and the operator sends `+` +- **THEN** the tone keeps playing at a higher level +- **AND** the host shows + = louder + +#### Scenario: Quieter on the speaker step +- **WHEN** the tone step is active and the operator sends `-` +- **THEN** the tone keeps playing at a lower level + +#### Scenario: Other steps ignore volume keys +- **WHEN** the servos step is active and the operator sends `+` +- **THEN** the servo pulse continues unchanged + +### Requirement: One verdict line per board +When the step list finishes, the image MUST print a single RESULT line. RESULT PASS MUST be used only when every completed step was accepted. Otherwise the line MUST be RESULT FAIL followed by the failed step names separated by spaces, in step order. + +#### Scenario: All steps accepted +- **WHEN** the operator has accepted tone, servos, and oe +- **THEN** the image prints RESULT PASS + +#### Scenario: Continue past two failures +- **WHEN** the operator continued past failed tone and failed servos +- **THEN** the image prints RESULT FAIL tone servos + +### Requirement: Next board or stop after the verdict +After RESULT, removing USB power MUST end the session, and the next USB connect MUST start a new session at the banner. `q` MUST print STOP and MUST NOT start another step until power is removed and restored. `n` after RESULT MUST be ignored. + +#### Scenario: Cable leaves after the verdict +- **WHEN** the module loses USB power after RESULT +- **THEN** a later USB connect starts a new session at the banner + +#### Scenario: Operator stops +- **WHEN** the operator sends `q` after RESULT +- **THEN** the image prints STOP +- **AND** it does not announce another step until power is removed and restored + +#### Scenario: Reject key after the verdict +- **WHEN** the operator sends `n` after RESULT +- **THEN** the image does not start another step +- **AND** it does not treat that key as a board failure + +### Requirement: Host shows the step and survives the cable move +Each host MUST open the CDC port, show the current step or the RESULT line, and send Enter, `n`, `r`, `c`, `a`, and `q`. A step FAIL MUST show `r`, `c`, and `a`. FAIL pca MUST NOT. When the port returns, the host MUST attach without a relaunch. + +#### Scenario: Key forwards to the module +- **WHEN** the operator presses Enter while a step is on screen +- **THEN** the host sends a line feed to the module + +#### Scenario: Failure menu is on screen +- **WHEN** the module prints FAIL servos +- **THEN** the host shows `r`, `c`, and `a` + +#### Scenario: PCA failure hides the step menu +- **WHEN** the module prints FAIL pca +- **THEN** the host does not show `r`, `c`, or `a` + +#### Scenario: Cable moves to the next board +- **WHEN** the serial port drops and a CDC port returns +- **THEN** the host shows the new session's banner or step +- **AND** the operator does not start the program again + +### Requirement: Web tester lives at /pcba_test and checks the browser +https://tinyengineer.dev MUST serve the web host at `/pcba_test`. No other page on that site MUST link to it. When Web Serial is unavailable or the page is not a secure context, the page MUST say this browser cannot run the test and MUST name the Python host command, and it MUST NOT offer a port connection. When Web Serial is available in a secure context, the page MUST offer a connection and then run the same session as the script host. + +#### Scenario: Incompatible browser +- **WHEN** the page loads and Web Serial is missing or the context is not secure +- **THEN** the page states that this browser cannot run the test +- **AND** it names the Python host command +- **AND** it does not offer a port connection + +#### Scenario: Compatible browser +- **WHEN** the page loads in a secure context with Web Serial +- **THEN** the page offers a port connection +- **AND** after the port is open it shows steps and forwards the operator keys + +#### Scenario: No link from another site page +- **WHEN** a reader opens any other HTML page on the project site +- **THEN** that page does not link to `/pcba_test` + +### Requirement: Procedure card and script name the web tester +The procedure card MUST describe the bench, the step order, the expected lamps, the keys, and both ways to run the session. The card MUST say the golden module stays with the tester. The card and the script host MUST name the web tester as https://tinyengineer.dev/pcba_test. The script MUST show that address as the alternate to itself. + +#### Scenario: Card names the public page +- **WHEN** the operator reads the procedure card +- **THEN** it includes https://tinyengineer.dev/pcba_test +- **AND** it includes the Python host command +- **AND** it lists the step order and the keys + +#### Scenario: Script offers the web tester +- **WHEN** the script starts +- **THEN** it shows https://tinyengineer.dev/pcba_test as the alternate tester + +#### Scenario: Card keeps the golden module +- **WHEN** the operator reads the bench setup +- **THEN** the card says the golden module stays with the tester + +### Requirement: Plug position and lamp pattern +The procedure card MUST say signal is on all five servo outputs and the operator plugs the testing board into one column at a time: HEAD, NECK, ARM L, ARM R, and BODY. The card MUST name the silk-side pin as yellow, the middle pin as red, and the square pad as GND. Each column MUST show red on and yellow pulsing. Tone MUST be judged by ear on J1. + +#### Scenario: Servo step is one plug sweep +- **WHEN** the operator reads the servos step on the card +- **THEN** the card says signal is on all five servo outputs +- **AND** it says to plug the testing board into one column at a time +- **AND** the columns are HEAD, NECK, ARM L, ARM R, and BODY +- **AND** the expected look is red stays on and yellow pulses + +#### Scenario: Plug faces the square pad +- **WHEN** the operator reads how to seat the plug +- **THEN** the card names the square pad as GND +- **AND** it names the silk-side pin as the yellow LED + +### Requirement: Module LED shows the test image is running +While the test image has USB power it MUST hold the onboard LED steady white, including during a step, the failure menu, a PCA failure, RESULT, and STOP. The three channels MUST be equal. The LED MUST NOT blink. The procedure card MUST say this steady white means the test image is running, and that board pass or fail is the ear on J1 and the plug lamps. + +#### Scenario: Session stays white +- **WHEN** the image is in a step, the failure menu, RESULT, or STOP +- **THEN** the onboard LED is steady white + +#### Scenario: PCA failure stays white +- **WHEN** address 0x40 does not acknowledge +- **THEN** the onboard LED is steady white +- **AND** the image still prints RESULT FAIL pca + +#### Scenario: Card names the module lamp +- **WHEN** the operator reads the bench setup +- **THEN** the card says the module LED stays steady white while the test image has power + +### Requirement: Host explains the current step in English +Each host MUST show English text for the current step: what is tested, what to do, and the expected effect. A waiting step MUST NOT show the STEP token. Steps MUST be numbered 1/3 through 3/3. A waiting step MUST show Enter as good and n as bad. A step FAIL MUST show that step's expected effect and words for r, c, and a. FAIL pca MUST NOT show r, c, or a. Both hosts MUST use the same step sentences. + +#### Scenario: Tone step explains the ear +- **WHEN** the module prints STEP tone +- **THEN** the host shows 1/3 +- **AND** it shows Speaker on the JST speaker port. Expect a repeating tone. +- **AND** it does not show the token STEP tone +- **AND** it shows Enter = good and n = bad + +#### Scenario: Servo step is one sweep +- **WHEN** the module prints STEP servos +- **THEN** the host shows 2/3 +- **AND** it shows Signal is on all five servo outputs. Take the testing board. Plug it into one column at a time. Columns are HEAD, NECK, ARM L, ARM R, and BODY. Check the LEDs on every column. Red stays on and yellow pulses. +- **AND** it does not show the token STEP servos + +#### Scenario: OE step releases the outputs +- **WHEN** the module prints STEP oe +- **THEN** the host shows 3/3 OE OUTPUT ENABLE +- **AND** it shows The red LED stays on. Only the yellow LED goes out. +- **AND** it shows No need to test each servo column. +- **AND** it does not show the token STEP oe + +#### Scenario: Step failure names the keys +- **WHEN** the module prints FAIL servos +- **THEN** the host shows Red stays on and yellow pulses. +- **AND** it shows r = try this step again +- **AND** it shows c = keep the fail, go on +- **AND** it shows a = start this board over + +#### Scenario: PCA failure explains the stop +- **WHEN** the module prints FAIL pca +- **THEN** the host shows PCA9685 did not answer. This board fails before the speaker test. +- **AND** it does not show r, c, or a + +### Requirement: Banner lamp sentence is page only +When the banner arrives, the page MUST show that the module LED stays steady white and is not the board verdict. The script MUST NOT show that sentence. + +#### Scenario: Page names the module lamp +- **WHEN** the page reads PCBA-TEST v1 +- **THEN** it shows The module LED stays steady white while the test image has power. That lamp is not the board verdict. + +#### Scenario: Script hides the banner sentence +- **WHEN** the script reads PCBA-TEST v1 +- **THEN** it does not show that lamp sentence + +### Requirement: Hosts name the board revision +The script, the page, and the procedure card MUST say the test is compatible with main control board rev 3. + +#### Scenario: Script names the revision +- **WHEN** the script starts +- **THEN** it shows Compatible with main control board rev 3. + +#### Scenario: Page names the revision +- **WHEN** the page loads +- **THEN** it shows Compatible with main control board rev 3. + +#### Scenario: Card names the revision +- **WHEN** the operator reads the procedure card +- **THEN** the card says the test is compatible with main control board rev 3 + +### Requirement: Web page controls the open port +After the port opens, the page MUST hide Connect and show Disconnect. Disconnect MUST close the port and reload the page. The page MUST keep the serial grant and reopen it when the cable returns, without a reload. After the link drops, Connect MUST return and that click MUST ask for the port again. + +#### Scenario: Port is open +- **WHEN** the operator opens the port +- **THEN** Connect is hidden +- **AND** Disconnect is shown + +#### Scenario: Operator disconnects +- **WHEN** the operator presses Disconnect +- **THEN** the page closes the port and reloads + +#### Scenario: Link drops +- **WHEN** the open port is lost +- **THEN** Connect returns +- **AND** that click asks for the port again + +### Requirement: Page names the script and the procedure +The page MUST name `python3 scripts/pcba_test.py` and MUST link the procedure at `https://github.com/jamro/tiny-engineer/blob/main/docs/hardware/pcba-test.md`. + +#### Scenario: Script path on the page +- **WHEN** the page loads in a browser that can run the test +- **THEN** the page names python3 scripts/pcba_test.py +- **AND** it links the procedure doc on GitHub + +### Requirement: Script waits on a busy port +When the script cannot open the board port because it is busy, it MUST say the port is busy and MUST keep waiting. It MUST NOT exit with a traceback. + +#### Scenario: Another program holds the port +- **WHEN** the board port is busy +- **THEN** the script shows Port is busy. Close the other program using the board. This script will keep waiting. +- **AND** the script stays running + +### Requirement: Hosts require the test firmware +Both hosts MUST say the ESP32 must already run the test firmware. The script MUST print, and the page MUST link, the procedure section on how to get one. The procedure MUST say the product firmware cannot run this test and MUST include the pcba-test upload command. + +#### Scenario: Script names the firmware +- **WHEN** the script starts +- **THEN** it shows The ESP32 must already run the test firmware. +- **AND** it shows https://github.com/jamro/tiny-engineer/blob/main/docs/hardware/pcba-test.md#get-an-esp32-with-the-test-firmware + +#### Scenario: Page names the firmware +- **WHEN** the page loads +- **THEN** it shows The ESP32 must already run the test firmware. +- **AND** it links https://github.com/jamro/tiny-engineer/blob/main/docs/hardware/pcba-test.md#get-an-esp32-with-the-test-firmware + +#### Scenario: Card explains how to get one +- **WHEN** the operator reads how to get the test firmware +- **THEN** the card says the product firmware cannot run this test +- **AND** it includes pio run -e pcba-test -t upload diff --git a/platformio.ini b/platformio.ini index 58d83047..1a3d95d1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -88,3 +88,17 @@ build_src_filter = -<*> +<../examples/expression-demo/main.cpp> test_ignore = * + +; Opt-in PCBA session. Building does not upload or change the default robot env. +[env:pcba-test] +platform = ${env:esp32-c3-devkitm-1.platform} +board = ${env:esp32-c3-devkitm-1.board} +framework = ${env:esp32-c3-devkitm-1.framework} +monitor_speed = ${env:esp32-c3-devkitm-1.monitor_speed} +build_flags = ${env:esp32-c3-devkitm-1.build_flags} +lib_deps = + adafruit/Adafruit PWM Servo Driver Library +build_src_filter = + -<*> + +<../examples/pcba-test/main.cpp> +test_ignore = * diff --git a/scripts/pcba_test.py b/scripts/pcba_test.py new file mode 100644 index 00000000..7f5d4ae4 --- /dev/null +++ b/scripts/pcba_test.py @@ -0,0 +1,440 @@ +#!/usr/bin/env python3 +"""PCBA host for the golden-module line protocol. Does not flash.""" + +import argparse +import os +import sys +import time + +WEB_TESTER = "https://tinyengineer.dev/pcba_test" +BOARD_REV = "Compatible with main control board rev 3." +FIRMWARE_LINE = "The ESP32 must already run the test firmware." +FIRMWARE_DOC = ( + "https://github.com/jamro/tiny-engineer/blob/main/docs/hardware/" + "pcba-test.md#get-an-esp32-with-the-test-firmware" +) +ESPRESSIF_VID = 0x303A + + +class PortLost(OSError): + pass + + +HEADER = """\ + .---. + | o o | TINY ENGINEER + | ^ | PCBA test + '---' +""" + + +def startup_text(): + return "%sWeb tester: %s\n%s\n%s\nHow to get one: %s\n" % ( + HEADER, + WEB_TESTER, + BOARD_REV, + FIRMWARE_LINE, + FIRMWARE_DOC, + ) + + +def prep_text(): + return card( + [ + "PREPARE", + "", + "1 Take the board.", + "2 Plug in the ESP32.", + " Match the USB mark on the board.", + "3 Plug the speaker into the JST speaker port.", + "4 Connect USB to the board.", + " Use the board socket, not the ESP32.", + ] + ) + + +READY_COACH = "The board is connected." +BANNER_COACH = ( + "The module LED stays steady white while the test image has power. " + "That lamp is not the board verdict." +) +PCA_COACH = "PCA9685 did not answer. This board fails before the speaker test." +WAIT_KEYS = ("Enter = good", "n = bad") +TONE_KEYS = ("+ = louder", "- = quieter") +TONE_LEVELS = 10 +TONE_LEVEL_DEFAULT = 6 +FAIL_KEYS = ( + "r = try this step again", + "c = keep the fail, go on", + "a = start this board over", +) +STEPS = ( + ("tone", "SPEAKER", "Speaker on the JST speaker port. Expect a repeating tone."), + ( + "servos", + "SERVOS", + "Signal is on all five servo outputs. Take the testing board. Plug it into one column at a time. Columns are HEAD, NECK, ARM L, ARM R, and BODY. Check the LEDs on every column. Red stays on and yellow pulses.", + ), + ( + "oe", + "OE OUTPUT ENABLE", + "The red LED stays on. Only the yellow LED goes out. No need to test each servo column.", + ), +) +STEP_TEXT = {name: sentence for name, _title, sentence in STEPS} +STEP_TITLE = {name: title for name, title, _sentence in STEPS} + + +def new_state(): + return {"shown": "", "menu": False, "volume": TONE_LEVEL_DEFAULT, "step": ""} + + +def card(lines, footer=None): + body = list(lines) + foot = list(footer or []) + width = max(len(line) for line in body + foot) + rule = "─" * (width + 2) + framed = ["┌" + rule + "┐"] + for line in body: + framed.append("│ " + line.ljust(width) + " │") + if foot: + framed.append("├" + rule + "┤") + for line in foot: + framed.append("│ " + line.ljust(width) + " │") + framed.append("└" + rule + "┘") + return "\n".join(framed) + + +def prose(text): + return [part if part.endswith(".") else part + "." for part in text.split(". ") if part] + + +def coach_step(name, volume=None): + index = next(i for i, (step, _title, _sentence) in enumerate(STEPS) if step == name) + lines = ["%d/%d %s" % (index + 1, len(STEPS), STEP_TITLE[name]), ""] + lines.extend(prose(STEP_TEXT[name])) + footer = WAIT_KEYS + if name == "tone": + level = TONE_LEVEL_DEFAULT if volume is None else volume + lines.append("Volume %d/%d." % (level, TONE_LEVELS)) + footer = TONE_KEYS + WAIT_KEYS + return card(lines, footer=footer) + + +def coach_fail(name, token): + if name == "pca": + return card(["PCA", ""] + prose(PCA_COACH) + ["", token]) + if name not in STEP_TEXT: + return token + lines = [STEP_TITLE[name], ""] + lines.extend(prose(STEP_TEXT[name])) + lines.append("") + lines.extend(FAIL_KEYS) + lines.extend(["", token]) + return card(lines) + + +def coach_ready(): + return card(["CONNECTED", ""] + prose(READY_COACH), footer=("Enter = continue",)) + + +def feed_line(state, line): + text = line.strip() + if not text: + return False + if text.startswith("STEP "): + name = text[5:] + state["step"] = name + state["shown"] = coach_step(name, state.get("volume")) if name in STEP_TEXT else text + state["menu"] = False + elif text.startswith("VOL "): + try: + level = int(text[4:]) + except ValueError: + return False + if level < 1 or level > TONE_LEVELS: + return False + state["volume"] = level + if state.get("step") == "tone": + state["shown"] = coach_step("tone", level) + state["menu"] = False + elif text.startswith("FAIL "): + name = text[5:] + state["shown"] = coach_fail(name, text) + state["menu"] = name in STEP_TEXT + elif text.startswith("RESULT "): + state["shown"] = card(["BOARD", "", text, "", next_action_text().strip()]) + state["menu"] = False + elif text == "READY": + state["shown"] = coach_ready() + state["menu"] = False + elif text.startswith("PCBA-TEST"): + state["shown"] = "%s\n%s" % (BANNER_COACH, text) + state["menu"] = False + elif text == "STOP": + state["shown"] = text + state["menu"] = False + else: + return False + state["echo"] = not text.startswith("PCBA-TEST") + return True + + +def display(state): + return state["shown"] + + +KEY_CHARS = frozenset("nrcaq+-=") + + +def token_for_char(ch): + if ch in ("\n", "\r"): + return "enter" + if ch in KEY_CHARS: + return ch + return None + + +def windows_key(ch, read_extended): + if ch in ("\x00", "\xe0"): + read_extended() + return None + return token_for_char(ch) + + +def encode_key(key): + if key == "enter": + return b"\n" + if key in ("+", "="): + return b"+" + if key in ("n", "r", "c", "a", "q", "-"): + return key.encode("ascii") + raise ValueError(key) + + +def next_action_text(): + return "unplug USB for the next board, or q to stop\n" + + +def read_chunk(port): + """Bytes already buffered, or one fake-port line. Does not block the keyboard. + + macOS CDC often leaves in_waiting at 0 until read() polls the kernel, so a + select-then-in_waiting loop never sees the banner. + """ + if not hasattr(port, "fileno"): + return port.readline() + try: + waiting = getattr(port, "in_waiting", 0) or 0 + if waiting: + return port.read(waiting) + timeout = port.timeout + port.timeout = 0.05 + try: + return port.read(4096) or b"" + finally: + port.timeout = timeout + except OSError as exc: + # macOS pyserial raises when a CDC read is signaled but returns nothing. + if "no data" in str(exc).lower(): + return b"" + raise PortLost(str(exc)) from exc + + +def serve(open_port, out, key_source=None, max_lines=None): + """Read device lines until STOP, max_lines protocol lines, or the opener is done. + + open_port() returns a port or None. A port raises PortLost when the cable drops. + """ + if key_source is None: + key_source = lambda: None + state = new_state() + out.write(startup_text()) + out.write("\n") + out.write(prep_text()) + out.write("\n") + out.flush() + port = None + pending = b"" + seen = 0 + painted = {"screen": None} + + def emit(screen): + if screen == painted["screen"]: + return + painted["screen"] = screen + out.write("\n") + out.write(screen + "\n") + out.flush() + + while max_lines is None or seen < max_lines: + if port is None: + try: + port = open_port() + except PortLost: + port = None + if port is None: + if max_lines is not None: + break + time.sleep(0.3) + continue + try: + raw = read_chunk(port) + except PortLost: + port = None + pending = b"" + continue + if raw: + pending += raw + changed = False + while b"\n" in pending and (max_lines is None or seen < max_lines): + chunk, pending = pending.split(b"\n", 1) + line = chunk.decode("utf-8", "replace") + if not feed_line(state, line): + continue + text = line.strip() + changed = True + seen += 1 + if text == "STOP": + if changed and state.get("echo", True): + emit(display(state)) + return state + if changed and state.get("echo", True): + emit(display(state)) + key = key_source() + if key and port is not None: + try: + port.write(encode_key(key)) + except OSError: + port = None + pending = b"" + if max_lines is None and not raw and key is None and not hasattr(port, "fileno"): + time.sleep(0.02) + return state + + +def cdc_devices(list_ports): + found = [] + for port in list_ports: + if getattr(port, "vid", None) == ESPRESSIF_VID and getattr(port, "device", None): + found.append(port.device) + return found + + +def port_open_message(exc): + text = str(exc).lower() + if "busy" in text or "permission" in text or "access is denied" in text: + return ( + "Port is busy. Close the other program using the board. " + "This script will keep waiting." + ) + return "Port did not open. Unplug the board and plug it back in." + + +def open_cdc(explicit, list_ports, serial_module): + if explicit: + return open_serial(serial_module, explicit) + devices = cdc_devices(list_ports()) + if len(devices) != 1: + return None, devices + return open_serial(serial_module, devices[0]), devices + + +def open_serial(serial_module, device): + port = serial_module.Serial() + port.port = device + port.baudrate = 115200 + port.timeout = 0 + port.write_timeout = 1 + # Leave DTR and RTS low so opening the port does not reset the module. + port.dtr = False + port.rts = False + port.open() + # One byte marks the USB link up. It is not Enter. + try: + port.write(b"?") + except OSError: + pass + return port + + +def read_key_nonblocking(): + if os.name == "nt": + import msvcrt + + if not msvcrt.kbhit(): + return None + return windows_key(msvcrt.getwch(), msvcrt.getwch) + import select + + if not select.select([sys.stdin], [], [], 0)[0]: + return None + return token_for_char(sys.stdin.read(1)) + + +def configure_stdin(): + if os.name == "nt" or not sys.stdin.isatty(): + return None + import termios + import tty + + fd = sys.stdin.fileno() + previous = termios.tcgetattr(fd) + tty.setcbreak(fd) + return previous + + +def restore_stdin(previous): + if previous is None: + return + import termios + + termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, previous) + + +def main(argv=None): + parser = argparse.ArgumentParser(description="PCBA test host") + parser.add_argument("--port", help="CDC serial device") + args = parser.parse_args(argv) + try: + import serial + from serial.tools import list_ports + except ImportError: + sys.stderr.write("Install pyserial: python3 -m pip install pyserial\n") + return 1 + + announced = {"waiting": False} + + def open_port(): + try: + opened, devices = open_cdc(args.port, list_ports.comports, serial) + except serial.SerialException as exc: + text = port_open_message(exc) + if announced.get("error") != text: + sys.stdout.write("\n%s\n" % text) + sys.stdout.flush() + announced["error"] = text + return None + if opened is not None: + announced["waiting"] = False + announced["error"] = None + return opened + if not announced["waiting"]: + if devices: + sys.stdout.write("CDC ports: %s\n" % ", ".join(devices)) + sys.stdout.flush() + announced["waiting"] = True + return None + + previous = configure_stdin() + try: + serve(open_port, sys.stdout, key_source=read_key_nonblocking) + except KeyboardInterrupt: + return 0 + finally: + restore_stdin(previous) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/test_pcba_test.py b/scripts/test_pcba_test.py new file mode 100644 index 00000000..ab108173 --- /dev/null +++ b/scripts/test_pcba_test.py @@ -0,0 +1,341 @@ +#!/usr/bin/env python3 +"""Host checks for scripts/pcba_test.py. Does not flash.""" + +import io +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from pcba_test import ( # noqa: E402 + BOARD_REV, + FIRMWARE_DOC, + FIRMWARE_LINE, + PortLost, + STEPS, + WEB_TESTER, + display, + port_open_message, + encode_key, + windows_key, + feed_line, + new_state, + next_action_text, + serve, + prep_text, + startup_text, +) + +ROOT = Path(__file__).resolve().parents[1] +CARD = ROOT / "docs" / "hardware" / "pcba-test.md" + + +class FakePort: + def __init__(self, lines, drop_when_empty=False): + self.lines = list(lines) + self.drop_when_empty = drop_when_empty + self.writes = [] + + def readline(self): + if not self.lines: + if self.drop_when_empty: + raise PortLost("cable") + return b"" + return (self.lines.pop(0) + "\n").encode() + + def write(self, data): + self.writes.append(data) + + +def play(script): + state = new_state() + shown = [] + writes = [] + for kind, value in script: + if kind == "line": + feed_line(state, value) + shown.append(display(state)) + else: + writes.append(encode_key(value)) + return shown, writes, state + + +class HostTests(unittest.TestCase): + def test_windows_key_ignores_unmapped_and_extended(self): + extra = [] + + def read_extended(): + extra.append("scan") + + for ch in ("x", " ", "\x03"): + extra.clear() + self.assertIsNone(windows_key(ch, read_extended)) + self.assertEqual(extra, []) + + for prefix in ("\x00", "\xe0"): + extra.clear() + self.assertIsNone(windows_key(prefix, read_extended)) + self.assertEqual(extra, ["scan"]) + + self.assertEqual(windows_key("\r", read_extended), "enter") + self.assertEqual(windows_key("n", read_extended), "n") + self.assertEqual(windows_key("+", read_extended), "+") + + def test_busy_port_waits_without_a_traceback(self): + self.assertIn("Port is busy", port_open_message(OSError("Resource busy"))) + self.assertIn("keep waiting", port_open_message(OSError("[Errno 16] Resource busy"))) + + def open_port(): + raise PortLost("could not open port /dev/cu.usbmodem: Resource busy") + + out = io.StringIO() + serve(open_port, out, max_lines=1) + self.assertIn("PREPARE", out.getvalue()) + self.assertNotIn("Traceback", out.getvalue()) + + def test_startup_names_web_tester(self): + self.assertIn(WEB_TESTER, startup_text()) + self.assertIn(BOARD_REV, startup_text()) + self.assertIn(FIRMWARE_LINE, startup_text()) + self.assertIn(FIRMWARE_DOC, startup_text()) + prep = prep_text() + self.assertIn("Take the board.", prep) + self.assertIn("Match the USB mark on the board.", prep) + self.assertIn("Plug the speaker into the JST speaker port.", prep) + self.assertIn("Use the board socket, not the ESP32.", prep) + + def test_connected_board_waits_for_confirmation(self): + port = FakePort(["READY"]) + + def open_port(): + return port + + out = io.StringIO() + serve(open_port, out, max_lines=1) + log = out.getvalue() + self.assertEqual(port.writes, []) + self.assertIn("The board is connected.", log) + self.assertIn("Enter = continue", log) + self.assertNotIn("1/3", log) + + def test_ready_waits_for_the_operator(self): + shown, _writes, _state = play([("line", "READY")]) + self.assertIn("CONNECTED", shown[0]) + self.assertIn("The board is connected.", shown[0]) + self.assertIn("Enter = continue", shown[0]) + self.assertNotIn("speaker", shown[0].lower()) + self.assertNotIn("1/3", shown[0]) + + def test_accept_path_and_fail_menu(self): + shown, writes, state = play( + [ + ("line", "PCBA-TEST v1"), + ("line", "STEP tone"), + ("key", "enter"), + ("line", "STEP servos"), + ("line", "STEP oe"), + ("line", "RESULT PASS"), + ] + ) + self.assertIn("1/3", shown[1]) + self.assertIn("SPEAKER", shown[1]) + self.assertIn("Speaker on the JST speaker port.", shown[1]) + self.assertNotIn("Lamps do not count.", shown[1]) + self.assertIn("Expect a repeating tone.", shown[1]) + self.assertNotIn("STEP tone", shown[1]) + self.assertIn("Volume 6/10.", shown[1]) + self.assertIn("+ = louder", shown[1]) + self.assertIn("- = quieter", shown[1]) + self.assertIn("Enter = good", shown[1]) + self.assertIn("n = bad", shown[1]) + self.assertIn("├", shown[1]) + self.assertIn("2/3", shown[2]) + self.assertIn("SERVOS", shown[2]) + self.assertNotIn("+ = louder", shown[2]) + self.assertNotIn("Volume", shown[2]) + self.assertIn("Signal is on all five servo outputs.", shown[2]) + self.assertIn("Plug it into one column at a time.", shown[2]) + self.assertIn("Columns are HEAD, NECK, ARM L, ARM R, and BODY.", shown[2]) + self.assertIn("Check the LEDs on every column.", shown[2]) + self.assertIn("Red stays on and yellow pulses.", shown[2]) + self.assertNotIn("No key between columns.", shown[2]) + self.assertNotIn("STEP servos", shown[2]) + self.assertIn("3/3", shown[3]) + self.assertIn("OE OUTPUT ENABLE", shown[3]) + self.assertIn("The red LED stays on.", shown[3]) + self.assertIn("Only the yellow LED goes out.", shown[3]) + self.assertIn("No need to test each servo column.", shown[3]) + self.assertNotIn("PWM", shown[3]) + self.assertNotIn("5 V", shown[3]) + self.assertNotIn("STEP oe", shown[3]) + self.assertIn("├", shown[2]) + self.assertEqual(writes, [b"\n"]) + self.assertEqual(encode_key("+"), b"+") + self.assertEqual(encode_key("="), b"+") + self.assertEqual(encode_key("-"), b"-") + louder, _writes, louder_state = play([("line", "STEP tone"), ("line", "VOL 8")]) + self.assertIn("Volume 8/10.", louder[1]) + self.assertEqual(louder_state["volume"], 8) + self.assertIn("RESULT PASS", shown[-1]) + + shown, writes, _state = play( + [ + ("line", "STEP servos"), + ("key", "n"), + ("line", "FAIL servos"), + ("key", "c"), + ] + ) + self.assertIn("2/3", shown[0]) + self.assertNotIn("STEP servos", shown[0]) + self.assertIn("Red stays on and yellow pulses.", shown[1]) + self.assertIn("r = try this step again", shown[1]) + self.assertIn("c = keep the fail, go on", shown[1]) + self.assertIn("a = start this board over", shown[1]) + self.assertIn("FAIL servos", shown[1]) + self.assertEqual(writes, [b"n", b"c"]) + + banner, _writes, _state = play([("line", "PCBA-TEST v1")]) + self.assertIn( + "The module LED stays steady white while the test image has power. That lamp is not the board verdict.", + banner[0], + ) + self.assertIn("PCBA-TEST v1", banner[0]) + + pca, _writes, _state = play([("line", "FAIL pca")]) + self.assertIn("PCA9685 did not answer.", pca[0]) + self.assertIn("This board fails before the speaker test.", pca[0]) + self.assertNotIn("r = try this step again", pca[0]) + self.assertNotIn("c = keep the fail, go on", pca[0]) + self.assertNotIn("a = start this board over", pca[0]) + + page = (ROOT / "web" / "js" / "pcba-test.mjs").read_text() + script = (ROOT / "scripts" / "pcba_test.py").read_text() + for _name, _title, sentence in STEPS: + self.assertIn(sentence, page) + self.assertIn(sentence, script) + + def test_reconnect_shows_the_next_session(self): + ports = [ + FakePort(["RESULT FAIL tone body"], drop_when_empty=True), + FakePort(["PCBA-TEST v1"]), + ] + + def open_port(): + if not ports: + return None + return ports.pop(0) + + out = io.StringIO() + state = serve(open_port, out, max_lines=2) + log = out.getvalue() + + self.assertIn("RESULT FAIL tone body", log) + self.assertIn("PCBA-TEST v1", state["shown"]) + + def test_write_oserror_opens_the_next_port(self): + class WriteFails(FakePort): + def write(self, data): + raise OSError("device disconnected") + + ports = [ + WriteFails(["READY"]), + FakePort(["PCBA-TEST v1"]), + ] + keys = iter(["q"]) + + def open_port(): + if not ports: + return None + return ports.pop(0) + + def key_source(): + return next(keys, None) + + out = io.StringIO() + state = serve(open_port, out, key_source=key_source, max_lines=2) + + self.assertNotIn("Traceback", out.getvalue()) + self.assertIn("PCBA-TEST v1", state["shown"]) + + def test_result_says_what_to_do_and_stop_returns(self): + port = FakePort(["RESULT PASS", "STOP"]) + + def open_port(): + return port + + out = io.StringIO() + serve(open_port, out) + log = out.getvalue() + self.assertIn("RESULT PASS", log) + self.assertIn(next_action_text().strip(), log) + self.assertIn("STOP", log) + + def test_scripted_sessions_match_the_card(self): + pass_shown, _writes, _state = play( + [ + ("line", "STEP tone"), + ("key", "enter"), + ("line", "STEP servos"), + ("key", "enter"), + ("line", "RESULT PASS"), + ] + ) + fail_shown, fail_writes, _state = play( + [ + ("line", "STEP tone"), + ("key", "n"), + ("line", "FAIL tone"), + ("key", "c"), + ("line", "STEP servos"), + ("key", "n"), + ("line", "FAIL servos"), + ("key", "c"), + ("line", "RESULT FAIL tone servos"), + ] + ) + self.assertIn("RESULT PASS", pass_shown[-1]) + self.assertIn("RESULT FAIL tone servos", fail_shown[-1]) + self.assertIn(b"n", fail_writes) + self.assertIn(b"c", fail_writes) + + card = CARD.read_text() + for phrase in ( + "tone", + "servos", + "HEAD", + "NECK", + "ARM L", + "ARM R", + "BODY", + "testing board", + "oe", + "+ = louder", + "- = quieter", + "Only the yellow LED goes out", + "No need to test each servo column", + "RESULT PASS", + "RESULT FAIL tone servos", + "python3 scripts/pcba_test.py", + WEB_TESTER, + "main control board rev 3", + "Red stays on and yellow pulses", + "square pad as GND", + "J1", + "golden module stays with the tester", + "The ESP32 must already run the test firmware", + "pio run -e pcba-test -t upload", + "The product firmware cannot run this test", + "J2", + "OLED", + "Wi-Fi", + "2 A load", + ): + self.assertIn(phrase, card) + for key in ("r", "c", "a", "n", "q"): + self.assertIn("`%s`" % key, card) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_pcba_web.mjs b/scripts/test_pcba_web.mjs new file mode 100644 index 00000000..89f23b23 --- /dev/null +++ b/scripts/test_pcba_web.mjs @@ -0,0 +1,267 @@ +import assert from "node:assert/strict"; +import { readdir, readFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { + PYTHON_COMMAND, + canTest, + createHost, + PORT_LOST, + activatePort, + releasePort, + display, + encodeKey, + acceptKeyEvent, + keyToken, + feed, + grant, + connectTarget, + prepText, + splitCard, + onDisconnect, + onPortReturn, +} from "../web/js/pcba-test.mjs"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const webRoot = path.join(root, "web"); + +function assertGate() { + assert.equal( + canTest({ isSecureContext: true, navigator: {} }), + false + ); + assert.equal( + canTest({ isSecureContext: false, navigator: { serial: {} } }), + false + ); + assert.equal( + canTest({ isSecureContext: true, navigator: { serial: {} } }), + true + ); +} + +async function walkHtml(dir, found = []) { + const entries = await readdir(dir, { withFileTypes: true }); + for (const entry of entries) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + await walkHtml(full, found); + } else if (entry.name.endsWith(".html")) { + found.push(full); + } + } + return found; +} + +async function assertNoInboundLinks() { + const files = await walkHtml(webRoot); + const page = path.join(webRoot, "pcba_test", "index.html"); + for (const file of files) { + if (file === page) { + continue; + } + const text = await readFile(file, "utf8"); + assert.equal(text.includes("pcba_test"), false, file); + } + const html = await readFile(page, "utf8"); + assert.match(html, /Compatible with main control board rev 3/); + assert.match(html, /The ESP32 must already run the test firmware/); + assert.match( + html, + /https:\/\/github.com\/jamro\/tiny-engineer\/blob\/main\/docs\/hardware\/pcba-test\.md#get-an-esp32-with-the-test-firmware/ + ); + assert.match(html, /This browser cannot run the test/); + assert.match( + html, + /https:\/\/github.com\/jamro\/tiny-engineer\/blob\/main\/docs\/hardware\/pcba-test\.md/ + ); + assert.match(html, new RegExp(PYTHON_COMMAND.replace(/[.]/g, "\\."))); + assert.match(html, /id="connect"[^>]*hidden/); + assert.match(html, /id="disconnect"[^>]*hidden/); + assert.equal(html.includes("Save CSV"), false); + assert.equal(html.includes('id="save"'), false); +} + +function assertSession() { + const host = createHost(); + grant(host); + feed(host, "READY"); + assert.match(display(host), /CONNECTED/); + assert.match(display(host), /The board is connected/); + assert.match(display(host), /Enter = continue/); + assert.doesNotMatch(display(host), /speaker/i); + assert.doesNotMatch(display(host), /1\/3/); + feed(host, "PCBA-TEST v1"); + assert.match(display(host), /The module LED stays steady white while the test image has power/); + assert.match(display(host), /That lamp is not the board verdict/); + assert.match(display(host), /PCBA-TEST v1/); + feed(host, "STEP tone"); + assert.match(display(host), /1\/3/); + assert.match(display(host), /SPEAKER/); + assert.match(display(host), /Speaker on the JST speaker port/); + assert.doesNotMatch(display(host), /Lamps do not count/); + assert.match(display(host), /Expect a repeating tone/); + assert.match(display(host), /Volume 6\/10/); + assert.match(display(host), /\+ = louder/); + assert.match(display(host), /- = quieter/); + assert.match(display(host), /Enter = good/); + assert.match(display(host), /n = bad/); + assert.doesNotMatch(display(host), /STEP tone/); + feed(host, "STEP servos"); + assert.match(display(host), /2\/3/); + assert.match(display(host), /SERVOS/); + assert.doesNotMatch(display(host), /\+ = louder/); + assert.doesNotMatch(display(host), /Volume/); + assert.match(display(host), /Signal is on all five servo outputs/); + assert.match(display(host), /Plug it into one column at a time/); + assert.match(display(host), /Columns are HEAD, NECK, ARM L, ARM R, and BODY/); + assert.match(display(host), /Check the LEDs on every column/); + assert.match(display(host), /Red stays on and yellow pulses/); + assert.doesNotMatch(display(host), /No key between columns/); + assert.doesNotMatch(display(host), /STEP servos/); + feed(host, "STEP oe"); + assert.match(display(host), /3\/3/); + assert.match(display(host), /OE OUTPUT ENABLE/); + assert.match(display(host), /The red LED stays on/); + assert.match(display(host), /Only the yellow LED goes out/); + assert.match(display(host), /No need to test each servo column/); + assert.doesNotMatch(display(host), /PWM/); + assert.doesNotMatch(display(host), /5 V/); + assert.doesNotMatch(display(host), /STEP oe/); + assert.equal(keyToken("Enter"), "enter"); + assert.equal(encodeKey(keyToken("Enter")), "\n"); + assert.equal(encodeKey(keyToken("+")), "+"); + assert.equal(encodeKey(keyToken("n")), "n"); + assert.equal(encodeKey("enter"), "\n"); + assert.equal(encodeKey("n"), "n"); + assert.equal(encodeKey("+"), "+"); + assert.equal(encodeKey("="), "+"); + assert.equal(encodeKey("-"), "-"); + assert.equal(acceptKeyEvent({ key: "Enter", repeat: true }), ""); + assert.equal(acceptKeyEvent({ key: "a", ctrlKey: true }), ""); + assert.equal(acceptKeyEvent({ key: "a", metaKey: true }), ""); + assert.equal(acceptKeyEvent({ key: "r", altKey: true }), ""); + assert.equal(acceptKeyEvent({ key: "Enter" }), "\n"); + assert.equal(acceptKeyEvent({ key: "a" }), "a"); + assert.equal(acceptKeyEvent({ key: "+", shiftKey: true }), "+"); + feed(host, "STEP tone"); + feed(host, "VOL 8"); + assert.match(display(host), /Volume 8\/10/); + feed(host, "FAIL servos"); + assert.match(display(host), /Red stays on and yellow pulses/); + assert.match(display(host), /r = try this step again/); + assert.match(display(host), /c = keep the fail, go on/); + assert.match(display(host), /a = start this board over/); + assert.match(display(host), /FAIL servos/); + feed(host, "FAIL pca"); + assert.match(display(host), /PCA9685 did not answer/); + assert.match(display(host), /This board fails before the speaker test/); + assert.doesNotMatch(display(host), /r = try this step again/); + assert.doesNotMatch(display(host), /c = keep the fail, go on/); + assert.doesNotMatch(display(host), /a = start this board over/); + feed(host, "RESULT PASS"); + assert.match(display(host), /RESULT PASS/); + const asks = host.grantAsks; + onDisconnect(host); + assert.equal(host.connected, false); + assert.equal(onPortReturn(host), true); + assert.equal(host.grantAsks, asks); + assert.equal(host.connected, true); + + const fresh = createHost(); + assert.equal(onPortReturn(fresh), false); + assert.equal(fresh.grantAsks, 1); +} + +function assertConnectAsksAgain() { + const port = { readable: {} }; + assert.equal(connectTarget(port, true), port); + assert.equal(connectTarget(port, false), null); +} + +function assertCardSplit() { + const prep = splitCard(prepText()); + assert.equal(prep.title, "PREPARE"); + assert.equal(prep.keys.length, 0); + assert.equal(prep.body[0].n, "1"); + assert.equal(prep.body[0].text, "Take the board."); + const tone = splitCard( + "1/3 SPEAKER\n\nSpeaker on the JST speaker port.\nExpect a repeating tone.\nVolume 6/10.\n\n+ = louder\n- = quieter\nEnter = good\nn = bad" + ); + assert.equal(tone.title, "1/3 SPEAKER"); + assert.deepEqual(tone.keys.map((item) => item.key), ["+", "-", "Enter", "n"]); + assert.equal(tone.body.some((item) => item.text.startsWith("Volume")), true); +} + +async function assertActivatePort() { + const prep = prepText(); + assert.match(prep, /PREPARE/); + assert.match(prep, /Take the board/); + assert.match(prep, /JST speaker port/); + assert.match(prep, /not the ESP32/); + const signals = []; + const writes = []; + let closed = 0; + const port = { + readable: {}, + writable: { + getWriter() { + return { + async write(data) { + writes.push(Array.from(data)); + }, + releaseLock() {}, + }; + }, + }, + async open() { + this.readable = {}; + }, + async close() { + closed += 1; + this.readable = null; + this.writable = this.writable; + }, + async setSignals(value) { + signals.push(value); + }, + }; + const released = { closed: 0, cancelled: 0 }; + const live = { + readable: {}, + writable: {}, + async close() { + released.closed += 1; + this.readable = null; + this.writable = null; + }, + }; + const session = { + reader: { + async cancel() { + released.cancelled += 1; + }, + releaseLock() {}, + }, + }; + await releasePort(live, session); + assert.equal(released.cancelled, 1); + assert.equal(released.closed, 1); + assert.equal(session.closing, true); + assert.equal(session.connected, false); + assert.equal(session.reader, null); + await activatePort(port); + assert.equal(closed, 1); + assert.deepEqual(signals, [{ dataTerminalReady: false, requestToSend: false }]); + assert.deepEqual(writes, [[0x3f]]); + assert.match(PORT_LOST, /press Connect again/); +} + +assertGate(); +await assertNoInboundLinks(); +assertSession(); +assertCardSplit(); +assertConnectAsksAgain(); +await assertActivatePort(); +console.log("pcba web ok"); diff --git a/web/js/pcba-test.mjs b/web/js/pcba-test.mjs new file mode 100644 index 00000000..a8f34aa6 --- /dev/null +++ b/web/js/pcba-test.mjs @@ -0,0 +1,732 @@ +export const WEB_TESTER = "https://tinyengineer.dev/pcba_test"; +export const PYTHON_COMMAND = "python3 scripts/pcba_test.py"; +export const INCOMPATIBLE = + "This browser cannot run the test. Use " + PYTHON_COMMAND; + +export function canTest(scope) { + return Boolean( + scope && + scope.isSecureContext && + scope.navigator && + scope.navigator.serial + ); +} + +export function createHost() { + return { + shown: "", + menu: false, + volume: 6, + step: "", + granted: false, + connected: false, + grantAsks: 0, + heard: false, + opening: false, + reopenTries: 0, + session: 0, + }; +} + +export function connectTarget(port, connected) { + return connected ? port : null; +} + +export function prepText() { + return [ + "PREPARE", + "", + "1 Take the board.", + "2 Plug in the ESP32.", + " Match the USB mark on the board.", + "3 Plug the speaker into the JST speaker port.", + "4 Connect USB to the board.", + " Use the board socket, not the ESP32.", + ].join("\n"); +} + +export const PORT_LOST = "Port lost. Unplug the board and press Connect again."; + +export async function releasePort(port, host) { + if (host) { + host.closing = true; + host.connected = false; + } + const reader = host && host.reader; + if (reader) { + try { + await reader.cancel(); + } catch (_err) { + // The stream already ended. + } + try { + reader.releaseLock(); + } catch (_err) { + // Lock already released. + } + host.reader = null; + } + if (port && (port.readable || port.writable)) { + try { + await port.close(); + } catch (_err) { + // The port dropped before close. + } + } +} + +export async function activatePort(port) { + if (port.readable || port.writable) { + try { + await port.close(); + } catch (_err) { + // Already closed after a USB reset. + } + } + await port.open({ baudRate: 115200 }); + if (port.setSignals) { + await port.setSignals({ dataTerminalReady: false, requestToSend: false }); + } + const writer = port.writable.getWriter(); + try { + await writer.write(new Uint8Array([0x3f])); + } finally { + writer.releaseLock(); + } +} + +export function grant(host) { + host.granted = true; + host.connected = true; +} + +export function onDisconnect(host) { + host.connected = false; +} + +export function onPortReturn(host) { + if (!host.granted) { + host.grantAsks += 1; + return false; + } + host.connected = true; + return true; +} + +const READY_COACH = "The board is connected."; +const BANNER_COACH = + "The module LED stays steady white while the test image has power. That lamp is not the board verdict."; +const PCA_COACH = "PCA9685 did not answer. This board fails before the speaker test."; +const WAIT_KEYS = "Enter = good\nn = bad"; +const TONE_KEYS = "+ = louder\n- = quieter"; +const TONE_LEVELS = 10; +const TONE_LEVEL_DEFAULT = 6; +const FAIL_KEYS = "r = try this step again\nc = keep the fail, go on\na = start this board over"; +const STEPS = [ + ["tone", "SPEAKER", "Speaker on the JST speaker port. Expect a repeating tone."], + [ + "servos", + "SERVOS", + "Signal is on all five servo outputs. Take the testing board. Plug it into one column at a time. Columns are HEAD, NECK, ARM L, ARM R, and BODY. Check the LEDs on every column. Red stays on and yellow pulses.", + ], + [ + "oe", + "OE OUTPUT ENABLE", + "The red LED stays on. Only the yellow LED goes out. No need to test each servo column.", + ], +]; + +function stepSentence(name) { + const found = STEPS.find((row) => row[0] === name); + return found ? found[2] : ""; +} + +function stepTitle(name) { + const found = STEPS.find((row) => row[0] === name); + return found ? found[1] : name; +} + +function prose(text) { + return text + .split(". ") + .filter(Boolean) + .map((part) => (part.endsWith(".") ? part : part + ".")) + .join("\n"); +} + +function coachStep(name, volume) { + const index = STEPS.findIndex((row) => row[0] === name); + const body = `${index + 1}/${STEPS.length} ${stepTitle(name)}\n\n${prose(stepSentence(name))}`; + if (name !== "tone") { + return `${body}\n\n${WAIT_KEYS}`; + } + const level = volume == null ? TONE_LEVEL_DEFAULT : volume; + return `${body}\nVolume ${level}/${TONE_LEVELS}.\n\n${TONE_KEYS}\n${WAIT_KEYS}`; +} + +function coachFail(name, token) { + if (name === "pca") { + return `PCA\n\n${prose(PCA_COACH)}\n\n${token}`; + } + const sentence = stepSentence(name); + if (!sentence) { + return token; + } + return `${stepTitle(name)}\n\n${prose(sentence)}\n\n${FAIL_KEYS}\n\n${token}`; +} + +export function feed(host, line) { + const text = String(line || "").trim(); + if (!text) { + return false; + } + host.heard = true; + host.reopenTries = 0; + if (text.startsWith("STEP ")) { + const name = text.slice(5); + host.step = name; + host.shown = stepSentence(name) ? coachStep(name, host.volume) : text; + host.menu = false; + } else if (text.startsWith("VOL ")) { + const level = Number(text.slice(4)); + if (!Number.isInteger(level) || level < 1 || level > TONE_LEVELS) { + return false; + } + host.volume = level; + if (host.step === "tone") { + host.shown = coachStep("tone", level); + } + host.menu = false; + } else if (text.startsWith("FAIL ")) { + const name = text.slice(5); + host.shown = coachFail(name, text); + host.menu = Boolean(stepSentence(name)); + } else if (text.startsWith("RESULT ")) { + host.shown = text; + host.menu = false; + } else if (text === "READY") { + host.shown = `CONNECTED\n\n${prose(READY_COACH)}\n\nEnter = continue`; + host.menu = false; + } else if (text.startsWith("PCBA-TEST")) { + host.shown = `${BANNER_COACH}\n${text}`; + host.menu = false; + } else if (text === "STOP") { + host.shown = text; + host.menu = false; + } else { + return false; + } + return true; +} + +export function display(host) { + return host.shown; +} + +export function splitCard(text) { + const lines = String(text || "").split("\n"); + const keys = []; + const body = []; + let title = ""; + for (const raw of lines) { + const line = raw.trim(); + if (!line) { + continue; + } + const key = line.match(/^(\S+)\s+=\s+(.+)$/); + if (key && key[1].length <= 12 && !key[1].includes(".")) { + keys.push({ key: key[1], label: key[2] }); + continue; + } + if (!title) { + if (line.length > 48) { + title = "BOARD"; + body.push({ kind: "line", text: line }); + } else { + title = line; + } + continue; + } + const step = line.match(/^(\d+)\s{2,}(.+)$/); + if (step) { + body.push({ kind: "step", n: step[1], text: step[2] }); + continue; + } + if (raw.startsWith(" ")) { + body.push({ kind: "note", text: line }); + continue; + } + const kind = line === PORT_LOST || line.startsWith("FAIL ") || line.startsWith("RESULT FAIL") + ? "alert" + : "line"; + body.push({ kind, text: line }); + } + return { title: title || "PREPARE", body, keys }; +} + +export function keyToken(label) { + return label === "Enter" ? "enter" : label; +} + +export function encodeKey(key) { + if (key === "enter") { + return "\n"; + } + if (key === "+" || key === "=") { + return "+"; + } + if (key === "n" || key === "r" || key === "c" || key === "a" || key === "q" || key === "-") { + return key; + } + return ""; +} + +export function acceptKeyEvent(event) { + if (event.repeat || event.ctrlKey || event.metaKey || event.altKey) { + return ""; + } + const key = event.key === "Enter" ? "enter" : event.key; + return encodeKey(key); +} + +function ledClass(card, text) { + if (text.includes(PORT_LOST) || text.includes("RESULT FAIL") || text.includes("FAIL ")) { + return "led led-err"; + } + if (text.includes("RESULT PASS")) { + return "led led-ok"; + } + if (card.title === "CONNECTED" || /^\d\/\d/.test(card.title)) { + return "led led-ok led-pulse"; + } + return "led led-warn"; +} + +function lineRole(text) { + if (text.startsWith("Volume ")) { + return "meta"; + } + if (text.startsWith("No need")) { + return "aside"; + } + if (text.startsWith("Columns are ")) { + return "columns"; + } + if ( + text.startsWith("Expect ") || + text.startsWith("Check the LEDs") || + text.startsWith("Red stays") || + text.startsWith("The red LED") || + text.startsWith("Only the yellow") + ) { + return "look"; + } + return "line"; +} + +function columnList() { + const wrap = document.createElement("div"); + wrap.className = "pcba-columns"; + const caption = document.createElement("p"); + caption.className = "pcba-columns-label"; + caption.textContent = "Columns"; + const list = document.createElement("ol"); + for (const name of ["HEAD", "NECK", "ARM L", "ARM R", "BODY"]) { + const item = document.createElement("li"); + item.textContent = name; + list.appendChild(item); + } + wrap.append(caption, list); + return wrap; +} + +function paintBody(step, items) { + step.replaceChildren(); + let index = 0; + while (index < items.length) { + const item = items[index]; + if (item.kind === "step") { + const p = document.createElement("p"); + p.className = "pcba-step"; + const n = document.createElement("span"); + n.className = "pcba-step-n"; + n.textContent = item.n; + const label = document.createElement("span"); + label.textContent = item.text; + p.append(n, label); + step.appendChild(p); + index += 1; + continue; + } + const role = item.kind === "line" ? lineRole(item.text) : item.kind; + if (role === "look") { + const box = document.createElement("div"); + box.className = "pcba-look"; + while (index < items.length && items[index].kind === "line" && lineRole(items[index].text) === "look") { + const p = document.createElement("p"); + p.textContent = items[index].text; + box.appendChild(p); + index += 1; + } + step.appendChild(box); + continue; + } + if (role === "columns") { + step.appendChild(columnList()); + index += 1; + continue; + } + const p = document.createElement("p"); + p.className = role === "note" ? "pcba-note" : role === "aside" ? "pcba-aside" : role === "alert" ? "pcba-alert" : role === "meta" ? "pcba-meta" : "pcba-line"; + p.textContent = item.text; + step.appendChild(p); + index += 1; + } +} + +function render(host) { + const step = document.getElementById("step"); + const title = document.getElementById("pcba-title"); + const keys = document.getElementById("pcba-keys"); + const led = document.getElementById("pcba-led"); + const text = display(host) || prepText(); + const card = splitCard(text); + if (title) { + title.textContent = card.title; + } + if (step) { + const signature = card.body.map((item) => item.kind + "\n" + item.text).join("\n"); + if (step.dataset.sig !== signature) { + step.dataset.sig = signature; + paintBody(step, card.body); + } + } + if (keys) { + const signature = card.keys.map((item) => item.key + "\n" + item.label).join("\n") + + (host.connected ? "\n1" : "\n0"); + keys.hidden = card.keys.length === 0; + if (keys.dataset.sig !== signature) { + keys.dataset.sig = signature; + keys.replaceChildren(); + card.keys.forEach((item) => { + const row = document.createElement("p"); + const button = document.createElement("button"); + button.type = "button"; + button.className = "pcba-key"; + button.dataset.key = keyToken(item.key); + button.textContent = item.key; + button.disabled = !host.connected; + if (button.dataset.key === "enter") { + row.className = "pcba-key-default"; + } + row.append(button, document.createTextNode(item.label)); + keys.appendChild(row); + }); + } + } + if (led) { + led.className = ledClass(card, text); + } +} + +function delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function showConnect() { + if (typeof document === "undefined") { + return; + } + const connect = document.getElementById("connect"); + const disconnect = document.getElementById("disconnect"); + if (connect) { + connect.hidden = false; + connect.disabled = false; + } + if (disconnect) { + disconnect.hidden = true; + } +} + +function showLost(host) { + onDisconnect(host); + host.opening = false; + host.shown = `${prepText()}\n\n${PORT_LOST}`; + render(host); + showConnect(); +} + +function armWatch(host) { + clearTimeout(host.watch); + const session = host.session; + host.watch = setTimeout(() => { + if (!host.heard && host.session === session) { + showLost(host); + } + }, 2000); +} + +async function readLoop(port, host) { + const session = host.session; + let reader; + try { + reader = port.readable.getReader(); + host.reader = reader; + } catch (_err) { + if (host.session === session) { + await recover(port, host); + } + return; + } + const decoder = new TextDecoder(); + let buffer = ""; + let lost = false; + try { + while (true) { + const { value, done } = await reader.read(); + if (done) { + lost = true; + break; + } + buffer += decoder.decode(value, { stream: true }); + let nl = buffer.indexOf("\n"); + while (nl >= 0) { + const line = buffer.slice(0, nl).replace(/\r$/, ""); + buffer = buffer.slice(nl + 1); + feed(host, line); + render(host); + nl = buffer.indexOf("\n"); + } + } + } catch (_err) { + lost = true; + } finally { + if (host.reader === reader) { + host.reader = null; + } + try { + reader.releaseLock(); + } catch (_err) { + // Reader already released after the port dropped. + } + } + if (lost && !host.closing && host.session === session) { + await recover(port, host); + } +} + +function showDisconnect() { + if (typeof document === "undefined") { + return; + } + const connect = document.getElementById("connect"); + const disconnect = document.getElementById("disconnect"); + if (connect) { + connect.hidden = true; + } + if (disconnect) { + disconnect.hidden = false; + } +} + +async function recover(port, host) { + const session = host.session; + if (host.closing) { + return; + } + onDisconnect(host); + if (host.reopenTries >= 2) { + showLost(host); + return; + } + host.reopenTries += 1; + await delay(600); + if (host.session !== session || host.closing) { + return; + } + try { + await openGranted(port, host); + } catch (_err) { + if (host.session === session) { + await recover(port, host); + } + } +} + +async function openGranted(port, host) { + if (host.opening) { + return; + } + const session = host.session; + host.opening = true; + clearTimeout(host.watch); + host.heard = false; + try { + await activatePort(port); + } catch (err) { + if (host.session === session) { + host.connected = false; + host.opening = false; + } + throw err; + } + if (host.session !== session) { + return; + } + host.opening = false; + host.connected = true; + host.granted = true; + if (!host.shown || host.shown.includes(PORT_LOST)) { + host.shown = prepText(); + } + render(host); + showDisconnect(); + readLoop(port, host); + armWatch(host); +} + +function boot() { + if (typeof document === "undefined") { + return; + } + const host = createHost(); + const incompatible = document.getElementById("incompatible"); + const connect = document.getElementById("connect"); + const disconnect = document.getElementById("disconnect"); + let grantedPort = null; + + if (!canTest(window)) { + return; + } + if (incompatible) { + incompatible.hidden = true; + } + if (connect) { + connect.hidden = false; + } + host.shown = prepText(); + render(host); + + connect?.addEventListener("click", async () => { + host.session += 1; + const session = host.session; + host.closing = true; + host.opening = false; + host.reopenTries = 0; + clearTimeout(host.watch); + try { + await releasePort(grantedPort, host); + } catch (_err) { + // The old port is already gone. + } + if (host.session !== session) { + return; + } + host.closing = false; + let port = connectTarget(grantedPort, host.connected); + try { + if (!port) { + port = await navigator.serial.requestPort(); + } + } catch (err) { + if (err && err.name === "NotFoundError") { + return; + } + showLost(host); + return; + } + if (host.session !== session) { + return; + } + grantedPort = port; + try { + await openGranted(port, host); + } catch (_err) { + if (host.session === session) { + await recover(port, host); + } + } + }); + + disconnect?.addEventListener("click", async () => { + disconnect.disabled = true; + clearTimeout(host.watch); + try { + await releasePort(grantedPort, host); + } catch (_err) { + // Refresh anyway. The next load starts from Connect. + } + location.reload(); + }); + + navigator.serial.addEventListener("disconnect", (event) => { + if (event.target === grantedPort) { + onDisconnect(host); + render(host); + } + }); + + navigator.serial.addEventListener("connect", async (event) => { + if (host.opening || host.connected || host.closing || !host.granted) { + return; + } + grantedPort = event.target; + host.session += 1; + const session = host.session; + host.reopenTries = 0; + try { + await openGranted(grantedPort, host); + } catch (_err) { + if (host.session === session) { + await recover(grantedPort, host); + } + } + }); + + let portWrite = Promise.resolve(); + + function writeEncoded(encoded) { + const job = portWrite.then(async () => { + if (!host.connected || !grantedPort?.writable) { + return; + } + const writer = grantedPort.writable.getWriter(); + try { + await writer.write(new TextEncoder().encode(encoded)); + } finally { + writer.releaseLock(); + } + }); + portWrite = job.catch(() => {}); + return job; + } + + document.getElementById("pcba-keys")?.addEventListener("pointerdown", (event) => { + if (event.button !== 0) { + return; + } + const button = event.target.closest("button[data-key]"); + if (!button || button.disabled) { + return; + } + const encoded = encodeKey(button.dataset.key); + if (!encoded) { + return; + } + writeEncoded(encoded); + }); + + document.addEventListener("keydown", (event) => { + if (!host.connected || !grantedPort?.writable) { + return; + } + const encoded = acceptKeyEvent(event); + if (!encoded) { + return; + } + event.preventDefault(); + writeEncoded(encoded); + }); +} + +boot(); diff --git a/web/pcba_test/index.html b/web/pcba_test/index.html new file mode 100644 index 00000000..ed732be2 --- /dev/null +++ b/web/pcba_test/index.html @@ -0,0 +1,61 @@ + + + + + + + PCBA test · Tiny Engineer + + + + + + + +
+ +
+
+

Line test · ESP32-C3

+

Board test

+

Compatible with main control board rev 3. Listen, look, then press the key on the latest card.

+

The ESP32 must already run the test firmware. How to get one.

+
+

This browser cannot run the test. Use python3 scripts/pcba_test.py

+
+
+ +

PREPARE

+
+
+

1Take the board.

+

2Plug in the ESP32.

+

Match the USB mark on the board.

+

3Plug the speaker into the JST speaker port.

+

4Connect USB to the board.

+

Use the board socket, not the ESP32.

+
+
+ +
+ + +
+
+
+
+

Use Chrome or Edge.

+

For the next board, unplug USB first.

+

Same test from a terminal: python3 scripts/pcba_test.py

+

How to run the script

+
+
+
+ + + diff --git a/web/pcba_test/pcba.css b/web/pcba_test/pcba.css new file mode 100644 index 00000000..51130daf --- /dev/null +++ b/web/pcba_test/pcba.css @@ -0,0 +1,286 @@ +.page-pcba .pcba-main { + max-width: 36rem; +} + +.page-pcba .pcba-intro .lede { + max-width: 38rem; +} + +.pcba-card { + gap: 0; + padding: 1.25rem 1.3rem 1.15rem; +} + +.pcba-head { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 1rem; +} + +.pcba-title { + margin: 0; + font-family: var(--font-mono); + font-size: clamp(1.35rem, 3vw, 1.7rem); + font-weight: 500; + letter-spacing: 0.01em; + line-height: 1.2; +} + +.pcba-body { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.pcba-line, +.pcba-note, +.pcba-alert, +.pcba-meta, +.pcba-step { + margin: 0; + font-size: 1.12rem; + line-height: 1.5; +} + +.pcba-note, +.pcba-aside, +.pcba-meta { + color: var(--muted); + font-size: 0.98rem; +} + +.pcba-note { + margin-left: 2.25rem; +} + +.pcba-alert { + color: var(--error); +} + +.pcba-meta { + font-family: var(--font-mono); +} + +.pcba-look { + display: flex; + flex-direction: column; + gap: 0.2rem; + margin: 0; + padding: 0.75rem 0.9rem; + border-left: 3px solid var(--accent); + border-radius: 0 8px 8px 0; + background: var(--accent-dim); +} + +.pcba-look p { + margin: 0; + font-size: 1.12rem; + line-height: 1.45; +} + +.pcba-columns { + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.pcba-columns-label { + margin: 0; + color: var(--muted); + font-size: 0.78rem; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.pcba-columns ol { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + margin: 0; + padding: 0; + list-style: none; +} + +.pcba-columns li { + padding: 0.28rem 0.55rem; + border: 1px solid var(--border-strong); + border-radius: 6px; + background: var(--raised); + font-family: var(--font-mono); + font-size: 0.82rem; +} + +.pcba-alert { + margin-top: 0.85rem; + color: var(--error); +} + +.pcba-step { + display: grid; + grid-template-columns: 1.5rem 1fr; + gap: 0.7rem; + align-items: baseline; + margin-top: 0.45rem; +} + +.pcba-step-n { + font-family: var(--font-mono); + color: var(--accent); +} + +.pcba-bar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.55rem 0.7rem; + margin-top: 1.05rem; + padding-top: 0.9rem; + border-top: 1px solid var(--border); +} + +.pcba-bar:not(:has(#pcba-keys:not([hidden]))):not(:has(.pcba-actions button:not([hidden]))) { + display: none; +} + +.pcba-keys[hidden], +.pcba-actions .btn[hidden] { + display: none; +} + +.pcba-keys { + display: flex; + flex: 1 1 auto; + flex-wrap: wrap; + align-items: center; + gap: 0.4rem 0.65rem; + margin: 0; + padding: 0; + border: 0; +} + +.pcba-keys p { + display: flex; + align-items: center; + gap: 0.4rem; + margin: 0; + color: var(--muted); + font-size: 0.9rem; +} + +.pcba-key, +.pcba-actions .btn { + appearance: none; + min-height: 2rem; + min-width: 2rem; + padding: 0.28rem 0.7rem; + border: 1px solid var(--border-strong); + border-radius: var(--radius); + background: var(--raised); + color: var(--text); + font-family: var(--font-sans); + font-size: 0.88rem; + font-weight: 500; + line-height: 1.2; + box-shadow: none; + cursor: pointer; +} + +.pcba-key { + font-family: var(--font-mono); + font-size: 0.82rem; + touch-action: manipulation; +} + +.pcba-key:hover, +.pcba-actions .btn:hover { + border-color: #55554c; +} + +.pcba-key:focus-visible, +.pcba-actions .btn:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +.pcba-key:active, +.pcba-actions .btn:active { + transform: translateY(1px); +} + +.pcba-key:disabled { + cursor: default; + opacity: 0.55; + transform: none; +} + +.pcba-keys .pcba-key-default { + color: var(--text); + font-weight: 500; +} + +.pcba-key[data-key="enter"] { + background: var(--accent); + border-color: #5fd652; + color: #102010; + font-weight: 600; +} + +.pcba-key[data-key="enter"]:hover { + background: #8fff7d; + border-color: #8fff7d; +} + +.pcba-actions { + display: flex; + align-items: center; + margin: 0; +} + +.pcba-actions:has(#disconnect:not([hidden])) { + margin-left: auto; +} + +.pcba-actions:not(:has(button:not([hidden]))) { + display: none; +} + +.pcba-actions .btn-primary { + min-width: 7.5rem; + background: var(--accent); + border-color: #5fd652; + color: #102010; + font-weight: 600; +} + +.pcba-actions .btn-primary:hover { + background: #8fff7d; + border-color: #8fff7d; +} + +.page-pcba #incompatible { + margin: 0 0 1rem; +} + +.page-pcba .pcba-hint { + display: flex; + flex-direction: column; + gap: 0.35rem; + margin: 1rem 0 0; + color: var(--muted); + font-size: 0.88rem; +} + +.page-pcba .pcba-hint p { + margin: 0; +} + +.page-pcba .pcba-hint a, +.page-pcba .pcba-firmware a { + color: var(--accent); +} + +.page-pcba .pcba-firmware { + margin: 0 0 1rem; +}