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": {
"active_layer": 0,
"active_layer_preset": "All Layers",
"active_layer_preset": "",
"auto_track_width": true,
"hidden_netclasses": [],
"hidden_nets": [],
@ -53,7 +53,7 @@
"board_outline_area",
"ly_points"
],
"visible_layers": "ffffffff_ffffffff_ffffffff_ffffffff",
"visible_layers": "00000000_00000000_00000000_00000005",
"zone_display_mode": 0
},
"git": {

@ -72,20 +72,16 @@
"diff_pair_dimensions": [],
"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"
],
[
"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"
],
[
"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"
],
[
"silk_overlap|141660000|112580000|2cc6141b-7cd1-460c-a770-d3699aad6c47|3ef1bc1b-f726-479c-8480-140a894c0811",
"dont care"
]
],
"meta": {
@ -167,7 +163,7 @@
"min_microvia_drill": 0.1,
"min_resolved_spokes": 1,
"min_silk_clearance": 0.0,
"min_text_height": 0.8,
"min_text_height": 0.5,
"min_text_thickness": 0.08,
"min_through_hole_diameter": 0.2,
"min_track_width": 0.1,

@ -7162,6 +7162,12 @@
(embedded_fonts no)
)
)
(junction
(at 135.89 153.67)
(diameter 0)
(color 0 0 0 0)
(uuid "141c20c8-c1ee-4e54-b15e-697a300ec7ff")
)
(junction
(at 177.8 142.24)
(diameter 0)
@ -7180,12 +7186,6 @@
(color 0 0 0 0)
(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
(at 167.64 156.21)
(diameter 0)
@ -7204,12 +7204,6 @@
(color 0 0 0 0)
(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
(at 241.3 113.03)
(diameter 0)
@ -7222,18 +7216,6 @@
(color 0 0 0 0)
(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
(at 147.32 135.89)
(diameter 0)
@ -7246,6 +7228,12 @@
(color 0 0 0 0)
(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
(at 224.79 90.17)
(diameter 0)
@ -7264,6 +7252,12 @@
(color 0 0 0 0)
(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
(at 129.54 152.4)
(diameter 0)
@ -7307,10 +7301,10 @@
(uuid "a9cd98f1-9f54-41c4-894e-c88e8ab740c3")
)
(junction
(at 134.62 153.67)
(at 143.51 153.67)
(diameter 0)
(color 0 0 0 0)
(uuid "acd837b6-549f-44fe-882b-3a75c4647f9d")
(uuid "b556ae62-5fd0-463b-861f-d3c7f2ffea3b")
)
(junction
(at -53.34 124.46)
@ -7330,6 +7324,12 @@
(color 0 0 0 0)
(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
(at 91.44 162.56)
(diameter 0)
@ -7363,24 +7363,16 @@
(uuid "123b5ab3-d175-47cf-afc5-65abdc87935d")
)
(no_connect
(at 90.17 81.28)
(uuid "2d05179e-1d49-46d3-91cf-e98d990bcc88")
)
(no_connect
(at 140.97 81.28)
(uuid "3244eb8c-4267-44c9-87bc-597418d0ef20")
(at 140.97 111.76)
(uuid "296c05cb-2968-486d-90ae-9e25e432cce5")
)
(no_connect
(at 140.97 99.06)
(uuid "4b07ca14-149f-499b-8f59-5bbdf08ff777")
)
(no_connect
(at 140.97 78.74)
(uuid "4f0c4ccc-fcb3-454b-be4e-870597b96911")
(at 90.17 81.28)
(uuid "2d05179e-1d49-46d3-91cf-e98d990bcc88")
)
(no_connect
(at 140.97 63.5)
(uuid "7118add9-3a4c-446d-9571-a83f3a3a4e5d")
(at 140.97 104.14)
(uuid "4ffd468e-34f6-4523-83ca-abb86de5ffb1")
)
(no_connect
(at 90.17 78.74)
@ -7398,6 +7390,10 @@
(at 90.17 76.2)
(uuid "895e7c3b-b76c-4e3e-891e-d858e9fdecdb")
)
(no_connect
(at 140.97 96.52)
(uuid "9371ed91-2fa9-44f7-82e9-8e83bd43adf0")
)
(no_connect
(at 119.38 170.18)
(uuid "a749c33b-5a17-4525-8b9b-d217166ebfc6")
@ -7414,6 +7410,10 @@
(at 110.49 91.44)
(uuid "da21b2ae-600b-4056-88ba-6f6f8e02cddb")
)
(no_connect
(at 140.97 106.68)
(uuid "dc9a2174-bb71-40bd-94b8-e13d573094f7")
)
(no_connect
(at 90.17 83.82)
(uuid "ef967d6a-32bf-41d8-9e22-bf8921f6feff")
@ -7512,6 +7512,16 @@
)
(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
(pts
(xy 21.59 17.78) (xy 24.13 17.78)
@ -7664,7 +7674,7 @@
)
(wire
(pts
(xy 104.14 152.4) (xy 104.14 162.56)
(xy 101.6 152.4) (xy 101.6 162.56)
)
(stroke
(width 0)
@ -7674,7 +7684,17 @@
)
(wire
(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
(width 0)
@ -7712,6 +7732,16 @@
)
(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
(pts
(xy 241.3 113.03) (xy 245.11 113.03)
@ -7754,7 +7784,7 @@
)
(wire
(pts
(xy 134.62 146.05) (xy 130.81 146.05)
(xy 134.62 146.05) (xy 133.35 146.05)
)
(stroke
(width 0)
@ -7932,6 +7962,16 @@
)
(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
(pts
(xy 143.51 142.24) (xy 162.56 142.24)
@ -7974,7 +8014,7 @@
)
(wire
(pts
(xy 134.62 153.67) (xy 129.54 153.67)
(xy 130.81 153.67) (xy 135.89 153.67)
)
(stroke
(width 0)
@ -8122,6 +8162,16 @@
)
(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
(pts
(xy 173.99 110.49) (xy 173.99 111.76)
@ -8262,6 +8312,16 @@
)
(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
(pts
(xy 90.17 71.12) (xy 92.71 71.12)
@ -8414,17 +8474,7 @@
)
(wire
(pts
(xy 134.62 153.67) (xy 143.51 153.67)
)
(stroke
(width 0)
(type default)
)
(uuid "d87d4f3a-d608-4815-b9b5-3474a894e3ac")
)
(wire
(pts
(xy 129.54 153.67) (xy 129.54 167.64)
(xy 130.81 153.67) (xy 130.81 167.64)
)
(stroke
(width 0)
@ -8434,7 +8484,7 @@
)
(wire
(pts
(xy 130.81 146.05) (xy 129.54 146.05)
(xy 129.54 146.05) (xy 133.35 146.05)
)
(stroke
(width 0)
@ -8532,6 +8582,16 @@
)
(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
(pts
(xy 218.44 48.26) (xy 219.71 48.26)
@ -8572,6 +8632,26 @@
)
(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+"
(at 54.61 85.09 0)
(effects
@ -8582,6 +8662,136 @@
)
(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-"
(at 54.61 80.01 0)
(effects
@ -8592,6 +8802,26 @@
)
(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"
(shape input)
(at 140.97 88.9 0)
@ -8618,7 +8848,7 @@
)
(global_label "SW_FORWARD"
(shape input)
(at 140.97 111.76 0)
(at 140.97 99.06 0)
(fields_autoplaced yes)
(effects
(font
@ -8628,7 +8858,7 @@
)
(uuid "0dee8414-e2b9-46de-b0f1-f18955d322a5")
(property "Intersheetrefs" "${INTERSHEET_REFS}"
(at 156.3528 111.76 0)
(at 156.3528 99.06 0)
(hide yes)
(show_name no)
(do_not_autoplace no)
@ -8906,7 +9136,7 @@
)
(global_label "LED_G"
(shape input)
(at 140.97 71.12 0)
(at 140.97 81.28 0)
(fields_autoplaced yes)
(effects
(font
@ -8916,7 +9146,7 @@
)
(uuid "3df55247-016a-48cd-ba6d-9915d63c88c7")
(property "Intersheetrefs" "${INTERSHEET_REFS}"
(at 149.6399 71.12 0)
(at 149.6399 81.28 0)
(hide yes)
(show_name no)
(do_not_autoplace no)
@ -8954,7 +9184,7 @@
)
(global_label "LED_B"
(shape input)
(at 140.97 58.42 0)
(at 140.97 63.5 0)
(fields_autoplaced yes)
(effects
(font
@ -8964,7 +9194,7 @@
)
(uuid "41f26c42-ae9c-4814-824f-12ac2d943d77")
(property "Intersheetrefs" "${INTERSHEET_REFS}"
(at 149.6399 58.42 0)
(at 149.6399 63.5 0)
(hide yes)
(show_name no)
(do_not_autoplace no)
@ -9026,7 +9256,7 @@
)
(global_label "SW_BACK"
(shape input)
(at 140.97 104.14 0)
(at 140.97 71.12 0)
(fields_autoplaced yes)
(effects
(font
@ -9036,7 +9266,7 @@
)
(uuid "50726c1e-d662-4d75-a295-e08a6339295c")
(property "Intersheetrefs" "${INTERSHEET_REFS}"
(at 152.4823 104.14 0)
(at 152.4823 71.12 0)
(hide yes)
(show_name no)
(do_not_autoplace no)
@ -9146,7 +9376,7 @@
)
(global_label "SW_LEFT"
(shape input)
(at 140.97 96.52 0)
(at 140.97 58.42 0)
(fields_autoplaced yes)
(effects
(font
@ -9156,7 +9386,7 @@
)
(uuid "6fc8de8b-dc46-4650-8ed3-c0f9d25701a3")
(property "Intersheetrefs" "${INTERSHEET_REFS}"
(at 151.817 96.52 0)
(at 151.817 58.42 0)
(hide yes)
(show_name no)
(do_not_autoplace no)
@ -9362,7 +9592,7 @@
)
(global_label "SW_RIGHT"
(shape input)
(at 140.97 106.68 0)
(at 140.97 76.2 0)
(fields_autoplaced yes)
(effects
(font
@ -9372,7 +9602,7 @@
)
(uuid "8cf8cd8d-831a-4922-821f-08d49c938430")
(property "Intersheetrefs" "${INTERSHEET_REFS}"
(at 153.0266 106.68 0)
(at 153.0266 76.2 0)
(hide yes)
(show_name no)
(do_not_autoplace no)
@ -9818,7 +10048,7 @@
)
(global_label "LED_R"
(shape input)
(at 140.97 76.2 0)
(at 140.97 78.74 0)
(fields_autoplaced yes)
(effects
(font
@ -9828,7 +10058,7 @@
)
(uuid "e2be692c-fea7-4b57-be7f-047ae289f695")
(property "Intersheetrefs" "${INTERSHEET_REFS}"
(at 149.6399 76.2 0)
(at 149.6399 78.74 0)
(hide yes)
(show_name no)
(do_not_autoplace no)
@ -10752,7 +10982,7 @@
)
(symbol
(lib_id "power:-BATT")
(at 143.51 153.67 180)
(at 143.51 158.75 180)
(unit 1)
(body_style 1)
(exclude_from_sim no)
@ -10763,7 +10993,7 @@
(fields_autoplaced yes)
(uuid "19a37a06-0355-4ccc-ad60-6d57a093f309")
(property "Reference" "#PWR01"
(at 143.51 149.86 0)
(at 143.51 154.94 0)
(hide yes)
(show_name no)
(do_not_autoplace no)
@ -10774,7 +11004,7 @@
)
)
(property "Value" "-BATT"
(at 143.51 158.75 0)
(at 143.51 163.83 0)
(show_name no)
(do_not_autoplace no)
(effects
@ -10784,7 +11014,7 @@
)
)
(property "Footprint" ""
(at 143.51 153.67 0)
(at 143.51 158.75 0)
(hide yes)
(show_name no)
(do_not_autoplace no)
@ -10795,7 +11025,7 @@
)
)
(property "Datasheet" ""
(at 143.51 153.67 0)
(at 143.51 158.75 0)
(hide yes)
(show_name no)
(do_not_autoplace no)
@ -10806,7 +11036,7 @@
)
)
(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)
(show_name no)
(do_not_autoplace no)
@ -11409,7 +11639,7 @@
(justify left)
)
)
(property "Value" "0.1uF"
(property "Value" "100n"
(at 160.02 160.0262 0)
(show_name no)
(do_not_autoplace no)
@ -11864,7 +12094,7 @@
)
(symbol
(lib_id "Device:R_Small")
(at 106.68 152.4 270)
(at 104.14 152.4 270)
(mirror x)
(unit 1)
(body_style 1)
@ -11876,7 +12106,7 @@
(fields_autoplaced yes)
(uuid "3521fb3f-2fc4-4cb7-9527-719e0f03b2df")
(property "Reference" "R4"
(at 106.68 147.32 90)
(at 104.14 147.32 90)
(show_name no)
(do_not_autoplace no)
(effects
@ -11886,7 +12116,7 @@
)
)
(property "Value" "1k"
(at 106.68 149.86 90)
(at 104.14 149.86 90)
(show_name no)
(do_not_autoplace no)
(effects
@ -11896,7 +12126,7 @@
)
)
(property "Footprint" "Resistor_SMD:R_0402_1005Metric"
(at 106.68 152.4 0)
(at 104.14 152.4 0)
(hide yes)
(show_name no)
(do_not_autoplace no)
@ -11907,7 +12137,7 @@
)
)
(property "Datasheet" ""
(at 106.68 152.4 0)
(at 104.14 152.4 0)
(hide yes)
(show_name no)
(do_not_autoplace no)
@ -11918,7 +12148,7 @@
)
)
(property "Description" "Resistor, small symbol"
(at 106.68 152.4 0)
(at 104.14 152.4 0)
(hide yes)
(show_name 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)
(hide yes)
(show_name no)
@ -13771,7 +14001,7 @@
(justify left)
)
)
(property "Value" "0.1uF"
(property "Value" "100n"
(at 132.08 151.1362 0)
(show_name no)
(do_not_autoplace no)
@ -15410,7 +15640,7 @@
)
(symbol
(lib_id "power:PWR_FLAG")
(at 134.62 153.67 180)
(at 135.89 153.67 180)
(unit 1)
(body_style 1)
(exclude_from_sim no)
@ -15421,7 +15651,7 @@
(fields_autoplaced yes)
(uuid "9dc5a694-53aa-4ed2-9c3b-854e4b0c2cc4")
(property "Reference" "#FLG05"
(at 134.62 155.575 0)
(at 135.89 155.575 0)
(hide yes)
(show_name no)
(do_not_autoplace no)
@ -15432,7 +15662,7 @@
)
)
(property "Value" "PWR_FLAG"
(at 134.62 158.75 0)
(at 135.89 158.75 0)
(show_name no)
(do_not_autoplace no)
(effects
@ -15442,7 +15672,7 @@
)
)
(property "Footprint" ""
(at 134.62 153.67 0)
(at 135.89 153.67 0)
(hide yes)
(show_name no)
(do_not_autoplace no)
@ -15453,7 +15683,7 @@
)
)
(property "Datasheet" ""
(at 134.62 153.67 0)
(at 135.89 153.67 0)
(hide yes)
(show_name no)
(do_not_autoplace no)
@ -15464,7 +15694,7 @@
)
)
(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)
(show_name no)
(do_not_autoplace no)
@ -15969,7 +16199,7 @@
)
(symbol
(lib_id "power:VBUS")
(at 77.47 96.52 180)
(at 77.47 99.06 90)
(unit 1)
(body_style 1)
(exclude_from_sim no)
@ -15977,10 +16207,9 @@
(on_board yes)
(in_pos_files yes)
(dnp no)
(fields_autoplaced yes)
(uuid "ae31c6e4-cf6b-4e36-8e9e-f6b757db0e90")
(property "Reference" "#PWR023"
(at 77.47 92.71 0)
(at 81.28 99.06 0)
(hide yes)
(show_name no)
(do_not_autoplace no)
@ -15991,7 +16220,7 @@
)
)
(property "Value" "VBUS"
(at 77.47 101.6 0)
(at 71.374 99.06 90)
(show_name no)
(do_not_autoplace no)
(effects
@ -16001,7 +16230,7 @@
)
)
(property "Footprint" ""
(at 77.47 96.52 0)
(at 77.47 99.06 0)
(hide yes)
(show_name no)
(do_not_autoplace no)
@ -16012,7 +16241,7 @@
)
)
(property "Datasheet" ""
(at 77.47 96.52 0)
(at 77.47 99.06 0)
(hide yes)
(show_name no)
(do_not_autoplace no)
@ -16023,7 +16252,7 @@
)
)
(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)
(show_name no)
(do_not_autoplace no)
@ -18220,7 +18449,7 @@
(justify right)
)
)
(property "Value" "0.1u"
(property "Value" "100n"
(at -41.91 64.7635 0)
(show_name no)
(do_not_autoplace no)
@ -18439,7 +18668,7 @@
)
(symbol
(lib_id "power:PWR_FLAG")
(at 130.81 146.05 0)
(at 133.35 146.05 0)
(unit 1)
(body_style 1)
(exclude_from_sim no)
@ -18450,7 +18679,7 @@
(fields_autoplaced yes)
(uuid "e8290c21-e02c-4052-898e-92d7b888e592")
(property "Reference" "#FLG04"
(at 130.81 144.145 0)
(at 133.35 144.145 0)
(hide yes)
(show_name no)
(do_not_autoplace no)
@ -18461,7 +18690,7 @@
)
)
(property "Value" "PWR_FLAG"
(at 130.81 140.97 0)
(at 133.35 140.97 0)
(show_name no)
(do_not_autoplace no)
(effects
@ -18471,7 +18700,7 @@
)
)
(property "Footprint" ""
(at 130.81 146.05 0)
(at 133.35 146.05 0)
(hide yes)
(show_name no)
(do_not_autoplace no)
@ -18482,7 +18711,7 @@
)
)
(property "Datasheet" ""
(at 130.81 146.05 0)
(at 133.35 146.05 0)
(hide yes)
(show_name no)
(do_not_autoplace no)
@ -18493,7 +18722,7 @@
)
)
(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)
(show_name 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
(version 7)
(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
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 PEER_DETECT_S (SCAN_CYCLE_S + 12)
#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 ───────────────────────────────────────────────────── */
#define LBUF 512
@ -54,6 +56,12 @@ static int g_serial_fd = -1;
static pthread_t g_serial_thr;
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)
{
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);
}
/* ── Serial reader thread ────────────────────────────────────────────────── */
/* ── Serial reader threads ────────────────────────────────────────────────── */
static void *serial_reader(void *arg)
{
(void)arg;
@ -163,6 +171,34 @@ static void *serial_reader(void *arg)
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)
{
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");
}
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 ──────────────────────────────────────────────── */
#define MAX_CHILDREN 32
static pid_t g_children[MAX_CHILDREN];
@ -436,11 +490,26 @@ static int g_pass = 0, g_fail = 0;
kill_all_children(); \
fprintf(stderr, " %-55s", #fn); \
lq_drain(&g_q); \
if (g_serial_fd2 >= 0) lq_drain(&g_q2); \
int _r = fn(); \
if (_r == 0) { g_pass++; fputs(" PASS\n", stderr); } \
else { g_fail++; fputs(" FAIL\n", stderr); } \
} 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 ───────────────────────────────────────────────────────────────── */
/* Device must emit DBG STATE at boot with clean starting values. */
@ -818,16 +887,135 @@ static int t_life_cycle_visual(void)
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 ─────────────────────────────────────────────────────────── */
int main(int argc, char *argv[])
{
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--port") && i + 1 < argc)
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)
g_sim_bin = argv[++i];
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;
}
}
@ -852,11 +1040,23 @@ int main(int argc, char *argv[])
}
lq_init(&g_q);
lq_init(&g_q2);
lq_init(&g_sim_q);
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, "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 ────────── */
fprintf(stderr, " setup: CLEARNVS + SET ble=1 ...");
@ -888,6 +1088,34 @@ int main(int argc, char *argv[])
lq_drain(&g_q);
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 ────────────────────────────────────────────── */
RUN(t_startup_state);
RUN(t_adv_tx_fields);
@ -924,10 +1152,26 @@ int main(int argc, char *argv[])
/* ── Visual life-cycle (operator can watch OLED + LED) ─────────────────── */
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);
g_serial_stop = 1;
pthread_join(g_serial_thr, NULL);
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;
}

@ -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;
}