main
noah metz 2026-06-10 17:55:57 -06:00
parent 2734d01c89
commit 9d2c54a3a0
9 changed files with 10175 additions and 10604 deletions

File diff suppressed because it is too large Load Diff

@ -1,7 +1,7 @@
{ {
"board": { "board": {
"active_layer": 0, "active_layer": 0,
"active_layer_preset": "All Layers", "active_layer_preset": "",
"auto_track_width": true, "auto_track_width": true,
"hidden_netclasses": [], "hidden_netclasses": [],
"hidden_nets": [], "hidden_nets": [],
@ -53,7 +53,7 @@
"board_outline_area", "board_outline_area",
"ly_points" "ly_points"
], ],
"visible_layers": "ffffffff_ffffffff_ffffffff_ffffffff", "visible_layers": "00000000_00000000_00000000_00000005",
"zone_display_mode": 0 "zone_display_mode": 0
}, },
"git": { "git": {

@ -72,20 +72,16 @@
"diff_pair_dimensions": [], "diff_pair_dimensions": [],
"drc_exclusions": [ "drc_exclusions": [
[ [
"courtyards_overlap|159715000|118735000|257763a6-a28f-4593-aaa9-2e37189e4f27|673cc59d-648a-4c4f-a29f-9570d89fd83c", "courtyards_overlap|139465000|102315000|41b54fc4-8da1-40f5-accb-b253b89be436|d274ea6f-3e08-483d-8beb-c20430a43558",
"switch" "switch"
], ],
[ [
"courtyards_overlap|162985000|118735000|6adb571e-9354-43da-8d12-4ff77c380479|701a6a58-ca95-429e-90ea-2c6312a80224", "courtyards_overlap|139465000|95815000|257763a6-a28f-4593-aaa9-2e37189e4f27|673cc59d-648a-4c4f-a29f-9570d89fd83c",
"switch" "switch"
], ],
[ [
"courtyards_overlap|166177500|118735000|41b54fc4-8da1-40f5-accb-b253b89be436|d274ea6f-3e08-483d-8beb-c20430a43558", "courtyards_overlap|139465000|99075000|6adb571e-9354-43da-8d12-4ff77c380479|701a6a58-ca95-429e-90ea-2c6312a80224",
"switch" "switch"
],
[
"silk_overlap|141660000|112580000|2cc6141b-7cd1-460c-a770-d3699aad6c47|3ef1bc1b-f726-479c-8480-140a894c0811",
"dont care"
] ]
], ],
"meta": { "meta": {
@ -167,7 +163,7 @@
"min_microvia_drill": 0.1, "min_microvia_drill": 0.1,
"min_resolved_spokes": 1, "min_resolved_spokes": 1,
"min_silk_clearance": 0.0, "min_silk_clearance": 0.0,
"min_text_height": 0.8, "min_text_height": 0.5,
"min_text_thickness": 0.08, "min_text_thickness": 0.08,
"min_through_hole_diameter": 0.2, "min_through_hole_diameter": 0.2,
"min_track_width": 0.1, "min_track_width": 0.1,

@ -7162,6 +7162,12 @@
(embedded_fonts no) (embedded_fonts no)
) )
) )
(junction
(at 135.89 153.67)
(diameter 0)
(color 0 0 0 0)
(uuid "141c20c8-c1ee-4e54-b15e-697a300ec7ff")
)
(junction (junction
(at 177.8 142.24) (at 177.8 142.24)
(diameter 0) (diameter 0)
@ -7180,12 +7186,6 @@
(color 0 0 0 0) (color 0 0 0 0)
(uuid "2d8b3380-de7d-40ce-9a46-b5d2b5c2c4d8") (uuid "2d8b3380-de7d-40ce-9a46-b5d2b5c2c4d8")
) )
(junction
(at 104.14 162.56)
(diameter 0)
(color 0 0 0 0)
(uuid "3893c4d0-819c-4b6b-a028-eb7ad865a20a")
)
(junction (junction
(at 167.64 156.21) (at 167.64 156.21)
(diameter 0) (diameter 0)
@ -7204,12 +7204,6 @@
(color 0 0 0 0) (color 0 0 0 0)
(uuid "4399545d-63d2-4f0c-be59-85ed2795ab34") (uuid "4399545d-63d2-4f0c-be59-85ed2795ab34")
) )
(junction
(at 130.81 146.05)
(diameter 0)
(color 0 0 0 0)
(uuid "4870541e-fdf3-44dc-99a6-adb0e3fc2b52")
)
(junction (junction
(at 241.3 113.03) (at 241.3 113.03)
(diameter 0) (diameter 0)
@ -7222,18 +7216,6 @@
(color 0 0 0 0) (color 0 0 0 0)
(uuid "535a0cdc-8a3f-4a90-b193-cce2df3a847f") (uuid "535a0cdc-8a3f-4a90-b193-cce2df3a847f")
) )
(junction
(at 129.54 153.67)
(diameter 0)
(color 0 0 0 0)
(uuid "587888a9-efdc-4616-8e7f-0dad25e04caf")
)
(junction
(at 143.51 153.67)
(diameter 0)
(color 0 0 0 0)
(uuid "5ada1970-0364-4665-bc5f-1ad9a2e6c1bb")
)
(junction (junction
(at 147.32 135.89) (at 147.32 135.89)
(diameter 0) (diameter 0)
@ -7246,6 +7228,12 @@
(color 0 0 0 0) (color 0 0 0 0)
(uuid "6f098570-37c6-4414-a226-1d4297e3ab0b") (uuid "6f098570-37c6-4414-a226-1d4297e3ab0b")
) )
(junction
(at 130.81 153.67)
(diameter 0)
(color 0 0 0 0)
(uuid "7c3e5f17-3bdf-4dde-8bb4-e0bd93571f82")
)
(junction (junction
(at 224.79 90.17) (at 224.79 90.17)
(diameter 0) (diameter 0)
@ -7264,6 +7252,12 @@
(color 0 0 0 0) (color 0 0 0 0)
(uuid "832f484f-490e-4166-9bdf-33cc113719ab") (uuid "832f484f-490e-4166-9bdf-33cc113719ab")
) )
(junction
(at 101.6 162.56)
(diameter 0)
(color 0 0 0 0)
(uuid "83dd94d4-2477-456d-a464-179ea7baaa59")
)
(junction (junction
(at 129.54 152.4) (at 129.54 152.4)
(diameter 0) (diameter 0)
@ -7307,10 +7301,10 @@
(uuid "a9cd98f1-9f54-41c4-894e-c88e8ab740c3") (uuid "a9cd98f1-9f54-41c4-894e-c88e8ab740c3")
) )
(junction (junction
(at 134.62 153.67) (at 143.51 153.67)
(diameter 0) (diameter 0)
(color 0 0 0 0) (color 0 0 0 0)
(uuid "acd837b6-549f-44fe-882b-3a75c4647f9d") (uuid "b556ae62-5fd0-463b-861f-d3c7f2ffea3b")
) )
(junction (junction
(at -53.34 124.46) (at -53.34 124.46)
@ -7330,6 +7324,12 @@
(color 0 0 0 0) (color 0 0 0 0)
(uuid "c1e63513-f46c-453f-b135-ef59ca2378fb") (uuid "c1e63513-f46c-453f-b135-ef59ca2378fb")
) )
(junction
(at 133.35 146.05)
(diameter 0)
(color 0 0 0 0)
(uuid "caf9af50-2a4a-4657-93f1-3a9b78bf43ba")
)
(junction (junction
(at 91.44 162.56) (at 91.44 162.56)
(diameter 0) (diameter 0)
@ -7363,24 +7363,16 @@
(uuid "123b5ab3-d175-47cf-afc5-65abdc87935d") (uuid "123b5ab3-d175-47cf-afc5-65abdc87935d")
) )
(no_connect (no_connect
(at 90.17 81.28) (at 140.97 111.76)
(uuid "2d05179e-1d49-46d3-91cf-e98d990bcc88") (uuid "296c05cb-2968-486d-90ae-9e25e432cce5")
)
(no_connect
(at 140.97 81.28)
(uuid "3244eb8c-4267-44c9-87bc-597418d0ef20")
) )
(no_connect (no_connect
(at 140.97 99.06) (at 90.17 81.28)
(uuid "4b07ca14-149f-499b-8f59-5bbdf08ff777") (uuid "2d05179e-1d49-46d3-91cf-e98d990bcc88")
)
(no_connect
(at 140.97 78.74)
(uuid "4f0c4ccc-fcb3-454b-be4e-870597b96911")
) )
(no_connect (no_connect
(at 140.97 63.5) (at 140.97 104.14)
(uuid "7118add9-3a4c-446d-9571-a83f3a3a4e5d") (uuid "4ffd468e-34f6-4523-83ca-abb86de5ffb1")
) )
(no_connect (no_connect
(at 90.17 78.74) (at 90.17 78.74)
@ -7398,6 +7390,10 @@
(at 90.17 76.2) (at 90.17 76.2)
(uuid "895e7c3b-b76c-4e3e-891e-d858e9fdecdb") (uuid "895e7c3b-b76c-4e3e-891e-d858e9fdecdb")
) )
(no_connect
(at 140.97 96.52)
(uuid "9371ed91-2fa9-44f7-82e9-8e83bd43adf0")
)
(no_connect (no_connect
(at 119.38 170.18) (at 119.38 170.18)
(uuid "a749c33b-5a17-4525-8b9b-d217166ebfc6") (uuid "a749c33b-5a17-4525-8b9b-d217166ebfc6")
@ -7414,6 +7410,10 @@
(at 110.49 91.44) (at 110.49 91.44)
(uuid "da21b2ae-600b-4056-88ba-6f6f8e02cddb") (uuid "da21b2ae-600b-4056-88ba-6f6f8e02cddb")
) )
(no_connect
(at 140.97 106.68)
(uuid "dc9a2174-bb71-40bd-94b8-e13d573094f7")
)
(no_connect (no_connect
(at 90.17 83.82) (at 90.17 83.82)
(uuid "ef967d6a-32bf-41d8-9e22-bf8921f6feff") (uuid "ef967d6a-32bf-41d8-9e22-bf8921f6feff")
@ -7512,6 +7512,16 @@
) )
(uuid "0f8cdce3-567f-4dc0-a185-40c20ccf2405") (uuid "0f8cdce3-567f-4dc0-a185-40c20ccf2405")
) )
(wire
(pts
(xy 101.6 162.56) (xy 104.14 162.56)
)
(stroke
(width 0)
(type default)
)
(uuid "152bc6fc-35b6-4b6f-865a-b09a26397266")
)
(wire (wire
(pts (pts
(xy 21.59 17.78) (xy 24.13 17.78) (xy 21.59 17.78) (xy 24.13 17.78)
@ -7664,7 +7674,7 @@
) )
(wire (wire
(pts (pts
(xy 104.14 152.4) (xy 104.14 162.56) (xy 101.6 152.4) (xy 101.6 162.56)
) )
(stroke (stroke
(width 0) (width 0)
@ -7674,7 +7684,17 @@
) )
(wire (wire
(pts (pts
(xy 99.06 162.56) (xy 104.14 162.56) (xy 135.89 153.67) (xy 143.51 153.67)
)
(stroke
(width 0)
(type default)
)
(uuid "2f78ea9a-edaf-47df-aa6e-a655dd7473fb")
)
(wire
(pts
(xy 99.06 162.56) (xy 101.6 162.56)
) )
(stroke (stroke
(width 0) (width 0)
@ -7712,6 +7732,16 @@
) )
(uuid "39db3354-35c9-4b81-b8dc-74f272c87053") (uuid "39db3354-35c9-4b81-b8dc-74f272c87053")
) )
(wire
(pts
(xy 106.68 152.4) (xy 109.22 152.4)
)
(stroke
(width 0)
(type default)
)
(uuid "3ae15295-329c-4c54-b623-73e3283cb927")
)
(wire (wire
(pts (pts
(xy 241.3 113.03) (xy 245.11 113.03) (xy 241.3 113.03) (xy 245.11 113.03)
@ -7754,7 +7784,7 @@
) )
(wire (wire
(pts (pts
(xy 134.62 146.05) (xy 130.81 146.05) (xy 134.62 146.05) (xy 133.35 146.05)
) )
(stroke (stroke
(width 0) (width 0)
@ -7932,6 +7962,16 @@
) )
(uuid "596977dd-af06-4a84-9371-8065e3a3e2ef") (uuid "596977dd-af06-4a84-9371-8065e3a3e2ef")
) )
(wire
(pts
(xy 77.47 99.06) (xy 77.47 96.52)
)
(stroke
(width 0)
(type default)
)
(uuid "5bf87ad9-d9a7-4acb-9c68-8d2aee6f8a03")
)
(wire (wire
(pts (pts
(xy 143.51 142.24) (xy 162.56 142.24) (xy 143.51 142.24) (xy 162.56 142.24)
@ -7974,7 +8014,7 @@
) )
(wire (wire
(pts (pts
(xy 134.62 153.67) (xy 129.54 153.67) (xy 130.81 153.67) (xy 135.89 153.67)
) )
(stroke (stroke
(width 0) (width 0)
@ -8122,6 +8162,16 @@
) )
(uuid "8c88bc8f-7498-4179-bb99-15d31d820e8a") (uuid "8c88bc8f-7498-4179-bb99-15d31d820e8a")
) )
(wire
(pts
(xy 143.51 158.75) (xy 143.51 153.67)
)
(stroke
(width 0)
(type default)
)
(uuid "8ffdfa2f-84a5-4ff7-b775-800789b09cc0")
)
(wire (wire
(pts (pts
(xy 173.99 110.49) (xy 173.99 111.76) (xy 173.99 110.49) (xy 173.99 111.76)
@ -8262,6 +8312,16 @@
) )
(uuid "a566fe63-02a3-4a0b-9aaa-23ab50dd6dc5") (uuid "a566fe63-02a3-4a0b-9aaa-23ab50dd6dc5")
) )
(wire
(pts
(xy 130.81 167.64) (xy 129.54 167.64)
)
(stroke
(width 0)
(type default)
)
(uuid "a9eb0195-a812-4a41-9ac6-6c08be6030d9")
)
(wire (wire
(pts (pts
(xy 90.17 71.12) (xy 92.71 71.12) (xy 90.17 71.12) (xy 92.71 71.12)
@ -8414,17 +8474,7 @@
) )
(wire (wire
(pts (pts
(xy 134.62 153.67) (xy 143.51 153.67) (xy 130.81 153.67) (xy 130.81 167.64)
)
(stroke
(width 0)
(type default)
)
(uuid "d87d4f3a-d608-4815-b9b5-3474a894e3ac")
)
(wire
(pts
(xy 129.54 153.67) (xy 129.54 167.64)
) )
(stroke (stroke
(width 0) (width 0)
@ -8434,7 +8484,7 @@
) )
(wire (wire
(pts (pts
(xy 130.81 146.05) (xy 129.54 146.05) (xy 129.54 146.05) (xy 133.35 146.05)
) )
(stroke (stroke
(width 0) (width 0)
@ -8532,6 +8582,16 @@
) )
(uuid "ef8e431e-f98f-43d5-8180-1d608838dd2b") (uuid "ef8e431e-f98f-43d5-8180-1d608838dd2b")
) )
(wire
(pts
(xy 130.81 153.67) (xy 129.54 153.67)
)
(stroke
(width 0)
(type default)
)
(uuid "f03ffd06-0f71-482d-b91d-7d89188c4586")
)
(wire (wire
(pts (pts
(xy 218.44 48.26) (xy 219.71 48.26) (xy 218.44 48.26) (xy 219.71 48.26)
@ -8572,6 +8632,26 @@
) )
(uuid "fdc47c48-9d28-4b43-b132-83f05feec48e") (uuid "fdc47c48-9d28-4b43-b132-83f05feec48e")
) )
(label "USB_CC1"
(at 54.61 72.39 0)
(effects
(font
(size 1.27 1.27)
)
(justify left bottom)
)
(uuid "05d5060b-3a0e-464f-98bd-570e931e0906")
)
(label "BATT_OC"
(at 116.84 160.02 180)
(effects
(font
(size 1.27 1.27)
)
(justify right bottom)
)
(uuid "082a48de-a06e-4b7a-ac6b-6ab65f86c3c3")
)
(label "USB_D+" (label "USB_D+"
(at 54.61 85.09 0) (at 54.61 85.09 0)
(effects (effects
@ -8582,6 +8662,136 @@
) )
(uuid "11833148-dee9-4bd1-a447-fe0521dc8724") (uuid "11833148-dee9-4bd1-a447-fe0521dc8724")
) )
(label "DW01A_VCC"
(at 130.81 146.05 90)
(effects
(font
(size 1.27 1.27)
)
(justify left bottom)
)
(uuid "1786b443-2994-4023-b533-f4665a9b916e")
)
(label "LED_G1"
(at 220.98 36.83 0)
(effects
(font
(size 1.27 1.27)
)
(justify left bottom)
)
(uuid "2faf74c5-bd30-487b-bc77-ef3b93f9fabf")
)
(label "LED_R2"
(at 245.11 49.53 0)
(effects
(font
(size 1.27 1.27)
)
(justify left bottom)
)
(uuid "5aadc68c-58dd-47d1-8601-f5201c328e03")
)
(label "DISP_PUMP_2"
(at -53.34 121.92 180)
(effects
(font
(size 1.27 1.27)
)
(justify right bottom)
)
(uuid "74cbe728-57d0-4f00-ae5b-71a6b2d626e4")
)
(label "BATT_OD"
(at 121.92 160.02 0)
(effects
(font
(size 1.27 1.27)
)
(justify left bottom)
)
(uuid "8f98ce0b-ffad-4d09-8c5d-789777429f05")
)
(label "DW01A_CS"
(at 107.95 152.4 90)
(effects
(font
(size 1.27 1.27)
)
(justify left bottom)
)
(uuid "92799202-0d7d-49fe-833f-354f2792e1db")
)
(label "LED_B2"
(at 245.11 34.29 0)
(effects
(font
(size 1.27 1.27)
)
(justify left bottom)
)
(uuid "9be8551b-3bc0-41d4-809e-9f45329a7dcb")
)
(label "LED_R1"
(at 220.98 49.53 0)
(effects
(font
(size 1.27 1.27)
)
(justify left bottom)
)
(uuid "a05c5156-3350-4f8b-b1a0-efd64d2da431")
)
(label "LED_G2"
(at 245.11 41.91 0)
(effects
(font
(size 1.27 1.27)
)
(justify left bottom)
)
(uuid "a3fae242-d595-4331-86d9-382279484ec4")
)
(label "DISP_IREF"
(at -54.61 109.22 0)
(effects
(font
(size 1.27 1.27)
)
(justify left bottom)
)
(uuid "a437efe1-4f0d-4e10-99ec-bc0611f2d363")
)
(label "CH340C_V3"
(at 80.01 99.06 270)
(effects
(font
(size 1.27 1.27)
)
(justify right bottom)
)
(uuid "be543f87-02fd-41cb-8a48-45b30d5c110e")
)
(label "LED_B1"
(at 220.98 22.86 0)
(effects
(font
(size 1.27 1.27)
)
(justify left bottom)
)
(uuid "be7e8d9e-b245-4e8a-a4b3-7d10541766ea")
)
(label "USB_CC2"
(at 54.61 74.93 0)
(effects
(font
(size 1.27 1.27)
)
(justify left bottom)
)
(uuid "c34d876f-c953-4a4d-8ba1-e9d6087ea41f")
)
(label "USB_D-" (label "USB_D-"
(at 54.61 80.01 0) (at 54.61 80.01 0)
(effects (effects
@ -8592,6 +8802,26 @@
) )
(uuid "d1fb1bfd-ca83-4d58-92ab-86d35c1ae372") (uuid "d1fb1bfd-ca83-4d58-92ab-86d35c1ae372")
) )
(label "DISP_PUMP_1"
(at -48.26 121.92 0)
(effects
(font
(size 1.27 1.27)
)
(justify left bottom)
)
(uuid "d72a72a9-4b22-4bcb-b44a-86bbae626f4c")
)
(label "TP4056_PROG"
(at 90.17 143.51 270)
(effects
(font
(size 1.27 1.27)
)
(justify right bottom)
)
(uuid "dbabb0e5-2f76-4fb1-a6b7-c62fc8f5c3ab")
)
(global_label "ESP_SDA" (global_label "ESP_SDA"
(shape input) (shape input)
(at 140.97 88.9 0) (at 140.97 88.9 0)
@ -8618,7 +8848,7 @@
) )
(global_label "SW_FORWARD" (global_label "SW_FORWARD"
(shape input) (shape input)
(at 140.97 111.76 0) (at 140.97 99.06 0)
(fields_autoplaced yes) (fields_autoplaced yes)
(effects (effects
(font (font
@ -8628,7 +8858,7 @@
) )
(uuid "0dee8414-e2b9-46de-b0f1-f18955d322a5") (uuid "0dee8414-e2b9-46de-b0f1-f18955d322a5")
(property "Intersheetrefs" "${INTERSHEET_REFS}" (property "Intersheetrefs" "${INTERSHEET_REFS}"
(at 156.3528 111.76 0) (at 156.3528 99.06 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -8906,7 +9136,7 @@
) )
(global_label "LED_G" (global_label "LED_G"
(shape input) (shape input)
(at 140.97 71.12 0) (at 140.97 81.28 0)
(fields_autoplaced yes) (fields_autoplaced yes)
(effects (effects
(font (font
@ -8916,7 +9146,7 @@
) )
(uuid "3df55247-016a-48cd-ba6d-9915d63c88c7") (uuid "3df55247-016a-48cd-ba6d-9915d63c88c7")
(property "Intersheetrefs" "${INTERSHEET_REFS}" (property "Intersheetrefs" "${INTERSHEET_REFS}"
(at 149.6399 71.12 0) (at 149.6399 81.28 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -8954,7 +9184,7 @@
) )
(global_label "LED_B" (global_label "LED_B"
(shape input) (shape input)
(at 140.97 58.42 0) (at 140.97 63.5 0)
(fields_autoplaced yes) (fields_autoplaced yes)
(effects (effects
(font (font
@ -8964,7 +9194,7 @@
) )
(uuid "41f26c42-ae9c-4814-824f-12ac2d943d77") (uuid "41f26c42-ae9c-4814-824f-12ac2d943d77")
(property "Intersheetrefs" "${INTERSHEET_REFS}" (property "Intersheetrefs" "${INTERSHEET_REFS}"
(at 149.6399 58.42 0) (at 149.6399 63.5 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -9026,7 +9256,7 @@
) )
(global_label "SW_BACK" (global_label "SW_BACK"
(shape input) (shape input)
(at 140.97 104.14 0) (at 140.97 71.12 0)
(fields_autoplaced yes) (fields_autoplaced yes)
(effects (effects
(font (font
@ -9036,7 +9266,7 @@
) )
(uuid "50726c1e-d662-4d75-a295-e08a6339295c") (uuid "50726c1e-d662-4d75-a295-e08a6339295c")
(property "Intersheetrefs" "${INTERSHEET_REFS}" (property "Intersheetrefs" "${INTERSHEET_REFS}"
(at 152.4823 104.14 0) (at 152.4823 71.12 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -9146,7 +9376,7 @@
) )
(global_label "SW_LEFT" (global_label "SW_LEFT"
(shape input) (shape input)
(at 140.97 96.52 0) (at 140.97 58.42 0)
(fields_autoplaced yes) (fields_autoplaced yes)
(effects (effects
(font (font
@ -9156,7 +9386,7 @@
) )
(uuid "6fc8de8b-dc46-4650-8ed3-c0f9d25701a3") (uuid "6fc8de8b-dc46-4650-8ed3-c0f9d25701a3")
(property "Intersheetrefs" "${INTERSHEET_REFS}" (property "Intersheetrefs" "${INTERSHEET_REFS}"
(at 151.817 96.52 0) (at 151.817 58.42 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -9362,7 +9592,7 @@
) )
(global_label "SW_RIGHT" (global_label "SW_RIGHT"
(shape input) (shape input)
(at 140.97 106.68 0) (at 140.97 76.2 0)
(fields_autoplaced yes) (fields_autoplaced yes)
(effects (effects
(font (font
@ -9372,7 +9602,7 @@
) )
(uuid "8cf8cd8d-831a-4922-821f-08d49c938430") (uuid "8cf8cd8d-831a-4922-821f-08d49c938430")
(property "Intersheetrefs" "${INTERSHEET_REFS}" (property "Intersheetrefs" "${INTERSHEET_REFS}"
(at 153.0266 106.68 0) (at 153.0266 76.2 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -9818,7 +10048,7 @@
) )
(global_label "LED_R" (global_label "LED_R"
(shape input) (shape input)
(at 140.97 76.2 0) (at 140.97 78.74 0)
(fields_autoplaced yes) (fields_autoplaced yes)
(effects (effects
(font (font
@ -9828,7 +10058,7 @@
) )
(uuid "e2be692c-fea7-4b57-be7f-047ae289f695") (uuid "e2be692c-fea7-4b57-be7f-047ae289f695")
(property "Intersheetrefs" "${INTERSHEET_REFS}" (property "Intersheetrefs" "${INTERSHEET_REFS}"
(at 149.6399 76.2 0) (at 149.6399 78.74 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -10752,7 +10982,7 @@
) )
(symbol (symbol
(lib_id "power:-BATT") (lib_id "power:-BATT")
(at 143.51 153.67 180) (at 143.51 158.75 180)
(unit 1) (unit 1)
(body_style 1) (body_style 1)
(exclude_from_sim no) (exclude_from_sim no)
@ -10763,7 +10993,7 @@
(fields_autoplaced yes) (fields_autoplaced yes)
(uuid "19a37a06-0355-4ccc-ad60-6d57a093f309") (uuid "19a37a06-0355-4ccc-ad60-6d57a093f309")
(property "Reference" "#PWR01" (property "Reference" "#PWR01"
(at 143.51 149.86 0) (at 143.51 154.94 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -10774,7 +11004,7 @@
) )
) )
(property "Value" "-BATT" (property "Value" "-BATT"
(at 143.51 158.75 0) (at 143.51 163.83 0)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
(effects (effects
@ -10784,7 +11014,7 @@
) )
) )
(property "Footprint" "" (property "Footprint" ""
(at 143.51 153.67 0) (at 143.51 158.75 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -10795,7 +11025,7 @@
) )
) )
(property "Datasheet" "" (property "Datasheet" ""
(at 143.51 153.67 0) (at 143.51 158.75 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -10806,7 +11036,7 @@
) )
) )
(property "Description" "Power symbol creates a global label with name \"-BATT\"" (property "Description" "Power symbol creates a global label with name \"-BATT\""
(at 143.51 153.67 0) (at 143.51 158.75 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -11409,7 +11639,7 @@
(justify left) (justify left)
) )
) )
(property "Value" "0.1uF" (property "Value" "100n"
(at 160.02 160.0262 0) (at 160.02 160.0262 0)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -11864,7 +12094,7 @@
) )
(symbol (symbol
(lib_id "Device:R_Small") (lib_id "Device:R_Small")
(at 106.68 152.4 270) (at 104.14 152.4 270)
(mirror x) (mirror x)
(unit 1) (unit 1)
(body_style 1) (body_style 1)
@ -11876,7 +12106,7 @@
(fields_autoplaced yes) (fields_autoplaced yes)
(uuid "3521fb3f-2fc4-4cb7-9527-719e0f03b2df") (uuid "3521fb3f-2fc4-4cb7-9527-719e0f03b2df")
(property "Reference" "R4" (property "Reference" "R4"
(at 106.68 147.32 90) (at 104.14 147.32 90)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
(effects (effects
@ -11886,7 +12116,7 @@
) )
) )
(property "Value" "1k" (property "Value" "1k"
(at 106.68 149.86 90) (at 104.14 149.86 90)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
(effects (effects
@ -11896,7 +12126,7 @@
) )
) )
(property "Footprint" "Resistor_SMD:R_0402_1005Metric" (property "Footprint" "Resistor_SMD:R_0402_1005Metric"
(at 106.68 152.4 0) (at 104.14 152.4 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -11907,7 +12137,7 @@
) )
) )
(property "Datasheet" "" (property "Datasheet" ""
(at 106.68 152.4 0) (at 104.14 152.4 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -11918,7 +12148,7 @@
) )
) )
(property "Description" "Resistor, small symbol" (property "Description" "Resistor, small symbol"
(at 106.68 152.4 0) (at 104.14 152.4 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -13110,7 +13340,7 @@
) )
) )
) )
(property "Footprint" "Connector_FFC-FPC:TE_2-84982-4_2Rows-24Pins-P1.0mm_Vertical" (property "Footprint" "commeownder:F31G1A7H111024"
(at -67.31 87.63 0) (at -67.31 87.63 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
@ -13771,7 +14001,7 @@
(justify left) (justify left)
) )
) )
(property "Value" "0.1uF" (property "Value" "100n"
(at 132.08 151.1362 0) (at 132.08 151.1362 0)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -15410,7 +15640,7 @@
) )
(symbol (symbol
(lib_id "power:PWR_FLAG") (lib_id "power:PWR_FLAG")
(at 134.62 153.67 180) (at 135.89 153.67 180)
(unit 1) (unit 1)
(body_style 1) (body_style 1)
(exclude_from_sim no) (exclude_from_sim no)
@ -15421,7 +15651,7 @@
(fields_autoplaced yes) (fields_autoplaced yes)
(uuid "9dc5a694-53aa-4ed2-9c3b-854e4b0c2cc4") (uuid "9dc5a694-53aa-4ed2-9c3b-854e4b0c2cc4")
(property "Reference" "#FLG05" (property "Reference" "#FLG05"
(at 134.62 155.575 0) (at 135.89 155.575 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -15432,7 +15662,7 @@
) )
) )
(property "Value" "PWR_FLAG" (property "Value" "PWR_FLAG"
(at 134.62 158.75 0) (at 135.89 158.75 0)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
(effects (effects
@ -15442,7 +15672,7 @@
) )
) )
(property "Footprint" "" (property "Footprint" ""
(at 134.62 153.67 0) (at 135.89 153.67 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -15453,7 +15683,7 @@
) )
) )
(property "Datasheet" "" (property "Datasheet" ""
(at 134.62 153.67 0) (at 135.89 153.67 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -15464,7 +15694,7 @@
) )
) )
(property "Description" "Special symbol for telling ERC where power comes from" (property "Description" "Special symbol for telling ERC where power comes from"
(at 134.62 153.67 0) (at 135.89 153.67 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -15969,7 +16199,7 @@
) )
(symbol (symbol
(lib_id "power:VBUS") (lib_id "power:VBUS")
(at 77.47 96.52 180) (at 77.47 99.06 90)
(unit 1) (unit 1)
(body_style 1) (body_style 1)
(exclude_from_sim no) (exclude_from_sim no)
@ -15977,10 +16207,9 @@
(on_board yes) (on_board yes)
(in_pos_files yes) (in_pos_files yes)
(dnp no) (dnp no)
(fields_autoplaced yes)
(uuid "ae31c6e4-cf6b-4e36-8e9e-f6b757db0e90") (uuid "ae31c6e4-cf6b-4e36-8e9e-f6b757db0e90")
(property "Reference" "#PWR023" (property "Reference" "#PWR023"
(at 77.47 92.71 0) (at 81.28 99.06 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -15991,7 +16220,7 @@
) )
) )
(property "Value" "VBUS" (property "Value" "VBUS"
(at 77.47 101.6 0) (at 71.374 99.06 90)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
(effects (effects
@ -16001,7 +16230,7 @@
) )
) )
(property "Footprint" "" (property "Footprint" ""
(at 77.47 96.52 0) (at 77.47 99.06 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -16012,7 +16241,7 @@
) )
) )
(property "Datasheet" "" (property "Datasheet" ""
(at 77.47 96.52 0) (at 77.47 99.06 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -16023,7 +16252,7 @@
) )
) )
(property "Description" "Power symbol creates a global label with name \"VBUS\"" (property "Description" "Power symbol creates a global label with name \"VBUS\""
(at 77.47 96.52 0) (at 77.47 99.06 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -18220,7 +18449,7 @@
(justify right) (justify right)
) )
) )
(property "Value" "0.1u" (property "Value" "100n"
(at -41.91 64.7635 0) (at -41.91 64.7635 0)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -18439,7 +18668,7 @@
) )
(symbol (symbol
(lib_id "power:PWR_FLAG") (lib_id "power:PWR_FLAG")
(at 130.81 146.05 0) (at 133.35 146.05 0)
(unit 1) (unit 1)
(body_style 1) (body_style 1)
(exclude_from_sim no) (exclude_from_sim no)
@ -18450,7 +18679,7 @@
(fields_autoplaced yes) (fields_autoplaced yes)
(uuid "e8290c21-e02c-4052-898e-92d7b888e592") (uuid "e8290c21-e02c-4052-898e-92d7b888e592")
(property "Reference" "#FLG04" (property "Reference" "#FLG04"
(at 130.81 144.145 0) (at 133.35 144.145 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -18461,7 +18690,7 @@
) )
) )
(property "Value" "PWR_FLAG" (property "Value" "PWR_FLAG"
(at 130.81 140.97 0) (at 133.35 140.97 0)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
(effects (effects
@ -18471,7 +18700,7 @@
) )
) )
(property "Footprint" "" (property "Footprint" ""
(at 130.81 146.05 0) (at 133.35 146.05 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -18482,7 +18711,7 @@
) )
) )
(property "Datasheet" "" (property "Datasheet" ""
(at 130.81 146.05 0) (at 133.35 146.05 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)
@ -18493,7 +18722,7 @@
) )
) )
(property "Description" "Special symbol for telling ERC where power comes from" (property "Description" "Special symbol for telling ERC where power comes from"
(at 130.81 146.05 0) (at 133.35 146.05 0)
(hide yes) (hide yes)
(show_name no) (show_name no)
(do_not_autoplace no) (do_not_autoplace no)

@ -0,0 +1,61 @@
(module "F31G1A7H111024" (layer F.Cu)
(descr "F31G-1A7H1-11024-3")
(tags "Connector")
(attr smd)
(fp_text reference J** (at 0.000 -0) (layer F.SilkS)
(effects (font (size 1.27 1.27) (thickness 0.254)))
)
(fp_text user %R (at 0.000 -0) (layer F.Fab)
(effects (font (size 1.27 1.27) (thickness 0.254)))
)
(fp_text value "F31G1A7H111024" (at 0.000 -0) (layer F.SilkS) hide
(effects (font (size 1.27 1.27) (thickness 0.254)))
)
(fp_line (start -7.5 -1.25) (end 7.5 -1.25) (layer F.Fab) (width 0.1))
(fp_line (start 7.5 -1.25) (end 7.5 1.75) (layer F.Fab) (width 0.1))
(fp_line (start 7.5 1.75) (end -7.5 1.75) (layer F.Fab) (width 0.1))
(fp_line (start -7.5 1.75) (end -7.5 -1.25) (layer F.Fab) (width 0.1))
(fp_line (start -8.5 -2.95) (end 8.5 -2.95) (layer F.CrtYd) (width 0.1))
(fp_line (start 8.5 -2.95) (end 8.5 2.95) (layer F.CrtYd) (width 0.1))
(fp_line (start 8.5 2.95) (end -8.5 2.95) (layer F.CrtYd) (width 0.1))
(fp_line (start -8.5 2.95) (end -8.5 -2.95) (layer F.CrtYd) (width 0.1))
(fp_line (start -7.5 -1.25) (end -7.5 1.75) (layer F.SilkS) (width 0.2))
(fp_line (start 7.5 -1.25) (end 7.5 1.75) (layer F.SilkS) (width 0.2))
(fp_line (start 5.75 -2.4) (end 5.75 -2.4) (layer F.SilkS) (width 0.1))
(fp_line (start 5.75 -2.5) (end 5.75 -2.5) (layer F.SilkS) (width 0.1))
(fp_arc (start 5.75 -2.45) (end 5.750 -2.4) (angle -180) (layer F.SilkS) (width 0.1))
(fp_arc (start 5.75 -2.45) (end 5.750 -2.5) (angle -180) (layer F.SilkS) (width 0.1))
(pad 1 smd rect (at 5.750 -1.35 0) (size 0.400 1.200) (layers F.Cu F.Paste F.Mask))
(pad 2 smd rect (at 5.250 1.35 0) (size 0.400 1.200) (layers F.Cu F.Paste F.Mask))
(pad 3 smd rect (at 4.750 -1.35 0) (size 0.400 1.200) (layers F.Cu F.Paste F.Mask))
(pad 4 smd rect (at 4.250 1.35 0) (size 0.400 1.200) (layers F.Cu F.Paste F.Mask))
(pad 5 smd rect (at 3.750 -1.35 0) (size 0.400 1.200) (layers F.Cu F.Paste F.Mask))
(pad 6 smd rect (at 3.250 1.35 0) (size 0.400 1.200) (layers F.Cu F.Paste F.Mask))
(pad 7 smd rect (at 2.750 -1.35 0) (size 0.400 1.200) (layers F.Cu F.Paste F.Mask))
(pad 8 smd rect (at 2.250 1.35 0) (size 0.400 1.200) (layers F.Cu F.Paste F.Mask))
(pad 9 smd rect (at 1.750 -1.35 0) (size 0.400 1.200) (layers F.Cu F.Paste F.Mask))
(pad 10 smd rect (at 1.250 1.35 0) (size 0.400 1.200) (layers F.Cu F.Paste F.Mask))
(pad 11 smd rect (at 0.750 -1.35 0) (size 0.400 1.200) (layers F.Cu F.Paste F.Mask))
(pad 12 smd rect (at 0.250 1.35 0) (size 0.400 1.200) (layers F.Cu F.Paste F.Mask))
(pad 13 smd rect (at -0.250 -1.35 0) (size 0.400 1.200) (layers F.Cu F.Paste F.Mask))
(pad 14 smd rect (at -0.750 1.35 0) (size 0.400 1.200) (layers F.Cu F.Paste F.Mask))
(pad 15 smd rect (at -1.250 -1.35 0) (size 0.400 1.200) (layers F.Cu F.Paste F.Mask))
(pad 16 smd rect (at -1.750 1.35 0) (size 0.400 1.200) (layers F.Cu F.Paste F.Mask))
(pad 17 smd rect (at -2.250 -1.35 0) (size 0.400 1.200) (layers F.Cu F.Paste F.Mask))
(pad 18 smd rect (at -2.750 1.35 0) (size 0.400 1.200) (layers F.Cu F.Paste F.Mask))
(pad 19 smd rect (at -3.250 -1.35 0) (size 0.400 1.200) (layers F.Cu F.Paste F.Mask))
(pad 20 smd rect (at -3.750 1.35 0) (size 0.400 1.200) (layers F.Cu F.Paste F.Mask))
(pad 21 smd rect (at -4.250 -1.35 0) (size 0.400 1.200) (layers F.Cu F.Paste F.Mask))
(pad 22 smd rect (at -4.750 1.35 0) (size 0.400 1.200) (layers F.Cu F.Paste F.Mask))
(pad 23 smd rect (at -5.250 -1.35 0) (size 0.400 1.200) (layers F.Cu F.Paste F.Mask))
(pad 24 smd rect (at -5.750 1.35 0) (size 0.400 1.200) (layers F.Cu F.Paste F.Mask))
(pad MP1 smd rect (at 6.750 -1.35 0) (size 0.600 1.200) (layers F.Cu F.Paste F.Mask))
(pad MP2 smd rect (at 6.750 1.35 0) (size 0.600 1.200) (layers F.Cu F.Paste F.Mask))
(pad MP3 smd rect (at -6.750 -1.35 0) (size 0.600 1.200) (layers F.Cu F.Paste F.Mask))
(pad MP4 smd rect (at -6.750 1.35 0) (size 0.600 1.200) (layers F.Cu F.Paste F.Mask))
(model F31G-1A7H1-11024.stp
(at (xyz 0 0 0))
(scale (xyz 1 1 1))
(rotate (xyz 0 0 0))
)
)

@ -1,4 +1,5 @@
(fp_lib_table (fp_lib_table
(version 7) (version 7)
(lib (name "commeownder") (type "KiCad") (uri "${KIPRJMOD}/commeownder.pretty") (options "") (descr "Project-specific footprints")) (lib (name "commeownder") (type "KiCad") (uri "${KIPRJMOD}/commeownder.pretty") (options "") (descr "Project-specific footprints"))
(lib (name "pcb") (type "KiCad") (uri "/home/noah/Code/commeownder/pcb.pretty") (options "") (descr ""))
) )

@ -31,3 +31,11 @@ add_dependencies(test_ble simulator)
target_compile_definitions(test_ble PRIVATE target_compile_definitions(test_ble PRIVATE
SIM_BIN="$<TARGET_FILE:simulator>" SIM_BIN="$<TARGET_FILE:simulator>"
) )
# Long-running latency benchmark forks the simulator, needs pthreads + libm
add_executable(test_latency test_latency.c)
target_link_libraries(test_latency PRIVATE pthread m)
add_dependencies(test_latency simulator)
target_compile_definitions(test_latency PRIVATE
SIM_BIN="$<TARGET_FILE:simulator>"
)

@ -37,6 +37,8 @@ static const char *g_sim_bin = SIM_BIN;
#define SCAN_CYCLE_S 21 #define SCAN_CYCLE_S 21
#define PEER_DETECT_S (SCAN_CYCLE_S + 12) #define PEER_DETECT_S (SCAN_CYCLE_S + 12)
#define MAX_BLE_PEERS 4 #define MAX_BLE_PEERS 4
#define MAX_LATENCY_S 3 /* wait budget for propagation latency tests */
#define MAX_LATENCY_MS 2000 /* fail if measured latency exceeds this */
/* ── Serial line queue ───────────────────────────────────────────────────── */ /* ── Serial line queue ───────────────────────────────────────────────────── */
#define LBUF 512 #define LBUF 512
@ -54,6 +56,12 @@ static int g_serial_fd = -1;
static pthread_t g_serial_thr; static pthread_t g_serial_thr;
static volatile int g_serial_stop = 0; static volatile int g_serial_stop = 0;
/* ── Second device (optional, --port2) ───────────────────────────────────── */
static const char *g_port2 = NULL;
static int g_serial_fd2 = -1;
static lq_t g_q2;
static pthread_t g_serial_thr2;
static void lq_init(lq_t *q) static void lq_init(lq_t *q)
{ {
memset(q, 0, sizeof *q); memset(q, 0, sizeof *q);
@ -134,7 +142,7 @@ static int wait2(const char *p1, const char *p2, int secs, char *out)
return lq_wait(&g_q, pats, secs, out); return lq_wait(&g_q, pats, secs, out);
} }
/* ── Serial reader thread ────────────────────────────────────────────────── */ /* ── Serial reader threads ────────────────────────────────────────────────── */
static void *serial_reader(void *arg) static void *serial_reader(void *arg)
{ {
(void)arg; (void)arg;
@ -163,6 +171,34 @@ static void *serial_reader(void *arg)
return NULL; return NULL;
} }
static void *serial_reader2(void *arg)
{
(void)arg;
char line[LBUF];
int pos = 0;
while (!g_serial_stop) {
char c;
ssize_t n = read(g_serial_fd2, &c, 1);
if (n == 0) continue;
if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) { usleep(1000); continue; }
break;
}
if (c == '\n' || c == '\r') {
if (pos > 0) {
line[pos] = '\0';
if (strncmp(line, "DBG ", 4) == 0)
lq_push(&g_q2, line);
pos = 0;
}
} else if (pos < LBUF - 1) {
line[pos++] = c;
}
}
return NULL;
}
static int open_serial(const char *port) static int open_serial(const char *port)
{ {
int fd = open(port, O_RDWR | O_NOCTTY); int fd = open(port, O_RDWR | O_NOCTTY);
@ -187,6 +223,24 @@ static void serial_send(const char *cmd)
if (write(g_serial_fd, cmd, len) < 0) perror("serial_send"); if (write(g_serial_fd, cmd, len) < 0) perror("serial_send");
} }
static void serial_send_peer(const char *cmd)
{
size_t len = strlen(cmd);
if (write(g_serial_fd2, cmd, len) < 0) perror("serial_send_peer");
}
static int wait1_peer(const char *p, int secs, char *out)
{
const char *pats[] = {p, NULL};
return lq_wait(&g_q2, pats, secs, out);
}
static int wait2_peer(const char *p1, const char *p2, int secs, char *out)
{
const char *pats[] = {p1, p2, NULL};
return lq_wait(&g_q2, pats, secs, out);
}
/* ── Child process tracking ──────────────────────────────────────────────── */ /* ── Child process tracking ──────────────────────────────────────────────── */
#define MAX_CHILDREN 32 #define MAX_CHILDREN 32
static pid_t g_children[MAX_CHILDREN]; static pid_t g_children[MAX_CHILDREN];
@ -436,11 +490,26 @@ static int g_pass = 0, g_fail = 0;
kill_all_children(); \ kill_all_children(); \
fprintf(stderr, " %-55s", #fn); \ fprintf(stderr, " %-55s", #fn); \
lq_drain(&g_q); \ lq_drain(&g_q); \
if (g_serial_fd2 >= 0) lq_drain(&g_q2); \
int _r = fn(); \ int _r = fn(); \
if (_r == 0) { g_pass++; fputs(" PASS\n", stderr); } \ if (_r == 0) { g_pass++; fputs(" PASS\n", stderr); } \
else { g_fail++; fputs(" FAIL\n", stderr); } \ else { g_fail++; fputs(" FAIL\n", stderr); } \
} while (0) } while (0)
#define CHECK_SEEN_PEER(pat, secs, linebuf) \
do { \
if (!wait1_peer((pat), (secs), (linebuf))) { \
FAIL_MSG("timeout on peer device waiting for: %s", (pat)); return 1; \
} \
} while (0)
#define CHECK_SEEN2_PEER(p1, p2, secs, linebuf) \
do { \
if (!wait2_peer((p1), (p2), (secs), (linebuf))) { \
FAIL_MSG("timeout on peer device waiting for: %s + %s", (p1), (p2)); return 1; \
} \
} while (0)
/* ── Tests ───────────────────────────────────────────────────────────────── */ /* ── Tests ───────────────────────────────────────────────────────────────── */
/* Device must emit DBG STATE at boot with clean starting values. */ /* Device must emit DBG STATE at boot with clean starting values. */
@ -818,16 +887,135 @@ static int t_life_cycle_visual(void)
return 0; return 0;
} }
/* ── Multi-device tests (require --port2) ────────────────────────────────── */
/*
* The peer device must log PEER_RX carrying the primary device's life value.
* Uses a distinctive life value to unambiguously identify the primary's adv.
*/
static int t_peer_device_detects_primary(void)
{
serial_send("SET life=41\n");
CHECK_SEEN("DBG STATE", 5, NULL);
lq_drain(&g_q2);
CHECK_SEEN2_PEER("DBG PEER_RX", "life=41", PEER_DETECT_S, NULL);
serial_send("RESET\n");
wait1("DBG STATE", 5, NULL);
return 0;
}
/*
* The primary device must log PEER_RX carrying the peer device's life value.
*/
static int t_primary_device_detects_peer(void)
{
serial_send_peer("SET life=43\n");
CHECK_SEEN_PEER("DBG STATE", 5, NULL);
lq_drain(&g_q);
CHECK_SEEN2("DBG PEER_RX", "life=43", PEER_DETECT_S, NULL);
serial_send_peer("RESET\n");
wait1_peer("DBG STATE", 5, NULL);
return 0;
}
/*
* Measures the wall-clock latency from SET life on the primary device to the
* peer device logging PEER_RX with the new value. Must be < MAX_LATENCY_MS.
*/
static int t_life_update_latency(void)
{
lq_drain(&g_q2);
struct timespec t0, t1;
clock_gettime(CLOCK_MONOTONIC, &t0);
serial_send("SET life=37\n");
wait1("DBG STATE", 5, NULL);
int found = wait2_peer("DBG PEER_RX", "life=37", MAX_LATENCY_S, NULL);
clock_gettime(CLOCK_MONOTONIC, &t1);
serial_send("RESET\n");
wait1("DBG STATE", 5, NULL);
CHECK(found);
long ms = (t1.tv_sec - t0.tv_sec) * 1000L + (t1.tv_nsec - t0.tv_nsec) / 1000000L;
fprintf(stderr, "\n life update latency: %ld ms", ms);
CHECK(ms < MAX_LATENCY_MS);
return 0;
}
/*
* Same latency measurement for poison (counter0) updates.
*/
static int t_poison_update_latency(void)
{
lq_drain(&g_q2);
struct timespec t0, t1;
clock_gettime(CLOCK_MONOTONIC, &t0);
serial_send("SET counter0=7\n");
wait1("DBG STATE", 5, NULL);
int found = wait2_peer("DBG PEER_RX", "poison=7", MAX_LATENCY_S, NULL);
clock_gettime(CLOCK_MONOTONIC, &t1);
serial_send("RESET\n");
wait1("DBG STATE", 5, NULL);
CHECK(found);
long ms = (t1.tv_sec - t0.tv_sec) * 1000L + (t1.tv_nsec - t0.tv_nsec) / 1000000L;
fprintf(stderr, "\n poison update latency: %ld ms", ms);
CHECK(ms < MAX_LATENCY_MS);
return 0;
}
/*
* Three consecutive life changes on the primary device must each be received
* by the peer device within MAX_LATENCY_MS milliseconds.
*/
static int t_sequential_update_latency(void)
{
static const int vals[] = {31, 22, 13};
for (int i = 0; i < 3; i++) {
char cmd[32], pat[32];
snprintf(cmd, sizeof cmd, "SET life=%d\n", vals[i]);
snprintf(pat, sizeof pat, "life=%d", vals[i]);
lq_drain(&g_q2);
struct timespec t0, t1;
clock_gettime(CLOCK_MONOTONIC, &t0);
serial_send(cmd);
wait1("DBG STATE", 5, NULL);
int found = wait2_peer("DBG PEER_RX", pat, MAX_LATENCY_S, NULL);
clock_gettime(CLOCK_MONOTONIC, &t1);
long ms = (t1.tv_sec - t0.tv_sec) * 1000L + (t1.tv_nsec - t0.tv_nsec) / 1000000L;
fprintf(stderr, "\n update %d (life=%d) latency: %ld ms", i + 1, vals[i], ms);
if (!found) { FAIL_MSG("peer did not receive life=%d", vals[i]); return 1; }
if (ms >= MAX_LATENCY_MS) { FAIL_MSG("latency %ld ms >= %d ms", ms, MAX_LATENCY_MS); return 1; }
}
serial_send("RESET\n");
wait1("DBG STATE", 5, NULL);
return 0;
}
/* ── Entry point ─────────────────────────────────────────────────────────── */ /* ── Entry point ─────────────────────────────────────────────────────────── */
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
for (int i = 1; i < argc; i++) { for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--port") && i + 1 < argc) if (!strcmp(argv[i], "--port") && i + 1 < argc)
g_port = argv[++i]; g_port = argv[++i];
else if (!strcmp(argv[i], "--port2") && i + 1 < argc)
g_port2 = argv[++i];
else if (!strcmp(argv[i], "--sim") && i + 1 < argc) else if (!strcmp(argv[i], "--sim") && i + 1 < argc)
g_sim_bin = argv[++i]; g_sim_bin = argv[++i];
else { else {
fprintf(stderr, "usage: %s [--port DEV] [--sim PATH]\n", argv[0]); fprintf(stderr, "usage: %s [--port DEV] [--port2 DEV] [--sim PATH]\n", argv[0]);
return 1; return 1;
} }
} }
@ -852,11 +1040,23 @@ int main(int argc, char *argv[])
} }
lq_init(&g_q); lq_init(&g_q);
lq_init(&g_q2);
lq_init(&g_sim_q); lq_init(&g_sim_q);
pthread_create(&g_serial_thr, NULL, serial_reader, NULL); pthread_create(&g_serial_thr, NULL, serial_reader, NULL);
if (g_port2) {
g_serial_fd2 = open_serial(g_port2);
if (g_serial_fd2 < 0) {
fprintf(stderr, "cannot open port2: %s\n", g_port2);
return 1;
}
pthread_create(&g_serial_thr2, NULL, serial_reader2, NULL);
}
fprintf(stderr, "\ncommeownder BLE integration tests\n"); fprintf(stderr, "\ncommeownder BLE integration tests\n");
fprintf(stderr, "port: %s sim: %s\n\n", g_port, g_sim_bin); fprintf(stderr, "port: %s sim: %s", g_port, g_sim_bin);
if (g_port2) fprintf(stderr, " port2: %s", g_port2);
fprintf(stderr, "\n\n");
/* ── Device setup: wipe NVS, confirm clean state, enable BLE ────────── */ /* ── Device setup: wipe NVS, confirm clean state, enable BLE ────────── */
fprintf(stderr, " setup: CLEARNVS + SET ble=1 ..."); fprintf(stderr, " setup: CLEARNVS + SET ble=1 ...");
@ -888,6 +1088,34 @@ int main(int argc, char *argv[])
lq_drain(&g_q); lq_drain(&g_q);
fprintf(stderr, " OK\n\n"); fprintf(stderr, " OK\n\n");
if (g_port2) {
fprintf(stderr, " setup port2 (%s): CLEARNVS + SET ble=1 ...", g_port2);
{
int ready = 0;
for (int i = 0; i < 10 && !ready; i++) {
serial_send_peer("STATE\n");
if (wait1_peer("DBG STATE", 3, NULL)) ready = 1;
}
if (!ready) {
fprintf(stderr, " FAIL (peer device not responding)\n");
return 1;
}
lq_drain(&g_q2);
}
serial_send_peer("CLEARNVS\n");
if (!wait1_peer("DBG STATE", 10, NULL)) {
fprintf(stderr, " FAIL (no response to CLEARNVS)\n");
return 1;
}
serial_send_peer("SET ble=1\n");
if (!wait1_peer("DBG STATE", 5, NULL)) {
fprintf(stderr, " FAIL (no response to SET ble=1)\n");
return 1;
}
lq_drain(&g_q2);
fprintf(stderr, " OK\n\n");
}
/* ── Advertising & state ────────────────────────────────────────────── */ /* ── Advertising & state ────────────────────────────────────────────── */
RUN(t_startup_state); RUN(t_startup_state);
RUN(t_adv_tx_fields); RUN(t_adv_tx_fields);
@ -924,10 +1152,26 @@ int main(int argc, char *argv[])
/* ── Visual life-cycle (operator can watch OLED + LED) ─────────────────── */ /* ── Visual life-cycle (operator can watch OLED + LED) ─────────────────── */
RUN(t_life_cycle_visual); RUN(t_life_cycle_visual);
/* ── Multi-device tests (requires --port2) ──────────────────────────────── */
if (g_serial_fd2 >= 0) {
fprintf(stderr, "\nMulti-device tests (port=%s, port2=%s)\n\n", g_port, g_port2);
RUN(t_peer_device_detects_primary);
RUN(t_primary_device_detects_peer);
RUN(t_life_update_latency);
RUN(t_poison_update_latency);
RUN(t_sequential_update_latency);
} else {
fprintf(stderr, "\n [multi-device tests skipped: no --port2]\n");
}
fprintf(stderr, "\n%d passed, %d failed\n", g_pass, g_fail); fprintf(stderr, "\n%d passed, %d failed\n", g_pass, g_fail);
g_serial_stop = 1; g_serial_stop = 1;
pthread_join(g_serial_thr, NULL); pthread_join(g_serial_thr, NULL);
close(g_serial_fd); close(g_serial_fd);
if (g_serial_fd2 >= 0) {
pthread_join(g_serial_thr2, NULL);
close(g_serial_fd2);
}
return g_fail > 0 ? 1 : 0; return g_fail > 0 ? 1 : 0;
} }

@ -0,0 +1,485 @@
/* simulator/test_latency.c
*
* Long-running BLE latency benchmark for commeownder.
*
* Opens 13 physical device serial ports, spawns the BLE simulator as a
* background 4th peer, then measures advertisement propagation latency for
* every directed pair of physical devices. Reports per-pair, per-device-as-
* source, per-device-as-receiver, and overall statistics.
*
* Life values used for measurement are globally unique per iteration so that
* a PEER_RX on the destination can only have come from the source's fresh SET.
*
* Usage:
* ./test_latency --port1 /dev/ttyUSB0 --port2 /dev/ttyUSB1 --port3 /dev/ttyUSB2
* [--sim PATH] [--game-id ID] [--iterations N] [--timeout S]
*/
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <math.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>
#ifndef SIM_BIN
# define SIM_BIN "./simulator"
#endif
#define MAX_DEVS 3
#define LBUF 512
#define QSIZE 256
#define DEFAULT_ITERS 30
#define DEFAULT_TIMEOUT_S 5
#define DEFAULT_GAME_ID "4242"
#define SETTLE_S 3 /* settle time after setup before measuring */
#define LIFE_BASE 50 /* first life value used for measurement */
/* ── Line queue ─────────────────────────────────────────────────────────── */
typedef struct {
char data[QSIZE][LBUF];
int head, tail, count;
pthread_mutex_t mtx;
pthread_cond_t cond;
} lq_t;
static void lq_init(lq_t *q)
{
memset(q, 0, sizeof *q);
pthread_mutex_init(&q->mtx, NULL);
pthread_condattr_t attr;
pthread_condattr_init(&attr);
pthread_condattr_setclock(&attr, CLOCK_MONOTONIC);
pthread_cond_init(&q->cond, &attr);
pthread_condattr_destroy(&attr);
}
static void lq_push(lq_t *q, const char *line)
{
pthread_mutex_lock(&q->mtx);
if (q->count < QSIZE) {
strncpy(q->data[q->tail], line, LBUF - 1);
q->data[q->tail][LBUF - 1] = '\0';
q->tail = (q->tail + 1) % QSIZE;
q->count++;
pthread_cond_signal(&q->cond);
}
pthread_mutex_unlock(&q->mtx);
}
static void lq_drain(lq_t *q)
{
pthread_mutex_lock(&q->mtx);
q->head = q->tail = q->count = 0;
pthread_mutex_unlock(&q->mtx);
}
static int lq_wait(lq_t *q, const char **pats, int secs, char *out)
{
struct timespec dl;
clock_gettime(CLOCK_MONOTONIC, &dl);
dl.tv_sec += secs;
pthread_mutex_lock(&q->mtx);
for (;;) {
while (q->count > 0) {
char *line = q->data[q->head];
q->head = (q->head + 1) % QSIZE;
q->count--;
int ok = 1;
for (int i = 0; pats[i]; i++)
if (!strstr(line, pats[i])) { ok = 0; break; }
if (ok) {
if (out) strncpy(out, line, LBUF - 1);
pthread_mutex_unlock(&q->mtx);
return 1;
}
}
if (pthread_cond_timedwait(&q->cond, &q->mtx, &dl) == ETIMEDOUT) {
pthread_mutex_unlock(&q->mtx);
return 0;
}
}
}
/* ── Device ──────────────────────────────────────────────────────────────── */
typedef struct {
const char *port;
int fd;
lq_t q;
pthread_t thr;
} device_t;
static device_t g_devs[MAX_DEVS];
static int g_ndevs = 0;
static volatile int g_stop = 0;
static void *serial_reader(void *arg)
{
device_t *dev = (device_t *)arg;
char line[LBUF];
int pos = 0;
while (!g_stop) {
char c;
ssize_t n = read(dev->fd, &c, 1);
if (n == 0) continue;
if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) { usleep(1000); continue; }
break;
}
if (c == '\n' || c == '\r') {
if (pos > 0) {
line[pos] = '\0';
if (strncmp(line, "DBG ", 4) == 0)
lq_push(&dev->q, line);
pos = 0;
}
} else if (pos < LBUF - 1) {
line[pos++] = c;
}
}
return NULL;
}
static int open_serial(const char *port)
{
int fd = open(port, O_RDWR | O_NOCTTY);
if (fd < 0) { perror("open"); return -1; }
struct termios tty;
memset(&tty, 0, sizeof tty);
tty.c_cflag = CS8 | CREAD | CLOCAL;
cfsetispeed(&tty, B115200);
cfsetospeed(&tty, B115200);
tty.c_oflag = 0;
tty.c_lflag = 0;
tty.c_cc[VMIN] = 0;
tty.c_cc[VTIME] = 1;
if (tcsetattr(fd, TCSANOW, &tty) < 0) { perror("tcsetattr"); close(fd); return -1; }
return fd;
}
static void dev_send(device_t *dev, const char *cmd)
{
if (write(dev->fd, cmd, strlen(cmd)) < 0) perror("write");
}
/* Short label from port path, e.g. "/dev/ttyUSB1" → "USB1". */
static const char *port_label(const char *port)
{
const char *p = strstr(port, "tty");
return p ? p + 3 : port;
}
/* ── Simulator ───────────────────────────────────────────────────────────── */
static pid_t g_sim_pid = -1;
static const char *g_sim_bin = SIM_BIN;
static const char *g_game_id = DEFAULT_GAME_ID;
static pid_t sim_start(void)
{
pid_t pid = fork();
if (pid < 0) { perror("fork"); return -1; }
if (pid == 0) {
for (int i = 0; i < g_ndevs; i++) close(g_devs[i].fd);
const char *args[] = {
g_sim_bin,
"--name", "LATSIM",
"--life", "40",
"--poison", "0",
"--game-id", g_game_id,
NULL
};
execv(g_sim_bin, (char *const *)args);
_exit(1);
}
return pid;
}
static void sig_cleanup(int sig)
{
(void)sig;
if (g_sim_pid > 0) { kill(g_sim_pid, SIGTERM); waitpid(g_sim_pid, NULL, WNOHANG); }
_exit(1);
}
/* ── Statistics ──────────────────────────────────────────────────────────── */
typedef struct {
long min, max;
double sum, sum_sq;
int count, timeouts;
} stats_t;
static void stats_init(stats_t *s)
{
memset(s, 0, sizeof *s);
s->min = LONG_MAX;
s->max = 0;
}
static void stats_record(stats_t *s, long ms)
{
s->count++;
s->sum += ms;
s->sum_sq += (double)ms * ms;
if (ms < s->min) s->min = ms;
if (ms > s->max) s->max = ms;
}
static void stats_merge(stats_t *dst, const stats_t *src)
{
if (src->count == 0 && src->timeouts == 0) return;
dst->count += src->count;
dst->sum += src->sum;
dst->sum_sq += src->sum_sq;
dst->timeouts += src->timeouts;
if (src->count > 0) {
if (src->min < dst->min) dst->min = src->min;
if (src->max > dst->max) dst->max = src->max;
}
}
static void stats_print(const stats_t *s, const char *label)
{
printf(" %-28s", label);
if (s->count == 0) {
printf(" no samples");
} else {
double avg = s->sum / s->count;
double var = s->sum_sq / s->count - avg * avg;
double sd = var > 0.0 ? sqrt(var) : 0.0;
printf(" avg=%4.0fms min=%4ldms max=%4ldms sd=%3.0fms n=%d",
avg, s->min, s->max, sd, s->count);
}
if (s->timeouts) printf(" timeouts=%d", s->timeouts);
printf("\n");
}
/* ── Device setup ────────────────────────────────────────────────────────── */
static int setup_device(device_t *dev)
{
const char *ps[] = {"DBG STATE", NULL};
int ready = 0;
for (int i = 0; i < 10 && !ready; i++) {
dev_send(dev, "STATE\n");
if (lq_wait(&dev->q, ps, 3, NULL)) ready = 1;
}
if (!ready) return 0;
lq_drain(&dev->q);
dev_send(dev, "CLEARNVS\n");
if (!lq_wait(&dev->q, ps, 10, NULL)) return 0;
dev_send(dev, "SET ble=1\n");
if (!lq_wait(&dev->q, ps, 5, NULL)) return 0;
lq_drain(&dev->q);
return 1;
}
/* ── Measurement ─────────────────────────────────────────────────────────── */
static int g_iterations = DEFAULT_ITERS;
static int g_timeout_s = DEFAULT_TIMEOUT_S;
static int g_life_seq = 0; /* global counter for unique life values */
/*
* SET life=<val> on src, wait for dst to log PEER_RX carrying that value.
* Returns elapsed milliseconds, or -1 on timeout.
* The life value is chosen globally unique to avoid false matches from other
* devices that may be advertising stale values.
*/
static long measure_once(device_t *src, device_t *dst)
{
int val = LIFE_BASE + g_life_seq++;
char pat[32], cmd[32];
snprintf(pat, sizeof pat, "life=%d", val);
snprintf(cmd, sizeof cmd, "SET life=%d\n", val);
lq_drain(&dst->q);
struct timespec t0, t1;
clock_gettime(CLOCK_MONOTONIC, &t0);
dev_send(src, cmd);
const char *pats[] = {"DBG PEER_RX", pat, NULL};
int found = lq_wait(&dst->q, pats, g_timeout_s, NULL);
clock_gettime(CLOCK_MONOTONIC, &t1);
if (!found) return -1;
return (t1.tv_sec - t0.tv_sec) * 1000L + (t1.tv_nsec - t0.tv_nsec) / 1000000L;
}
/* ── Entry point ─────────────────────────────────────────────────────────── */
int main(int argc, char *argv[])
{
const char *ports[MAX_DEVS] = {NULL};
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--port1") && i + 1 < argc) ports[0] = argv[++i];
else if (!strcmp(argv[i], "--port2") && i + 1 < argc) ports[1] = argv[++i];
else if (!strcmp(argv[i], "--port3") && i + 1 < argc) ports[2] = argv[++i];
else if (!strcmp(argv[i], "--sim") && i + 1 < argc) g_sim_bin = argv[++i];
else if (!strcmp(argv[i], "--game-id") && i + 1 < argc) g_game_id = argv[++i];
else if (!strcmp(argv[i], "--iterations") && i + 1 < argc) g_iterations = atoi(argv[++i]);
else if (!strcmp(argv[i], "--timeout") && i + 1 < argc) g_timeout_s = atoi(argv[++i]);
else {
fprintf(stderr,
"usage: %s --port1 DEV [--port2 DEV] [--port3 DEV]\n"
" [--sim PATH] [--game-id ID]\n"
" [--iterations N] [--timeout S]\n", argv[0]);
return 1;
}
}
if (!ports[0]) {
fprintf(stderr, "error: --port1 is required\n");
return 1;
}
signal(SIGINT, sig_cleanup);
signal(SIGTERM, sig_cleanup);
/* ── Open devices ──────────────────────────────────────────────────── */
for (int i = 0; i < MAX_DEVS; i++) {
if (!ports[i]) break;
g_devs[g_ndevs].port = ports[i];
lq_init(&g_devs[g_ndevs].q);
g_devs[g_ndevs].fd = open_serial(ports[i]);
if (g_devs[g_ndevs].fd < 0) {
fprintf(stderr, "cannot open %s\n", ports[i]);
return 1;
}
pthread_create(&g_devs[g_ndevs].thr, NULL, serial_reader, &g_devs[g_ndevs]);
g_ndevs++;
}
if (g_ndevs < 2) {
fprintf(stderr, "error: need at least 2 devices (--port1 and --port2)\n");
return 1;
}
/* ── Print header ──────────────────────────────────────────────────── */
printf("\ncommeownder BLE latency benchmark\n");
printf("devices:");
for (int i = 0; i < g_ndevs; i++) printf(" %s", g_devs[i].port);
printf("\n");
printf("simulator: %s game-id: %s\n", g_sim_bin, g_game_id);
printf("iterations: %d per pair timeout: %ds\n\n", g_iterations, g_timeout_s);
/* ── Set up devices ────────────────────────────────────────────────── */
printf("Setting up devices:\n");
for (int i = 0; i < g_ndevs; i++) {
printf(" %s ... ", g_devs[i].port); fflush(stdout);
if (!setup_device(&g_devs[i])) {
printf("FAIL\n");
return 1;
}
printf("OK\n");
}
/* ── Start simulator ───────────────────────────────────────────────── */
printf("Starting simulator (LATSIM, life=40) ... ");
fflush(stdout);
g_sim_pid = sim_start();
if (g_sim_pid < 0) { printf("FAIL\n"); return 1; }
printf("OK (pid %d)\n", g_sim_pid);
/* ── Settle ────────────────────────────────────────────────────────── */
printf("Settling (%ds) ...\n\n", SETTLE_S);
sleep(SETTLE_S);
/* Drain all queues after settle so old PEER_RX lines don't skew results. */
for (int i = 0; i < g_ndevs; i++) lq_drain(&g_devs[i].q);
/* ── Run measurements ──────────────────────────────────────────────── */
int npairs = g_ndevs * (g_ndevs - 1);
stats_t pair_stats[MAX_DEVS][MAX_DEVS];
stats_t src_stats[MAX_DEVS];
stats_t dst_stats[MAX_DEVS];
stats_t overall;
for (int i = 0; i < MAX_DEVS; i++) {
for (int j = 0; j < MAX_DEVS; j++) stats_init(&pair_stats[i][j]);
stats_init(&src_stats[i]);
stats_init(&dst_stats[i]);
}
stats_init(&overall);
printf("Running measurements (%d pairs × %d iterations):\n", npairs, g_iterations);
for (int si = 0; si < g_ndevs; si++) {
for (int di = 0; di < g_ndevs; di++) {
if (si == di) continue;
printf(" %s → %s [", port_label(g_devs[si].port), port_label(g_devs[di].port));
fflush(stdout);
for (int iter = 0; iter < g_iterations; iter++) {
long ms = measure_once(&g_devs[si], &g_devs[di]);
if (ms < 0) {
pair_stats[si][di].timeouts++;
printf("T"); fflush(stdout);
} else {
stats_record(&pair_stats[si][di], ms);
printf("."); fflush(stdout);
}
}
printf("]\n");
stats_merge(&src_stats[si], &pair_stats[si][di]);
stats_merge(&dst_stats[di], &pair_stats[si][di]);
stats_merge(&overall, &pair_stats[si][di]);
}
}
/* ── Stop simulator ────────────────────────────────────────────────── */
if (g_sim_pid > 0) {
kill(g_sim_pid, SIGTERM);
waitpid(g_sim_pid, NULL, 0);
g_sim_pid = -1;
}
/* ── Report ────────────────────────────────────────────────────────── */
printf("\n────────────────────────────────────────────────────────────────\n");
printf("Pair statistics:\n");
for (int si = 0; si < g_ndevs; si++) {
for (int di = 0; di < g_ndevs; di++) {
if (si == di) continue;
char label[64];
snprintf(label, sizeof label, "%s → %s",
port_label(g_devs[si].port), port_label(g_devs[di].port));
stats_print(&pair_stats[si][di], label);
}
}
printf("\nPer-device as source:\n");
for (int i = 0; i < g_ndevs; i++) {
char label[64];
snprintf(label, sizeof label, "%s", port_label(g_devs[i].port));
stats_print(&src_stats[i], label);
}
printf("\nPer-device as receiver:\n");
for (int i = 0; i < g_ndevs; i++) {
char label[64];
snprintf(label, sizeof label, "%s", port_label(g_devs[i].port));
stats_print(&dst_stats[i], label);
}
printf("\nOverall (%d samples):\n", overall.count);
stats_print(&overall, "all pairs");
printf("────────────────────────────────────────────────────────────────\n\n");
g_stop = 1;
for (int i = 0; i < g_ndevs; i++) {
pthread_join(g_devs[i].thr, NULL);
close(g_devs[i].fd);
}
return (overall.count == 0) ? 1 : 0;
}