From 73e5680fc338b5197fed2d2910dcca68c8e844cc Mon Sep 17 00:00:00 2001 From: Jay Date: Mon, 17 Aug 2026 17:33:44 -0400 Subject: [PATCH] tighten transport failure semantics --- docs/binary-record-v1.md | 16 ++++--- docs/transport-layer-validation-2026-08-17.md | 45 ++++++++++++++---- main/trikke_sensor_main.c | 19 ++++++-- main/trikke_transport.c | 19 ++++++-- main/trikke_transport.h | 7 ++- main/trikke_usb_transport.c | 3 ++ tests/fixtures/README.md | 12 +++++ tests/fixtures/direct_usb_3c95f3d.trk | Bin 0 -> 41496 bytes tests/fixtures/direct_usb_3c95f3d.wire | Bin 0 -> 42059 bytes tests/test_trikke_protocol.py | 41 ++++++++++++++++ tests/transport_fixture.c | 11 +++-- 11 files changed, 144 insertions(+), 29 deletions(-) create mode 100644 tests/fixtures/direct_usb_3c95f3d.trk create mode 100644 tests/fixtures/direct_usb_3c95f3d.wire diff --git a/docs/binary-record-v1.md b/docs/binary-record-v1.md index 5da056d..c9d2980 100644 --- a/docs/binary-record-v1.md +++ b/docs/binary-record-v1.md @@ -89,11 +89,13 @@ new samples rather than overwriting older ones; sequence gaps and the cumulative lost-sample counter expose that permanent loss. The shared transport state machine distinguishes three nonfatal states. `RETRY` -means zero bytes were accepted and the complete frame may be submitted again. -`PENDING` means the backend owns an in-flight frame, so firmware may only poll -that transfer. `COMPLETE` permits the output task to reuse its packet buffer and -consume more samples. This prevents a timeout after partial progress from -causing an ambiguous whole-frame duplicate. +is valid only from initial submission: it means zero bytes were accepted and the +complete frame may be submitted again. `PENDING` means the backend owns an +in-flight frame, so firmware may only poll that transfer. A `RETRY` returned by +polling fails closed as `FATAL`, because generic code cannot prove whole-frame +resubmission is duplicate-safe. `COMPLETE` permits the output task to reuse its +packet buffer and consume more samples. This prevents a timeout after partial +progress from causing an ambiguous whole-frame duplicate. The direct USB Serial/JTAG backend atomically copies a complete frame into its TX ring, then polls a bounded transmit-drain wait. A timeout remains `PENDING`; it @@ -104,7 +106,9 @@ USB drain is not end-to-end application delivery confirmation. A host process may attach after earlier frames have already left the endpoint, or fail after the endpoint accepts them. CRC and sequence checks make resulting loss visible, but an application acknowledgement and replay window are still required to -guarantee receipt. +guarantee receipt. Accordingly, USB `COMPLETE` means endpoint drain, while the +planned reliable BLE backend will reserve `COMPLETE` for an application ACK of +the exact frame. Receivers report bytes left in an incomplete trailing frame when capture ends. Those bytes cannot pass CRC validation and are not silently admitted as samples. diff --git a/docs/transport-layer-validation-2026-08-17.md b/docs/transport-layer-validation-2026-08-17.md index f0f273d..4b5da1b 100644 --- a/docs/transport-layer-validation-2026-08-17.md +++ b/docs/transport-layer-validation-2026-08-17.md @@ -6,23 +6,38 @@ ESP-IDF's interrupt-driven USB Serial/JTAG driver. ## Transaction contract -- `RETRY`: the backend accepted zero bytes, so whole-frame resubmission is safe. +- `RETRY`: valid only from initial submission; the backend accepted zero bytes, + so whole-frame resubmission is safe. - `PENDING`: the backend owns an in-flight frame. Only completion polling is allowed; the caller must retain and not modify the packet buffer. -- `COMPLETE`: the backend's completion criterion is satisfied and the caller may - reuse the packet buffer. +- `COMPLETE`: the backend's documented completion criterion is satisfied and the + caller may reuse the packet buffer. USB means endpoint drain; reliable BLE + will mean application acknowledgement of the exact frame. - `FATAL`: a programming or backend invariant failed. The output task stops consuming the sample queue rather than silently discarding its in-flight data. + `RETRY` from a pending poll is promoted to `FATAL` because generic code cannot + prove resubmission is duplicate-safe. For USB, submission uses a 512-byte TX ring and a bounded 50 ms write. ESP-IDF's ring-buffer send is all-or-nothing for each `TRK1` frame. Once accepted, bounded 50 ms `usb_serial_jtag_wait_tx_done()` calls continue returning `PENDING` until -the host drains the endpoint. A timeout never resubmits the frame. +the host drains the endpoint. The first completion poll happens immediately +after acceptance; repeated pending polls are scheduler-paced. A timeout never +resubmits the frame. The VFS is switched to driver mode after readable startup output so any unexpected diagnostic output cannot race the driver's ISR by accessing the hardware FIFO directly. Firmware logs are disabled before binary telemetry -tasks start, as before. +tasks start, as before. A terminal transport invariant re-enables error logging, +emits one final diagnostic, and suspends the output task. Because no later binary +frame can follow, that diagnostic cannot corrupt a recoverable stream. + +Queue-allocation and task-creation failure paths switch the VFS back to its +non-driver mode and uninstall the USB driver before emitting their error, so the +only startup diagnostic is not stranded in a TX ring that is immediately freed. +The USB backend's zero-write-to-`RETRY` mapping depends explicitly on its checked +initialized/sole-owner lifecycle; a torn-down backend fails the guard as +`FATAL` instead of masquerading as backpressure. ## Verification @@ -33,12 +48,24 @@ The host C transport fixture compiles the production state machine with - accepted transfers become pending; - repeated pending polls never call submission again; - completion returns the sender to idle; -- backend-confirmed safe retry returns to idle; +- retry returned after acceptance fails closed without resubmission; - invalid arguments and unknown backend states fail closed. -The assembled ESP32-C3 prototype produced a normal 1,728-sample direct-driver -capture with no packet gaps, sample gaps, resets, CRC failures, reported drops, -loop overruns, trailing partial bytes, or timestamp-saturation frames. +The assembled ESP32-C3 prototype first produced a normal 1,728-sample +pre-commit direct-driver capture with no packet gaps, sample gaps, resets, CRC +failures, reported drops, loop overruns, trailing partial bytes, or +timestamp-saturation frames. + +After commit `3c95f3d` was built and flashed exactly, a second capture contained +1,680 contiguous samples, sequences 0 through 1,679, with the same zero-loss +integrity result. Its optional raw wire file contains all 563 startup bytes and +re-decodes to CSV byte-for-byte identical to the live-rendered CSV. Both sides +of that exact capture are tracked as fixtures: + +- `tests/fixtures/direct_usb_3c95f3d.trk`, SHA-256 + `f495486f094a758bb785e145026e3934d60b52dd5083aef7f5d896195d967869` +- `tests/fixtures/direct_usb_3c95f3d.wire`, SHA-256 + `3bdaeadff7962c6eac48c4ebeda285c8eb359e439d5e1728104add2009122c03` For the connected-stall case, the USB endpoint was left enumerated without a serial reader long enough to overflow the 512-sample acquisition queue. When the diff --git a/main/trikke_sensor_main.c b/main/trikke_sensor_main.c index 5bb0bf4..5c0b709 100644 --- a/main/trikke_sensor_main.c +++ b/main/trikke_sensor_main.c @@ -168,6 +168,7 @@ static void write_binary_packet_until_sent( size_t packet_size) { while (true) { + const bool was_pending = sender->pending; const trikke_transport_status_t status = trikke_transport_sender_step( sender, &context->transport, packet, packet_size); @@ -175,12 +176,20 @@ static void write_binary_packet_until_sent( return; } if (status == TRIKKE_TRANSPORT_FATAL) { - // Preserve the in-flight packet and stop consuming the queue. A - // fatal backend invariant is not safely recoverable or retryable. + // The binary stream is terminal at this point, so a final text + // diagnostic cannot corrupt later frames. Preserve the packet and + // stop consuming the sample queue. + esp_log_level_set(TAG, ESP_LOG_ERROR); + ESP_LOGE(TAG, "fatal transport invariant; output task suspended"); while (true) { - vTaskDelay(portMAX_DELAY); + vTaskSuspend(NULL); } } + if (status == TRIKKE_TRANSPORT_PENDING && !was_pending) { + // Poll once immediately after acceptance. Subsequent pending polls + // are paced so a future nonblocking backend cannot busy-spin. + continue; + } vTaskDelay(pdMS_TO_TICKS(TRIKKE_TRANSPORT_RETRY_DELAY_MS)); } } @@ -349,11 +358,11 @@ void app_main(void) s_context.sample_queue = xQueueCreate(TRIKKE_SAMPLE_QUEUE_DEPTH, sizeof(trikke_wire_sample_t)); if (s_context.sample_queue == NULL) { + trikke_usb_transport_deinit(&s_context.usb_transport); ESP_LOGE(TAG, "sample queue allocation failed"); l3g4200d_deinit(&s_context.gyroscope); adxl345_deinit(&s_context.accelerometer); i2c_del_master_bus(bus); - trikke_usb_transport_deinit(&s_context.usb_transport); return; } @@ -370,11 +379,11 @@ void app_main(void) vTaskDelete(acquisition_task_handle); } vQueueDelete(s_context.sample_queue); + trikke_usb_transport_deinit(&s_context.usb_transport); ESP_LOGE(TAG, "telemetry task creation failed"); l3g4200d_deinit(&s_context.gyroscope); adxl345_deinit(&s_context.accelerometer); i2c_del_master_bus(bus); - trikke_usb_transport_deinit(&s_context.usb_transport); return; } diff --git a/main/trikke_transport.c b/main/trikke_transport.c index 29eeac0..7d81889 100644 --- a/main/trikke_transport.c +++ b/main/trikke_transport.c @@ -24,20 +24,29 @@ trikke_transport_status_t trikke_transport_sender_step( return TRIKKE_TRANSPORT_FATAL; } - const trikke_transport_status_t status = sender->pending + const bool was_pending = sender->pending; + const trikke_transport_status_t status = was_pending ? transport->poll(transport->context) : transport->begin(transport->context, packet, packet_size); if (!status_is_valid(status)) { - sender->pending = false; + sender->pending = was_pending; return TRIKKE_TRANSPORT_FATAL; } if (status == TRIKKE_TRANSPORT_PENDING) { sender->pending = true; - } else { - // RETRY from poll is allowed only when the backend has discarded or - // otherwise resolved the old transfer and knows resubmission is safe. + } else if (status == TRIKKE_TRANSPORT_COMPLETE) { sender->pending = false; + } else if (status == TRIKKE_TRANSPORT_RETRY) { + if (was_pending) { + // Once accepted, generic transport code cannot prove that retrying + // the whole frame is duplicate-safe. Fail closed and keep ownership. + sender->pending = true; + return TRIKKE_TRANSPORT_FATAL; + } + sender->pending = false; + } else { + sender->pending = was_pending; } return status; } diff --git a/main/trikke_transport.h b/main/trikke_transport.h index b90fb28..2e295ba 100644 --- a/main/trikke_transport.h +++ b/main/trikke_transport.h @@ -36,7 +36,12 @@ void trikke_transport_sender_init(trikke_transport_sender_t *sender); // Advances one bounded transport operation. The packet storage must remain valid // and unchanged from the first PENDING result through COMPLETE. While pending, -// only poll is called: an ambiguous timeout can never duplicate a frame. +// only poll is called: an ambiguous timeout can never duplicate a frame. RETRY +// is valid only from begin, where it guarantees that no bytes were accepted. +// +// COMPLETE is backend-specific. USB uses endpoint drain, which does not prove +// application receipt. A reliable BLE backend must reserve COMPLETE for an +// application acknowledgement covering this exact frame. trikke_transport_status_t trikke_transport_sender_step( trikke_transport_sender_t *sender, const trikke_transport_t *transport, diff --git a/main/trikke_usb_transport.c b/main/trikke_usb_transport.c index 9d17576..d912808 100644 --- a/main/trikke_usb_transport.c +++ b/main/trikke_usb_transport.c @@ -26,6 +26,9 @@ static trikke_transport_status_t usb_begin_packet( return TRIKKE_TRANSPORT_PENDING; } if (written == 0) { + // Arguments and lifecycle were validated above, and this backend is the + // driver's sole owner. Under that invariant, zero means the all-or-none + // ring submission timed out without accepting this frame. return TRIKKE_TRANSPORT_RETRY; } return TRIKKE_TRANSPORT_FATAL; diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md index dd5b940..dcfa6fe 100644 --- a/tests/fixtures/README.md +++ b/tests/fixtures/README.md @@ -31,6 +31,18 @@ removed before production firmware was built and flashed. capture application opened. That is deliberate evidence of the remaining distinction: USB endpoint drain is observable, but application receipt requires the planned acknowledgement/replay layer. +- `direct_usb_3c95f3d.trk` — SHA-256 + `f495486f094a758bb785e145026e3934d60b52dd5083aef7f5d896195d967869`. + This is the validated-frame output from the final exact-commit smoke capture: + 1,680 contiguous samples, sequences 0 through 1,679, with zero packet/sample + gaps, resets, CRC failures, reported drops, loop overruns, trailing bytes, or + timestamp saturation. +- `direct_usb_3c95f3d.wire` — SHA-256 + `3bdaeadff7962c6eac48c4ebeda285c8eb359e439d5e1728104add2009122c03`. + This is the byte-for-byte wire side of the same capture. It contains 563 bytes + of startup text before the valid frames. That text includes the literal + `TRK1`, producing one rejected candidate header as designed. Extracting all + valid frames reproduces `direct_usb_3c95f3d.trk` byte-for-byte. `tests/test_trikke_protocol.py` verifies the hashes, parses the captures in fragmented chunks, and asserts these signatures so the hardware evidence remains diff --git a/tests/fixtures/direct_usb_3c95f3d.trk b/tests/fixtures/direct_usb_3c95f3d.trk new file mode 100644 index 0000000000000000000000000000000000000000..039c3b17264207e039ccdd4696d2cb3df915ae52 GIT binary patch literal 41496 zcmbuIb(mbm(#B_IeY4q(I}ix&u0etXClEps;>1H-ncxI>cXuv+xws{`y9al7NG3Yp z`>X2d?M?pt&hy;dvoBqJs=CX%y3d(Dz4zTLn_V-L$!x0sWiqEcUuFMa`sbA^+Kd>P zsm}b9ZOF`?^}yWD@18ZUW_^AYKnY&GVu0@YKuIczp;ps2tWin^= z=tG~mHQQ8YuFGWA(To?G_W96LuMP43bD2fOY_@e~bn(~XiACC;TUM4?T+C%FGrfx6 z6z4SLg#A!fE-ahPX7()pAgs)=GSTLQ^%CuzhVpAE-Vf#{vj-t0Uq*8tMzZ=_3aj8FLDIla@pm@{>AS^SEDBIyQrAS z)=94U!fFjeu57k>W>oQ0Va*KlXI5qoEPgMnF0^AzDl+>RZ_Pu0(AH%RHu>w5b`6sM zyJD`PA;DT@4k&&tER}&VD{KiTjn{RXO`hMv{w&KZQU6^sgNt8@kCwuG%yYuV*fU!h z=3`!w8EIqGI$-ejdvRFtN71&4?K0U#!jQFXY?sT{W`-4)6z3G%#dg{3(&F&qziQXs zFdxryJ>wT)sSM0aCCFY@A^Sn@fbux^($!2qz zKE=htI)`@1P@mbm_`9$!kv5lYp}F})*eZc`iRQXm{e3KKRl{6|D>H`~?P{^VT((BM z@QhRjbh2ZFPY`mU2 z=7n9>wLe{_D>5UBnarGq^@Nr9D3^XQ9_uGqjr9EAVy?JBz+BJwHGVgYSVpoQWHz#q zVa#Dp_Hbx%nflu}VAx@;=7#5P65C-*{Y{5c8L;_6JFKShTI1ZS_I5Am{-!*$Z}9{5 zznPjK1N~QL_A}l#4;Xw@>6y~$hAonIHJUH5Ee*?wwk+E|Gtg{gtE62=`K2$_ZtH** ztiOTgOSXyave_lYQ8urs4EUdXFSlUb+I8F8{T|xZ?%Y2xz62ClS=SZ8CKNws5 zt2pP{R0jNI;Z!_Mf|pTOtPSX_)(sdhxQNU&5>A=iwn6uXop5-g7Ve zvlP#UnqS&mSjisnh48VDVXkv6#V@g7-_WigK3Xb{{3&d|NSn>JkqnIQ{(%;M&{?`f zy8{gKx#=Q5hhOd$+PRJNEq);^od>*Xp@2tkyk7lvhxrHEpKfo9izAG;gM>Be(nQnC z-T|Zk7SjEn)&IeU`5czXSCz>w`$Q}wTXG+KNT9{Hwv<1>fAx+1WwWg^hnTGWB5h9o zV4&$?Dg$0MzZub{@tQYa-)w*Tvs^I_pWMgB=uly| z&#aRTex>++m@v;`VWs>oBYVKM z4iA|7_(RNY#z!nGoAwwt!7#V4HsY6DZeqZ&oAw$L_?;9m`sjZzGoJ?wd=&rZDvieJC}VG=>zsQRha7%@nNX>n`sftW|t|yAtp^X%;hJ>R|=aE z+Y!4whnN}Lkp~a5crz=s!_Jw1@{}VI+E$X494D25$dGSM*3)?P%XFQVw?BywSv_;A z@itpn$sWq2`&sdJq+#wyijo0;b5v*tZ7FX%I>Cy?F|zYq!!d@r9#$xakWVWf8{08X zgH5jELOc9zm3%sKr7{p13j30!HD0gn`r(7e+n+uUMdj?ohZBUA<{>M60GpFw&4rQM zpO|0`8cS^OB*U;T^7dgCe@~9>7^k78U#En2_&L@T=-jEH9r~3~o;q zyw2V5+@7b|pCuXk+xVU?tmHE)HAd*y8HTyuG|1j~=9vjr8q2c+ExE>`Vjr{Lvtxh6 z-(E(0PHcw^{Z04JjTrH9WqTkp6dqqi|2AGPzW>xc=h>g0Ym{e>Fduimu#!zzNiXr| z7Z~QUHdhX%wNmjy!x(ecabql=UliJ*UzM3-iUrYL9I=dI@;I$+<}_RqF!ZLq);yJ7%-0y{q31IMOyS`pkX%$ z3|Y(c4D9QcNSo6<46^=ijkLLJN3GeFLlskdAiCv8udaU^uLnDIuf5IwbidR>c7(0| zM_9=oYUCf63cKAf*Bjy#<9kPFhg~*PJ7VpfNxKS-FKf)ZLOb&8rA2RdchYW|a#Y6Y zp3si@s@5~89i%c4-3r&Np?@2%y5C+Key{!M^Uy+i$@u-60AybTK}!#e#6`cS7p+_x#38QXLBQMMtS8}oBwnkI_kd{8rIUkjn~pXH@rK~{&YPr z>BCdPkbyBT7jM|e(}uY&Rmryy3!VuWGPF=T)&S2&TJq6;79XBVXjxNGJ9$2|!!MO( zrdXc+LcoYEOSK-ur>8Oyq4RsKt$!P@nlar*zG#2CJuEE_vbldrSV@Lz*&Dk5a=`GJ ztu$_|<6be$$Gl2D2zz)n(w3>t)!S_PwWQs$qSpytPq1dn1-_9Uz7c4Vp;mH{SEVu# zp$o^aqkkK(TPIxB{Z0F`(J$3XZ{8B-_Q2X^kmW&dCs@747rEXstmF^&GrM`$Fzf+Y z-AB9^+A*)qB|o*l_d|c!LzQwqV$cVHc9Hyih31-Pq%shp^N*~ne;cpeKR#*A5A9Fq zqe`&=d-zCL$)?K{yXb4aVcw4QI_*AA+AY!g3G5TYT<6e3a^6o9+6Ku$&FQmFdab>P=idhmeQnTsZ^gWo2%TSl1O3~0 z)o(Ph><9bPV{L2sM||9m!mtNoQd{vRzuxeZVdzMibYy_F``IvG0}_+*@xK^WX}XkA zO@#XGuYnfcvY81M<9{>EeMz}|HTlQy0rQ;UP@_%fVKx2t!tf3CZ{yW)%r+POVSl)*!fnVZ&L!!{-|&PN-~18W<01WGZkg=APRjL25(pOW#LDGceg0EjBFo6vZQ3Y+?*6Ph==y5WWlRR@CZo&- zYr-?h?^>z8KuoGNtRk~iyiso@CO3=E1nXBU7aw&A)>d*cU(Ew88i|O!{vvd&57r3K)Km z+7EeE>xeN+11%=Ci5PxqfazCT!?1nUaU;$5wM*JnOD@KweQ1ZQWno>I&cmAGG=I$I zk$-Mpee<+l9Ylu=*dX~BI@mGvk6i2*5HC9={nu;UsKIqM%x${PVy*Tc0tO$=w5DQA zRtf!i>>6YCuxiA}(GM_xuv)-)?qaR?ewNOqGLR`19@qkgcJM1){O1gR~dm%E3Rc2Dg&8P{B^jrC$*W!j?~UHnkk`T=vD^E%fC zhWVaHMmj={wqc~rN;gN?m~Rv@pI7pVjRWRu(cYG8Y+@LCT_(T0pXFbv3}i}$wYP@h zc-{5-4^M6?x=hC7Z!_r#Hn>^nAATz}=GepLvHy&)QMNwW!Z7zs4Z?^8TZVS%W;@Lx zdcIX?$2>I1&hf!p8-_n5Uz%Xg-6qhYhjp3hreCQHWJ>v=+eH4kIqC54{@zw}C3|S0 z^)zF&UFaYGSfM;#YoNK?$7f=zW6baG&;&b7I+APXo?zG#F>}WRYbO07585fg%0dle z=Ya9t<;7{Huc-`VN`*_eh2ePJG3v*ScM+Y>18b3qmV5R{`eH3goZ2-$vrM{wh>g*1 zO|Sz@r+1GSW7*IA!5*cw7+4_ zFYyuGJRtVR+IG5)U9U)+lgF?`j*W;cB!hVP>;ooncq zU~ME9x|z-c*o^o5UGMfN~n!y`uSd64Pqh=`SC7brfkUog@zm$jfZG1#csE+<=>Xf`rB zV8p$Jsx908L&T`zj4}Dsc_7=%-?vlbpPNT^%8eN-I+uZY_MC5=^@ZM$KaVm#4o|RV zs*hvmeDD0ofWcdxWTl32lws&iHoLm)m;B@Cq+KV;j~*Tq+W9yQvN1n4VC2S?ib3SY z$3zvqGKMYBi5_FfIggHeR&?(OuEE6ZcgZ*G3WdP zvFpTuL7P)7z~7#fw5tkpcyh$Z1BY20IVGX3mmmE|w5J;8@u7wK150HfW6fW*8w|&5 z-TMw6aGK~mP9wYTshyrYle~@f&KVKQSq{P8(3t@vj|JAw!8$3VuMJ_Ya z=Kg~COxDhxw_a!%u_YsW@crbAB8DFBZ?(UR6KsKEJm)no3H`xuF6iN}#$7|llzg%~j=t{n|t#pKZ>GIeYb2HlX>k7l%XR^P=JY1Qy%SmtWRaXTJ z-YTUx#JH=IcH~Bk?==xahOx%)wT8KUQ8#2fQyI1rr-dE&jQn#m=KNDHzfN?G_AtQo z;rh@&x=-y#KBIU;e5UVhk}upCF}YF25p?dRfMKf@!my>A4RhXFOD^pEme7u8P$R=f z+#1?JOKc^FPGunL&d=HlhT~N;qSYU_iOzM2+m8o?-`E#&?PYp%pJ6$xn>AC;gRcHBw4;`mRZKz;?~k)|`Ly7EQQ zxjj&WJk-YWrO+3?g?$_1@5@d4_dUQ@4D(n(jYzeQYhO*;k$>PvUyJ>*J{x2<{d%Cq zZgLuPV#^z`Kh8b&TWLMb%As)H0g-=h4tV?L*WVN!y5u(KHSV`UU+7J>VmI^fc7nB0 z&cj)ycMS8|a#!tR!pFM-V_sW}4{YSU1S=|UWsUuQf-NhKGk!lX%*TW?5%{Wf9#}c# zU+M+J@p^3WcfWinx{`0LQ7oYUk7EDG;5p^|fFUcn2J5|#1BR@`WNhgZ!(8{9%WlYB zK22!r6*Gw~pCwp>)~CdR&jT&_*}!6 zp?`7+@<;mrD)f)v>!7(I7JMBs&Vf$1TIDx^7FjvJ1?{&PNqvzj6T4M5G8@ul# zEoTSDt>jx-IpnwR4a4#Je%&S0e-K?sms+cqLR|eZd1igcWqyhnd7J0iKgVaX_tf8f z%P$d=>1#}gXTL^_`SSgN-vWltQP*K^evj?gqa0#bs!OaK3eygb{Bv{W%dM~fLv$q> zs6*iQ|Fph5Z*49+XTS5W*gxm_M%aGT-wD=S@-x2w1X|{>wR9R=`Zu%#tJRp`BNjy3 ztk!^ju3@2J_}jAVAH{>rmQopZ5vTb(`oM6!{@S|rDh1J%WLQ@0XZ^^uh$<;4r-Vk%s#cJVVI98zRupMNZOT2w>bk-ne zQOsR27iKjwyGy?&GCVbJ)+yDZE6r_#=8Sk)lk~+|VA)-Oz z81)l;UGvZmABOGljQWI@cL>k{UOw^tAZ&5C*?X#=tVRlb`Ztl`(Tk@oYazOl45fUw zrS;|dRN8-Sl{}OF$yHiMjJ)61t!)AZztrLJ;cb(4_1Ys~UfTr>Ur#>IUTFISE0_QO zYXyJ3hd9ljeP|OIj=CzdLkH0z1Nu;}@n!xxCeN%2>xE8+QH#synY_oNJ&mLt{)BaQ zmw;hU>`M{*S24_McI?yQ|5gnc?K)~qL|a_VFya;OHypOoSsYeWh35t|k>R1Sm(A!Z zI?rF2=ixSPt6N{54>0D$^feNUbD6A-x`k&h(KGRX~Pu#=K{}&=-EQN->ymTR-Wa z8ZJ5c28MZ@=e!m=y`f>~VOIXY=XIlm_FvWKh-({1+FbT;tt0TAn}l}AN*xO7FOT)Yt@HjB?>kDl{S+S5od)<*1IZDCj`XL8%$GPFa6X39&T z-O4c6r54JC!M2Y5k*f^0wa_-9KV+!Z+%Si!46LZ~lZP~s;j+$0yu7XGm9o5+xDxc;79L|4)!&fa6wJ(9j!DHlXXb`2PEwUVBbZ|)Y`dEJrq z=2fUCA#s zQ!QP(dhOofnapXW_Rq2DeGGG3vyZZ0q-72dG+Fmguv+QipVGqv z60Ayo8T;y$^vAl2aZ2ZbRbipS@W?+mlUJYF_dwD4JfMR1<$vvmiLPYRU9{#QmpRP( z@|>rW>>RmYA?^)MrSB_~U5nw4#Si?J{quZy?pS)Ui3?`cpM@w1?_WBo(WlISH20nunNiqG3K?$iTRrlwj-`E|4Cc9B8o{ z_(k_miT$xgnF3ZkHDYC29Isp}VP%`Y>2Mg1*Um3I@ZM>nE6Gq3WIsJVld<&ogU^T< z^@Cyd{=k_5!&YYw8Qqx88FsGOH>y@ zuDgsQyExE=NC?e;dt%5`=V_g7M;(7@3X`2Be8$h&I2r8dNlO!Yr8>Ki+e1= z{N0{h!{Y(-I5N`KtWQLYJlOY8pES(JoE)7Tb#82j%?&mGoyx#Uy0FRg$Uir0Ub5%d zd7?vxl5fH9J(cudqi2#+J{|gZdvhCn#xP%>W#yO2p`H!x&7^J8bgaJ(+f)X#lUbUqKf_qxB$-%Ft{bh=vlMP2^o z*ngRLn`AclO2FW)O6Q4)53d^LIeJ^!7qRfQNK3Bab?w(fJ7jIpc|h#p4a3|Ymg!xg z6?2V2;Rd@J+?Z*}rjkNw&3gfZU)Dg>MBXWU8nBVw3!5TEzwx&wXu}}>tw9H5Txw-#~JvaP9bd5GW!N%yzXYzcFz7A{`{x-O!Q`=y;uHo#K^OcF}Z$8 zu+kpMuL*{4#Yg-WFy^b;*4Wz9NU#>tL-Nm5hGF8gFy}}Zj@QfooU+~@qARUMYZV)* z!To7{dHh{k^mn5EO0c5h8Rz%@j?ZN6Jix~7pMWvG&jPRlj-zW=&7Vze7(ye$dO zgkRQx_@$)*E7=2nX<2ON`-7|>mIsWSw?h8AxyDY09qQinH`?A&NM#rnig8Pa*6qLIJ)co>e?{kxwx zt%K-F^H8a0D%U7>jD4{dvD#llr+{(Rr$#ZE7}_~$M=h9~w@buar&$B7Vwn5PO8F(` zX4TNnb*Z1NvsQ~3`?P+hB$a`RM&Z2^BLCd{w%MoGbQK*kpbyJ+b_PCHkA1PP=x1`* z2pD=^I`7ggV&rlYZ0^@gXnA*tynU?%E7Lr%C%bk+ORh!CSjRBeSMn&ulAXnZXV9ZS?`6~SRhKBi=Q2WEa zHj3>sTE`7TH;Wr5{c$#c8qOvOwp?R}4yQ6ul_~T*De}+FQCB_q?53ha26F0UvP<%t z%|c(ybA$M0ZN7QL_*T=*Vl&CGMe@uBox?&$wlvKBK5JX}*ebLm#@8y>qQ9+^b}h9Q zCx_Z5_JEqyO-`+%WK^|D{) zVTag`Gb=}1y|uexzQ$}OyTKlI3>afVJsVrvDYo;R1i5xLjCWB0m#E4V9y%rR z&rS6O{rm4CI@cx6w0R!i!}{{|NtMQpSi5V)$TRJn-`w2{^VpJ6d_bpnPcU?o{h~b* zjCbj%-S-Urxli-@+MW@^Z+QJ9od>Ei`9DsD;doW;a{7Y3L|2-JX7X{^^xol_*g12L zZS4~<#)x+%u+@FTGs*et<@eF){UXNP9AZ9m|D?ax^82in4v6iTn^6|ydxdta*%yVq z{Zxht;5+eK4mx_j+YS_6$uAX)J{||fXRN((?TQ36>Lv3Zb;H!gVu`J$k5my zIphSJ*Hi{7ONGnNjQn#m=&_r38z#Dv3^keK#IL^T6#F9QnQi(t+%UKE3i%9bbt4RO z`(+)6E{zNr>%!KWFY>BUNxL@ESL|za=nvhjP;6!WI3{4|@KVj;ifbS$OZm6Yg5h{A zd8J$BSkbu*8Krtt?V01OFXo=HoNnua!wqx$t&%Uqw~h}O`jrXm-wA;h-ZC0rc$*m7 z!5i-!;)5n7SPR)X{%dler9XT+`C>W`RF(?0=S2RwIkNNPZ%+|j$)-!^+@^;9;iH}O z3w@Yon4g(tELmet518kfes+0ApoQOMI_C**GYw-*sHORts9B*M&){2;$a+NRk9NE> zyyBiBm8JY{=fZHjjym+Ozw)AU--_N0H-9rb^ba580qo@*88Fr-oDpP_1Jin!s^^7QfU{&m{(>^Ds@E7LGbE^3Tn|C$yV= zyy)C7k$aA{ICVnO|04Mt_6p}j%-_pIA5IL&;*#b=yueYwBkTn;(f8L@wA zuoG;a&kPv)+g@`I?OA~qx!P;KSlgX#n9J25-%37yPQaK4aslS%+|VBwV?utP%0P9j z@XG~}e{Ndjo_zT{(Ut6>mF$h!b$;xheN5lCx*+rqZ+yFqal0^L)Y?5axG2!lj`b-z zeQ{_k)v?0hizENs9JK7LCD)0LdBBb;wT8u3uMf{eSL-zQU^j&RUH85J8zY8| z3^BQG3beE%CL`C)hEYqT*5T(`Zb@i)C!6`YHMDa-=e3Abh9ksj{*p^zI9{WkZu9wV zqH}xTee|&wSN;?Ff{(>oJEOn1hi9Uzb$V}5akY3y#8^)bGyiyJpv70!%E#j~?=p{#~AZ_BF6hZ<7~~I%0Pv$@cN~Ze{N(6jcz^^X~`icS^kmA zK!q<~bvX>jEBkl%whxQWW#E~<5A;av3%&Hd9yJU;yk3EhJZ6~l!TW&d!{Y%%_cK>H`8-qv`%3453SZ>0Mp0ES|j9 zc5jN#eJgv+{toe5p?~zDy>tm(eLMEg82QBSA7;~$+M@M@B2K#7AiNyzET;e zQWjRfHuBF+_u|$MeIYv6rHpdQ5!Tn2u`lw+@upv2C0HxzD*4CP2}Yd{AN-ABu5(3w zD}gcj)-WIQWm*@|?z_;An8Do8-}j+EbeQu+E6PBXGC$xt7>?J0Po7=;L3GGKUz{gz zt@-;g^ab{hVn#E4PxPmNF%JdFN<9BLK9l^mpXvTD5hKR=_cMPr3_T~88*g*-ThiZB z)r9cDzZ>RrxIoWcaV>hhI4xXoJq*X|rrYkz{2{th-nu-jiT_Oc$3Kvx{uMCz;7kwx z=5NE?-%|HyJ^4?hrRFl!`1m(stRI}W1xbHd={_^0zx4y9 zkh)xV7E2C%t`t1h>;<5{$u|VLG7HDbMTByc@x@xsuk%wQ3=N0CGYB2 z8s_ZWEkKWS^rhw8Pes72R$g`olhX-;cOfAKHQOt_^1?`Js-|I~D9* z!%HdymCwSATVOa|N7iosUJKD71G1MZR&ge(rS(;=?^<~+wO&}OfH`lr|5j`rpUK+T zzcJk=V)zL^@6y&Vf2SW?WZly)w8Lgws{V_OwT~FJrz5SFk;*{jGk@2uk$-O1`KfkF z2ho-GWtz(;u$JwZ^u;$X=)Y6Iu;C@4KG!+HTF7@W=3Nrn&axfi;VMnq9b|gAYHWx6 z_HJ&&Y6-?Wb;PDr1}dM0Z*PO)cy)bX%r#v_=eopuLV5A4??hQ&u1jqc=gC3VFbw@I zQ_eKNVtTiLv7T8N-t}BFVB`&@?|-djnAeJ^of1pdHjHQR&cg_kf1N;!P858d*5sRG zr-{@2s<%h}xtVwS{$=Zmt|UXH_P6n2>&0i{>xP-Ft{*Y-DE~gs1_5)O_A|*FChe-E z=gi?o0fV_s+;aH=94(XP0e7=Q0q}{9B9LC4KSTA?AL2!#pqH+oS0D4u-j3 zqTcbl^ssw^)oFdNcb(?$7%=4Ly+CwxC&T<4Q&G>wSMMCLjDE{z%1Sm(^|tWBT`(N4 z%l`i0qFqGiakD)8i*lhhvgsc2nZ!Q-=EtsvAp^CTVde{Wix|9(v)I48Va^-1WX?qG z5i$JoNb|uxBL=PO>z)Zl?U1}Iod>G7`OH0$e{Q;LF?{vCL|2Lr{N4+5zqj?}HeIe< zoAt*&N&mIN(CK|cJJx(p z`2Dtu@#x4Qp`F`)Z;P3IBZhDFZ_o5gFuonO;yZU#pz~Az8~NwviDA$F)n9b3OXP=B zO;-<1`eLocm=7?FoLQZ8vO)60k)a*&0Xt_NMum3dmdo_s0{S&N>90!TL>*vE#K<@OI})i3RG{-;{V(#* z&5YgVuQyh7CBHQPIV7I>S?D7It?Pu_ zemW0Spz{Ym82RUB&JC~S@}eunIQC`8mu82)=%2H&#M&bbgAc~d>m5fK=J|70w!}Of z9cWos@|_0s{FtO&u{gl=>)50}Y#QHpT%dKk8EJEv%5b4LEu8rf499D&BX<4!c+r(? zx^yP@grqONJ%X*yi5T@Mudkk%U^&%9(dm-{hTTxtC5JdUX;&`YXG~5p%=MN1F?{-| zp&dC%OZi)3VJZVv^!&3ANB+6#aZ;zxP7@t65JPhsbLdV_`dX&-IDDL8n71R>Ah$o$ zFz16a5YV0#Xc@~^k`-Tlc4)`>q?U>qd~ffZ1mldt7sAdp44=+t&D9aZXZAFHuSsaz>ifai&9xDu zj=6Fzoho`^;^QzJuQy+*{q;K0mEr?=0Q_EWeGz}zgE+|a=7!Ke80Rv{18+<){0(+_ zQ^2r&et$x9SiCuD*G9U8uf8R=^K(qde`^yg`F2D2?aIrDA!|qDjEtQ?q?ry`f zRxA5gwFu_*p3shYfWBx$ISW}I*b=HH1VK;mWvzO&i4<+p?Y>%(04D1Bt z+s}jHcs=p?mRCJ2I+uYt9c4b~k=Q@jRMtRqA5Hq_x9?c*Jr*#=jk+;9{dlC!Xy4j@ ztMG|POT2MCe=@!we|zVJbQ zF5v0-OzhX!n9msIc2lNj;?tguwA9!6maD!Km0;&Xbdm4u0_0=BTFM3gQ#A%PK{ySVR#lBb<_}TcE6O41k)aqYJuvXF~@}O5EMs3Dx zX|E+1bwbAcb;Iz7Wzw$+VAsA8Ft;V&GfQP)C!jFznaDpk&%HS2*EdC1@=K-f3B47b ziH`6ceQfaU1gi=D{GISj+VNW>Z>!zA0mE+iKGXZc-b*mf$6_Pz$Ns3N_;)5gh?vI* z<~5b!8gZIm^eha=>z3ZFkNHq^$bjsncYQue`l6=IKInYI+%GLrO&_1}act-Nsyy=( z!+ib3d%x5PJ`L@71~C)8`7E?UZ#c(-U-~?0$L|5sE|uY0aa!2y`N%&vAHK29@GnGH zlA%FfoFLvL6E(%*N1 z78%;ge^ER6K4QcrKRcDmaGf~KAN~Ri$7{~{>s0(8x{@yOeF}8-$M8&SgtNQ&+n+-J z#4d6T^x>_ir)qyU-~ul2W{z{|KAdfx*7S`?-6614!7Jem4V%i z!VND*{<(SkklH?fh_18-;@jV>&;AU3`P}soq%MJ6|32`5O zi=iDp;;%3!sSG!Y(?aW4BLCd1J?{01nF{W(=`y`j;O{PLU~GS+G``qY&M?NPOzQ%# z!Iedfx$$??$`jh=I$wyttuU;Ve;sXWo611Tnw57ov74%h5i<{2sm^zkIL+_(Dh$VK zwfbmSe`N_Jdb-<9dO8E`GrHx_kgL&VDJh*K_%QvC%%k2WjbE*IEjP?me-H`W^ zQ(a>BB>%+gFdVN<|C!&jgXlb8;*8A%v%!wmm;20WjU_(5Q|zByW{A}hI|mFI_*NZ! zbO{(XLfsI4S24`(t4i`C*Q$mgE9dXW+d6BtK#N@es6N3sr82O4Quz0c$Uir8PyOcN zuA=jN2|oNcdsa{SVn0sn-{KkpLr0WpS8oVc6&*HB?BYE%V)A;)GjrmD8u$7E zLk8Y=;oXA`B1V3Dg!z^Y6Rf4igm|`*VZ?6sF8q588^?C&<}91*O#;T4)X7&d|LHvZ zN1PT$za9DKrt25EZkvkEW1Pny;?!obe|)$9_S@!7uxYY4ebdP>*O9u+v1XTBhIZHu zdt;2rRtYWV(D1EW2U_&7gZz@d*-_jkv_qF#Nat3}m)HTyU;7RW$LoMip7>*1(UoK% zUqYw1i+$lUy|%i2^2{V9X6??x-*IjP3AgV~pQDlm2)&f^}i4OY8vUTf7g$@j8FM4zKSeI*)Pi zJJn*r-qx4<$IkLq_=kN0hOTxAGVE)Z>v@A{WrNr5XBe@Ncs|==*Z!d$wCpSWDm^@) z2{ux4x0NYH=S;o-|j%sxnC;NnMQwa@F45U&w_FukUXfj zVV=wIyXlPi!Qq+YqqS%d^+sJ=NXosE`Rrf}}`o?ztE;RPr&oGytZ$9$e zREE37Y5s%{VK`pbeDK9a{YBSk4}MPO(9oCr%*p0&21JZJV7ld~10#lx_}=EAh*3-O z--#Y2ZwgmY=cdc&Z{0CP zbncg^RZcS5r&?b=Zsf)2<+P;#1**y8_ooL8J?HmWk!wb5N388*^ExxZSYwk{&N7T= zWEA&inlCvbV9aZ4$%?N|=Yd_U{D@CsI9|_w)$z2v=$Hq_vUCo5cJfU2ld-oW6Rh-( z^HBlgnY=s2Gmj1!I@eNu8Qni7Y1ctIN4sNVf5cYDj!UqG`u)`v{Sv!ag)2Xc{Bu*W z_o3i*)7!ojxIXCchicGv`E%yke%+vrmi|XJKd9{_IH+jt>^TW-tzt4c*|`Bj zHyM)^>l*CD73#i>{ByJY8{Ko~iLMl<%arevQ=XsnpVN2{3oeKlacZ*habbKW`=@RX z7X^&*<$ILGjEfC(xtLe5OCl}ntjXs4E={mn+4+k35<7AEJ-&kBc>OlE>jRgGt~3wL zboPgH@RwU(o>SLkPBnh7h!|t!-=etEFz1c$DdP*TitX3~_IEn34((Wr^KL11f@=)( z_f3~;Ev_7@c&%X`r@7I;_?rE1H;6=JTd8Z!#s!Jdt20@?ns_lC*F|Z z&Oqz+Fh7TKSJJMv{60EzcY?K5t_5~a#C#9ja+l=WDeS}*e)~4^&&_$;PMvzM=t_Qx zccRe!|AxL8%L>&NvETb5hVJ{fNd6ZvY><6&J+pX!(ym;3j!%0a!J3O-=J3IUwxe=X z=IbHDJWjV$oL=$!V2_H^{5s#maJ+h6@WFWxi>^_Iex`$uB+p!;Sc`vnG(MC0@-_Tp z3D#DAl^~pfXd!C#lW?tupcIYd=>xJEvjL-B>0Qm| z0t&XrUf8Q{dzg5t~4-%}o;vVmQrt`pVV!q}V7>?IvBkETFP;@>I zBBOWqwNf&x$wBCj1=Rp}zx6M}9HPW6Kid zRq*?3z+Crz5BRsFKXMZM?e7U~N9Aewh;$yF5~qdxevkZf)8f$V_kW1abt$KBs12}j z`!n>9A6Zo~jxql$!8nWlsdV*k!`z=&%P*lf|Acm|3&_!_Vf<^DpH(6jqH_xZ#+b8a z{ao0>K+AJEv+#kiR0eiL^FRLq!|^JVR`z!XxO~)6yy_2cr&gZ9IF_^lklGE^B0Oy)~?>!eV z#+}?9eJV@ZEmM7lZ}yf4jCB>?7e~h`4D)BO)Okk`c729u*C{}V|v9L?0Iooxac1kj@Qt#o)@$bT}cLP9h+!r zeR(d-*#`8aRqP*MH_md4)}bA;^1cW0rA=rD#&;C)VQmfb9D(m-<2%~LcIc;{v1%VM z?2h+cuHg>I>P*|9-hfqwv&ypeqJMDoZZda7cF5H{ z)D+f^?KqR}^}cl?hAz3!N@ZY|HNR6K^3Top^S1wdUD5eG`1vyO&-G$od@F|Ypc@BTZWf;Daig#Kk(;FUM!OoFZ}?FCZIfV}&mj*>k>&_HLzi)U5VMFy04Ojytz!pye6t2l3oJLpz^CfA>9|2X>0{ zU6)1vx!JVy%%k=ao$nVD&-}ZRds|=VCGUldGo9WiVDQU#chH-ClV|cyJ9@rf#IRNW zZt4Dp`IwN;6I%`l{lRY+*$Do-S7=AR**V0=R0ej63;mbFaJ)WS?^!2S6C34A#qiLp%5-o>OP&7uq4$ zGX3WFig_eE#rgB4?#VU#moJw07hNgWSW@)7`q0on_S;f?pi2V+#@w^tz?csV&!ipa z7r_QaT55-f86SfKhJJB25BnOD&{B(|mN7Kavc?;0YqxYB-V~>Wm#D`#UKh6frS&k; zxjpdRH1;a=ov6?k`!b`FcAUF{-_fBR zy2;vwHUF4MOP$z%>oApp-R^vjz4^wgPv+^%$BK>^$DHyzwdm@&z4`0z~l=qy@foe(hOZzo@kznK^@&Y^joe^Nr*CcN7+IkqD|IMDPfm4V&v!uE_| zMc{XO(ypa!m0I$Q1Y;iwTb&s&=Br-) zQ74|2w5!$mXs{y^tU@t^xR=VnZg+lqc@r6?U;5(4dC`?@n)fKlHD+61p7XWQ+K?L1 zk)eOax3ktztb>k9u(slZ-1F!_%e=C`O)NYnw)6TC`Teo69c#B?W+TT1Y;mzdSpSvI zo3h(oxTB(p3^QJQwEyv>FI z?B26$&u-oN_UqB5d-tw=`te|W~{X6yO)wgTke%*Ta>fNPBuYO(o z_UP4p&z@a+^z7BOPp@9RdiU$hBWEnBo!8)rQ){Nw`RDcP+O22b9$j|t-fPdUy}S35 z$M)&kt;e2yJ9n8oZ%)nJDYGl*RL!r`2hOgW+P{0BuD!a_lv42n5Z z=FhL0J7b`ZNvBMgC0&+w9@uH_l!Y~mt1Fju+1=e`y11LAo%ceP#x4DSUsB(Y?&}s# zS-7aKa?X^xS#|b|%Bm>~r&QKfPpMkczgwnnw=Omt@Kd{Ju0Y+ochS*m(5_ow1M8dh z?MDAoWprwnRMst&4(ALUIsCBhU5=>{@ujyqCboN*+UjZZMBuubrPTv_baQ{&Sk9j^ zZC3TdfqhGgAWSBAOe7NBw))3d*2Ps!5uf^<5uA)89(C5C3d%`PiT3HD<;N3divnK&r>qhM*p z(n6aOY^cyKE6Z^hv^k04+3y6)4cg@=MrFScEYG(S(!`|f&w|0-k7;;GCD&!gXa5u| zlmQ+xn+oLL`s=$rXI^Ruyrq&`vSYH}3tgc!f#3DnM6y`qS|wPKV#t+DmL#gPp9|JR zF?(iS;>hd|f)%@Vj7e_dQ01+}^#^TnVuZ?H8ni1D`M=Mm%E|()A~7WUjbNb+j9JtZ zPU^24y3boxu79Qz8>RpDiHX^-g^#9!*_fvUo1xFFP|U_WH&LZy)XZV<_D6Pd_9vlj z?%O4j>jgvB7QS67S(KQZ-I!gLZRy)3lbfl_aJrZ*2toQFMuU$dP$oCw;Y5jJZiBQ;Ct;4T80E?U11~aY*(L!P#uhTzP_}B z{%JarD?0MG&}}bRJP$d7F&;ZO3>iw?Gducr>14~qxa{xJ4vRMVQ^^*Iv1$)HIn4C1 zGP_#Z?d;nnWt^s}&F!LCuIhDKu(i@IlmQ#ctVT2IuV;>bY2Qxzr|EQVVp=wlSXS0q zu$Ye=(J#hh*8nRNJ^weG%I@Yc)APfW-!2|Yh^)iZMs`<>IZTN?jLmMA{<=C0J1mm9 z;kkSGcG%Jw)!|SEY(8p<)zn`*TyX6n<^|2)OmnO(S{{@M|mA4)agO7Z9 zrs#Cpo1x5FasZ@l`FKE7QtxiMR<^BT&4 z|H%wB3zl2G?s#|fBYpMGY;{X^j_OiB!E7GT{dw7c0X~kCwzdU0fPn#5fFf+R*yKH$V1O75P1CLXGz5Mim zv-Z_L&9<6~-e3=d1dHdPqs%GeyPwCB$sNSEpkMoY4Bf9%yF9>S$z&6mFXG670am6q zk}4bQF#I95L<~AeG4mUx!b@Yngjdb1#6#9!?|0rZ_+b4rj%StXmktpuwg-G6d>pEn z>0DFcmsoI^YZnP0O(l-}CD`GfHkoWLGBCbJI9mKcJJBWD4N=VIroH$%{PIxO&TM2< z_DjLSdBCei5gxt%di}S(RvoE-n!RnvPE+2730AL5b5t*fJB9K^^lKJ4H94)@JsrUo@*C^j#GFg#0N@X4GX;b14#;YENGT>D+-HA5!*UE8+ zCCBKWITGXW$s=`)#tLS-gnyW*{EqWjO6*~lwj1xUWHRk+dV*pmSCR8+6BV;@sz^*w z-bixdxo#;k;72P1teNB~%zr2YUNx#AI@Vtu?zw%-Wc|~gSu8gAwZ!)+f?3{@l(9S- zKUy|bF^h%7qUB!H=8Na+ zoyoq9=mYk)KrquK;zOnSn_7=0lba>KAtu!+X7Ur`^8{Py+Y!4ghgjs>kq1xEc(d5G z!_Jw1@|0r&+6s}C94C~4$dGA9)>D6tPIRcv=%2)gq&#zh^0q{<*dEfN`$^$#sbc0w zvLXZi=2+Jb+Bk1JF2J(c8Di(Dvf~vqJ#qyCTrl`d{WF$fjE?VF zg2g@~PsRxSI$JT*n=-LCo_S7y#bbG{qb1i^pB<_8d!Fx)_&Zc-&-d++VT|hj1s)?l zZfy@lhUm#{$n*j&o6cD(67A2@!3ddFY{PJVseeFZI+c??lAPGm8^kQ33f$*6^q^c+-MEdT>g9J zU$XZ4>-+f&Z@p6gWPH;y&vR6Us{}&^c&pI$>9Vq`6*C{)RQe}QU!xf7C+v5;&eyfB z9sZWN!QWoz+REt8yv5U^H{%t%)nUk*mS%)o0xnd#fhzxCI{Z3h(Hp?{iRDi=G#R{tYdY!8LvA2$hhr(&iz z#3{!2F4qpbY$EN5wRZ>Ya%Fs3W8UN1k!NqpTDyCLcAF(fWt{GF?U=6uc?PwEPzIt~ zbo~zUZ~ayL`x{g5*FS9@%0(|3-~S30&qJB4&Bgo)J3hNbboGA$mM?Ky{#)6D zikT12m9;4O_(MUvl*pA5U;VIa2fr!FA(8bF-_GKGu3(RQ48J#3^MEHDhAt74dFGRfS${cF!w_u6Q@&kF;^Qdw z5l{R6$PKGBo~`h-3CSyG>imcE&_@1yRJN1+TYqgDdDHtV^-t6DSRbAd3>g^n9N`Tc zc~&vgrF`)%#DeD>h79G>jy1sZo|b%cw8n=Q0$SD-)J|S>?eI(K#C*-OUve0+Ws|JO z@adrpMCi=HJIlZISK*9~RWIwGW)GXP6Ljuh5iFLWKXW@DZ& zJ_vhw-P5L}&NWiD0o! z=Sb|LuT_d!JJ##8`!r~`QPxjjpDAWKhaQsiejd=4i44@7R(p(f>{RtjU%38|p;T-i ze;CR@gpU5|EdSPD&)+?A^_Tjm=|fU-Px$>xFk~>FQKLTNYsF%Fn5sJRjbdgutmCke zZ+$!JMzhqYo;C zn~8tK$NeN2_CQQ(A-suSFZ)?BbR;c0GEUq5qL{4#iOKl*Ulq$!T}ntzg!=7ojuzgM ziP;+Ce^<>@p{&DT#w}p%^GW_dl@pGeezSjD7X~Ar6)+uH> zL0zZuJamx%p1Eds`M3V6{HoK8NdL5&ajC00tQRa^izZzx*x)d1wM=3cwV92Inf%Qp z7N8HC0<1)I8a_6ATJohyI)__aJM@(t9sex`rO)LVO~noy=iX!)(R*Fx-}>vhTXx<- zHzpFsM{}76);8=2#4)a1WY{7YkuCPO)bz05v|<*Mi{zQm<~R)AN~PVeqKCNw#$0a} zEYJ5xZa6zzEdAwsEJu8+&3~v%WEq*_ZuJ7l-CxDSZY&VGgvvnOq*`sT&^?pp|mSg%yj8b zsr!q6X^``gWkd&bhhcvm@Z2t$a-mBkpi3oLW8yQK2K_e^d&56gI1Il>?T5Uona7x= z@fwqwdknucPW7vWV%R?GxGMF1ErWIiA{S%Q%C$q*v|AU3^RT0EnmN9Q=bxL`-#&9_ zYoS91Y><2m9c<(JM=tgYh?i}H{!3-tsKK>U%xt$ zb_n_}kQj>Ycl7P3Q%}^Gw4-mwzREOwT6QPJ7^g(Ct&BNx?HtgS%X|^Lckvjx>^NP= zhBA;TWq$AJ`RC@LKRb`>By_P139$$EH#+;i(y~W4J^Qg>yE@Et&gxvdDQ0^f3DFU9 zv@V`DDY`jL$9#8>*}Rfhbaj}mMTcvyv4>*lbz1!L;hKMiGLR`nJNJfRf8F!uk56|K zx%&~_azW;<^)w({}Q!(>PWr7h4db)P#W=okv^t_jA$2^pY zo#TUhD~3NLUz)AY?c-?C!{S7p>Q^WOnNp^*kLRD8Q;+`s@4iA8+e5jmrx~Muu7CVv zuH^Bu23pbIKNDM>p?-g_2G|tQkyP1$0K=AunF9l?iRc%3(B1)-b~TKB9L96EWNTGl zLm9}FqAU8su)prA{;BJ}LTB^9T4avqo`ZtESc?*;_Vdq7i|!w#W3+z*Y>4Xg0Ul#4 zN2@SNXHTL+RU31A>kzs+FR>8YpA@tsM<*^-23W4>b>p=t*i)k~J|{wQrXaTbiRbGTmXsy>(Js*8K;M zQNx*`@`v+4wwHNeZ_htBkL{hBF;nPF2Ikpvz8dWdy&-?DRz8jnuqIL;$IfTDXVNYu z7&V;Pt{wA`l>I~IVNTF4DfyS~KX{BhWxCqoJjIwV&L_;@DsSCGIE{YZ2ZsIi`+GAU zoG*0vCHSEB#kehS{o~`B2p{CdwGFT_G7qV;I`>T46^bsw+d|il{t8^~vM9g`U5#*Y z&|jJCi<5sHI>+@$=a9YflWl0SjR1M{WIpAe;{_9;xK4a z5)1IRrv~lv-5j3gG4jC48b?kKXiLS9ej>DIC}#1YT>1kGWguhCT)ICD`>WFfBgUO6 zbQY(P-S*VZ3Z6;c#(L*$kEJw+U~lLghY?3wiElx!a}_fm#6F{(YsjA002`ke%2BHtIeOqI_4#r~PB zoh@&@L@{DZLhQlzlP~ocdU%A^{w@o!wG!hwuW`BS4}Mcl53g|T&^h*inb&Y0$XGME zgJIZTD?k19hAV|G_N^^MN6441@_jKk(^bE&R?K`R`&-PzH9@SwT#h-Two#_&}{Z!#MRrVkMO!k3}65C3Z-RYib^E_1T@-E*F zzdT;~z1y{ew_=e2KX;GEIQKYK$Nb&^!_VQD?{gTjH6eK*^BT%P)*an@i07Z1eb(%D z$^Ftgj&WI8@9@n3`o56sVAY!k6iaE{tcm12=<5GmJ8Ef3iAm_;gPs<+qrQH zWgzR0wmaPO&&?6XKl1WQp+lG8gBmyb`%KUm`{dMMpLG~{KvMh?IoflIS#65_>CXjw zz5zB_&L5`AUhr5#?&jP6gU8rcnXkI}Qh;$L1X)8F`U|I-F-O3#zaG7-*EKH-o!J95 z$gw(>ueiSOE$rJ6e_w6TzwH6OrkKS7YD7}&SpIs@j{E~Z`iAe1_1Ofq={FrMc9W7Z zC$_xh`{Uf>=&jb%tQ?{Xhj{+E8S?HgZ@w*b=#tr>)wthreW5o661$m)cLS`sAr^e&G0uV3X|3{GM~keS--7nLpuZxiH>2m@ds<@hL>;>yJS}Gj zYPRyNtQ<1^hr_VHe(1EZ?nj}Eb*Y)uQi!WR1`_iqEYu}d4pHq0&p$WkyxQ!>KZP!qfjR_! z|1a&!^41cubM`ye`2IQ1H%<4W{tmDbk)QGX$I&u}%|xfMrGH&Jup$`~e8gH$o0K)6 zooiU982&b${4+aDZ7GytU*R-!*GL%l*P7nVwuyu;mSJ;twD!MV`$7hCjj1}P8{9J) zx7OnKu+@zLMvl(j<|c{^j)tm4g~)Yqd|=(@@OD6kn(BR0QqPqT8H-$qV``6um&F@*Rc_G?$p$ zI2UF$vZVd!1~NRea`EW}LKn|%nammSurTP0v%q3^*&>f&!{gOYG--r!E}~d5(=qBN z__`9;4j+c?@Ql)cmUjry0bV|_{UB^{s@i*~pR7irkz*Rj@c3o5o6ChRmLbk(n`&RC zPx1b1Met1eCs%3aG4g&}w>Ebe{8ERC#ys-^rIQH7aUaf@=8PJDP8DHkFP4LWow_a$g7`3>BJd^i$WKSb#hd*JR-QHo? z6Z=xc{%sVqnjQPJ_`hu(M!PmLCPJItPBG#Y?>9`@>MRZ`s_2Dr4P9V)_mN#<@(^MjhQVH_9{df8=jFDrPxdE8zn_ypzM=7u_Up*x9$k z7agg(xr<^}cPy9sN#ivME2_+v@eO2nsrKvbItiWWQhnaj+4Y6r%$FFM%M=ZI@vJ#8xax2z-ZoqM=;$VwdxEVKt!R8jAV z4P=--?fN#|gf7m7ON2LKM0fv8_UJkPBzqbG#@dLzt34Hqb0)L>o~|7-G?Ban+Fpv8 zE|p6z4A$HCN3Jqa*Ft?sZy4a)C1hW9me$S&DrR#-eG(nn+tV`Ub_Qgh0Ndi~ zsi6$4sG{2|8_19>yYaq#g)Y`5&fa6wgMz*)Bo{2KK=m8$X zKic~U2YSrL92o{HW^>3{b8@1#4VqI7~Rrd^s70{dLQXHbV{;y4Wu@ky^Uw z>heR}Gnvyo*+0jo4^_-;l`{p**I}+5^H3pk&pyiGo|ZX0Qe{0Nz=}i<{}MeM5@7k_ zm$9#*L4T~P7^iR^SQSRCr+WUmnYaC-QAY}$%>z1U<2EeltBK45ess8EmY4CK0nZ$v znCS?)mD-@AMgC&Z5!xN)F>GXlwj1SW(P8#E@EfBQv$<(4@4q$PTVhq188!`u{k88E z$ITuibg@mhliYy4?6K~d*8g19{c#S%SLKMUqI2WjGnsqNc5n`Uf?^gQ%S0d0uZf

)WjRk< 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 UuR!#Mxw$|wdoI7t#+vH?0Pz&#CjbBd literal 0 HcmV?d00001 diff --git a/tests/test_trikke_protocol.py b/tests/test_trikke_protocol.py index a79bbad..148b988 100644 --- a/tests/test_trikke_protocol.py +++ b/tests/test_trikke_protocol.py @@ -185,6 +185,7 @@ class ProtocolContractTest(unittest.TestCase): "first_sequence": 0, "last_sequence": 2143, "max_dropped": 0, + "timing_anomalies": 2, "gaps": [], }, "forced_outage_7s.trk": { @@ -193,6 +194,7 @@ class ProtocolContractTest(unittest.TestCase): "first_sequence": 0, "last_sequence": 1977, "max_dropped": 138, + "timing_anomalies": 3, "gaps": [(511, 650, 1_390_000)], }, "direct_usb_stall.trk": { @@ -201,8 +203,18 @@ class ProtocolContractTest(unittest.TestCase): "first_sequence": 8, "last_sequence": 2065, "max_dropped": 1194, + "timing_anomalies": 1, "gaps": [(511, 1706, 11_950_000)], }, + "direct_usb_3c95f3d.trk": { + "sha256": "f495486f094a758bb785e145026e3934d60b52dd5083aef7f5d896195d967869", + "sample_count": 1680, + "first_sequence": 0, + "last_sequence": 1679, + "max_dropped": 0, + "timing_anomalies": 8, + "gaps": [], + }, } for name, contract in expected.items(): @@ -251,6 +263,10 @@ class ProtocolContractTest(unittest.TestCase): contract["max_dropped"], integrity.sample_gap_count ) self.assertEqual(0, integrity.sample_reset_count) + self.assertEqual( + contract["timing_anomalies"], + integrity.timing_anomaly_count, + ) self.assertEqual( contract["max_dropped"], integrity.final_dropped_sample_count, @@ -268,6 +284,31 @@ class ProtocolContractTest(unittest.TestCase): ] self.assertEqual(contract["gaps"], gaps) + def test_exact_usb_raw_wire_evidence(self) -> None: + wire = ( + ROOT / "tests" / "fixtures" / "direct_usb_3c95f3d.wire" + ).read_bytes() + validated = ( + ROOT / "tests" / "fixtures" / "direct_usb_3c95f3d.trk" + ).read_bytes() + self.assertEqual( + "3bdaeadff7962c6eac48c4ebeda285c8eb359e439d5e1728104add2009122c03", + hashlib.sha256(wire).hexdigest(), + ) + + parser = StreamParser() + frames = [] + for offset in range(0, len(wire), 113): + frames.extend(parser.feed(wire[offset : offset + 113])) + + self.assertEqual(214, len(frames)) + self.assertEqual(validated, b"".join(frame.raw for frame in frames)) + self.assertEqual(0, parser.startup_crc_errors) + self.assertEqual(0, parser.crc_errors) + self.assertEqual(1, parser.header_errors) + self.assertEqual(563, parser.skipped_bytes) + self.assertEqual(0, parser.buffered_bytes) + if __name__ == "__main__": unittest.main() diff --git a/tests/transport_fixture.c b/tests/transport_fixture.c index 32f9c33..af43c04 100644 --- a/tests/transport_fixture.c +++ b/tests/transport_fixture.c @@ -99,11 +99,16 @@ int main(void) TRIKKE_TRANSPORT_PENDING || trikke_transport_sender_step( &sender, &transport, packet, sizeof(packet)) != - TRIKKE_TRANSPORT_RETRY || - sender.pending) { - return fail(6, "backend-confirmed safe retry must return to idle"); + TRIKKE_TRANSPORT_FATAL || + !sender.pending || mock.begin_calls != 4 || mock.poll_calls != 3 || + trikke_transport_sender_step( + &sender, &transport, packet, sizeof(packet)) != + TRIKKE_TRANSPORT_FATAL || + mock.begin_calls != 4 || mock.poll_calls != 4) { + return fail(6, "retry after acceptance must fail closed without resubmit"); } + trikke_transport_sender_init(&sender); if (trikke_transport_sender_step( NULL, &transport, packet, sizeof(packet)) != TRIKKE_TRANSPORT_FATAL ||