From aba7fc3a0be84d38b3343c20b529fe15e62d1c7e Mon Sep 17 00:00:00 2001
From: Michael Maaseide
Date: Thu, 9 Apr 2026 12:46:59 -0400
Subject: [PATCH 01/18] fixed
---
data/hurdle_400m.csv | 5802 ++++++++++++++++++++++++++
frontend/src/hooks/useBle.hooks.ts | 13 +
frontend/src/pages/RecordRunPage.tsx | 184 +-
3 files changed, 5943 insertions(+), 56 deletions(-)
create mode 100644 data/hurdle_400m.csv
diff --git a/data/hurdle_400m.csv b/data/hurdle_400m.csv
new file mode 100644
index 00000000..a66f6bf8
--- /dev/null
+++ b/data/hurdle_400m.csv
@@ -0,0 +1,5802 @@
+Time,Force_Foot1,Force_Foot2
+0,279,0
+10,535,0
+20,840,0
+30,1578,0
+40,1564,0
+50,2595,0
+60,3318,0
+70,2838,0
+80,2145,0
+90,3302,0
+100,2186,0
+110,2937,0
+120,1541,0
+130,1654,0
+140,1175,0
+150,709,0
+160,434,0
+170,0,0
+180,0,0
+190,0,0
+200,0,440
+210,0,837
+220,0,803
+230,0,1386
+240,0,2024
+250,278,2251
+260,598,2536
+270,1140,1976
+280,1115,3432
+290,1898,2303
+300,1890,2025
+310,3020,1779
+320,2451,1897
+330,3053,1334
+340,1747,958
+350,2167,601
+360,948,0
+370,818,0
+380,0,0
+390,0,0
+400,0,0
+410,0,0
+420,0,0
+430,0,0
+440,0,0
+450,0,0
+460,0,204
+470,0,591
+480,228,712
+490,386,1826
+500,632,2653
+510,1058,2717
+520,2278,2342
+530,2105,2718
+540,3004,2377
+550,3523,1731
+560,3598,1532
+570,2766,756
+580,2788,500
+590,1397,0
+600,1076,0
+610,528,0
+620,400,0
+630,0,0
+640,0,0
+650,0,0
+660,0,0
+670,0,0
+680,0,381
+690,0,753
+700,0,974
+710,0,1488
+720,356,2625
+730,507,2733
+740,1114,2506
+750,1395,3220
+760,2680,2698
+770,2182,2781
+780,2930,1948
+790,3098,1658
+800,2806,790
+810,2286,601
+820,1312,0
+830,1362,0
+840,555,0
+850,584,0
+860,0,0
+870,0,0
+880,0,0
+890,0,0
+900,0,0
+910,0,0
+920,0,368
+930,0,813
+940,252,967
+950,525,1521
+960,639,1454
+970,1166,2816
+980,1268,3023
+990,1357,3365
+1000,1771,3636
+1010,2287,2499
+1020,3935,3340
+1030,3351,2360
+1040,3440,1834
+1050,3046,918
+1060,2102,989
+1070,1617,432
+1080,1422,0
+1090,491,0
+1100,534,0
+1110,0,0
+1120,0,0
+1130,0,0
+1140,0,0
+1150,0,0
+1160,0,0
+1170,0,0
+1180,0,292
+1190,314,467
+1200,578,878
+1210,633,1410
+1220,1170,1642
+1230,1063,2136
+1240,2366,2740
+1250,2872,3354
+1260,2181,1994
+1270,2934,2118
+1280,3041,1392
+1290,3114,915
+1300,2390,596
+1310,2633,0
+1320,1350,0
+1330,904,0
+1340,828,0
+1350,477,0
+1360,0,0
+1370,0,0
+1380,0,0
+1390,0,0
+1400,0,293
+1410,0,660
+1420,0,878
+1430,0,1286
+1440,0,1425
+1450,0,2245
+1460,0,2903
+1470,418,3002
+1480,857,3420
+1490,1165,3759
+1500,2297,2921
+1510,1685,2927
+1520,2741,1409
+1530,2706,1407
+1540,2740,738
+1550,2253,624
+1560,1743,0
+1570,1551,0
+1580,641,0
+1590,615,0
+1600,0,0
+1610,0,0
+1620,0,0
+1630,0,0
+1640,0,0
+1650,0,0
+1660,0,0
+1670,0,246
+1680,242,385
+1690,521,656
+1700,939,1100
+1710,1728,2163
+1720,2103,2188
+1730,2253,2913
+1740,3284,2068
+1750,3910,3758
+1760,2568,2557
+1770,2763,3201
+1780,2002,1310
+1790,1186,1436
+1800,479,747
+1810,0,496
+1820,0,0
+1830,0,0
+1840,0,0
+1850,0,0
+1860,0,0
+1870,0,0
+1880,0,0
+1890,0,0
+1900,0,0
+1910,351,0
+1920,462,313
+1930,907,419
+1940,1277,979
+1950,1830,1179
+1960,1904,1604
+1970,3844,2311
+1980,3580,2539
+1990,2678,2701
+2000,3098,3399
+2010,1989,2927
+2020,1744,2511
+2030,628,2556
+2040,637,2148
+2050,0,1217
+2060,0,845
+2070,0,412
+2080,0,0
+2090,0,0
+2100,0,0
+2110,0,0
+2120,0,0
+2130,0,0
+2140,0,0
+2150,0,0
+2160,334,0
+2170,465,0
+2180,699,0
+2190,1004,0
+2200,1040,249
+2210,1537,607
+2220,2423,618
+2230,2497,1580
+2240,3223,2101
+2250,3538,1831
+2260,2860,2292
+2270,2768,2008
+2280,2049,2561
+2290,1966,2359
+2300,855,2661
+2310,677,1857
+2320,461,1309
+2330,0,1461
+2340,0,809
+2350,0,551
+2360,0,0
+2370,0,0
+2380,0,0
+2390,0,0
+2400,0,0
+2410,289,0
+2420,357,0
+2430,975,0
+2440,962,0
+2450,1498,425
+2460,2558,737
+2470,2808,1158
+2480,2789,1428
+2490,2232,2000
+2500,2038,2439
+2510,3686,2068
+2520,2649,3152
+2530,2133,2202
+2540,1824,2308
+2550,892,2763
+2560,827,1887
+2570,348,1629
+2580,0,820
+2590,0,579
+2600,0,0
+2610,0,0
+2620,0,0
+2630,0,0
+2640,0,0
+2650,0,0
+2660,0,0
+2670,288,0
+2680,571,0
+2690,578,0
+2700,1302,0
+2710,1608,0
+2720,1583,440
+2730,2119,757
+2740,3574,723
+2750,3867,1832
+2760,2171,2422
+2770,2607,2501
+2780,2966,3027
+2790,1340,2442
+2800,1479,2686
+2810,1144,1768
+2820,464,2191
+2830,0,1645
+2840,0,964
+2850,0,605
+2860,0,0
+2870,0,0
+2880,0,0
+2890,0,0
+2900,0,0
+2910,0,0
+2920,0,0
+2930,0,0
+2940,319,0
+2950,745,0
+2960,1088,339
+2970,1228,644
+2980,1982,1535
+2990,2829,1232
+3000,2424,2304
+3010,2848,2378
+3020,3577,2711
+3030,2544,3604
+3040,2039,3024
+3050,1269,1385
+3060,1007,1279
+3070,663,1037
+3080,0,344
+3090,0,0
+3100,0,0
+3110,0,0
+3120,0,0
+3130,0,0
+3140,0,0
+3150,0,0
+3160,0,0
+3170,0,0
+3180,0,0
+3190,226,254
+3200,459,529
+3210,619,654
+3220,1482,1043
+3230,1696,2293
+3240,2065,2872
+3250,2903,3063
+3260,3549,3409
+3270,2264,2541
+3280,3732,2945
+3290,1981,2025
+3300,2076,1276
+3310,2379,776
+3320,1847,0
+3330,1379,0
+3340,707,0
+3350,423,0
+3360,0,0
+3370,0,0
+3380,0,0
+3390,0,0
+3400,0,0
+3410,0,0
+3420,0,0
+3430,0,0
+3440,0,350
+3450,0,469
+3460,529,1007
+3470,900,1016
+3480,1249,2159
+3490,1544,1824
+3500,2946,3245
+3510,1896,2357
+3520,2579,3647
+3530,3297,2036
+3540,2090,2729
+3550,2253,1965
+3560,1316,1752
+3570,794,1070
+3580,453,918
+3590,0,566
+3600,0,0
+3610,0,0
+3620,0,0
+3630,0,0
+3640,0,0
+3650,0,0
+3660,0,0
+3670,0,0
+3680,0,0
+3690,0,0
+3700,0,0
+3710,0,0
+3720,0,0
+3730,0,0
+3740,0,0
+3750,0,0
+3760,0,0
+3770,0,0
+3780,0,0
+3790,0,0
+3800,0,0
+3810,0,0
+3820,0,0
+3830,0,0
+3840,0,0
+3850,0,0
+3860,0,0
+3870,0,0
+3880,0,0
+3890,0,0
+3900,0,0
+3910,0,0
+3920,0,0
+3930,0,0
+3940,0,0
+3950,0,0
+3960,0,0
+3970,406,0
+3980,847,0
+3990,798,223
+4000,1446,447
+4010,2048,943
+4020,2437,1075
+4030,2529,1495
+4040,2667,2359
+4050,3081,2259
+4060,3220,3864
+4070,2399,3747
+4080,1692,2871
+4090,1316,3152
+4100,1199,2243
+4110,517,1809
+4120,0,746
+4130,0,493
+4140,0,0
+4150,0,0
+4160,0,0
+4170,0,0
+4180,0,0
+4190,0,0
+4200,0,0
+4210,0,0
+4220,312,0
+4230,362,0
+4240,879,0
+4250,990,358
+4260,1797,614
+4270,2063,1088
+4280,2206,1340
+4290,3686,2581
+4300,3569,1791
+4310,2628,2482
+4320,1650,3767
+4330,1643,3101
+4340,1573,1756
+4350,876,2016
+4360,617,1506
+4370,0,556
+4380,0,339
+4390,0,0
+4400,0,0
+4410,0,0
+4420,0,0
+4430,0,0
+4440,0,0
+4450,0,0
+4460,408,0
+4470,699,0
+4480,877,0
+4490,1328,295
+4500,2205,716
+4510,2995,835
+4520,3501,1649
+4530,3334,1491
+4540,2218,2651
+4550,2537,3262
+4560,2681,3267
+4570,1024,2803
+4580,745,2917
+4590,610,2031
+4600,0,2416
+4610,0,2011
+4620,0,1174
+4630,0,1071
+4640,0,617
+4650,0,374
+4660,0,0
+4670,0,0
+4680,0,0
+4690,0,0
+4700,313,0
+4710,491,0
+4720,722,0
+4730,1620,0
+4740,1358,0
+4750,3034,0
+4760,3509,395
+4770,2899,520
+4780,2115,1289
+4790,2565,2078
+4800,2529,2891
+4810,1363,3265
+4820,605,2176
+4830,602,2766
+4840,0,2669
+4850,0,2495
+4860,0,1326
+4870,0,1170
+4880,0,804
+4890,0,0
+4900,0,0
+4910,0,0
+4920,182,0
+4930,406,0
+4940,900,0
+4950,1224,0
+4960,2083,0
+4970,1598,0
+4980,1816,0
+4990,2825,369
+5000,3598,457
+5010,3861,774
+5020,2211,1547
+5030,1946,1628
+5040,1821,1872
+5050,1353,2698
+5060,734,1907
+5070,530,3773
+5080,0,3506
+5090,0,3337
+5100,0,2667
+5110,0,1328
+5120,0,1574
+5130,0,1244
+5140,0,592
+5150,0,0
+5160,170,0
+5170,535,0
+5180,640,0
+5190,1388,0
+5200,1792,0
+5210,2630,0
+5220,2638,0
+5230,2669,0
+5240,3954,0
+5250,2544,298
+5260,2801,534
+5270,2428,887
+5280,2023,1069
+5290,1318,2052
+5300,718,1518
+5310,541,3123
+5320,0,2980
+5330,0,2814
+5340,0,2355
+5350,0,3426
+5360,0,2053
+5370,0,2023
+5380,0,952
+5390,0,780
+5400,0,405
+5410,0,0
+5420,360,0
+5430,393,0
+5440,952,0
+5450,899,0
+5460,1409,0
+5470,2371,0
+5480,2912,0
+5490,3710,0
+5500,2584,0
+5510,2414,304
+5520,3180,722
+5530,1699,799
+5540,1818,1224
+5550,1051,1931
+5560,1210,2485
+5570,749,2068
+5580,489,3122
+5590,0,3716
+5600,0,2762
+5610,0,2876
+5620,0,1718
+5630,0,1803
+5640,0,1097
+5650,0,731
+5660,0,0
+5670,0,0
+5680,315,0
+5690,361,0
+5700,842,0
+5710,1076,0
+5720,2244,0
+5730,2195,0
+5740,3719,0
+5750,2054,0
+5760,1938,205
+5770,2053,346
+5780,2093,575
+5790,819,1265
+5800,607,1919
+5810,0,2555
+5820,0,2446
+5830,0,1913
+5840,0,2244
+5850,0,3848
+5860,0,3430
+5870,0,1838
+5880,0,1862
+5890,0,1428
+5900,0,691
+5910,0,908
+5920,238,409
+5930,414,0
+5940,693,0
+5950,828,0
+5960,1262,0
+5970,2042,0
+5980,2302,0
+5990,2928,0
+6000,3367,0
+6010,3592,182
+6020,2194,490
+6030,1901,962
+6040,1559,895
+6050,1091,2168
+6060,1170,2500
+6070,909,2361
+6080,417,2298
+6090,0,3746
+6100,0,3526
+6110,0,2848
+6120,0,1459
+6130,0,864
+6140,0,832
+6150,0,393
+6160,0,0
+6170,260,0
+6180,486,0
+6190,593,0
+6200,1106,0
+6210,2022,0
+6220,2204,0
+6230,1934,0
+6240,2362,0
+6250,2942,0
+6260,3204,226
+6270,2192,345
+6280,2126,720
+6290,1402,1330
+6300,1332,2140
+6310,527,2405
+6320,611,2034
+6330,0,2041
+6340,0,2809
+6350,0,3131
+6360,0,2214
+6370,0,2406
+6380,0,1794
+6390,0,1299
+6400,0,589
+6410,319,411
+6420,442,0
+6430,921,0
+6440,928,0
+6450,1699,0
+6460,2050,0
+6470,1870,0
+6480,1985,0
+6490,2874,0
+6500,3908,0
+6510,2523,408
+6520,2622,433
+6530,1757,1039
+6540,1710,947
+6550,1476,1160
+6560,717,1780
+6570,537,2571
+6580,0,2070
+6590,0,2269
+6600,0,3142
+6610,0,2210
+6620,0,2368
+6630,0,2122
+6640,0,1382
+6650,0,683
+6660,0,456
+6670,0,487
+6680,323,0
+6690,607,0
+6700,1166,0
+6710,1161,0
+6720,1423,0
+6730,2064,0
+6740,3264,0
+6750,2878,0
+6760,2217,0
+6770,2422,0
+6780,2484,0
+6790,2438,239
+6800,1700,588
+6810,1500,598
+6820,877,1536
+6830,775,1649
+6840,370,2557
+6850,0,2028
+6860,0,2926
+6870,0,2134
+6880,0,2288
+6890,0,2214
+6900,0,1371
+6910,0,1596
+6920,0,1115
+6930,0,416
+6940,0,0
+6950,0,0
+6960,0,0
+6970,276,0
+6980,842,0
+6990,1186,0
+7000,1432,0
+7010,1737,0
+7020,1682,0
+7030,2999,0
+7040,3767,0
+7050,3309,284
+7060,3142,553
+7070,2565,880
+7080,2235,1738
+7090,836,1594
+7100,1012,1739
+7110,516,2483
+7120,0,3498
+7130,0,3662
+7140,0,3389
+7150,0,3249
+7160,0,1632
+7170,0,1867
+7180,0,1400
+7190,0,650
+7200,0,390
+7210,0,0
+7220,0,0
+7230,266,0
+7240,433,0
+7250,923,0
+7260,1519,0
+7270,2193,0
+7280,1940,0
+7290,3235,291
+7300,3594,533
+7310,2442,563
+7320,2924,1378
+7330,2733,1976
+7340,1550,1784
+7350,966,2626
+7360,823,2685
+7370,343,3449
+7380,0,2232
+7390,0,2562
+7400,0,2157
+7410,0,1017
+7420,0,1135
+7430,0,507
+7440,0,0
+7450,0,0
+7460,314,0
+7470,465,0
+7480,1086,0
+7490,1767,0
+7500,2417,0
+7510,3111,0
+7520,3285,0
+7530,3364,0
+7540,3344,366
+7550,1672,686
+7560,1765,959
+7570,776,1857
+7580,803,1648
+7590,0,1833
+7600,0,3682
+7610,0,2204
+7620,0,2174
+7630,0,2928
+7640,0,1478
+7650,0,1527
+7660,0,1102
+7670,0,532
+7680,0,0
+7690,419,0
+7700,732,0
+7710,727,0
+7720,1343,0
+7730,2476,0
+7740,2453,0
+7750,1982,0
+7760,3186,0
+7770,2633,0
+7780,2957,0
+7790,1554,314
+7800,1115,755
+7810,1351,843
+7820,573,1117
+7830,0,2069
+7840,0,1656
+7850,0,2157
+7860,0,2459
+7870,0,3317
+7880,0,2121
+7890,0,2244
+7900,0,2406
+7910,0,1996
+7920,282,1504
+7930,672,1144
+7940,894,707
+7950,1115,349
+7960,1571,0
+7970,1865,0
+7980,3420,0
+7990,3102,0
+8000,3659,0
+8010,3396,0
+8020,1889,0
+8030,1567,0
+8040,1038,0
+8050,820,0
+8060,0,0
+8070,0,300
+8080,0,594
+8090,0,864
+8100,0,802
+8110,0,2072
+8120,0,2711
+8130,0,2933
+8140,0,3695
+8150,0,2612
+8160,0,2281
+8170,0,2511
+8180,0,2134
+8190,0,1481
+8200,0,791
+8210,0,735
+8220,0,0
+8230,0,0
+8240,0,0
+8250,0,0
+8260,0,0
+8270,0,0
+8280,0,0
+8290,0,0
+8300,0,0
+8310,0,0
+8320,0,0
+8330,0,0
+8340,0,0
+8350,0,0
+8360,0,0
+8370,0,0
+8380,0,0
+8390,0,0
+8400,0,0
+8410,0,0
+8420,0,0
+8430,0,0
+8440,0,0
+8450,0,0
+8460,0,0
+8470,0,0
+8480,0,0
+8490,0,0
+8500,0,0
+8510,0,0
+8520,0,0
+8530,0,0
+8540,0,0
+8550,0,0
+8560,0,0
+8570,0,0
+8580,0,0
+8590,0,0
+8600,0,0
+8610,0,0
+8620,0,0
+8630,0,0
+8640,0,0
+8650,0,0
+8660,0,0
+8670,0,0
+8680,0,255
+8690,0,413
+8700,192,853
+8710,445,816
+8720,972,1951
+8730,797,2701
+8740,1193,3085
+8750,2422,3784
+8760,3351,2489
+8770,2327,2925
+8780,3752,2445
+8790,3317,1640
+8800,3119,1430
+8810,2574,1257
+8820,1694,927
+8830,1765,565
+8840,1133,0
+8850,706,0
+8860,0,0
+8870,0,0
+8880,0,0
+8890,0,0
+8900,0,0
+8910,0,0
+8920,0,0
+8930,0,0
+8940,0,0
+8950,0,241
+8960,0,621
+8970,0,1261
+8980,0,999
+8990,192,1847
+9000,445,3293
+9010,972,2542
+9020,797,2269
+9030,1193,2656
+9040,2422,3110
+9050,3315,2441
+9060,2327,1506
+9070,3752,902
+9080,3317,524
+9090,3119,0
+9100,2574,0
+9110,1694,0
+9120,1765,0
+9130,1133,0
+9140,706,0
+9150,0,0
+9160,0,0
+9170,0,0
+9180,0,0
+9190,0,0
+9200,0,269
+9210,0,510
+9220,0,573
+9230,0,1312
+9240,0,1535
+9250,0,2684
+9260,0,2963
+9270,281,2023
+9280,438,2027
+9290,572,3146
+9300,1441,1929
+9310,1572,1564
+9320,1475,1558
+9330,2222,1112
+9340,3011,655
+9350,2251,788
+9360,2783,0
+9370,2298,0
+9380,2290,0
+9390,1801,0
+9400,1196,0
+9410,577,0
+9420,432,0
+9430,0,0
+9440,0,0
+9450,0,166
+9460,0,605
+9470,0,979
+9480,0,1215
+9490,0,1886
+9500,0,2085
+9510,0,3106
+9520,376,3458
+9530,838,2889
+9540,1171,2939
+9550,2000,1825
+9560,1872,1289
+9570,1853,980
+9580,3211,460
+9590,2475,0
+9600,2882,0
+9610,1793,0
+9620,2378,0
+9630,2133,0
+9640,1566,0
+9650,825,0
+9660,426,0
+9670,0,0
+9680,0,0
+9690,0,278
+9700,0,699
+9710,0,1005
+9720,0,1443
+9730,0,2029
+9740,0,1796
+9750,0,2323
+9760,0,2134
+9770,383,2625
+9780,587,2038
+9790,739,2537
+9800,1526,2665
+9810,1359,1273
+9820,2678,1510
+9830,3472,924
+9840,2946,366
+9850,3737,0
+9860,2942,0
+9870,1813,0
+9880,1702,0
+9890,978,0
+9900,476,0
+9910,0,0
+9920,0,0
+9930,0,0
+9940,0,0
+9950,0,0
+9960,0,0
+9970,0,359
+9980,0,505
+9990,0,813
+10000,0,1338
+10010,221,1862
+10020,385,2550
+10030,1247,3159
+10040,1018,3169
+10050,1795,3387
+10060,2588,3258
+10070,2180,1937
+10080,2188,2074
+10090,2271,2198
+10100,2898,1417
+10110,1759,1227
+10120,870,934
+10130,648,557
+10140,0,0
+10150,0,0
+10160,0,0
+10170,0,0
+10180,0,0
+10190,0,0
+10200,0,0
+10210,0,0
+10220,0,0
+10230,0,0
+10240,0,276
+10250,0,437
+10260,275,866
+10270,432,1141
+10280,829,2790
+10290,1428,2554
+10300,1765,2202
+10310,2233,2897
+10320,3134,2921
+10330,2837,2387
+10340,2814,1148
+10350,3193,955
+10360,3338,735
+10370,2956,0
+10380,1647,0
+10390,2139,0
+10400,1184,0
+10410,966,0
+10420,416,0
+10430,0,0
+10440,0,0
+10450,0,0
+10460,0,0
+10470,0,399
+10480,0,664
+10490,0,1123
+10500,0,1116
+10510,0,1449
+10520,343,1642
+10530,473,3232
+10540,818,2275
+10550,1060,3356
+10560,2041,1782
+10570,2673,1707
+10580,2117,1684
+10590,1934,1552
+10600,3394,689
+10610,2724,465
+10620,2241,0
+10630,2315,0
+10640,1879,0
+10650,1932,0
+10660,862,0
+10670,862,0
+10680,390,0
+10690,0,0
+10700,0,0
+10710,0,283
+10720,0,616
+10730,0,1064
+10740,0,1546
+10750,0,1581
+10760,0,1874
+10770,0,2584
+10780,202,2254
+10790,584,3432
+10800,803,3101
+10810,1405,2642
+10820,2440,1314
+10830,2694,1215
+10840,2581,814
+10850,2122,0
+10860,2709,0
+10870,2162,0
+10880,1603,0
+10890,1938,0
+10900,722,0
+10910,758,0
+10920,0,0
+10930,0,0
+10940,0,185
+10950,0,587
+10960,0,1119
+10970,0,955
+10980,0,1776
+10990,0,3089
+11000,0,2791
+11010,0,2548
+11020,0,2820
+11030,379,1615
+11040,652,1425
+11050,836,1288
+11060,932,886
+11070,1979,0
+11080,1837,0
+11090,2964,0
+11100,3247,0
+11110,3210,0
+11120,3736,0
+11130,1665,0
+11140,1381,0
+11150,1769,0
+11160,907,430
+11170,540,769
+11180,0,801
+11190,0,1626
+11200,0,2103
+11210,0,2662
+11220,0,2670
+11230,0,2107
+11240,0,2822
+11250,0,3009
+11260,0,1987
+11270,0,1059
+11280,223,821
+11290,593,418
+11300,809,0
+11310,908,0
+11320,1104,0
+11330,2467,0
+11340,2887,0
+11350,2893,0
+11360,2396,0
+11370,3184,0
+11380,2296,0
+11390,3061,0
+11400,2634,0
+11410,1352,263
+11420,1074,618
+11430,996,592
+11440,415,1496
+11450,0,1696
+11460,0,2319
+11470,0,2877
+11480,0,2092
+11490,0,2451
+11500,0,3483
+11510,0,2819
+11520,0,1632
+11530,0,1402
+11540,0,1951
+11550,0,1421
+11560,0,520
+11570,460,411
+11580,858,0
+11590,1153,0
+11600,1053,0
+11610,1810,0
+11620,3252,0
+11630,2231,0
+11640,2261,0
+11650,2912,0
+11660,2025,180
+11670,2303,446
+11680,1074,671
+11690,666,1031
+11700,604,2025
+11710,0,1576
+11720,0,1971
+11730,0,3693
+11740,0,3957
+11750,0,3680
+11760,0,1855
+11770,0,2384
+11780,0,2195
+11790,0,1716
+11800,0,1051
+11810,364,680
+11820,379,0
+11830,723,0
+11840,1003,0
+11850,1885,0
+11860,3132,0
+11870,3534,0
+11880,2470,0
+11890,2910,0
+11900,1988,0
+11910,1122,0
+11920,866,0
+11930,559,388
+11940,0,540
+11950,0,711
+11960,0,1105
+11970,0,1407
+11980,0,2029
+11990,0,3284
+12000,0,3296
+12010,0,2647
+12020,0,2516
+12030,0,3372
+12040,249,1719
+12050,499,2066
+12060,1302,1385
+12070,1593,812
+12080,2315,744
+12090,2038,529
+12100,3509,0
+12110,2074,0
+12120,2546,0
+12130,1972,0
+12140,1721,0
+12150,836,0
+12160,532,0
+12170,0,0
+12180,0,0
+12190,0,0
+12200,0,0
+12210,0,290
+12220,0,302
+12230,0,590
+12240,0,969
+12250,0,2078
+12260,0,2293
+12270,346,1888
+12280,413,3907
+12290,868,2788
+12300,1331,1930
+12310,1477,2849
+12320,3013,1682
+12330,2938,1489
+12340,2054,642
+12350,3472,515
+12360,3362,0
+12370,3129,0
+12380,2463,0
+12390,1630,0
+12400,1032,0
+12410,564,0
+12420,0,0
+12430,0,0
+12440,0,0
+12450,0,0
+12460,0,383
+12470,0,460
+12480,0,739
+12490,0,889
+12500,0,1761
+12510,0,2117
+12520,264,1739
+12530,576,2650
+12540,1198,2409
+12550,973,2516
+12560,1412,2760
+12570,2517,2657
+12580,3612,1516
+12590,2816,1487
+12600,2665,945
+12610,2646,458
+12620,1303,0
+12630,1199,0
+12640,846,0
+12650,522,0
+12660,0,0
+12670,0,0
+12680,0,0
+12690,0,0
+12700,0,0
+12710,0,0
+12720,0,0
+12730,0,376
+12740,0,613
+12750,336,1143
+12760,409,2081
+12770,1130,2107
+12780,920,3082
+12790,1530,3476
+12800,2366,2397
+12810,1992,3302
+12820,2050,3138
+12830,2257,2017
+12840,2495,1670
+12850,2911,1133
+12860,2499,475
+12870,1939,0
+12880,1418,0
+12890,1101,0
+12900,557,0
+12910,0,0
+12920,0,0
+12930,0,0
+12940,0,0
+12950,0,0
+12960,0,0
+12970,0,0
+12980,0,0
+12990,0,0
+13000,0,0
+13010,0,0
+13020,0,0
+13030,0,0
+13040,0,0
+13050,0,0
+13060,0,0
+13070,0,0
+13080,0,0
+13090,0,0
+13100,0,0
+13110,0,0
+13120,0,0
+13130,0,0
+13140,0,0
+13150,0,0
+13160,0,0
+13170,0,0
+13180,0,0
+13190,0,0
+13200,0,0
+13210,0,0
+13220,0,0
+13230,0,0
+13240,0,0
+13250,0,254
+13260,0,422
+13270,0,1399
+13280,0,1550
+13290,0,1526
+13300,0,2255
+13310,0,3093
+13320,0,2636
+13330,211,2812
+13340,463,2480
+13350,766,1598
+13360,1020,1112
+13370,1656,332
+13380,2261,0
+13390,2905,0
+13400,2841,0
+13410,2182,0
+13420,3295,0
+13430,2619,0
+13440,1304,0
+13450,1529,0
+13460,871,317
+13470,507,383
+13480,0,471
+13490,0,1281
+13500,0,1714
+13510,0,1873
+13520,0,3099
+13530,0,3313
+13540,0,3480
+13550,0,2574
+13560,0,1974
+13570,0,1829
+13580,0,1832
+13590,433,1078
+13600,698,840
+13610,969,556
+13620,1581,0
+13630,2335,0
+13640,1905,0
+13650,2628,0
+13660,2890,0
+13670,3788,0
+13680,2277,0
+13690,2265,0
+13700,1712,0
+13710,1738,0
+13720,945,537
+13730,578,504
+13740,0,1520
+13750,0,1273
+13760,0,2553
+13770,0,2300
+13780,0,2159
+13790,0,2069
+13800,0,3523
+13810,0,1870
+13820,0,2203
+13830,0,1482
+13840,0,767
+13850,244,409
+13860,567,0
+13870,613,0
+13880,1153,0
+13890,1783,0
+13900,2113,0
+13910,3154,0
+13920,2519,0
+13930,2671,0
+13940,3352,0
+13950,1564,0
+13960,1670,0
+13970,1255,372
+13980,573,706
+13990,0,804
+14000,0,1258
+14010,0,2341
+14020,0,2635
+14030,0,1920
+14040,0,3107
+14050,0,2193
+14060,0,2893
+14070,0,1924
+14080,0,2311
+14090,254,1977
+14100,472,1079
+14110,1317,550
+14120,1154,509
+14130,1574,0
+14140,2545,0
+14150,2158,0
+14160,3496,0
+14170,3192,0
+14180,2085,0
+14190,2082,0
+14200,880,0
+14210,755,310
+14220,287,351
+14230,0,754
+14240,0,1293
+14250,0,2116
+14260,0,1560
+14270,0,2066
+14280,0,2444
+14290,0,2523
+14300,0,3689
+14310,0,2415
+14320,331,3083
+14330,687,2335
+14340,857,1364
+14350,1258,1186
+14360,1465,849
+14370,2372,499
+14380,1900,0
+14390,2696,0
+14400,2718,0
+14410,3718,0
+14420,3036,0
+14430,3052,0
+14440,2213,0
+14450,1352,0
+14460,1256,0
+14470,849,243
+14480,470,457
+14490,0,868
+14500,0,1178
+14510,0,1609
+14520,0,1782
+14530,0,3759
+14540,0,2414
+14550,0,2156
+14560,0,2920
+14570,0,2254
+14580,0,1417
+14590,0,627
+14600,223,377
+14610,755,0
+14620,803,0
+14630,1505,0
+14640,1694,0
+14650,2206,0
+14660,2078,0
+14670,3177,0
+14680,2956,0
+14690,2116,0
+14700,1649,0
+14710,1790,0
+14720,1102,264
+14730,573,505
+14740,505,1039
+14750,0,1568
+14760,0,1899
+14770,0,2499
+14780,0,2207
+14790,0,3072
+14800,0,2483
+14810,0,3000
+14820,0,2034
+14830,0,942
+14840,0,704
+14850,0,356
+14860,201,0
+14870,431,0
+14880,962,0
+14890,1221,0
+14900,1499,0
+14910,1583,0
+14920,3047,0
+14930,2467,0
+14940,3174,0
+14950,2382,0
+14960,3173,242
+14970,2085,362
+14980,2093,864
+14990,1270,783
+15000,1077,1076
+15010,597,1509
+15020,0,2925
+15030,0,3136
+15040,0,2596
+15050,0,3666
+15060,0,3204
+15070,0,2791
+15080,0,2153
+15090,0,1058
+15100,0,884
+15110,0,895
+15120,0,563
+15130,0,0
+15140,470,0
+15150,705,0
+15160,1095,0
+15170,1646,0
+15180,2163,0
+15190,2544,0
+15200,1915,0
+15210,3231,0
+15220,3839,0
+15230,3017,0
+15240,2830,249
+15250,2060,361
+15260,1118,541
+15270,611,1265
+15280,440,1273
+15290,0,2629
+15300,0,2196
+15310,0,2589
+15320,0,3016
+15330,0,2089
+15340,0,3179
+15350,0,2171
+15360,0,1748
+15370,0,1978
+15380,0,925
+15390,195,555
+15400,493,682
+15410,880,0
+15420,1025,0
+15430,2181,0
+15440,1994,0
+15450,2425,0
+15460,1979,0
+15470,2354,0
+15480,3464,0
+15490,1773,0
+15500,1887,0
+15510,1412,289
+15520,769,658
+15530,345,820
+15540,0,1305
+15550,0,2026
+15560,0,2299
+15570,0,2200
+15580,0,2129
+15590,0,2944
+15600,0,2112
+15610,0,3103
+15620,0,2551
+15630,293,1237
+15640,743,869
+15650,1070,519
+15660,1856,337
+15670,1283,0
+15680,1699,0
+15690,3192,0
+15700,3034,0
+15710,2432,0
+15720,2612,0
+15730,2380,0
+15740,2008,0
+15750,1422,0
+15760,877,0
+15770,697,0
+15780,416,0
+15790,0,450
+15800,0,725
+15810,0,765
+15820,0,1020
+15830,0,1458
+15840,0,2809
+15850,0,2520
+15860,0,2701
+15870,0,3400
+15880,0,3129
+15890,0,3315
+15900,361,2328
+15910,519,1179
+15920,1186,1251
+15930,1985,845
+15940,2207,420
+15950,2801,0
+15960,2126,0
+15970,3785,0
+15980,1919,0
+15990,1747,0
+16000,1307,0
+16010,1345,0
+16020,754,0
+16030,0,0
+16040,0,0
+16050,0,0
+16060,0,498
+16070,0,768
+16080,0,688
+16090,0,933
+16100,0,1301
+16110,0,1834
+16120,250,3433
+16130,733,3243
+16140,776,2905
+16150,1368,3499
+16160,2105,1940
+16170,2978,1572
+16180,2825,2171
+16190,2964,1544
+16200,2921,1049
+16210,2156,666
+16220,1578,0
+16230,958,0
+16240,559,0
+16250,0,0
+16260,0,0
+16270,0,0
+16280,0,0
+16290,0,0
+16300,0,0
+16310,0,0
+16320,0,0
+16330,0,281
+16340,315,433
+16350,455,669
+16360,526,1912
+16370,945,1977
+16380,1401,1772
+16390,1418,2255
+16400,2019,3890
+16410,3061,2998
+16420,3321,2343
+16430,3209,2336
+16440,2864,1571
+16450,2646,819
+16460,2444,518
+16470,1516,0
+16480,899,0
+16490,737,0
+16500,555,0
+16510,0,0
+16520,0,0
+16530,0,0
+16540,0,0
+16550,0,0
+16560,0,0
+16570,0,0
+16580,0,223
+16590,0,514
+16600,275,994
+16610,318,1238
+16620,496,1388
+16630,905,2045
+16640,1383,2166
+16650,2725,3770
+16660,2049,2379
+16670,3400,2431
+16680,2279,2156
+16690,3470,2004
+16700,2222,1999
+16710,1918,858
+16720,1542,508
+16730,939,490
+16740,567,0
+16750,531,0
+16760,0,0
+16770,0,0
+16780,0,0
+16790,0,0
+16800,0,0
+16810,0,0
+16820,0,0
+16830,0,267
+16840,0,496
+16850,0,647
+16860,0,1340
+16870,0,1497
+16880,391,2270
+16890,597,2558
+16900,822,3756
+16910,1196,3099
+16920,2006,2718
+16930,2748,1632
+16940,3396,1292
+16950,2439,1078
+16960,2689,755
+16970,2271,367
+16980,1959,0
+16990,1174,0
+17000,756,0
+17010,0,0
+17020,0,0
+17030,0,0
+17040,0,0
+17050,0,0
+17060,0,0
+17070,0,0
+17080,0,0
+17090,0,0
+17100,0,323
+17110,0,480
+17120,348,1070
+17130,464,1829
+17140,951,2379
+17150,1844,2525
+17160,2311,2372
+17170,1700,3287
+17180,2710,3531
+17190,2030,2690
+17200,2885,2565
+17210,3476,1084
+17220,1629,1021
+17230,2220,837
+17240,1126,468
+17250,907,0
+17260,509,0
+17270,0,0
+17280,0,0
+17290,0,0
+17300,0,0
+17310,0,0
+17320,0,0
+17330,0,0
+17340,0,0
+17350,0,0
+17360,0,314
+17370,0,574
+17380,0,937
+17390,364,1577
+17400,547,1778
+17410,1049,2033
+17420,1468,3431
+17430,1720,3078
+17440,1973,3025
+17450,2001,2537
+17460,2772,2376
+17470,2037,1802
+17480,3760,1294
+17490,2220,809
+17500,1606,703
+17510,1899,535
+17520,1459,0
+17530,799,0
+17540,442,0
+17550,0,0
+17560,0,0
+17570,0,0
+17580,0,0
+17590,0,0
+17600,0,0
+17610,0,0
+17620,0,0
+17630,0,0
+17640,0,0
+17650,0,0
+17660,0,0
+17670,0,0
+17680,0,0
+17690,0,0
+17700,0,0
+17710,0,0
+17720,0,0
+17730,0,0
+17740,0,0
+17750,0,0
+17760,0,0
+17770,0,0
+17780,0,0
+17790,0,0
+17800,0,0
+17810,0,0
+17820,0,0
+17830,0,0
+17840,0,0
+17850,0,0
+17860,0,0
+17870,0,0
+17880,0,0
+17890,0,0
+17900,0,0
+17910,0,0
+17920,0,244
+17930,0,369
+17940,0,1131
+17950,363,1633
+17960,564,2227
+17970,1014,2034
+17980,1056,1960
+17990,1160,3636
+18000,1991,2913
+18010,3102,2746
+18020,2085,2025
+18030,2487,2023
+18040,2851,1271
+18050,2928,635
+18060,2231,606
+18070,1971,0
+18080,1051,0
+18090,970,0
+18100,867,0
+18110,375,0
+18120,0,0
+18130,0,0
+18140,0,0
+18150,0,0
+18160,0,0
+18170,0,0
+18180,0,314
+18190,0,540
+18200,447,1001
+18210,617,1963
+18220,918,1938
+18230,1486,2361
+18240,2669,3085
+18250,2111,3550
+18260,2134,3058
+18270,2999,2579
+18280,2925,1655
+18290,1617,637
+18300,1651,619
+18310,1090,0
+18320,520,0
+18330,0,0
+18340,0,0
+18350,0,0
+18360,0,0
+18370,0,0
+18380,0,0
+18390,0,0
+18400,0,0
+18410,0,472
+18420,0,517
+18430,0,1084
+18440,0,1881
+18450,463,2479
+18460,754,2365
+18470,915,2620
+18480,1351,3914
+18490,2346,3542
+18500,3312,3102
+18510,3508,1565
+18520,3320,1148
+18530,3530,810
+18540,1957,644
+18550,1713,0
+18560,1136,0
+18570,809,0
+18580,558,0
+18590,0,0
+18600,0,0
+18610,0,0
+18620,0,0
+18630,0,0
+18640,0,0
+18650,0,284
+18660,0,684
+18670,0,1062
+18680,0,1545
+18690,0,1693
+18700,325,3040
+18710,569,2950
+18720,1264,2040
+18730,1325,3398
+18740,2703,3117
+18750,3246,2093
+18760,3321,2372
+18770,2109,1374
+18780,2063,913
+18790,2786,407
+18800,1849,0
+18810,1124,0
+18820,618,0
+18830,0,0
+18840,0,0
+18850,0,0
+18860,0,0
+18870,0,0
+18880,0,0
+18890,0,0
+18900,0,326
+18910,0,685
+18920,0,652
+18930,301,1491
+18940,623,2018
+18950,888,2693
+18960,1488,3386
+18970,1840,2865
+18980,2160,3105
+18990,2990,2575
+19000,3789,2114
+19010,3802,2305
+19020,3005,1276
+19030,2794,1003
+19040,2286,679
+19050,1733,520
+19060,996,0
+19070,929,0
+19080,600,0
+19090,0,0
+19100,0,0
+19110,0,0
+19120,0,0
+19130,0,0
+19140,0,0
+19150,0,337
+19160,0,473
+19170,0,662
+19180,398,1520
+19190,537,2030
+19200,730,1708
+19210,1330,2596
+19220,1323,3871
+19230,2298,3035
+19240,2633,1904
+19250,3243,2214
+19260,2608,958
+19270,3399,720
+19280,2099,530
+19290,2142,0
+19300,1069,0
+19310,969,0
+19320,710,0
+19330,307,0
+19340,0,0
+19350,0,0
+19360,0,0
+19370,0,0
+19380,0,301
+19390,0,867
+19400,0,1305
+19410,0,1959
+19420,336,2664
+19430,653,2304
+19440,1035,2061
+19450,1075,2040
+19460,1470,2158
+19470,2356,2580
+19480,3575,2181
+19490,2585,1767
+19500,2047,686
+19510,2743,646
+19520,2487,0
+19530,1812,0
+19540,709,0
+19550,651,0
+19560,0,0
+19570,0,0
+19580,0,0
+19590,0,0
+19600,0,0
+19610,0,0
+19620,0,165
+19630,0,537
+19640,0,777
+19650,0,1514
+19660,0,1601
+19670,241,3042
+19680,436,1967
+19690,697,2748
+19700,1170,3372
+19710,1830,3446
+19720,2369,2394
+19730,3584,1481
+19740,3586,1131
+19750,3463,475
+19760,3427,0
+19770,1539,0
+19780,1623,0
+19790,1470,0
+19800,870,0
+19810,429,0
+19820,0,0
+19830,0,0
+19840,0,0
+19850,0,0
+19860,0,341
+19870,0,607
+19880,0,1016
+19890,0,1293
+19900,0,3036
+19910,0,2126
+19920,0,3482
+19930,396,3842
+19940,519,2489
+19950,958,2478
+19960,1747,1623
+19970,2170,1256
+19980,2701,552
+19990,2279,0
+20000,3911,0
+20010,1898,0
+20020,2132,0
+20030,1192,0
+20040,1319,0
+20050,836,0
+20060,0,0
+20070,0,0
+20080,0,0
+20090,0,0
+20100,0,426
+20110,0,569
+20120,0,788
+20130,0,1667
+20140,0,2581
+20150,0,2537
+20160,0,3177
+20170,0,3798
+20180,308,3849
+20190,409,2957
+20200,880,2801
+20210,853,1678
+20220,1694,1110
+20230,2429,1036
+20240,2072,625
+20250,2032,0
+20260,2500,0
+20270,3232,0
+20280,2348,0
+20290,1872,0
+20300,1454,0
+20310,980,0
+20320,644,0
+20330,0,0
+20340,0,0
+20350,0,320
+20360,0,353
+20370,0,667
+20380,0,947
+20390,0,1769
+20400,0,2255
+20410,0,2108
+20420,0,2094
+20430,0,2343
+20440,0,3462
+20450,214,1856
+20460,433,3193
+20470,699,2273
+20480,1466,1990
+20490,1359,1143
+20500,1915,673
+20510,2362,561
+20520,3382,0
+20530,2683,0
+20540,3332,0
+20550,2916,0
+20560,1636,0
+20570,2250,0
+20580,1532,0
+20590,1079,0
+20600,567,0
+20610,0,0
+20620,0,0
+20630,0,458
+20640,0,854
+20650,0,1058
+20660,0,999
+20670,0,1537
+20680,0,2604
+20690,0,2462
+20700,0,3862
+20710,0,3606
+20720,0,3616
+20730,184,2523
+20740,432,1507
+20750,603,1495
+20760,1344,973
+20770,2055,431
+20780,2189,0
+20790,2388,0
+20800,2505,0
+20810,3404,0
+20820,3278,0
+20830,2392,0
+20840,2120,0
+20850,1398,0
+20860,727,0
+20870,0,530
+20880,0,711
+20890,0,860
+20900,0,1466
+20910,0,2566
+20920,0,1908
+20930,0,2439
+20940,0,2695
+20950,0,2444
+20960,319,1825
+20970,348,1987
+20980,671,1620
+20990,830,1088
+21000,1734,352
+21010,2389,0
+21020,1972,0
+21030,3742,0
+21040,2715,0
+21050,2523,0
+21060,3377,0
+21070,2323,0
+21080,1805,0
+21090,1237,0
+21100,669,0
+21110,323,292
+21120,0,493
+21130,0,802
+21140,0,1212
+21150,0,1826
+21160,0,2633
+21170,0,2266
+21180,0,2781
+21190,0,2022
+21200,0,2464
+21210,413,2113
+21220,725,1815
+21230,1260,1901
+21240,998,846
+21250,1618,709
+21260,2413,402
+21270,2484,0
+21280,3246,0
+21290,3616,0
+21300,2545,0
+21310,3112,0
+21320,1600,0
+21330,1859,0
+21340,1381,0
+21350,1010,0
+21360,685,0
+21370,0,0
+21380,0,0
+21390,0,488
+21400,0,551
+21410,0,1418
+21420,0,1204
+21430,0,1667
+21440,0,2626
+21450,0,2896
+21460,0,3331
+21470,0,2322
+21480,269,2476
+21490,525,997
+21500,1075,999
+21510,1379,411
+21520,1417,0
+21530,2775,0
+21540,3255,0
+21550,2860,0
+21560,2807,0
+21570,2094,0
+21580,1153,0
+21590,1029,0
+21600,585,0
+21610,0,0
+21620,0,0
+21630,0,270
+21640,0,606
+21650,0,724
+21660,0,1076
+21670,0,1733
+21680,0,2001
+21690,0,1920
+21700,0,3583
+21710,0,2979
+21720,0,3450
+21730,224,3179
+21740,666,2196
+21750,677,1742
+21760,1137,1302
+21770,2321,810
+21780,2708,351
+21790,2697,0
+21800,2358,0
+21810,2446,0
+21820,2105,0
+21830,1831,0
+21840,1346,0
+21850,1182,0
+21860,1134,0
+21870,679,0
+21880,0,0
+21890,0,0
+21900,0,295
+21910,0,689
+21920,0,844
+21930,0,2131
+21940,0,2921
+21950,0,1933
+21960,0,2601
+21970,0,1951
+21980,0,2170
+21990,183,1588
+22000,499,1696
+22010,1042,583
+22020,1439,311
+22030,2454,0
+22040,2970,0
+22050,3693,0
+22060,3067,0
+22070,1907,0
+22080,2544,0
+22090,1509,0
+22100,1414,0
+22110,791,0
+22120,0,0
+22130,0,306
+22140,0,465
+22150,0,1219
+22160,0,1310
+22170,0,1503
+22180,0,2837
+22190,0,2018
+22200,0,3111
+22210,330,3837
+22220,402,3381
+22230,924,1601
+22240,1189,1503
+22250,1374,1263
+22260,1771,828
+22270,2130,580
+22280,3641,0
+22290,3401,0
+22300,3332,0
+22310,2641,0
+22320,2356,0
+22330,1835,0
+22340,763,0
+22350,717,0
+22360,0,0
+22370,0,0
+22380,0,0
+22390,0,0
+22400,0,0
+22410,0,0
+22420,0,0
+22430,0,0
+22440,0,0
+22450,0,0
+22460,0,0
+22470,0,0
+22480,0,0
+22490,0,0
+22500,0,0
+22510,0,0
+22520,0,0
+22530,0,0
+22540,0,0
+22550,0,0
+22560,0,0
+22570,0,0
+22580,0,0
+22590,0,0
+22600,0,0
+22610,0,0
+22620,0,0
+22630,0,0
+22640,0,0
+22650,0,0
+22660,0,0
+22670,0,0
+22680,0,0
+22690,0,0
+22700,0,0
+22710,0,0
+22720,0,382
+22730,0,739
+22740,0,690
+22750,0,1394
+22760,0,2546
+22770,0,2689
+22780,270,2171
+22790,355,3223
+22800,808,3770
+22810,1196,2624
+22820,2068,1800
+22830,2725,1296
+22840,2294,997
+22850,3963,825
+22860,2855,471
+22870,2883,0
+22880,1897,0
+22890,1972,0
+22900,1367,0
+22910,887,0
+22920,418,0
+22930,0,0
+22940,0,0
+22950,0,0
+22960,0,0
+22970,0,0
+22980,0,0
+22990,0,513
+23000,0,502
+23010,262,912
+23020,587,1575
+23030,776,1925
+23040,1236,2669
+23050,2572,3491
+23060,2984,3419
+23070,3402,3101
+23080,2957,2422
+23090,3730,2847
+23100,3192,1823
+23110,1678,1438
+23120,1436,998
+23130,1206,600
+23140,876,365
+23150,0,0
+23160,0,0
+23170,0,0
+23180,0,0
+23190,0,0
+23200,0,0
+23210,0,0
+23220,0,0
+23230,0,0
+23240,0,0
+23250,0,0
+23260,0,0
+23270,307,315
+23280,533,637
+23290,927,1025
+23300,1740,1823
+23310,1749,2012
+23320,3071,1936
+23330,2703,2019
+23340,3019,2738
+23350,2843,1919
+23360,2204,2405
+23370,2316,1459
+23380,924,1276
+23390,895,1210
+23400,325,674
+23410,0,0
+23420,0,0
+23430,0,0
+23440,0,0
+23450,0,0
+23460,0,0
+23470,0,0
+23480,0,0
+23490,374,0
+23500,566,0
+23510,632,0
+23520,1547,267
+23530,1412,491
+23540,1519,775
+23550,2858,976
+23560,2870,1273
+23570,3789,1553
+23580,2673,2191
+23590,2743,2322
+23600,2883,3270
+23610,1278,2349
+23620,1462,1640
+23630,1077,1720
+23640,561,1512
+23650,0,580
+23660,0,400
+23670,0,0
+23680,0,0
+23690,0,0
+23700,0,0
+23710,0,0
+23720,0,0
+23730,0,0
+23740,506,0
+23750,496,0
+23760,863,290
+23770,1030,401
+23780,2244,697
+23790,1820,1081
+23800,2320,1963
+23810,2872,2267
+23820,3842,2107
+23830,2881,2063
+23840,1924,3306
+23850,2563,3049
+23860,1551,2493
+23870,847,1797
+23880,1032,1639
+23890,640,1653
+23900,0,817
+23910,0,510
+23920,0,0
+23930,0,0
+23940,0,0
+23950,0,0
+23960,0,0
+23970,0,0
+23980,0,0
+23990,292,0
+24000,794,0
+24010,1040,0
+24020,2024,0
+24030,1951,197
+24040,3446,478
+24050,2018,609
+24060,2826,1177
+24070,1916,1918
+24080,2121,2497
+24090,1721,2462
+24100,1074,2454
+24110,597,3410
+24120,377,1933
+24130,0,1771
+24140,0,792
+24150,0,512
+24160,0,0
+24170,0,0
+24180,0,0
+24190,0,0
+24200,0,0
+24210,0,0
+24220,0,0
+24230,0,0
+24240,293,0
+24250,606,0
+24260,817,0
+24270,970,427
+24280,1660,711
+24290,3036,1110
+24300,1988,1062
+24310,3649,1421
+24320,3455,2747
+24330,3162,2539
+24340,2269,3645
+24350,2546,3038
+24360,1259,2516
+24370,911,2941
+24380,565,2015
+24390,449,1293
+24400,0,651
+24410,0,500
+24420,0,0
+24430,0,0
+24440,0,0
+24450,0,0
+24460,0,0
+24470,0,0
+24480,0,0
+24490,386,0
+24500,664,0
+24510,880,0
+24520,1539,170
+24530,2311,568
+24540,1816,738
+24550,3303,1531
+24560,3430,1833
+24570,2897,2224
+24580,2575,3671
+24590,2378,3082
+24600,1111,3328
+24610,1482,3292
+24620,530,2773
+24630,292,1554
+24640,0,1114
+24650,0,817
+24660,0,0
+24670,0,0
+24680,0,0
+24690,0,0
+24700,0,0
+24710,0,0
+24720,0,0
+24730,0,0
+24740,0,0
+24750,277,0
+24760,442,0
+24770,1039,211
+24780,937,483
+24790,1726,733
+24800,2768,951
+24810,2944,1756
+24820,3074,3102
+24830,3071,3532
+24840,2821,3460
+24850,2030,1990
+24860,2841,2538
+24870,1670,2101
+24880,1224,1829
+24890,923,946
+24900,533,664
+24910,313,0
+24920,0,0
+24930,0,0
+24940,0,0
+24950,0,0
+24960,0,0
+24970,0,0
+24980,0,0
+24990,0,0
+25000,0,0
+25010,0,0
+25020,188,0
+25030,410,346
+25040,821,618
+25050,1421,932
+25060,1852,956
+25070,2187,1273
+25080,1908,2852
+25090,2196,3059
+25100,3679,2802
+25110,3503,2353
+25120,2304,3836
+25130,1880,2073
+25140,1264,3004
+25150,530,1456
+25160,501,1626
+25170,0,739
+25180,0,453
+25190,0,0
+25200,0,0
+25210,0,0
+25220,0,0
+25230,0,0
+25240,0,0
+25250,0,0
+25260,0,0
+25270,0,166
+25280,282,409
+25290,543,512
+25300,720,1238
+25310,1544,1501
+25320,1433,1654
+25330,2078,3089
+25340,2694,3122
+25350,1999,2300
+25360,3164,3607
+25370,2345,2825
+25380,1738,1855
+25390,1632,1509
+25400,990,1579
+25410,508,948
+25420,0,649
+25430,0,0
+25440,0,0
+25450,0,0
+25460,0,0
+25470,0,0
+25480,0,0
+25490,0,0
+25500,0,0
+25510,0,0
+25520,303,0
+25530,536,0
+25540,565,0
+25550,1209,469
+25560,2206,446
+25570,1966,693
+25580,1850,1268
+25590,3818,1903
+25600,3076,2133
+25610,2986,2779
+25620,2589,2596
+25630,2533,3752
+25640,1628,2464
+25650,1187,2650
+25660,506,2142
+25670,422,2231
+25680,0,1610
+25690,0,595
+25700,0,688
+25710,0,0
+25720,0,0
+25730,0,0
+25740,0,0
+25750,0,0
+25760,0,0
+25770,0,0
+25780,387,0
+25790,520,269
+25800,585,502
+25810,1373,867
+25820,1601,1325
+25830,2033,2235
+25840,2207,2305
+25850,3427,2107
+25860,2032,2847
+25870,2612,2201
+25880,3076,1895
+25890,1505,2023
+25900,1464,1441
+25910,964,910
+25920,899,579
+25930,530,0
+25940,0,0
+25950,0,0
+25960,0,0
+25970,0,0
+25980,0,0
+25990,0,0
+26000,0,0
+26010,0,0
+26020,0,0
+26030,0,0
+26040,0,537
+26050,317,944
+26060,369,856
+26070,961,2214
+26080,1280,1672
+26090,1953,2864
+26100,2554,3568
+26110,2315,2167
+26120,2191,3184
+26130,2090,1858
+26140,2623,2005
+26150,2694,1101
+26160,1938,822
+26170,2043,0
+26180,1330,0
+26190,633,0
+26200,617,0
+26210,0,0
+26220,0,0
+26230,0,0
+26240,0,0
+26250,0,0
+26260,0,0
+26270,0,0
+26280,0,233
+26290,0,614
+26300,458,786
+26310,757,1094
+26320,1050,1701
+26330,1786,2638
+26340,2086,2783
+26350,2833,3161
+26360,2298,3162
+26370,3657,2573
+26380,2147,1459
+26390,1738,1221
+26400,2242,895
+26410,1685,604
+26420,1227,0
+26430,558,0
+26440,0,0
+26450,0,0
+26460,0,0
+26470,0,0
+26480,0,0
+26490,0,0
+26500,0,0
+26510,0,292
+26520,0,724
+26530,0,1149
+26540,280,1757
+26550,636,2377
+26560,963,2414
+26570,1226,2007
+26580,2814,2808
+26590,2234,3626
+26600,3806,3140
+26610,2016,1767
+26620,2235,1650
+26630,2600,807
+26640,1243,420
+26650,921,0
+26660,489,0
+26670,0,0
+26680,0,0
+26690,0,0
+26700,0,0
+26710,0,0
+26720,0,0
+26730,0,0
+26740,0,0
+26750,0,0
+26760,0,450
+26770,351,651
+26780,578,1343
+26790,957,1283
+26800,1264,2592
+26810,1453,2307
+26820,3106,1942
+26830,3269,2689
+26840,2364,3554
+26850,2304,3128
+26860,1967,1858
+26870,1964,1416
+26880,1219,966
+26890,413,899
+26900,0,430
+26910,0,0
+26920,0,0
+26930,0,0
+26940,0,0
+26950,0,0
+26960,0,0
+26970,0,0
+26980,0,0
+26990,0,0
+27000,0,0
+27010,0,0
+27020,0,0
+27030,0,0
+27040,0,0
+27050,0,0
+27060,0,0
+27070,0,0
+27080,0,0
+27090,0,0
+27100,0,0
+27110,0,0
+27120,0,0
+27130,0,0
+27140,0,0
+27150,0,0
+27160,0,0
+27170,0,0
+27180,0,0
+27190,0,0
+27200,0,0
+27210,0,0
+27220,0,0
+27230,0,0
+27240,0,0
+27250,0,0
+27260,0,0
+27270,0,0
+27280,0,0
+27290,0,0
+27300,414,385
+27310,606,600
+27320,754,949
+27330,1046,910
+27340,1254,2274
+27350,2581,2761
+27360,2335,2150
+27370,2750,2202
+27380,3813,2614
+27390,2768,3839
+27400,3188,3027
+27410,2268,2079
+27420,2427,1985
+27430,1903,1472
+27440,954,710
+27450,614,425
+27460,568,0
+27470,0,0
+27480,0,0
+27490,0,0
+27500,0,0
+27510,0,0
+27520,0,0
+27530,0,0
+27540,0,0
+27550,0,0
+27560,227,374
+27570,561,534
+27580,1109,979
+27590,1528,1497
+27600,1944,1351
+27610,3434,1872
+27620,3581,2668
+27630,3020,2412
+27640,2246,3823
+27650,2572,3347
+27660,1098,2180
+27670,1132,2118
+27680,534,1300
+27690,0,1029
+27700,0,543
+27710,0,0
+27720,0,0
+27730,0,0
+27740,0,0
+27750,0,0
+27760,0,0
+27770,181,0
+27780,409,0
+27790,730,0
+27800,1241,0
+27810,1118,291
+27820,1937,396
+27830,1999,899
+27840,2373,808
+27850,2154,1677
+27860,2162,2622
+27870,2571,2040
+27880,2921,2083
+27890,1929,2629
+27900,1553,3230
+27910,1292,2447
+27920,1010,1801
+27930,717,1054
+27940,0,1111
+27950,0,708
+27960,0,0
+27970,0,0
+27980,0,0
+27990,0,0
+28000,0,0
+28010,0,0
+28020,0,0
+28030,344,0
+28040,496,0
+28050,1170,0
+28060,1477,0
+28070,1869,369
+28080,2481,800
+28090,2012,1073
+28100,3486,1030
+28110,3582,1595
+28120,3176,2013
+28130,1773,3631
+28140,1541,3705
+28150,907,2308
+28160,488,3123
+28170,0,1435
+28180,0,1256
+28190,0,701
+28200,0,618
+28210,0,0
+28220,0,0
+28230,0,0
+28240,0,0
+28250,0,0
+28260,447,0
+28270,894,0
+28280,1596,0
+28290,1262,0
+28300,1698,241
+28310,3161,749
+28320,3303,827
+28330,2710,1517
+28340,2260,1440
+28350,1940,1937
+28360,1607,2454
+28370,972,3603
+28380,522,2945
+28390,0,2882
+28400,0,2367
+28410,0,2293
+28420,0,2077
+28430,0,861
+28440,0,865
+28450,0,326
+28460,0,0
+28470,0,0
+28480,0,0
+28490,258,0
+28500,818,0
+28510,719,0
+28520,1211,0
+28530,2700,0
+28540,2885,272
+28550,3344,504
+28560,3553,855
+28570,2576,1337
+28580,1496,1652
+28590,1193,1708
+28600,745,3418
+28610,514,3630
+28620,0,2969
+28630,0,3114
+28640,0,2770
+28650,0,2045
+28660,0,1619
+28670,0,1038
+28680,0,1148
+28690,0,768
+28700,0,0
+28710,0,0
+28720,0,0
+28730,275,0
+28740,476,0
+28750,677,0
+28760,1060,0
+28770,1551,0
+28780,2897,0
+28790,2804,323
+28800,2028,768
+28810,3221,1265
+28820,2136,1693
+28830,2660,1455
+28840,1784,2131
+28850,1718,1941
+28860,785,2786
+28870,470,2048
+28880,0,2051
+28890,0,2746
+28900,0,1788
+28910,0,1441
+28920,0,1046
+28930,0,389
+28940,0,0
+28950,0,0
+28960,0,0
+28970,0,0
+28980,373,0
+28990,754,0
+29000,703,0
+29010,1486,0
+29020,1429,0
+29030,2410,315
+29040,2500,400
+29050,2773,953
+29060,2631,1115
+29070,3123,2186
+29080,2117,2008
+29090,1693,2445
+29100,2042,3332
+29110,817,2664
+29120,498,2425
+29130,526,1237
+29140,0,1572
+29150,0,929
+29160,0,574
+29170,0,0
+29180,0,0
+29190,0,0
+29200,0,0
+29210,0,0
+29220,0,0
+29230,246,0
+29240,525,0
+29250,836,394
+29260,1121,711
+29270,2134,717
+29280,2229,1608
+29290,1903,2858
+29300,2415,2054
+29310,3582,3302
+29320,2478,2300
+29330,3084,2784
+29340,2236,2480
+29350,1498,973
+29360,1495,754
+29370,830,439
+29380,719,0
+29390,0,0
+29400,0,0
+29410,0,0
+29420,0,0
+29430,0,0
+29440,0,0
+29450,0,0
+29460,0,0
+29470,0,245
+29480,360,524
+29490,472,980
+29500,600,1035
+29510,832,1970
+29520,1170,2987
+29530,2504,3113
+29540,1751,3278
+29550,2716,2533
+29560,2333,2585
+29570,3808,2876
+29580,3213,1578
+29590,2370,1477
+29600,1456,676
+29610,1880,422
+29620,1256,0
+29630,992,0
+29640,587,0
+29650,0,0
+29660,0,0
+29670,0,0
+29680,0,0
+29690,0,0
+29700,0,307
+29710,0,485
+29720,0,892
+29730,0,1131
+29740,0,1963
+29750,0,2239
+29760,215,3186
+29770,480,3142
+29780,631,2810
+29790,1224,3124
+29800,1071,1982
+29810,2072,1558
+29820,2534,1906
+29830,2419,1594
+29840,3803,1347
+29850,2580,844
+29860,3417,529
+29870,1825,0
+29880,2180,0
+29890,1627,0
+29900,1278,0
+29910,729,0
+29920,0,0
+29930,0,0
+29940,0,0
+29950,0,0
+29960,0,374
+29970,0,737
+29980,0,1235
+29990,0,1734
+30000,0,2329
+30010,0,2253
+30020,359,2547
+30030,433,3391
+30040,1051,2073
+30050,1166,2391
+30060,1770,1339
+30070,1823,1216
+30080,1977,765
+30090,2342,596
+30100,3620,0
+30110,2187,0
+30120,2123,0
+30130,2570,0
+30140,1656,0
+30150,814,0
+30160,729,0
+30170,522,0
+30180,0,0
+30190,0,306
+30200,0,727
+30210,0,776
+30220,0,1116
+30230,0,2000
+30240,0,2615
+30250,0,3520
+30260,0,3740
+30270,0,3329
+30280,0,2480
+30290,429,2007
+30300,652,1602
+30310,1119,1633
+30320,2010,1363
+30330,2294,635
+30340,2013,608
+30350,3199,0
+30360,3782,0
+30370,1988,0
+30380,1638,0
+30390,1548,0
+30400,1157,0
+30410,909,0
+30420,529,0
+30430,0,0
+30440,0,286
+30450,0,798
+30460,0,1242
+30470,0,1708
+30480,0,2275
+30490,0,2494
+30500,0,2541
+30510,234,3282
+30520,471,2650
+30530,766,3237
+30540,1532,1949
+30550,1748,2483
+30560,2951,1149
+30570,2989,1373
+30580,2149,852
+30590,2165,473
+30600,3327,0
+30610,1733,0
+30620,1540,0
+30630,641,0
+30640,505,0
+30650,0,0
+30660,0,0
+30670,0,0
+30680,0,0
+30690,0,205
+30700,0,550
+30710,0,668
+30720,0,1008
+30730,0,1413
+30740,0,2341
+30750,0,1698
+30760,377,2990
+30770,738,3434
+30780,1046,3123
+30790,1343,3327
+30800,1506,1854
+30810,3134,1671
+30820,3491,1599
+30830,3591,1095
+30840,3050,498
+30850,2910,0
+30860,2346,0
+30870,1337,0
+30880,1431,0
+30890,1047,0
+30900,361,0
+30910,0,0
+30920,0,0
+30930,0,0
+30940,0,0
+30950,0,366
+30960,0,577
+30970,0,991
+30980,0,1604
+30990,0,1816
+31000,0,2989
+31010,0,3573
+31020,390,3335
+31030,682,2712
+31040,1346,2798
+31050,1887,2066
+31060,1404,2342
+31070,3035,1082
+31080,2115,1164
+31090,3050,657
+31100,2270,0
+31110,2352,0
+31120,1777,0
+31130,1394,0
+31140,1160,0
+31150,1156,0
+31160,640,0
+31170,0,0
+31180,0,0
+31190,0,354
+31200,0,493
+31210,0,586
+31220,0,952
+31230,0,1116
+31240,0,1956
+31250,0,1821
+31260,0,3239
+31270,0,2499
+31280,237,3467
+31290,500,3432
+31300,1255,2614
+31310,1940,2459
+31320,2112,2080
+31330,3318,932
+31340,2523,1060
+31350,3707,586
+31360,1962,0
+31370,1876,0
+31380,1326,0
+31390,1213,0
+31400,871,0
+31410,0,0
+31420,0,0
+31430,0,0
+31440,0,0
+31450,0,0
+31460,0,0
+31470,0,0
+31480,0,428
+31490,0,551
+31500,323,1142
+31510,502,1126
+31520,754,1732
+31530,1684,2382
+31540,1857,2406
+31550,2869,3502
+31560,3042,3456
+31570,2167,1998
+31580,2710,1639
+31590,2411,1413
+31600,1249,922
+31610,1127,362
+31620,816,0
+31630,0,0
+31640,0,0
+31650,0,0
+31660,0,0
+31670,0,0
+31680,0,0
+31690,0,0
+31700,0,0
+31710,0,0
+31720,0,0
+31730,0,0
+31740,0,0
+31750,0,0
+31760,0,0
+31770,0,0
+31780,0,0
+31790,0,0
+31800,0,0
+31810,0,0
+31820,0,0
+31830,0,0
+31840,0,0
+31850,0,0
+31860,0,0
+31870,0,0
+31880,0,0
+31890,0,0
+31900,0,0
+31910,0,0
+31920,0,0
+31930,0,0
+31940,0,0
+31950,0,0
+31960,0,0
+31970,0,0
+31980,0,0
+31990,0,0
+32000,0,352
+32010,0,556
+32020,0,1112
+32030,0,1319
+32040,0,2006
+32050,0,3145
+32060,0,2140
+32070,0,2962
+32080,431,2031
+32090,530,3224
+32100,842,2300
+32110,1730,1674
+32120,2254,1155
+32130,2934,908
+32140,3680,442
+32150,3238,0
+32160,2595,0
+32170,3196,0
+32180,2742,0
+32190,1683,0
+32200,777,0
+32210,887,0
+32220,539,0
+32230,0,211
+32240,0,489
+32250,0,990
+32260,0,914
+32270,0,1590
+32280,0,1525
+32290,0,3107
+32300,0,3170
+32310,180,2762
+32320,348,3054
+32330,568,2415
+32340,1652,2304
+32350,1964,1401
+32360,2749,1932
+32370,2900,1379
+32380,2519,573
+32390,3289,358
+32400,2187,0
+32410,2683,0
+32420,1823,0
+32430,1149,0
+32440,844,0
+32450,503,0
+32460,0,0
+32470,0,0
+32480,0,257
+32490,0,508
+32500,0,867
+32510,0,1446
+32520,0,2405
+32530,0,3067
+32540,0,2723
+32550,217,2281
+32560,578,2601
+32570,1212,3364
+32580,1575,2590
+32590,2061,1364
+32600,2157,1468
+32610,3165,577
+32620,3959,480
+32630,2500,0
+32640,2487,0
+32650,1654,0
+32660,1749,0
+32670,1159,0
+32680,651,0
+32690,0,0
+32700,0,0
+32710,0,0
+32720,0,0
+32730,0,0
+32740,0,213
+32750,0,470
+32760,0,562
+32770,0,968
+32780,0,1724
+32790,0,1461
+32800,0,1694
+32810,383,2791
+32820,584,2559
+32830,1009,2353
+32840,1059,3095
+32850,2235,2590
+32860,1719,1304
+32870,2162,1259
+32880,2883,930
+32890,2813,370
+32900,2724,0
+32910,2305,0
+32920,1505,0
+32930,1244,0
+32940,1031,0
+32950,467,0
+32960,0,0
+32970,0,0
+32980,0,0
+32990,0,0
+33000,0,242
+33010,0,550
+33020,0,934
+33030,0,1478
+33040,0,1282
+33050,254,1738
+33060,629,2977
+33070,935,2866
+33080,1070,3242
+33090,1965,2559
+33100,2725,2071
+33110,2351,1587
+33120,3865,1504
+33130,2815,705
+33140,2176,376
+33150,3044,0
+33160,1954,0
+33170,1663,0
+33180,1225,0
+33190,571,0
+33200,421,0
+33210,0,0
+33220,0,0
+33230,0,220
+33240,0,377
+33250,0,870
+33260,0,984
+33270,0,1945
+33280,0,1961
+33290,0,3347
+33300,265,1927
+33310,559,3818
+33320,694,2610
+33330,1179,3214
+33340,1861,2899
+33350,2020,1955
+33360,2140,1149
+33370,2192,696
+33380,2053,771
+33390,2276,285
+33400,2851,0
+33410,1529,0
+33420,1440,0
+33430,796,0
+33440,409,0
+33450,0,0
+33460,0,0
+33470,0,0
+33480,0,0
+33490,0,0
+33500,0,0
+33510,0,0
+33520,0,418
+33530,0,779
+33540,0,893
+33550,0,1284
+33560,339,2394
+33570,930,3266
+33580,1401,2620
+33590,2075,2636
+33600,1669,2136
+33610,3204,1804
+33620,2604,1801
+33630,3901,1422
+33640,2972,1000
+33650,2783,499
+33660,1660,0
+33670,1080,0
+33680,1062,0
+33690,418,0
+33700,0,0
+33710,0,0
+33720,0,0
+33730,0,0
+33740,0,0
+33750,0,282
+33760,0,445
+33770,0,1229
+33780,243,1025
+33790,436,1730
+33800,693,3255
+33810,1082,2456
+33820,1934,2930
+33830,1767,3387
+33840,2487,2660
+33850,1924,2301
+33860,2176,1663
+33870,3865,1016
+33880,2524,700
+33890,2325,547
+33900,1780,0
+33910,1775,0
+33920,760,0
+33930,473,0
+33940,0,0
+33950,0,0
+33960,0,0
+33970,0,0
+33980,0,0
+33990,0,0
+34000,0,0
+34010,0,251
+34020,0,327
+34030,0,688
+34040,0,1570
+34050,0,1778
+34060,379,2127
+34070,592,3272
+34080,756,2377
+34090,1134,2272
+34100,1880,3385
+34110,2690,2650
+34120,3182,1931
+34130,3759,1195
+34140,3326,548
+34150,2436,0
+34160,1821,0
+34170,1068,0
+34180,562,0
+34190,0,0
+34200,0,0
+34210,0,0
+34220,0,0
+34230,0,0
+34240,0,0
+34250,0,0
+34260,0,0
+34270,0,241
+34280,398,545
+34290,423,910
+34300,1200,1943
+34310,1020,1706
+34320,1770,2220
+34330,1745,3486
+34340,3412,1990
+34350,2486,2745
+34360,3484,2049
+34370,2618,1123
+34380,3027,686
+34390,1999,479
+34400,1516,0
+34410,840,0
+34420,771,0
+34430,366,0
+34440,0,0
+34450,0,0
+34460,0,0
+34470,0,0
+34480,0,0
+34490,0,0
+34500,0,338
+34510,0,552
+34520,307,949
+34530,579,804
+34540,755,1483
+34550,1334,2628
+34560,2127,2769
+34570,2214,2383
+34580,2088,3568
+34590,2782,3044
+34600,2798,2971
+34610,2254,1641
+34620,2075,2026
+34630,1239,995
+34640,627,1121
+34650,499,488
+34660,0,451
+34670,0,0
+34680,0,0
+34690,0,0
+34700,0,0
+34710,0,0
+34720,0,0
+34730,0,0
+34740,0,0
+34750,322,0
+34760,617,0
+34770,995,333
+34780,1051,325
+34790,1477,549
+34800,3306,1359
+34810,2096,1252
+34820,2806,2076
+34830,2636,2544
+34840,3001,2700
+34850,1759,2251
+34860,1473,2962
+34870,1201,2100
+34880,499,2015
+34890,0,1752
+34900,0,819
+34910,0,551
+34920,0,463
+34930,0,0
+34940,0,0
+34950,0,0
+34960,0,0
+34970,0,0
+34980,0,0
+34990,0,0
+35000,454,0
+35010,486,371
+35020,785,603
+35030,1514,580
+35040,1906,1100
+35050,2205,1644
+35060,2748,2456
+35070,2051,1920
+35080,2083,3241
+35090,1950,3065
+35100,1878,2465
+35110,2334,2851
+35120,1380,2372
+35130,1335,1973
+35140,721,959
+35150,460,821
+35160,0,678
+35170,0,0
+35180,0,0
+35190,0,0
+35200,0,0
+35210,0,0
+35220,0,0
+35230,0,0
+35240,0,0
+35250,0,0
+35260,0,0
+35270,351,317
+35280,490,364
+35290,1118,893
+35300,1958,1622
+35310,2115,2177
+35320,1958,1908
+35330,2585,2508
+35340,3903,2221
+35350,3066,3283
+35360,2151,1652
+35370,1488,1648
+35380,1286,861
+35390,982,681
+35400,635,0
+35410,0,0
+35420,0,0
+35430,0,0
+35440,0,0
+35450,0,0
+35460,0,0
+35470,0,0
+35480,0,0
+35490,0,0
+35500,0,0
+35510,0,277
+35520,504,692
+35530,642,763
+35540,1293,1621
+35550,1590,1825
+35560,2379,3002
+35570,3446,2139
+35580,3039,2664
+35590,2974,3437
+35600,2505,2710
+35610,2246,2214
+35620,1439,1643
+35630,753,1136
+35640,486,1231
+35650,0,800
+35660,0,642
+35670,0,0
+35680,0,0
+35690,0,0
+35700,0,0
+35710,0,0
+35720,0,0
+35730,0,0
+35740,418,0
+35750,398,0
+35760,857,328
+35770,1226,766
+35780,2106,1355
+35790,1747,1481
+35800,2615,1766
+35810,3164,2021
+35820,2604,1874
+35830,3329,2016
+35840,3017,3322
+35850,2172,1972
+35860,1733,2973
+35870,1061,1359
+35880,644,1666
+35890,0,973
+35900,0,655
+35910,0,0
+35920,0,0
+35930,0,0
+35940,0,0
+35950,0,0
+35960,0,0
+35970,184,0
+35980,513,0
+35990,775,0
+36000,1007,0
+36010,2213,0
+36020,2415,407
+36030,3134,498
+36040,3867,1280
+36050,3587,999
+36060,3038,2181
+36070,2684,2299
+36080,1702,1900
+36090,1817,3373
+36100,771,3481
+36110,760,2139
+36120,426,3399
+36130,0,1905
+36140,0,2013
+36150,0,1132
+36160,0,952
+36170,0,563
+36180,0,343
+36190,0,0
+36200,0,0
+36210,0,0
+36220,214,0
+36230,486,0
+36240,841,0
+36250,1336,0
+36260,1757,0
+36270,2508,0
+36280,3150,0
+36290,2648,219
+36300,2300,302
+36310,3587,556
+36320,3000,1475
+36330,1862,1374
+36340,1209,2906
+36350,1358,1905
+36360,1148,3723
+36370,633,2542
+36380,0,2401
+36390,0,2597
+36400,0,1784
+36410,0,933
+36420,0,619
+36430,0,0
+36440,0,0
+36450,0,0
+36460,0,0
+36470,0,0
+36480,0,0
+36490,0,0
+36500,0,0
+36510,0,0
+36520,0,0
+36530,0,0
+36540,0,0
+36550,0,0
+36560,0,0
+36570,0,0
+36580,0,0
+36590,0,0
+36600,0,0
+36610,0,0
+36620,0,0
+36630,0,0
+36640,0,0
+36650,0,0
+36660,0,0
+36670,0,0
+36680,0,0
+36690,0,0
+36700,0,0
+36710,0,0
+36720,0,0
+36730,0,0
+36740,0,0
+36750,0,0
+36760,0,0
+36770,0,0
+36780,0,0
+36790,179,0
+36800,341,0
+36810,479,0
+36820,1046,0
+36830,1353,0
+36840,1932,0
+36850,1915,0
+36860,2685,0
+36870,3406,0
+36880,2367,264
+36890,2535,321
+36900,2287,549
+36910,2131,1486
+36920,1410,2039
+36930,1142,2889
+36940,548,1897
+36950,0,2000
+36960,0,3691
+36970,0,2041
+36980,0,2479
+36990,0,1847
+37000,0,894
+37010,0,635
+37020,0,369
+37030,0,0
+37040,0,0
+37050,0,0
+37060,0,0
+37070,485,0
+37080,791,0
+37090,1143,0
+37100,1633,0
+37110,1332,0
+37120,2692,0
+37130,2074,0
+37140,3016,433
+37150,2413,577
+37160,2953,1277
+37170,2195,1570
+37180,2263,1669
+37190,1533,3119
+37200,1126,3309
+37210,673,2187
+37220,396,2679
+37230,0,3170
+37240,0,1910
+37250,0,1262
+37260,0,819
+37270,0,655
+37280,0,381
+37290,0,0
+37300,0,0
+37310,0,0
+37320,504,0
+37330,529,0
+37340,1066,0
+37350,2063,0
+37360,2211,0
+37370,2365,0
+37380,3048,0
+37390,2713,0
+37400,3131,327
+37410,2808,418
+37420,2290,588
+37430,1487,1288
+37440,934,1366
+37450,536,2203
+37460,0,2979
+37470,0,2732
+37480,0,3594
+37490,0,2847
+37500,0,3115
+37510,0,3140
+37520,0,2459
+37530,0,1218
+37540,0,834
+37550,0,570
+37560,464,540
+37570,766,0
+37580,915,0
+37590,1720,0
+37600,2124,0
+37610,2083,0
+37620,3860,0
+37630,2496,0
+37640,2395,0
+37650,2499,0
+37660,1619,0
+37670,1086,0
+37680,1129,400
+37690,682,410
+37700,0,1193
+37710,0,1653
+37720,0,1401
+37730,0,1684
+37740,0,2554
+37750,0,3788
+37760,0,3201
+37770,0,2824
+37780,243,3035
+37790,562,2163
+37800,643,1767
+37810,1266,1073
+37820,1238,580
+37830,2267,489
+37840,2787,0
+37850,2775,0
+37860,2623,0
+37870,2962,0
+37880,2534,0
+37890,2589,0
+37900,2658,0
+37910,2057,0
+37920,1383,0
+37930,901,0
+37940,422,231
+37950,0,511
+37960,0,623
+37970,0,1112
+37980,0,989
+37990,0,2385
+38000,0,2660
+38010,0,3442
+38020,0,3649
+38030,0,3481
+38040,0,3795
+38050,0,3246
+38060,201,1609
+38070,345,1663
+38080,868,1587
+38090,1483,844
+38100,1964,405
+38110,2271,0
+38120,3655,0
+38130,2807,0
+38140,3467,0
+38150,2166,0
+38160,2418,0
+38170,1505,0
+38180,946,0
+38190,331,0
+38200,0,0
+38210,0,433
+38220,0,562
+38230,0,922
+38240,0,984
+38250,0,1365
+38260,0,2042
+38270,0,3118
+38280,0,2633
+38290,0,1984
+38300,328,3059
+38310,485,2805
+38320,996,1937
+38330,2263,1247
+38340,2648,948
+38350,1989,563
+38360,2700,0
+38370,3777,0
+38380,2470,0
+38390,2241,0
+38400,1200,0
+38410,1070,0
+38420,518,0
+38430,0,0
+38440,0,0
+38450,0,249
+38460,0,534
+38470,0,855
+38480,0,1061
+38490,0,2457
+38500,0,2718
+38510,227,3580
+38520,379,2691
+38530,918,2924
+38540,1141,2527
+38550,1304,2183
+38560,2597,1544
+38570,2551,931
+38580,3168,358
+38590,2531,0
+38600,2266,0
+38610,2076,0
+38620,2682,0
+38630,2390,0
+38640,1683,0
+38650,949,0
+38660,646,0
+38670,347,0
+38680,0,0
+38690,0,390
+38700,0,751
+38710,0,1202
+38720,0,1237
+38730,0,1957
+38740,0,2194
+38750,0,3126
+38760,0,3365
+38770,0,3269
+38780,0,2539
+38790,367,1799
+38800,706,2316
+38810,887,1704
+38820,1035,999
+38830,1899,779
+38840,1813,0
+38850,1834,0
+38860,2129,0
+38870,2846,0
+38880,2584,0
+38890,3020,0
+38900,2172,0
+38910,1267,0
+38920,1428,0
+38930,1028,0
+38940,418,373
+38950,0,408
+38960,0,614
+38970,0,1471
+38980,0,2357
+38990,0,1767
+39000,0,2717
+39010,0,3662
+39020,0,2954
+39030,0,3539
+39040,0,2512
+39050,0,2583
+39060,150,1219
+39070,571,797
+39080,786,865
+39090,1560,452
+39100,1343,0
+39110,2156,0
+39120,3041,0
+39130,3884,0
+39140,3738,0
+39150,3364,0
+39160,2159,0
+39170,1611,0
+39180,1223,0
+39190,545,0
+39200,0,0
+39210,0,235
+39220,0,372
+39230,0,633
+39240,0,1400
+39250,0,1258
+39260,0,1347
+39270,0,2391
+39280,0,2229
+39290,431,3071
+39300,801,3310
+39310,807,2641
+39320,1196,2406
+39330,1567,2629
+39340,2532,1400
+39350,1895,903
+39360,3774,854
+39370,3569,551
+39380,2076,0
+39390,2820,0
+39400,2227,0
+39410,1343,0
+39420,1213,0
+39430,927,0
+39440,578,0
+39450,0,0
+39460,0,0
+39470,0,0
+39480,0,0
+39490,0,465
+39500,0,692
+39510,0,1003
+39520,0,1090
+39530,281,2518
+39540,563,2507
+39550,519,2763
+39560,1537,2644
+39570,2231,2178
+39580,1782,2629
+39590,2588,2542
+39600,3770,1838
+39610,3662,790
+39620,2109,557
+39630,2889,427
+39640,2157,0
+39650,790,0
+39660,883,0
+39670,380,0
+39680,0,0
+39690,0,0
+39700,0,0
+39710,0,0
+39720,0,327
+39730,0,399
+39740,0,945
+39750,0,1368
+39760,397,1188
+39770,546,2775
+39780,667,1938
+39790,1592,3530
+39800,1953,3234
+39810,2800,3791
+39820,3195,2777
+39830,3690,2333
+39840,2584,1433
+39850,3735,1507
+39860,1892,806
+39870,1597,524
+39880,1683,0
+39890,1558,0
+39900,1141,0
+39910,526,0
+39920,583,0
+39930,0,0
+39940,0,0
+39950,0,0
+39960,0,0
+39970,0,0
+39980,0,254
+39990,0,641
+40000,0,1076
+40010,250,1417
+40020,439,2630
+40030,834,2259
+40040,1443,2630
+40050,1901,3689
+40060,2411,2005
+40070,2779,3018
+40080,3800,1591
+40090,3849,1735
+40100,2017,892
+40110,2749,486
+40120,1689,0
+40130,1144,0
+40140,629,0
+40150,343,0
+40160,0,0
+40170,0,0
+40180,0,0
+40190,0,0
+40200,0,0
+40210,0,0
+40220,0,0
+40230,0,292
+40240,0,610
+40250,0,939
+40260,333,1426
+40270,385,1840
+40280,794,2757
+40290,1075,3116
+40300,2271,2736
+40310,2433,3378
+40320,3181,2547
+40330,2525,1586
+40340,3286,1164
+40350,1933,1281
+40360,1278,750
+40370,1555,0
+40380,759,0
+40390,589,0
+40400,0,0
+40410,0,0
+40420,0,0
+40430,0,0
+40440,0,0
+40450,0,0
+40460,0,0
+40470,0,0
+40480,0,313
+40490,0,444
+40500,0,955
+40510,199,1412
+40520,453,2571
+40530,832,2820
+40540,1374,3838
+40550,1334,3636
+40560,1733,2464
+40570,2884,2579
+40580,3684,2025
+40590,3290,1336
+40600,3541,1115
+40610,2220,443
+40620,2256,0
+40630,1252,0
+40640,869,0
+40650,454,0
+40660,0,0
+40670,0,0
+40680,0,0
+40690,0,0
+40700,0,0
+40710,0,0
+40720,0,172
+40730,0,582
+40740,0,599
+40750,0,1196
+40760,0,1296
+40770,0,2730
+40780,279,3088
+40790,586,3457
+40800,1063,3458
+40810,985,2625
+40820,1424,1753
+40830,1713,1561
+40840,2618,1451
+40850,2578,708
+40860,3916,427
+40870,2428,0
+40880,1770,0
+40890,1899,0
+40900,1667,0
+40910,1113,0
+40920,919,0
+40930,736,0
+40940,0,0
+40950,0,0
+40960,0,0
+40970,0,0
+40980,0,0
+40990,0,254
+41000,0,778
+41010,0,1209
+41020,0,1345
+41030,327,1599
+41040,820,2410
+41050,840,3485
+41060,1168,3596
+41070,1899,2317
+41080,2592,2996
+41090,2777,1904
+41100,1953,1180
+41110,2534,1302
+41120,3244,662
+41130,2425,283
+41140,2595,0
+41150,1515,0
+41160,979,0
+41170,1098,0
+41180,486,0
+41190,399,0
+41200,0,0
+41210,0,0
+41220,0,0
+41230,0,0
+41240,0,0
+41250,0,0
+41260,0,0
+41270,0,0
+41280,0,0
+41290,0,0
+41300,0,0
+41310,0,0
+41320,0,0
+41330,0,0
+41340,0,0
+41350,0,0
+41360,0,0
+41370,0,0
+41380,0,0
+41390,0,0
+41400,0,0
+41410,0,0
+41420,0,0
+41430,0,0
+41440,0,0
+41450,0,0
+41460,0,0
+41470,0,0
+41480,0,474
+41490,0,663
+41500,0,1153
+41510,0,1511
+41520,0,1931
+41530,0,1706
+41540,0,3185
+41550,0,3732
+41560,355,2919
+41570,524,2806
+41580,986,2858
+41590,1701,2283
+41600,2012,2197
+41610,3189,1224
+41620,3597,624
+41630,3805,692
+41640,3377,0
+41650,3208,0
+41660,2005,0
+41670,2041,0
+41680,977,0
+41690,977,0
+41700,718,0
+41710,0,0
+41720,0,0
+41730,0,254
+41740,0,327
+41750,0,880
+41760,0,1577
+41770,0,1344
+41780,0,2578
+41790,0,3738
+41800,0,2885
+41810,0,3512
+41820,0,2578
+41830,454,1856
+41840,496,1680
+41850,809,824
+41860,1385,552
+41870,1411,0
+41880,1784,0
+41890,3425,0
+41900,2184,0
+41910,2978,0
+41920,2252,0
+41930,2603,0
+41940,2678,0
+41950,1965,0
+41960,1255,351
+41970,907,614
+41980,718,611
+41990,0,1336
+42000,0,1746
+42010,0,1679
+42020,0,3008
+42030,0,3533
+42040,0,2262
+42050,0,2130
+42060,0,2403
+42070,0,2386
+42080,0,1771
+42090,0,1268
+42100,0,985
+42110,376,422
+42120,805,0
+42130,983,0
+42140,1728,0
+42150,2041,0
+42160,2989,0
+42170,3168,0
+42180,3683,0
+42190,2383,0
+42200,3119,0
+42210,1819,0
+42220,1881,0
+42230,1384,332
+42240,815,693
+42250,573,790
+42260,0,1181
+42270,0,1829
+42280,0,2784
+42290,0,2930
+42300,0,3407
+42310,0,2506
+42320,0,2499
+42330,0,2510
+42340,0,3031
+42350,293,1649
+42360,330,1821
+42370,802,832
+42380,1189,613
+42390,1798,0
+42400,2622,0
+42410,2491,0
+42420,2139,0
+42430,3825,0
+42440,2830,0
+42450,1661,0
+42460,1202,0
+42470,1449,0
+42480,774,0
+42490,552,396
+42500,0,418
+42510,0,661
+42520,0,1211
+42530,0,1882
+42540,0,2545
+42550,0,3355
+42560,0,3431
+42570,0,3047
+42580,0,2502
+42590,0,3185
+42600,386,2441
+42610,537,1716
+42620,1129,1492
+42630,1616,703
+42640,1614,575
+42650,2010,0
+42660,2587,0
+42670,3745,0
+42680,2892,0
+42690,2370,0
+42700,1447,0
+42710,1593,0
+42720,755,0
+42730,659,0
+42740,0,0
+42750,0,0
+42760,0,359
+42770,0,687
+42780,0,878
+42790,0,1370
+42800,0,1402
+42810,0,1773
+42820,0,2128
+42830,0,3182
+42840,386,3461
+42850,616,2482
+42860,706,2899
+42870,1807,1905
+42880,1935,2103
+42890,2317,1306
+42900,3374,583
+42910,2096,547
+42920,2894,0
+42930,2071,0
+42940,2382,0
+42950,1764,0
+42960,1335,0
+42970,976,0
+42980,413,0
+42990,0,0
+43000,0,346
+43010,0,779
+43020,0,1309
+43030,0,1195
+43040,0,2115
+43050,0,2706
+43060,0,3347
+43070,0,3283
+43080,315,2423
+43090,526,2603
+43100,987,1258
+43110,1608,1350
+43120,1607,628
+43130,2714,0
+43140,1873,0
+43150,2786,0
+43160,2489,0
+43170,3324,0
+43180,1532,0
+43190,2093,0
+43200,1037,0
+43210,889,0
+43220,363,0
+43230,0,274
+43240,0,480
+43250,0,870
+43260,0,1337
+43270,0,2348
+43280,0,2604
+43290,0,3165
+43300,0,2833
+43310,0,3326
+43320,0,3150
+43330,0,2833
+43340,237,2089
+43350,567,1204
+43360,859,639
+43370,1614,545
+43380,1365,0
+43390,2504,0
+43400,1981,0
+43410,3899,0
+43420,3755,0
+43430,1902,0
+43440,2905,0
+43450,1696,0
+43460,837,0
+43470,686,0
+43480,454,348
+43490,0,488
+43500,0,1040
+43510,0,882
+43520,0,1875
+43530,0,1740
+43540,0,1893
+43550,0,2765
+43560,0,2254
+43570,195,1870
+43580,587,2477
+43590,667,1178
+43600,1667,932
+43610,2209,627
+43620,2974,507
+43630,3232,0
+43640,3953,0
+43650,2603,0
+43660,2061,0
+43670,2088,0
+43680,996,0
+43690,612,0
+43700,0,0
+43710,0,0
+43720,0,0
+43730,0,488
+43740,0,507
+43750,0,851
+43760,0,1584
+43770,0,1553
+43780,0,1766
+43790,0,3328
+43800,0,2077
+43810,373,3411
+43820,638,3454
+43830,992,2241
+43840,1046,1751
+43850,1438,2025
+43860,1452,1314
+43870,2766,605
+43880,3683,520
+43890,3323,0
+43900,3766,0
+43910,2904,0
+43920,1878,0
+43930,2290,0
+43940,2009,0
+43950,1059,0
+43960,601,0
+43970,408,0
+43980,0,0
+43990,0,0
+44000,0,496
+44010,0,651
+44020,0,753
+44030,0,1587
+44040,0,2443
+44050,0,2708
+44060,0,3243
+44070,0,3122
+44080,0,3737
+44090,0,2269
+44100,393,2818
+44110,645,2150
+44120,1162,1532
+44130,1262,953
+44140,2610,553
+44150,2225,412
+44160,3679,0
+44170,1970,0
+44180,3118,0
+44190,1455,0
+44200,1358,0
+44210,1014,0
+44220,695,0
+44230,0,0
+44240,0,0
+44250,0,0
+44260,0,0
+44270,0,372
+44280,0,632
+44290,0,998
+44300,0,1383
+44310,0,1437
+44320,445,2435
+44330,654,2869
+44340,922,2003
+44350,1419,3894
+44360,1705,2713
+44370,2829,2484
+44380,2214,2161
+44390,2452,2385
+44400,2939,1617
+44410,2579,1241
+44420,1967,684
+44430,1815,0
+44440,1315,0
+44450,1574,0
+44460,1036,0
+44470,606,0
+44480,0,0
+44490,0,0
+44500,0,0
+44510,0,0
+44520,0,0
+44530,0,0
+44540,0,357
+44550,0,894
+44560,0,1084
+44570,309,1047
+44580,334,1337
+44590,499,2871
+44600,1332,2349
+44610,1714,2404
+44620,1915,3219
+44630,2669,3416
+44640,3333,2775
+44650,3895,2297
+44660,3761,1698
+44670,3495,891
+44680,1607,719
+44690,1590,0
+44700,1164,0
+44710,721,0
+44720,488,0
+44730,435,0
+44740,0,0
+44750,0,0
+44760,0,0
+44770,0,0
+44780,0,0
+44790,0,0
+44800,0,0
+44810,0,296
+44820,0,802
+44830,189,1087
+44840,502,1664
+44850,951,2257
+44860,1545,2713
+44870,2216,2585
+44880,3114,3537
+44890,1998,3133
+44900,2501,2757
+44910,3348,2590
+44920,1719,1837
+44930,1819,968
+44940,1326,948
+44950,731,556
+44960,535,0
+44970,0,0
+44980,0,0
+44990,0,0
+45000,0,0
+45010,0,0
+45020,0,0
+45030,0,0
+45040,0,0
+45050,0,0
+45060,0,407
+45070,220,420
+45080,615,726
+45090,1136,1207
+45100,1693,1346
+45110,1406,2483
+45120,1931,1876
+45130,2931,2305
+45140,2586,3970
+45150,2300,2263
+45160,2852,3012
+45170,2275,2182
+45180,1397,1242
+45190,778,1105
+45200,359,557
+45210,0,479
+45220,0,0
+45230,0,0
+45240,0,0
+45250,0,0
+45260,0,0
+45270,0,0
+45280,0,0
+45290,0,0
+45300,322,0
+45310,571,0
+45320,1027,328
+45330,1449,689
+45340,2182,883
+45350,2017,1148
+45360,2233,1584
+45370,2575,2685
+45380,3775,3565
+45390,2135,2521
+45400,2700,1937
+45410,1977,2286
+45420,1169,2060
+45430,928,1809
+45440,539,1420
+45450,0,534
+45460,0,281
+45470,0,0
+45480,0,0
+45490,0,0
+45500,0,0
+45510,0,0
+45520,0,0
+45530,0,0
+45540,0,0
+45550,235,0
+45560,364,215
+45570,916,665
+45580,1778,760
+45590,2037,1413
+45600,3084,1295
+45610,3309,2831
+45620,3864,3625
+45630,2269,3266
+45640,2766,3189
+45650,2276,3275
+45660,1398,2424
+45670,1007,1421
+45680,580,1122
+45690,0,850
+45700,0,429
+45710,0,0
+45720,0,0
+45730,0,0
+45740,0,0
+45750,0,0
+45760,0,0
+45770,0,0
+45780,0,0
+45790,0,0
+45800,0,0
+45810,0,0
+45820,0,0
+45830,0,0
+45840,0,0
+45850,0,0
+45860,0,0
+45870,0,0
+45880,0,0
+45890,0,0
+45900,0,0
+45910,0,0
+45920,0,0
+45930,0,0
+45940,0,0
+45950,0,0
+45960,0,0
+45970,0,0
+45980,0,0
+45990,0,0
+46000,0,0
+46010,0,0
+46020,0,0
+46030,0,0
+46040,0,0
+46050,0,0
+46060,0,0
+46070,0,0
+46080,0,423
+46090,0,647
+46100,0,813
+46110,0,1988
+46120,0,2528
+46130,0,2356
+46140,183,3025
+46150,598,2432
+46160,650,3618
+46170,1415,1823
+46180,1675,1803
+46190,2231,879
+46200,2251,946
+46210,3345,462
+46220,3048,0
+46230,2629,0
+46240,3175,0
+46250,2309,0
+46260,1876,0
+46270,1164,0
+46280,942,0
+46290,666,0
+46300,356,0
+46310,0,0
+46320,0,0
+46330,0,0
+46340,0,0
+46350,0,0
+46360,0,0
+46370,0,0
+46380,0,0
+46390,0,0
+46400,0,0
+46410,456,0
+46420,643,0
+46430,932,0
+46440,1112,0
+46450,2225,0
+46460,1833,0
+46470,2289,0
+46480,3627,0
+46490,3448,0
+46500,2230,0
+46510,3236,0
+46520,2238,0
+46530,2363,0
+46540,975,0
+46550,1027,0
+46560,616,0
+46570,538,0
+46580,0,0
+46590,0,0
+46600,0,0
+46610,0,0
+46620,0,0
+46630,0,0
+46640,0,276
+46650,0,618
+46660,0,1114
+46670,0,1658
+46680,0,2046
+46690,314,1942
+46700,659,2674
+46710,1104,2428
+46720,1241,3431
+46730,2083,1808
+46740,2273,1860
+46750,2509,1003
+46760,2351,613
+46770,2279,0
+46780,2217,0
+46790,1010,0
+46800,1139,0
+46810,480,0
+46820,0,0
+46830,0,0
+46840,0,0
+46850,0,0
+46860,0,0
+46870,0,0
+46880,0,0
+46890,0,307
+46900,0,527
+46910,0,817
+46920,0,1977
+46930,310,2424
+46940,418,2784
+46950,556,2983
+46960,1216,2968
+46970,1945,2960
+46980,2703,2971
+46990,2105,2229
+47000,2056,1491
+47010,2542,586
+47020,2598,383
+47030,2740,0
+47040,2090,0
+47050,1530,0
+47060,664,0
+47070,751,0
+47080,0,0
+47090,0,0
+47100,0,0
+47110,0,0
+47120,0,312
+47130,0,462
+47140,0,837
+47150,0,1100
+47160,0,2389
+47170,240,2636
+47180,488,3033
+47190,1240,3870
+47200,1823,3189
+47210,2243,2175
+47220,3024,2815
+47230,3295,1101
+47240,3130,1224
+47250,2250,854
+47260,2302,0
+47270,1426,0
+47280,1532,0
+47290,978,0
+47300,386,0
+47310,0,0
+47320,0,0
+47330,0,0
+47340,0,0
+47350,0,0
+47360,0,0
+47370,0,205
+47380,0,398
+47390,0,946
+47400,222,1387
+47410,644,2160
+47420,1089,1616
+47430,900,2448
+47440,1533,2465
+47450,3181,3097
+47460,2261,3257
+47470,3969,2078
+47480,3363,1306
+47490,1854,1790
+47500,2291,944
+47510,1634,696
+47520,883,0
+47530,668,0
+47540,0,0
+47550,0,0
+47560,0,0
+47570,0,0
+47580,0,0
+47590,0,0
+47600,0,0
+47610,0,0
+47620,0,0
+47630,337,0
+47640,750,412
+47650,1076,531
+47660,1623,910
+47670,1513,1577
+47680,2186,1784
+47690,3498,2162
+47700,2443,2630
+47710,3123,2025
+47720,2523,2606
+47730,3031,3697
+47740,1814,2887
+47750,1522,1742
+47760,1179,1516
+47770,842,1276
+47780,578,620
+47790,0,720
+47800,0,0
+47810,0,0
+47820,0,0
+47830,0,0
+47840,0,0
+47850,0,0
+47860,0,0
+47870,0,0
+47880,291,0
+47890,497,0
+47900,783,0
+47910,1266,0
+47920,1944,234
+47930,3068,442
+47940,2823,1225
+47950,2946,1820
+47960,2148,2267
+47970,3198,2483
+47980,2520,2880
+47990,1626,2007
+48000,1360,3216
+48010,908,1890
+48020,380,1789
+48030,0,1897
+48040,0,1300
+48050,0,915
+48060,0,369
+48070,0,0
+48080,0,0
+48090,0,0
+48100,0,0
+48110,0,0
+48120,323,0
+48130,343,0
+48140,626,0
+48150,898,305
+48160,1360,518
+48170,1317,751
+48180,2451,1525
+48190,3397,1301
+48200,2403,1858
+48210,3513,2834
+48220,2194,3669
+48230,1740,3382
+48240,1825,3377
+48250,1780,2931
+48260,1645,1940
+48270,932,1357
+48280,605,685
+48290,0,449
+48300,0,0
+48310,0,0
+48320,0,0
+48330,0,0
+48340,0,0
+48350,0,0
+48360,0,0
+48370,0,0
+48380,0,0
+48390,470,299
+48400,654,597
+48410,933,557
+48420,1194,1431
+48430,1651,1972
+48440,2258,1751
+48450,2363,1852
+48460,2776,2907
+48470,3053,3429
+48480,3180,2423
+48490,1763,1572
+48500,1612,1262
+48510,1166,872
+48520,803,548
+48530,562,359
+48540,0,0
+48550,0,0
+48560,0,0
+48570,0,0
+48580,0,0
+48590,0,0
+48600,0,0
+48610,0,0
+48620,0,0
+48630,375,0
+48640,486,357
+48650,702,456
+48660,1432,956
+48670,2010,998
+48680,2090,2059
+48690,3323,3026
+48700,3274,2342
+48710,3297,3864
+48720,3474,2424
+48730,2773,2373
+48740,2382,1862
+48750,1563,1412
+48760,1806,1049
+48770,1361,542
+48780,697,0
+48790,385,0
+48800,0,0
+48810,0,0
+48820,0,0
+48830,0,0
+48840,0,0
+48850,0,0
+48860,0,0
+48870,0,0
+48880,0,276
+48890,0,493
+48900,309,660
+48910,768,1416
+48920,1052,2387
+48930,1441,1978
+48940,1516,3052
+48950,3067,1984
+48960,3496,2923
+48970,3071,2455
+48980,3295,1653
+48990,3700,691
+49000,1736,660
+49010,2059,0
+49020,1058,0
+49030,1354,0
+49040,904,0
+49050,323,0
+49060,0,0
+49070,0,0
+49080,0,0
+49090,0,0
+49100,0,0
+49110,0,0
+49120,0,261
+49130,0,460
+49140,0,531
+49150,0,1392
+49160,0,2195
+49170,444,1659
+49180,735,2515
+49190,1093,3882
+49200,1014,2957
+49210,2352,1925
+49220,1663,1816
+49230,2698,1850
+49240,2280,1157
+49250,3596,535
+49260,2815,0
+49270,1718,0
+49280,1107,0
+49290,1220,0
+49300,512,0
+49310,0,0
+49320,0,0
+49330,0,0
+49340,0,0
+49350,0,0
+49360,0,334
+49370,0,428
+49380,0,867
+49390,0,1846
+49400,239,2785
+49410,534,2566
+49420,1101,2743
+49430,1594,2771
+49440,2032,3263
+49450,1906,2396
+49460,3231,1067
+49470,3255,1000
+49480,2975,456
+49490,2804,0
+49500,1711,0
+49510,1340,0
+49520,617,0
+49530,0,0
+49540,0,0
+49550,0,0
+49560,0,0
+49570,0,255
+49580,0,349
+49590,0,522
+49600,0,1219
+49610,0,1362
+49620,0,2330
+49630,0,2875
+49640,0,3414
+49650,304,3073
+49660,476,2593
+49670,524,3732
+49680,783,2744
+49690,1683,1579
+49700,2217,1389
+49710,2432,1305
+49720,1943,996
+49730,2033,640
+49740,2982,0
+49750,3022,0
+49760,1856,0
+49770,1738,0
+49780,1044,0
+49790,548,0
+49800,506,0
+49810,0,0
+49820,0,0
+49830,0,0
+49840,0,474
+49850,0,784
+49860,0,1343
+49870,0,1855
+49880,0,2390
+49890,0,2439
+49900,0,3890
+49910,464,3913
+49920,801,1837
+49930,1309,2364
+49940,1661,2155
+49950,2814,1218
+49960,3631,551
+49970,3854,304
+49980,2286,0
+49990,1864,0
+50000,2545,0
+50010,1011,0
+50020,890,0
+50030,377,0
+50040,0,0
+50050,0,0
+50060,0,0
+50070,0,0
+50080,0,0
+50090,0,0
+50100,0,282
+50110,0,446
+50120,283,1076
+50130,472,1557
+50140,1033,1803
+50150,1437,2467
+50160,1601,2868
+50170,2937,3897
+50180,2532,2158
+50190,2534,2437
+50200,3566,1968
+50210,2520,2326
+50220,1629,1756
+50230,1617,1357
+50240,1660,809
+50250,783,385
+50260,496,0
+50270,0,0
+50280,0,0
+50290,0,0
+50300,0,0
+50310,0,0
+50320,0,0
+50330,0,0
+50340,0,200
+50350,0,467
+50360,386,1129
+50370,494,1552
+50380,1030,2496
+50390,1057,2596
+50400,1506,2229
+50410,2468,2317
+50420,3176,2163
+50430,3571,1827
+50440,2162,1781
+50450,3271,1327
+50460,3395,528
+50470,2606,0
+50480,2263,0
+50490,1004,0
+50500,765,0
+50510,667,0
+50520,414,0
+50530,0,0
+50540,0,0
+50550,0,0
+50560,0,0
+50570,0,0
+50580,0,0
+50590,0,271
+50600,0,444
+50610,0,761
+50620,222,1608
+50630,478,2365
+50640,786,2579
+50650,881,3525
+50660,1973,2107
+50670,1571,3643
+50680,3174,3087
+50690,2340,2812
+50700,2119,1703
+50710,2636,1118
+50720,2642,808
+50730,1510,447
+50740,1166,0
+50750,774,0
+50760,0,0
+50770,0,0
+50780,0,0
+50790,0,0
+50800,0,0
+50810,0,0
+50820,0,0
+50830,0,0
+50840,0,0
+50850,0,275
+50860,0,551
+50870,262,1106
+50880,522,1007
+50890,981,1585
+50900,1539,1730
+50910,1168,2194
+50920,2637,3441
+50930,2563,3170
+50940,3653,2795
+50950,2078,3216
+50960,2435,1774
+50970,2221,1372
+50980,3043,802
+50990,2423,786
+51000,1663,476
+51010,1318,0
+51020,724,0
+51030,423,0
+51040,0,0
+51050,0,0
+51060,0,0
+51070,0,0
+51080,0,0
+51090,0,0
+51100,0,0
+51110,0,194
+51120,0,315
+51130,0,621
+51140,223,1219
+51150,313,1143
+51160,932,2707
+51170,771,2056
+51180,1706,2555
+51190,2560,2994
+51200,2072,2795
+51210,3545,2256
+51220,3080,2243
+51230,3132,1731
+51240,3120,941
+51250,1692,784
+51260,1592,432
+51270,2017,0
+51280,1391,0
+51290,661,0
+51300,320,0
+51310,0,0
+51320,0,0
+51330,0,0
+51340,0,0
+51350,0,0
+51360,0,293
+51370,0,735
+51380,0,1215
+51390,442,1570
+51400,497,2795
+51410,1095,2437
+51420,1185,3949
+51430,2498,2792
+51440,3215,1994
+51450,3387,1346
+51460,3658,1255
+51470,2571,759
+51480,2967,628
+51490,1236,0
+51500,1021,0
+51510,563,0
+51520,426,0
+51530,0,0
+51540,0,0
+51550,0,0
+51560,0,0
+51570,0,0
+51580,0,0
+51590,0,331
+51600,0,769
+51610,261,1198
+51620,609,1612
+51630,1197,1871
+51640,1833,2054
+51650,1892,3433
+51660,2872,2711
+51670,3705,2254
+51680,3103,3130
+51690,2513,2803
+51700,2230,1733
+51710,2201,1848
+51720,1535,1017
+51730,638,656
+51740,587,394
+51750,0,0
+51760,0,0
+51770,0,0
+51780,0,0
+51790,0,0
+51800,0,0
+51810,0,0
+51820,0,0
+51830,0,229
+51840,0,586
+51850,0,752
+51860,284,1358
+51870,571,2020
+51880,626,1841
+51890,1392,2261
+51900,2313,3320
+51910,3356,3044
+51920,2616,1722
+51930,2104,2258
+51940,2036,1073
+51950,2231,971
+51960,1427,532
+51970,1440,0
+51980,503,0
+51990,0,0
+52000,0,0
+52010,0,0
+52020,0,0
+52030,0,0
+52040,0,0
+52050,0,0
+52060,0,0
+52070,0,222
+52080,0,636
+52090,0,823
+52100,329,947
+52110,645,1387
+52120,1089,2472
+52130,1423,3243
+52140,2322,3134
+52150,2380,2154
+52160,2453,3401
+52170,3843,1910
+52180,3583,2877
+52190,2590,1211
+52200,1820,1638
+52210,2486,914
+52220,1542,634
+52230,1627,0
+52240,926,0
+52250,626,0
+52260,0,0
+52270,0,0
+52280,0,0
+52290,0,0
+52300,0,0
+52310,0,0
+52320,0,265
+52330,0,766
+52340,0,1016
+52350,0,1446
+52360,0,1715
+52370,0,2711
+52380,258,2849
+52390,526,2371
+52400,1010,3854
+52410,1925,1994
+52420,1873,2706
+52430,2446,1846
+52440,2298,1791
+52450,3388,1628
+52460,3001,913
+52470,2684,565
+52480,1724,0
+52490,950,0
+52500,1000,0
+52510,530,0
+52520,0,0
+52530,0,0
+52540,0,0
+52550,0,0
+52560,0,0
+52570,0,0
+52580,0,0
+52590,0,0
+52600,0,279
+52610,0,587
+52620,0,664
+52630,352,1255
+52640,875,1824
+52650,972,2050
+52660,1967,3681
+52670,1622,3576
+52680,1970,2990
+52690,2528,2741
+52700,2274,2221
+52710,2451,1043
+52720,2485,559
+52730,1914,399
+52740,1173,0
+52750,500,0
+52760,0,0
+52770,0,0
+52780,0,0
+52790,0,0
+52800,0,0
+52810,0,0
+52820,0,225
+52830,0,438
+52840,0,551
+52850,475,1132
+52860,645,1600
+52870,960,2321
+52880,1435,1851
+52890,1546,3337
+52900,1868,2438
+52910,1936,3277
+52920,3930,2835
+52930,2513,1924
+52940,2445,1151
+52950,1862,733
+52960,1804,0
+52970,887,0
+52980,485,0
+52990,0,0
+53000,0,0
+53010,0,0
+53020,0,0
+53030,0,0
+53040,0,0
+53050,0,0
+53060,0,0
+53070,0,0
+53080,0,249
+53090,0,668
+53100,268,933
+53110,481,1044
+53120,882,1830
+53130,1605,1850
+53140,2196,2194
+53150,2867,3143
+53160,3120,3873
+53170,2969,2821
+53180,2427,2504
+53190,1494,2180
+53200,983,1626
+53210,665,613
+53220,475,561
+53230,0,0
+53240,0,0
+53250,0,0
+53260,0,0
+53270,0,0
+53280,0,0
+53290,0,0
+53300,0,0
+53310,0,286
+53320,0,454
+53330,290,1014
+53340,374,1101
+53350,926,1197
+53360,1424,1840
+53370,2236,3202
+53380,2118,2958
+53390,3510,2452
+53400,2780,1940
+53410,1974,1952
+53420,2261,1634
+53430,2022,1039
+53440,1479,566
+53450,1295,542
+53460,487,0
+53470,0,0
+53480,0,0
+53490,0,0
+53500,0,0
+53510,0,0
+53520,0,0
+53530,0,0
+53540,0,0
+53550,0,327
+53560,0,671
+53570,362,1283
+53580,747,1707
+53590,1033,1714
+53600,1932,2172
+53610,1729,2673
+53620,2741,3807
+53630,2992,2924
+53640,2336,3612
+53650,3125,1769
+53660,2128,1463
+53670,1355,1823
+53680,899,1063
+53690,619,602
+53700,354,597
+53710,0,0
+53720,0,0
+53730,0,0
+53740,0,0
+53750,0,0
+53760,0,0
+53770,0,0
+53780,0,0
+53790,0,0
+53800,0,0
+53810,310,0
+53820,602,512
+53830,706,637
+53840,941,1040
+53850,1393,1660
+53860,2162,1533
+53870,2259,2779
+53880,2777,3095
+53890,2612,2458
+53900,2883,3185
+53910,3131,3015
+53920,1582,2785
+53930,1586,1765
+53940,1075,1659
+53950,1162,1498
+53960,478,875
+53970,0,571
+53980,0,0
+53990,0,0
+54000,0,0
+54010,0,0
+54020,0,0
+54030,0,0
+54040,0,0
+54050,0,0
+54060,0,0
+54070,0,0
+54080,403,0
+54090,621,0
+54100,875,300
+54110,1344,523
+54120,1841,947
+54130,1718,1351
+54140,2502,1089
+54150,2111,2006
+54160,2272,2013
+54170,2576,2541
+54180,1827,3938
+54190,3103,3843
+54200,1800,2318
+54210,1312,2566
+54220,1307,1984
+54230,773,1495
+54240,415,1484
+54250,0,1054
+54260,0,489
+54270,0,0
+54280,0,0
+54290,0,0
+54300,0,0
+54310,0,0
+54320,0,0
+54330,0,0
+54340,464,0
+54350,838,0
+54360,952,0
+54370,1513,405
+54380,1879,799
+54390,2479,1003
+54400,3555,1822
+54410,3682,2053
+54420,3547,1583
+54430,2720,2758
+54440,2584,3751
+54450,1510,2274
+54460,711,2724
+54470,497,1936
+54480,0,1683
+54490,0,1129
+54500,0,1147
+54510,0,836
+54520,0,406
+54530,0,0
+54540,0,0
+54550,0,0
+54560,0,0
+54570,245,0
+54580,554,0
+54590,1144,0
+54600,1533,0
+54610,1518,0
+54620,2403,0
+54630,2484,221
+54640,2360,365
+54650,3843,611
+54660,2399,898
+54670,2727,1483
+54680,1329,2275
+54690,867,2952
+54700,710,3514
+54710,0,2241
+54720,0,3255
+54730,0,2165
+54740,0,2095
+54750,0,2100
+54760,0,1113
+54770,0,974
+54780,0,392
+54790,0,0
+54800,0,0
+54810,0,0
+54820,378,0
+54830,689,0
+54840,1068,0
+54850,2156,0
+54860,2350,0
+54870,3259,0
+54880,2479,0
+54890,2606,0
+54900,2387,338
+54910,1753,671
+54920,1773,951
+54930,1107,1411
+54940,490,1569
+54950,0,2602
+54960,0,2184
+54970,0,3316
+54980,0,3620
+54990,0,2502
+55000,0,1495
+55010,0,1396
+55020,0,820
+55030,0,575
+55040,277,481
+55050,397,0
+55060,1131,0
+55070,1371,0
+55080,1450,0
+55090,2472,0
+55100,2829,0
+55110,2339,0
+55120,3368,0
+55130,3282,0
+55140,2455,0
+55150,1272,0
+55160,1780,258
+55170,1055,332
+55180,552,535
+55190,0,1185
+55200,0,1562
+55210,0,1406
+55220,0,3170
+55230,0,2993
+55240,0,2294
+55250,0,2070
+55260,0,2089
+55270,0,1910
+55280,0,2310
+55290,0,1027
+55300,471,899
+55310,729,733
+55320,828,0
+55330,1460,0
+55340,2479,0
+55350,2196,0
+55360,3558,0
+55370,3417,0
+55380,2039,0
+55390,3057,0
+55400,2483,0
+55410,1766,0
+55420,1184,293
+55430,568,559
+55440,0,1080
+55450,0,1252
+55460,0,1799
+55470,0,1424
+55480,0,3192
+55490,0,3285
+55500,0,3479
+55510,0,3301
+55520,0,3348
+55530,0,2030
+55540,354,2511
+55550,581,1052
+55560,733,839
+55570,1604,667
+55580,1759,461
+55590,2103,0
+55600,2422,0
+55610,3346,0
+55620,2186,0
+55630,2811,0
+55640,1818,0
+55650,1631,0
+55660,809,0
+55670,369,0
+55680,0,0
+55690,0,0
+55700,0,0
+55710,0,401
+55720,0,928
+55730,0,1201
+55740,0,1389
+55750,0,1917
+55760,0,3450
+55770,395,2837
+55780,731,3108
+55790,1050,1912
+55800,1616,2360
+55810,1899,1458
+55820,2566,1060
+55830,2872,584
+55840,3885,0
+55850,3789,0
+55860,3336,0
+55870,2524,0
+55880,1636,0
+55890,1279,0
+55900,566,0
+55910,607,0
+55920,0,0
+55930,0,0
+55940,0,0
+55950,0,0
+55960,0,282
+55970,0,713
+55980,0,1207
+55990,0,1643
+56000,386,1854
+56010,455,2091
+56020,915,3439
+56030,1520,3361
+56040,2014,2411
+56050,2202,2837
+56060,2749,1579
+56070,3166,1161
+56080,3870,698
+56090,2112,0
+56100,1955,0
+56110,2650,0
+56120,1460,0
+56130,1424,0
+56140,974,0
+56150,501,0
+56160,0,0
+56170,0,0
+56180,0,0
+56190,0,275
+56200,0,511
+56210,0,867
+56220,0,1237
+56230,0,1342
+56240,0,1645
+56250,0,1771
+56260,316,2324
+56270,787,3069
+56280,1231,3666
+56290,1152,3449
+56300,2311,2331
+56310,2952,2114
+56320,3331,1350
+56330,3299,1244
+56340,2575,520
+56350,3055,460
+56360,2082,0
+56370,1101,0
+56380,1042,0
+56390,553,0
+56400,0,0
+56410,0,0
+56420,0,0
+56430,0,0
+56440,0,0
+56450,0,233
+56460,0,448
+56470,0,923
+56480,0,1793
+56490,0,2092
+56500,0,2618
+56510,368,1953
+56520,482,3790
+56530,691,3506
+56540,1093,2684
+56550,1684,2158
+56560,2025,1895
+56570,2875,826
+56580,3523,511
+56590,2985,404
+56600,2762,0
+56610,2335,0
+56620,1921,0
+56630,1273,0
+56640,645,0
+56650,465,0
+56660,0,0
+56670,0,0
+56680,0,329
+56690,0,412
+56700,0,1178
+56710,0,1516
+56720,0,2027
+56730,0,3056
+56740,0,1909
+56750,0,3836
+56760,331,3738
+56770,593,2967
+56780,1011,2213
+56790,1818,1560
+56800,2331,882
+56810,2728,558
+56820,3292,0
+56830,3022,0
+56840,2186,0
+56850,2223,0
+56860,1336,0
+56870,1412,0
+56880,694,0
+56890,496,0
+56900,0,0
+56910,0,0
+56920,0,387
+56930,0,656
+56940,0,1444
+56950,0,1160
+56960,0,2531
+56970,0,2758
+56980,0,2837
+56990,0,2445
+57000,385,3554
+57010,964,2753
+57020,1619,1714
+57030,1308,1436
+57040,1778,894
+57050,3485,399
+57060,3925,0
+57070,3326,0
+57080,1860,0
+57090,1686,0
+57100,1124,0
+57110,708,0
+57120,423,0
+57130,0,0
+57140,0,0
+57150,0,0
+57160,0,0
+57170,0,245
+57180,0,556
+57190,0,1099
+57200,0,1659
+57210,0,2566
+57220,412,3006
+57230,473,3592
+57240,949,3418
+57250,1359,2949
+57260,1679,1710
+57270,2322,1685
+57280,2006,908
+57290,2863,617
+57300,3451,402
+57310,3162,0
+57320,2328,0
+57330,1686,0
+57340,1005,0
+57350,844,0
+57360,429,0
+57370,0,0
+57380,0,0
+57390,0,0
+57400,0,482
+57410,0,617
+57420,0,1588
+57430,0,2279
+57440,0,1738
+57450,0,2440
+57460,0,3225
+57470,282,3589
+57480,495,2492
+57490,546,1720
+57500,782,1038
+57510,2053,595
+57520,1957,272
+57530,2919,0
+57540,2170,0
+57550,3376,0
+57560,3403,0
+57570,1882,0
+57580,2488,0
+57590,1628,0
+57600,771,0
+57610,578,0
+57620,0,0
+57630,0,333
+57640,0,588
+57650,0,970
+57660,0,808
+57670,0,1774
+57680,0,2209
+57690,0,1936
+57700,0,2954
+57710,318,2014
+57720,727,2668
+57730,853,2886
+57740,1423,1742
+57750,2225,1899
+57760,2483,1463
+57770,3841,710
+57780,2943,473
+57790,2338,0
+57800,2983,0
+57810,1853,0
+57820,1578,0
+57830,767,0
+57840,474,0
+57850,0,0
+57860,0,0
+57870,0,0
+57880,0,0
+57890,0,0
+57900,0,0
+57910,0,332
+57920,0,449
+57930,0,729
+57940,0,1309
+57950,271,2049
+57960,477,2467
+57970,668,2273
+57980,985,2491
+57990,1021,3738
+58000,2296,2916
diff --git a/frontend/src/hooks/useBle.hooks.ts b/frontend/src/hooks/useBle.hooks.ts
index 9e3ce541..3a1d9360 100644
--- a/frontend/src/hooks/useBle.hooks.ts
+++ b/frontend/src/hooks/useBle.hooks.ts
@@ -24,6 +24,7 @@ export function useBle() {
const [bleIsScanning, setBleIsScanning] = useState(false);
const bufferRef = useRef([]);
+ const startIndexRef = useRef(null);
const deviceIdRef = useRef(null);
const checkAvailability = useCallback(async (): Promise => {
@@ -134,8 +135,18 @@ export function useBle() {
return [...bufferRef.current];
}, []);
+ const bleMarkStart = useCallback(() => {
+ startIndexRef.current = bufferRef.current.length;
+ }, []);
+
+ const bleRunBuffer = useCallback((): BleForceRow[] => {
+ const start = startIndexRef.current ?? 0;
+ return bufferRef.current.slice(start);
+ }, []);
+
const bleClearBuffer = useCallback(() => {
bufferRef.current = [];
+ startIndexRef.current = null;
}, []);
return {
@@ -145,6 +156,8 @@ export function useBle() {
bleConnect,
bleDisconnect,
bleDataBuffer,
+ bleMarkStart,
+ bleRunBuffer,
bleClearBuffer,
};
}
diff --git a/frontend/src/pages/RecordRunPage.tsx b/frontend/src/pages/RecordRunPage.tsx
index 209cb574..9e1bf7ae 100644
--- a/frontend/src/pages/RecordRunPage.tsx
+++ b/frontend/src/pages/RecordRunPage.tsx
@@ -1,5 +1,6 @@
-import { useState, useRef, useCallback } from "react";
+import { useState, useRef, useCallback, useEffect } from "react";
import { useQueryClient } from "@tanstack/react-query";
+import { useNavigate } from "react-router-dom";
import { AthleteSelector } from "@/components/athletes/AthleteSelector";
import EventSelector from "@/components/events/EventSelector";
import { useGetAllAthletes } from "@/hooks/useAthletes.hooks";
@@ -10,6 +11,7 @@ import type { EventTypeEnum } from "@/types/event.types";
export default function RecordRunPage() {
const queryClient = useQueryClient();
+ const navigate = useNavigate();
// Which screen we're on — setup or recording
const [screen, setScreen] = useState<"setup" | "recording">("setup");
@@ -40,10 +42,38 @@ export default function RecordRunPage() {
bleIsScanning,
bleConnect,
bleDisconnect,
- bleDataBuffer,
+ bleMarkStart,
+ bleRunBuffer,
bleClearBuffer,
} = useBle();
+ // Tooltip visibility
+ const [showTooltip, setShowTooltip] = useState(false);
+
+ // Connect BLE as soon as we enter the recording screen
+ useEffect(() => {
+ if (screen !== "recording") return;
+ let cancelled = false;
+
+ async function connect() {
+ setIsConnecting(true);
+ try {
+ await bleConnect();
+ } catch (error) {
+ if (!cancelled) console.error("BLE connection failed:", error);
+ } finally {
+ if (!cancelled) setIsConnecting(false);
+ }
+ }
+
+ connect();
+
+ return () => {
+ cancelled = true;
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [screen]);
+
// Both selections required to proceed
const canProceed = athleteId !== null && eventType !== null;
@@ -51,18 +81,9 @@ export default function RecordRunPage() {
const selectedAthlete = athletes.find((a) => a.athlete_id === athleteId);
const selectedEvent = events.find((e) => e.value === eventType);
- // Start the timer — connects BLE first, then captures real start time
- const startTimer = useCallback(async () => {
- setIsConnecting(true);
- try {
- await bleConnect();
- } catch (error) {
- console.error("BLE connection failed:", error);
- setIsConnecting(false);
- return;
- }
- setIsConnecting(false);
-
+ // Start the timer — marks the buffer start position, then begins timing
+ const startTimer = useCallback(() => {
+ bleMarkStart();
startTimeRef.current = Date.now();
setIsRunning(true);
setIsStopped(false);
@@ -70,26 +91,25 @@ export default function RecordRunPage() {
intervalRef.current = setInterval(() => {
setElapsedMs(Date.now() - startTimeRef.current);
}, 10);
- }, [bleConnect]);
+ }, [bleMarkStart]);
- // Stop the timer — clears interval, takes final measurement, disconnects BLE
- const stopTimer = useCallback(async () => {
+ // Stop the timer — clears interval, takes final measurement
+ const stopTimer = useCallback(() => {
if (intervalRef.current) clearInterval(intervalRef.current);
stopTimeRef.current = Date.now();
setElapsedMs(stopTimeRef.current - startTimeRef.current);
setIsRunning(false);
setIsStopped(true);
- await bleDisconnect();
- }, [bleDisconnect]);
+ }, []);
- // Save the run to the database, then reset
+ // Save the run to the database, then navigate to the run page
const handleSave = async () => {
if (!athleteId || !eventType) return;
setIsSaving(true);
try {
- // Buffer only contains rows received between connect and disconnect
- const rows = bleDataBuffer();
+ // Only rows from after Start was pressed
+ const rows = bleRunBuffer();
const csvLines = ["Time,Force_Foot1,Force_Foot2"];
for (const row of rows) {
@@ -106,13 +126,16 @@ export default function RecordRunPage() {
formData.append("elapsed_ms", String(elapsedMs));
formData.append("file", file);
- await api.post("/csv/upload-run", formData, {
+ const response = await api.post("/csv/upload-run", formData, {
headers: { "Content-Type": "multipart/form-data" },
});
await queryClient.invalidateQueries({ queryKey: ["runs"] });
+ await bleDisconnect();
bleClearBuffer();
- resetAll();
+
+ const runId = response.data.run_id;
+ navigate(`/athletes/${athleteId}/runs/${runId}`);
} catch (error) {
console.error("Failed to save run:", error);
} finally {
@@ -137,6 +160,13 @@ export default function RecordRunPage() {
resetAll();
};
+ // Go back to setup — disconnect BLE and clear buffer
+ const handleChangeEvent = async () => {
+ await bleDisconnect();
+ bleClearBuffer();
+ resetAll();
+ };
+
// Format milliseconds as M:SS.cc (centisecond precision)
const formatTime = (ms: number) => {
const totalSeconds = Math.floor(ms / 1000);
@@ -223,30 +253,76 @@ export default function RecordRunPage() {
{selectedAthlete?.name} · {selectedEvent?.label}
- {/* BLE status indicator during recording */}
- {isRunning && (
-
+ setShowTooltip((v) => !v)}
+ className="flex items-center gap-2 px-4 py-1.5 rounded-full text-xs font-medium"
style={{
- backgroundColor: bleIsConnected
- ? "hsl(var(--primary) / 0.1)"
- : "hsl(var(--destructive) / 0.1)",
- color: bleIsConnected
- ? "hsl(var(--primary))"
- : "hsl(var(--destructive))",
+ backgroundColor:
+ isConnecting || bleIsScanning
+ ? "hsl(var(--muted))"
+ : bleIsConnected
+ ? "hsl(var(--primary) / 0.1)"
+ : "hsl(var(--destructive) / 0.1)",
+ color:
+ isConnecting || bleIsScanning
+ ? "hsl(var(--muted-foreground))"
+ : bleIsConnected
+ ? "hsl(var(--primary))"
+ : "hsl(var(--destructive))",
}}
>
- {bleIsConnected ? "Connected" : "Sensor Disconnected — buffering..."}
-
- )}
+ {isConnecting || bleIsScanning
+ ? "Connecting..."
+ : bleIsConnected
+ ? "Connected"
+ : "Disconnected"}
+
+
+
+
+
+
+ {showTooltip && (
+
+ Losing connection mid-run is expected. The sensor will automatically
+ reconnect when the runner comes back within range. Data is buffered
+ locally.
+
+ )}
+
{/* Timer circle */}
- {isConnecting
- ? "Connecting..."
- : isRunning
- ? "Recording"
- : isStopped
- ? "Stopped"
- : "Ready"}
+ {isRunning
+ ? "Recording"
+ : isStopped
+ ? "Stopped"
+ : bleIsConnected
+ ? "Ready"
+ : "Waiting for sensor"}
- {isConnecting || bleIsScanning
- ? "Connecting..."
- : isRunning
- ? "Stop"
- : "Start"}
+ {isRunning ? "Stop" : "Start"}
)}
@@ -325,9 +397,9 @@ export default function RecordRunPage() {
)}
{/* Change selection link — only before starting */}
- {!isRunning && !isStopped && !isConnecting && (
+ {!isRunning && !isStopped && (
setScreen("setup")}
+ onClick={handleChangeEvent}
className="mt-5 text-sm font-medium cursor-pointer"
style={{ color: "hsl(var(--primary))" }}
>
From b248e3019c44b30c0b21ed9229cdb123581ebaa7 Mon Sep 17 00:00:00 2001
From: Michael Maaseide
Date: Thu, 9 Apr 2026 19:10:50 -0400
Subject: [PATCH 02/18] profile bar fixed
---
frontend/index.html | 5 ++++-
frontend/src/components/layout/AppLayout.tsx | 2 +-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/frontend/index.html b/frontend/index.html
index e9c84308..ab4a46c9 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -6,7 +6,10 @@
rel="stylesheet"
/>
-
+
StrideTrack
{/* Mobile header only */}
-
@@ -90,7 +90,7 @@ export function AddAthleteModal({ open, onClose }: AddAthleteModalProps) {
value={weightLbs}
onChange={(e) => setWeightLbs(e.target.value)}
placeholder="180"
- className="w-full rounded-xl border border-input bg-background px-4 py-2.5 text-sm placeholder:text-muted-foreground focus:outline-none"
+ className="w-full rounded-xl border border-input bg-background px-4 py-2.5 text-base placeholder:text-muted-foreground focus:outline-none"
/>
diff --git a/frontend/src/context/auth.context.tsx b/frontend/src/context/auth.context.tsx
index e430c8ff..b407f54d 100644
--- a/frontend/src/context/auth.context.tsx
+++ b/frontend/src/context/auth.context.tsx
@@ -8,6 +8,8 @@ import {
useState,
type ReactNode,
} from "react";
+import { Capacitor } from "@capacitor/core";
+import { App as CapApp } from "@capacitor/app";
import { supabase } from "@/lib/supabase";
import { getDevToken, setDevToken, clearDevToken } from "@/lib/dev";
import api from "@/lib/api";
@@ -110,14 +112,43 @@ export function AuthProvider({ children }: { children: ReactNode }) {
setLoading(false);
});
- return () => data.subscription.unsubscribe();
+ // Listen for deep-link redirects from OAuth on native platforms
+ let appUrlListener: { remove: () => Promise } | undefined;
+ if (Capacitor.isNativePlatform()) {
+ CapApp.addListener("appUrlOpen", ({ url }) => {
+ // URL looks like: com.stridetrack.app://auth/callback#access_token=...&refresh_token=...
+ const hashIndex = url.indexOf("#");
+ if (hashIndex === -1) return;
+
+ const params = new URLSearchParams(url.substring(hashIndex + 1));
+ const accessToken = params.get("access_token");
+ const refreshToken = params.get("refresh_token");
+
+ if (accessToken && refreshToken) {
+ supabase.auth.setSession({
+ access_token: accessToken,
+ refresh_token: refreshToken,
+ });
+ }
+ }).then((listener) => {
+ appUrlListener = listener;
+ });
+ }
+
+ return () => {
+ data.subscription.unsubscribe();
+ appUrlListener?.remove();
+ };
}, [checkAuth, fetchProfile]);
const loginWithGoogle = async () => {
clearDevToken();
+ const redirectTo = Capacitor.isNativePlatform()
+ ? "com.stridetrack.app://auth/callback"
+ : window.location.origin;
const { error } = await supabase.auth.signInWithOAuth({
provider: "google",
- options: { redirectTo: window.location.origin },
+ options: { redirectTo },
});
if (error) throw error;
};
diff --git a/supabase/config.toml b/supabase/config.toml
index 5a645221..05cd2cf6 100644
--- a/supabase/config.toml
+++ b/supabase/config.toml
@@ -153,7 +153,7 @@ enabled = true
# in emails.
site_url = "http://localhost:5173"
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
-additional_redirect_urls = ["http://localhost:5173"]
+additional_redirect_urls = ["http://localhost:5173", "com.stridetrack.app://auth/callback"]
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
jwt_expiry = 3600
# JWT issuer URL. If not set, defaults to the local API URL (http://127.0.0.1:/auth/v1).
From 6527fba4be4de0eb14dda28576b46002fc270c57 Mon Sep 17 00:00:00 2001
From: Michael Maaseide
Date: Thu, 16 Apr 2026 22:36:07 -0400
Subject: [PATCH 04/18] web app ble
---
frontend/src/hooks/useBle.hooks.ts | 36 +++++++++++----------------
frontend/src/pages/RecordRunPage.tsx | 37 ++++++++++++++++++++++++++--
2 files changed, 49 insertions(+), 24 deletions(-)
diff --git a/frontend/src/hooks/useBle.hooks.ts b/frontend/src/hooks/useBle.hooks.ts
index 3a1d9360..e973098a 100644
--- a/frontend/src/hooks/useBle.hooks.ts
+++ b/frontend/src/hooks/useBle.hooks.ts
@@ -1,5 +1,4 @@
import { useState, useRef, useCallback, useEffect } from "react";
-import { Capacitor } from "@capacitor/core";
import { BleClient } from "@capacitor-community/bluetooth-le";
const FORCE_PLATE_SERVICE_UUID = "0000180d-0000-1000-8000-00805f9b34fb";
@@ -27,40 +26,33 @@ export function useBle() {
const startIndexRef = useRef(null);
const deviceIdRef = useRef(null);
- const checkAvailability = useCallback(async (): Promise => {
- if (!Capacitor.isNativePlatform()) {
- setBleIsAvailable(false);
- return false;
- }
+ // On web, BleClient.isEnabled() is not supported and throws.
+ // Fall back to checking navigator.bluetooth (Web Bluetooth API) instead.
+ const resolveAvailability = useCallback(async (): Promise => {
try {
await BleClient.initialize();
const enabled = await BleClient.isEnabled();
- setBleIsAvailable(enabled);
return enabled;
} catch {
- setBleIsAvailable(false);
- return false;
+ return "bluetooth" in navigator;
}
}, []);
+ const checkAvailability = useCallback(async (): Promise => {
+ const available = await resolveAvailability();
+ setBleIsAvailable(available);
+ return available;
+ }, [resolveAvailability]);
+
useEffect(() => {
let cancelled = false;
- async function init() {
- const isNative = Capacitor.isNativePlatform();
- if (!isNative) return;
- try {
- await BleClient.initialize();
- const enabled = await BleClient.isEnabled();
- if (!cancelled) setBleIsAvailable(enabled);
- } catch {
- if (!cancelled) setBleIsAvailable(false);
- }
- }
- init();
+ resolveAvailability().then((available) => {
+ if (!cancelled) setBleIsAvailable(available);
+ });
return () => {
cancelled = true;
};
- }, []);
+ }, [resolveAvailability]);
const startListening = useCallback(async (deviceId: string) => {
await BleClient.startNotifications(
diff --git a/frontend/src/pages/RecordRunPage.tsx b/frontend/src/pages/RecordRunPage.tsx
index 9e1bf7ae..72286584 100644
--- a/frontend/src/pages/RecordRunPage.tsx
+++ b/frontend/src/pages/RecordRunPage.tsx
@@ -1,6 +1,7 @@
import { useState, useRef, useCallback, useEffect } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
+import { Capacitor } from "@capacitor/core";
import { AthleteSelector } from "@/components/athletes/AthleteSelector";
import EventSelector from "@/components/events/EventSelector";
import { useGetAllAthletes } from "@/hooks/useAthletes.hooks";
@@ -50,9 +51,14 @@ export default function RecordRunPage() {
// Tooltip visibility
const [showTooltip, setShowTooltip] = useState(false);
- // Connect BLE as soon as we enter the recording screen
+ // On native the OS can show the BLE picker programmatically.
+ // On web the browser requires a direct user gesture, so we skip auto-connect.
+ const isNative = Capacitor.isNativePlatform();
+
+ // Auto-connect BLE when entering the recording screen (native only)
useEffect(() => {
if (screen !== "recording") return;
+ if (!isNative) return;
let cancelled = false;
async function connect() {
@@ -74,6 +80,18 @@ export default function RecordRunPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [screen]);
+ // Manual connect triggered by a button click (web only — satisfies browser gesture requirement)
+ const handleManualConnect = useCallback(async () => {
+ setIsConnecting(true);
+ try {
+ await bleConnect();
+ } catch (error) {
+ console.error("BLE connection failed:", error);
+ } finally {
+ setIsConnecting(false);
+ }
+ }, [bleConnect]);
+
// Both selections required to proceed
const canProceed = athleteId !== null && eventType !== null;
@@ -254,7 +272,7 @@ export default function RecordRunPage() {
{/* BLE status indicator — visible in all recording states */}
-
+
setShowTooltip((v) => !v)}
@@ -322,6 +340,21 @@ export default function RecordRunPage() {
locally.
)}
+
+ {/* Web-only manual connect button — browser requires a click gesture */}
+ {!isNative && !bleIsConnected && !isConnecting && (
+
+ Connect Sensor
+
+ )}
{/* Timer circle */}
From 685a2150f48aff935dcc2b95d7c14404cc2614db Mon Sep 17 00:00:00 2001
From: samuelbaldwin05
Date: Thu, 16 Apr 2026 23:07:28 -0400
Subject: [PATCH 05/18] capacitor fix
---
frontend/src/context/auth.context.tsx | 41 ++++++++++++++++-----------
frontend/vite.config.ts | 3 ++
2 files changed, 27 insertions(+), 17 deletions(-)
diff --git a/frontend/src/context/auth.context.tsx b/frontend/src/context/auth.context.tsx
index b407f54d..8e44eb45 100644
--- a/frontend/src/context/auth.context.tsx
+++ b/frontend/src/context/auth.context.tsx
@@ -9,7 +9,6 @@ import {
type ReactNode,
} from "react";
import { Capacitor } from "@capacitor/core";
-import { App as CapApp } from "@capacitor/app";
import { supabase } from "@/lib/supabase";
import { getDevToken, setDevToken, clearDevToken } from "@/lib/dev";
import api from "@/lib/api";
@@ -115,24 +114,32 @@ export function AuthProvider({ children }: { children: ReactNode }) {
// Listen for deep-link redirects from OAuth on native platforms
let appUrlListener: { remove: () => Promise } | undefined;
if (Capacitor.isNativePlatform()) {
- CapApp.addListener("appUrlOpen", ({ url }) => {
- // URL looks like: com.stridetrack.app://auth/callback#access_token=...&refresh_token=...
- const hashIndex = url.indexOf("#");
- if (hashIndex === -1) return;
-
- const params = new URLSearchParams(url.substring(hashIndex + 1));
- const accessToken = params.get("access_token");
- const refreshToken = params.get("refresh_token");
-
- if (accessToken && refreshToken) {
- supabase.auth.setSession({
- access_token: accessToken,
- refresh_token: refreshToken,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (import(/* @vite-ignore */ "@capacitor/app") as Promise).then(
+ (mod) => {
+ mod.App.addListener(
+ "appUrlOpen",
+ ({ url }: { url: string }) => {
+ // URL looks like: com.stridetrack.app://auth/callback#access_token=...&refresh_token=...
+ const hashIndex = url.indexOf("#");
+ if (hashIndex === -1) return;
+
+ const params = new URLSearchParams(url.substring(hashIndex + 1));
+ const accessToken = params.get("access_token");
+ const refreshToken = params.get("refresh_token");
+
+ if (accessToken && refreshToken) {
+ supabase.auth.setSession({
+ access_token: accessToken,
+ refresh_token: refreshToken,
+ });
+ }
+ }
+ ).then((listener: { remove: () => Promise }) => {
+ appUrlListener = listener;
});
}
- }).then((listener) => {
- appUrlListener = listener;
- });
+ );
}
return () => {
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index 22ea2ad3..f4582353 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -10,6 +10,9 @@ export default defineConfig({
"@": path.resolve(__dirname, "./src"),
},
},
+ optimizeDeps: {
+ exclude: ["@capacitor/app"],
+ },
server: {
// Proxy OTLP traces to Jaeger so the browser doesn't hit CORS.
// When frontend runs in Docker, set OTEL_PROXY_TARGET=http://host.docker.internal:4318 so the container can reach Jaeger on the host.
From ae4cac10b25b3d3003557caaced8f6351f59ccfb Mon Sep 17 00:00:00 2001
From: Michael Maaseide
Date: Thu, 16 Apr 2026 23:10:36 -0400
Subject: [PATCH 06/18] debugging web ble
---
frontend/bun.lock | 3 +++
frontend/package.json | 1 +
frontend/src/context/auth.context.tsx | 33 ++++++++++++---------------
frontend/src/hooks/useBle.hooks.ts | 4 +++-
frontend/vite.config.ts | 4 +++-
supabase/config.toml | 2 +-
6 files changed, 26 insertions(+), 21 deletions(-)
diff --git a/frontend/bun.lock b/frontend/bun.lock
index 2c9055df..1af7ead2 100644
--- a/frontend/bun.lock
+++ b/frontend/bun.lock
@@ -40,6 +40,7 @@
"@types/node": "^24.10.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-basic-ssl": "^2.3.0",
"@vitejs/plugin-react": "^5.1.1",
"autoprefixer": "^10.4.23",
"eslint": "^9.39.1",
@@ -478,6 +479,8 @@
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA=="],
+ "@vitejs/plugin-basic-ssl": ["@vitejs/plugin-basic-ssl@2.3.0", "", { "peerDependencies": { "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ=="],
+
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.53", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ=="],
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.12", "", {}, "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg=="],
diff --git a/frontend/package.json b/frontend/package.json
index 3a3634a7..9a96bb33 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -47,6 +47,7 @@
"@types/node": "^24.10.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-basic-ssl": "^2.3.0",
"@vitejs/plugin-react": "^5.1.1",
"autoprefixer": "^10.4.23",
"eslint": "^9.39.1",
diff --git a/frontend/src/context/auth.context.tsx b/frontend/src/context/auth.context.tsx
index 8e44eb45..d4f2e026 100644
--- a/frontend/src/context/auth.context.tsx
+++ b/frontend/src/context/auth.context.tsx
@@ -117,25 +117,22 @@ export function AuthProvider({ children }: { children: ReactNode }) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(import(/* @vite-ignore */ "@capacitor/app") as Promise).then(
(mod) => {
- mod.App.addListener(
- "appUrlOpen",
- ({ url }: { url: string }) => {
- // URL looks like: com.stridetrack.app://auth/callback#access_token=...&refresh_token=...
- const hashIndex = url.indexOf("#");
- if (hashIndex === -1) return;
-
- const params = new URLSearchParams(url.substring(hashIndex + 1));
- const accessToken = params.get("access_token");
- const refreshToken = params.get("refresh_token");
-
- if (accessToken && refreshToken) {
- supabase.auth.setSession({
- access_token: accessToken,
- refresh_token: refreshToken,
- });
- }
+ mod.App.addListener("appUrlOpen", ({ url }: { url: string }) => {
+ // URL looks like: com.stridetrack.app://auth/callback#access_token=...&refresh_token=...
+ const hashIndex = url.indexOf("#");
+ if (hashIndex === -1) return;
+
+ const params = new URLSearchParams(url.substring(hashIndex + 1));
+ const accessToken = params.get("access_token");
+ const refreshToken = params.get("refresh_token");
+
+ if (accessToken && refreshToken) {
+ supabase.auth.setSession({
+ access_token: accessToken,
+ refresh_token: refreshToken,
+ });
}
- ).then((listener: { remove: () => Promise }) => {
+ }).then((listener: { remove: () => Promise }) => {
appUrlListener = listener;
});
}
diff --git a/frontend/src/hooks/useBle.hooks.ts b/frontend/src/hooks/useBle.hooks.ts
index e973098a..15ebbc51 100644
--- a/frontend/src/hooks/useBle.hooks.ts
+++ b/frontend/src/hooks/useBle.hooks.ts
@@ -90,9 +90,11 @@ export function useBle() {
await BleClient.connect(device.deviceId, () =>
handleDisconnect(device.deviceId)
);
- setBleIsConnected(true);
await startListening(device.deviceId);
+
+ // Only mark connected after notifications are confirmed active
+ setBleIsConnected(true);
} catch (error) {
setBleIsScanning(false);
throw error;
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index f4582353..3f03274b 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -1,10 +1,11 @@
import path from "path";
import react from "@vitejs/plugin-react";
import svgr from "vite-plugin-svgr";
+import basicSsl from "@vitejs/plugin-basic-ssl";
import { defineConfig } from "vite";
export default defineConfig({
- plugins: [svgr(), react()],
+ plugins: [svgr(), react(), basicSsl()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
@@ -14,6 +15,7 @@ export default defineConfig({
exclude: ["@capacitor/app"],
},
server: {
+ host: true, // expose on local network so other devices can connect
// Proxy OTLP traces to Jaeger so the browser doesn't hit CORS.
// When frontend runs in Docker, set OTEL_PROXY_TARGET=http://host.docker.internal:4318 so the container can reach Jaeger on the host.
proxy: {
diff --git a/supabase/config.toml b/supabase/config.toml
index 05cd2cf6..ec1b1546 100644
--- a/supabase/config.toml
+++ b/supabase/config.toml
@@ -153,7 +153,7 @@ enabled = true
# in emails.
site_url = "http://localhost:5173"
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
-additional_redirect_urls = ["http://localhost:5173", "com.stridetrack.app://auth/callback"]
+additional_redirect_urls = ["http://localhost:5173", "https://localhost:5173", "com.stridetrack.app://auth/callback"]
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
jwt_expiry = 3600
# JWT issuer URL. If not set, defaults to the local API URL (http://127.0.0.1:/auth/v1).
From ebf7ded3f2d22f67ac0b1e0e8a91d6751792c166 Mon Sep 17 00:00:00 2001
From: Michael Maaseide
Date: Thu, 16 Apr 2026 23:27:29 -0400
Subject: [PATCH 07/18] ble subscriber fix
---
frontend/bun.lock | 5 +-
frontend/package.json | 1 +
frontend/src/hooks/useBle.hooks.ts | 149 +++++++++++++++++++++--------
3 files changed, 116 insertions(+), 39 deletions(-)
diff --git a/frontend/bun.lock b/frontend/bun.lock
index 1af7ead2..755047c8 100644
--- a/frontend/bun.lock
+++ b/frontend/bun.lock
@@ -40,6 +40,7 @@
"@types/node": "^24.10.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
+ "@types/web-bluetooth": "^0.0.21",
"@vitejs/plugin-basic-ssl": "^2.3.0",
"@vitejs/plugin-react": "^5.1.1",
"autoprefixer": "^10.4.23",
@@ -455,7 +456,7 @@
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
- "@types/web-bluetooth": ["@types/web-bluetooth@0.0.20", "", {}, "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="],
+ "@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="],
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
@@ -1105,6 +1106,8 @@
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+ "@capacitor-community/bluetooth-le/@types/web-bluetooth": ["@types/web-bluetooth@0.0.20", "", {}, "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="],
+
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
diff --git a/frontend/package.json b/frontend/package.json
index 9a96bb33..1f2212c2 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -47,6 +47,7 @@
"@types/node": "^24.10.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
+ "@types/web-bluetooth": "^0.0.21",
"@vitejs/plugin-basic-ssl": "^2.3.0",
"@vitejs/plugin-react": "^5.1.1",
"autoprefixer": "^10.4.23",
diff --git a/frontend/src/hooks/useBle.hooks.ts b/frontend/src/hooks/useBle.hooks.ts
index 15ebbc51..d4960673 100644
--- a/frontend/src/hooks/useBle.hooks.ts
+++ b/frontend/src/hooks/useBle.hooks.ts
@@ -4,6 +4,11 @@ import { BleClient } from "@capacitor-community/bluetooth-le";
const FORCE_PLATE_SERVICE_UUID = "0000180d-0000-1000-8000-00805f9b34fb";
const FORCE_PLATE_CHARACTERISTIC_UUID = "00002a37-0000-1000-8000-00805f9b34fb";
+// True when running in a browser that supports Web Bluetooth.
+// On native Capacitor (iOS/Android), WKWebView/WebView don't expose navigator.bluetooth,
+// so this is false and we fall through to the BleClient (Capacitor) path.
+const IS_WEB_BLUETOOTH = "bluetooth" in navigator;
+
export interface BleForceRow {
time: number;
force_foot1: number;
@@ -24,57 +29,106 @@ export function useBle() {
const bufferRef = useRef([]);
const startIndexRef = useRef(null);
+
+ // ── Native (Capacitor) refs ──────────────────────────────────
const deviceIdRef = useRef(null);
- // On web, BleClient.isEnabled() is not supported and throws.
- // Fall back to checking navigator.bluetooth (Web Bluetooth API) instead.
- const resolveAvailability = useCallback(async (): Promise => {
- try {
- await BleClient.initialize();
- const enabled = await BleClient.isEnabled();
- return enabled;
- } catch {
- return "bluetooth" in navigator;
- }
- }, []);
+ // ── Web Bluetooth refs ───────────────────────────────────────
+ const webDeviceRef = useRef(null);
+ const webCharRef = useRef(null);
- const checkAvailability = useCallback(async (): Promise => {
- const available = await resolveAvailability();
- setBleIsAvailable(available);
- return available;
- }, [resolveAvailability]);
+ // ── Availability check ───────────────────────────────────────
useEffect(() => {
let cancelled = false;
- resolveAvailability().then((available) => {
+
+ async function resolve() {
+ let available = false;
+ if (IS_WEB_BLUETOOTH) {
+ available = true; // picker will surface unavailability at connect time
+ } else {
+ try {
+ await BleClient.initialize();
+ available = await BleClient.isEnabled();
+ } catch {
+ available = false;
+ }
+ }
if (!cancelled) setBleIsAvailable(available);
- });
+ }
+
+ resolve();
return () => {
cancelled = true;
};
- }, [resolveAvailability]);
-
- const startListening = useCallback(async (deviceId: string) => {
- await BleClient.startNotifications(
- deviceId,
- FORCE_PLATE_SERVICE_UUID,
- FORCE_PLATE_CHARACTERISTIC_UUID,
- (value: DataView) => {
- const row = parseNotification(value);
- bufferRef.current.push(row);
- }
+ }, []);
+
+ // ── Web Bluetooth path ───────────────────────────────────────
+
+ const webConnect = useCallback(async () => {
+ // requestDevice() opens the browser device picker — must be called from a user gesture
+ const device = await navigator.bluetooth.requestDevice({
+ filters: [{ services: [FORCE_PLATE_SERVICE_UUID] }],
+ });
+
+ webDeviceRef.current = device;
+
+ // React to unexpected disconnects (out of range, etc.)
+ device.addEventListener("gattserverdisconnected", () => {
+ setBleIsConnected(false);
+ });
+
+ const server = await device.gatt!.connect();
+ const service = await server.getPrimaryService(FORCE_PLATE_SERVICE_UUID);
+ const characteristic = await service.getCharacteristic(
+ FORCE_PLATE_CHARACTERISTIC_UUID
);
+
+ webCharRef.current = characteristic;
+
+ await characteristic.startNotifications();
+
+ characteristic.addEventListener("characteristicvaluechanged", (event) => {
+ const char = event.target as BluetoothRemoteGATTCharacteristic;
+ if (char.value) {
+ bufferRef.current.push(parseNotification(char.value));
+ }
+ });
+
+ // Only mark connected once notifications are live
+ setBleIsConnected(true);
+ }, []);
+
+ const webDisconnect = useCallback(async () => {
+ if (webCharRef.current) {
+ try {
+ await webCharRef.current.stopNotifications();
+ } catch {
+ // may already be stopped
+ }
+ webCharRef.current = null;
+ }
+ if (webDeviceRef.current?.gatt?.connected) {
+ webDeviceRef.current.gatt.disconnect();
+ }
+ webDeviceRef.current = null;
+ setBleIsConnected(false);
}, []);
- const handleDisconnect = useCallback((deviceId: string) => {
+ // ── Native (Capacitor) path ──────────────────────────────────
+
+ const handleNativeDisconnect = useCallback((deviceId: string) => {
if (deviceIdRef.current === deviceId) {
setBleIsConnected(false);
}
}, []);
- const bleConnect = useCallback(async () => {
- const available = await checkAvailability();
- if (!available) {
+ const nativeConnect = useCallback(async () => {
+ try {
+ await BleClient.initialize();
+ const enabled = await BleClient.isEnabled();
+ if (!enabled) throw new Error("Bluetooth is not enabled");
+ } catch {
throw new Error("BLE is not available");
}
@@ -88,20 +142,27 @@ export function useBle() {
deviceIdRef.current = device.deviceId;
await BleClient.connect(device.deviceId, () =>
- handleDisconnect(device.deviceId)
+ handleNativeDisconnect(device.deviceId)
);
- await startListening(device.deviceId);
+ await BleClient.startNotifications(
+ device.deviceId,
+ FORCE_PLATE_SERVICE_UUID,
+ FORCE_PLATE_CHARACTERISTIC_UUID,
+ (value: DataView) => {
+ bufferRef.current.push(parseNotification(value));
+ }
+ );
- // Only mark connected after notifications are confirmed active
+ // Only mark connected once notifications are live
setBleIsConnected(true);
} catch (error) {
setBleIsScanning(false);
throw error;
}
- }, [checkAvailability, handleDisconnect, startListening]);
+ }, [handleNativeDisconnect]);
- const bleDisconnect = useCallback(async () => {
+ const nativeDisconnect = useCallback(async () => {
const deviceId = deviceIdRef.current;
if (!deviceId) return;
@@ -125,6 +186,18 @@ export function useBle() {
deviceIdRef.current = null;
}, []);
+ // ── Public API (routes to correct path) ─────────────────────
+
+ const bleConnect = useCallback(async () => {
+ if (IS_WEB_BLUETOOTH) return webConnect();
+ return nativeConnect();
+ }, [webConnect, nativeConnect]);
+
+ const bleDisconnect = useCallback(async () => {
+ if (IS_WEB_BLUETOOTH) return webDisconnect();
+ return nativeDisconnect();
+ }, [webDisconnect, nativeDisconnect]);
+
const bleDataBuffer = useCallback((): BleForceRow[] => {
return [...bufferRef.current];
}, []);
From 1dacb972f2d106507bfee21c33982ee23d8ecf0b Mon Sep 17 00:00:00 2001
From: Michael Maaseide
Date: Thu, 16 Apr 2026 23:32:09 -0400
Subject: [PATCH 08/18] build error fix
---
frontend/tsconfig.app.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json
index 60077525..d0957579 100644
--- a/frontend/tsconfig.app.json
+++ b/frontend/tsconfig.app.json
@@ -5,7 +5,7 @@
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
- "types": ["vite/client"],
+ "types": ["vite/client", "web-bluetooth"],
"skipLibCheck": true,
/* Bundler mode */
From 52d41ec2c92d7e3d25f9fcd8e1cb39abb93cbbcf Mon Sep 17 00:00:00 2001
From: Michael Maaseide
Date: Thu, 16 Apr 2026 23:56:11 -0400
Subject: [PATCH 09/18] logging
---
data/mock_ble_client.py | 128 ++++++++++++++++-------------
frontend/src/hooks/useBle.hooks.ts | 49 ++++++++---
2 files changed, 109 insertions(+), 68 deletions(-)
diff --git a/data/mock_ble_client.py b/data/mock_ble_client.py
index d37599da..48a63ea9 100644
--- a/data/mock_ble_client.py
+++ b/data/mock_ble_client.py
@@ -2,8 +2,8 @@
"""Mock BLE peripheral that simulates a StrideTrack insole sensor.
Uses CoreBluetooth via pyobjc directly (instead of bless) for reliable
-advertising on macOS. Runs on the CFRunLoop, which CBPeripheralManager
-requires on macOS.
+advertising on macOS. All setup is callback-driven on the main run loop
+so delegate methods fire reliably.
Reads a CSV file and streams rows continuously as BLE notifications at
~100Hz. Loops back to the start of the CSV when all rows have been sent,
@@ -28,14 +28,16 @@
from CoreBluetooth import (
CBAdvertisementDataLocalNameKey,
CBAdvertisementDataServiceUUIDsKey,
+ CBATTErrorAttributeNotFound,
+ CBATTErrorSuccess,
CBAttributePermissionsReadable,
CBCharacteristicPropertyNotify,
+ CBCharacteristicPropertyRead,
CBMutableCharacteristic,
CBMutableService,
CBPeripheralManager,
)
from Foundation import CBUUID, NSData, NSObject
-from dispatch import dispatch_queue_create, DISPATCH_QUEUE_SERIAL
from PyObjCTools import AppHelper
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
@@ -47,7 +49,7 @@
NOTIFICATION_INTERVAL_S = 0.01 # 100 Hz
-# ── CSV loading (unchanged) ─────────────────────────────────────
+# ── CSV loading ────────────────────────────────────────────────
def load_csv(path: str) -> list[tuple[float, float, float]]:
@@ -126,39 +128,62 @@ def pack_notification(time_val: float, foot1: float, foot2: float) -> bytes:
class PeripheralDelegate(NSObject):
- """CBPeripheralManagerDelegate that manages the GATT service lifecycle."""
+ """CBPeripheralManagerDelegate — fully callback-driven on the main queue."""
def init(self):
self = objc.super(PeripheralDelegate, self).init()
if self is None:
return None
- self._powered_on = threading.Event()
self._subscribed = threading.Event()
- self._service_added = threading.Event()
self._characteristic = None
self._manager = None
self._rows = []
return self
- # -- State changes --
+ # -- State changes → build service --
def peripheralManagerDidUpdateState_(self, peripheral):
state = peripheral.state()
- # CBManagerStatePoweredOn == 5
- if state == 5:
- logger.info("Bluetooth powered on")
- self._powered_on.set()
+ if state == 5: # CBManagerStatePoweredOn
+ logger.info("Bluetooth powered on — adding service...")
+ self._build_and_add_service(peripheral)
else:
logger.warning("Bluetooth state changed: %d (need 5/poweredOn)", state)
- # -- Service added --
+ @objc.python_method
+ def _build_and_add_service(self, peripheral):
+ char_uuid = CBUUID.UUIDWithString_(CHARACTERISTIC_UUID)
+ characteristic = (
+ CBMutableCharacteristic.alloc()
+ .initWithType_properties_value_permissions_(
+ char_uuid,
+ CBCharacteristicPropertyNotify | CBCharacteristicPropertyRead,
+ None,
+ CBAttributePermissionsReadable,
+ )
+ )
+ self._characteristic = characteristic
+
+ service_uuid = CBUUID.UUIDWithString_(SERVICE_UUID)
+ service = CBMutableService.alloc().initWithType_primary_(service_uuid, True)
+ service.setCharacteristics_([characteristic])
+
+ peripheral.addService_(service)
+
+ # -- Service added → start advertising --
def peripheralManager_didAddService_error_(self, peripheral, service, error):
if error:
logger.error("Failed to add service: %s", error)
return
- logger.info("Service added successfully")
- self._service_added.set()
+ logger.info("Service added — starting advertising...")
+ service_uuid = CBUUID.UUIDWithString_(SERVICE_UUID)
+ peripheral.startAdvertising_(
+ {
+ CBAdvertisementDataLocalNameKey: "StrideTrack Sensor",
+ CBAdvertisementDataServiceUUIDsKey: [service_uuid],
+ }
+ )
# -- Advertising started --
@@ -184,6 +209,28 @@ def peripheralManager_central_didUnsubscribeFromCharacteristic_(
logger.info("Central unsubscribed: %s", central.identifier())
self._subscribed.clear()
+ # -- Read requests --
+
+ def peripheralManager_didReceiveReadRequest_(self, peripheral, request):
+ logger.info("Read request for %s", request.characteristic().UUID())
+ char_uuid = CBUUID.UUIDWithString_(CHARACTERISTIC_UUID)
+ if request.characteristic().UUID().isEqual_(char_uuid):
+ request.setValue_(NSData.data())
+ peripheral.respondToRequest_withResult_(request, CBATTErrorSuccess)
+ else:
+ peripheral.respondToRequest_withResult_(request, CBATTErrorAttributeNotFound)
+
+ # -- Write requests --
+
+ def peripheralManager_didReceiveWriteRequests_(self, peripheral, requests):
+ logger.info("Received %d write request(s)", requests.count())
+ for i in range(requests.count()):
+ req = requests.objectAtIndex_(i)
+ logger.info(" Write to %s: %s", req.characteristic().UUID(), req.value())
+ peripheral.respondToRequest_withResult_(
+ requests.objectAtIndex_(0), CBATTErrorSuccess
+ )
+
# ── Notification streaming (runs on a background thread) ────────
@@ -206,12 +253,10 @@ def _stream_notifications(delegate):
payload = pack_notification(time_val, foot1, foot2)
ns_data = NSData.dataWithBytes_length_(payload, len(payload))
- # Send notification — returns False when the transmit queue is full
did_send = manager.updateValue_forCharacteristic_onSubscribedCentrals_(
ns_data, characteristic, None
)
if not did_send:
- # Back off briefly and retry the same row
time.sleep(NOTIFICATION_INTERVAL_S)
continue
@@ -232,7 +277,7 @@ def _stream_notifications(delegate):
def run_peripheral(csv_path: str) -> None:
- """Run the mock BLE peripheral on the CFRunLoop."""
+ """Run the mock BLE peripheral on the main run loop."""
rows = load_csv(csv_path)
if not rows:
logger.error("CSV file is empty or has no valid rows")
@@ -243,54 +288,21 @@ def run_peripheral(csv_path: str) -> None:
delegate = PeripheralDelegate.alloc().init()
delegate._rows = rows
- # Use a serial dispatch queue so CoreBluetooth callbacks are
- # delivered off the main thread but in order.
- queue = dispatch_queue_create(b"com.stridetrack.ble", DISPATCH_QUEUE_SERIAL)
- manager = CBPeripheralManager.alloc().initWithDelegate_queue_(delegate, queue)
+ # Use None (main queue) — callbacks fire on the main run loop,
+ # so no GIL contention with a separate dispatch queue thread.
+ manager = CBPeripheralManager.alloc().initWithDelegate_queue_(delegate, None)
delegate._manager = manager
- # Wait for Bluetooth to power on
- logger.info("Waiting for Bluetooth to power on...")
- delegate._powered_on.wait()
-
- # Build the characteristic
- char_uuid = CBUUID.UUIDWithString_(CHARACTERISTIC_UUID)
- characteristic = (
- CBMutableCharacteristic.alloc()
- .initWithType_properties_value_permissions_(
- char_uuid,
- CBCharacteristicPropertyNotify,
- None,
- CBAttributePermissionsReadable,
- )
- )
- delegate._characteristic = characteristic
-
- # Build the service
- service_uuid = CBUUID.UUIDWithString_(SERVICE_UUID)
- service = CBMutableService.alloc().initWithType_primary_(service_uuid, True)
- service.setCharacteristics_([characteristic])
-
- manager.addService_(service)
- delegate._service_added.wait()
-
- # Start advertising
- manager.startAdvertising_(
- {
- CBAdvertisementDataLocalNameKey: "StrideTrack Sensor",
- CBAdvertisementDataServiceUUIDsKey: [service_uuid],
- }
- )
-
- # Wait for a central to subscribe, then stream from a background thread
+ # Background thread waits for subscription, then streams data.
def _wait_and_stream():
delegate._subscribed.wait()
- logger.info("Central subscribed — starting notifications")
+ logger.info("Starting notification stream...")
_stream_notifications(delegate)
threading.Thread(target=_wait_and_stream, daemon=True).start()
- # Run the CFRunLoop on the main thread (required by CoreBluetooth)
+ # The main run loop processes all CoreBluetooth callbacks.
+ # Setup is driven by the delegate: poweredOn → addService → advertise.
AppHelper.runConsoleEventLoop()
diff --git a/frontend/src/hooks/useBle.hooks.ts b/frontend/src/hooks/useBle.hooks.ts
index d4960673..6153a1ff 100644
--- a/frontend/src/hooks/useBle.hooks.ts
+++ b/frontend/src/hooks/useBle.hooks.ts
@@ -66,28 +66,40 @@ export function useBle() {
// ── Web Bluetooth path ───────────────────────────────────────
const webConnect = useCallback(async () => {
- // requestDevice() opens the browser device picker — must be called from a user gesture
+ console.log("[BLE-web] requestDevice...");
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: [FORCE_PLATE_SERVICE_UUID] }],
});
+ console.log("[BLE-web] device selected:", device.name, device.id);
webDeviceRef.current = device;
- // React to unexpected disconnects (out of range, etc.)
device.addEventListener("gattserverdisconnected", () => {
+ console.log("[BLE-web] GATT server disconnected");
setBleIsConnected(false);
});
+ console.log("[BLE-web] connecting to GATT server...");
const server = await device.gatt!.connect();
+ console.log("[BLE-web] GATT connected:", server.connected);
+
+ console.log("[BLE-web] getPrimaryService...");
const service = await server.getPrimaryService(FORCE_PLATE_SERVICE_UUID);
+ console.log("[BLE-web] service found");
+
+ console.log("[BLE-web] getCharacteristic...");
const characteristic = await service.getCharacteristic(
FORCE_PLATE_CHARACTERISTIC_UUID
);
+ console.log("[BLE-web] characteristic properties:", {
+ notify: characteristic.properties.notify,
+ read: characteristic.properties.read,
+ indicate: characteristic.properties.indicate,
+ write: characteristic.properties.write,
+ });
webCharRef.current = characteristic;
- await characteristic.startNotifications();
-
characteristic.addEventListener("characteristicvaluechanged", (event) => {
const char = event.target as BluetoothRemoteGATTCharacteristic;
if (char.value) {
@@ -95,7 +107,10 @@ export function useBle() {
}
});
- // Only mark connected once notifications are live
+ console.log("[BLE-web] startNotifications...");
+ await characteristic.startNotifications();
+ console.log("[BLE-web] notifications started OK");
+
setBleIsConnected(true);
}, []);
@@ -124,27 +139,40 @@ export function useBle() {
}, []);
const nativeConnect = useCallback(async () => {
+ console.log("[BLE-native] initializing...");
try {
await BleClient.initialize();
const enabled = await BleClient.isEnabled();
+ console.log("[BLE-native] enabled:", enabled);
if (!enabled) throw new Error("Bluetooth is not enabled");
- } catch {
+ } catch (e) {
+ console.error("[BLE-native] init failed:", e);
throw new Error("BLE is not available");
}
setBleIsScanning(true);
try {
+ console.log("[BLE-native] requestDevice...");
const device = await BleClient.requestDevice({
services: [FORCE_PLATE_SERVICE_UUID],
});
+ console.log(
+ "[BLE-native] device selected:",
+ device.deviceId,
+ device.name
+ );
setBleIsScanning(false);
deviceIdRef.current = device.deviceId;
- await BleClient.connect(device.deviceId, () =>
- handleNativeDisconnect(device.deviceId)
- );
+ console.log("[BLE-native] connecting...");
+ await BleClient.connect(device.deviceId, () => {
+ console.log("[BLE-native] disconnected callback fired");
+ handleNativeDisconnect(device.deviceId);
+ });
+ console.log("[BLE-native] connected");
+ console.log("[BLE-native] startNotifications...");
await BleClient.startNotifications(
device.deviceId,
FORCE_PLATE_SERVICE_UUID,
@@ -153,10 +181,11 @@ export function useBle() {
bufferRef.current.push(parseNotification(value));
}
);
+ console.log("[BLE-native] notifications started OK");
- // Only mark connected once notifications are live
setBleIsConnected(true);
} catch (error) {
+ console.error("[BLE-native] connect failed:", error);
setBleIsScanning(false);
throw error;
}
From 8cfee9e58315e1730c76827e7e7fe8e38225870d Mon Sep 17 00:00:00 2001
From: Michael Maaseide
Date: Fri, 17 Apr 2026 10:21:00 -0400
Subject: [PATCH 10/18] ble connection works - fixing save
---
backend/app/services/csv_service.py | 9 ----
data/test_peripheral | Bin 0 -> 81360 bytes
data/test_peripheral.swift | 59 +++++++++++++++++++++++++++
frontend/src/pages/RecordRunPage.tsx | 31 ++++++++++----
4 files changed, 82 insertions(+), 17 deletions(-)
create mode 100755 data/test_peripheral
create mode 100644 data/test_peripheral.swift
diff --git a/backend/app/services/csv_service.py b/backend/app/services/csv_service.py
index f75e17ad..228c5683 100644
--- a/backend/app/services/csv_service.py
+++ b/backend/app/services/csv_service.py
@@ -37,15 +37,6 @@ async def ingest_stride_csv(
if not athlete_check.data:
raise HTTPException(status_code=404, detail="Athlete not found")
- # Transform
- try:
- transformed_df = transform_feet_to_stride_cycles(raw_df)
- except Exception as e:
- logger.exception("Service: Run data transform failed")
- raise HTTPException(
- status_code=500, detail=f"Run data transform failed: {str(e)}"
- ) from e
-
tracer = get_tracer()
with tracer.start_as_current_span("csv.ingest") as span:
span.set_attribute("csv.rows_in", len(raw_df))
diff --git a/data/test_peripheral b/data/test_peripheral
new file mode 100755
index 0000000000000000000000000000000000000000..2788e9228ae905d0e094cbf5a5aa010b4e1a580c
GIT binary patch
literal 81360
zcmeHw3t&{$ng5wgfS?EiqM`^44-o|+39lGmlT3KYBOw#s)X5|>Nk)>HapndRv2`@s
z($-o-Sxc?kCg`>SwdLQ^E_Q8=1ucKqx~N@iT~`BAH*~ej_Rnr-teF4rJLkJIcW$0&
z_wnEVy?S!a?>xS9&Ue1=JKsI`-r?^){NUqJLf8bZEQCo2onwT!UpSEx;&O!L2wrbl
z(Xu6*mz0-MZssK$XJ++6M5kal(WzLnu_CirwvT7#3C6>aP2plL_IkBIlg0p%nV!?3
zCgYs0>JX-RrLJt9vdZy#V_IWXELCi#=bEqR?d3@lHqy&sdf=hFyxwrt?bT_bW_n!<
z6unNSN0{c9x_XUI(rXB4b@joR)@#0Pixj<4JSf8U6tUh3ueUm;MT4PQE;5hrV1c6d
z1s)&aYy%nN+$kIU+YzkM7J9YjNFW-hG1Dt3RP@|TL&pY^8A*&U9Kt;H%Srtzgxwrgj#cIBf^7sf3JYSZVnqZ*b
z-{3dT*8xEx(SzE`i|~->RW%0dwP46h&&l-0Gd;pXq6b;7ho+e6Ens>hnI7SRu9SXY
zeZ$pSvzeZY=}llFgoiY~NHna4d&}QOrl-pv;UUo@X@MCukFU~1kMO|s8bdpRA-^{m
zs=@Gj(W_&6Z}5Bx4@8gljajJR^_CP>6nWjHYdjUQXl&So|KZl=q+b@P96wh6Fn|b5sxp};HwSL(wfItXBr>jtPBd7fa%HcLDgbr
zdJ(3lPm1tB(~a~)Cix;TrlkzTr{N_o90QZmCl
zU*}OpuZ+h>INMCbfJrYJ8p0tx%SumYjKD{-k!Z0e(MH1eDX?
z91RrLHwLtDSgV^`qZiCuABe_+(7Sm>nN>hf^;3yh@2AJMe5??2YyoF{>bevKb9-t#K>HP?D}!39vFbOYe=*{(A?p41Us`p0pjr!V
z54f3#x&JZ#_(1({EYlmJ7G2angO4gZEnk^@^cJ%_TT>tQ87DFm?MMX=!>+gxU&EN}
z3|(~pPUXi7fsKZh3O~WsW6yf!Z0Yd*=XQ?Rf9{B_71MaN>q3$A2(jm^T^!hqxEpcT
zg{~y**O}8JL~_>CS;^Zde}pSZb;Q%f$*wWTM|sX?TuEWO<@FP&-!;aStVccY<&5}g
z7Rux0UG|&x{yInO>AF}XG5q7Ab6RViJzjo7){B>C*~GY?p!_tA
z;bK>^+ip8>;xbnf_Up{8jjrScSBm6Fhe+m(7fGn>p0<`ri3sAZi6Yr
ziFeVyt>rU`Q`CN)NIpybPH`n)LfnQrFH(#;#}LyP#S)jS({8^lF?^)hbIv~L
zz|T-dxUFS!A`f|OEw?4)d^V|hgMa1cb}rk0wsh%!@V2$n?tT5-&S&?Zvx`>ZL(bIE
zt->c-_k)fNygs+{xelt+(;{9ux6`)&oLjVZ*~cIFCdyBMzX`4stjjK&_z|RHyd3)_uH-{#`&|TDw`=Zkw9-1xI`{2kRF~>?fxa4pf&LKzdB8Y!
zX6-+RKB?{ryFee~543_-Bf<&0T6bz696{qN#(Y8J2#?W3gVwAYbRa938?AHv)s>(#
z6QK*V%7$An|-{Z
zt?SJ)$UX7Tt?SM7)|_m`ryS6~24TPZ;=`mrzXLs~KW^5aWqs?9%_Q6YIizX4vQM2KtQT3Bo31-2VB3vzAUl3H?6TzPblWQ(EfW({!Go!BS8^KS
z;$7HlxS#HMuBYXA+NQUXt)j6kqOrifyAgN6mTyJeVc(kQz8Z6A*_!B@E|Qz64CAEz
z=frf3_i9)2D&$E$n9X`HuWvm#{xRhL7Lk0L;`z{n8$|Lwq)*eb>PSW9XJ2%5VZi0Oru538$fYH7#r}e+5I5`sY)7NKz%ap{iEM?Eh
z&Px70H=@;6BwFVXeb`ajr^&WuUwlmvs*95nB+dw-uFI}v4^?E
zD?N}^l2@u@7G4O1_NtzeSMad#>;C_1Xu`
z^e6R0|8iII$DsX7o&Jd-(udBS+i8c-C0g6?Bz72ej
zXW?%=3)|j-wQq-iL;lDi@l$Ohp>0H!Pa+RpOJQ>^D4Q=bR1EB
zN+;0?i{vj6AHsgx6?P?mi*yHkr2UZL{q~E|c4EQ>`zGRJu^))zQ(f1#{u9}Fl2N-S
zahTh79Pu1B?;9N`Ylk28BgAd+CtfDnEn6_ZOW@0GMYtsK5|yEEvi)iwUzhkg@@U_n
zy_)9Jjy<`r`;TI*VHl=ahCcNCcj3QxZ4t@8MV{<$3;L}VNjt{b
zhWnEfUbL@vCGQ39LJVFV*L_#A8RvFx!Tzurd(VhIeVr4KryK$Q8+%ud&9yscnE2O9
zo2XFs#tYkpds(|k9uvwQkxexEDcu)hJ~r6-bCq8c&t3CnJNSCiZa;7a<2@~98~3<2
zvb!SruRT4N^|a95hkbA7Fx*>X?}P1=HW@PgE@b?1*h4u7-4;V`jhOH6kL%|eZMgS0
zmOqYtG25ow&f5~*&a#^SE1JgxvEApky
zna=kb@R<$w8V7~ATHR}Sm94s&=s@pQB0jqq^q^Hhgbx+wGP24+n+e{$VE3hb^6txC^?tO47#t19VW{KR`Ecr84xr6?Ttw
zkNo*dkSBG@O}dDCC+HY#=yCHoROg>)AGkd6GR4>f+Odbej`Tk4V`?AR#QVTT>;n@L
zlR%fw3a+IXb!JJtDM8~ToumBgAUitmGx!d<8i8`&0}cw;%+|VL>2mPOC-_4G05o!zK+tY2gw=&UKFcSuXU!n!C3fY#|cn(ILqP@(B%|7qh(w{-U)LC865&J7`Ee_n%U!2%ycl6RRc@6@}9Cufe%6cbeZ%!a_;dW9-qjW3cG&J^;*tNqIKj4ckSM9ADhU9Y&nXt4`to*`hNRb
zYS(JFjr`}w;HTjJS^Dwh%k5)dPM43ima)o4v2N1Y)#uQEd&^qr`{nW+h3ssbeO#gu
z`6SEj_Hy`xk=oW|FtgRMjh(613p?C+E7`}
zFxsncdA%L}S;vv}zfmg!=6r)eQi$6
zqZoCrlXy#l#`&z9?!k+oGe7TjCcGSd=PXR$vzYJ6-^vnuPC}l^{y9-s%yK2?f+p^@
zj$hN>_dQI;{WRA8Y$?us`JOc6{ssE^F7)dZbji5J^h_WFL$@-py3a}NohM26(1!GD
z9_yd1ukUSodmZcab=*rY-QX3@FzjmY`y_h#Ao%uQsh4}vzpj_ddCw%h{3`MX)Jvhx
zx^=z0L(&_NI-E-Io0A1;^lb<*m`aT9{2liIF$20nebVfvb3;BlnzQX4i$8!w(N;jRALvLwc
z(dC!!8Q+oT>U_TZE}kio&qwDE@FAsrjw|`?W}ojlq60hgGGgiTLD%0yTKAVHV;x9d
z-==!-HONn~ao%qz4|b1yj}y9Is@n|sqa9c;c{VK19dO1v8}}gOu_x$$
zWDD|!BExf8hHo^;aLE8N{AJK>!}-l#iXp=fOS~mP<2<*s6t;2=Y$f|*ndf20bFS}y
zZ}}<4p0T&w=t_PaH09p%+$Y>yu$Sn*AMGs}`-X1!+1KOqveZ7J?;)k67oT#^z}}HM
zhkX|Jq=yhXaDGZYB>8TwiJq_P_my4MqV+Unv>Upz9Q&%xR+L--K9Ibd$KM7zbPj8j
zA=)p`?M$D`8uu&OZ~qbV$=Lsoq@NSe*t)Tw(tWc$OTf6uZbOD!>Fk*9FXfni>q?H1
z>uosOH_#zDm*>9l`QDXlF#bR->P!6rFZoRQpngwJW1fa`b*5~f>COV*a&VrUg?s8r
z@WUp-|DBv5KmKvM;^Rm>r>h%PvO5PpYdlN8$GnSC*|8CP3`IV{$Jtj`!PJbm$nFeKLx!%1)jL{xnUk;
zU$7pjxzRI!BPPG(+|K1V+g8u*jnCR8-*DeRdosx*?mfm$)oGL81zu**0xvWCE5jO)
zZ0P6jb7-!pOJ@PfU-Tw$hI8<~`&>ypM8*GNobmj_I3^{duH*>f4QL1(_%T&npV4m%
zp0$7vB%@@%NnhyR4$nKvK}+42p^kn(Lp0-A&~Mloou-+dUKjFqFz5dM?Z#5j(Cr51
z@sM!L#@Uv%8wanMO3$ZyzmJg7mw{(Ab~-zn_0i{E!F9Y{r2OX0^
zD{G2MPcx)v8q#wO=^8`&c0)SfkX~d+hYabcAzf%lf6kD8(2(A3NIz^yKVnFK%aGn@
zNFOkyZ#AUL4e9lUbd@3f14H`9hV)Mi>7NBTGX~>hj
zim*OYyIo1SPHR+|Q#gO62U-69AmwKUDL*?%d18?Aj|M3hMFWpNdyw*+LCQxBQtlX}
zeEcBg69y^2vj6f?*vw$R#NIDMNuj8RPxbWdL3#qxCBN(Gc?jvrNFV*6r{@8r1=1Bb
z*xiNn*&HFXvpqfcA^joJd;i$e<3Reo3xs$)(bGc$LiC~Eq5%1aP)ENg|6+hR@*hKe
zy6!@HAE%p;{uZY-q#xmQ1nF;Zx*q9oa=H%bHctDIew5RdNI%5stw=BC^hTsxI9-8s
zKBvo&-p=V&NMFI}pU<@Ak6p9FvCqWQkfX~Hz$3lUd-Nr;z;;kXtc
z{c}z`k$#-hGmzeE$bW#-S0aD6A^-D+{EeKRfI6S$^mwH2;Isqj5>8X!`nv2z{vyLT
z%Ns)>e7!bz?p)_Aw-&9R?RIKmXG{x6uAyck^$|1R13z`DAJUTOO>AQTHnGixk~M#E84x5hyh`BT(oZ##TJeAQIztU;?7K2)rZ1|#y7W(<`sonESy
zkr>KvzWHXSXI-(!UB04tNr|bD%h#>xzXr80Us6g)BoP|q%Cb{m3P
zokxB;Stu%8)7E$tm^SB@IpuTQA{Z-+hMSs2#o9ULBH9=d71NRM2YmJQ;p)r=R6UCM
z_+kM`SwOg0&I*OEQNugfs<67w2SL)ng_sts7V;xOf$#Ak3=Of0Fgn!2)#3U=QQa7g
z;)6se$Et8Rf?lf{YfAztVL?VZUFVC{3CL(j3*sBRs0fCFn*2i()Y)8kM`NHdP?*ZE
zz}I1gktja@i8lAj4P%|OU?5hw-G|ru3xOJfG5IP#s>YVw(dfevgRzzPEUdJ~Oa?7;ApKn+-Cs>bre@={n$pKW3|{-EEZw5cjk5q4+r
zS(5L+-MSF#VO3yhG~6(FTl7_wUf=*3fWEpwwbDo+v?||SR0g7F^BpmWXjnX0)4QRe#
z2t^dR@%e}*@ZnYs{%D!YjP}@GGDk@{Fh;RPa7tEtn8pgtowv|r
z-3oNIgJml-^Ep+f+f$vCjJM}6aOD;jxbpDbH(lEoSHb_KKDbMn4g*st-|1pFlxG)8^s{U
zVlX{Vb}S$glqEnjD@$ad);B;E|o(J7=_)3lX`zg|Uf0ZuN`&p0t5`+=tf#O<)
zG`SWz@DA9;5aqrd>uIN>@;NBaMRO
zus?)>HH{Qmr$!2!3yf^LK-jtwFTYUOD$!q;L)ad@4C9|5Yzcfo@r;A}&fGpvZL)V9I9G;c7IAC$W;()~givt!1EDl&4usC3Gz~X?#
z0gD3`2P_U)9I!ZGalqn$#Q}>076&X2SRAl8U~$0WfW-le0~QA?4p076&X2SRAl8U~%A6aljF$
z@00QMa9n-R>xj!Qi-GB*WXi`shACt9owPCKKh;H_sHSpXhPK={s%0^jks{tFNdX
zas1|*#Pl04ls|@J`i7e@{xMG(kK>qrdx`SLb9@QMmvW5Hm2o-Z6F9z{WBf~|xE%3`
z9Md<~gz4W!q076&X2SRAl8U~$0WfW-le0~QA?4p076&X2SRAl8U~$0W
zfW-le0~QA?4p-
zsw4bCe!%V!YxP*p$00T|evji?j{lzHMI8Tv>A%YHn;f6y_ydmr#PKNpo_@S3Ud?z4
z$2y&BO?W=z8#van-hUP6>#>)|8RvK=$M_?+qLT{$LoTT%n2#Qt
z=jG_6Zk_Z;USDPY*L4zvR?8o-a1xMLj{M;Y7o&7k$;o&pB`WF9kgg|Wf6i3h38wz`
z%Kp;jU7b|bnd)KIbA+wS;()~givt!1EDl&4usC3Gz~X?#0gD3`2P_U)9I!ZGalqn$
z#Q}>076&X2SRAl8U~$0WfW-le0~QA?4p076&X2SRAl8U~$0W!1;5)i6(Uj5t|V7xiF&CH7u1<
z-xKFDrf-XdYdAhI<_tvCE|uSD%+C?@@66ju5FS8y5#dssuvH-JwTY~sBjjd@tQQeJ
zM5wZhtTzx!vPITg2up_v`;Ui->^FvqVL8Lauv~;y2pYm}gzq678!m=BMu_1x2)hwJ
zMtC4c41WaSAVMd?%Lu0st{sUo1m^{a5ppjS!yiI;6X7hvs!?M2W`u_jjv~B|FzzBT
zd=0{b2rnRv8!d*r5N<(|oJK)xP>QzJ|ccKy#lJJb~!;V0B>Kx)micS*Nu4R=3x^s>tK<&HzPs
zoiFOE)&kLBObb>Euc>%-qvors4-Bmq_tlF?8K_6<0#RT6YG24#8;JI*LBkW?8L_fagG9<#U*B9TsVt3#8`5-|ixeqp;`Y=o-P|{^)xN&-WBK#ljWI3U;L)PNP^~*0
zlB)xL6&0-pFK5I&CB_*o>1Q%|`It=*Q^QE3%{(WMXDCez)_Ayc#R*fZA+o{a@r2Wq
zbVhi5Lm9#1HJ+kq)R!7I5mFp>_s<_AVd9k6TV1y;rfu{3ebF7kkV!t)c*+|?tHR-k
z@P?~yul8a&!_^ytTHWSwD8P9Qv06_cg-3v(rZc|dE7hSvM)8__^6>tcUu!Bz#KKE8TSNpsU3-epbRJG`3Tt5w&y
zW!1I$fj~Ie?5S$ZT!FGb$tiRI?aFA3E68tXN+uEn|Q
z)3iMKdETnVV7(R$sZ#DbZ*ikmQ{WE!1M5OT%?%aO76)TR9`}kB9#3(^TfCz${2CRjMnEa@vpkC#74uw+QeAP8GskzzX8AAVg3+vT-x_uE}bx>>0D+n6bNOHHR*c0@6
zg8CxI@)s+i^l7vLSc9OiK6q!qFEyjt<7;kO<}HqB#XIzI8)U+>sr*jQ+JbW54k=&d
z5q)wCmqerC=xRv)j96@81SUrFULOlYV6Y*XdASgrhG2-&&CLkOQF7SO7Dd|@w0qWLNoAZp-Y6IF*C`hn2RM8v>tcC)?Zo^V4A}B?-Zg;&e
z79*oOPY2>SBxW<34W5W
zi#8`x-iA7Fpeaz@s0F-r0iPdR?#Ko*a2MhCnw+PyX3VNAam|^tYvdkV{gtySE9^7ZAO;IK+4*<1WTKB^H8ye}>w>$mM%3Q}|uR
zv*18ddDexB-X6v?86RZ4lJOgi8yFW{uG)W=@m|J%$vFE8Rqh(4=vOk{!nl+19>zKN
zog5m^_kl+vQOB6pfcQthqeJDj630c{sqi;|slJ%1aMoy5pMGzLvV*`>{=f`{U*&T8
z9UdxQ16Psw`OMV{-v>QE#>W{?8n5b$d{uur;~9*1GA?8M2;*lMKhJo5qg;uQa2NL7JP|F0QWGEM-G
z22Vt%DxW@CVFzOm5VNAcXMfid#ir%c}6wW7%Ik}D~ypi!P
z#?8Q!QQpS*J0|=B;Yr~4QPutw<5`UDPF0@E_$tN)j2AL?F)m}gobfisWsG+)-pKgN
zj4K&G!nls{amEqGkF;e}bgu&wo
zW7?+(H!;3?8u$yI#2GJTEM8FUvp{2v4Vw@yq6hN>EQAwTgu!}>shYez_-n*%CVbe0
z-&c4zB6*#|U$jr*VE{(#P&n1!N>llE6Mn#i_nGhkg;VspOn44Tkd+!wl?m@R;e#gp
zHzxdR6Hc12&7R4>MGB|-zuAPBm~fd1#}q~{>UzRd{+0>P%+8$eRulf73BPT^_FBD@g;7ggg$iR9>MA#tHz*7)sq1s5
z@>Ypm^wW&;`ia6hI{rt6Q~b|DO`CAR%U0Jkh2bTuYnj6EYSk4`7(0%-9+X(5#=p;m
z51Q~%6MoHvPnhuUO?Vt;VwC^uOn8|IZ!zJJ3FGKCgWi)Se9VM@Z^FaCIpcW7oA4LF
zrdh;RphaN1vNMr>`S#F9OZ$TL|AqXhZ04jvXkYc^*JGpShvB
zaN;((_ie0K-p1m#*m#8aKjEmj*Xe`0@6SA`_!P&
zj#M|L&W?JY45iEZJy+thqfc}M|7lKh<%0lqT6P}i_c)-mPJ#a$o&x_*e`46rp{Xf5
z_4KI!L)6|+p!ztX<%8#?rRDA=-c_YVC3u1=Pnq2~2Twn^#J;7@o_kj;3C1Emt-5Z2
zDnp{PqI6|Y8-R#XV0u(KK&6dk1C@G<)q^H)O?}vhzQX?vp1dixs^`I`6aBu5j87~6
zzj&yTw@?ejG%p@{^7HnB5H6nq92&?67;W&;IUW0qu~#pFwz8#yu$?LGdXZ=
zfxDPn$;#{1iu6$c)~)rpH>gKYz1yYy3Qur*t(@9`TknQaSQ|KMVZ5wR+~?~YsfAtS
zE|zaX=p)ndBK4>mw7!@E6ov+MPgo(ui3gKdwChsDD&|l>DFO(*(PZ0!rq0u@6V(P&%
zX(T-`)_1F5D5m*BcybME5e`-3C5Qk$g_RF))6Zeotcq1b#_9>69Y)7(lh(sua#@BYq5NIvl(2jzrxUhp2-fSRl-
z4N9veC7ORBldn#s334XKIk=zd@#(nw=f;XnMT}vq8BLy*wk&CRFZyMpBY`
z_olywDc1;lcP-w~GVIyC29(){Jfoow@b*rB)Ik)DSm1lVsJhTw9*9H(F&JX`GGFP2
zQuVT+FE8JVH{i$(p;tVc=}k>}H^uKP&LCUdM;5)|s9y6cG?n`Ne%{Q@CDo?ac{BQ{
zZj44Ty43A(Mq$v8qF_ys-p@|e0O3NznUN_v~%y+K!wHiuy
zL?Yc~thu2oTpz4NFx6
zsHvwn*25t$UJR$3o<4V|)f-p$nV-6McCYTcVYoL95VW8(8_svjJmS0fzoo>zT&s@zu#IzH;`&JBqP1KuRjs%AVs8O-+?kQzQjkh0U~
zt<>sO{b)QKZ8`wu;sMSrxrXk6qzLwL=%@G9`*4I?Dc1lW5MjWXu7S);y9N@T_gRW*
zsGs0Q8x(pyzZz_DR100`q1pp#X5VVAbIyDR#kWH!?$t|LBKwjK{Xs!dQQCpoF28c=
zw@^$(M|I*g!;g?<&%m4n1SARFB(f5T{
zcW!BW^G}|49{Q)S?^`!4+4{$u-U&ZG^5Uxxu0Hb3@6335&hLNte8JCt@y0*oO|Jj)
z(Z)%4thL|OzB1~(b>-R3S3JLL;mGUX8}-#~k#A{BihsK8nS$ERXDa_9xcCci+&yZ3
z$w#xkH*?EHGqPsf6?T5*=pP@wY;4a>Z~m<2iOFYXb{_lT3qSeFn`PgR`&(~me(O&6
zj8osAbnl;rKXvz$KiC+4;dD)B_OD-gWm{J1#$i2o9lGnz```TOmzRHW&UZg;`RwmJ
zIlTSGZ@hFta?#!iV&Ncob=h!?f?2-#ifPgia&ns
f$0L4NdFK-sU$<&z$JcHh{oSL#*n7j@kX-&>H*I^O
literal 0
HcmV?d00001
diff --git a/data/test_peripheral.swift b/data/test_peripheral.swift
new file mode 100644
index 00000000..4b8afdee
--- /dev/null
+++ b/data/test_peripheral.swift
@@ -0,0 +1,59 @@
+import CoreBluetooth
+import Foundation
+
+class Delegate: NSObject, CBPeripheralManagerDelegate {
+ func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
+ guard peripheral.state == .poweredOn else {
+ print("Bluetooth state: \(peripheral.state.rawValue)")
+ return
+ }
+ print("Powered on — adding service...")
+ let characteristic = CBMutableCharacteristic(
+ type: CBUUID(string: "2A37"),
+ properties: .notify,
+ value: nil,
+ permissions: .readable
+ )
+ let service = CBMutableService(type: CBUUID(string: "180D"), primary: true)
+ service.characteristics = [characteristic]
+ peripheral.add(service)
+ }
+
+ func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: (any Error)?) {
+ if let error = error {
+ print("Error adding service: \(error)")
+ return
+ }
+ print("Service added — advertising...")
+ peripheral.startAdvertising([
+ CBAdvertisementDataLocalNameKey: "StrideTrack Sensor",
+ CBAdvertisementDataServiceUUIDsKey: [CBUUID(string: "180D")]
+ ])
+ }
+
+ func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: (any Error)?) {
+ if let error = error {
+ print("Error advertising: \(error)")
+ return
+ }
+ print("Advertising — waiting for subscription...")
+ }
+
+ func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
+ print(">>> SUBSCRIBED: \(central.identifier)")
+ }
+
+ func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
+ print(">>> UNSUBSCRIBED: \(central.identifier)")
+ }
+
+ func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) {
+ print(">>> READ REQUEST: \(request.characteristic.uuid)")
+ }
+}
+
+let delegate = Delegate()
+let manager = CBPeripheralManager(delegate: delegate, queue: nil)
+
+print("Running... (Ctrl+C to stop)")
+RunLoop.current.run()
diff --git a/frontend/src/pages/RecordRunPage.tsx b/frontend/src/pages/RecordRunPage.tsx
index 72286584..f4fb20f1 100644
--- a/frontend/src/pages/RecordRunPage.tsx
+++ b/frontend/src/pages/RecordRunPage.tsx
@@ -27,6 +27,7 @@ export default function RecordRunPage() {
const [isStopped, setIsStopped] = useState(false);
const [isConnecting, setIsConnecting] = useState(false);
const [isSaving, setIsSaving] = useState(false);
+ const [saveError, setSaveError] = useState(null);
// Refs persist across re-renders without causing re-renders
const intervalRef = useRef | null>(null);
@@ -111,22 +112,23 @@ export default function RecordRunPage() {
}, 10);
}, [bleMarkStart]);
- // Stop the timer — clears interval, takes final measurement
- const stopTimer = useCallback(() => {
+ // Stop the timer — clears interval, takes final measurement, disconnects BLE
+ const stopTimer = useCallback(async () => {
if (intervalRef.current) clearInterval(intervalRef.current);
stopTimeRef.current = Date.now();
setElapsedMs(stopTimeRef.current - startTimeRef.current);
setIsRunning(false);
setIsStopped(true);
- }, []);
+ await bleDisconnect();
+ }, [bleDisconnect]);
// Save the run to the database, then navigate to the run page
const handleSave = async () => {
if (!athleteId || !eventType) return;
setIsSaving(true);
+ setSaveError(null);
try {
- // Only rows from after Start was pressed
const rows = bleRunBuffer();
const csvLines = ["Time,Force_Foot1,Force_Foot2"];
@@ -144,18 +146,21 @@ export default function RecordRunPage() {
formData.append("elapsed_ms", String(elapsedMs));
formData.append("file", file);
- const response = await api.post("/csv/upload-run", formData, {
- headers: { "Content-Type": "multipart/form-data" },
- });
+ const response = await api.post("/csv/upload-run", formData);
await queryClient.invalidateQueries({ queryKey: ["runs"] });
- await bleDisconnect();
bleClearBuffer();
const runId = response.data.run_id;
+ if (!runId) {
+ throw new Error("Server did not return a run ID");
+ }
navigate(`/athletes/${athleteId}/runs/${runId}`);
} catch (error) {
console.error("Failed to save run:", error);
+ setSaveError(
+ error instanceof Error ? error.message : "Failed to save run"
+ );
} finally {
setIsSaving(false);
}
@@ -429,6 +434,16 @@ export default function RecordRunPage() {
)}
+ {/* Save error feedback */}
+ {saveError && (
+
+ {saveError}
+
+ )}
+
{/* Change selection link — only before starting */}
{!isRunning && !isStopped && (
Date: Fri, 17 Apr 2026 10:30:00 -0400
Subject: [PATCH 11/18] fixing merge changes
---
backend/app/repositories/csv_repository.py | 18 +++++++
backend/app/routes/csv_routes.py | 25 +++-------
backend/app/services/csv_service.py | 56 ++++++----------------
frontend/src/hooks/useBle.hooks.ts | 43 ++++++++++++-----
frontend/src/hooks/useUploadCSV.hooks.ts | 7 +--
frontend/src/pages/RecordRunPage.tsx | 14 +++---
frontend/src/types/ble.types.ts | 5 ++
7 files changed, 84 insertions(+), 84 deletions(-)
create mode 100644 frontend/src/types/ble.types.ts
diff --git a/backend/app/repositories/csv_repository.py b/backend/app/repositories/csv_repository.py
index 3590ad2a..c2da6607 100644
--- a/backend/app/repositories/csv_repository.py
+++ b/backend/app/repositories/csv_repository.py
@@ -1,19 +1,37 @@
import logging
+from uuid import UUID
import pandas as pd
from supabase._async.client import AsyncClient
+from app.core.exceptions import NotFoundException
from app.schemas.csv_schemas import CSVInsertResult
logger = logging.getLogger(__name__)
class CSVRepository:
+ """Repository for CSV-based run ingestion and stride data storage."""
+
def __init__(self, supabase: AsyncClient) -> None:
self.supabase = supabase
self.metrics_table = "run_metrics"
self.run_table = "run"
+ async def verify_athlete_belongs_to_coach(
+ self, athlete_id: str, coach_id: UUID
+ ) -> None:
+ """Verify an athlete belongs to the coach. Raises NotFoundException if not."""
+ result = (
+ await self.supabase.table("athletes")
+ .select("athlete_id")
+ .eq("athlete_id", athlete_id)
+ .eq("coach_id", str(coach_id))
+ .execute()
+ )
+ if not result.data:
+ raise NotFoundException("Athlete", athlete_id)
+
async def create_record(
self,
athlete_id: str,
diff --git a/backend/app/routes/csv_routes.py b/backend/app/routes/csv_routes.py
index 6a8c9a25..4d599166 100644
--- a/backend/app/routes/csv_routes.py
+++ b/backend/app/routes/csv_routes.py
@@ -71,21 +71,10 @@ async def upload_data_csv(
status_code=400, detail=f"Failed to read CSV file: {str(e)}"
) from e
- try:
- result = await service.ingest_stride_csv(
- raw_df,
- athlete_id=str(athlete_id),
- event_type=event_type,
- name=name,
- elapsed_ms=elapsed_ms,
- )
- return result
- except HTTPException:
- span.set_attribute("error", True)
- raise
- except Exception as e:
- logger.exception("Failed to ingest run data frame")
- span.set_attribute("error", True)
- raise HTTPException(
- status_code=500, detail=f"Failed to ingest run data frame: {str(e)}"
- ) from e
+ return await service.ingest_stride_csv(
+ raw_df,
+ athlete_id=str(athlete_id),
+ event_type=event_type,
+ name=name,
+ elapsed_ms=elapsed_ms,
+ )
diff --git a/backend/app/services/csv_service.py b/backend/app/services/csv_service.py
index 228c5683..c4952402 100644
--- a/backend/app/services/csv_service.py
+++ b/backend/app/services/csv_service.py
@@ -2,7 +2,6 @@
from uuid import UUID
import pandas as pd
-from fastapi import HTTPException
from app.core.observability import get_tracer
from app.repositories.csv_repository import CSVRepository
@@ -13,6 +12,8 @@
class CSVService:
+ """Service for CSV-based run ingestion."""
+
def __init__(self, repository: CSVRepository, coach_id: UUID) -> None:
self.repository = repository
self.coach_id = coach_id
@@ -25,55 +26,28 @@ async def ingest_stride_csv(
name: str | None = None,
elapsed_ms: int | None = None,
) -> CSVUploadResponse:
-
- # Athlete Check
- athlete_check = (
- await self.repository.supabase.table("athletes")
- .select("athlete_id")
- .eq("athlete_id", athlete_id)
- .eq("coach_id", str(self.coach_id))
- .execute()
- )
- if not athlete_check.data:
- raise HTTPException(status_code=404, detail="Athlete not found")
+ """Transform raw CSV data into stride cycles and persist a complete run."""
+ await self.repository.verify_athlete_belongs_to_coach(athlete_id, self.coach_id)
tracer = get_tracer()
with tracer.start_as_current_span("csv.ingest") as span:
span.set_attribute("csv.rows_in", len(raw_df))
- # Use client-provided elapsed_ms (wall-clock); fall back to CSV Time delta
if elapsed_ms is None and "Time" in raw_df.columns and len(raw_df) > 0:
elapsed_ms = int(raw_df["Time"].max() - raw_df["Time"].min())
- # Transform
- try:
- transformed_df = transform_feet_to_stride_cycles(raw_df)
- span.set_attribute("csv.rows_transformed", len(transformed_df))
- except Exception as e:
- logger.exception("Service: Run data transform failed")
- span.set_attribute("error", True)
- raise HTTPException(
- status_code=500, detail=f"Run data transform failed: {str(e)}"
- ) from e
+ transformed_df = transform_feet_to_stride_cycles(raw_df)
+ span.set_attribute("csv.rows_transformed", len(transformed_df))
- # Load
- try:
- result = await self.repository.insert_complete_run(
- df=transformed_df,
- athlete_id=athlete_id,
- event_type=event_type,
- name=name,
- elapsed_ms=elapsed_ms,
- )
- span.set_attribute("csv.run_id", result.run_id)
- span.set_attribute("csv.rows_inserted", result.rows_inserted)
- except Exception as e:
- logger.exception("Service: Transformed run data insert failed")
- span.set_attribute("error", True)
- raise HTTPException(
- status_code=500,
- detail=f"Transformed run data insert failed: {str(e)}",
- ) from e
+ result = await self.repository.insert_complete_run(
+ df=transformed_df,
+ athlete_id=athlete_id,
+ event_type=event_type,
+ name=name,
+ elapsed_ms=elapsed_ms,
+ )
+ span.set_attribute("csv.run_id", result.run_id)
+ span.set_attribute("csv.rows_inserted", result.rows_inserted)
logger.info(
f"Service: ingest_stride_csv rows_in={len(raw_df)} rows_out={len(transformed_df)} run_id={result.run_id}"
diff --git a/frontend/src/hooks/useBle.hooks.ts b/frontend/src/hooks/useBle.hooks.ts
index 6153a1ff..307a5979 100644
--- a/frontend/src/hooks/useBle.hooks.ts
+++ b/frontend/src/hooks/useBle.hooks.ts
@@ -1,5 +1,8 @@
import { useState, useRef, useCallback, useEffect } from "react";
import { BleClient } from "@capacitor-community/bluetooth-le";
+import type { BleForceRow } from "@/types/ble.types";
+
+export type { BleForceRow };
const FORCE_PLATE_SERVICE_UUID = "0000180d-0000-1000-8000-00805f9b34fb";
const FORCE_PLATE_CHARACTERISTIC_UUID = "00002a37-0000-1000-8000-00805f9b34fb";
@@ -9,12 +12,6 @@ const FORCE_PLATE_CHARACTERISTIC_UUID = "00002a37-0000-1000-8000-00805f9b34fb";
// so this is false and we fall through to the BleClient (Capacitor) path.
const IS_WEB_BLUETOOTH = "bluetooth" in navigator;
-export interface BleForceRow {
- time: number;
- force_foot1: number;
- force_foot2: number;
-}
-
function parseNotification(value: DataView): BleForceRow {
const time = value.getFloat32(0, true);
const force_foot1 = value.getFloat32(4, true);
@@ -36,6 +33,8 @@ export function useBle() {
// ── Web Bluetooth refs ───────────────────────────────────────
const webDeviceRef = useRef(null);
const webCharRef = useRef(null);
+ const webDisconnectListenerRef = useRef<(() => void) | null>(null);
+ const webValueListenerRef = useRef<((event: Event) => void) | null>(null);
// ── Availability check ───────────────────────────────────────
@@ -74,10 +73,12 @@ export function useBle() {
webDeviceRef.current = device;
- device.addEventListener("gattserverdisconnected", () => {
+ const onDisconnect = () => {
console.log("[BLE-web] GATT server disconnected");
setBleIsConnected(false);
- });
+ };
+ webDisconnectListenerRef.current = onDisconnect;
+ device.addEventListener("gattserverdisconnected", onDisconnect);
console.log("[BLE-web] connecting to GATT server...");
const server = await device.gatt!.connect();
@@ -100,12 +101,14 @@ export function useBle() {
webCharRef.current = characteristic;
- characteristic.addEventListener("characteristicvaluechanged", (event) => {
+ const onValue = (event: Event) => {
const char = event.target as BluetoothRemoteGATTCharacteristic;
if (char.value) {
bufferRef.current.push(parseNotification(char.value));
}
- });
+ };
+ webValueListenerRef.current = onValue;
+ characteristic.addEventListener("characteristicvaluechanged", onValue);
console.log("[BLE-web] startNotifications...");
await characteristic.startNotifications();
@@ -116,6 +119,13 @@ export function useBle() {
const webDisconnect = useCallback(async () => {
if (webCharRef.current) {
+ if (webValueListenerRef.current) {
+ webCharRef.current.removeEventListener(
+ "characteristicvaluechanged",
+ webValueListenerRef.current
+ );
+ webValueListenerRef.current = null;
+ }
try {
await webCharRef.current.stopNotifications();
} catch {
@@ -123,8 +133,17 @@ export function useBle() {
}
webCharRef.current = null;
}
- if (webDeviceRef.current?.gatt?.connected) {
- webDeviceRef.current.gatt.disconnect();
+ if (webDeviceRef.current) {
+ if (webDisconnectListenerRef.current) {
+ webDeviceRef.current.removeEventListener(
+ "gattserverdisconnected",
+ webDisconnectListenerRef.current
+ );
+ webDisconnectListenerRef.current = null;
+ }
+ if (webDeviceRef.current.gatt?.connected) {
+ webDeviceRef.current.gatt.disconnect();
+ }
}
webDeviceRef.current = null;
setBleIsConnected(false);
diff --git a/frontend/src/hooks/useUploadCSV.hooks.ts b/frontend/src/hooks/useUploadCSV.hooks.ts
index 4a74ae5f..b2b3115c 100644
--- a/frontend/src/hooks/useUploadCSV.hooks.ts
+++ b/frontend/src/hooks/useUploadCSV.hooks.ts
@@ -13,12 +13,7 @@ export function useUploadCSV() {
const response = await api.post(
`/athletes/${athleteId}/csv/upload-run`,
- formData,
- {
- headers: {
- "Content-Type": "multipart/form-data",
- },
- }
+ formData
);
return response.data;
diff --git a/frontend/src/pages/RecordRunPage.tsx b/frontend/src/pages/RecordRunPage.tsx
index f4fb20f1..43d57134 100644
--- a/frontend/src/pages/RecordRunPage.tsx
+++ b/frontend/src/pages/RecordRunPage.tsx
@@ -141,12 +141,14 @@ export default function RecordRunPage() {
const file = new File([blob], "run_data.csv", { type: "text/csv" });
const formData = new FormData();
- formData.append("athlete_id", athleteId);
formData.append("event_type", eventType);
formData.append("elapsed_ms", String(elapsedMs));
formData.append("file", file);
- const response = await api.post("/csv/upload-run", formData);
+ const response = await api.post(
+ `/athletes/${athleteId}/csv/upload-run`,
+ formData
+ );
await queryClient.invalidateQueries({ queryKey: ["runs"] });
bleClearBuffer();
@@ -266,8 +268,6 @@ export default function RecordRunPage() {
}
// ── Recording Screen ──
- const saving = isSaving;
-
return (
{/* Header */}
@@ -415,21 +415,21 @@ export default function RecordRunPage() {
Delete
- {saving ? "Saving..." : "Save"}
+ {isSaving ? "Saving..." : "Save"}
)}
diff --git a/frontend/src/types/ble.types.ts b/frontend/src/types/ble.types.ts
new file mode 100644
index 00000000..b05acedf
--- /dev/null
+++ b/frontend/src/types/ble.types.ts
@@ -0,0 +1,5 @@
+export interface BleForceRow {
+ time: number;
+ force_foot1: number;
+ force_foot2: number;
+}
From c64dd6eb2fa5b1a4fe39d96a2a49aa7736b57258 Mon Sep 17 00:00:00 2001
From: Michael Maaseide
Date: Fri, 17 Apr 2026 10:44:07 -0400
Subject: [PATCH 12/18] ble and save working
---
frontend/ios/App/App/config 3.xml | 6 ++++++
1 file changed, 6 insertions(+)
create mode 100644 frontend/ios/App/App/config 3.xml
diff --git a/frontend/ios/App/App/config 3.xml b/frontend/ios/App/App/config 3.xml
new file mode 100644
index 00000000..1b1b0e0d
--- /dev/null
+++ b/frontend/ios/App/App/config 3.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
From 6c548169f6906b9d0373951cc406acf2454c60b1 Mon Sep 17 00:00:00 2001
From: Michael Maaseide
Date: Fri, 17 Apr 2026 10:45:45 -0400
Subject: [PATCH 13/18] connection button look changes
---
frontend/src/pages/RecordRunPage.tsx | 84 ++++++++++++++++++----------
1 file changed, 53 insertions(+), 31 deletions(-)
diff --git a/frontend/src/pages/RecordRunPage.tsx b/frontend/src/pages/RecordRunPage.tsx
index 43d57134..a1060c3e 100644
--- a/frontend/src/pages/RecordRunPage.tsx
+++ b/frontend/src/pages/RecordRunPage.tsx
@@ -345,21 +345,6 @@ export default function RecordRunPage() {
locally.
)}
-
- {/* Web-only manual connect button — browser requires a click gesture */}
- {!isNative && !bleIsConnected && !isConnecting && (
-
- Connect Sensor
-
- )}
{/* Timer circle */}
@@ -393,22 +378,59 @@ export default function RecordRunPage() {
- {/* Start/Stop button — hidden after stopping */}
- {!isStopped && (
-
- {isRunning ? "Stop" : "Start"}
-
- )}
+ {/* Connect / Start / Stop button — hidden after stopping */}
+ {!isStopped &&
+ (() => {
+ const needsConnect =
+ !isNative && !bleIsConnected && !isRunning && !isConnecting;
+ const showConnecting = isConnecting;
+
+ if (showConnecting) {
+ return (
+
+ Connecting...
+
+ );
+ }
+
+ if (needsConnect) {
+ return (
+
+ Connect Sensor
+
+ );
+ }
+
+ return (
+
+ {isRunning ? "Stop" : "Start"}
+
+ );
+ })()}
{/* Save/Delete buttons — shown after stopping */}
{isStopped && (
From ce395f55cade8a09d4219f7028bce2612540b0d3 Mon Sep 17 00:00:00 2001
From: Michael Maaseide
Date: Fri, 17 Apr 2026 11:02:51 -0400
Subject: [PATCH 14/18] minor file cleaning
---
.gitignore | 3 +++
data/test_peripheral | Bin 81360 -> 0 bytes
frontend/ios/App/App/config 3.xml | 6 ------
3 files changed, 3 insertions(+), 6 deletions(-)
delete mode 100755 data/test_peripheral
delete mode 100644 frontend/ios/App/App/config 3.xml
diff --git a/.gitignore b/.gitignore
index e58910f3..febec926 100644
--- a/.gitignore
+++ b/.gitignore
@@ -237,6 +237,9 @@ lerna-debug.log*
# Mise
mise.toml
+# Compiled test binaries
+data/test_peripheral
+
# Claude Code (personal/local only)
.mcp.json
.claude/settings.local.json
\ No newline at end of file
diff --git a/data/test_peripheral b/data/test_peripheral
deleted file mode 100755
index 2788e9228ae905d0e094cbf5a5aa010b4e1a580c..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 81360
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
diff --git a/frontend/ios/App/App/config 3.xml b/frontend/ios/App/App/config 3.xml
deleted file mode 100644
index 1b1b0e0d..00000000
--- a/frontend/ios/App/App/config 3.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
From 3b4a4958415dad9c63c075d1d26808409436eb45 Mon Sep 17 00:00:00 2001
From: Michael Maaseide
Date: Fri, 17 Apr 2026 12:34:20 -0400
Subject: [PATCH 15/18] device data storage added
---
frontend/src/hooks/useBle.hooks.ts | 180 ++++++++++++++++++++-------
frontend/src/pages/RecordRunPage.tsx | 21 ++--
2 files changed, 149 insertions(+), 52 deletions(-)
diff --git a/frontend/src/hooks/useBle.hooks.ts b/frontend/src/hooks/useBle.hooks.ts
index 307a5979..0cd461ef 100644
--- a/frontend/src/hooks/useBle.hooks.ts
+++ b/frontend/src/hooks/useBle.hooks.ts
@@ -19,14 +19,22 @@ function parseNotification(value: DataView): BleForceRow {
return { time, force_foot1, force_foot2 };
}
+const RECONNECT_INTERVAL_MS = 1000;
+const RECONNECT_MAX_ATTEMPTS = 30;
+
export function useBle() {
const [bleIsAvailable, setBleIsAvailable] = useState(false);
const [bleIsConnected, setBleIsConnected] = useState(false);
const [bleIsScanning, setBleIsScanning] = useState(false);
+ const [bleIsReconnecting, setBleIsReconnecting] = useState(false);
const bufferRef = useRef([]);
const startIndexRef = useRef(null);
+ // When true, disconnect was intentional — skip auto-reconnect.
+ const intentionalDisconnectRef = useRef(false);
+ const reconnectTimerRef = useRef | null>(null);
+
// ── Native (Capacitor) refs ──────────────────────────────────
const deviceIdRef = useRef(null);
@@ -62,6 +70,112 @@ export function useBle() {
};
}, []);
+ // ── Reconnect helpers ─────────────────────────────────────────
+
+ const cancelReconnect = useCallback(() => {
+ if (reconnectTimerRef.current) {
+ clearTimeout(reconnectTimerRef.current);
+ reconnectTimerRef.current = null;
+ }
+ setBleIsReconnecting(false);
+ }, []);
+
+ const webSubscribeToNotifications = useCallback(
+ async (device: BluetoothDevice) => {
+ const server = await device.gatt!.connect();
+ console.log("[BLE-web] GATT reconnected:", server.connected);
+
+ const service = await server.getPrimaryService(FORCE_PLATE_SERVICE_UUID);
+ const characteristic = await service.getCharacteristic(
+ FORCE_PLATE_CHARACTERISTIC_UUID
+ );
+ webCharRef.current = characteristic;
+
+ const onValue = (event: Event) => {
+ const char = event.target as BluetoothRemoteGATTCharacteristic;
+ if (char.value) {
+ bufferRef.current.push(parseNotification(char.value));
+ }
+ };
+ webValueListenerRef.current = onValue;
+ characteristic.addEventListener("characteristicvaluechanged", onValue);
+
+ await characteristic.startNotifications();
+ console.log("[BLE-web] notifications re-started OK");
+ },
+ []
+ );
+
+ const attemptWebReconnect = useCallback(
+ (device: BluetoothDevice, attempt: number) => {
+ if (intentionalDisconnectRef.current) return;
+ if (attempt > RECONNECT_MAX_ATTEMPTS) {
+ console.log("[BLE-web] max reconnect attempts reached");
+ setBleIsReconnecting(false);
+ return;
+ }
+
+ setBleIsReconnecting(true);
+ console.log(
+ `[BLE-web] reconnect attempt ${attempt}/${RECONNECT_MAX_ATTEMPTS}`
+ );
+
+ reconnectTimerRef.current = setTimeout(async () => {
+ try {
+ await webSubscribeToNotifications(device);
+ setBleIsConnected(true);
+ setBleIsReconnecting(false);
+ console.log("[BLE-web] reconnected successfully");
+ } catch {
+ attemptWebReconnect(device, attempt + 1);
+ }
+ }, RECONNECT_INTERVAL_MS);
+ },
+ [webSubscribeToNotifications]
+ );
+
+ const attemptNativeReconnect = useCallback(
+ (deviceId: string, attempt: number) => {
+ if (intentionalDisconnectRef.current) return;
+ if (attempt > RECONNECT_MAX_ATTEMPTS) {
+ console.log("[BLE-native] max reconnect attempts reached");
+ setBleIsReconnecting(false);
+ return;
+ }
+
+ setBleIsReconnecting(true);
+ console.log(
+ `[BLE-native] reconnect attempt ${attempt}/${RECONNECT_MAX_ATTEMPTS}`
+ );
+
+ reconnectTimerRef.current = setTimeout(async () => {
+ try {
+ await BleClient.connect(deviceId, () => {
+ console.log("[BLE-native] disconnected during reconnect session");
+ setBleIsConnected(false);
+ if (!intentionalDisconnectRef.current) {
+ attemptNativeReconnect(deviceId, 1);
+ }
+ });
+ await BleClient.startNotifications(
+ deviceId,
+ FORCE_PLATE_SERVICE_UUID,
+ FORCE_PLATE_CHARACTERISTIC_UUID,
+ (value: DataView) => {
+ bufferRef.current.push(parseNotification(value));
+ }
+ );
+ setBleIsConnected(true);
+ setBleIsReconnecting(false);
+ console.log("[BLE-native] reconnected successfully");
+ } catch {
+ attemptNativeReconnect(deviceId, attempt + 1);
+ }
+ }, RECONNECT_INTERVAL_MS);
+ },
+ []
+ );
+
// ── Web Bluetooth path ───────────────────────────────────────
const webConnect = useCallback(async () => {
@@ -72,52 +186,28 @@ export function useBle() {
console.log("[BLE-web] device selected:", device.name, device.id);
webDeviceRef.current = device;
+ intentionalDisconnectRef.current = false;
const onDisconnect = () => {
console.log("[BLE-web] GATT server disconnected");
setBleIsConnected(false);
+ if (!intentionalDisconnectRef.current && webDeviceRef.current) {
+ attemptWebReconnect(webDeviceRef.current, 1);
+ }
};
webDisconnectListenerRef.current = onDisconnect;
device.addEventListener("gattserverdisconnected", onDisconnect);
console.log("[BLE-web] connecting to GATT server...");
- const server = await device.gatt!.connect();
- console.log("[BLE-web] GATT connected:", server.connected);
-
- console.log("[BLE-web] getPrimaryService...");
- const service = await server.getPrimaryService(FORCE_PLATE_SERVICE_UUID);
- console.log("[BLE-web] service found");
-
- console.log("[BLE-web] getCharacteristic...");
- const characteristic = await service.getCharacteristic(
- FORCE_PLATE_CHARACTERISTIC_UUID
- );
- console.log("[BLE-web] characteristic properties:", {
- notify: characteristic.properties.notify,
- read: characteristic.properties.read,
- indicate: characteristic.properties.indicate,
- write: characteristic.properties.write,
- });
-
- webCharRef.current = characteristic;
-
- const onValue = (event: Event) => {
- const char = event.target as BluetoothRemoteGATTCharacteristic;
- if (char.value) {
- bufferRef.current.push(parseNotification(char.value));
- }
- };
- webValueListenerRef.current = onValue;
- characteristic.addEventListener("characteristicvaluechanged", onValue);
-
- console.log("[BLE-web] startNotifications...");
- await characteristic.startNotifications();
- console.log("[BLE-web] notifications started OK");
+ await webSubscribeToNotifications(device);
+ console.log("[BLE-web] connected and subscribed");
setBleIsConnected(true);
- }, []);
+ }, [attemptWebReconnect, webSubscribeToNotifications]);
const webDisconnect = useCallback(async () => {
+ intentionalDisconnectRef.current = true;
+ cancelReconnect();
if (webCharRef.current) {
if (webValueListenerRef.current) {
webCharRef.current.removeEventListener(
@@ -147,18 +237,13 @@ export function useBle() {
}
webDeviceRef.current = null;
setBleIsConnected(false);
- }, []);
+ }, [cancelReconnect]);
// ── Native (Capacitor) path ──────────────────────────────────
- const handleNativeDisconnect = useCallback((deviceId: string) => {
- if (deviceIdRef.current === deviceId) {
- setBleIsConnected(false);
- }
- }, []);
-
const nativeConnect = useCallback(async () => {
console.log("[BLE-native] initializing...");
+ intentionalDisconnectRef.current = false;
try {
await BleClient.initialize();
const enabled = await BleClient.isEnabled();
@@ -187,7 +272,13 @@ export function useBle() {
console.log("[BLE-native] connecting...");
await BleClient.connect(device.deviceId, () => {
console.log("[BLE-native] disconnected callback fired");
- handleNativeDisconnect(device.deviceId);
+ setBleIsConnected(false);
+ if (
+ !intentionalDisconnectRef.current &&
+ deviceIdRef.current === device.deviceId
+ ) {
+ attemptNativeReconnect(device.deviceId, 1);
+ }
});
console.log("[BLE-native] connected");
@@ -208,9 +299,11 @@ export function useBle() {
setBleIsScanning(false);
throw error;
}
- }, [handleNativeDisconnect]);
+ }, [attemptNativeReconnect]);
const nativeDisconnect = useCallback(async () => {
+ intentionalDisconnectRef.current = true;
+ cancelReconnect();
const deviceId = deviceIdRef.current;
if (!deviceId) return;
@@ -232,7 +325,7 @@ export function useBle() {
setBleIsConnected(false);
deviceIdRef.current = null;
- }, []);
+ }, [cancelReconnect]);
// ── Public API (routes to correct path) ─────────────────────
@@ -268,6 +361,7 @@ export function useBle() {
bleIsAvailable,
bleIsConnected,
bleIsScanning,
+ bleIsReconnecting,
bleConnect,
bleDisconnect,
bleDataBuffer,
diff --git a/frontend/src/pages/RecordRunPage.tsx b/frontend/src/pages/RecordRunPage.tsx
index a1060c3e..a0bbc645 100644
--- a/frontend/src/pages/RecordRunPage.tsx
+++ b/frontend/src/pages/RecordRunPage.tsx
@@ -42,6 +42,7 @@ export default function RecordRunPage() {
bleIsAvailable,
bleIsConnected,
bleIsScanning,
+ bleIsReconnecting,
bleConnect,
bleDisconnect,
bleMarkStart,
@@ -284,13 +285,13 @@ export default function RecordRunPage() {
className="flex items-center gap-2 px-4 py-1.5 rounded-full text-xs font-medium"
style={{
backgroundColor:
- isConnecting || bleIsScanning
+ isConnecting || bleIsScanning || bleIsReconnecting
? "hsl(var(--muted))"
: bleIsConnected
? "hsl(var(--primary) / 0.1)"
: "hsl(var(--destructive) / 0.1)",
color:
- isConnecting || bleIsScanning
+ isConnecting || bleIsScanning || bleIsReconnecting
? "hsl(var(--muted-foreground))"
: bleIsConnected
? "hsl(var(--primary))"
@@ -298,10 +299,10 @@ export default function RecordRunPage() {
}}
>
{isConnecting || bleIsScanning
? "Connecting..."
- : bleIsConnected
- ? "Connected"
- : "Disconnected"}
+ : bleIsReconnecting
+ ? "Reconnecting..."
+ : bleIsConnected
+ ? "Connected"
+ : "Disconnected"}
Losing connection mid-run is expected. The sensor will automatically
- reconnect when the runner comes back within range. Data is buffered
- locally.
+ reconnect when the runner comes back within range. The sensor stores
+ data on-device, so missed readings are recovered on reconnection.
)}
From 80ed2cc4dc2bbbe2b9daa17aa61a2a8d7995c72a Mon Sep 17 00:00:00 2001
From: Michael Maaseide
Date: Fri, 17 Apr 2026 12:38:24 -0400
Subject: [PATCH 16/18] mock ble bug fix
---
data/mock_ble_client.py | 90 +++++++++++++++++++++++++++--------------
1 file changed, 59 insertions(+), 31 deletions(-)
diff --git a/data/mock_ble_client.py b/data/mock_ble_client.py
index 48a63ea9..90e52ead 100644
--- a/data/mock_ble_client.py
+++ b/data/mock_ble_client.py
@@ -235,42 +235,74 @@ def peripheralManager_didReceiveWriteRequests_(self, peripheral, requests):
# ── Notification streaming (runs on a background thread) ────────
-def _stream_notifications(delegate):
- """Stream CSV rows continuously as BLE notifications at ~100Hz.
+def _stream_notifications(delegate: PeripheralDelegate) -> None:
+ """Stream CSV rows as BLE notifications, buffering data during disconnects.
- Loops back to the start of the CSV when all rows have been sent,
- so the stream runs until the central disconnects.
+ Simulates on-device storage: when the central disconnects, data continues
+ to be collected into a backlog at ~100Hz. On reconnection the backlog is
+ flushed first, then live streaming resumes — so no gap in the data.
"""
rows = delegate._rows
- manager = delegate._manager
- characteristic = delegate._characteristic
row_index = 0
notifications_sent = 0
+ backlog: list[tuple[float, float, float]] = []
- while delegate._subscribed.is_set():
- time_val, foot1, foot2 = rows[row_index]
- payload = pack_notification(time_val, foot1, foot2)
- ns_data = NSData.dataWithBytes_length_(payload, len(payload))
+ while True:
+ delegate._subscribed.wait()
+ manager = delegate._manager
+ characteristic = delegate._characteristic
+
+ if backlog:
+ logger.info("Flushing %d backlogged rows...", len(backlog))
+ for bl_time, bl_foot1, bl_foot2 in backlog:
+ if not delegate._subscribed.is_set():
+ break
+ payload = pack_notification(bl_time, bl_foot1, bl_foot2)
+ ns_data = NSData.dataWithBytes_length_(payload, len(payload))
+ while not manager.updateValue_forCharacteristic_onSubscribedCentrals_(
+ ns_data, characteristic, None
+ ):
+ time.sleep(0.001)
+ notifications_sent += 1
+ backlog.clear()
+ logger.info("Backlog flushed, resuming live stream")
+
+ while delegate._subscribed.is_set():
+ time_val, foot1, foot2 = rows[row_index]
+ payload = pack_notification(time_val, foot1, foot2)
+ ns_data = NSData.dataWithBytes_length_(payload, len(payload))
+
+ did_send = manager.updateValue_forCharacteristic_onSubscribedCentrals_(
+ ns_data, characteristic, None
+ )
+ if not did_send:
+ time.sleep(NOTIFICATION_INTERVAL_S)
+ continue
- did_send = manager.updateValue_forCharacteristic_onSubscribedCentrals_(
- ns_data, characteristic, None
- )
- if not did_send:
- time.sleep(NOTIFICATION_INTERVAL_S)
- continue
+ row_index = (row_index + 1) % len(rows)
+ notifications_sent += 1
+
+ if notifications_sent % 100 == 0:
+ logger.info(
+ "Sent %d notifications (row %d/%d)",
+ notifications_sent,
+ row_index,
+ len(rows),
+ )
- row_index = (row_index + 1) % len(rows)
- notifications_sent += 1
+ time.sleep(NOTIFICATION_INTERVAL_S)
- if notifications_sent % 100 == 0:
- logger.info("Sent %d notifications (row %d/%d)", notifications_sent, row_index, len(rows))
+ logger.info("Central disconnected — buffering data for reconnection...")
+ while not delegate._subscribed.is_set():
+ time_val, foot1, foot2 = rows[row_index]
+ backlog.append((time_val, foot1, foot2))
+ row_index = (row_index + 1) % len(rows)
- time.sleep(NOTIFICATION_INTERVAL_S)
+ if len(backlog) % 100 == 0:
+ logger.info("Backlog: %d rows buffered", len(backlog))
- logger.info(
- "Central unsubscribed — stopped after %d notifications", notifications_sent
- )
+ time.sleep(NOTIFICATION_INTERVAL_S)
# ── Main entry point ────────────────────────────────────────────
@@ -293,13 +325,9 @@ def run_peripheral(csv_path: str) -> None:
manager = CBPeripheralManager.alloc().initWithDelegate_queue_(delegate, None)
delegate._manager = manager
- # Background thread waits for subscription, then streams data.
- def _wait_and_stream():
- delegate._subscribed.wait()
- logger.info("Starting notification stream...")
- _stream_notifications(delegate)
-
- threading.Thread(target=_wait_and_stream, daemon=True).start()
+ threading.Thread(
+ target=_stream_notifications, args=(delegate,), daemon=True
+ ).start()
# The main run loop processes all CoreBluetooth callbacks.
# Setup is driven by the delegate: poweredOn → addService → advertise.
From 265e4cea7dddc1b6714d2da885004795a88794a9 Mon Sep 17 00:00:00 2001
From: Michael Maaseide
Date: Fri, 17 Apr 2026 15:11:30 -0400
Subject: [PATCH 17/18] zoom ios bug
---
frontend/index.html | 2 +-
frontend/ios/App/App.xcodeproj/project.pbxproj | 4 ++--
frontend/src/index.css | 8 ++++++++
3 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/frontend/index.html b/frontend/index.html
index ab4a46c9..13653b9c 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -8,7 +8,7 @@
StrideTrack
Date: Fri, 17 Apr 2026 16:46:00 -0400
Subject: [PATCH 18/18] sam fix
---
.gitignore | 3 --
backend/app/services/csv_service.py | 60 ++++++++++++++++-------------
data/test_peripheral.swift | 59 ----------------------------
3 files changed, 33 insertions(+), 89 deletions(-)
delete mode 100644 data/test_peripheral.swift
diff --git a/.gitignore b/.gitignore
index febec926..e58910f3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -237,9 +237,6 @@ lerna-debug.log*
# Mise
mise.toml
-# Compiled test binaries
-data/test_peripheral
-
# Claude Code (personal/local only)
.mcp.json
.claude/settings.local.json
\ No newline at end of file
diff --git a/backend/app/services/csv_service.py b/backend/app/services/csv_service.py
index c4952402..f514e193 100644
--- a/backend/app/services/csv_service.py
+++ b/backend/app/services/csv_service.py
@@ -2,6 +2,7 @@
from uuid import UUID
import pandas as pd
+from opentelemetry.trace import StatusCode
from app.core.observability import get_tracer
from app.repositories.csv_repository import CSVRepository
@@ -31,30 +32,35 @@ async def ingest_stride_csv(
tracer = get_tracer()
with tracer.start_as_current_span("csv.ingest") as span:
- span.set_attribute("csv.rows_in", len(raw_df))
-
- if elapsed_ms is None and "Time" in raw_df.columns and len(raw_df) > 0:
- elapsed_ms = int(raw_df["Time"].max() - raw_df["Time"].min())
-
- transformed_df = transform_feet_to_stride_cycles(raw_df)
- span.set_attribute("csv.rows_transformed", len(transformed_df))
-
- result = await self.repository.insert_complete_run(
- df=transformed_df,
- athlete_id=athlete_id,
- event_type=event_type,
- name=name,
- elapsed_ms=elapsed_ms,
- )
- span.set_attribute("csv.run_id", result.run_id)
- span.set_attribute("csv.rows_inserted", result.rows_inserted)
-
- logger.info(
- f"Service: ingest_stride_csv rows_in={len(raw_df)} rows_out={len(transformed_df)} run_id={result.run_id}"
- )
-
- return CSVUploadResponse(
- message=f"CSV uploaded successfully. Run ID: {result.run_id}, Rows inserted: {result.rows_inserted}",
- run_id=str(result.run_id),
- rows_inserted=result.rows_inserted,
- )
+ try:
+ span.set_attribute("csv.rows_in", len(raw_df))
+
+ if elapsed_ms is None and "Time" in raw_df.columns and len(raw_df) > 0:
+ elapsed_ms = int(raw_df["Time"].max() - raw_df["Time"].min())
+
+ transformed_df = transform_feet_to_stride_cycles(raw_df)
+ span.set_attribute("csv.rows_transformed", len(transformed_df))
+
+ result = await self.repository.insert_complete_run(
+ df=transformed_df,
+ athlete_id=athlete_id,
+ event_type=event_type,
+ name=name,
+ elapsed_ms=elapsed_ms,
+ )
+ span.set_attribute("csv.run_id", result.run_id)
+ span.set_attribute("csv.rows_inserted", result.rows_inserted)
+
+ logger.info(
+ f"Service: ingest_stride_csv rows_in={len(raw_df)} rows_out={len(transformed_df)} run_id={result.run_id}"
+ )
+
+ return CSVUploadResponse(
+ message=f"CSV uploaded successfully. Run ID: {result.run_id}, Rows inserted: {result.rows_inserted}",
+ run_id=str(result.run_id),
+ rows_inserted=result.rows_inserted,
+ )
+ except Exception as e:
+ span.set_status(StatusCode.ERROR, str(e))
+ span.record_exception(e)
+ raise
diff --git a/data/test_peripheral.swift b/data/test_peripheral.swift
deleted file mode 100644
index 4b8afdee..00000000
--- a/data/test_peripheral.swift
+++ /dev/null
@@ -1,59 +0,0 @@
-import CoreBluetooth
-import Foundation
-
-class Delegate: NSObject, CBPeripheralManagerDelegate {
- func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
- guard peripheral.state == .poweredOn else {
- print("Bluetooth state: \(peripheral.state.rawValue)")
- return
- }
- print("Powered on — adding service...")
- let characteristic = CBMutableCharacteristic(
- type: CBUUID(string: "2A37"),
- properties: .notify,
- value: nil,
- permissions: .readable
- )
- let service = CBMutableService(type: CBUUID(string: "180D"), primary: true)
- service.characteristics = [characteristic]
- peripheral.add(service)
- }
-
- func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: (any Error)?) {
- if let error = error {
- print("Error adding service: \(error)")
- return
- }
- print("Service added — advertising...")
- peripheral.startAdvertising([
- CBAdvertisementDataLocalNameKey: "StrideTrack Sensor",
- CBAdvertisementDataServiceUUIDsKey: [CBUUID(string: "180D")]
- ])
- }
-
- func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: (any Error)?) {
- if let error = error {
- print("Error advertising: \(error)")
- return
- }
- print("Advertising — waiting for subscription...")
- }
-
- func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
- print(">>> SUBSCRIBED: \(central.identifier)")
- }
-
- func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
- print(">>> UNSUBSCRIBED: \(central.identifier)")
- }
-
- func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) {
- print(">>> READ REQUEST: \(request.characteristic.uuid)")
- }
-}
-
-let delegate = Delegate()
-let manager = CBPeripheralManager(delegate: delegate, queue: nil)
-
-print("Running... (Ctrl+C to stop)")
-RunLoop.current.run()