mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-04 11:33:47 +08:00
Compare commits
850 Commits
sgsp-local-app
...
Dom
| Author | SHA1 | Date | |
|---|---|---|---|
| 14534287c1 | |||
| 2929a25afe | |||
| 465db1bc61 | |||
| 89af8a6569 | |||
| dd4214f8f5 | |||
| 180f94dc35 | |||
| e997847db0 | |||
| 228534e0cf | |||
| 5e02c2387a | |||
| e6f1ad93ee | |||
| 5f2caf8249 | |||
| 7b16c71824 | |||
| e381bf6a44 | |||
| c7b81a5768 | |||
| 7920339796 | |||
| fc45eb6862 | |||
| b302c2ef8f | |||
| 4d804768ab | |||
| 9b7f4c6646 | |||
| 14205db69d | |||
| 4cf5c9e098 | |||
| bf4e27fca5 | |||
| d42ddc3ac5 | |||
| 55d6fb1778 | |||
| 91dad8b66d | |||
| 047294a1d0 | |||
| a336e65dd8 | |||
| dd9608eecd | |||
| 123e3c20a0 | |||
| 80975369ee | |||
| 7ec8cc721d | |||
| 1d2d18fa2a | |||
| 42a1244e1b | |||
| f148770788 | |||
| 755d82a7e6 | |||
| 7d7bf43fac | |||
| 63b05a9294 | |||
| 80f707aa3d | |||
| 6d99039382 | |||
| cfdcd14ca8 | |||
| 5be95f48df | |||
| fb9b3516b1 | |||
| 15b2612d7f | |||
| e99e552f21 | |||
| f0c5a666c5 | |||
| 3b8d201839 | |||
| 067f30dfa4 | |||
| 1d0525fe41 | |||
| 363d942ec9 | |||
| b21952a392 | |||
| 2678e29cb3 | |||
| 75a29d3e71 | |||
| e5a981b5ad | |||
| 54eab0df8d | |||
| 99a9025afe | |||
| 0783710c9a | |||
| 3d26454122 | |||
| 57900f3d73 | |||
| 2af7bdd1dd | |||
| 5dbcbb9618 | |||
| d5f6d6ee72 | |||
| cad0022d3e | |||
| dbb1fe6547 | |||
| 6854118aa2 | |||
| 8e0073f110 | |||
| fd660ef6f7 | |||
| 745b1e94de | |||
| 085aa2a173 | |||
| b8f3bcebcb | |||
| 29f498d77e | |||
| 31d5868e90 | |||
| 0f57b620ef | |||
| bd917d43d8 | |||
| be17536f2b | |||
| ef974ecc66 | |||
| 10d170e799 | |||
| 3924bd2e39 | |||
| 208dc09bca | |||
| ff9b397393 | |||
| de7c4c30ad | |||
| 507373cb7d | |||
| 58fc0bb4e7 | |||
| 112c775a2c | |||
| a5622e8933 | |||
| fb7592f834 | |||
| 5f960fba75 | |||
| 4a46f59825 | |||
| 6df20cbb76 | |||
| f5c84b4871 | |||
| 35f3058ea1 | |||
| 8defcc42b5 | |||
| b044e891e9 | |||
| 6bcdbd0e81 | |||
| 7d062147d3 | |||
| 1ecde54bb0 | |||
| f00fc85847 | |||
| 1650f86d22 | |||
| 788e655999 | |||
| 61bc147001 | |||
| 8e828c2c70 | |||
| 2e39fc2162 | |||
| a61a0873cc | |||
| e2b90a34a6 | |||
| 2e6845146b | |||
| 242e2ac0a0 | |||
| f34746452a | |||
| 97b5237e94 | |||
| 20da579f92 | |||
| 789b5365c0 | |||
| a931a94f79 | |||
| 9576fd0c02 | |||
| ccd9bfc4b8 | |||
| c802cb51a3 | |||
| 7125680cd3 | |||
| dea0ca1a91 | |||
| 4975f7c387 | |||
| 63beafc6af | |||
| aa397cad54 | |||
| 962d13da89 | |||
| 26b75e83fa | |||
| ba4d173709 | |||
| 1e91930094 | |||
| c7d78e5462 | |||
| 2645933f91 | |||
| 9640b6e049 | |||
| 63944ec1c3 | |||
| ccf112fb9c | |||
| fb72658c89 | |||
| bc57fa8eb8 | |||
| d2680ea3db | |||
| a0283d271b | |||
| 55e4f4289a | |||
| 70f3c8897d | |||
| cfefb493aa | |||
| 05d60a67ea | |||
| 9f15200211 | |||
| e07299dd9a | |||
| ba40d9fe5a | |||
| f2bac62f71 | |||
| f27f1f22c2 | |||
| d1f4a06744 | |||
| a4cf46600d | |||
| bfca6d7f3d | |||
| e094c3b624 | |||
| 4f4525e59c | |||
| 6140bd54a6 | |||
| 07606c8f9e | |||
| b409ef52cb | |||
| fa3138c929 | |||
| fa02d93a01 | |||
| aa3266824c | |||
| d7b5b8aef2 | |||
| ad67ae64b4 | |||
| 969ab9083c | |||
| 3fdd0d122b | |||
| 3c9a722083 | |||
| cbd6e1eb52 | |||
| f1e5ee7312 | |||
| 91c1fee59a | |||
| c08eb2fc0b | |||
| 295374ecec | |||
| c710aea5ce | |||
| e5931a7321 | |||
| 25247f09ba | |||
| 393a6139cf | |||
| 9ef9ef54b2 | |||
| 3705334acc | |||
| 63ef69c04b | |||
| 5bbfafa3e7 | |||
| ca5cbb342b | |||
| 8f59bfc56c | |||
| 7bcbee716a | |||
| 8ef661eb59 | |||
| 2ccc5fa9db | |||
| 97c6040a9f | |||
| 1c035f3e6d | |||
| 8ff493640d | |||
| ffc1aec50b | |||
| d55e0b01f6 | |||
| dc6c5868f4 | |||
| 4d8df1f754 | |||
| 1e5f5c6791 | |||
| 5808a49a52 | |||
| 77cf37119c | |||
| f7d3dfd037 | |||
| dea30ba5e2 | |||
| 69711eb73f | |||
| 50514ff9ec | |||
| 88eb1f00af | |||
| 993726afba | |||
| 30d4b9e23f | |||
| 15e63f04f2 | |||
| 281098b7ab | |||
| 2c9dced9a8 | |||
| 849c3715d9 | |||
| 8ef2ce2a0e | |||
| fd1eb1662f | |||
| 939b4889b2 | |||
| 7d6cf4e387 | |||
| 25830237de | |||
| b90a0d485c | |||
| 0b7df146f8 | |||
| 7f630d2358 | |||
| 4c49768b24 | |||
| e20a02d930 | |||
| 04441d6049 | |||
| ce9d9492c2 | |||
| 1376cf9d00 | |||
| 9c4be6a6c1 | |||
| 7c3bf67791 | |||
| f353afa400 | |||
| 9e2d7594eb | |||
| 5f741d9887 | |||
| 865948a00e | |||
| 5e92608ab7 | |||
| f429155f0f | |||
| 4e14f14461 | |||
| 2ee94f3643 | |||
| 9e4cba8695 | |||
| 7cb8ea919f | |||
| f2f8cfffa8 | |||
| 45c081b181 | |||
| 736580721a | |||
| 374a4134f6 | |||
| b16fc29ed5 | |||
| c1e2b5f181 | |||
| 2c7f252feb | |||
| e522e5c6a3 | |||
| 55f23c7fd5 | |||
| 61ad53c61f | |||
| 36e0e37086 | |||
| 407288852f | |||
| 0384be1253 | |||
| a13b6395d4 | |||
| e7cca0a136 | |||
| c0bbed7762 | |||
| 6df04fa8e0 | |||
| 40cd8ca264 | |||
| d9b60fddc1 | |||
| 153b95a88f | |||
| 5b6bdbe8ef | |||
| b70b7e16c4 | |||
| 195b7000aa | |||
| 1c7e21c43e | |||
| e61c7993af | |||
| b158bc378a | |||
| 72f16053b5 | |||
| 1fe202b904 | |||
| 33bc40d061 | |||
| 0102ad2a35 | |||
| 5b0f81caa1 | |||
| 77ff308c76 | |||
| cd585f55c2 | |||
| feb1f442c2 | |||
| a7f4ba8254 | |||
| 0f71f4669f | |||
| 2bc4f59d41 | |||
| e8256ee5b2 | |||
| 1738b3798d | |||
| d69ba777b5 | |||
| 0206e5dfc1 | |||
| ff15fc4fae | |||
| 83e6132b6d | |||
| 31b34665ce | |||
| 8df7979373 | |||
| 6f88e79025 | |||
| 23123aefb4 | |||
| ddd67e58bc | |||
| b6e25bfa6d | |||
| f10a7e229b | |||
| ac318aa31e | |||
| 15134f1ec2 | |||
| cb0c1507de | |||
| 3926f11699 | |||
| 8a760a3381 | |||
| 58c38a4834 | |||
| eb56047d57 | |||
| 67ad8ff05b | |||
| fdbce123c8 | |||
| 5235f58e07 | |||
| f26ab5e7f1 | |||
| 14acf1ed31 | |||
| 6d2e382e92 | |||
| fcb59fa1da | |||
| 8c404b3b8a | |||
| 3498e84770 | |||
| 72f04816c3 | |||
| 9068ec74f7 | |||
| 2df9c98963 | |||
| 70f523edc5 | |||
| c1f37c8314 | |||
| 15ed9799cb | |||
| f9dccd0c67 | |||
| 6a972ae87e | |||
| 5197fe4568 | |||
| 642d9d657d | |||
| cd0f46b9d6 | |||
| a6030fa25d | |||
| b8200e7bab | |||
| f9b678a7c3 | |||
| 68fca49aab | |||
| 731a9e6c79 | |||
| f506c8f299 | |||
| 6c38692ff7 | |||
| 5668bafc4c | |||
| 859145dd8a | |||
| 37341d2bca | |||
| 23a7b93df0 | |||
| 7bce4121e5 | |||
| 5c32d95d7b | |||
| a0d80165df | |||
| 67f4250d7e | |||
| ed3b1c4c73 | |||
| 49b651af70 | |||
| 40147f9f74 | |||
| bcb0abe576 | |||
| 7b36024f6d | |||
| c5ce014573 | |||
| 8da0c9d916 | |||
| 2fd0741f56 | |||
| c3d5939ffe | |||
| 7f8aa3dceb | |||
| a1fccf70b4 | |||
| aac227a103 | |||
| 0d8c81e294 | |||
| b9744f1cab | |||
| 70b705782c | |||
| efc8527820 | |||
| d421abeee1 | |||
| 7da9cea0f6 | |||
| 24b3d17c58 | |||
| 64eba05e62 | |||
| 87736cf206 | |||
| fb20651274 | |||
| 84bc481dd0 | |||
| e438a1a374 | |||
| f7c38a24a8 | |||
| d56266b63b | |||
| 6984f1da29 | |||
| ce6545467b | |||
| 1a89d5577a | |||
| 39298d97be | |||
| d84abc611d | |||
| 60b0f1e826 | |||
| 2487154a1e | |||
| 5bedae0794 | |||
| 1ab130e4a5 | |||
| 320e399f98 | |||
| 1d7e01f9f1 | |||
| 37bbd2711d | |||
| d513137550 | |||
| 0d32c47b56 | |||
| 6bc374c7d4 | |||
| 867ed373d6 | |||
| 0fd2353c55 | |||
| 21f675eae0 | |||
| f4dde57839 | |||
| b2604f4b8a | |||
| ebe7f28aae | |||
| bf0243f41b | |||
| da0d0d204b | |||
| e9e7481416 | |||
| 0fcf127704 | |||
| f7b85976a5 | |||
| 2b3e186822 | |||
| 8daabf71f9 | |||
| 1fcfefc977 | |||
| 01529d935d | |||
| ba0e051293 | |||
| bfca5476e6 | |||
| bfa9427dbd | |||
| 02588a19a2 | |||
| f4ec1c187e | |||
| bc494bd7d5 | |||
| 0b94e10fd9 | |||
| f09bf1fc28 | |||
| de00bb2f11 | |||
| 7acb954a6d | |||
| 5e057b9848 | |||
| 4fcf11f07a | |||
| 6120b36217 | |||
| 5c662d9e43 | |||
| b64d7cf9e9 | |||
| 18432adb3d | |||
| fae3f5ccfc | |||
| 8fdee1d44e | |||
| b3b96adecc | |||
| df9898673e | |||
| 66beca6de7 | |||
| 717a82245f | |||
| d955ebd7a2 | |||
| 2cca20624c | |||
| 7db42dd5d6 | |||
| eff809d839 | |||
| b3a4b964c2 | |||
| 0114eb04bb | |||
| 890c30c3cb | |||
| 7e00ee80c4 | |||
| e00fb66bc8 | |||
| 0d4422552b | |||
| c7f3de2416 | |||
| 4ea9adbdb9 | |||
| 9e7e4dde52 | |||
| a8bc9d1f24 | |||
| 63395a56f1 | |||
| 0a786d37e3 | |||
| 6fe76b728b | |||
| c65296f6df | |||
| 008687eb87 | |||
| dc7ff90070 | |||
| c6ef6b138a | |||
| 56c997a284 | |||
| f1f8f5696e | |||
| 16f31abd75 | |||
| 77babc4c85 | |||
| d1c2bc14a4 | |||
| 86315e76cb | |||
| 52347a3593 | |||
| 0933e58afc | |||
| cc125cf1eb | |||
| c073497d44 | |||
| b817c3c26e | |||
| 075d5c0ffe | |||
| 1f1b1d5db9 | |||
| 4dd24beabc | |||
| ad7bfa4899 | |||
| 5910ddb754 | |||
| 21a3921b16 | |||
| 33e87cea5a | |||
| e3fbbdddb9 | |||
| 76d33aaa81 | |||
| ff62737ac4 | |||
| 87b8b2ada5 | |||
| b43db836a5 | |||
| 1b9bb0cd51 | |||
| c3de3defc3 | |||
| 268370640d | |||
| 0d795930ff | |||
| ec5739441e | |||
| e94198f98e | |||
| 5f8638e894 | |||
| 98d2865a29 | |||
| 0e55c02b28 | |||
| 3d7df63099 | |||
| 47e4855580 | |||
| b33e4a071b | |||
| bc3dce2c0c | |||
| ed5e9e56f2 | |||
| 30625319b3 | |||
| 51e866a038 | |||
| 21c34c8f4d | |||
| 2800e5b452 | |||
| 85997f719f | |||
| 138da851ee | |||
| 556f90458e | |||
| e2fdc2a4dc | |||
| 312eb9e94d | |||
| 37fc53c48a | |||
| efbe7a776f | |||
| 7c5e70fd13 | |||
| 06a74465c0 | |||
| 42057a3f8f | |||
| d85372232f | |||
| a952a8f8a8 | |||
| 0826f3b22d | |||
| 58fb8e820b | |||
| 38e14008b9 | |||
| 4c015f6f49 | |||
| 256628e50b | |||
| fa25ea0567 | |||
| 99f52a7048 | |||
| eb2c90b80e | |||
| 53b82a1726 | |||
| 3b04ff3cc4 | |||
| 6e11c50dfc | |||
| 34ae63cbf6 | |||
| 1f79ca865f | |||
| 87cb8f9a63 | |||
| 21b9fd8136 | |||
| d317185c00 | |||
| f47452c4b1 | |||
| 6e124d1e45 | |||
| 50b4da94ba | |||
| 66bf42ac1e | |||
| 8aa35ed00f | |||
| 6639a95904 | |||
| 2b069cffc2 | |||
| ac97f5f447 | |||
| e7b53f3099 | |||
| 6418bfd5b7 | |||
| 6d87ba090c | |||
| 3fb884b5ba | |||
| aa99e3b6ca | |||
| ad397f89cc | |||
| 4e3775fa06 | |||
| 01416612a1 | |||
| 8fcd77c953 | |||
| 9e002886f9 | |||
| 1f9825e594 | |||
| 4a82e92c73 | |||
| 507e464c59 | |||
| b6b548fc9a | |||
| 434c89ce71 | |||
| 50781a9736 | |||
| 31b4abe4c6 | |||
| cdc97b58df | |||
| 4a3a989b3c | |||
| b75d9f8baf | |||
| b12ed577c0 | |||
| 2567a32f70 | |||
| 639866e6f7 | |||
| ccdddea522 | |||
| b5f1b8a8e3 | |||
| 14835e1b42 | |||
| f1bc3a8c5b | |||
| cbeaffba9a | |||
| 6537a58bd6 | |||
| 5a12f032ed | |||
| 094c9ae84e | |||
| 83d4bf30a8 | |||
| fdaaae86b0 | |||
| 8995ddb0df | |||
| 390438432e | |||
| 26ab9745af | |||
| d8ccb7fd68 | |||
| 03feefed84 | |||
| 1fa29f6c33 | |||
| 34dfc2e1b5 | |||
| 5309a0d6bc | |||
| 3c770dc283 | |||
| 265ca6c17d | |||
| 04b5dd0ad5 | |||
| cdef286dbf | |||
| 41692084c9 | |||
| dbba95d7fb | |||
| fee87a4576 | |||
| df8e234ff6 | |||
| ba2275363f | |||
| a4e552c7a3 | |||
| 65ac3bccf6 | |||
| 3e7a3dded8 | |||
| 62dd77d6bb | |||
| 7a7b6d2208 | |||
| b8aa3b9eca | |||
| b0e93e304f | |||
| 3baf4f7c82 | |||
| 621c8d31e3 | |||
| e095ab6b71 | |||
| e0fc923875 | |||
| 4848960966 | |||
| 2e70c76990 | |||
| b6bfd182dd | |||
| c0ed5f309a | |||
| f2294f575e | |||
| 0fcc393380 | |||
| 2af7ef756b | |||
| db651809df | |||
| 39392c37a6 | |||
| d594c6772a | |||
| 1fcd8b3353 | |||
| 7d9cb9b37b | |||
| c55ef78274 | |||
| e9b29f139c | |||
| 7d3ba7adf9 | |||
| d2199ddc20 | |||
| afd33370f6 | |||
| b0417bbf61 | |||
| 6617a1675d | |||
| 9f4fb7dbb4 | |||
| 316ed6b8b2 | |||
| dbd72abae6 | |||
| 9ade200473 | |||
| c1a00e3ef5 | |||
| c03f84cf61 | |||
| 5ce3f02b99 | |||
| 63243d7e2d | |||
| 7b6d9cb64b | |||
| d61cfcb04f | |||
| c3c7ee985a | |||
| 51af702b2c | |||
| 3621ccf9c2 | |||
| af48674fae | |||
| 08fa2bfde2 | |||
| 5b2e2dae0f | |||
| cc184af2b5 | |||
| 13a76dfb8e | |||
| 9736d887b3 | |||
| efe875ffd6 | |||
| e53ec2605f | |||
| 65660a4d5c | |||
| c64e7a8852 | |||
| f11f2357d8 | |||
| b57687cda8 | |||
| 65d02d27a1 | |||
| 2dfa4b6fe1 | |||
| d436c06f99 | |||
| 05d14fc86e | |||
| e4ad4faf9f | |||
| 5f0bda110a | |||
| a1d8204b25 | |||
| 3e0a3e3d7a | |||
| 4cbffe53b3 | |||
| 4b94f415ad | |||
| 487dfc4bf4 | |||
| baddf7a59d | |||
| a2160580a4 | |||
| 4b754298e7 | |||
| cc5b8b672c | |||
| 9e5bc45ff7 | |||
| 44dfd7493f | |||
| 0422970f2d | |||
| 3b67457f9a | |||
| 16d118c4a3 | |||
| b5807a459d | |||
| 43c5c1538a | |||
| 6294915b59 | |||
| 1454919bfe | |||
| c1df455977 | |||
| ac9cb41002 | |||
| c9734821f9 | |||
| e1e02b3949 | |||
| 6a029b3faa | |||
| a30733cf64 | |||
| ff161633bf | |||
| cf4301b16e | |||
| 4cc02a4a8f | |||
| 8f86a97616 | |||
| 6bad7eb908 | |||
| 6effaaa356 | |||
| 81f9630470 | |||
| ec56bb1f3e | |||
| 0ae41eacb6 | |||
| dd37cc81e9 | |||
| eec5cde2c5 | |||
| a6654bf867 | |||
| 42c0d6f24a | |||
| 860d2559c5 | |||
| 1184e06221 | |||
| 5fc5709e4d | |||
| f63c935029 | |||
| c8ae020038 | |||
| 887401fc58 | |||
| d60f2d7ada | |||
| 7a2a88c40c | |||
| 3f251eb5ee | |||
| 5f84b6c763 | |||
| 5b486edff4 | |||
| 7dc37f190f | |||
| 2571b8d5c4 | |||
| d0badbe715 | |||
| 0c5f83e690 | |||
| 7d81b58d52 | |||
| 8f6d643936 | |||
| d08cd1b08b | |||
| 50e60ef156 | |||
| cb16d27d3d | |||
| 6f3e156abd | |||
| 6221a89f4b | |||
| c79315f8d4 | |||
| 12dab8c31d | |||
| 03f9a9576e | |||
| 1320712ab4 | |||
| 3758148f83 | |||
| 6ebdb1b748 | |||
| 7d218d0a4b | |||
| bbbed56e54 | |||
| 34dc3be528 | |||
| 080d1d3a04 | |||
| 6103e0395b | |||
| be522b4ccf | |||
| 9c0cbcd5f4 | |||
| ed2abcc4b9 | |||
| eebd74e745 | |||
| 8e4710a4e6 | |||
| 0d01b3d818 | |||
| 1e90f2da72 | |||
| 6a07f58ea6 | |||
| 632fa39562 | |||
| 2e395aa714 | |||
| 8b4902209c | |||
| 0701c25b14 | |||
| f6b2c132fb | |||
| f36b39a68b | |||
| bf09b8eea4 | |||
| 6e430242e3 | |||
| 6c9ae11f63 | |||
| db94a2be1a | |||
| c417e793c5 | |||
| 718a2c3ddd | |||
| 76b5f7e512 | |||
| 5e5fa8d9c1 | |||
| 36111dfdb6 | |||
| 8ead1ab0f9 | |||
| 6ba879ca3b | |||
| a9af491104 | |||
| 42a918acc2 | |||
| c604521745 | |||
| 09d7e2873e | |||
| 5189f468d2 | |||
| 40a23df6be | |||
| 7a38780739 | |||
| 5c78ec40d7 | |||
| c6f00f7a4d | |||
| a119dfbae0 | |||
| fc6501364b | |||
| 1fa5818bb3 | |||
| ebfea6b3b7 | |||
| e672bd30c1 | |||
| 4c37c009b7 | |||
| ba87dd4c70 | |||
| a581b01d84 | |||
| 63888e0fd8 | |||
| 44d9c868b2 | |||
| e2a5d6ca66 | |||
| 8f3be8f6fb | |||
| b7fcbfee64 | |||
| a420ff55ed | |||
| 54b2f060e7 | |||
| 8822e26d5b | |||
| 1d38987d61 | |||
| f7fed1776d | |||
| 8d2872bed4 | |||
| 2141786f05 | |||
| 2f7d990597 | |||
| 49660054f9 | |||
| 4c07adab7d | |||
| 6b09da8676 | |||
| 8d9115f9e7 | |||
| 2c80f161a2 | |||
| 2635773393 | |||
| 7fea4f939d | |||
| 55a41e9026 | |||
| 06fefc6768 | |||
| dbc8e0ea39 | |||
| 0037bdd836 | |||
| 02ae25b975 | |||
| b976cd2e7c | |||
| 6c58d58a7a | |||
| 8098eb454c | |||
| 6e88336f3a | |||
| 76c2aba14e | |||
| 261e2ca45d | |||
| d96f6af078 | |||
| e1818affa2 | |||
| a71a67fd1f | |||
| 4d0246997b | |||
| d41159e65b | |||
| 306afa06a8 | |||
| ec25e2330a | |||
| 2551c9ce5f | |||
| cabc7da5b9 | |||
| df021f4e14 | |||
| be8cf95de8 | |||
| 7f5d30c30d | |||
| 41543cd593 | |||
| 0f00621c98 | |||
| 099cbd2809 | |||
| 0e5e5574c4 | |||
| 347397ad38 | |||
| 35f62c3c5a | |||
| 3da487cd19 | |||
| d914cf86ba | |||
| 6413fef0f9 | |||
| 87b42f9127 | |||
| 822dbe692f | |||
| eb2b6c2d23 | |||
| 0009df9084 | |||
| f3bbd38e81 | |||
| a3811ef5dd | |||
| 8807d791f1 | |||
| a168542200 | |||
| aa17fb6f0e | |||
| d59a37d37f | |||
| fff069c0e3 | |||
| f2b235bc7b | |||
| 318c440b2b | |||
| 567c190d5d | |||
| ef6354430a | |||
| da972263c3 | |||
| 1cd3c7f179 | |||
| 19d4b21f6b | |||
| 0ddf2cb8de | |||
| eb7afba097 | |||
| 27b552cbd8 | |||
| 13e1875d20 | |||
| 5ccbcc5752 | |||
| b5c90af1cf | |||
| 33fc8be564 | |||
| 2084622bec | |||
| d3193635a2 | |||
| 3b3960a5b8 | |||
| de97a6eff3 | |||
| 9fc77a256b | |||
| 447373d78b | |||
| 8a5fbf0be8 | |||
| 1a5ee718c9 | |||
| 5e83cb218f | |||
| fc5875831c | |||
| 24174ec6a6 | |||
| ef03c7ba36 | |||
| 59c6935f3c | |||
| 3c740d2050 | |||
| 3b67ac199e | |||
| 90916a5ff2 | |||
| 71632a6b48 | |||
| baae36fc3a | |||
| 0c842da32e | |||
| d6fe10f60b | |||
| faa61d48fc | |||
| d33f5b550c | |||
| e404020aca | |||
| 0421b31bb1 | |||
| 32360730ba | |||
| 4a84cf290d | |||
| 2248773fcc | |||
| 286641c1fe | |||
| 4b30638588 | |||
| 4cd78e7366 | |||
| d17623d930 | |||
| 9ca75ddefd | |||
| cafb6df8c2 | |||
| b6e7e63d87 | |||
| 82abd03443 | |||
| 5674f11442 | |||
| 49ab8cf392 | |||
| 202a52c353 | |||
| 766f4ceabb | |||
| baab9caf43 | |||
| 4c250a6a06 | |||
| 809681dc24 | |||
| c12afe13e7 | |||
| d6b8ade37f | |||
| 0427ba9fbf | |||
| 3d8f901f1c | |||
| 58bb172bda | |||
| e3872c4760 | |||
| b83bddccf3 | |||
| d5dcbd1891 | |||
| 192b64e407 | |||
| b1b48eaaf3 | |||
| 3530f447c1 | |||
| e97e07e1b3 | |||
| 2251e13068 | |||
| 69b0d7c600 | |||
| 1c332c67f8 | |||
| eb97258ed1 | |||
| 4fe5cca267 | |||
| d656e9f422 | |||
| 44ad6f5342 | |||
| a6779a8aa0 |
+13
-1
@@ -39,7 +39,6 @@ tinygrad_model_runtime/** !filter !diff !merge -text
|
||||
*.a -text
|
||||
*.so -text
|
||||
*.so.* -text
|
||||
openpilot/cereal/messaging/bridge -text
|
||||
openpilot/selfdrive/pandad/pandad -text
|
||||
openpilot/tools/replay/replay -text
|
||||
openpilot/system/camerad/camerad -text
|
||||
@@ -48,4 +47,17 @@ openpilot/system/loggerd/encoderd -text
|
||||
openpilot/system/loggerd/loggerd -text
|
||||
openpilot/starpilot/galaxy/bin/frpc_linux_arm64 -text
|
||||
openpilot/starpilot/maps/provider/mapd -text
|
||||
openpilot/tools/arabesque_firefox/offline/*.deb -text
|
||||
openpilot/tools/arabesque_firefox/offline/*.tar.xz -text
|
||||
openpilot/selfdrive/test/process_replay/refs/starpilot/*.zst -text
|
||||
|
||||
# Published Panda object files are AArch64 ELF, not text.
|
||||
openpilot/selfdrive/pandad/main.o -text
|
||||
openpilot/selfdrive/pandad/panda.o -text
|
||||
openpilot/selfdrive/pandad/panda_safety.o -text
|
||||
openpilot/selfdrive/pandad/panda_usb.o -text
|
||||
openpilot/selfdrive/pandad/pandad.o -text
|
||||
openpilot/selfdrive/pandad/spi.o -text
|
||||
|
||||
# Compressed recorded regression fixture; retain exact binary bytes.
|
||||
openpilot/starpilot/tests/testdata/ford_mache_lead_departure.json.gz -text
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
name: Android companion release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: android-companion-release
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Test, build and sign the companion APK
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
environment: android-release
|
||||
defaults:
|
||||
run:
|
||||
working-directory: openpilot/starpilot/companion/android
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: false
|
||||
lfs: false
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '17'
|
||||
- name: Install Android SDK platform and build tools
|
||||
run: yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "platforms;android-37" "build-tools;37.0.0"
|
||||
- name: Unit tests
|
||||
run: ./gradlew testDebugUnitTest
|
||||
- name: Restore signing key
|
||||
env:
|
||||
KEYSTORE_BASE64: ${{ secrets.COMPANION_KEYSTORE_BASE64 }}
|
||||
run: |
|
||||
umask 077
|
||||
printf '%s' "$KEYSTORE_BASE64" | base64 -d > "$RUNNER_TEMP/companion.jks"
|
||||
- name: Build and sign the APK
|
||||
env:
|
||||
COMPANION_KEYSTORE: ${{ runner.temp }}/companion.jks
|
||||
COMPANION_KEY_PASSWORD: ${{ secrets.COMPANION_KEY_PASSWORD }}
|
||||
run: ./build_apk.sh
|
||||
- name: Remove signing key
|
||||
if: always()
|
||||
run: rm -f "$RUNNER_TEMP/companion.jks"
|
||||
- name: Check the APK
|
||||
working-directory: openpilot/starpilot/galaxy/bin
|
||||
run: |
|
||||
if unzip -l starpilot-companion.apk | grep -q 'assets/galaxy/'; then
|
||||
echo 'APK still bundles Galaxy' >&2
|
||||
exit 1
|
||||
fi
|
||||
BT="$ANDROID_HOME/build-tools/37.0.0"
|
||||
"$BT/aapt2" dump badging starpilot-companion.apk | head -1
|
||||
"$BT/apksigner" verify --print-certs starpilot-companion.apk | grep -E 'certificate (DN|SHA-256)'
|
||||
cat starpilot-companion.apk.version
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: starpilot-companion-apk
|
||||
path: |
|
||||
openpilot/starpilot/galaxy/bin/starpilot-companion.apk
|
||||
openpilot/starpilot/galaxy/bin/starpilot-companion.apk.version
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- Domathon
|
||||
- SecretGoodStarPilot
|
||||
- Dom
|
||||
pull_request:
|
||||
workflow_call:
|
||||
inputs:
|
||||
@@ -29,6 +29,11 @@ jobs:
|
||||
steps:
|
||||
- uses: commaai/timeout@v1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
sparse-checkout: |
|
||||
/docs/
|
||||
/openpilot/selfdrive/assets/icons_mici/settings/comma_icon.png
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
# Build
|
||||
- name: Build docs
|
||||
|
||||
@@ -2,7 +2,7 @@ name: iOS companion
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [SecretGoodStarPilot, secretgood-ios-integration]
|
||||
branches: [Dom, secretgood-ios-integration]
|
||||
paths:
|
||||
- 'openpilot/starpilot/companion/ios/**'
|
||||
- 'openpilot/starpilot/galaxy_bluetooth/**'
|
||||
|
||||
@@ -2,7 +2,7 @@ name: safety qualification
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, Domathon, SecretGoodStarPilot]
|
||||
branches: [master, Domathon, Dom]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- Domathon
|
||||
- SecretGoodStarPilot
|
||||
- Dom
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
@@ -35,17 +35,19 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Validate published device build receipt
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/SecretGoodStarPilot'
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/Dom'
|
||||
run: python3 tools/ci/check_prebuilt_receipt.py --revision HEAD
|
||||
- name: Check shipped device runtime dependencies
|
||||
run: python3 -m unittest tools.laptop_device_build.tests.test_runtime_dependencies -v
|
||||
run: |
|
||||
python3 -m unittest tools.laptop_device_build.tests.test_runtime_dependencies -v
|
||||
python3 -c 'from pathlib import Path; assert b"starting pandad (comma hardware)" in Path("openpilot/selfdrive/pandad/pandad").read_bytes(), "Shipped pandad must use the comma hardware build profile"'
|
||||
- name: Validate tracked dependencies
|
||||
run: python3 tools/vendor/check.py --revision HEAD
|
||||
- run: ./tools/op.sh setup
|
||||
id: setup
|
||||
- name: Build native dependencies for packaging tests
|
||||
id: build
|
||||
run: scons -j2 openpilot/common/libparams_c.so msgq_repo/msgq/ipc_pyx.so
|
||||
run: scons -j2 openpilot/common/libparams_c.so msgq_repo/msgq/_ipc.so
|
||||
- name: Test dependency tools
|
||||
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
|
||||
run: python3 -m unittest discover -s tools/vendor/tests -v
|
||||
@@ -207,6 +209,15 @@ jobs:
|
||||
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
|
||||
timeout-minutes: 5
|
||||
run: tools/op.sh test opendbc_repo/opendbc/car/gm/tests --json-output gm-test-results.json
|
||||
- name: Test shared GM discovery, routing, and manual overrides
|
||||
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
|
||||
timeout-minutes: 2
|
||||
run: >-
|
||||
.venv/bin/python -m pytest -q --import-mode=importlib
|
||||
opendbc_repo/opendbc/car/gm/tests/test_shared_discovery.py
|
||||
opendbc_repo/opendbc/car/gm/tests/test_automatic_discovery.py
|
||||
opendbc_repo/opendbc/car/gm/tests/test_bolt_vin_discovery.py
|
||||
--junitxml=gm-discovery-results.xml
|
||||
- name: Test native independent-axis protocol
|
||||
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
|
||||
run: |
|
||||
@@ -214,6 +225,7 @@ jobs:
|
||||
openpilot/selfdrive/pandad/tests/test_pandad_canprotocol
|
||||
openpilot/selfdrive/pandad/tests/test_aol_protocol
|
||||
openpilot/selfdrive/pandad/tests/test_aol_wire
|
||||
openpilot/selfdrive/pandad/tests/test_panda_faults
|
||||
openpilot/selfdrive/pandad/tests/test_producer_timing
|
||||
openpilot/selfdrive/pandad/tests/test_producer_sink
|
||||
- name: Test map provider to host messaging
|
||||
@@ -227,13 +239,14 @@ jobs:
|
||||
STARPILOT_PYTHON: ${{ github.workspace }}/.venv/bin/python
|
||||
run: go test -mod=readonly -race -run '^Test(MsgqNativeLayout|Shadow(GoPythonIPC|ExecutableHostGpsIPC))$' -count=1 -v
|
||||
- name: Save complete unit test results
|
||||
if: ${{ always() && (hashFiles('unit-test-results.json') != '' || hashFiles('gm-test-results.json') != '' || hashFiles('ci-results/host-test-diagnostics/**') != '') }}
|
||||
if: ${{ always() && (hashFiles('unit-test-results.json') != '' || hashFiles('gm-test-results.json') != '' || hashFiles('gm-discovery-results.xml') != '' || hashFiles('ci-results/host-test-diagnostics/**') != '') }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: unit-test-results
|
||||
path: |
|
||||
unit-test-results.json
|
||||
gm-test-results.json
|
||||
gm-discovery-results.xml
|
||||
ci-results/host-test-diagnostics/
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -246,7 +259,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
|
||||
shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Provide JavaScript runtime for Galaxy contract tests
|
||||
@@ -279,7 +292,7 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
RAYLIB_BACKEND: headless
|
||||
run: .venv/bin/python -m tools.ci.custom_pytest_shards run --index "${{ matrix.shard }}" --count 24 --output "ci-results/custom-pytest-${{ matrix.shard }}"
|
||||
run: .venv/bin/python -m tools.ci.custom_pytest_shards run --index "${{ matrix.shard }}" --count 32 --output "ci-results/custom-pytest-${{ matrix.shard }}"
|
||||
- name: Save collection, every outcome, JUnit and durable worker diagnostics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -302,7 +315,7 @@ jobs:
|
||||
pattern: custom-pytest-*
|
||||
path: ci-results/custom-pytest-shards
|
||||
- name: Require every case passed or audited capability-unavailable
|
||||
run: python3 -m tools.ci.custom_pytest_shards aggregate --directory ci-results/custom-pytest-shards --count 24 --output ci-results/custom-pytest-complete.json
|
||||
run: python3 -m tools.ci.custom_pytest_shards aggregate --directory ci-results/custom-pytest-shards --count 32 --output ci-results/custom-pytest-complete.json
|
||||
- name: Save complete custom catalog verdict
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
+2
-1
@@ -49,6 +49,8 @@ bin/
|
||||
!openpilot/starpilot/soloist/pulseaudio/modules/*.so
|
||||
*.a
|
||||
st[0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z]
|
||||
!/openpilot/starpilot/system/starport/
|
||||
!/openpilot/tools/starport/
|
||||
*.unchunked
|
||||
*.clb
|
||||
*.class
|
||||
@@ -72,7 +74,6 @@ openpilot/selfdrive/pandad/tests/test_aol_wire
|
||||
openpilot/selfdrive/pandad/tests/test_panda_usb
|
||||
openpilot/cereal/services.h
|
||||
openpilot/cereal/gen
|
||||
openpilot/cereal/messaging/bridge
|
||||
openpilot/selfdrive/ui/translations/tmp
|
||||
openpilot/selfdrive/car/tests/cars_dump
|
||||
openpilot/system/camerad/camerad
|
||||
|
||||
+18
-8
@@ -2,7 +2,7 @@
|
||||
|
||||
## Jetlink external inference
|
||||
|
||||
The comma-side Jetlink client and USB transport in `jetlink_repo` are by Zeph Leggett and the [Jetlink contributors](https://github.com/zoompilot/jetlink). They are vendored from revision `d24328d849b0e5154b55f149dc18c5dfe8bd79cc` under the MIT license, retained in [jetlink_repo/LICENSE](jetlink_repo/LICENSE). StarPilot's adapter (`openpilot/starpilot/models/jetlink_adapter.py`) builds on Jetlink's openpilot layer and owns its param names, Always On Lateral rule, model runner reset, settings and alerts. Companion applications and inference servers remain distributed by Jetlink.
|
||||
The comma-side Jetlink client and USB transport in `jetlink_repo` are by Zeph Leggett and the [Jetlink contributors](https://github.com/zoompilot/jetlink). They are vendored from revision `a863c3801da6fd925d14eb1f5b1e250f8efcc649` under the MIT license, retained in [jetlink_repo/LICENSE](jetlink_repo/LICENSE). StarPilot's adapter (`openpilot/starpilot/models/jetlink_adapter.py`) builds on Jetlink's openpilot layer and owns its param names, Always On Lateral rule, model runner reset, settings and alerts. Companion applications and inference servers remain distributed by Jetlink.
|
||||
|
||||
## MRR35 Hyundai angle and radar
|
||||
|
||||
@@ -12,15 +12,11 @@ The copied MRR35 generator source and DBC definition are at `opendbc_repo/opendb
|
||||
|
||||
## Ford support adapted from BluePilot
|
||||
|
||||
The Ford lateral strategies, extended CAN constructors, camera-radar filtering and native checks are adapted from StarPilot `2efe6017bfe14cee4807833e05f9a7f6ba255cdb`. Their lineage includes BluePilot's `bp-7.0` Ford work, principally by Alan Polk, with contributions from John Christman, Jacob Neulight, tonesto7, Haibin Wen and other sunnypilot and BluePilot contributors. The reference snapshot is [`e1d051d7ba270261b4455068bd68f1a58db15a4a`](https://github.com/BluePilotDev/bluepilot/commit/e1d051d7ba270261b4455068bd68f1a58db15a4a); the exact checkout used for the earlier StarPilot import was not recorded.
|
||||
Ford lateral control vendors [BluePilot lateral](https://github.com/BluePilotDev/bluepilot-lateral) v0.2.1 (`2b9dc6cc784faac47a1885d0bda0561f7b76f74d`), developed by Alan Polk (`alan-polk`) and the BluePilot contributors. Alan provided the [StarPilot integration in PR #269](https://github.com/StarPilotHQ/StarPilot/pull/269); his commits and authorship are retained. BluePilot maintains the library and its integration; StarPilot keeps its settings, AOL policy, packaging and integration tests separate. Complete contributor and license notices remain in [the library credits](opendbc_repo/opendbc/bluepilot_lateral/CREDITS.md) and [license](opendbc_repo/opendbc/bluepilot_lateral/LICENSE).
|
||||
|
||||
Relevant lineage includes the controller and integration work in [`db2bdff05`](https://github.com/BluePilotDev/bluepilot/commit/db2bdff05df103d71df62f45c2a3cb5211aba6e6) and [`d0aac605f`](https://github.com/BluePilotDev/bluepilot/commit/d0aac605f99d37e9da205e419f7989c1e9eaa386), extended CAN and safety work in [`8f8d6d15f`](https://github.com/BluePilotDev/bluepilot/commit/8f8d6d15f0a590f42b78de964ffb0d0af7f5d63d), and manual-turn detection in [`97867c1eb`](https://github.com/BluePilotDev/bluepilot/commit/97867c1eb57b7472f6fc3de62f0fef576e5a5497).
|
||||
The Ford CAN FD camera-lead radar filtering is adapted from StarPilot `2efe6017bfe14cee4807833e05f9a7f6ba255cdb`. Its lineage includes BluePilot's `bp-7.0` Ford work, principally by Alan Polk, with contributions from John Christman, Jacob Neulight, tonesto7, Haibin Wen and other sunnypilot and BluePilot contributors. The reference snapshot is [`e1d051d7ba270261b4455068bd68f1a58db15a4a`](https://github.com/BluePilotDev/bluepilot/commit/e1d051d7ba270261b4455068bd68f1a58db15a4a); the exact checkout used for the earlier StarPilot import was not recorded.
|
||||
|
||||
The vehicle strategy and CAN/native owners preserve that attribution while adapting to the current controller interfaces and shared safety limits. Upstream contributors do not maintain or endorse this adaptation. Applicable notices are retained in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
||||
|
||||
## Optional Ford angle controller
|
||||
|
||||
The vendored `opendbc_repo/opendbc/bluepilot_lateral` angle core, codecs and Ford-only safety hooks are adapted directly from [BluePilot bp-7.0 e1d051d7ba270261b4455068bd68f1a58db15a4a](https://github.com/BluePilotDev/bluepilot/commit/e1d051d7ba270261b4455068bd68f1a58db15a4a), principally by Alan Polk, with John Christman, Jacob Neulight, Nathan Ingraham, Praeuner and other BluePilot, sunnypilot and comma contributors. Exact source paths and intentionally stricter safety checks are recorded in that folder. Both upstream `LICENSE` and `LICENSE.md` notices are retained. Its layout follows Alan Polk's September 2026 library proposal; this is a StarPilot adapter, not a published BluePilot library release.
|
||||
The radar interface preserves that attribution while adapting to the current interfaces. Upstream contributors do not maintain or endorse this adaptation. Applicable notices are retained in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
||||
|
||||
## RAV4 Continental ARS510 receiver
|
||||
|
||||
@@ -33,3 +29,17 @@ Spotify's Bluetooth output binds the comma's BlueALSA A2DP source to the selecte
|
||||
## Tesla Pre-AP pedal control
|
||||
|
||||
The Pre-AP configuration, pedal codec/feedback, VirtualDAS feedforward table, grade compensation, torque-zero estimator and vehicle-specific gains preserve firestar5683's intentional Dom work, including `7837a3c1aab`, `aed0b7fdb9e9` and pinned `b220a37f4bd119e402225a7aa16a7cd84878d0af`. Original author attribution: firestar5683 <168790843+firestar5683@users.noreply.github.com>. Startup/calibration ownership, current upstream button/PID interfaces and native command bounds are adapted to the current StarPilot architecture; the old permissive native gateway is not transplanted. Existing source/license notices remain intact.
|
||||
|
||||
## Subaru angle steering from Jacob Waller's fork
|
||||
|
||||
Subaru angle support includes implementation maintained and integrated by **Jacob Waller (jacobw)** in [jacobwaller/openpilot, jul-angle-based](https://github.com/jacobwaller/openpilot/tree/542816196a18dba5f6f0af093c1ece4f415b5a22), with its [opendbc implementation at 8acbeccdab45a47aa7af170d2d17b61f0eb5e4f0](https://github.com/jacobwaller/opendbc/tree/8acbeccdab45a47aa7af170d2d17b61f0eb5e4f0). Jacob authored the VehicleModel angle-limit type correction and integrated the Crosstrek support. The branch also retains the original angle implementation and follow-up work by **lukasloetkolben and elkoled**, alongside comma contributors; their authorship remains theirs.
|
||||
|
||||
The inherited source covers Subaru angle platform/firmware definitions, `ES_Status` cruise state, angle controller/CAN integration, vehicle-model limits and native angle enforcement. StarPilot has additional platform-specific limits, driver handoff, discovery and AOL safety guards; this attribution does not claim identical tuning or complete parity with the reference fork. Original source commits, author names, emails and titles are recorded in [the Subaru origin history](opendbc_repo/opendbc/car/subaru/ORIGIN_HISTORY.tsv).
|
||||
|
||||
The explicit StarPilot [jacobw Subaru adapter](opendbc_repo/opendbc/car/subaru/JACOBW.md) additionally integrates that branch's low-speed filter, target-first limiting and Crosstrek EPS envelope. Its preserved stricter guards and platform scope are documented alongside the code.
|
||||
|
||||
## StarPilot Ford demand tuning
|
||||
|
||||
The previous optional StarPilot Controller, formerly in `openpilot/starpilot/car/ford/starpilot_tuning.py`, restored demand tuning from StarPilot `8da0c9d916^`, descended from BluePilot bp-7.0 `e1d051d7ba270261b4455068bd68f1a58db15a4a` through StarPilot `2efe6017bfe14cee4807833e05f9a7f6ba255cdb`. Contributors to that lineage include Alan Polk, John Christman, Jacob Neulight, tonesto7 and Haibin Wen. The current protocol adapter is maintained by StarPilot separately from the pinned BluePilot library; this attribution does not imply BluePilot maintains or endorses the adapted tune.
|
||||
|
||||
The current StarPilot Controller replaces that historical curvature-primary tune with a separately maintained adaptation of BluePilot v0.2.1's angle strategy (`2b9dc6cc784faac47a1885d0bda0561f7b76f74d`, `core/lateral_angle_ext.py`). Alan Polk is the principal controller author; John Christman contributed angle-mode anti-stall work, Jacob Neulight the pinion/shadow measurement, Nathan Ingraham and Praeuner high-speed damping, and other contributors are retained in the pinned library's `CREDITS.md`. StarPilot adds bounded small-signal conditioning ahead of the unchanged angle limit chain. This adaptation is not maintained or endorsed by BluePilot; the pinned library and its imported hooks are unchanged.
|
||||
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,16 +1,90 @@
|
||||
# StarPilot
|
||||
|
||||
StarPilot is an independent fork of [openpilot](https://github.com/commaai/openpilot), with custom driving controls, vehicle integrations, raylib interfaces for comma 3 and comma four, and the Galaxy companion interface.
|
||||
|
||||
**Domathon is the development branch for StarPilot 7.0.** It rebuilds StarPilot on a current openpilot foundation. Feature migration and vehicle qualification are still in progress; this branch is not a completed replacement for the existing Dom release.
|
||||
[](https://deepwiki.com/StarPilotHQ/StarPilot)
|
||||
[](https://firestar.link/discord)
|
||||
[](https://github.com/StarPilotHQ/StarPilot)
|
||||
[](https://wiki.firestar.link)
|
||||
[](https://apps.apple.com/ca/app/starpilot-bt/id6821039119)
|
||||
[](https://groups.google.com/g/galaxy-starpilot-android-beta)
|
||||
|
||||
## Explore the project
|
||||
**StarPilot** is a custom fork of [comma.ai's openpilot](https://comma.ai/openpilot),
|
||||
an open source driver assistance system.
|
||||
|
||||
- [Developer commands](tools/STARPILOT_DEVELOPMENT.md): host tools, replay, and device builds.
|
||||
- [Driving interfaces](openpilot/starpilot/ui/README.md) and [Galaxy](openpilot/starpilot/galaxy/README.md): native and companion controls.
|
||||
- [Driving models](openpilot/starpilot/models/README.md): model management, artifact compatibility, and Model Laboratory status.
|
||||
- [Builds and developer tools](docs/how-to/laptop-device-build.md): local development and device builds.
|
||||
- [Dependency maintenance](tools/vendor/README.md): tracked source folders and upstream sync records.
|
||||
**This is the StarPilot 7.0 development branch (`SecretGoodStarPilot`). Feature migration,
|
||||
vehicle qualification, and Auto Learn validation are still in progress.**
|
||||
|
||||
Openpilot provides
|
||||
* Automated Lane Centering
|
||||
* Adaptive Cruise Control
|
||||
* Lane Change Assist
|
||||
* Driver Monitoring *without wheel nags*
|
||||
|
||||
Ford-specific lateral-control and vehicle-support work includes substantial adaptations from
|
||||
[BluePilot](https://github.com/BluePilotDev/bluepilot/tree/bp-7.0), principally developed by
|
||||
[Alan Polk](https://github.com/alan-polk) with additional BluePilot contributors. StarPilot's
|
||||
implementation has since diverged, but that does not erase its lineage. See [CREDITS.md](CREDITS.md)
|
||||
for the code-level provenance and [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for applicable
|
||||
upstream notices and terms. BluePilot and its contributors do not maintain or endorse StarPilot;
|
||||
please direct support requests for this adaptation to the StarPilot project.
|
||||
|
||||
Hyundai, Kia, and Genesis angle steering and related vehicle support include substantial adaptations
|
||||
from [sunnypilot](https://github.com/sunnypilot/sunnypilot/tree/hkg-angle-steering-2025) and its
|
||||
[opendbc angle-steering branch](https://github.com/sunnypilot/opendbc/tree/hkg-angle-steering-2025).
|
||||
StarPilot's implementation has diverged significantly; the upstream contributors do not maintain
|
||||
this adaptation. Detailed code lineage is recorded in [CREDITS.md](CREDITS.md).
|
||||
|
||||
StarPilot has a vibrant, welcoming community [discord](https://firestar.link/discord).
|
||||
Stop by to chat or ask questions!
|
||||
If your car isn't driving as well as you'd like, [open a ticket so we can help](https://firestar.link/logs)!
|
||||
|
||||
## Documentation
|
||||
|
||||
Please see [https://wiki.firestar.link](https://wiki.firestar.link) for hardware lists,
|
||||
installation guides, and software configuration.
|
||||
|
||||
## Features
|
||||
|
||||
* **Full support for Comma C3, C3X, and C4**
|
||||
* Full support for chestnut/eGPU on C3, C3X, and C4. C3/C3X may require special gps handling.
|
||||
* Model switcher with all of comma's tinygrad driving models
|
||||
* StarPilot custom torque controller
|
||||
* StarPilot custom longitudinal planner for both radar and VoACC (visual only, radar-less) vehicles
|
||||
* **Custom tuning for an expanding list of vehicles**
|
||||
* Firestar Lateral Method -
|
||||
Auto tuning system to adapt openpilot to *your* car's unique lateral characteristics.
|
||||
* Galaxy: StarPilot's portal to configure your comma device using your phone from anywhere.
|
||||
Download models, change settings, update software, diagnostics, manage dashcam video, and more.
|
||||
* Navigation - Display routes and give the driving model hints on taking exits and turns.
|
||||
* Always On Lateral (full time steering assist)
|
||||
* Lane Centering - Enable to center between detected lane lines,
|
||||
with graceful fallback to model E2E lateral outputs and safety breakout
|
||||
* Force Stop 2.0 - Better stops at stop signs and traffic lights.
|
||||
* Speed Limit Controller - Use dashboard, map data, and/or vision detection for speed limit signs
|
||||
* Curve Speed Controller - Learns from how you take curves
|
||||
* Conditional Experimental Mode (CEM)
|
||||
* Conditional Chill Mode (CCM)
|
||||
* Driving Profiles and Traffic Mode
|
||||
* Custom themes
|
||||
* Alert volume customization
|
||||
* Comma Pedal Interceptor support
|
||||
* Bluetooth support - Connect keyboards, controllers, steering wheel remotes, and more.
|
||||
* Sentry mode - Record when motion is detected while parked.
|
||||
* V-ASM - Detect vehicles in the adjacent/blind spots using the driver-facing camera.
|
||||
* Additional car support - Toyota SDSU, ZSS, CAN filter; pre-AP tesla; ford; Polestar 2; and more.
|
||||
* And more!
|
||||
|
||||
|
||||
## GM-only Features
|
||||
|
||||
* Increased LKAS fault resiliency
|
||||
* ASCM_INT and SASCM support
|
||||
* Custom lateral torque controller, with special tuning for Bolts
|
||||
* 50% extra torque on 2017 Chevy Bolt
|
||||
* Improved lateral and longitudinal tuning
|
||||
* Dashboard cruise control display speed spoofing for vehicles with pedal interceptor
|
||||
* Extra steering wheel button functionality for vehicles with pedal interceptor
|
||||
* Optional toggle to boot comma when remote starting your vehicle
|
||||
|
||||
## Development
|
||||
|
||||
@@ -36,8 +110,10 @@ StarPilot retains upstream vehicle and Panda safety interfaces and adds regressi
|
||||
|
||||
Support depends on the vehicle configuration and enabled feature. An existing vehicle identifier or settings page does not by itself establish driving support. Model Laboratory currently exposes management controls, but paired driving inference remains unavailable pending integration and Chestnut validation.
|
||||
|
||||
## Upstream and attribution
|
||||
## Third-Party Notices
|
||||
|
||||
The current openpilot baseline is [`78dccf0b482ea1298fd1f6fa59b0554187b02ce6`](https://github.com/commaai/openpilot/commit/78dccf0b482ea1298fd1f6fa59b0554187b02ce6). Its history remains intact, followed by the StarPilot foundation and feature commits. Upstream development continues independently; the baseline changes only after an update has been integrated and checked.
|
||||
* Portions of this software include modified versions of the Material Design Icons provided by Google under the Apache License 2.0. A copy of the license is included in the `LICENSE-MDI` file.
|
||||
* Ford support includes software adapted from BluePilot's `bp-7.0` branch. The source repository contains both a standard MIT notice and a separate custom SUNNYPILOT LLC notice; StarPilot preserves both in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
||||
* Hyundai, Kia, and Genesis support includes software adapted directly from sunnypilot's HKG angle-steering branch and sunnypilot/opendbc. StarPilot preserves the applicable notices in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
||||
|
||||
See [LICENSE](LICENSE), [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md), and [CREDITS.md](CREDITS.md) for licensing and source attribution. Component licenses remain with their source folders. StarPilot is maintained independently of comma.
|
||||
> This project uses software from Haibin Wen and SUNNYPILOT LLC and is licensed under a custom license requiring permission for use.
|
||||
|
||||
+17
-16
@@ -57,7 +57,7 @@ assert arch in [
|
||||
"Darwin", # macOS arm64 (x86 not supported)
|
||||
]
|
||||
|
||||
pkg_names = ['acados', 'capnproto', 'ffmpeg', 'json11', 'ncurses', 'zeromq', 'zstd']
|
||||
pkg_names = ['acados', 'capnproto', 'ffmpeg', 'json11', 'ncurses', 'zstd']
|
||||
if arch != 'Darwin':
|
||||
pkg_names.append('libusb')
|
||||
pkgs = [importlib.import_module(name) for name in pkg_names]
|
||||
@@ -172,7 +172,7 @@ env = Environment(
|
||||
COMPILATIONDB_USE_ABSPATH=True,
|
||||
REDNOSE_ROOT="#rednose_repo",
|
||||
tools=["default", "cython", "compilation_db", "rednose_filter"],
|
||||
toolpath=["#msgq_repo/site_scons/site_tools", "#rednose_repo/site_scons/site_tools"],
|
||||
toolpath=["#rednose_repo/site_scons/site_tools"],
|
||||
)
|
||||
# SCons' Darwin linker tool doesn't define the variables used to expand RPATH.
|
||||
if arch == "Darwin":
|
||||
@@ -292,7 +292,7 @@ SConscript(['opendbc_repo/opendbc/can/SConscript'])
|
||||
# Build common module
|
||||
SConscript(['openpilot/common/SConscript'])
|
||||
Import('_common')
|
||||
common = [_common, 'json11', 'zmq']
|
||||
common = [_common, 'json11']
|
||||
Export('common')
|
||||
|
||||
# Build messaging (cereal + msgq + socketmaster + their dependencies)
|
||||
@@ -318,7 +318,7 @@ SConscript([
|
||||
|
||||
if arch == "comma_arm64":
|
||||
SConscript(['openpilot/system/camerad/SConscript'])
|
||||
SConscript(['openpilot/starpilot/system/android_auto/SConscript'])
|
||||
SConscript(['openpilot/starpilot/system/starport/SConscript'])
|
||||
|
||||
# Build selfdrive
|
||||
SConscript([
|
||||
@@ -329,6 +329,7 @@ SConscript([
|
||||
'openpilot/selfdrive/ui/SConscript',
|
||||
'openpilot/starpilot/audio/native/SConscript',
|
||||
'openpilot/starpilot/models/runtime_bridge/SConscript',
|
||||
'openpilot/starpilot/ui/native/SConscript',
|
||||
'openpilot/starpilot/flm/SConscript',
|
||||
])
|
||||
|
||||
@@ -347,7 +348,7 @@ if GetOption('extras') and arch != "comma_arm64":
|
||||
env.CompilationDatabase('compile_commands.json')
|
||||
|
||||
# progress output
|
||||
def count_scons_nodes(nodes):
|
||||
def get_scons_nodes(nodes):
|
||||
seen = set()
|
||||
stack = list(nodes)
|
||||
|
||||
@@ -356,28 +357,28 @@ def count_scons_nodes(nodes):
|
||||
if node in seen:
|
||||
continue
|
||||
seen.add(node)
|
||||
executor = node.get_executor()
|
||||
if executor is not None:
|
||||
stack += executor.get_all_prerequisites() + executor.get_all_children()
|
||||
# Only scan directories; generated sources are not ready yet.
|
||||
stack += (node.prerequisites or []) + node.children(scan=isinstance(node, SCons.Node.FS.Dir))
|
||||
|
||||
return len(seen)
|
||||
return seen
|
||||
|
||||
progress_interval = 5
|
||||
progress_count = 0
|
||||
progress_total = max(1, count_scons_nodes(env.arg2nodes(BUILD_TARGETS or [Dir('.')], env.fs.Entry)))
|
||||
progress_nodes = get_scons_nodes(env.arg2nodes(BUILD_TARGETS or [Dir('.')], env.fs.Entry))
|
||||
progress_total = max(1, len(progress_nodes))
|
||||
|
||||
def progress_function(node):
|
||||
global progress_count
|
||||
if progress_count >= progress_total:
|
||||
if node not in progress_nodes:
|
||||
return
|
||||
progress_count = min(progress_count + progress_interval, progress_total)
|
||||
progress = round(100. * progress_count / progress_total, 1)
|
||||
progress_nodes.remove(node)
|
||||
if len(progress_nodes) % progress_interval:
|
||||
return
|
||||
progress = round(100. * (1 - len(progress_nodes) / progress_total), 1)
|
||||
sys.stderr.write("\rBuilding: %5.1f%%" % progress if sys.stderr.isatty() else "progress: %.1f\n" % progress)
|
||||
if progress == 100. and sys.stderr.isatty():
|
||||
sys.stderr.write("\n")
|
||||
sys.stderr.flush()
|
||||
|
||||
Progress(progress_function, interval=progress_interval)
|
||||
Progress(progress_function)
|
||||
AddPostAction(BUILD_TARGETS or [Dir('.')], prune_cache_dir)
|
||||
|
||||
# Release CI rebuilds tracked device outputs for the host architecture.
|
||||
|
||||
+23
-12
@@ -1,5 +1,16 @@
|
||||
# Third-party notices
|
||||
|
||||
## Arabesque Firefox
|
||||
|
||||
The offline payload contains original Mozilla and Ubuntu archives. The pinned
|
||||
versions, upstream download URLs and hashes are listed in
|
||||
[`runtime.json`](openpilot/tools/arabesque_firefox/runtime.json). Mozilla publishes
|
||||
the [Firefox 153.6.0esr source](https://archive.mozilla.org/pub/firefox/releases/153.6.0esr/source/)
|
||||
and [Firefox notices](https://www.mozilla.org/en-US/about/legal/firefox/).
|
||||
The Ubuntu archives retain their package copyright files. The
|
||||
[Ubuntu package archive](https://ubuntu.com/project/docs/how-ubuntu-is-made/concepts/package-archive/)
|
||||
provides source packages alongside the binaries.
|
||||
|
||||
## Jetlink
|
||||
|
||||
Jetlink's comma client is copyright (c) 2026 Zeph Leggett and distributed under the MIT License. Its complete license and permission notice are retained in [jetlink_repo/LICENSE](jetlink_repo/LICENSE); source and revision are recorded in [CREDITS.md](CREDITS.md#jetlink-external-inference) and `upstream-sync.json`.
|
||||
@@ -73,17 +84,7 @@ the exclusion of the other.
|
||||
|
||||
## BluePilot Ford support
|
||||
|
||||
The Ford lateral strategies, CAN constructors, camera-radar filtering and native checks include work adapted from BluePilot's `bp-7.0` source, through StarPilot `2efe6017bfe14cee4807833e05f9a7f6ba255cdb`. See [CREDITS.md](CREDITS.md#ford-support-adapted-from-bluepilot) for contributors and source revisions. The upstream extension-file and `LICENSE.md` notices reproduced above also apply to the Ford extension sources; they are retained without resolving the difference between their published license descriptions.
|
||||
|
||||
## Optional BluePilot angle library adapter
|
||||
|
||||
The angle-only adapter in `opendbc_repo/opendbc/bluepilot_lateral` is sourced directly from BluePilot bp-7.0 commit e1d051d7ba270261b4455068bd68f1a58db15a4a. The complete upstream MIT notice is retained in that folder's `LICENSE`, and the simultaneously published Custom MIT notice is retained verbatim in `LICENSE.md`. The applicable SUNNYPILOT acknowledgments reproduced above are also retained in its `CREDITS.md`. This preserves the published terms without resolving their scope or representing the adapted extension as unambiguously plain MIT.
|
||||
|
||||
The optional, default-disabled angle lane-centering trim additionally derives from
|
||||
BluePilot bp-dev e22afa6be9b881fa784c92ebb316db47728a3d81. Its exact source and
|
||||
contributors are recorded in the library CREDITS.md; the same root/opendbc MIT
|
||||
and Custom MIT notices coexist unchanged at that revision. This adds attribution,
|
||||
not a license-scope decision or permission grant.
|
||||
The Ford CAN FD camera-radar filtering includes work adapted from BluePilot's `bp-7.0` source, through StarPilot `2efe6017bfe14cee4807833e05f9a7f6ba255cdb`. See [CREDITS.md](CREDITS.md#ford-support-adapted-from-bluepilot) for contributors and source revisions. The upstream extension-file and `LICENSE.md` notices reproduced above also apply to that source; they are retained without resolving the difference between their published license descriptions.
|
||||
|
||||
## Continental ARS510 radar
|
||||
|
||||
@@ -129,7 +130,7 @@ UI framework is bundled.
|
||||
|
||||
## Galaxy Bluetooth companion source snapshot
|
||||
|
||||
The Galaxy Bluetooth phone bridge and iOS app from https://github.com/firestar5683/StarPilot
|
||||
The Galaxy Bluetooth phone bridge and iOS app from https://github.com/StarPilotHQ/StarPilot
|
||||
(natehuby76, PRs #224/#225) are included under `openpilot/starpilot/galaxy_bluetooth/`
|
||||
and `openpilot/starpilot/companion/ios/`, with their license files (`UPSTREAM-LICENSE`, `openpilot/starpilot/companion/ios/UPSTREAM-GALAXY-LICENSE`).
|
||||
The bridge runs on openpilot's Python with PyCryptodome 3.23.0 (BSD/public-domain
|
||||
@@ -140,3 +141,13 @@ dbus-next 0.2.3 (MIT), ifaddr 0.2.0 (MIT) and Zeroconf 0.151.3
|
||||
`vendor/VENDOR.json` records each PyPI source file and its SHA-256. Zeroconf is
|
||||
kept as separate, unmodified source files that can be replaced with another
|
||||
compatible version, as the LGPL requires.
|
||||
|
||||
## Subaru angle support from jacobw
|
||||
|
||||
The Subaru angle implementation includes work from Jacob Waller's `jul-angle-based` openpilot/opendbc branches and their upstream contributors. See [CREDITS.md](CREDITS.md#subaru-angle-steering-from-jacob-wallers-fork) and `opendbc_repo/opendbc/car/subaru/ORIGIN_HISTORY.tsv` for pinned sources, original authors and titles. The source repositories publish the comma MIT license; the complete applicable notice remains in the project `LICENSE` and `opendbc_repo/LICENSE`. StarPilot's adaptations preserve additional native and caller safety guards.
|
||||
|
||||
## StarPilot Ford demand tuning
|
||||
|
||||
The historical tuning formerly in `openpilot/starpilot/car/ford/starpilot_tuning.py` included the BluePilot bp-7.0-derived work identified in [CREDITS.md](CREDITS.md#starpilot-ford-demand-tuning). The published upstream license notices retained above and in the pinned BluePilot library remain applicable to the adapted source. StarPilot's separate protocol adapter does not modify those notices or resolve their scope.
|
||||
|
||||
The angle-primary adaptation in `openpilot/starpilot/car/ford/starpilot_controller.py` is derived from BluePilot v0.2.1. It retains the complete MIT copyright and permission notice in `opendbc_repo/opendbc/bluepilot_lateral/LICENSE`, including Comma.ai, BluePilot and StarPilot contributors. See the contributor attribution in `CREDITS.md`.
|
||||
|
||||
@@ -77,7 +77,7 @@ while [[ $# -gt 0 ]]; do
|
||||
;;
|
||||
--cereal|-cereal)
|
||||
shortcut_targets+=(openpilot/cereal/libcereal.a openpilot/cereal/libsocketmaster.a
|
||||
openpilot/cereal/messaging/bridge openpilot/cereal/services.h)
|
||||
openpilot/cereal/services.h)
|
||||
;;
|
||||
-j|--jobs)
|
||||
passthrough_args+=("$1")
|
||||
|
||||
+26
-1
@@ -5,6 +5,8 @@ openpilot ALC and openpilot LDW do not automatically drive the vehicle or reduce
|
||||
|
||||
While changing lanes, openpilot is not capable of looking next to you or checking your blind spot. Only nudge the wheel to initiate a lane change after you have confirmed it's safe to do so.
|
||||
|
||||
Unexpected steering may occur. openpilot ALC may not steer to avoid stopped or slow-moving vehicles, cross traffic, construction barriers or cones, motorcycles, children, pedestrians, animals, or other objects on the road.
|
||||
|
||||
Many factors can impact the performance of openpilot ALC and openpilot LDW, causing them to be unable to function as intended. These include, but are not limited to:
|
||||
|
||||
* Poor visibility (heavy rain, snow, fog, etc.) or weather conditions that may interfere with sensor operation.
|
||||
@@ -13,6 +15,14 @@ Many factors can impact the performance of openpilot ALC and openpilot LDW, caus
|
||||
* The device is mounted incorrectly.
|
||||
* When in sharp curves, like on-off ramps, intersections etc...; openpilot is designed to be limited in the amount of steering torque it can produce.
|
||||
* In the presence of restricted lanes or construction zones.
|
||||
* When the lane markings are unclear, worn, missing, or they cross each other.
|
||||
* Tar marks, shadows, road cracks, temporary or construction lane markings, or other road imperfections.
|
||||
* When lanes merge or split.
|
||||
* When entering a highway on-ramp or exiting an off-ramp.
|
||||
* On roads with intersections or roundabouts.
|
||||
* On slippery roads.
|
||||
* When towing a trailer.
|
||||
* When entering or exiting a tunnel.
|
||||
* When driving on highly banked roads or in presence of strong cross-wind.
|
||||
* Extremely hot or cold temperatures.
|
||||
* Bright light (due to oncoming headlights, direct sunlight, etc.).
|
||||
@@ -24,6 +34,8 @@ The list above does not represent an exhaustive list of situations that may inte
|
||||
|
||||
openpilot ACC and openpilot FCW are not systems that allow careless or inattentive driving. It is still necessary for the driver to pay close attention to the vehicle’s surroundings and to be ready to re-take control of the gas and the brake at all times.
|
||||
|
||||
openpilot ACC may not stop your vehicle in time to avoid a collision with a vehicle that quickly stops in front of you. openpilot ACC may occasionally cause your vehicle to brake when not required or when you are not expecting it.
|
||||
|
||||
Many factors can impact the performance of openpilot ACC and openpilot FCW, causing them to be unable to function as intended. These include, but are not limited to:
|
||||
|
||||
* Poor visibility (heavy rain, snow, fog, etc.) or weather conditions that may interfere with sensor operation.
|
||||
@@ -35,9 +47,20 @@ Many factors can impact the performance of openpilot ACC and openpilot FCW, caus
|
||||
* In presence of traffic signs or stop lights, which are not detected by openpilot at this time.
|
||||
* When the posted speed limit is below the user selected set speed. openpilot does not detect speed limits at this time.
|
||||
* In presence of vehicles in the same lane that are not moving.
|
||||
* When a vehicle you are following moves out of your driving path and a stationary or slow-moving vehicle or object is in front of you.
|
||||
* Narrow vehicles, such as bicycles or motorcycles.
|
||||
* Vehicles or objects only partially in the driving lane.
|
||||
* On curves, openpilot ACC may not detect a vehicle in the same lane, and may accelerate to the set speed.
|
||||
* Cross traffic.
|
||||
* Oncoming vehicles.
|
||||
* Animals or objects in the road.
|
||||
* Rare objects such as trailers, ramps, cargo, open doors, etc. protruding from vehicles.
|
||||
* When abrupt braking maneuvers are required. openpilot is designed to be limited in the amount of deceleration and acceleration that it can produce.
|
||||
* When surrounding vehicles perform close cut-ins from neighbor lanes.
|
||||
* Driving on hills, narrow, or winding roads.
|
||||
* On slippery roads.
|
||||
* When towing a trailer.
|
||||
* When entering or exiting a tunnel.
|
||||
* Extremely hot or cold temperatures.
|
||||
* Bright light (due to oncoming headlights, direct sunlight, etc.).
|
||||
* Interference from other equipment that generates radar waves.
|
||||
@@ -46,7 +69,9 @@ The list above does not represent an exhaustive list of situations that may inte
|
||||
|
||||
## Limitations of openpilot DM
|
||||
|
||||
openpilot DM should not be considered an exact measurement of the alertness of the driver.
|
||||
openpilot DM displays a series of escalating warnings. Disregard of these warnings may result in openpilot being unavailable for a period of time.
|
||||
|
||||
openpilot DM should not be considered an exact measurement of the alertness of the driver. openpilot DM cannot always detect if the driver is tired or inattentive.
|
||||
|
||||
Many factors can impact the performance of openpilot DM, causing it to be unable to function as intended. These include, but are not limited to:
|
||||
|
||||
|
||||
@@ -4,6 +4,12 @@ openpilot is an Adaptive Cruise Control (ACC) and Automated Lane Centering (ALC)
|
||||
Like other ACC and ALC systems, openpilot is a failsafe passive system and it requires the
|
||||
driver to be alert and to pay attention at all times.
|
||||
|
||||
openpilot is not an autonomous driving system. It is not a collision avoidance system or a
|
||||
substitute for safe and attentive driving. The driver is always responsible for the safe
|
||||
operation of the vehicle and must be ready to take control at any time. The driver can override
|
||||
openpilot at any time by braking, accelerating, or steering the vehicle. openpilot may
|
||||
disengage without warning. Read the [limitations](https://github.com/commaai/openpilot/blob/master/docs/LIMITATIONS.md) carefully before using openpilot.
|
||||
|
||||
To assist the driver in maintaining alertness, openpilot includes a driver monitoring feature
|
||||
that alerts when it detects driver distraction.
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PYTHON_BIN="${HEATDEATH_PYTHON:-python3}"
|
||||
if [[ -z "${HEATDEATH_PYTHON:-}" && -x /usr/local/venv/bin/python3 ]]; then
|
||||
PYTHON_BIN=/usr/local/venv/bin/python3
|
||||
fi
|
||||
exec "$PYTHON_BIN" -B "$ROOT_DIR/tools/heatdeath/heatdeath.py" "$@"
|
||||
@@ -10,4 +10,4 @@ jetlink: run openpilot's large driving models on an attached Jetson.
|
||||
# so does the release check (macos/scripts/check-version.sh). The comma runs
|
||||
# jetlink from a checkout, not an install, so this is also all it has. The Swift
|
||||
# reads it from Pinned.swift: run JetlinkKit/Scripts/make_pins.py after a bump.
|
||||
__version__ = '0.8.7'
|
||||
__version__ = '0.8.12'
|
||||
|
||||
@@ -224,12 +224,16 @@ class JetlinkClient:
|
||||
|
||||
# -- handshake ------------------------------------------------------------
|
||||
|
||||
def hello(self, timeout: float = 5.0) -> dict:
|
||||
def hello(self, timeout: float = 5.0, pick: dict | None = None) -> dict:
|
||||
"""Introduce this client. The server starts its session over on a hello,
|
||||
so this is also how a new owner of the gadget takes over one the server
|
||||
never saw end; see Session.greet in JetlinkServer."""
|
||||
never saw end; see Session.greet in JetlinkServer. `pick` is the comma's
|
||||
own big-model pick and its age, {ref, sha256, nbytes, name, age_s}; the
|
||||
answer's `pick` is the newer of it and the server's (PickStore)."""
|
||||
seq = self._next_seq()
|
||||
client = {'nonce': self.nonce, 'name': self.name}
|
||||
if pick is not None:
|
||||
client['pick'] = pick
|
||||
try:
|
||||
link = self.t.link_info()
|
||||
except Exception:
|
||||
@@ -301,7 +305,8 @@ class JetlinkClient:
|
||||
frame_skip: int = DEFAULT_FRAME_SKIP,
|
||||
progress: ProgressFn | None = None,
|
||||
build_timeout: float = 900.0,
|
||||
should_stop: StopFn | None = None) -> ModelSpec:
|
||||
should_stop: StopFn | None = None,
|
||||
fetch: bool = False, ref: str | None = None) -> ModelSpec:
|
||||
"""Make the server ready to run this model, uploading and building if needed.
|
||||
|
||||
Blocks until ready or the build fails, and returns the spec the server
|
||||
@@ -309,8 +314,12 @@ class JetlinkClient:
|
||||
parser rejects an export (tinygrad and the org.tinygrad domain) still runs.
|
||||
|
||||
`onnx_path` None means the caller cannot upload (modeld) and a missing
|
||||
engine is EngineMissing. `progress(stage, frac, msg)` has stage in
|
||||
upload/patch/parse/build/load. `should_stop` is polled during long waits.
|
||||
engine is EngineMissing. `fetch` asks a server that lacks the model to
|
||||
download it itself (state downloading, stage download) rather than answer
|
||||
need_upload; one that cannot, or is too old to, answers need_upload as
|
||||
before. `ref` is the catalog ref the model is known by.
|
||||
`progress(stage, frac, msg)` has stage in
|
||||
download/upload/patch/parse/build/load. `should_stop` is polled during long waits.
|
||||
"""
|
||||
self.progress_cb = progress
|
||||
self._should_stop = should_stop or (lambda: False)
|
||||
@@ -318,7 +327,12 @@ class JetlinkClient:
|
||||
self._engine_state = None
|
||||
|
||||
seq = self._next_seq()
|
||||
self.t.send_json(P.Msg.ENGINE_REQ, seq, {'sha256': sha256, 'nbytes': nbytes, 'frame_skip': frame_skip})
|
||||
request = {'sha256': sha256, 'nbytes': nbytes, 'frame_skip': frame_skip}
|
||||
if fetch:
|
||||
request['fetch'] = True
|
||||
if ref:
|
||||
request['ref'] = ref
|
||||
self.t.send_json(P.Msg.ENGINE_REQ, seq, request)
|
||||
resp = json.loads(bytes(self._expect(P.Msg.ENGINE_RESP, seq, 60.0).payload))
|
||||
self._engine_state = resp
|
||||
log.info("server engine state: %s (%s)", resp['state'], resp.get('detail', ''))
|
||||
|
||||
@@ -55,7 +55,7 @@ POLL = 0.5
|
||||
# what a borrower passes on of the server's hello (Loan.note_server): the owner
|
||||
# never speaks the protocol, and sleep_after is how it knows whether letting
|
||||
# go of the gadget lets the Jetson sleep. The rest is for its status record
|
||||
SERVER_FIELDS = ('protocol', 'device', 'backend', 'runtime_version', 'trt_version', 'sleep_after')
|
||||
SERVER_FIELDS = ('protocol', 'device', 'backend', 'runtime_version', 'trt_version', 'sleep_after', 'pick', 'fetches')
|
||||
|
||||
|
||||
def _send(conn: socket.socket, msg: dict) -> None:
|
||||
|
||||
@@ -45,15 +45,23 @@ class Keys:
|
||||
offroad: str # BOOL: is the car parked (manager's IsOffroad)
|
||||
progress: str # JSON {stage, frac, msg, drops}: provisioning and join progress
|
||||
spec: str # JSON: the built model's spec and whether its engine is built
|
||||
pointers: str # JSON: catalog ref -> {oid, size}
|
||||
pointers: str # JSON: catalog ref, or a hash-named pick's sha256 -> {oid, size}
|
||||
# sunnypilot's model manager: the big-model pick, JSON {ref, displayName},
|
||||
# and the big-model catalog, JSON {bundles}. Without one both are None, and
|
||||
# jetlink runs its own default big model
|
||||
# jetlink runs its own default big model, or the one picked on the server.
|
||||
# A fork with a picker of its own and no catalog (StarPilot) declares only
|
||||
# big_model, and may name its pick by the ONNX's hash, {sha256,
|
||||
# displayName}; a newer pick made on the server is written into it
|
||||
big_model: str | None = None
|
||||
catalog: str | None = None
|
||||
# BOOL: an iPhone on a direct cable is asked to charge from the comma
|
||||
# (comma.port). Off when unset, and without the key
|
||||
charge_phone: str | None = None
|
||||
# STRING: where the model manager takes a pick by its ref and stores it in
|
||||
# big_model (sunnypilot's ModelManager_DownloadRef). A newer pick made on
|
||||
# the server is written there, so the comma's own list shows it; without
|
||||
# the key it drives all the same
|
||||
select: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -152,6 +160,16 @@ class ModelSide(WorkerSide, Protocol):
|
||||
frame thread at a hand-back, so it zeroes the history in place and
|
||||
allocates and compiles nothing; warp.reset_queues makes one."""
|
||||
|
||||
def model_warp(self, small: Any, cam_w: int, cam_h: int, model_w: int, model_h: int) -> FrameWarp | None:
|
||||
"""Optional (OPTIONAL): the warp the link sends frames through, warmed,
|
||||
or None for openpilot's driving warp pickle at warp_path (warp.Warp). For
|
||||
a fork whose runtime cannot load that pickle, typically with its small
|
||||
model's own warp. Asked once on modeld's main thread before the frame
|
||||
loop, after the small model's warmup; raising keeps the drive on the
|
||||
small model, as a pickle that does not load does. The offroad checks
|
||||
still look for the pickle (Warps.built): only modeld has a small model
|
||||
to ask, so warp_path must name a file either way."""
|
||||
|
||||
|
||||
class Openpilot(ModelSide, Protocol):
|
||||
"""The whole adapter: one object implements every side."""
|
||||
@@ -198,13 +216,28 @@ class JoinedModel(Protocol):
|
||||
"""Let go of the link at modeld's exit."""
|
||||
|
||||
|
||||
class FrameWarp(Protocol):
|
||||
"""What an adapter's model_warp returns, warp.Warp's shape, run on modeld's
|
||||
frame thread. Only one of the small and the large model runs a frame, so a
|
||||
warp that shares the small model's JIT and buffers is never in use by both.
|
||||
On a comma, a warp that reads its GPU output back moves it first with
|
||||
warp.make_coherent, before the warp's JIT first runs."""
|
||||
output: Any # the warped frame, (2, 6, model_h/2, model_w/2) uint8 as bytes: read after wait(), until the next start()
|
||||
|
||||
def start(self, frame: int, big_frame: int, tfm: Any, big_tfm: Any) -> None:
|
||||
"""Warp the camera buffers at these addresses under their 3x3 transforms."""
|
||||
|
||||
def wait(self) -> None:
|
||||
"""Return once `output` holds what start() warped."""
|
||||
|
||||
|
||||
# The members of each protocol an implementation may lack: jetlink reads them
|
||||
# with getattr and a fallback. conformance() passes an object without one and
|
||||
# holds one with it to its signature, and adding one keeps API. Kept out of
|
||||
# the protocols, where a name would be a member itself; a type checker still
|
||||
# asks for these, as typing has no optional member
|
||||
OPTIONAL: dict[type, tuple[str, ...]] = {
|
||||
ModelSide: ('small_reset',),
|
||||
ModelSide: ('small_reset', 'model_warp'),
|
||||
SmallModel: ('desire_key', 'numpy_inputs', 'lat_delay', 'PLANPLUS_CONTROL'),
|
||||
JoinedModel: ('get_action_from_model', 'constants', 'LAT_SMOOTH_SECONDS', 'LONG_SMOOTH_SECONDS', 'lat_delay'),
|
||||
}
|
||||
|
||||
@@ -334,7 +334,7 @@ class JoiningModelState:
|
||||
self._demote(BEHIND)
|
||||
elif self._active is not self._small and self._frame_drop_ratio > DROP_LIMIT:
|
||||
# modeld is behind the large model; this frame is the small model's
|
||||
self._log.warning("jetlink: modeld dropped %.2f %% of camera frames behind the large model, "
|
||||
self._log.warning("jetlink: modeld dropped %.2f %% of camera frames behind the large model, " +
|
||||
"the small model drives from this one", self._frame_drop_ratio * 100)
|
||||
self._demote(BEHIND)
|
||||
self._maybe_swap()
|
||||
@@ -745,6 +745,7 @@ def join(parts, cam_w: int, cam_h: int, small) -> JoiningModelState:
|
||||
link = links.Link(parts.log)
|
||||
|
||||
def prepare():
|
||||
from jetlink.openpilot.interface import FrameWarp, conformance
|
||||
from jetlink.openpilot.warp import Warp, prepare_reset
|
||||
# the gadget first, so the Jetson enumerates while the warp loads. Left to
|
||||
# the join thread the bind lands ~3 s later, behind the small model's
|
||||
@@ -759,7 +760,12 @@ def join(parts, cam_w: int, cam_h: int, small) -> JoiningModelState:
|
||||
try:
|
||||
# the fork's own reset of its small model, else jetlink's from its shape
|
||||
ready['reset_small'] = getattr(op, 'small_reset', lambda small: None)(small) or prepare_reset(small)
|
||||
warp = Warp(parts.warps.load(cam_w, cam_h, *geometry), face.frame_size(cam_w, cam_h))
|
||||
# the fork's own warp, else openpilot's driving warp pickle
|
||||
warp = getattr(op, 'model_warp', lambda *args: None)(small, cam_w, cam_h, *geometry)
|
||||
if warp is None:
|
||||
warp = Warp(parts.warps.load(cam_w, cam_h, *geometry), face.frame_size(cam_w, cam_h))
|
||||
elif problems := conformance(warp, FrameWarp):
|
||||
raise RuntimeError(f"the adapter's model_warp is not a warp: {', '.join(problems)}")
|
||||
except Exception:
|
||||
link.close()
|
||||
raise
|
||||
|
||||
@@ -43,6 +43,8 @@ PRESENT_TIMEOUT = 5.0
|
||||
# hardware; the ceiling is only there so a server that has stopped answering
|
||||
# does not hold the caller for the rest of the day.
|
||||
BUILD_TIMEOUT = 1800.0
|
||||
# and on top of that, a server downloading the model itself: 1.75 GB at 1 MB/s
|
||||
FETCH_TIMEOUT = 1800.0
|
||||
|
||||
|
||||
class ModelMissing(RuntimeError):
|
||||
@@ -226,12 +228,15 @@ def identity(parts, entry: dict) -> tuple[str, int]:
|
||||
"""The picked model's sha256 and byte count, from the catalog model's LFS
|
||||
pointer, so the comma can name the model without holding or hashing the
|
||||
ONNX. Looked up once per model ever and kept in a param: the one part of
|
||||
provisioning that needs the internet."""
|
||||
provisioning that needs the internet. A model picked by its hash alone is
|
||||
only sized."""
|
||||
sha256, nbytes = entry.get('oid'), entry.get('size')
|
||||
if sha256 and nbytes:
|
||||
return sha256, int(nbytes)
|
||||
parts.progress.report('connect', 0.0, 'finding model', code='finding_model')
|
||||
return parts.models.resolve_pointer(entry['ref'])
|
||||
if entry.get('ref'):
|
||||
return parts.models.resolve_pointer(entry['ref'])
|
||||
return parts.models.resolve_size(sha256)
|
||||
|
||||
|
||||
# What has already been hashed in this process, keyed on the file as it was
|
||||
@@ -278,21 +283,27 @@ def verified_upload(log, model_path: Path | None, sha256: str, nbytes: int) -> P
|
||||
|
||||
|
||||
def ensure(parts, client, sha256: str, nbytes: int, model_path: Path | None, *,
|
||||
progress=None, should_stop=None, build_timeout: float = BUILD_TIMEOUT):
|
||||
progress=None, should_stop=None, build_timeout: float = BUILD_TIMEOUT, ref: str | None = None,
|
||||
fetch: bool = True):
|
||||
"""Make the server ready for this model and remember what it answered.
|
||||
Asks without the file first: the server answers from the sha alone when it
|
||||
has the model, which is every parked poll and every join. EngineMissing means
|
||||
the Jetson has neither the plan nor the bytes, and neither has the caller."""
|
||||
has the model, which is every parked poll and every join. Without the file
|
||||
here either, the server is asked to download the model itself, which only
|
||||
the Jetson needs. EngineMissing means the Jetson has neither the plan nor
|
||||
the bytes and could not get them, and neither has the caller. `fetch` False
|
||||
never asks for the download: a stand-in is a model the server has. `ref`,
|
||||
the catalog's name for the model, goes with a download."""
|
||||
from jetlink.client import EngineMissing
|
||||
ask = functools.partial(client.ensure_engine, sha256, nbytes, progress=progress,
|
||||
build_timeout=build_timeout, should_stop=should_stop)
|
||||
fetch = fetch and model_path is None
|
||||
ask = functools.partial(client.ensure_engine, sha256, nbytes, progress=progress, should_stop=should_stop)
|
||||
try:
|
||||
spec = ask(onnx_path=None)
|
||||
spec = ask(onnx_path=None, fetch=fetch, ref=ref if fetch else None,
|
||||
build_timeout=build_timeout + (FETCH_TIMEOUT if fetch else 0.0))
|
||||
except EngineMissing:
|
||||
upload = verified_upload(parts.log, model_path, sha256, nbytes)
|
||||
if upload is None:
|
||||
raise
|
||||
spec = ask(onnx_path=upload)
|
||||
spec = ask(onnx_path=upload, build_timeout=build_timeout)
|
||||
parts.spec.store(spec)
|
||||
return spec
|
||||
|
||||
@@ -346,11 +357,14 @@ def open_link(parts, link: Link, should_stop=None):
|
||||
# booting; both resolve on their own
|
||||
client = connect_patiently(link)
|
||||
try:
|
||||
hello = client.hello(timeout=10.0)
|
||||
hello = parts.models.heard(client.hello(timeout=10.0, pick=parts.models.hello_pick()))
|
||||
parts.log.warning("jetlink: %s trt %s, engine %s, loaded %s",
|
||||
hello.get('device'), hello.get('trt_version'),
|
||||
hello.get('engine_state'), str(hello.get('loaded'))[:16])
|
||||
link.note_server(hello)
|
||||
# a newer pick on the server drives, and is ours from now on
|
||||
parts.models.adopt()
|
||||
selected = parts.models.selected_model() or selected
|
||||
sha256, nbytes = identity(parts, selected)
|
||||
path = parts.models.shipped_model_path()
|
||||
built = parts.spec.ready_spec()
|
||||
@@ -370,7 +384,7 @@ def open_link(parts, link: Link, should_stop=None):
|
||||
# server that restarted reloads from the plan cache, 13 to 25 s; one
|
||||
# that has never seen this model builds it, 102 to 294 s
|
||||
spec = ensure(parts, client, sha256, nbytes, path, progress=parts.progress.report_with_eta,
|
||||
should_stop=should_stop)
|
||||
should_stop=should_stop, ref=selected.get('ref'), fetch=standin is None)
|
||||
client.deadline = INFERENCE_TIMEOUT
|
||||
return client, spec
|
||||
except EngineMissing as e:
|
||||
|
||||
@@ -157,9 +157,9 @@ class JetlinkModelState:
|
||||
# model manager key off this flag
|
||||
self.chestnut = True
|
||||
|
||||
# a warp.Warp, loaded and warmed ahead: its first call links the JIT, and
|
||||
# this runs on modeld's frame thread. Frames go out of its output as they
|
||||
# are, over either transport
|
||||
# a warp.Warp or the adapter's model_warp, loaded and warmed ahead: its
|
||||
# first call links the JIT, and this runs on modeld's frame thread. Frames
|
||||
# go out of its output as they are, over either transport
|
||||
self.warp = warp
|
||||
|
||||
self.input_shapes = spec.input_shapes
|
||||
@@ -305,9 +305,9 @@ class JetlinkModelState:
|
||||
# over the cable: how frames go, how many never came back, and what the
|
||||
# kernel dropped on the way in (softnet), since the link came up
|
||||
drops = getattr(self.client.t, 'net_drops', lambda: None)()
|
||||
cable = (f"; frames {'as datagrams' if self.client.t.datagrams else 'on the stream'}, "
|
||||
cable = (f"; frames {'as datagrams' if self.client.t.datagrams else 'on the stream'}, " +
|
||||
f"{self.client.frames_lost} lost, net drops {drops}" if drops is not None else '')
|
||||
self._log.warning("jetlink: frame %d warp %.1f data %.1f send %.1f wait %.1f ms%s; "
|
||||
self._log.warning("jetlink: frame %d warp %.1f data %.1f send %.1f wait %.1f ms%s; " +
|
||||
"server gpu %.1f queue %.1f total %.1f ms; ffs maxima prepare %.1f read_wait %.1f handoff %.1f ms%s",
|
||||
self._frame_id, (frame.t1 - frame.t0) * 1e3, (frame.t2 - frame.t1) * 1e3,
|
||||
(frame.sent - frame.t2) * 1e3, (t4 - waiting_from) * 1e3,
|
||||
|
||||
@@ -16,16 +16,25 @@ export, and jetlink's registry follows that to comma's model repo; see
|
||||
jetlink.registry.lfs.
|
||||
|
||||
Without a model manager (Keys.big_model and Keys.catalog None) there is no
|
||||
pick and no catalog, and the large model is jetlink's own default, named by its
|
||||
ref: its pointer resolves from the ref as any pick's does.
|
||||
pick and no catalog, and the large model is the one picked on the server, else
|
||||
jetlink's own default, named by its ref: its pointer resolves from the ref as
|
||||
any pick's does.
|
||||
|
||||
A fork with a picker of its own but no catalog jetlink reads (Keys.big_model
|
||||
without Keys.catalog, StarPilot's) may name its pick by the ONNX's sha256
|
||||
instead of a ref, which is all its catalog knows of a model. Its size is looked
|
||||
up from comma's LFS servers once (resolve_size). With no catalog no model
|
||||
manager owns the slot either, so jetlink writes a newer pick from the server
|
||||
into it itself, and the fork's picker shows what drives.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from jetlink.registry.catalog import DEFAULT_BIG_MODEL_NAME, DEFAULT_BIG_MODEL_REF
|
||||
from jetlink.registry.catalog import DEFAULT_BIG_MODEL_NAME, DEFAULT_BIG_MODEL_REF, is_ref, is_sha256
|
||||
|
||||
POINTER_TIMEOUT = 10.0
|
||||
_REF = re.compile(r'[0-9a-f]{40}')
|
||||
@@ -39,13 +48,32 @@ INDEX_TTL = 2.0
|
||||
# on from the .part the last left, so a stall costs the wait, not the bytes
|
||||
# (lfs.lfs_download)
|
||||
DOWNLOAD_RETRY_DELAYS = (5.0, 15.0, 30.0, 60.0, 120.0)
|
||||
# server_pick() before this process has heard a hello
|
||||
_UNHEARD = object()
|
||||
|
||||
|
||||
def _dated(pick) -> dict | None:
|
||||
"""A server's pick as its hello carries it, dated by this clock:
|
||||
{ref, sha256, nbytes, name, at}. None for none, or one naming no model."""
|
||||
if not isinstance(pick, dict) or not isinstance(pick.get('age_s'), (int, float)):
|
||||
return None
|
||||
ref = pick['ref'] if is_ref(pick.get('ref')) else None
|
||||
sha256 = pick['sha256'] if is_sha256(pick.get('sha256')) else None
|
||||
if ref is None and sha256 is None:
|
||||
return None
|
||||
return {'ref': ref, 'sha256': sha256, 'nbytes': pick.get('nbytes'), 'name': pick.get('name'),
|
||||
'at': time.time() - float(pick['age_s'])}
|
||||
|
||||
|
||||
class Models:
|
||||
def __init__(self, op):
|
||||
self.op = op
|
||||
self._index_cache: tuple[float, list[dict], dict[str, dict]] | None = None
|
||||
self._slot_cache: tuple[float, dict | None] | None = None
|
||||
self._slot_cache: tuple[float, dict | None, float | None] | None = None
|
||||
# the pick of the last hello this process heard, and the owner's record
|
||||
# of the last hello any process heard
|
||||
self._heard: dict | None | object = _UNHEARD
|
||||
self._record_cache: tuple[float, dict | None] | None = None
|
||||
|
||||
def catalog(self) -> list[dict]:
|
||||
"""sunnypilot's big-model bundles as {name, ref}, newest first, as the model
|
||||
@@ -77,13 +105,28 @@ class Models:
|
||||
|
||||
def resolve_pointer(self, ref: str) -> tuple[str, int]:
|
||||
"""The oid and size behind a catalog model, fetched the first time and kept for good."""
|
||||
return self._resolve(ref, self.fetch_pointer)
|
||||
|
||||
def fetch_size(self, sha256: str) -> tuple[str, int]:
|
||||
"""The ONNX with this hash and its byte count, from the LFS servers a
|
||||
download asks."""
|
||||
from jetlink.registry.lfs import fetch_size as registry_fetch_size
|
||||
pointer = registry_fetch_size(sha256, self.lfs_endpoints(), timeout=POINTER_TIMEOUT)
|
||||
return pointer.oid, pointer.size
|
||||
|
||||
def resolve_size(self, sha256: str) -> tuple[str, int]:
|
||||
"""A model picked by its hash, sized the first time and kept for good with
|
||||
the catalog's pointers, under the hash."""
|
||||
return self._resolve(sha256, self.fetch_size)
|
||||
|
||||
def _resolve(self, key: str, fetch) -> tuple[str, int]:
|
||||
known = self.pointers()
|
||||
if ref in known:
|
||||
return known[ref]['oid'], int(known[ref]['size'])
|
||||
oid, size = self.fetch_pointer(ref)
|
||||
known[ref] = {'oid': oid, 'size': size}
|
||||
if key in known:
|
||||
return known[key]['oid'], int(known[key]['size'])
|
||||
oid, size = fetch(key)
|
||||
known[key] = {'oid': oid, 'size': size}
|
||||
self._keep_pointers(known)
|
||||
self.op.log.warning("jetlink: %s is %s, %d MB", ref[:10], oid[:16], size >> 20)
|
||||
self.op.log.warning("jetlink: %s is %s, %d MB", key[:10], oid[:16], size >> 20)
|
||||
return oid, size
|
||||
|
||||
def _keep_pointers(self, known: dict[str, dict]) -> None:
|
||||
@@ -92,9 +135,11 @@ class Models:
|
||||
self._index_cache = None
|
||||
|
||||
@staticmethod
|
||||
def _row(name: str, ref: str, known: dict[str, dict]) -> dict:
|
||||
p = known.get(ref) or {}
|
||||
return {'name': name, 'ref': ref, 'oid': p.get('oid'), 'size': int(p['size']) if p.get('size') else None}
|
||||
def _row(name: str, ref: str | None, known: dict[str, dict], sha256: str | None = None) -> dict:
|
||||
"""A model as a catalog row, by its ref's pointer, else by its hash, sized
|
||||
once resolve_size has looked it up."""
|
||||
p = known.get(ref) or known.get(sha256) or {}
|
||||
return {'name': name, 'ref': ref, 'oid': p.get('oid') or sha256, 'size': int(p['size']) if p.get('size') else None}
|
||||
|
||||
def _index(self) -> tuple[list[dict], dict[str, dict]]:
|
||||
"""The catalog's rows and the pointers behind them, read together once per INDEX_TTL."""
|
||||
@@ -110,18 +155,27 @@ class Models:
|
||||
return self._index()[0]
|
||||
|
||||
def selected_slot(self) -> dict | None:
|
||||
"""The big-model slot's pick as {name, ref}, from the raw param rather than a
|
||||
parsed bundle, and memoised: the UI asks every frame."""
|
||||
"""The big-model slot's pick as {name, ref, sha256}, from the raw param
|
||||
rather than a parsed bundle, and memoised with when it was made
|
||||
(picked_at): the UI asks every frame. It names its model by the catalog's
|
||||
ref, by the ONNX's sha256 (a fork whose catalog knows only that), or by
|
||||
both; the one it lacks is None."""
|
||||
now = time.monotonic()
|
||||
if self._slot_cache is not None and now - self._slot_cache[0] < INDEX_TTL:
|
||||
return self._slot_cache[1]
|
||||
key = self.op.keys.big_model
|
||||
slot = self.op.get(key) if key else None
|
||||
ref = slot.get('ref') if isinstance(slot, dict) else None
|
||||
pick = None
|
||||
if isinstance(ref, str) and ref:
|
||||
pick = {'name': str(slot.get('displayName') or ref[:10]), 'ref': ref}
|
||||
self._slot_cache = (now, pick)
|
||||
slot = slot if isinstance(slot, dict) else {}
|
||||
ref = slot['ref'] if isinstance(slot.get('ref'), str) and slot['ref'] else None
|
||||
sha256 = slot['sha256'] if is_sha256(slot.get('sha256')) else None
|
||||
pick, at = None, None
|
||||
if ref or sha256:
|
||||
pick = {'name': str(slot.get('displayName') or (ref or sha256)[:10]), 'ref': ref, 'sha256': sha256}
|
||||
try:
|
||||
at = os.stat(Path(self.op.params_dir()) / key).st_mtime
|
||||
except OSError:
|
||||
pass
|
||||
self._slot_cache = (now, pick, at)
|
||||
return pick
|
||||
|
||||
def default_model(self) -> dict | None:
|
||||
@@ -142,12 +196,129 @@ class Models:
|
||||
return _TRAILING_DATE.sub('', model['name']) if model else None
|
||||
|
||||
def selected_model(self) -> dict | None:
|
||||
"""The slot's pick, listed in the catalog or not: a ref is enough to find its
|
||||
pointer. With no pick, default_model()."""
|
||||
if (pick := self.selected_slot()) is not None:
|
||||
return self._row(pick['name'], pick['ref'], self._index()[1])
|
||||
"""The model picked last, here or on the server: the slot's pick, listed
|
||||
in the catalog or not (a ref is enough to find its pointer), unless the
|
||||
server's last hello named a newer one. With neither, default_model()."""
|
||||
if (theirs := self._newer_server_pick()) is not None:
|
||||
return self._server_row(theirs)
|
||||
if (own := self.selected_slot()) is not None:
|
||||
return self._own_row(own)
|
||||
return self.default_model()
|
||||
|
||||
def _own_row(self, own: dict) -> dict:
|
||||
return self._row(own['name'], own['ref'], self._index()[1], own['sha256'])
|
||||
|
||||
# -- picks on both ends -----------------------------------------------------
|
||||
# A pick is made on either end: in the comma's list, or on the server (its
|
||||
# web page, an app's Use Model). Each hello carries the comma's own pick and
|
||||
# how long ago it was made, and the server answers with the newer of that
|
||||
# and its own (PickStore in JetlinkServer). The answer is kept with the rest
|
||||
# of the hello (gadget.SERVER, through the owner), dated by this clock, and
|
||||
# selected_model() takes whichever pick is newer; so a model picked on the
|
||||
# server drives without being picked here too, and one picked here goes to
|
||||
# the server at the next hello.
|
||||
|
||||
def picked_at(self) -> float | None:
|
||||
"""When the slot was last written, by this clock: when the pick was made.
|
||||
None with no pick of our own, or no date for it."""
|
||||
return self._slot_cache[2] if self.selected_slot() is not None and self._slot_cache else None
|
||||
|
||||
def hello_pick(self) -> dict | None:
|
||||
"""Our own pick, for the hello: {ref, sha256, nbytes, name, age_s}, the
|
||||
pointer where it is resolved already (no network here). None with nothing
|
||||
picked here, or no date for it: the server's pick, if any, stands."""
|
||||
own, at = self.selected_slot(), self.picked_at()
|
||||
if own is None or at is None:
|
||||
return None
|
||||
row = self._own_row(own)
|
||||
return {'ref': row['ref'], 'sha256': row['oid'], 'nbytes': row['size'], 'name': row['name'],
|
||||
'age_s': round(max(0.0, time.time() - at), 1)}
|
||||
|
||||
def heard(self, hello: dict) -> dict:
|
||||
"""The server's hello with its `pick` dated by this clock (_dated), which
|
||||
is how the owner keeps it (lending.SERVER_FIELDS) and server_pick() reads
|
||||
it. This process goes by it at once, whatever the owner does with it."""
|
||||
self._heard = _dated(hello.get('pick'))
|
||||
return {**hello, 'pick': self._heard} if self._heard is not None else hello
|
||||
|
||||
def server_pick(self) -> dict | None:
|
||||
"""The pick the server last answered a hello with, {ref, sha256, nbytes,
|
||||
name, at}, or None: a server too old to say, or one with no pick. What
|
||||
this process heard last, else the owner's record of the last hello: the
|
||||
UI says no hello, and reads it every frame."""
|
||||
if self._heard is not _UNHEARD:
|
||||
return self._heard
|
||||
now = time.monotonic()
|
||||
if self._record_cache is None or now - self._record_cache[0] >= INDEX_TTL:
|
||||
from jetlink.comma import gadget
|
||||
pick = (gadget.read_record(gadget.SERVER) or {}).get('pick')
|
||||
self._record_cache = (now, pick if isinstance(pick, dict) and isinstance(pick.get('at'), (int, float)) else None)
|
||||
return self._record_cache[1]
|
||||
|
||||
def _newer_server_pick(self) -> dict | None:
|
||||
"""The server's last pick, when it is another model than ours and was
|
||||
picked after it, or we have none."""
|
||||
theirs, own = self.server_pick(), self.selected_slot()
|
||||
if theirs is None or (own is not None and (self._same(theirs, self._own_row(own)) or
|
||||
theirs['at'] <= (self.picked_at() or 0.0))):
|
||||
return None
|
||||
return theirs
|
||||
|
||||
@staticmethod
|
||||
def _same(pick: dict, row: dict) -> bool:
|
||||
"""One model: by the catalog ref where both have one, since two refs can
|
||||
share one export, else by the hash. The server's PickStore.sameModel."""
|
||||
if pick['ref'] and row['ref']:
|
||||
return pick['ref'] == row['ref']
|
||||
return bool(pick['sha256']) and pick['sha256'] == row['oid']
|
||||
|
||||
def _server_row(self, pick: dict) -> dict:
|
||||
"""The server's pick as a catalog row: ours where we list it, with the
|
||||
server's oid and size, which name the bytes it has."""
|
||||
listed = next((m for m in self.model_index() if pick['ref'] and m['ref'] == pick['ref']), None)
|
||||
row = self._row(listed['name'] if listed else pick.get('name') or (pick['ref'] or pick['sha256'])[:16],
|
||||
pick['ref'], self._index()[1], pick['sha256'])
|
||||
if pick['sha256'] and pick.get('nbytes'):
|
||||
row['oid'], row['size'] = pick['sha256'], int(pick['nbytes'])
|
||||
return row
|
||||
|
||||
def adopt(self) -> None:
|
||||
"""Keep the server's pointer for its pick, by ref and by hash, so naming
|
||||
the model here needs no lookup; and make a newer pick from the server
|
||||
ours. With a catalog, ask the model manager for it (Keys.select), so the
|
||||
comma's own list shows what drives: only a model the catalog lists, which
|
||||
is all the model manager takes; any other drives all the same
|
||||
(selected_model). Without one, no model manager owns the slot, so the
|
||||
pick is written into it, whatever the model. Never raises."""
|
||||
try:
|
||||
if (theirs := self.server_pick()) is None:
|
||||
return
|
||||
known = self.pointers()
|
||||
if theirs['sha256'] and theirs.get('nbytes'):
|
||||
pointer = {'oid': theirs['sha256'], 'size': int(theirs['nbytes'])}
|
||||
missing = {k: pointer for k in (theirs['ref'], theirs['sha256']) if k and k not in known}
|
||||
if missing:
|
||||
self._keep_pointers({**known, **missing})
|
||||
if self._newer_server_pick() is None:
|
||||
return
|
||||
model, keys = self._server_row(theirs), self.op.keys
|
||||
if keys.catalog is None:
|
||||
if keys.big_model:
|
||||
self.op.log.warning("jetlink: %s was picked on the server, picking it here too", model['name'])
|
||||
slot = {k: v for k, v in (('displayName', model['name']), ('ref', model['ref']), ('sha256', model['oid'])) if v}
|
||||
self.op.put(keys.big_model, slot, block=True)
|
||||
self._slot_cache = None
|
||||
return
|
||||
# a download the user asked for that the model manager has not taken
|
||||
# yet is left alone: the next hello asks again
|
||||
key = keys.select
|
||||
if key and model['ref'] and self.op.get(key) is None and any(m['ref'] == model['ref'] for m in self.model_index()):
|
||||
self.op.log.warning("jetlink: %s was picked on the server, picking it here too", model['name'])
|
||||
self.op.put(key, model['ref'])
|
||||
self._slot_cache = None
|
||||
except Exception:
|
||||
self.op.log.exception("jetlink: could not take the server's pick")
|
||||
|
||||
def selected_model_name(self) -> str | None:
|
||||
selected = self.selected_model()
|
||||
return selected['name'] if selected else None
|
||||
@@ -227,11 +398,8 @@ class Models:
|
||||
|
||||
def _fetch_once(self, pointer, dest: Path, progress, should_stop) -> Path:
|
||||
from jetlink.registry.catalog import NetworkError
|
||||
from jetlink.registry.lfs import lfs_download, lfs_resolve
|
||||
for endpoint in self.lfs_endpoints():
|
||||
href = lfs_resolve(endpoint, pointer)
|
||||
if href is None:
|
||||
continue
|
||||
from jetlink.registry.lfs import lfs_download, lfs_hrefs
|
||||
for endpoint, href in lfs_hrefs(self.lfs_endpoints(), pointer):
|
||||
self.op.log.warning("jetlink: fetching the large model (%d MB) from %s", pointer.size >> 20, endpoint)
|
||||
return lfs_download(href, pointer, dest, progress=progress, should_stop=should_stop)
|
||||
raise NetworkError(f"no LFS server has {pointer.oid[:16]}")
|
||||
|
||||
@@ -126,10 +126,8 @@ class ProvisioningRun:
|
||||
return path
|
||||
|
||||
def needs_download(self) -> bool:
|
||||
"""Does the comma have to fetch the picked model before a Jetson can build
|
||||
it? Only while its engine is not known to be built and the file is not here.
|
||||
Asked without the Jetson: a download needs only the internet, and a car
|
||||
whose Jetson goes off with the ignition has no Jetson while parked."""
|
||||
"""Could the Jetson need the picked model from the comma? Only while its
|
||||
engine is not known to be built and the file is not here."""
|
||||
parts = self.parts
|
||||
entry = parts.models.selected_model()
|
||||
if entry is None:
|
||||
@@ -140,12 +138,17 @@ class ProvisioningRun:
|
||||
return parts.models.shipped_model_path() is None
|
||||
|
||||
def provision(self) -> bool:
|
||||
"""Make the Jetson ready for the selected model; a host is attached. run()
|
||||
has fetched the file already unless the spec record said the model was
|
||||
built; a server that lost it since asks for the bytes, and they are
|
||||
fetched then."""
|
||||
"""Make the Jetson ready for the picked model; a host is attached. The
|
||||
hello settles which pick that is, the comma's or a newer one made on the
|
||||
server. A server without the model downloads it itself; only one that
|
||||
cannot (no internet, or too old) gets the bytes from the comma, fetched
|
||||
here then."""
|
||||
from jetlink.client import EngineMissing
|
||||
parts = self.parts
|
||||
parts.progress.report('connect', 0.0, 'connecting', code='connecting')
|
||||
hello = self.hello()
|
||||
self.log.warning("jetlink: server %s trt %s", hello.get('device'), hello.get('trt_version'))
|
||||
parts.models.adopt()
|
||||
entry = parts.models.selected_model()
|
||||
if entry is None:
|
||||
# no catalog yet; not an error
|
||||
@@ -154,27 +157,24 @@ class ProvisioningRun:
|
||||
return False
|
||||
sha256, nbytes = link.identity(parts, entry)
|
||||
|
||||
# only needed if the server turns out not to have this model; None is a
|
||||
# legitimate state here, see EngineMissing below
|
||||
# only needed if the server turns out not to have this model and cannot
|
||||
# get it; None is a legitimate state here, see EngineMissing below
|
||||
model_path = parts.models.shipped_model_path()
|
||||
|
||||
self.log.warning("jetlink: provisioning %s (%d MB, sha %s)",
|
||||
entry.get('name', sha256[:16]), nbytes >> 20, sha256[:16])
|
||||
parts.progress.report('connect', 0.0, 'connecting', code='connecting')
|
||||
|
||||
def ensure(path):
|
||||
return link.ensure(parts, self.client, sha256, nbytes, path, progress=parts.progress.report_with_eta,
|
||||
should_stop=lambda: self.stop)
|
||||
should_stop=lambda: self.stop, ref=entry.get('ref'))
|
||||
|
||||
hello = self.hello()
|
||||
self.log.warning("jetlink: server %s trt %s", hello.get('device'), hello.get('trt_version'))
|
||||
try:
|
||||
spec = ensure(model_path)
|
||||
except EngineMissing:
|
||||
# the server has nothing to build from, and neither have we: fetch the
|
||||
# model and hand it over in this run. The owner lends the link for the
|
||||
# whole run; leaving after the download would put the build a
|
||||
# WORKER_BACKOFF later
|
||||
# the server has nothing to build from and could not download it, and
|
||||
# neither have we: fetch the model and hand it over in this run. The
|
||||
# owner lends the link for the whole run; leaving after the download
|
||||
# would put the build a WORKER_BACKOFF later
|
||||
if model_path is not None or (model_path := self.fetch_model()) is None:
|
||||
raise
|
||||
# a download of minutes can outlast the session; a hello starts a new one
|
||||
@@ -186,16 +186,35 @@ class ProvisioningRun:
|
||||
return True
|
||||
|
||||
def hello(self) -> dict:
|
||||
"""Say hello, and pass the answer on to the owner, which needs its
|
||||
sleep_after to decide whether letting go of the gadget is worth what it
|
||||
costs and never speaks the protocol to learn it."""
|
||||
hello = self.client.hello(timeout=10.0)
|
||||
"""Say hello with our pick, and pass the answer on to the owner, which
|
||||
needs its sleep_after to decide whether letting go of the gadget is worth
|
||||
what it costs, never speaks the protocol to learn it, and keeps the
|
||||
server's pick and whether it downloads for the comma (Models.heard)."""
|
||||
models = self.parts.models
|
||||
hello = models.heard(self.client.hello(timeout=10.0, pick=models.hello_pick()))
|
||||
if self.loan is not None:
|
||||
self.loan.note_server(hello)
|
||||
return hello
|
||||
|
||||
# -- the parked car -------------------------------------------------------
|
||||
|
||||
def without_jetson(self) -> None:
|
||||
"""No Jetson came to the bind: a car whose Jetson is switched with the
|
||||
ignition has none while parked. Only the Jetson needs the model, and one
|
||||
that can download it gets it itself when it is on; the comma fetches it
|
||||
now only for a Jetson that cannot (no internet when it was last here, or
|
||||
too old to say), so the build can start the moment it is."""
|
||||
if not self.has_work():
|
||||
return
|
||||
if self.needs_download():
|
||||
if server_fetches():
|
||||
self.parts.progress.report('waiting', 0.0, 'the jetson downloads it when it is on', code='waiting_for_host')
|
||||
return
|
||||
if self.fetch_model() is None:
|
||||
return
|
||||
self.parts.progress.report('waiting', 0.0, 'downloaded, builds when the jetson is on',
|
||||
code='downloaded_waiting_host')
|
||||
|
||||
def has_work(self) -> bool:
|
||||
"""Is there a reason to wake the Jetson? Only things the link can fix count."""
|
||||
spec = self.parts.spec.load()
|
||||
@@ -259,25 +278,21 @@ class ProvisioningRun:
|
||||
self.log.warning("jetlink: no warp built for this camera, nothing to provision for")
|
||||
return True
|
||||
|
||||
if not self.has_work():
|
||||
# Nothing to build, unless a model was picked on the server since: a
|
||||
# Jetson awake on the bus is asked, which costs a hello and no wake. Not
|
||||
# a phone's cable, which is there only once the app dials in
|
||||
if not self.has_work() and not (gadget.host_attached() and gadget.link_kind(self.parts.settings.mode()) == 'usb'):
|
||||
self.log.warning("jetlink: nothing to provision")
|
||||
return True
|
||||
|
||||
finished = False
|
||||
try:
|
||||
# the bytes first while no host is on the cable: a parked car whose
|
||||
# Jetson is switched with the ignition never has one to wait for. A host
|
||||
# that is there is asked first, since one with the model built needs
|
||||
# nothing from the comma; provision() fetches it if the host has not
|
||||
if self.needs_download() and not gadget.host_attached() and self.fetch_model() is None:
|
||||
return False
|
||||
if not self.open_link():
|
||||
return False
|
||||
if not self.wait_for_jetson():
|
||||
self.log.warning("jetlink: no jetson within %.0f s, leaving it for the next run", WAKE_TIMEOUT)
|
||||
if self.parts.models.shipped_model_path() is not None:
|
||||
self.parts.progress.report('waiting', 0.0, 'downloaded, builds when the jetson is on',
|
||||
code='downloaded_waiting_host')
|
||||
self.close_link()
|
||||
self.without_jetson()
|
||||
return False
|
||||
finished = self.provision()
|
||||
except Exception:
|
||||
@@ -288,6 +303,12 @@ class ProvisioningRun:
|
||||
return finished
|
||||
|
||||
|
||||
def server_fetches() -> bool:
|
||||
"""Did the server the comma last spoke to say it downloads a model itself
|
||||
(its hello's `fetches`, as the owner keeps it)? No for one too old to say."""
|
||||
return (gadget.read_record(gadget.SERVER) or {}).get('fetches') is True
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> None:
|
||||
"""One provisioning round over the fork's adapter, as the owner starts it.
|
||||
Exits 0 when nothing is left to do, and 1 when the owner should try again."""
|
||||
|
||||
@@ -31,14 +31,17 @@ PROGRESS_MIN_INTERVAL = 0.25
|
||||
|
||||
# what a progress record's `code` may be, for a fork that words the line
|
||||
# itself; `msg` stays the English one. `args`, where a code has them:
|
||||
# downloading and upload {got_mb, total_mb}, download_stalled {retry_s},
|
||||
# reconnecting {why: lost, behind, load_failed or bad_output}, build {eta_s}.
|
||||
# From upload on they are stage names, and the server's stages are its own:
|
||||
# downloading, download and upload {got_mb, total_mb}, download_stalled
|
||||
# {retry_s}, reconnecting {why: lost, behind, load_failed or bad_output},
|
||||
# build {eta_s}. downloading is the comma fetching the model, download the
|
||||
# server fetching it itself. From download on they are stage names, and the
|
||||
# server's stages are its own:
|
||||
# a server can add one, so a fork falls back to `msg` for a code it does not
|
||||
# know. A server message with no stage has no code
|
||||
PROGRESS_CODES = ('waiting_for_host', 'finding_model', 'model_missing', 'ready_waiting_window', 'reconnecting',
|
||||
'connecting', 'downloading', 'download_stalled', 'download_failed', 'downloaded_waiting_host',
|
||||
'engine_ready', 'failed', 'upload', 'patch', 'parse', 'convert', 'compile', 'build', 'load', 'warm')
|
||||
'engine_ready', 'failed', 'download', 'upload', 'patch', 'parse', 'convert', 'compile', 'build',
|
||||
'load', 'warm')
|
||||
|
||||
|
||||
def estimated_build_seconds(size: int) -> int:
|
||||
@@ -104,12 +107,15 @@ class Progress:
|
||||
size. Only the build: the upload reports MB of MB and a connect has nothing
|
||||
to predict."""
|
||||
args = None
|
||||
size = self._size() if self._size is not None and stage in ('build', 'upload') else None
|
||||
size = self._size() if self._size is not None and stage in ('build', 'upload', 'download') else None
|
||||
if size and stage == 'build':
|
||||
left = estimated_build_seconds(size) * max(0.0, 1.0 - frac)
|
||||
msg, args = _eta(left), {'eta_s': round(left)}
|
||||
elif size:
|
||||
args = {'got_mb': int(frac * size) >> 20, 'total_mb': size >> 20}
|
||||
if stage == 'download' and args:
|
||||
# the server downloading the model itself: not the comma's own 'downloading'
|
||||
msg = f"jetson downloading {args['got_mb']} of {args['total_mb']} MB"
|
||||
self.report(stage, frac, msg, code=stage or None, args=args)
|
||||
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ class Warp:
|
||||
out = jit.captured.ret.uop.base.buffer
|
||||
self._device = out.device
|
||||
if self._device.startswith('QCOM'):
|
||||
_make_coherent(out)
|
||||
make_coherent(out)
|
||||
# the two buffers every frame passes, written in place by start()
|
||||
self._tensors = {name: Tensor(np.zeros(shape, dtype=dtype), device=device).realize()
|
||||
for name, (shape, dtype, device) in specs.items()}
|
||||
@@ -191,10 +191,15 @@ class Warp:
|
||||
self._replay(self._inputs, {})
|
||||
|
||||
|
||||
def _make_coherent(out) -> None:
|
||||
"""Move a QCOM buffer to KGSL memory flagged IOCOHERENT and write-back.
|
||||
Raises if the kernel keeps either flag back (the allocation's record holds
|
||||
its answer): write-back memory that is not coherent reads stale frames."""
|
||||
def make_coherent(out) -> None:
|
||||
"""Move a QCOM buffer to KGSL memory flagged IOCOHERENT and write-back, so
|
||||
the CPU reads it through its cache once the GPU is done (see Warp). Raises
|
||||
if the kernel keeps either flag back (the allocation's record holds its
|
||||
answer): write-back memory that is not coherent reads stale frames.
|
||||
|
||||
For an adapter's model_warp too. Move a buffer before the first call of
|
||||
the JIT that writes it: that call links its address into the JIT, which
|
||||
would keep writing the memory this frees."""
|
||||
from tinygrad.device import Device
|
||||
dev = Device[out.device]
|
||||
mem = dev._gpu_alloc(out.nbytes, flags=COHERENT_WRITEBACK)
|
||||
|
||||
@@ -294,6 +294,36 @@ def lfs_resolve(endpoint: str, pointer: Pointer, timeout: float = CONNECT_TIMEOU
|
||||
return None
|
||||
|
||||
|
||||
def lfs_hrefs(endpoints, pointer: Pointer, timeout: float = CONNECT_TIMEOUT, opener=None):
|
||||
"""Each of `endpoints` that has the object, in order, with its download href."""
|
||||
for endpoint in endpoints:
|
||||
href = lfs_resolve(endpoint, pointer, timeout=timeout, opener=opener)
|
||||
if href is not None:
|
||||
yield endpoint, href
|
||||
|
||||
|
||||
def fetch_size(oid: str, endpoints=LFS_ENDPOINTS, timeout: float = POINTER_TIMEOUT, opener=None) -> Pointer:
|
||||
"""The pointer for an ONNX known by its oid alone, as a fork whose catalog
|
||||
names a model by its hash knows it (StarPilot's). comma's LFS servers hand
|
||||
out a download address for any oid they hold, whatever size the batch asks
|
||||
for, and the first byte of that download says how many there are. No
|
||||
server with it is NotFound; one that would not say, a NetworkError."""
|
||||
opener = opener or urllib.request.urlopen
|
||||
for endpoint, href in lfs_hrefs(endpoints, Pointer(oid, 1), timeout=timeout, opener=opener):
|
||||
request = urllib.request.Request(href, headers={'Range': 'bytes=0-0'})
|
||||
try:
|
||||
with opener(request, timeout=timeout) as response:
|
||||
# a server that ignores the range answers 200 with the whole length
|
||||
total = (response.headers.get('Content-Range') or '').rpartition('/')[2] \
|
||||
if response.status == 206 else response.headers.get('Content-Length')
|
||||
except (OSError, ValueError) as e:
|
||||
raise NetworkError(f"could not size {oid[:16]} at {endpoint}: {e}") from e
|
||||
if not str(total).isdigit() or int(total) <= 0:
|
||||
raise NetworkError(f"{endpoint} did not say how big {oid[:16]} is")
|
||||
return Pointer(oid, int(total))
|
||||
raise NotFound(f"no LFS server has {oid[:16]}")
|
||||
|
||||
|
||||
def lfs_download(href: str, pointer: Pointer, dest: Path, progress: ProgressFn | None = None,
|
||||
should_stop: StopFn | None = None, opener=None) -> Path:
|
||||
"""Stream to a .part file, hashing as we go, and only then take the name.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
exec "${ROOT_DIR}/scripts/host_tool_runner.sh" jotpluggler "$@"
|
||||
+15
-1
@@ -1,5 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Device Params must not depend on the platform used to compile native libraries.
|
||||
if [ -f /AGNOS ] && [ -z "${PARAMS_ROOT:-}" ]; then
|
||||
export PARAMS_ROOT="/data/params"
|
||||
fi
|
||||
|
||||
export OMP_NUM_THREADS=1
|
||||
export MKL_NUM_THREADS=1
|
||||
export NUMEXPR_NUM_THREADS=1
|
||||
@@ -21,9 +26,18 @@ fi
|
||||
export QCOM_PRIORITY=12
|
||||
|
||||
if [ -z "$AGNOS_VERSION" ]; then
|
||||
export AGNOS_VERSION="19.8.1"
|
||||
export AGNOS_VERSION="20.2.1"
|
||||
fi
|
||||
|
||||
export AGNOS_UPDATE_POLICY="auto"
|
||||
# C3 selects its complete matching firmware chain in the AGNOS manifest loader.
|
||||
# Unknown hardware retains its installed OS.
|
||||
if [ -f /AGNOS ] || [ -e /sys/firmware/devicetree/base/model ]; then
|
||||
case "$(tr -d '\0' 2>/dev/null < /sys/firmware/devicetree/base/model)" in
|
||||
"comma tici"|"comma tizi"|"comma mici") ;;
|
||||
*) export AGNOS_UPDATE_POLICY="retain" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
export STAGING_ROOT="/data/safe_staging"
|
||||
|
||||
|
||||
@@ -291,6 +291,11 @@ struct MapdOut @0xa4f1eb3323f5f582 {
|
||||
computedMonoTime @31 :UInt64; # Computation boot-time nanoseconds, not send time.
|
||||
sourceGeneration @32 :UInt64;
|
||||
producerSession @33 :UInt64; # Random nonzero process session.
|
||||
# Data-only preview from the same qualified GPS fix; no controller smoothing.
|
||||
previewSpeedLimit @34 :Float32;
|
||||
previewDistance @35 :Float32;
|
||||
previewBoundaryId @36 :Text;
|
||||
previewWayId @37 :Int64;
|
||||
|
||||
enum SampleStatus {
|
||||
unknown @0;
|
||||
|
||||
@@ -2887,12 +2887,12 @@ type MapdOut capnp.Struct
|
||||
const MapdOut_TypeID = 0xa4f1eb3323f5f582
|
||||
|
||||
func NewMapdOut(s *capnp.Segment) (MapdOut, error) {
|
||||
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 112, PointerCount: 6})
|
||||
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 128, PointerCount: 7})
|
||||
return MapdOut(st), err
|
||||
}
|
||||
|
||||
func NewRootMapdOut(s *capnp.Segment) (MapdOut, error) {
|
||||
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 112, PointerCount: 6})
|
||||
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 128, PointerCount: 7})
|
||||
return MapdOut(st), err
|
||||
}
|
||||
|
||||
@@ -3260,12 +3260,54 @@ func (s MapdOut) SetProducerSession(v uint64) {
|
||||
capnp.Struct(s).SetUint64(104, v)
|
||||
}
|
||||
|
||||
func (s MapdOut) PreviewSpeedLimit() float32 {
|
||||
return math.Float32frombits(capnp.Struct(s).Uint32(76))
|
||||
}
|
||||
|
||||
func (s MapdOut) SetPreviewSpeedLimit(v float32) {
|
||||
capnp.Struct(s).SetUint32(76, math.Float32bits(v))
|
||||
}
|
||||
|
||||
func (s MapdOut) PreviewDistance() float32 {
|
||||
return math.Float32frombits(capnp.Struct(s).Uint32(112))
|
||||
}
|
||||
|
||||
func (s MapdOut) SetPreviewDistance(v float32) {
|
||||
capnp.Struct(s).SetUint32(112, math.Float32bits(v))
|
||||
}
|
||||
|
||||
func (s MapdOut) PreviewBoundaryId() (string, error) {
|
||||
p, err := capnp.Struct(s).Ptr(6)
|
||||
return p.Text(), err
|
||||
}
|
||||
|
||||
func (s MapdOut) HasPreviewBoundaryId() bool {
|
||||
return capnp.Struct(s).HasPtr(6)
|
||||
}
|
||||
|
||||
func (s MapdOut) PreviewBoundaryIdBytes() ([]byte, error) {
|
||||
p, err := capnp.Struct(s).Ptr(6)
|
||||
return p.TextBytes(), err
|
||||
}
|
||||
|
||||
func (s MapdOut) SetPreviewBoundaryId(v string) error {
|
||||
return capnp.Struct(s).SetText(6, v)
|
||||
}
|
||||
|
||||
func (s MapdOut) PreviewWayId() int64 {
|
||||
return int64(capnp.Struct(s).Uint64(120))
|
||||
}
|
||||
|
||||
func (s MapdOut) SetPreviewWayId(v int64) {
|
||||
capnp.Struct(s).SetUint64(120, uint64(v))
|
||||
}
|
||||
|
||||
// MapdOut_List is a list of MapdOut.
|
||||
type MapdOut_List = capnp.StructList[MapdOut]
|
||||
|
||||
// NewMapdOut creates a new list of MapdOut.
|
||||
func NewMapdOut_List(s *capnp.Segment, sz int32) (MapdOut_List, error) {
|
||||
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 112, PointerCount: 6}, sz)
|
||||
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 128, PointerCount: 7}, sz)
|
||||
return capnp.StructList[MapdOut](l), err
|
||||
}
|
||||
|
||||
@@ -3395,312 +3437,318 @@ func NewMapdOut_GpsSource_List(s *capnp.Segment, sz int32) (MapdOut_GpsSource_Li
|
||||
return capnp.NewEnumList[MapdOut_GpsSource](s, sz)
|
||||
}
|
||||
|
||||
const schema_b526ba661d550a59 = "x\xda\x94Z}lT\xd7\x95?\xe7\xbd\xb1\xc7\x9f\x8c" +
|
||||
"\x877\x04\x0c&\x13(\xd0\x98\xc6i\x8c\xf16\xa6I" +
|
||||
"]c\x03\xc1\x1a\x83\xc7\xc3G@A\xea\xf5\xbc\xeb\x99" +
|
||||
"\x07\xcf\xefM\xde{c{\x10\x11\x84\x82\x04YP\x93" +
|
||||
"n\xd2\x92*lC\x02R\xda\x0d-\xed\x86\x15\x1bQ" +
|
||||
"-AD\"Y\x90\x92\xaa\xa9\x9a4\xbbI\xa3JI" +
|
||||
"\xba\x89\xd2v\x13m\xb6\xdb\xe8\xad\xce\xbd\xf3\xf1\xfc\xd1" +
|
||||
"\x10\xff\xc3\xf8\xfd\xcey\xe7\x9e{\xee\xb9\xe7\x9e\xdf}" +
|
||||
"\xdc\xf1\xf5\xfao\x86\xda\x1b3\x0d\xa0$\x1f\xae\xaa\xf6" +
|
||||
"\xa3{v\xbccy?9\x00\xd1E\xe8\xef\xa8\xdb\xba" +
|
||||
"x\xe4\xf9\x15\xe7!\x14\x06\xe8h\xae\xde\x89Z[u" +
|
||||
"\x18T\xff\xa7\x1f'\xbav\xfe\xf1\xf2\x83\x93\xb5\xaa\x84" +
|
||||
"Zm\xf5^\xd4\x16W\x87\x01\xb4\xe6\xeaw\x01\xfd{" +
|
||||
"{/\x7fgx\xc5\x0f\x0fCt\x91RQ\x06\xec\xc0" +
|
||||
"p?j\xf3\xc2\xf4R4l\x87\x00\xfd\xb6\x97\xf2\xf7" +
|
||||
"\xfe\xb8\xf9\xf8#\x90l\xc3Z\xff\xb2\xf5o\xf5\xdb\xae" +
|
||||
"\xdc\xf7\xdfP\x85\xa4s\xbc\xfe!\xd4\xce\xd4\xd3\x9f\xa7" +
|
||||
"\xea\xdfR\x00\xfd\x7f\xfc\xcaGyc\xef\xce\xefA\xf4" +
|
||||
"K\x8a\x7f\xf0\x93O\xbe\xd4\xf1_\x7f:M\x96_\x9f" +
|
||||
"s\x10\xb5\x0f\xe7\x90\x0f\xef\xcf\xf9>\xa0?\xf79\xcc" +
|
||||
"d\xae^\x7fb\x86iuE\x86Q\x1b\x88\xd0\xb4\xda" +
|
||||
"~\x1bW\xfb\xc2\xd9S3h\xb5Fv\xa2v\xb7\xd0" +
|
||||
"2\xd5\x8eo~:\\\xfd4$\x17\xe1\xb4\xd97\x93" +
|
||||
"^[D\xbe\xb2\x1d\x01+~%\x17\xe1\x9c\x80:E" +
|
||||
"\xa8\xe3d\xd3B\xd4\xce6}\x19\xa0\xe3\xa5\xa6\xf10" +
|
||||
"\xa0\xdf\xd7{\xe9\xe5\xb3o=}fZ\xb4\x92\xcd\x07" +
|
||||
"Q\xe3\xcd4'\xd6\xfc5@\x7f\xeb\xe9\xe4\xeb\xb7\x8d" +
|
||||
"]?3\x83\xb7\xbcy'j\x85f\xf2\xf6'\xbfM" +
|
||||
"?\xf6\xf7\xcf|\xf4O\xd3\xecmm^S\xb1\xb7\x19" +
|
||||
"\xd0\xdfu\xfe\xdd\xf6\xef|\xf6\xd6Of\xb0\x97'{" +
|
||||
"G\x85\xbd\xee\xb7\xfe\xf0\xc0\xc0-\xdb\xcf\xcd8\xeap" +
|
||||
"i\xd4\xab\x98Xb\xa4\xad\x0b3hm%[\x86\xd0" +
|
||||
"\xca^\xfc\x87GS_\xef\xfb\xc5\x0cZ\xeb\xc8\xd6\x0e" +
|
||||
"\xa1uH]\xfd6\xbfk\xd9\xe5\x19\xb4:Ik\xa3" +
|
||||
"\xd0\xfa\xe8\x9f\xbf\xf1\xea\x9dw\xef\xbe:uU\x14\xd2" +
|
||||
"[\xde<\x17\xb5N\x9akG{s\x9cVe\xcd\x8e" +
|
||||
"\xed9\xf3\xb5\x1f\xfe\xfbLC/\xa4\xa1\x17\x92\xd1W" +
|
||||
"\x0e>\x99\xf9\xbf7\xbew}\xa6\xa1Ik\xa3\xd0Z" +
|
||||
"|%\xfd\xe6\x9b\xde\xc1W\xa7\x85x\xf9\xc2\xb5\xa8u" +
|
||||
".\x14\xc3.\x14\xc9\xd0\xd5u\xf1\xd4\xb5\x87\xfe\xe7\xd7" +
|
||||
"\xe4\xa5:\xc5\xe2\x92E\xfd\xa8u.\xa2\x05i_D" +
|
||||
"\x1b\xa73\xd1\xf7\xed\x13\xdb\xbf\xf7\xc6\x0c\xa3\xcfk\xd9" +
|
||||
"\x89Zk\x0b\x8d\xde\xf5\xed\x97\x7f\xf0\xfd\xaa\xef\xff'" +
|
||||
"\x99T\xa6\xa8U\xb5\xacEm^\x0b\x99\x8c\xb6\x8c\x03" +
|
||||
"\xfa\xb7\xbe\xfa\xeb\x8fn[\xf7\xdc\x9f\xa7\xed\x98|\xcb" +
|
||||
"n\xd4\x8e\x0a\xcd\xc3-\x19\xc0\xca\xe6\x9blVn\xc5" +
|
||||
"\x9f\x91\xf6\x8b-\xf3\x01:\xae\xb7\x84)m\x9bO\x9e" +
|
||||
"\xdcX\xfb\xc1M\x1f\xcf\xe0\xebk\xcbv\xa2\xf6\xfe2" +
|
||||
"\xf2\xf5\xe9P\xee\xb3\xaf\x1f:\xfe\xbf3h\xbdHZ" +
|
||||
"\xaf\x0b\xad\x95\xef-\xdf\xf4\xee}\xdf\xf8\xcb\xb4x\x9e" +
|
||||
"_6\x8c\xdaK\xcb\xc8\xc9\x17\x97\xed\x07\xf4\x9fr\x07" +
|
||||
"\xaf\xbe\xbc\xeb\xe9\xbfLuR,z\xd5\xf2\x83\xa85" +
|
||||
"/\x17\xe1Z.\xa2\x1f\xfd\x97\xf1#\x1f\xf6\x0c\xffu" +
|
||||
"\x86\xe1\x1f[1\x8c\xda\x8fV\xd0\xf0\xfbO\x9c{7" +
|
||||
"u\xe2\x88?5\x93\xc4\xcc\x0f\xafx\x1e\xb5\x93+\xc8" +
|
||||
"\x85\xc7W\xfc\x14\xda\xfc4w83\xbf\x9a\x0e\xe5]" +
|
||||
"\xcf\x1e\xfdjZ\xfc\xdc\x9ef9+\xb7\xa6W<\x0c" +
|
||||
"q\x97;c\\\xd5W\x0f\"~\xae~\xcac\xce\xa0" +
|
||||
"a\xda^/sz\xbbm\xcbsls\x101\xd9\xa4" +
|
||||
"\x86\x00B\x08\x10e;\x01\x92\xdfR1i*\x18E" +
|
||||
"\x8c!\x81\xc6w\x01\x92\xa6\x8a\xc9\x09\x05\xa3\x8a\x12C" +
|
||||
"\x05 \x9a'MO\xc5\xe4\x01\x05\xa3\xaa\x1aC\x15 " +
|
||||
"\xfa@?@r\x9f\x8a\xc9#\x0a\xfa\xd9\xbc\xdeKc" +
|
||||
"\x80j\x9b8\x17\x14\x9c\x0b\xe8\xbb\x1e\xe7\x8eae0" +
|
||||
"a\x8c\x1a\xdeFk\x04\xed\xb2\xccbcF\x86y\x06" +
|
||||
"\xa8\xb6U\x06sY\xdb\xe2\x1br.\x00`S\xa5\x80" +
|
||||
"\x03b\x13|\xfel\x07XN\xdfh\xe5\xf2\xde\x96B" +
|
||||
"\x8e\x03\xd0D/\x92\xebZ\x9b\xd2\x0f\x80s\xb4V\xe5" +
|
||||
"\xe7\x00\x18\xd1Z\x95\xa7\xc8\xb8\xd6\xaa\xfc\x18\x00\xa3Z" +
|
||||
"\xabr\x19\x00\xe7j\xad\xca\x1b\x00X\xaf\xb5)\xc3\x00" +
|
||||
"\xa8i\xad\xca\xcb\x00\x18\xd3\xda\xc4/j\xed\xca^\x00" +
|
||||
"T\xb46e7\x00\xce\xd3Z\xc5\xf3M\xdar\xe5\x0f" +
|
||||
"\x008_kU~\x09\x80\x0b\xb46\xe5\x1d\x00l\xd6" +
|
||||
"\xda\xc5\xf3B\xadS\xf9\x01\x00.\xd2:\xc5\xb8-Z" +
|
||||
"\xa7\xb0\xb7X\xeb\x12\xcf7k]\xc2/\xb5\xf8\x1c\xd2" +
|
||||
"\xba\x84?q\xadK\xd8\xbdE\xbb[\xd8[\xd2\xd1\xa3" +
|
||||
"\xacA\x00\xac\xd26*\xcf\x03`\xb5\xb6Q8\xd0\xa8" +
|
||||
"\xadSv\x02`\x83\xd6#\x1c[\xaa\xdd-^\xfc\x92" +
|
||||
"\xd6#\x0c/+\xfe.\xef\xe8Q\xe6\x92\x81:m\xa3" +
|
||||
"\xf2\x10\x00\xae\xd06*\x7f\x06\xc0/w\x0c(KI" +
|
||||
"p\xab\xb6U\x84\xa0\xb5c\x87\xa2\x10\xb0\xb2c\x97\xfc" +
|
||||
"\xe3+\x1aS\xbe\x0b\x80\xb7iL\xbc\xda\xa61\x11\xbc" +
|
||||
"p\xf1\xb9Fc\xcaA\x00\xac\xd5v\x89\xdf\xdb\xb5\x1d" +
|
||||
"\xe4\x83\xaf\xdb\xe3\x96i3\x1d\x00|\x97{[\x98\x93" +
|
||||
"\xe1\xe8%\x98\xc7\x1df\xc6{\xd2in\x12\x9e\xcaq" +
|
||||
"\xae\xcb\x14\xd9<2\x12v\xb97\x05\xed\xb5\xad\x08e" +
|
||||
"/\xc1\x03,\xd7\x9b\xafr\xc6\xb8\x90\xf7\xca\xb4\x06\x97" +
|
||||
"{\xdb\x0c\xd7\xb0\xad\xde\xfc$\x91*_J\xd8\x99\x04" +
|
||||
"\x87\xf0\x98\x1cOh*RU\xf8D.\xf5H\x1f\x83" +
|
||||
"\xb2\x01\xc3\x92\xe2m\x00\xbe\xc3i&)\x0e\xdd\x9eg" +
|
||||
"X\x19\xd7w\xd9\x18Oq\xcf\x83\x88|\xe4\xde:\x8b" +
|
||||
"\x0d\x9b\xd0-\x87\x9fjl\xab\xcb\x85\x9c\xa7\"%\xb1" +
|
||||
"\x98\x8a2I\x96\xe3\x1c\xf5\xf2\xec\x151\xfb\x804\\" +
|
||||
"|\xf3\x1e\xdb\xd4\x13\x0as\xbd\x14\xe7\x96P%M\xf4" +
|
||||
"\x02Q\x16h?W\x9d=S\xc1\x9et\xb8\x18x\x81" +
|
||||
"*\x12\xddb\x8c\xf2\xcd##.\xf7d \xfa\xf8\x08" +
|
||||
"\xcb\xa3\xe9%\x98\xc5\xb7\x1ba\xdd\xcb\x96]\xc6R\xdc" +
|
||||
"\xe2\"p>\x05\x86\xd41oz\x14\x11#L\x01!" +
|
||||
"t\x88\xa7\xed\xaa\xd1Qn\xe9\\\x17\x12+\xe3\xd2Z" +
|
||||
"\xa5L{\xbc\xcf\x1e\xb7\xd6\xdb\xce&>!\x1dHD" +
|
||||
"h\xb2\x95\xb9o\xcdM\x92\x1a\xe1\xa2\x94\xe6\x9eRK" +
|
||||
"s\xf6\xb6g\x0d\x93\xf7f\x99\x951\xacL\x8awK" +
|
||||
"u1\xfa w\\4\\\x8f[\x1e\x09\xe4\xb2\xa5\x99" +
|
||||
"\x95\xe6f\x9f\x0d\xdd27\x8b\xe9\xd1\xefR\x1d*>" +
|
||||
"\xa4l\x88\xe4\x9d4\x17\x8b:\xe1qG\xb1\x98Y\x0e" +
|
||||
"\xf3\xa4t\x14b,\x89\xe3\x89Is\x90\xd9;\xe8\x18" +
|
||||
"q\xdb1\xbcB\x19W\xa5\x19r\x9a\x0f\xf1\xfb\xf3\x86" +
|
||||
"\xc3]\xda\x0d9\xf4|F\xbf^*\x87\xa5\xd1\xe4r" +
|
||||
"\x0c:\xdcu\x95\x0d\xcc\xddb\xf7\x145&\x8d\xd7\xa3" +
|
||||
"\xef\xce\xbb*\x85_\xaefP\xab\x12;\x01*^e" +
|
||||
"*\xb4\xea\xb6\x9a\xf7\xcaC\xd4\x8a!6\x8fq\xc71" +
|
||||
"t^Q\xa4U\xeb\xb5-\xdd\xf0\x0c{j0J;" +
|
||||
"0\x95e\xba=\xde\xcb\x9c\x94\xc7<\x14\xd1\x13\x10\x8e" +
|
||||
"\x0f\xd8:7\xb7\xad\x92S)b\x1brn\xc2N\xb3" +
|
||||
"\x08\x19$\xb8\xdf\xb5\xadAd^v\xbdi\xb3\xe2\xac" +
|
||||
"\x05V\xcd\xbc\xec\x16>!\\\x10\x00\xf3\xb2k\xed\xe0" +
|
||||
"\x90ESd\x89\xd6#\xe2X\xcc,\x1f\x16U78" +
|
||||
"\x1a\xc9Y~\xbb8s\xe8\xd0X]:\x1d\xb5]\xb8" +
|
||||
"\x17 u\x1f\xaa\x98\xcab\xe5\x80\xd48\xf6\x03\xa4t" +
|
||||
"\xc2sX9#\xb5Q\x1c\x02H\x99\x84O`\xe5\x98" +
|
||||
"\xd4\xf2B\xdf#\xfc\x00\xe1\xa1\x9a\x18\x86\x00\xb4\x07p" +
|
||||
"\x15@j\x82\xf0C\x84W\xd5\xc6\xb0\x0a@{\x10w" +
|
||||
"\x02\xa4\x0e\x10~\x8c\xf0\xea\xba\x18V\x03hG\xf1\x07" +
|
||||
"\x00\xa9c\x84\x9f <\\\x1f\xa3&B{\x0c\x1f\x02" +
|
||||
"H\x9d \xfc4\xe15\x0d1\xac\x01\xd0N\x09\xfd\xd3" +
|
||||
"\x84\x9f#\xbc\xb61\x86\xb5\x00\xdaYt\x00R\xcf\x12" +
|
||||
"~\x81\xf0\xbap\x0c\xeb\x00\xb4\xf3\xf8\x14@\xea\x02\xe1" +
|
||||
"WP\xc1%\xf5J\x0c\xeb\x01\xb4\x17p\x0d@\xea\"" +
|
||||
"\xe1WQAl\x88a\x03\xf5L\xb8\x12 u\x89\xe0" +
|
||||
"7QA\xdf\xb5i\xcb\x0c\xd8\xd0m\xd9\x94VX\x0b" +
|
||||
"\x0a\xd6\x02\xfa&\xf3\x0c/\xafs:\xd3\xebA\xc1z" +
|
||||
"\xc2l+C /c\xcc\x9c\xa6\x17w\xc56\xaa" +
|
||||
"\x03\x05\xeb\x00\xfda\xce\xa8\x9b\xe8\x03\x95g\xca`\xd6" +
|
||||
"v\x8c\xbd\xb6\xe5!3{\xd2\xe9\xbc\xc3\xd4t\xa1," +
|
||||
"\x1c\xe3\x8eg\xa4\x99\x89R\x94.\xd0\x018\xd9Z\x0f" +
|
||||
"\x16E}A\xab\xae\xac\x94i\x88\x0bY\x19\xcf[\xc6" +
|
||||
"\x84\x98\x9d\xeb\xb1\xd1\xdc\x80a\x86M\xc3\xc5*P\xb0" +
|
||||
"\x0a\xb0;\xcb\xdc\xf5\xc6\x04\"(\x88\x80\x91\xb1M\xeb" +
|
||||
"\xfap\x0e\xe0\xa0\x8a\xc2\xc0\x9c@\x0fS\xf57z\x98" +
|
||||
"\xcdy\xef\xf6\x14\x1b\xcd\x99\x9c\x123\xef\x8a\xa4\\ " +
|
||||
"\x9a\xb0\x9e\xb5\xd4\x88D\xbbVQ\x1f\x12m\xa7S_" +
|
||||
"\x8d\xb6\x11\x18\x8a.\xa7^\xa0*\xbad7\xc0\xfe\xbc" +
|
||||
"\xb5\xc7\xb2\xc7\xad\xb8eo\xc8\xb9\xbee\xf7\xdac\xdc" +
|
||||
"a\xa0f\xf8~\xcb\x1e`^:\xeb\x8f\xd2\xbf\\\xdf" +
|
||||
"\x04\xdd\xb6,!E \x01\xb2\x0a\x7f\xe1>4\xac\xb7" +
|
||||
"\xaf\xbaQ#:\xa5q\xed\xba\x91>\x85\x81\xf61\x9d" +
|
||||
"\x1a\x9b\xd5\xbc'\"PnZ\x1f\x7f\x08 \xf9\x84\x8a" +
|
||||
"\xc9g\x02M\xeb\x19jEO\xab\x98<\x17hZ\xcf" +
|
||||
"\xae\x04H>\xa3b\xf2J\xa0i}\x814/\xa9\x98" +
|
||||
"\xbc\xa6 \x86\xc4N\x8c\xbet\x10 yU\xc5\xe4\xaf" +
|
||||
"h\x1b\xa2\xd8\x86\xd1W\x87\x01\x92\xaf\xa8\x98|S\xc1" +
|
||||
"r'\x83\x83\x8e\x9d\xa1:)\xfa\xd32a(\xf6\xa7" +
|
||||
"n\xe9\x88\xa3~\x0c\x14l\x00\x8c\xe4\x98\x97-\xe5@" +
|
||||
"S\x85\xb1\x01\x8al\xc8\xd9\xae\xa8\xa8\xb2\xdf-\x93\xaf" +
|
||||
"\xa2=\xd3\xb6sC\xcc\xe3\xd8C\x0b\x98\xe1\x95\xdc-" +
|
||||
"I <`Xe\xb4\x14T\xf5o\xe5\x96\x9a\xf7\x92" +
|
||||
"5\x18\xbc\xea\xa8\xdd\x1d`q\xb5C~)\xf5 B" +
|
||||
"\xc9\xe7o\xc8\xb9)\xda\xd7\x80<y\xa4\\\x19\x7f\xa4" +
|
||||
"\xae\xa5\xca\xa2ReQ\x03\x95\xf1\xacJ\x95\xe2\x19\xc2" +
|
||||
"\x9fS\x03\x95\xf1g*U\xc0s\x84_T\x15\xc4b" +
|
||||
"a\xfcW\x95\x0a\xdd\x05\x82\xaf\x90z\x08ea|A" +
|
||||
"\xa5\xc2{\x89\xf0k\x84W)\xb20\xbe\xa4>\x0f\x90" +
|
||||
"\xbaF\xf8o\x08\xafVea|M\x0c\xfb\x0a\xe1o" +
|
||||
"\x12\x1e\x0e\xc9\xc2\xf8\xba\xb0\xff\x1b\xc2\x7fOx\x8d*" +
|
||||
"\x0b\xe3\xefT*\x8c\xbf'\xfc\x8f\x84\xd7\x86da\xfc" +
|
||||
"P\xa5\xc2\xf8\x01\xe1\x9f\x12^W%\x0b\xe3'\xeaw" +
|
||||
"\x01R\x9f\x12\x1e\x0a)\x18\xad\xaf\x96\x85\x11C\xbf\x04" +
|
||||
"H\xd5\x84TL\xc5\x08ox[V\xc6h\x88\xfci" +
|
||||
" |\x01\xe1\x8d\x8bc\xd8\x08\xa0\xcd\x0bQ\xc1o\"" +
|
||||
"\xbc\x85\xf09\xbf\x8b\xe1\x1c\x00\xad9D~. |" +
|
||||
"\x19\xe1\x91\x9a\x18F\x00\xb4%\xa1\x97\x01R\xb7\x12\xbe" +
|
||||
"\x9a\xf0\xa6\xda\x186\x11\x9f\x0fQ|\xee \xfc.\xc2" +
|
||||
"\xa3u1\x8c\x02h]!\x9a\xd7]\x84\xdfC\xf8\xdc" +
|
||||
"H\x0c\xe7\x02h\xebB\xc3\x00\xa9>\xc2\x07\x09\xd7\xea" +
|
||||
"c\xa8\x01h\x03\xa1\x9f\x03\xa4\x06\x09\xbf\x8f\xf0XC" +
|
||||
"\x0cc\x00\xda\x8e\x10\x1d(\xf7\x11\x9e%|^c\x0c" +
|
||||
"\xe7\xd1\xc1\x17\xa2\xf8\xe8\x84\xe7\x08\xbf\xa9%\x867\xd1" +
|
||||
"\xc1'\xf4s\x84\xef#|\xfe;1\x9c\x0f\xa0\x15\x84" +
|
||||
"?\xfb\x08?B\xf8\x82\xc51\\\x00\xa0\x1d\x0e\xed\x06" +
|
||||
"H\x1d\"\xfca\xc2\x9bkb\xd8\x0c\xa0\x1d\x17\xf19" +
|
||||
"B\xf8\xa3\x84/\xac\x8a\xe1B\x00\xed\x11\xe1\xe7\xa3\x84" +
|
||||
"?I\xf8\xa2\x9bc\xb8\x08@;)\xfcy\x82\xf0g" +
|
||||
"\x08o\x89\xc7\xb0\x05@;#\xe2y\x9a\xf0s\x84/" +
|
||||
"^\x16\xc3\xc5\x94\x9e!:\xa0\x9f%\xfc\x02\xe17\xd7" +
|
||||
"\xc5\xf0f:\xf8D|\x9e#\xfc\x12\xe1\xf1\xfa\x18\xc6" +
|
||||
"\x01\xb4_\x88y]\"\xfc\x1a\xe1\xb74\xc4\xf0\x16\xca" +
|
||||
"C\x81_#\xfc7\x84/i\x8c\xe1\x12\xca\xc3\xd0A" +
|
||||
"\x80\xd4\xaf\x08\x7f;\xa4\xe0\xfeqV\xd8\xc4Fy\xa9" +
|
||||
"\x14t\x8f\xb3\xc2\x10\x1f)=\xfa\x8e\xcdt\x92\x07\xaa" +
|
||||
"\x85<v\x12\xc6(\xa8\x86W\xde\xd6V\xb1\x09\x86n" +
|
||||
"\xd9rM\x13\xa0\xc4\xfb\x8cn\xd7\xa3\xf6\xb6\xa4\xd0\x9d" +
|
||||
"e{\x99\xa3\x97\xad\x93\xfe=l/\x03u\x06\x10\x1d" +
|
||||
"\xbd\xcf\xa0\xf7\xd5\x8a\x01\x9f\xe9c\x86k;\x05\x88\xa7" +
|
||||
"&\x9d\xc4\xf4R\x8f>f \x09\xa5\x0b\xd3dJI" +
|
||||
"V\xb4\x9b\xc6\x8ac\xb6\xc5\xb7\xb3B\xe9\xa0\x8c\x9b\xcc" +
|
||||
"\xe2.V\x83\x82\xd5\x80\xbeg\x98<A\xb4Q\xe5z" +
|
||||
"I\xa5\x1c\x19\xc5\xf0R\xf9L\x86\xbb\x1e\xd7\x85\xf1\xc0" +
|
||||
"\x89\xee\x16\x05\xd0\xadOv\x97\xbb\x9e1J\x85T\x1f" +
|
||||
"\xb2\x99\xbe\xdd\xd0U/[\x16\xd2:P\x17\x0ba>" +
|
||||
"\xe1a\xa4r\xc7\x09\x88\x11@_7\x8aQ]\xef\xd8" +
|
||||
"\xa3\xdbY\xa17\xce-j\xfa\xcb=F\x91xb\x89" +
|
||||
"y\x06<\x1a%\xbe\xe4\x8c\xf1\xa9\xf1\x1bg\x85\x147" +
|
||||
"y\x1a\xa9\xf8\xcb\x1b\x0b\x8cT\xae\xaa\x8a#\x97\xe6\x8c" +
|
||||
"\x86\xec\xdc\xbd`@\xb2F&;\xce\x0a\xbd\x101\x99" +
|
||||
"\xebb\xa4rq(\xdf\x8e\x8f\xb3\xc2F\xbd\xd4\x9a\xf8" +
|
||||
"\xe9b\xf3>\x85\xabT2O\x14\xfem\x1c\xe2\x0eM" +
|
||||
"\x07\xc3\xa0`\xb8\x18\x1d\xeaD@\xcd\xd3 \xe5\x93\xa3" +
|
||||
"\xe8b\xa6r@`\xa4r\x94T\xa4\x03\xb6eo\x81" +
|
||||
"p\xb0!L\xdb\xa3\xb9\xbc\xc7u\x142\x83v@Y" +
|
||||
"&\xbb\xc8\x0d\x1c-\xee0y2\x96e9\xc7\xd6\xf3" +
|
||||
"i\xee`\x8a\xbb\xe4cE\xf2\xb9\xbd~\x99\xaaHz" +
|
||||
"+\xa2M-E\x8dh\x12\xa2kDSU\xbb\x16\x80" +
|
||||
"v\x8fg\xa4\xf7\xe7\xb8\x93\xe6\x967\x9b\x9e\xa63\xd8" +
|
||||
"\xd3\xcct\xfcR\xde\x89K8>!\xfa\x99\x061\xf8" +
|
||||
"b\xd9\xd1\xcd[):\xba\xc6\xb5\x00\xfbG\x1c\xce\xc7" +
|
||||
"Y!\x926\xbcB\xa9\x93\x9b\x8d'\xed\xb3\xea\xc6\xc2" +
|
||||
"z\xfb\xeaY\xb6o_\x9b\xed\x00\x9d\xb3}\xa1\xfdF" +
|
||||
"\xc1\x14w}hQ\x1cc\xe5\xbe\xf0\x01\xea\xf6&T" +
|
||||
"L\x1e\x0a\xf4\x85\x0f\xae\xaa\\Qb\xb1-<\xbc\x14" +
|
||||
" y@\xc5\xe41j\x0b\x9bd[x\x94\xde>\xa4" +
|
||||
"b\xf2\xe1J#\x12=N\xbd\xe21\x15\x93'\x14\x8c" +
|
||||
"x\x85\x1c\xa5x\xf9\xfbSq\x97\x8d\x10[-\xed\xeb" +
|
||||
"\xb0\xeb9\xe5\xceo\xd8\xb6\xcd\xf2n\xdd]$\xae\xc1" +
|
||||
"b?\x9b\x98\xdc1\xdb v\xdc\xe8\x85{\x8a\xf5\x83" +
|
||||
"\xaa\x87\xd8\x0fw\x8a\xe8\x9c\x92)\xf9x\xbfH\xc9\xc7" +
|
||||
"v\x0b\x92\xf1\xc8*A2\x8e\x0e\x09\x92q\x98T\xaa" +
|
||||
"\xa3\x0f\x0c\x03`8Z \xb0&\x9awh\xa7F\xef" +
|
||||
"\xa7\xf7\xea\xa2\xa3\xf4^}\xd4\xa0\x9f\x86('\xcd\xc6" +
|
||||
"(\x0b\x90\x13\x7f\xd4\xf6lg\x9c\x15\x00\xa0\xf2w$" +
|
||||
"aX{\xe2\x9e\x93\xb7\xf6\xf8\xe2\xdf\x84a\x01\xee\xd9" +
|
||||
"\x9fs\x8cQ\xe6\x14\xfc\xe2o\x02\xc2\x86\xb5\xc7w9" +
|
||||
"\x155\xe6\x00\x16*\x7f\xc7\x0bd\xc3\xf7\x88\xf81G" +
|
||||
"\x98/\xff-\xcc\xfby+Ms6 2bp\xdd" +
|
||||
"w\xb8k\xe8\x9c\xca\xbf\xc1L\xdf4\xc6\x0c+C\xfd" +
|
||||
"\xaf\xc3\xb9wC\x9aBK:h\x1b\x96'/\x0f\x02" +
|
||||
"W\xeb\xfd3]\xad\x0f\x01$\xb3*&=j\x8eC" +
|
||||
"2\x1d\xef'0\xa7br\x1f\xa5c\x95L\xc7\xc2\xde" +
|
||||
"J2\x7fa\x02\x9d\xce;c\xcc\xcb;P9h}" +
|
||||
"O^`r\xe86m*)\xd3\xb8\xc2\x17\xd8\xf1\x1d" +
|
||||
"_\x84\xb0\x0d\x96\x98\x8c(\xad\xe58\xb4R\x1cnU" +
|
||||
"1\xb9:\x10\x87v\x9a\xf2\x1d*&\xef\x9a\xcd\xecn" +
|
||||
"\xe0\x00\x11gAW\xc4\xd5\x9dX\x0d\x11\xdf\xd6\x95\"" +
|
||||
"\xa1\x97\xc8\x84^\xdc/\x12\xbay)@\xc4\xb2-\xee" +
|
||||
"\x1bt\x96[\xcc\xa4D\xe1\x13\xe5\xbf\xc3i\xe6|\xe1" +
|
||||
"\xaf+\xa9\x88\xc7<\x9e\x0ca\xe0\x9bs\x14\x97\x867" +
|
||||
"\xe4\xdc\xe4\x812k\xba_\xa5\xb67G4b_\x90" +
|
||||
"5\x15\x04\xad\xd9G\xf8\x91 k:\xacR[z\x88" +
|
||||
"\xf0\x87\x09WQ\xd2\xa6\xe3\xeaS\x00\xa9\x87\x09\x7fB" +
|
||||
"\xd0&U\xd2\xa6\xc7\x85\xfd\x13\x84\x9f\x16\xb4)$i" +
|
||||
"\xd3)\x95\xda\xd2'\x09\x7fV\xd0\xa6*I\x9b~$" +
|
||||
"\xec<K\xf8\x05A\x9b\xaa%m:\xaf^\x9eD\xcb" +
|
||||
"j\xc2\x926\xbd X\xdfE\xc2\xaf\x0a\xdaT#i" +
|
||||
"\xd3\x8b\x82f]!\xfc\x15A\x9bj%m\xba.X" +
|
||||
"_\x85\xae\xd5\xd7I\xda\xf4\x9a\xf0\xf3W\x84\xbfMx" +
|
||||
"C\xbd\xa4M\xff!h\xd6\xdb\x84\x7f@xc\x83\xa4" +
|
||||
"M\xef\xab\xd4\xb6\xbfG\xf8\xc7\x84\xcfi\x94\xb4\xe9O" +
|
||||
"\x82\xc6}\xac\xaa8$X\xd3\x1c\xc9\x9a>\x13\xc3\xfe" +
|
||||
"\x95\xd4k\x04k\x8aH\xd6T%\xba\xfc\x10u\xe7M" +
|
||||
"\x8255I\xd6\xd4(XD\x85\xad\xcd\x8dJ\xd64" +
|
||||
"/\xf4\xfc$V\xa6\xa9\x925-\x09\x91\x9b\xcb\x08\xbf" +
|
||||
"C\xb0\xa6\xb9\x925\xb5\x09;\xb7\x11~\xa7`M\x9a" +
|
||||
"dM\x9dB\xffN\xc2\xfb\x04k\x8aI\xd6\xd4#X" +
|
||||
"M\x85\x95\xcd\x9f'Y\xd3\x80\xb0\x93 \xfc^\xc1\x9a" +
|
||||
"n\x92\xaci\xab`\x17[\x08\xff\x96`M\xf3%k" +
|
||||
"\xda%\xd8`\x85\xad-\xfc\xa6dM\\\x8c\x9b%\xdc" +
|
||||
"\x13\xac\xa9G\xb2\xa6\xfb\x85\x9d\x0a[kY+YS" +
|
||||
"A\xe0\x13\x84\x1f\x12\xac\xa9W\xb2\xa6\x07\x05~\x80\xf0" +
|
||||
"c\x825\xf5I\xd6tT\xf8Yfk\x18\x97\xa4\xe9" +
|
||||
"\x91\xd0R\x80\xd41\x82O\x84\x14\x14\xf7\xd9\xe6\xa0\xc3" +
|
||||
"!\xe2\xba\x81\xee\x95\x99\xe3\xac\xe0n\xb6\xb0\xf85h" +
|
||||
"]\xb7\xf8\xc4QQ\x18v\xd8\x1e\x9e02\x10\xcez" +
|
||||
"n\x19\xd5\x99\x9b\x1d\xb6\x99\x83z\xe0f\xbb\\\xd3t" +
|
||||
">\xe3X\xe5\x06^\xdcps\x1d\xa6K\x12\xb6\x95!" +
|
||||
"ix\xa6\xf7\x94m\xdc)\x944\x82\xef\xef\xe7i{" +
|
||||
"\x03gN\xf9\x8d\x11\xdbI\xf3^\x9b\x81\xeaze\xd0" +
|
||||
"p\x07\x99\xb3\x87hAe\xe0\x1c\xcb\xbb<\xc1<\x88" +
|
||||
"\xd0\xe4\xa7\xc0\xb6\x85\xa2\xfa\x19\x16C3@\x80l\xc7" +
|
||||
"\xdb\xc0\x19`e<\xcfa##Fz\x00m]~" +
|
||||
"#\x0a\x12\x84\x0csS\x1eK\xef\x09\x8e;j\xeb\x9c" +
|
||||
"f\x01\x93&*\x0b\xdb\xa0\x03q\xfe\xb9\x8b\xd4\xd3m" +
|
||||
"\x9a\xf6x0B\x95\xe5\xf0\xec\\\xca\xc8\xa0Ufq" +
|
||||
"\xf2C\xcbt\xa3\x12O\xd8X\x8a'N\x95m\xe3X" +
|
||||
"\x8a\xf7\x94ws\\g\xe6\x00\x9b\x80\xf8\x10\xcfpk" +
|
||||
"2.\xe2\xd6\x93\xf6\x8c1^\x99\xb0\xc7]\x93\xf5\xf6" +
|
||||
"\xae\x83n+\xc32\x01SEA\x1f\x1a.\x17\"\x9c" +
|
||||
"&\xdb\x84\xb6\xd7\xe3\x8c\x06\x13Fd\xf3=4\xe1^" +
|
||||
"'o\xb8\x81\x81D\xee\xcd(\xc9\xe5M\x97\xf7X:" +
|
||||
"\xc47\x98\x86\xceKx8\x93sg\xf9\x8d{\xca\xb9" +
|
||||
"|\xe7,;\xf7\xbf\xbb\x91\xfe\xf6\"+\x15\xa44\\" +
|
||||
"\xc8\x89STv)\x9d\xb2-l\x1b\x12\xa7h\xab<" +
|
||||
"E\x97\xf7\x8b\xb6p\xc9J\x80\xfd\xe9\xbc\xe3\x10i\xca" +
|
||||
"9\\7\xd2\x1e\x07\xd4\xfd\x9c\xed\xba\xc6\xb0\xc9Kg" +
|
||||
"\xab\xa5\x8b]\x10\x19a\x86y\xc3\x8b\xf0\xbe\xe2\x95k" +
|
||||
"\xf9\xc6u\xca5\xf0\x1a\x80\xe4\xa3*&\x9f\x0c4\x16" +
|
||||
"'\x87\x02w\xc3\x0aJ\xd7\xcf\xec\x0c\xdc\x0d\xab\x8al" +
|
||||
"\xb0\xce\x1e\x04H>\xabb\xf2B\xe5\x1a\xf8<\xbd\xfd" +
|
||||
"\x9c\x8a\xc9W\x02\xd7\xc0\xd7I\xf1\x9a\x8a\xc9\xf7\x14\xec" +
|
||||
"f\"\xbb\xa6\xa4\xabIs\xad\xe4\x8e\xed1s\xbda" +
|
||||
"\x82\xca]\xac\x01\x05k p}\xcc\xf5\xf5\x86\xc9]" +
|
||||
"(K\xcc\xe2w+@\xb7tK\xdcP\xfcRP\x12" +
|
||||
"a\x1f\xf7\x98a\xbaP\xb9F.\xff\xa7\x92\xe25\xf2" +
|
||||
"l\x88\xc1\xa4,\xb8Q\xf0K\x9f\xd5\xfa\xba\xa5\x0f\x82" +
|
||||
"\xba\x96\xd7`\x1d5w}*&\x07\x15,-\xc1\x00" +
|
||||
"E;\xa1b\xf2\xde\xc0\x12l\xa5 nQ1\xf9-" +
|
||||
"%0\xe5\x00\x15\x9a]\xd4\xfe?\x00\x00\xff\xff\xd6\xbd" +
|
||||
"\x94\xef"
|
||||
const schema_b526ba661d550a59 = "x\xda\x94zkp\x1c\xd5\x95\xff9\xdd3\x1a==" +
|
||||
"\x1a\xdf1\xd8\x92\xc5\xd8\xc6\xc2\xc8A\xc4F\xf8\x1f\xe4" +
|
||||
"\xe0\x08Y2\xb6U#\xacV\xdb\x16v\xe1\xaa\\M" +
|
||||
"_\xcd\xb4\xdd\xea\x1ew\xf7H\x1a\x17.\x1b\xc7T\x01" +
|
||||
"\x7f\xa8@6\x0f'\x156<LU\x92\x85\x84\xcd\xc2" +
|
||||
"\x96\x97\"\xb5\x84\"U\x86\xc5U\x90\x0a\xa9<\xc8\xe2" +
|
||||
"\xe4\x0bI\x96\x14\x9b,\xa9\xcdfC\xf5\xd6\xb9=\x8f" +
|
||||
"\xd6#\x18}\xf1\xa8\x7f\xe7\xf4\xb9\xe7\x9e{\xee\xb9\xe7" +
|
||||
"w\xdb[N\xb6\xdc\x16\xdb\xda\x96o\x05E{8\xde" +
|
||||
"\x10\xa4\x8e\x1d\xfa\xb5\xed\x7f\xe74\xa4:18\xd4|" +
|
||||
"\xa0k\xea\x85\xeb\x9e\x87X\x02\xa0oM\xc3ad\xbd" +
|
||||
"\x0d\x09P\x83\xef~\x90\xed?\xfc\x9f/\xdf3_+" +
|
||||
".\xd5\x9a\x1aN \xebjH\x00\xb05\x0d\xef\x02\x06" +
|
||||
"w\x0e\xbd\xfc\xf9\xc9\xeb\xbeq/\xa4:\x95\xba2`" +
|
||||
"\x1f&F\x90\xadJ\xd0K\xa9\x84\x13\x03\x0cz_-" +
|
||||
"\xdd\xf9\xed5\x0f=\x02Z/6\x05/\xdb\xff\xdar" +
|
||||
"\xf0\x95\xbb\xfe\x0b\xe2H:\x0f\xb5<\x80\xec\xa9\x16\xfa" +
|
||||
"\xf3\xf1\x96w\x14\xc0\xe0\xef?\xf1~\xc9<q\xf8\xcb" +
|
||||
"\x90\xbaV\x09\xce\xfc\xe9O\xd7\xf6\xfd\xc7\x1f\xce\x93\xe5" +
|
||||
"\x9f\xad8\x83\xec\xf7+\xc8\x87\xdf\xae\xf8\x0a`\xb0\xf2" +
|
||||
"9\xcc\xe7/^\xfa\xfa\x12\xd3\xeaON\"\x1bM\xd2" +
|
||||
"\xb4z\x7f\x91Q\x87\x13\x85\xc7\x97\xd0\xeaI\x1eF\xb6" +
|
||||
"CjYj\xdfm\x7f\x9elx\x12\xb4N\\4\xfb" +
|
||||
"5\xa4\xd7\x9b\x0c_\x99@\xc0\xba_Z'\xb6G\xd4" +
|
||||
"\xe5\xbc\x1fm\xef@\xf6L\xfb&\x80\xbeW\xdb'\x1a" +
|
||||
"\x01\x83\xe1\xa1\x97^{\xe6\x9d'\x9fZ\x14\xadC\x1d" +
|
||||
"g\x90Mw\xd0\x9c\xcc\x8eO\x01\x06\x07\xcek?\xbb" +
|
||||
"a\xe6\xd2SKx;\xddq\x18\xd9=\x1d\xe4\xedw" +
|
||||
"~\x91\xfb\xd2\xff\xff\xe6\xfb\xff\xb0\xc8\xde\x91\x8e\xedu" +
|
||||
"{\xfb\x00\x83#\xcf\xbf\xbb\xf5\xf3\x1f\xbe\xf3\x9d%\xec" +
|
||||
"\x9d${\x8fH{\x03\xef\xfc\xee\xe4\xe8\xba\x89g\x97" +
|
||||
"\x1cu\xb2:\xeaE\xcc\xae7s\xf6\x85%\xb4\x8e\x90" +
|
||||
"\xad\xe3R\xab\xf0\xe2\xdf}Q\xff\xf4\xf0\xf7\x97\xd0\x1a" +
|
||||
"%[\\j\x9dUo\xbe,n\xdd\xf8\xf2\x12Z;" +
|
||||
"HK\x93Z\xef\xff\xd3g\xde\xbce\xc7\xd1\x8b\x0bW" +
|
||||
"E!\xbd\xde\x8e\x95\xc8vt\xc8\xe5\xee\xc8\xd0\xaal" +
|
||||
"?4Q\xb4\xde\xfa\xc6\xbf-5t'\x0d\xddIF" +
|
||||
"\xdf8\xf3X\xfe\x7f\x7f\xfe\xe5KK\x0dMZ\x9a\xd4" +
|
||||
"\xeaz%\xf7\xf6\xdb\xfe\x997\x17\x85\xb8\xb7s'\xb2" +
|
||||
"\x1d\x9dr\xd8N\x99\x0c\xfd\xfd/>\xfe\xfa\x03\xff\xfd" +
|
||||
"\x13\xf2R]\x98bkG\x90\xedXK\x0b\xd2\xbf\x96" +
|
||||
"6\xce\xb6\xec\xf0\xe7\xceM|\xf9\xe7K\x8c\xde\xd5u" +
|
||||
"\x18\xd9\xd6.\x1a\xbd\xffs\xaf}\xed+\xf1\xaf\xfc;" +
|
||||
"\x99T\x16\xa8\xb5u\xedD\xd6\xd5%\xf7b\xd7,`" +
|
||||
"p\xfd\x9b?y\xff\x86]\xcf\xfdq\xd1\x8e9\xd9u" +
|
||||
"\x14\xd9#R\xf3\xa1\xae<`}\xf3\xcd7\x1bn\xc5" +
|
||||
"\x7f!\xedK]W\x03\xf4\xbd\xd5\x95H\x00\x06k\x1e" +
|
||||
"}to\xd3{W}\xb0\x84\xaf\xbf\xec>\x8c\xec\x0f" +
|
||||
"\xdd\xe4\xeb\x93\xb1\xe2\x87\x9f>\xfb\xd0\xff,\xa1u\x89" +
|
||||
"\xb4~%\xb56\xff\xa6\xfb\x8ew\xef\xfa\xcc_\x16\xc5" +
|
||||
"\xf3\xfb\xdd\x93\xc8\xde\xec&'/u\x9f\x02\x0c\x9e\xf0" +
|
||||
"\xc6.\xbev\xe4\xc9\xbf,tR.z\xdbug\x90" +
|
||||
"\xad\xbfN\x86\xeb:\x19\xfd\xd4?\xcf\xde\xf7\xfb\xc1\xc9" +
|
||||
"\xbf.1\xfc\xa3\x9b&\x91\xfd\xe3&\x1a\xfe\xd4\xb9g" +
|
||||
"\xdf\xd5\xcf\xdd\x17,\xcc\xa4\xb0\x08mz\x01\xd9S\x9b" +
|
||||
"\xc8\x85\xc77}\x17z\x83\x9cp\x05\xb7>\x99\x8b\x95" +
|
||||
"<\xdf\x99\xfedN\xfe\xdc\x98\xe3E\xbb\xb8}H>" +
|
||||
"\x8c\x0bO\xb83B5n\x1eC\xfcH}\xdd\xe7\xee" +
|
||||
"\x98i9\xfe\x10w\x87\x06\x1c\xdbw\x1dk\x0cQk" +
|
||||
"Wc\x001\x04H\xf1\xc3\x00\xdagU\xd4,\x05S" +
|
||||
"\x88i$\xd0\xfc\x02\x80f\xa9\xa8\xcd)\x98R\x944" +
|
||||
"*\x00\xa9\x12i\xfa*j\xa7\x15L\xa9j\x1aU\x80" +
|
||||
"\xd4\xc9\x11\x00\xedn\x15\xb5\xfb\x14\x0c\x0a%c\x88\xc6" +
|
||||
"\x00\xd5\xb1p%(\xb8\x120\xf0|!\\\xd3\xcec" +
|
||||
"\xd6\x9c6\xfd\xbd\xf6\x14:5\x99\xcdg\xcc<\xf7M" +
|
||||
"P\x1d\xbb\x06\x16\x0b\x8e-v\x17=\x00\xc0\xf6z\x01" +
|
||||
"\x07\xc4v\xf8\xe8\xd9\x8e\xf2\xa2\xb1\xd7.\x96\xfc\xfd\xe5" +
|
||||
"\xa2\x00\xa0\x89\xbeH\xae\xb3^e\x04\x00W\xb0\x1e\xe5" +
|
||||
"{\x00\x98d=\xca\x13d\x9c\xf5(\xdf\x06\xc0\x14\xeb" +
|
||||
"Q^\x06\xc0\x95\xacG\xf99\x00\xb6\xb0^e\x12\x00" +
|
||||
"\x19\xebQ^\x03\xc04\xeb\x95\xbf\xc8\xb6*'\x00P" +
|
||||
"a\xbd\xcaQ\x00\\\xc5z\xe4\xf3U\xac[\xf9\x1d\x00" +
|
||||
"^\xcdz\x94\x1f\x01\xe0j\xd6\xab\xfc\x1a\x00\xd7\xb0\xad" +
|
||||
"\xf2\xb9\x83mS\xbe\x06\x80\x9dl\x9b\x1cw-\xdb&" +
|
||||
"\xedu\xb1~\xf9|\x0d\xeb\x97~\xa9\x95\xe7\x18\xeb\x97" +
|
||||
"\xfedX\xbf\xb4\xbb\x8e\xed\x90\xf6\xd6\xf7\x0d*\xdb\x11" +
|
||||
"\x00\xe3l\xaf\xf2\x02\x006\xb0\xbd\xd2\x816\xb6K9" +
|
||||
"\x0c\x80\xadlP:\xb6\x81\xed\x90/^\xcb\x06\xa5\xe1" +
|
||||
"\x8d\x95\xdf\xee\xbeAe%\x19hf{\x95\x07\x00\xf0" +
|
||||
":\xb6W\xf9#\x00n\xea\x1bU6\x90\xe0zv@" +
|
||||
"\x86\xa0\xa7\xef\x90\xa2\x10\xb0\xb9\xefH\xf8\xc7'\x18W" +
|
||||
"\xbe\x00\x8070._\xede\\\x06/Qynd" +
|
||||
"\\9\x03\x80M\xec\x88\xfc\xbd\x91\x1d\"\x1f\x02\xc3\x99" +
|
||||
"\xb5-\x87\x1b\x00\x10x\xc2\xdf\xcf\xdd\xbc@?\xcb}" +
|
||||
"\xe1r+3\x98\xcb\x09\x8bp\xbd(\x84\x11\xa6\xc8\xbe" +
|
||||
"\xa9\xa9\x84'\xfc\x05\xe8\x90c'){\x09\x1e\xe5\xc5" +
|
||||
"\xa1R\xdc\x9d\x11R>\x14\xa65x\xc2?hz\xa6" +
|
||||
"c\x0f\x95\xe6\x89\xd4\xf0\xa5\xac\x93\xcf\x0aH\xcc\x84\xe3" +
|
||||
"IM%T\x95>\x91K\x83\xa1\x8fQ\xd9\xa8i\x87" +
|
||||
"\xe2\x83\x00\x81+h&\xba\x80\x01\xdf7\xed\xbc\x17x" +
|
||||
"|F\xe8\xc2\xf7!\x19>\x0a\x7f\x97\xcd'-\x18\x08" +
|
||||
"\x87_h\xec\x80'\xa4\\\xe8\xc9\xaaXNE\x99'" +
|
||||
"+\x0a\x81Fm\xf6\x8a\x9c}D\x9a\xa8\xbc\xb9\xc7\xb1" +
|
||||
"\x8c\xac\xc2=_\x17\xc2\x96\xaa\xa4\x89~$\xca\x12\x1d" +
|
||||
"\x11\xaa{l!8\x98KT\x02/Q%D\xf7\x9b" +
|
||||
"\xd3b\xdf\xd4\x94'\xfc0\x10\xc3b\x8a\x97\xd0\xf2\xb3" +
|
||||
"\xdc\x16\x13f\xc2\xf0\x0b5\x97\xb1\x1a\xb7\x8c\x0c\\@" +
|
||||
"\x81!u,Y>E\xc4LP@\x08\x1d\x179'" +
|
||||
">=-lC\x18Rb\xe7=Z+\xddrf\x87" +
|
||||
"\x9dY\xfbv\xc7\xbdC\xcc\x85\x0ed\x934\xd9\xfa\xdc" +
|
||||
"\x0f\x14\xe7I\xcdDEJs\xd7\xd5\xea\x9c\xfd\x89\x82" +
|
||||
"i\x89\xa1\x02\xb7\xf3\xa6\x9d\xd7\xc5@\xa8.G\x1f\x13" +
|
||||
"\xae\x87\xa6\xe7\x0b\xdb'A\xb8l9n\xe7\x845\xec" +
|
||||
"\xc0@\x98\x9b\x95\xf4\x18\xf1\xa8\x0eU\x1et\x07\x92%" +
|
||||
"7'\xe4\xa2\xce\xf9\xc2Uln\xd5\xc2</\x1d\xa5" +
|
||||
"\x18\xab\xe2Lv\xde\x1c\xc2\xec\x1ds\xcd\x8c\xe3\x9a~" +
|
||||
"\xb9\x86\xab\xa1\x19rZ\x8c\x8b\xe3%\xd3\x15\x1e\xed\x86" +
|
||||
"\"\xfa\x01\xa7__/bu\xb4p9\xc6\\\xe1y" +
|
||||
"\xcan\xee\xedw\x06+\x1a\xf3\xc6\x1b4\x8e\x96<\x95" +
|
||||
"\xc2\x1f\xaefT\xab\x1e;\x09*~}*\xb4\xea\x8e" +
|
||||
"Z\xf2kC4\xc9!\xf6\xcd\x08\xd75\x0dQW\xa4" +
|
||||
"U\x1brl\xc3\xf4Mga0\xaa;P/p\xc3" +
|
||||
"\x99\x1d\xe2\xae\xees\x1fe\xf4$\x84\xb3\xa3\x8e!\xac" +
|
||||
"\x837\x85S\xa9`\xbb\x8b^\xd6\xc9\xf1$\x19$x" +
|
||||
"\xc4s\xec1\xe4~\xe1v\xcb\xe1\x95YK\xac\x81\xfb" +
|
||||
"\x85\xfdbN\xba \x01\xee\x17v:\xd1!+\xa6\xc8" +
|
||||
"\x12\xadG\xd2\xb5\xb9U;,\xe2W8\x1a\xc9Yq" +
|
||||
"\xa3<s\xe8\xd0\xb8\xb9z:\xb2#x\x02@\xbf\x0b" +
|
||||
"U\xd4\x0bX? \x99\xc0\x11\x00\xdd \xbc\x88\xf53" +
|
||||
"\x92M\xe38\x80n\x11>\x87\xf5c\x92\x95\xa4\xbeO" +
|
||||
"\xf8i\xc2c\x8di\x8c\x01\xb0\x93x\x13\x80>G\xf8" +
|
||||
"Y\xc2\xe3Mi\x8c\x03\xb0{\xf00\x80~\x9a\xf0\x07" +
|
||||
"\x09ohNc\x03\x00\xbb\x1f\xbf\x06\xa0?H\xf89" +
|
||||
"\xc2\x13-ij\"\xd8\x97\xf0\x01\x00\xfd\x1c\xe1\xe7\x09" +
|
||||
"olMc#u\x14R\xff<\xe1\xcf\x12\xde\xd4\x96" +
|
||||
"\xc6&\x00\xf6\x0c\xba\x00\xfa\xd3\x84_ \xbc9\x91\xc6" +
|
||||
"f\x00\xf6<>\x01\xa0_ \xfc\x15Tp}\x8b\x92" +
|
||||
"\xc6\x16\x00\xf6\x03\xdc\x0e\xa0\xbfH\xf8ET\x10[\xd3" +
|
||||
"\xd8\x0a\xc0~\x88\x9b\x01\xf4\x97\x08~\x1b\x15\x0c<\x87" +
|
||||
"\xb6\xcc\xa8\x03\x03\xb6Ci\x85M\xa0`\x13``q" +
|
||||
"\xdf\xf4K\x86\xa03\xbd\x05\x14l!\xcc\xb1\xf3\x04\x02" +
|
||||
"\x8a\x1a\xc6\xadEz\x19On\xa3fP\xb0\x190\x98" +
|
||||
"\x14\x9c\xba\x89aPE\xbe\x06\x16\x1c\xd7<\xe1\xd8>" +
|
||||
"rk0\x97+\xb9\\\xcd\x95k\xc2\x19\xe1\xfaf\x8e" +
|
||||
"[\x18\x8are:\x00\xe7[\x1b\xc4\x8ah8j\xd5" +
|
||||
"\x0b+e\x0e2RV\xc3K\xb69'g\xe7\xf9|" +
|
||||
"\xba8jZ\x09\xcb\xf40\x0e\x0a\xc6\x01\x07\x0a\xdc\xbb" +
|
||||
"\xdd\x9cC\x04\x05\x1109s\xc7\xaea\\\x018\xa6" +
|
||||
"\xa24\xb0\"\xd2\xc3\xc4\xffF\x0f\xb3\xaf\xe4\xdf\xa8\xf3" +
|
||||
"\xe9\xa2%(1K\x9eL\xca\xd5\xb2\x09\x1b\xdcI\x8d" +
|
||||
"H\xaa\xff&\xeaCR[\xe9\xd4WS\xbd\x04\xc6R" +
|
||||
"\xdd\xd4\x0b\xc4S\xeb\x8f\x02\x9c*\xd9\xc7lg\xd6\xce" +
|
||||
"\xd8\xce\xee\xa2\x17\xd8\xce\x903#\\\x0ej^\x9c\xb2" +
|
||||
"\x9dQ\xee\xe7\x0a\xc14\xfd+\x8c;`\xc0\x09KH" +
|
||||
"\x05\xc8BX\x85?v\x1f\x9a0\xb6\xdet\xa5Ft" +
|
||||
"A\xe3\xda\x7f%}\x0a\x03\xedc:5\xf6\xa9%_" +
|
||||
"F\xa0\xd6\xb4~\xf5\x01\x00\xed\xeb*j\xdf\x8c4\xad" +
|
||||
"OQ+z^E\xed\xd9H\xd3\xfa\xccf\x00\xed\x9b" +
|
||||
"*j\xafD\x9a\xd6\x1f\x90\xe6K*j\xaf+\x881" +
|
||||
"\xb9\x13S\xaf\x9e\x01\xd0.\xaa\xa8\xfd\x98\xb6!\xcam" +
|
||||
"\x98zs\x12@{CE\xedm\x05k\x9d\x0c\x8e\xb9" +
|
||||
"N\x9e\xea\xa4\xecOk\x84\xa1\xd2\x9fz\xd5#\x8e\xfa" +
|
||||
"1P\xb0\x150Y\xe4~\xa1\x9a\x03\xedu\xc6\x06(" +
|
||||
"\xb3\xa1\xe8x\xb2\xa2\x86\xfdn\x8d|U\xecY\x8eS" +
|
||||
"\x1c\xe7\xbe\xc0AZ\xc0\xbc\xa8\xe7nU\x02\x89Q\xd3" +
|
||||
"\xae\xa1\xd5\xa0\xaa\x7f+\xb7\xd4\x92\xaf5b\xf4\xaa\xa3" +
|
||||
"\xe9h\x84\xc55\x8d\x07\xd5\xd4\x83$%_\xb0\xbb\xe8" +
|
||||
"\xe9\xb4\xaf\x01\x85v\xaeV\x19\xd7\xc4v\x02\xe8\xe9\x98" +
|
||||
"\x8a\xfa\xbaX\xa42v\xc5\xa8R\xac&|c,R" +
|
||||
"\x19\xd7\xc7\xa8\x02\xae#\xfc\x86\x98\x82X)\x8c=1" +
|
||||
"*t\xd7\x13|3\xa9\xc70,\x8c[cTx\xb7" +
|
||||
"\x10~+\xe1q%,\x8c\xfd\xb1\x17\x00\xf4[\x09\xdf" +
|
||||
"Cx\x83\x1a\x16\xc6]r\xd8\xdb\x08\xcf\x12\x9e\x88\x85" +
|
||||
"\x85q\xaf\xb4\xbf\x87\xf0\xfd\x847\xaaaa\xd4bT" +
|
||||
"\x18\xf7\x13\xfeY\xc2\x9bbaa<\x12\xa3\xc2x\x17" +
|
||||
"\xe1\x05\xc2\x9b\xe3aa\x14\xb1/\x00\xe8\x05\xc2}\xc2" +
|
||||
"[\x1a\xc2\xc2x<\xf6#*\xe0\x84\x9f%\xbc\xf5r" +
|
||||
"X\x19\xef\x91\xfe\xdcM\xf8}\x84\xb7u\xa5\xb1\x0d\x80" +
|
||||
"\xdd\x1b\xa3\x82\x7f\x9a\xf0\x07\x09_\xf1\xab4\xae\xa0\xc2" +
|
||||
".\xfd\xbc\x8f\xf0/\x12\x9elLc\x12\x80=\x12{" +
|
||||
"\x8d\x0a;\xe1\xe7\x09ooJc;\x15v\x19\x9f\xc7" +
|
||||
"\x08\x7f\x9a\xf0Ts\x1aS\x00\xec[r^O\x13~" +
|
||||
"\x81\xf0\x95\xc94\xae\xa4\xc2\x1e\x9b\x04\xd0\x9f#\xfc%" +
|
||||
"\xc2YK\x1a\x19\x00\xfb~\xec{T\xc1\x09\x7f\x9d\xf0" +
|
||||
"tk\x1a\xd3\x00\xec\xd5\x18\x1d(\xaf\x13\xfeS\xc2W" +
|
||||
"\xb5\xa5q\x15\x00{K\xc6\xe7\xc7\x84_&\xfc\xaa\xb5" +
|
||||
"i\xbc\x0a\x80\xfdR\xea_&\xfc=\xc2\xaf\xfeu\x1a" +
|
||||
"\xaf\x06`\xbf\x95\xfe\xbcG\xf8\x9f\x09_\xdd\x95\xc6\xd5" +
|
||||
"\x00\xecO\xb1\xa3\x00\xfa\x071\x15\xc7\xe3\x0a\xa6\xd64" +
|
||||
"\xa6q\x0d\x00\xfbP\x86\xe7\xcf\xa4\x1e#\xbc#\x9e\xc6" +
|
||||
"\x0e\x00\x86qr3\x16WQo'\xbc\xf3\x9a4v" +
|
||||
"\x02\xb0\xb68\xb9\xd3J\xf8j\xc2\xd7f\xd2\xb8\x16\x80" +
|
||||
"\xad\x8aS8\xd3\x84\xaf#\xbckc\x1a\xbb(;\xe3" +
|
||||
"t>\xaf%\xfcz\xc2\xafiN\xe35\x00\xac;N" +
|
||||
"\xe1\xd9H\xf8\x16\xc23-i\xcc\x10q\x8c\xd3\xb4\xb6" +
|
||||
"\x10~+\xe1\xebZ\xd3\xb8\x8e\xd2P\xe2\xb7\x12\xbe\x87" +
|
||||
"\xf0\xf5mi\\Oi\x18?\x03\xa0\x0f\x13>F\xf8" +
|
||||
"\x06\x96\xc6\x0d\x00l4N\xe93F\xf8]\x84_\xbb" +
|
||||
"6\x8d\xd7\x02\xb0CR\xffN\xc2\x0d\xc276\xa4q" +
|
||||
"#\x00\xe3R\xdf \xbcHxw2\x8d\xdd\xd4_\xc4" +
|
||||
")l\x16\xe1sq\x05O\xcd\xf2\xf2\x1d|ZT+" +
|
||||
"\xcd\xc0,/\x8f\x8b\xa9\xeac\xe0:\xdc y\xa4\x18" +
|
||||
"\x85\xa7Z\xd6\x9c\x06\xd5\xf4kU\xc3\xae\xf4\xd80\x10" +
|
||||
"vt\x8b\x04\x18\xe2\xc3\xe6\x80\xe7S\xf7\\U\x18(" +
|
||||
"\xf0\x13\xdc5j\xd6I\x7f\x0f?\xc1A]\x02D\xd7" +
|
||||
"\x186\xe9}\xb5n \xe0\xc6\x8c\xe99n\x192\xfa" +
|
||||
"\xbc\x83\x9e^\x1a4fL$a\xe8\xc2\"\x99R\x95" +
|
||||
"U\xec\xe6\xb0\xee\x98c\x8b\x09^\xae\x9e\xc3\x19\x8b\xdb" +
|
||||
"\xc2\xc3\x06P\xb0\x010\xf0MKd\x89\x95\xaa\xc2\xa8" +
|
||||
"\xaa\xd4\"\xa3\x98\xbe^\xca\xe7\x85\xe7\x0bC\x1a\x8f4" +
|
||||
"\x0c^E\x00\x03\xc6|w\x85\xe7\x9b\xd3T\xa7\x8dq" +
|
||||
"\x87\x1b\x13\xa6\xa1\xfa\x85\x9a\x90\xd6\x81\x9adH\x889" +
|
||||
"\x1f\x93\xf5+T@L\x02\x06\x86Y\x89\xea\xed\xae3" +
|
||||
"=\xc1\xcbC\x19a\x13\xa7\xa8\xb50\x15^\x8bUb" +
|
||||
"\x1b\xf1h\x9a\xe8\x98;#\x16\xc6o\x96\x97ua\x89" +
|
||||
"\x1c\xd2\xd9\x12^\x88`\xb2~\x13V\x19\xb9:g4" +
|
||||
"Cb\xe0G\x03R0\xf3\x85Y^\x1e\x82\xa4\xc5=" +
|
||||
"\x0f\x93\xf5{\xc9\xf0\xed\xcc,/\xef5\xaa\x9dO\x90" +
|
||||
"\xabp\x83\x05T\xa8\x9ey\xf2\\9( \xe3\xd2t" +
|
||||
"0\x01\x0a&*\xd1\xa1F\x07\xd4\x12\x0dR;\x98*" +
|
||||
".\xe6\xeb\xe7\x0f&\xeb'U]:\xea\xd8\xce~H" +
|
||||
"D\xfb\xcd\x9c3],\xf9\xc2@)3i\x07\xd4d" +
|
||||
"a\x93\xba[\xa0-\\\x1e\x1e\xbc5Y\xd1u\x8cR" +
|
||||
"N\xb8\xa8\x0b\x8f|\x8cJ\xc4\x8c)fu\xac3\xed" +
|
||||
"Z\xa8+\xb2a\xac,#,\x94\xecD\xa7d\x1b\xdc" +
|
||||
"-\xef\xc5\xfa\xae\xa8\xc8& 9?\x8a\x1f\xc5Yj" +
|
||||
"\x94+\xa4\xe9rY\xa95j\x94\xcdNj\xbbl\x0e" +
|
||||
"\x9bv\x02\xd06\xf5\xcd\xdc\xa9\xa2ps\xc2\xf6\x97\xd3" +
|
||||
"\x9bm\x8b\xf6fK\xb5\x11\x94\xe0\xf22Q\xcc\xc9\xbe" +
|
||||
"\xacU\x0e\xde\x15v\xa6\xab6\xcb\xce\xb4m'\xc0\xa9" +
|
||||
")W\x88Y^N\xe6L\xbf\\\xedH\x97\xe3\xc9\xd6" +
|
||||
"eu\x95\x09c\xeb\xcd\xcblC?\xb5\xdc\x01\xb6-" +
|
||||
"\xf7\x85\xadW\x0a\xa6\xbc\xb3D\x9b\xe2\x98\xae\xf5\xb7'" +
|
||||
"\xa9k\x9dSQ;\x1b\xe9o\xef\xb9\xa9~\xd5\x8a\x95" +
|
||||
"\xf6\xf6\xde\x0d\x00\xdai\x15\xb5\x07\xa9\xbdm\x0f\xdb\xdb" +
|
||||
"\xfb\xe9\xed\xb3*j\x0f\xd7\x1b\xaa\xd4C\xd4\xf3>\xa8" +
|
||||
"\xa2vN\xc1\xa4_.\xd2^\xaa}G\xabl\xe7)" +
|
||||
"b\xdd\xd5\xdcMx\xbe[\xeb`'\x1d\xc7\xaa\x95\x85" +
|
||||
"\xa3\x15\x02\x1e=U\x96\x13\x93-\xcb\x0db\xdf\x95^" +
|
||||
"\xd8S)TT\xa6\xe4~\xb8EF\xe7\xf10%\xbf" +
|
||||
":\"S\xf2KG%Yz\xe4&I\x96\xee\x1f\x97" +
|
||||
"d\xe9^RiH\x9d\x9c\x04\xc0D\xaaL`c\xaa" +
|
||||
"\xe4RIH\x1d\xa7\xf7\x9aS\xd3\xf4^K\xca\xa4\x9f" +
|
||||
"\xd6\x94 \xcd\xb6\x14\x8f\x90\xac`\xda\xf1\x1dw\x96\x97" +
|
||||
"\x01\xa0\xfew2k\xda\xc72\xbe[\xb2\x8f\x05\xf2\xdf" +
|
||||
"\xaci\x03\x1e;Ut\xcdi\xee\x96\x83\xcao\x16\x12" +
|
||||
"\xa6},\xf0\x04UO\xee\x02\x96\xeb\x7fg\xcad#" +
|
||||
"\xf0\x89\xc0rW\x9a\xaf\xfd-\xcd\x07%;Gs6" +
|
||||
"!9e\x0a#p\x85g\x1a\x82\xce\x19\x93[\x81e" +
|
||||
"\xce\x98v\x9e\xfaxW\x08\xff\x8at\x8b\x96t\xcc1" +
|
||||
"m?\xbc\x04\x89|\"\x18Y\xea\x13\xc18\x80VP" +
|
||||
"Q\xf3\xa9\xc9\x8f\x85\xe9x\x9c\xc0\xa2\x8a\xda\xdd\x94\x8e" +
|
||||
"\xf10\x1d\xcb'\xea\xc9\xfc\xb1/\x02r%w\x86\xfb" +
|
||||
"%\x17\xea'z\xe0\x87\x17\xb1\x02\x06,\x87J\xca\"" +
|
||||
"\xce\xf31v|\xdf\xc7!\x9ecUF&Kk-" +
|
||||
"\x0e=\x14\x87\xebU\xd4n\x8e\xc4a+My\x8b\x8a" +
|
||||
"\xda\xad\xcb\x99\xdd\x15\x1c\xd8W\xf2o\x94\xb4K^A" +
|
||||
"\xca\xd5\x90\xf1\xed\xd9,\x13z}\x98\xd0]#2\xa1" +
|
||||
"\xd7l\x00H\xda\x8e-\x02\x93\x9a\x06\x9b[\x94(b" +
|
||||
"\xae\xf6w\"\xc7\xdd\x8f\xfd\x95HO\xfa\xdc\x17Z\x0c" +
|
||||
"#\xdf\xceS\xb8!\xb1\xbb\xe8i\xa7k\xec\xef\xb8J" +
|
||||
"}hQUQ\xbf[\x8d\xb0\xbf\xb2J\xf4\xecn\xc2" +
|
||||
"\xefS#\xec\xef^\x95\xfa\xeb\xb3\x84?L\xb8\x8a!" +
|
||||
"\xfd{H}\x02@\x7f\x98\xf0\xaf\x13\x1eSC\xfa\xf7" +
|
||||
"Ui\xff\x1c\xe1\xe7\x09\x8f\xc7B\xfa\xf7\xb8J\xfd\xf2" +
|
||||
"c\x84?MxC<\xa4\x7f\xdf\x92v\x9e&\xfc\x02" +
|
||||
"\xe1\x89\x86\x90\xfe=\xaf\xbe\x0c\xa0_ \xfc\x15\xc2\x1b" +
|
||||
"\x13!\xfd\xfb\x81J\xec\xf5E\xc2/\x12\xde\xd4\x18\xd2" +
|
||||
"\xbf\x1f\xaa\xc4\x1b^!\xfc\x0d\xc2\x9b\x9bB\xfawI" +
|
||||
"%\xf6\xfa:\xe1?%\xbc\xa59\xa4\x7foI?\x7f" +
|
||||
"L\xf8e\xc2[[B\xfa\xf7K\x95\xfa\xf7\xcb\x84\xbf" +
|
||||
"Gx[kH\xff~\xab\x12\xff\xf8\x0d\xe1\x1f\x10\xbe" +
|
||||
"\xa2-\xa4\x7f\x7fP\x89&}\xa0\xaa8.\xd9\xdf\x8a" +
|
||||
"\x90\xfd}(\x87\xfd+\xa97J\xf6\x97\x0c\xd9_\\" +
|
||||
"\xb2\xb9\x18\xd1\xa4v\xc9\xfe\xdaC\xf6\xd7&\xd9Y+" +
|
||||
"\xe1\xab%\xfbK\x85\xeco\x95d\xcduR\xce\xd4\x90" +
|
||||
"\xfd\xad\x97\xacv#\xe1[$\xfb[\x19\xb2\xbf^i" +
|
||||
"\xe7\x06\xc2o\x91\xec\x8f\x85\xeco\x9b\xd4\xbf\x85\xf0a" +
|
||||
"\xc9\xfe\xd2!\xfb\x1b\x94,r\x98\xf01\xc9\xfeV\x85" +
|
||||
"\xecoT\xda\xc9\x12~\xa7d\x7fW\x85\xec\xef@\xec" +
|
||||
"\xcc<\xf6\xbd\xe6\xea\x90\xfe\x1d\x91\xac\xb6\xce\xbe;n" +
|
||||
"\x0b\xe9\xdfB\xf6\xdd9\x18\xd2\xbf\xe3\xd2N\x91\xf0\xbb" +
|
||||
"\x09_\xbb3\xa4\x7fe\x89\xd7Yy\xd7PH\xff\xee" +
|
||||
"\x91x\x9d}_3\x1c\xd2\xbf\xfb\xa5\x9f5\xf6\x8d\x99" +
|
||||
"\x90\xfd=\x12\xdb\x00\xa0?H\xf0\xb9\x98\x82\xf2^\xde" +
|
||||
"\x1as\x05$=/\xd2&sk\x96\x97\xbd}6V" +
|
||||
"\xbej\xed\x1a\x90\x9fj\xea\x0a\x93.?&\xb2f\x1e" +
|
||||
"\x12\x05\xdf\xab\xa1\x06\xf7\x0a\x93\x0ew\xd1\x88\xdc\xd0\xd7" +
|
||||
"j\x9a!\x96\x1c\xab\xc6\x14\xe4M\xbd0`\xb1$\xeb" +
|
||||
"\xd8y\x92&\x96zO9(\xdcrU#\xfa\xfe)" +
|
||||
"\x91sv\x0b\xee\xd6\xde\x98r\xdc\x9c\x18r8\xa8\x9e" +
|
||||
"_\x03Mo\x8c\xbb\xc7\x88\x7f\xd4\x07.\xf2\x92'\xb2" +
|
||||
"\xdc\x87$M~\x01\xec\xd8(\xab\x9fis\xb4\"L" +
|
||||
"\xcbq\xfd\xdd\x82\x03\xd6\xc7\xf3]>5e\xe6F\xd1" +
|
||||
"1\xc2o]Q&\x92\xe7\x9e\xee\xf3\xdc\xb1\xe8\xb8\xd3" +
|
||||
"\x8e!h\x160o\xa2aa\x1bs!#>r\x91" +
|
||||
"\x06\x07,\xcb\x99\x8dF\xa8\xbe\x1c\xbeS\xd4\xcd<\xda" +
|
||||
"5\xba\x18~0Zl4\xc4\xb3\x0eV\xe3\x89\x0be" +
|
||||
"\x07\x05V\xe3\xbd\xe0\xdd\xa20\xb85\xca\xe7 3." +
|
||||
"\xf2\xc2\x9e\x8f\xcb\xb8\x0d\xe6|sF\xd4'\xec\x0b\xcf" +
|
||||
"\xe2CC\xbb`\xc0\xce\xf3|\xc4TE@\xfcCH" +
|
||||
"\x11.\x92\xdd\x81\x8e?\xe8NG\x13Ff\xf3\x1e\x9a" +
|
||||
"\xf0\x90[2\xbd\xc8@2\xf7\x96\x94\x14K\x96'\x06" +
|
||||
"m\x032\xbb-\xd3\x10U<\x91/z\xcb\xfcV\xbf" +
|
||||
"\xe0\\\xbee\x99\x9d\xfb\xff\xbb\x92\xfeD\x85\xfeJ\xf6" +
|
||||
"\x9b(\x17\xe5)\x1av)\xdb\xc2\xb6\xb0w\\\x9e\xa2" +
|
||||
"=\xe1)\xda=\"\xdb\xc2\xf5\x9b\x01N\xe5J\xaeK" +
|
||||
"\xa4\xa9\xe8\x0a\xc3\xcc\xf9\x02\xd0\x08\x8a\x8e\xe7\x99\x93\x96" +
|
||||
"\xa8\x9e\xad\xb6!wAr\x8a\x9b\xd6\x15/\xf4\x87+" +
|
||||
"W\xc7\xb5\x9b\xe3\x05\xd7\xd9\xdb\x01\xb4/\xaa\xa8=\x16" +
|
||||
"i,\x1e\x1d\x8f\xdcq+\x18\xba\xfe\xd4\xe1\xc8\x1d\xb7" +
|
||||
"\xaa\x84\x0d\xd63g\x00\xb4\xa7U\xd4.\xd4\xaf\xb3\x9f" +
|
||||
"\xa7\xb7\x9fSQ{#r\x9d}\x89\x14_WQ\xfb" +
|
||||
"\x8d\x82\x03\\f\xd7\x82t\xb5h\xae\xf5\xdcq|n" +
|
||||
"\xddnZ\xa0\x0a\x0f\x1bA\xc1F\x88\\\x83\x0b\xe3v" +
|
||||
"\xd3\x12\x1e\xd4$V\xe5\xfb\x1b\xa0W\xbd\xedn\xad|" +
|
||||
"\xf1\xa8\x8apX\xf8\xdc\xb4<\xa8_\x87\xd7\xfesL" +
|
||||
"\xe5:|9\xc4`^\x16\\)\xf8\xd5\xcf\x83\xc3\x03" +
|
||||
"\xa1\x0f\x92\xba\xd6\xd6`\x175w\xc3*jc\x0aV" +
|
||||
"\x97`\x94\xa2\x9dUQ\xbb3\xb2\x04\x07(\x88\xfbU" +
|
||||
"\xd4>\xabD\xa6\x1c\xa1B\xcb\x8b\xda\xff\x05\x00\x00\xff" +
|
||||
"\xff1<\xb37"
|
||||
|
||||
func RegisterSchema(reg *schemas.Registry) {
|
||||
reg.Register(&schemas.Schema{
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"description":"Synthetic Go-produced v2 current-road and data-only preview fixture; no real road data.","event_base64":"AAAAACQAAAAAAAAAAgABAADyBSoBAAAAjwAAAAAAAAAAAAAAEAAHAAAAoEEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIABQABAAAAAAAAAAIAAAAAAHBBgAELJwEAAAAA8gUqAQAAAAEAAAAAAAAAKQAAAAAAAAAAAHBCAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAoCAABhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhAAAAAAAAAAA="}
|
||||
@@ -1,7 +1,6 @@
|
||||
package cereal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"capnproto.org/go/capnp/v3"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
@@ -61,7 +60,58 @@ func TestMapdV1HostWireFixture(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(raw, expected) {
|
||||
actualMessage, err := capnp.Unmarshal(raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
oldMessage, err := capnp.Unmarshal(expected)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
actualRoot, err := actualMessage.Root()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
oldRoot, err := oldMessage.Root()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Appended default fields can enlarge storage without changing old values.
|
||||
equal, err := capnp.Equal(actualRoot, oldRoot)
|
||||
if err != nil || !equal {
|
||||
t.Fatal("provider wire differs from shared host fixture; review both schemas")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreviewHostWireFixture(t *testing.T) {
|
||||
data, err := os.ReadFile("testdata/mapd_preview_wire.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var fixture struct {
|
||||
EventBase64 string `json:"event_base64"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &fixture); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, err := base64.StdEncoding.DecodeString(fixture.EventBase64)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
msg, err := capnp.Unmarshal(raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
event, err := log.ReadRootEvent(msg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := event.MapdOut()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
boundary, err := out.PreviewBoundaryId()
|
||||
if err != nil || !event.Valid() || out.SampleVersion() != 2 || out.SpeedLimit() != 20 || out.PreviewSpeedLimit() != 15 || out.PreviewDistance() != 60 || len(boundary) != 64 || out.PreviewWayId() != 2 {
|
||||
t.Fatal("provider preview differs from shared host fixture")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"pfeifer.dev/mapd/cereal/custom"
|
||||
"pfeifer.dev/mapd/maps"
|
||||
)
|
||||
|
||||
const previewHorizonM = 500
|
||||
const previewMaxWays = 64
|
||||
|
||||
type LimitPreview struct {
|
||||
Speed float32
|
||||
Distance float32
|
||||
BoundaryID string
|
||||
WayID int64
|
||||
}
|
||||
|
||||
// A preview follows only a unique legal continuation, never a guessed turn.
|
||||
func (s *State) previewLimit() LimitPreview {
|
||||
if !s.ShadowOnly || s.SnapshotID == "" || s.CurrentWay.SelectionType != custom.WaySelectionType_current || s.MatchedLimitMps <= 0 {
|
||||
return LimitPreview{}
|
||||
}
|
||||
way, forward, position := s.CurrentWay.Way, s.CurrentWay.OnWay.IsForward, s.Position
|
||||
distance := float32(0)
|
||||
seen := map[int64]bool{way.Id(): true}
|
||||
for range previewMaxWays {
|
||||
_, end := way.GetStartEnd(forward)
|
||||
if distance+position.DistanceTo(end) > previewHorizonM {
|
||||
return LimitPreview{}
|
||||
}
|
||||
segment, found, err := way.DistanceToNode(position, forward, end)
|
||||
if err != nil || !found || segment < 0 || math.IsNaN(float64(segment)) || math.IsInf(float64(segment), 0) {
|
||||
return LimitPreview{}
|
||||
}
|
||||
distance += segment
|
||||
box := s.Data.OverlapBox()
|
||||
if distance <= 0 || distance > previewHorizonM || !box.PosInside(end) {
|
||||
return LimitPreview{}
|
||||
}
|
||||
matches, err := way.MatchingWays(&s.Data, end)
|
||||
if err != nil {
|
||||
return LimitPreview{}
|
||||
}
|
||||
legal := map[int64]maps.Way{}
|
||||
for _, next := range matches {
|
||||
first, last := next.GetStartEnd(true)
|
||||
if first.Equals(last) && !next.OneWay() {
|
||||
return LimitPreview{} // A two-way loop has two possible entry directions.
|
||||
}
|
||||
if next.Id() > 0 && (next.IsForwardFrom(end) || !next.OneWay()) {
|
||||
legal[next.Id()] = next
|
||||
}
|
||||
}
|
||||
if len(legal) != 1 {
|
||||
return LimitPreview{}
|
||||
}
|
||||
var next maps.Way
|
||||
for _, candidate := range legal {
|
||||
next = candidate
|
||||
}
|
||||
if seen[next.Id()] {
|
||||
return LimitPreview{}
|
||||
}
|
||||
nextForward := next.IsForwardFrom(end)
|
||||
road := CurrentWay{Way: next, OnWay: maps.OnWayResult{IsForward: nextForward}}
|
||||
speed := road.EffectiveMaxSpeed()
|
||||
if speed <= 0 || math.IsNaN(speed) || math.IsInf(speed, 0) || speed > math.MaxFloat32 {
|
||||
return LimitPreview{}
|
||||
}
|
||||
if math.Abs(speed-float64(s.MatchedLimitMps)) > .02 {
|
||||
key := fmt.Sprintf("%s:%d:%d:%t:%.7f:%.7f", s.SnapshotID, way.Id(), next.Id(), nextForward, end.Lat(), end.Lon())
|
||||
return LimitPreview{float32(speed), distance, fmt.Sprintf("%x", sha256.Sum256([]byte(key))), next.Id()}
|
||||
}
|
||||
seen[next.Id()] = true
|
||||
way, forward, position = next, nextForward, end
|
||||
}
|
||||
return LimitPreview{}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"capnproto.org/go/capnp/v3"
|
||||
"pfeifer.dev/mapd/cereal"
|
||||
"pfeifer.dev/mapd/cereal/custom"
|
||||
"pfeifer.dev/mapd/cereal/offline"
|
||||
"pfeifer.dev/mapd/maps"
|
||||
m "pfeifer.dev/mapd/math"
|
||||
)
|
||||
|
||||
func previewState(t *testing.T, specs []syntheticWay, index int, forward bool, latitude float64) State {
|
||||
t.Helper()
|
||||
tile := packedRoadTile(t, specs)
|
||||
return State{ShadowOnly: true, SnapshotID: "verified-snapshot", Data: tile,
|
||||
Position: m.NewPosition(latitude, -97.9), MatchedLimitMps: float32(specs[index].speed),
|
||||
CurrentWay: CurrentWay{Way: tile.Ways.At(index), OnWay: maps.OnWayResult{IsForward: forward}, SelectionType: custom.WaySelectionType_current}}
|
||||
}
|
||||
|
||||
func previewRoads() []syntheticWay {
|
||||
return []syntheticWay{
|
||||
{1, 20, 35.1, -97.9, 35.101, -97.9, false, 2},
|
||||
{2, 20, 35.101, -97.9, 35.102, -97.9, false, 2},
|
||||
{3, 10, 35.102, -97.9, 35.103, -97.9, false, 2},
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreviewRawDistanceAndStableBoundaryAcrossApproachWays(t *testing.T) {
|
||||
s := previewState(t, previewRoads(), 0, true, 35.1005)
|
||||
a := s.previewLimit()
|
||||
s = previewState(t, previewRoads(), 1, true, 35.1015)
|
||||
b := s.previewLimit()
|
||||
if a.Speed != 10 || a.WayID != 3 || a.BoundaryID == "" || a.BoundaryID != b.BoundaryID || math.Abs(float64(a.Distance-b.Distance)-111.2) > 1 {
|
||||
t.Fatalf("unexpected road preview: %+v %+v", a, b)
|
||||
}
|
||||
s.Position = m.NewPosition(35.1014, -97.9)
|
||||
if s.previewLimit().Distance <= b.Distance {
|
||||
t.Fatal("raw distance was held at a previous minimum")
|
||||
}
|
||||
s.SnapshotID = "changed-snapshot"
|
||||
if s.previewLimit().BoundaryID == b.BoundaryID {
|
||||
t.Fatal("boundary identity survived a different snapshot")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreviewDirectionUnknownLimitsBranchesAndHorizon(t *testing.T) {
|
||||
s := previewState(t, previewRoads(), 2, false, 35.1025)
|
||||
if p := s.previewLimit(); p.Speed != 20 || p.WayID != 2 || p.Distance <= 0 {
|
||||
t.Fatalf("reverse traversal failed: %+v", p)
|
||||
}
|
||||
for _, mode := range []string{"branch", "unknown", "oneway", "horizon", "unqualified", "unverified"} {
|
||||
t.Run(mode, func(t *testing.T) {
|
||||
roads := previewRoads()
|
||||
index, forward, latitude := 0, true, 35.1005
|
||||
switch mode {
|
||||
case "branch":
|
||||
roads = append(roads, syntheticWay{4, 30, 35.101, -97.9, 35.102, -97.899, false, 2})
|
||||
case "unknown":
|
||||
roads[1].speed = 0
|
||||
case "oneway":
|
||||
roads[1].oneWay = true
|
||||
index, forward, latitude = 2, false, 35.1025
|
||||
case "horizon":
|
||||
roads[0].lat1 = 35.09
|
||||
latitude = 35.091
|
||||
}
|
||||
s := previewState(t, roads, index, forward, latitude)
|
||||
if mode == "unqualified" {
|
||||
s.CurrentWay.SelectionType = custom.WaySelectionType_possible
|
||||
}
|
||||
if mode == "unverified" {
|
||||
s.SnapshotID = ""
|
||||
}
|
||||
if p := s.previewLimit(); p != (LimitPreview{}) {
|
||||
t.Fatalf("uncertain preview escaped: %+v", p)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreviewClosedWayRequiresOneLegalDirection(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
oneWay, branch, wantPreview bool
|
||||
}{
|
||||
{"one-way loop", true, false, true},
|
||||
{"two-way loop", false, false, false},
|
||||
{"two-way loop plus continuation", false, true, false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
roads := []syntheticWay{
|
||||
{1, 20, 35.1, -97.9, 35.101, -97.9, true, 2},
|
||||
{2, 10, 35.101, -97.9, 35.102, -97.9, tc.oneWay, 2},
|
||||
}
|
||||
if tc.branch {
|
||||
roads = append(roads, syntheticWay{3, 15, 35.101, -97.9, 35.102, -97.899, false, 2})
|
||||
}
|
||||
decoded, err := capnp.UnmarshalPacked(packedRoadTileBytes(t, roads))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root, err := decoded.Root()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
msg, _, err := capnp.NewMessage(capnp.SingleSegment(nil))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := msg.SetRoot(root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, err := offline.ReadRootOffline(msg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ways, err := raw.Ways()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
loop := ways.At(1)
|
||||
loop.SetMaxLon(-97.898)
|
||||
nodes, err := loop.NewNodes(4)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, p := range [][2]float64{{35.101, -97.9}, {35.102, -97.9}, {35.102, -97.899}, {35.101, -97.9}} {
|
||||
nodes.At(i).SetLatitude(p[0])
|
||||
nodes.At(i).SetLongitude(p[1])
|
||||
}
|
||||
data, err := msg.MarshalPacked()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tile := maps.ReadOffline(data)
|
||||
if !tile.Loaded {
|
||||
t.Fatal("closed road tile rejected")
|
||||
}
|
||||
s := State{ShadowOnly: true, SnapshotID: "verified-snapshot", Data: tile,
|
||||
Position: m.NewPosition(35.1005, -97.9), MatchedLimitMps: 20,
|
||||
CurrentWay: CurrentWay{Way: tile.Ways.At(0), OnWay: maps.OnWayResult{IsForward: true}, SelectionType: custom.WaySelectionType_current}}
|
||||
p := s.previewLimit()
|
||||
if tc.wantPreview {
|
||||
if p.Speed != 10 || p.WayID != 2 || p.Distance <= 0 || p.BoundaryID == "" {
|
||||
t.Fatalf("legal loop preview missing: %+v", p)
|
||||
}
|
||||
} else if p != (LimitPreview{}) {
|
||||
t.Fatalf("ambiguous loop produced a preview: %+v", p)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShadowPreviewSharesFixFreshnessWithoutControlOutputs(t *testing.T) {
|
||||
roads := []syntheticWay{{1, 20, 35.149, -97.9, 35.151, -97.9, false, 2}, {2, 10, 35.151, -97.9, 35.153, -97.9, false, 2}}
|
||||
data := packedRoadTileBytes(t, roads)
|
||||
id := strings.Repeat("a", 64)
|
||||
dir := filepath.Join(t.TempDir(), id)
|
||||
area := maps.Area{Box: m.Box{MinPos: m.NewPosition(35, -98), MaxPos: m.NewPosition(35.25, -97.75)}}
|
||||
path := maps.GenerateBoundsFileName(area, maps.OfflineSettings{OutputDirectory: dir})
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rel, err := filepath.Rel(dir, path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
load := maps.SnapshotLoader(dir, maps.SnapshotReceipt{Tiles: []maps.SnapshotTile{{Path: rel, Size: int64(len(data)), SHA256: fmt.Sprintf("%x", sha256.Sum256(data))}}})
|
||||
s := State{ShadowOnly: true, SnapshotID: id}
|
||||
s.Init()
|
||||
boot := uint64(10_100_000_000)
|
||||
s.bootNow = func() uint64 { return boot }
|
||||
sample := syntheticSample(t, cereal.GpsSourceExternal, 10_000_000_000, 1, true, true)
|
||||
s.ProcessGps(sample, true, load, time.Unix(0, 0))
|
||||
sample.SourceChanged, sample.FixMonoTime = false, 10_050_000_000
|
||||
s.ProcessGps(sample, true, load, time.Unix(0, 1))
|
||||
event, output := decodedOutput(t, &s)
|
||||
boundary, _ := output.PreviewBoundaryId()
|
||||
if !event.Valid() || output.SpeedLimit() != 20 || output.PreviewSpeedLimit() != 10 || boundary == "" || output.PreviewDistance() <= 0 || output.NextSpeedLimit() != 0 || output.SuggestedSpeed() != 0 {
|
||||
t.Fatalf("shadow preview: valid=%v status=%v way=%v limit=%v preview=%+v", event.Valid(), output.RoadStatus(), output.WaySelectionType(), output.SpeedLimit(), s.LimitPreview)
|
||||
}
|
||||
boot = 10_600_000_000
|
||||
event, output = decodedOutput(t, &s)
|
||||
boundary, _ = output.PreviewBoundaryId()
|
||||
if event.Valid() || output.PreviewSpeedLimit() != 0 || boundary != "" {
|
||||
t.Fatal("expired GPS retained an upcoming limit")
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
@@ -162,7 +163,6 @@ func GenerateOffline(s OfflineSettings) error {
|
||||
MaxSpeedBackward: ParseMaxSpeed(tags["maxspeed:backward"]),
|
||||
MaxSpeedAdvisory: ParseMaxSpeed(tags["maxspeed:advisory"]),
|
||||
Lanes: uint8(lanes),
|
||||
OneWay: tags["oneway"] == "yes",
|
||||
Id: int64(way.ID),
|
||||
HighwayClass: HighwayClassFromTag(tags["highway"]),
|
||||
|
||||
@@ -191,6 +191,17 @@ func GenerateOffline(s OfflineSettings) error {
|
||||
tmpWay.Nodes[i].Latitude = n.Lat
|
||||
tmpWay.Nodes[i].Longitude = n.Lon
|
||||
}
|
||||
switch tags["oneway"] {
|
||||
case "yes", "1":
|
||||
tmpWay.OneWay = true
|
||||
case "-1":
|
||||
tmpWay.OneWay = true
|
||||
slices.Reverse(tmpWay.Nodes)
|
||||
tmpWay.MaxSpeedForward, tmpWay.MaxSpeedBackward = tmpWay.MaxSpeedBackward, tmpWay.MaxSpeedForward
|
||||
tmpWay.MaxSpeedForwardConditional, tmpWay.MaxSpeedBackwardConditional = tmpWay.MaxSpeedBackwardConditional, tmpWay.MaxSpeedForwardConditional
|
||||
case "":
|
||||
tmpWay.OneWay = tags["junction"] == "roundabout" || tags["highway"] == "motorway"
|
||||
}
|
||||
tmpWay.Box.MinPos = m.NewPosition(minLat, minLon)
|
||||
tmpWay.Box.MaxPos = m.NewPosition(maxLat, maxLon)
|
||||
scannedWays = append(scannedWays, tmpWay)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package maps
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
m "pfeifer.dev/mapd/math"
|
||||
)
|
||||
|
||||
func TestGeneratorPreservesFixedOneWayDirectionAndSpeedTags(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name, oneWay, highway, junction string
|
||||
wantOneWay, reverse bool
|
||||
}{
|
||||
{"ordinary", "", "residential", "", false, false},
|
||||
{"yes", "yes", "residential", "", true, false},
|
||||
{"numeric", "1", "residential", "", true, false},
|
||||
{"reverse", "-1", "residential", "", true, true},
|
||||
{"roundabout", "", "residential", "roundabout", true, false},
|
||||
{"motorway", "", "motorway", "", true, false},
|
||||
{"roundabout explicit no", "no", "residential", "roundabout", false, false},
|
||||
{"roundabout explicit zero", "0", "residential", "roundabout", false, false},
|
||||
{"motorway explicit no", "no", "motorway", "", false, false},
|
||||
{"motorway explicit zero", "0", "motorway", "", false, false},
|
||||
{"motorway link", "", "motorway_link", "", false, false},
|
||||
{"reversible", "reversible", "residential", "roundabout", false, false},
|
||||
{"alternating", "alternating", "motorway", "", false, false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
forwardRule, backwardRule := "15 @ (Mo-Fr 08:00-09:00)", "25 @ (Sa 10:00-11:00)"
|
||||
tags := []string{"highway", tc.highway, "maxspeed", "30 mph",
|
||||
"maxspeed:forward", "10", "maxspeed:backward", "20",
|
||||
"maxspeed:forward:conditional", forwardRule, "maxspeed:backward:conditional", backwardRule}
|
||||
if tc.oneWay != "" {
|
||||
tags = append(tags, "oneway", tc.oneWay)
|
||||
}
|
||||
if tc.junction != "" {
|
||||
tags = append(tags, "junction", tc.junction)
|
||||
}
|
||||
root := t.TempDir()
|
||||
pbf := filepath.Join(root, "road.osm.pbf")
|
||||
if err := os.WriteFile(pbf, syntheticPBF(tags...), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
output := filepath.Join(root, "tiles")
|
||||
if err := GenerateOffline(OfflineSettings{InputFile: pbf, OutputDirectory: output, Overlap: .001,
|
||||
Box: m.Box{MinPos: m.NewPosition(35, -98), MaxPos: m.NewPosition(35.25, -97.75)}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tile, err := FindWaysAroundPositionIn(m.NewPosition(35.15, -97.9), output)
|
||||
if err != nil || !tile.Loaded || tile.Ways.Len() != 1 {
|
||||
t.Fatalf("generated road not loaded: %v", err)
|
||||
}
|
||||
way := tile.Ways.At(0)
|
||||
if way.Nodes.Len() != 2 {
|
||||
t.Fatalf("generated road has %d nodes", way.Nodes.Len())
|
||||
}
|
||||
first, last := 35.1, 35.2
|
||||
forwardSpeed, backwardSpeed := 10./3.6, 20./3.6
|
||||
if tc.reverse {
|
||||
first, last = last, first
|
||||
forwardSpeed, backwardSpeed = backwardSpeed, forwardSpeed
|
||||
forwardRule, backwardRule = backwardRule, forwardRule
|
||||
}
|
||||
firstNode, lastNode := way.Nodes.At(0), way.Nodes.At(1)
|
||||
if way.Id() != 42 || way.OneWay() != tc.wantOneWay ||
|
||||
math.Abs(firstNode.Lat()-first) > 1e-9 || math.Abs(lastNode.Lat()-last) > 1e-9 ||
|
||||
math.Abs(way.MaxSpeed()-13.4112) > 1e-6 ||
|
||||
math.Abs(way.MaxSpeedForward()-forwardSpeed) > 1e-5 || math.Abs(way.MaxSpeedBackward()-backwardSpeed) > 1e-5 ||
|
||||
way.ConditionalMaxSpeedRaw(true) != forwardRule || way.ConditionalMaxSpeedRaw(false) != backwardRule {
|
||||
t.Fatalf("generated road lost direction or speed metadata: oneWay=%v forward=%v backward=%v",
|
||||
way.OneWay(), way.MaxSpeedForward(), way.MaxSpeedBackward())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -309,17 +309,25 @@ func pbfBlock(kind string, raw []byte) []byte {
|
||||
data = append(data, header...)
|
||||
return append(data, blob...)
|
||||
}
|
||||
func syntheticPBF() []byte {
|
||||
func syntheticPBF(tags ...string) []byte {
|
||||
header := pbfBytesField(nil, 4, []byte("OsmSchema-V0.6"))
|
||||
header = pbfBytesField(header, 5, []byte("LocationsOnWays"))
|
||||
if len(tags) == 0 {
|
||||
tags = []string{"highway", "residential", "maxspeed", "30 mph"}
|
||||
}
|
||||
var table []byte
|
||||
for _, value := range []string{"", "highway", "residential", "maxspeed", "30 mph"} {
|
||||
for _, value := range append([]string{""}, tags...) {
|
||||
table = pbfBytesField(table, 1, []byte(value))
|
||||
}
|
||||
var keys, values []uint64
|
||||
for i := 0; i < len(tags); i += 2 {
|
||||
keys = append(keys, uint64(i+1))
|
||||
values = append(values, uint64(i+2))
|
||||
}
|
||||
var way []byte
|
||||
way = pbfVarintField(way, 1, 42)
|
||||
way = pbfBytesField(way, 2, pbfPacked(1, 3))
|
||||
way = pbfBytesField(way, 3, pbfPacked(2, 4))
|
||||
way = pbfBytesField(way, 2, pbfPacked(keys...))
|
||||
way = pbfBytesField(way, 3, pbfPacked(values...))
|
||||
way = pbfBytesField(way, 8, pbfPacked(protowire.EncodeZigZag(1), protowire.EncodeZigZag(1)))
|
||||
way = pbfBytesField(way, 9, pbfPacked(protowire.EncodeZigZag(351000000), protowire.EncodeZigZag(1000000)))
|
||||
way = pbfBytesField(way, 10, pbfPacked(protowire.EncodeZigZag(-979000000), protowire.EncodeZigZag(0)))
|
||||
|
||||
@@ -149,8 +149,12 @@ func (w *Way) IsForwardFrom(matchNode m.Position) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
lastNode := w.Nodes.At(w.Nodes.Len()-1)
|
||||
return !lastNode.Equals(matchNode)
|
||||
lastNode := w.Nodes.At(w.Nodes.Len() - 1)
|
||||
if lastNode.Equals(matchNode) {
|
||||
// A closed one-way can be entered at its repeated endpoint in stored order.
|
||||
return w.OneWay() && matchNode.Equals(w.Nodes.At(0))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func IsForward(lineStart m.Position, lineEnd m.Position, bearing float64) bool {
|
||||
@@ -431,7 +435,7 @@ func (w *Way) DistanceFrom(pos m.Position) (DistanceResult, error) {
|
||||
minIdx := 0
|
||||
for i := 0; i < w.Nodes.Len()-1; i++ {
|
||||
nodeStart := w.Nodes.At(i)
|
||||
nodeEnd := w.Nodes.At(i+1)
|
||||
nodeEnd := w.Nodes.At(i + 1)
|
||||
line := m.Line{Start: nodeStart, End: nodeEnd}
|
||||
linePosition := line.NearestPosition(pos)
|
||||
distance := pos.DistanceTo(linePosition.Pos)
|
||||
@@ -447,7 +451,7 @@ func (w *Way) DistanceFrom(pos m.Position) (DistanceResult, error) {
|
||||
onWayDistance := minNodeStart.DistanceTo(minLinePosition.Pos)
|
||||
for i := range minIdx {
|
||||
nodeStart := w.Nodes.At(i)
|
||||
nodeEnd := w.Nodes.At(i+1)
|
||||
nodeEnd := w.Nodes.At(i + 1)
|
||||
onWayDistance += nodeStart.DistanceTo(nodeEnd)
|
||||
}
|
||||
|
||||
@@ -491,8 +495,7 @@ func (w *Way) MatchingWays(offlineMaps *Offline, matchNode m.Position) ([]Way, e
|
||||
continue
|
||||
}
|
||||
|
||||
box := way.Box()
|
||||
if box.Equals(w.Box()) {
|
||||
if way.Id() == w.Id() {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -55,3 +55,48 @@ func TestBearingAlignmentIsAnUndirectedNonnegativePenalty(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWayEntryDirectionForOpenAndClosedWays(t *testing.T) {
|
||||
start, end := m.NewPosition(35.15, -97.9), m.NewPosition(35.151, -97.9)
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
closed, oneWay, fromEnd, wantForward bool
|
||||
}{
|
||||
{"open start", false, true, false, true},
|
||||
{"open end", false, true, true, false},
|
||||
{"closed one-way", true, true, false, true},
|
||||
{"closed two-way", true, false, false, false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, err := offline.NewRootWay(seg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw.SetOneWay(tc.oneWay)
|
||||
positions := []m.Position{start, end}
|
||||
if tc.closed {
|
||||
positions = append(positions, start)
|
||||
}
|
||||
nodes, err := raw.NewNodes(int32(len(positions)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, p := range positions {
|
||||
nodes.At(i).SetLatitude(p.Lat())
|
||||
nodes.At(i).SetLongitude(p.Lon())
|
||||
}
|
||||
from := start
|
||||
if tc.fromEnd {
|
||||
from = end
|
||||
}
|
||||
way := NewWay(raw)
|
||||
if got := way.IsForwardFrom(from); got != tc.wantForward {
|
||||
t.Fatalf("entry direction: got %v want %v", got, tc.wantForward)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ type syntheticWay struct {
|
||||
|
||||
// The actual offline Capnp representation is packed, decoded, and passed to
|
||||
// State; tests do not mock GetCurrentWay or BuildMessage.
|
||||
func packedRoadTile(tb testing.TB, specs []syntheticWay) maps.Offline {
|
||||
func packedRoadTileBytes(tb testing.TB, specs []syntheticWay) []byte {
|
||||
tb.Helper()
|
||||
msg, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
|
||||
if err != nil {
|
||||
@@ -65,6 +65,12 @@ func packedRoadTile(tb testing.TB, specs []syntheticWay) maps.Offline {
|
||||
if err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func packedRoadTile(tb testing.TB, specs []syntheticWay) maps.Offline {
|
||||
tb.Helper()
|
||||
data := packedRoadTileBytes(tb, specs)
|
||||
parsed := maps.ReadOffline(data)
|
||||
if !parsed.Loaded {
|
||||
tb.Fatal("packed tile rejected")
|
||||
|
||||
@@ -46,6 +46,7 @@ type State struct {
|
||||
SourceGeneration uint64
|
||||
ProducerSession uint64
|
||||
MatchedLimitMps float32
|
||||
LimitPreview LimitPreview
|
||||
lastMapLoadAttempt time.Time
|
||||
bootNow func() uint64
|
||||
// The fix the current road result was computed from. The loop runs at
|
||||
@@ -90,6 +91,7 @@ func (s *State) clearRoadDerived() {
|
||||
s.TargetVelocities = nil
|
||||
s.MapCurveSpeed = 0
|
||||
s.MatchedLimitMps = 0
|
||||
s.LimitPreview = LimitPreview{}
|
||||
s.NextHazard.Reset()
|
||||
s.NextAdvisorySpeed.Reset()
|
||||
s.clearSpeedLimitEvidence()
|
||||
@@ -255,6 +257,7 @@ func (s *State) ProcessGps(sample cereal.GpsSample, success bool, load MapLoader
|
||||
s.MatchedLimitMps = float32(limit)
|
||||
s.RoadStatus = custom.MapdOut_SampleStatus_matchedLimit
|
||||
}
|
||||
s.LimitPreview = s.previewLimit()
|
||||
}
|
||||
|
||||
func (s *State) SuggestedSpeed() float32 {
|
||||
@@ -361,6 +364,12 @@ func (s *State) buildMessageAt(now uint64) (*capnp.Message, error) {
|
||||
output.SetRoadName(s.CurrentWay.Way.Name())
|
||||
|
||||
output.SetSpeedLimit(s.MatchedLimitMps)
|
||||
if s.LimitPreview.BoundaryID != "" {
|
||||
output.SetPreviewSpeedLimit(s.LimitPreview.Speed)
|
||||
output.SetPreviewDistance(s.LimitPreview.Distance)
|
||||
output.SetPreviewBoundaryId(s.LimitPreview.BoundaryID)
|
||||
output.SetPreviewWayId(s.LimitPreview.WayID)
|
||||
}
|
||||
|
||||
output.SetConditionalSpeedLimit(s.CurrentWay.ConditionalMaxSpeedRaw())
|
||||
|
||||
|
||||
@@ -20,3 +20,6 @@ libmessaging.*
|
||||
libmessaging_shared.*
|
||||
.sconsign.dblite
|
||||
.mypy_cache/
|
||||
|
||||
/msgq/_ipc.pyi
|
||||
/msgq/visionipc/_visionipc.pyi
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
include README.md pyproject.toml setup.py SConstruct SConscript
|
||||
recursive-include site_scons *.py
|
||||
recursive-include msgq *.py *.pyx *.pxd *.h *.cc
|
||||
global-exclude *.so *.o *.os *.a *.pyc
|
||||
recursive-include msgq *.py py.typed *.h *.cc
|
||||
global-exclude *.pyi *.so *.o *.os *.a *.pyc
|
||||
|
||||
+33
-4
@@ -1,7 +1,29 @@
|
||||
Import('env', 'envCython', 'common')
|
||||
import sys
|
||||
import sysconfig
|
||||
from pathlib import Path
|
||||
|
||||
import nanobind
|
||||
|
||||
Import('env', 'arch', 'common')
|
||||
|
||||
envNanobind = env.Clone(LIBS=[])
|
||||
nb_dir = Path(nanobind.source_dir()).parent
|
||||
envNanobind.Append(CPPPATH=[sysconfig.get_paths()['include'], nanobind.include_dir(), str(nb_dir / "ext/robin_map/include")],
|
||||
CCFLAGS=["-fvisibility=hidden", "-fno-strict-aliasing"])
|
||||
if arch == "Darwin":
|
||||
envNanobind.Append(LINKFLAGS=["-bundle", "-undefined", "dynamic_lookup"])
|
||||
else:
|
||||
# Python symbols resolve at import time.
|
||||
envNanobind["LINKFLAGS"] = [f for f in env["LINKFLAGS"] if f != "-Wl,--no-undefined"] + ["-pthread", "-shared"]
|
||||
|
||||
# Third-party runtime sources do not follow our shadowing warning policy.
|
||||
envNanobindRuntime = envNanobind.Clone()
|
||||
envNanobindRuntime['CCFLAGS'] = [flag for flag in envNanobindRuntime['CCFLAGS'] if not flag.startswith('-Wshadow')]
|
||||
nanobind_lib = envNanobindRuntime.SharedObject('msgq/nb_combined.os', str(nb_dir / "src/nb_combined.cpp"))
|
||||
|
||||
|
||||
visionipc_dir = Dir('msgq/visionipc')
|
||||
python_suffix = ".so"
|
||||
|
||||
# Build msgq
|
||||
msgq_objects = env.SharedObject([
|
||||
@@ -10,7 +32,7 @@ msgq_objects = env.SharedObject([
|
||||
'msgq/msgq.cc',
|
||||
])
|
||||
msgq = env.Library('msgq', msgq_objects)
|
||||
msgq_python = envCython.Program('msgq/ipc_pyx.so', 'msgq/ipc_pyx.pyx', LIBS=envCython["LIBS"]+[msgq]+common)
|
||||
msgq_python = envNanobind.Program(f'msgq/_ipc{python_suffix}', ['msgq/ipc_py.cc', nanobind_lib], LIBS=envNanobind["LIBS"]+[msgq]+common)
|
||||
|
||||
# Build Vision IPC
|
||||
vipc_files = ['visionipc.cc', 'visionipc_server.cc', 'visionipc_client.cc']
|
||||
@@ -24,10 +46,17 @@ vipc_objects = env.SharedObject(vipc_sources)
|
||||
visionipc = env.Library('visionipc', vipc_objects)
|
||||
|
||||
|
||||
vipc_libs = envCython["LIBS"] + [visionipc, msgq] + common
|
||||
envCython.Program(f'{visionipc_dir.abspath}/visionipc_pyx.so', f'{visionipc_dir.abspath}/visionipc_pyx.pyx',
|
||||
vipc_libs = envNanobind["LIBS"] + [visionipc, msgq] + common
|
||||
visionipc_python = envNanobind.Program(f'{visionipc_dir.abspath}/_visionipc{python_suffix}', [f'{visionipc_dir.abspath}/visionipc_py.cc', nanobind_lib],
|
||||
LIBS=vipc_libs)
|
||||
|
||||
# ASan modules require a preloaded runtime before Python can import them.
|
||||
if "-fsanitize=address" not in env["LINKFLAGS"]:
|
||||
root = Dir('.').abspath
|
||||
for module in ('msgq._ipc', 'msgq.visionipc._visionipc'):
|
||||
env.Command(module.replace('.', '/') + '.pyi', [msgq_python, visionipc_python],
|
||||
f'"{sys.executable}" -m nanobind.stubgen -i "{root}" -m {module} -o "$TARGET"')
|
||||
|
||||
if GetOption('extras'):
|
||||
env.Program('msgq/test_runner', ['msgq/msgq_tests.cc'], LIBS=[msgq]+common)
|
||||
|
||||
|
||||
+1
-15
@@ -1,7 +1,6 @@
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import sysconfig
|
||||
|
||||
arch = subprocess.check_output(["uname", "-m"], encoding='utf8').rstrip()
|
||||
if platform.system() == "Darwin":
|
||||
@@ -14,7 +13,6 @@ cpppath = [
|
||||
"#/",
|
||||
'#msgq/',
|
||||
'/usr/lib/include',
|
||||
sysconfig.get_paths()['include'],
|
||||
]
|
||||
|
||||
AddOption('--minimal',
|
||||
@@ -62,21 +60,9 @@ env = Environment(
|
||||
CFLAGS="-std=gnu11",
|
||||
CXXFLAGS="-std=c++1z",
|
||||
CPPPATH=cpppath,
|
||||
CYTHONCFILESUFFIX=".cpp",
|
||||
tools=["default", "cython"]
|
||||
tools=["default"]
|
||||
)
|
||||
|
||||
Export('env', 'arch', 'common')
|
||||
|
||||
envCython = env.Clone(LIBS=[])
|
||||
envCython["CCFLAGS"] += ["-Wno-#warnings", "-Wno-cpp", "-Wno-shadow", "-Wno-deprecated-declarations"]
|
||||
envCython["CCFLAGS"].remove('-Werror')
|
||||
if arch == "Darwin":
|
||||
envCython["LINKFLAGS"] = ["-bundle", "-undefined", "dynamic_lookup"]
|
||||
else:
|
||||
envCython["LINKFLAGS"] = ["-pthread", "-shared"]
|
||||
|
||||
Export('envCython')
|
||||
|
||||
|
||||
SConscript(['SConscript'])
|
||||
|
||||
@@ -18,12 +18,12 @@ test:
|
||||
ty:
|
||||
run: ty check .
|
||||
codespell:
|
||||
run: codespell {files} -L ned,stdio,master --builtin clear,rare,informal,usage,code,names,en-GB_to_en-US -S uv.lock,*_pyx.cpp
|
||||
run: codespell {files} -L ned,stdio,master --builtin clear,rare,informal,usage,code,names,en-GB_to_en-US -S uv.lock
|
||||
files: git ls-tree -r HEAD --name-only
|
||||
cppcheck:
|
||||
run: cppcheck --error-exitcode=1 --inline-suppr --language=c++ --force --quiet -j4 --check-level=exhaustive $(git ls-files '*.cc' | grep -v -E '(msgq_tests|test_runner)\.cc')
|
||||
cpplint:
|
||||
run: cpplint --exclude=msgq/ipc_pyx.cpp --exclude=msgq/visionipc/visionipc_pyx.cpp --recursive --quiet --counting=detailed --linelength=240 --filter=-build,-legal,-readability,-runtime,-whitespace,+build/include_subdir,+build/forward_decl,+build/include_what_you_use,+build/deprecated,+whitespace/comma,+whitespace/line_length,+whitespace/empty_if_body,+whitespace/empty_loop_body,+whitespace/empty_conditional_body,+whitespace/forcolon,+whitespace/parens,+whitespace/semicolon,+whitespace/tab,+readability/braces msgq/
|
||||
run: cpplint --recursive --quiet --counting=detailed --linelength=240 --filter=-build,-legal,-readability,-runtime,-whitespace,+build/include_subdir,+build/forward_decl,+build/include_what_you_use,+build/deprecated,+whitespace/comma,+whitespace/line_length,+whitespace/empty_if_body,+whitespace/empty_loop_body,+whitespace/empty_conditional_body,+whitespace/forcolon,+whitespace/parens,+whitespace/semicolon,+whitespace/tab,+readability/braces msgq/
|
||||
|
||||
# *** tests ***
|
||||
test_runner:
|
||||
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
ipc_pyx.cpp
|
||||
@@ -1,8 +1,8 @@
|
||||
from msgq.ipc_pyx import Poller, SubSocket, PubSocket, SocketEventHandle, toggle_fake_events, \
|
||||
from msgq._ipc import Poller, SubSocket, PubSocket, SocketEventHandle, toggle_fake_events, \
|
||||
set_fake_prefix, get_fake_prefix, delete_fake_prefix, wait_for_one_event
|
||||
from msgq.ipc_pyx import MultiplePublishersError, IpcError
|
||||
from msgq._ipc import MultiplePublishersError, IpcError
|
||||
|
||||
from typing import Optional, List, Union
|
||||
from typing import Optional, List
|
||||
|
||||
__all__ = [
|
||||
"Poller",
|
||||
@@ -25,7 +25,7 @@ __all__ = [
|
||||
|
||||
NO_TRAVERSAL_LIMIT = 2**64-1
|
||||
|
||||
def fake_event_handle(endpoint: str, identifier: Optional[Union[str, bytes]] = None, override: bool = True, enable: bool = False) -> SocketEventHandle:
|
||||
def fake_event_handle(endpoint: str, identifier: Optional[str] = None, override: bool = True, enable: bool = False) -> SocketEventHandle:
|
||||
ident = identifier if identifier is not None else get_fake_prefix()
|
||||
handle = SocketEventHandle(endpoint, ident, override)
|
||||
if override:
|
||||
|
||||
Executable
BIN
Binary file not shown.
@@ -1,61 +0,0 @@
|
||||
# distutils: language = c++
|
||||
#cython: language_level=3
|
||||
|
||||
from libcpp.string cimport string
|
||||
from libcpp.vector cimport vector
|
||||
from libcpp cimport bool
|
||||
|
||||
|
||||
cdef extern from "msgq/event.h":
|
||||
cdef cppclass Event:
|
||||
@staticmethod
|
||||
int wait_for_one(vector[Event], int) except +
|
||||
|
||||
Event()
|
||||
Event(int)
|
||||
void set()
|
||||
int clear()
|
||||
void wait(int) except +
|
||||
bool peek()
|
||||
int fd()
|
||||
|
||||
cdef cppclass SocketEventHandle:
|
||||
@staticmethod
|
||||
void toggle_fake_events(bool)
|
||||
@staticmethod
|
||||
void set_fake_prefix(string)
|
||||
@staticmethod
|
||||
string fake_prefix()
|
||||
|
||||
SocketEventHandle(string, string, bool)
|
||||
bool is_enabled()
|
||||
void set_enabled(bool)
|
||||
Event recv_called()
|
||||
Event recv_ready()
|
||||
|
||||
|
||||
cdef extern from "msgq/ipc.h":
|
||||
cdef cppclass Message:
|
||||
void init(size_t)
|
||||
void init(char *, size_t)
|
||||
void close()
|
||||
size_t getSize()
|
||||
char *getData()
|
||||
|
||||
cdef cppclass SubSocket:
|
||||
SubSocket() nogil
|
||||
int connect(string, bool, size_t) nogil
|
||||
Message * receive(bool) nogil
|
||||
void setTimeout(int) nogil
|
||||
|
||||
cdef cppclass PubSocket:
|
||||
PubSocket()
|
||||
int connect(string, size_t)
|
||||
int sendMessage(Message *)
|
||||
int send(char *, size_t)
|
||||
bool all_readers_updated()
|
||||
|
||||
cdef cppclass Poller:
|
||||
Poller()
|
||||
void registerSocket(SubSocket *)
|
||||
vector[SubSocket*] poll(int) nogil
|
||||
@@ -0,0 +1,113 @@
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <nanobind/nanobind.h>
|
||||
#include <nanobind/stl/optional.h>
|
||||
#include <nanobind/stl/string.h>
|
||||
#include <nanobind/stl/vector.h>
|
||||
|
||||
#include "msgq/event.h"
|
||||
#include "msgq/ipc.h"
|
||||
|
||||
namespace nb = nanobind;
|
||||
|
||||
namespace {
|
||||
|
||||
struct IpcError : std::runtime_error {
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
struct MultiplePublishersError : IpcError {
|
||||
using IpcError::IpcError;
|
||||
};
|
||||
|
||||
void check_error(bool failed, int error, const std::string &endpoint = "") {
|
||||
if (!failed) return;
|
||||
std::string message = "Messaging failure " + (endpoint.empty() ? "" : "with " + endpoint) + ": " + std::strerror(error);
|
||||
if (error == EADDRINUSE) throw MultiplePublishersError(message);
|
||||
throw IpcError(message);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
NB_MODULE(_ipc, m) {
|
||||
// Daemon threads can still hold sockets at exit.
|
||||
nb::set_leak_warnings(false);
|
||||
|
||||
nb::exception<IpcError> ipc_error(m, "IpcError");
|
||||
nb::exception<MultiplePublishersError>(m, "MultiplePublishersError", ipc_error);
|
||||
|
||||
m.def("toggle_fake_events", &SocketEventHandle::toggle_fake_events, nb::arg("enabled"));
|
||||
m.def("set_fake_prefix", &SocketEventHandle::set_fake_prefix, nb::arg("prefix"));
|
||||
m.def("get_fake_prefix", &SocketEventHandle::fake_prefix);
|
||||
m.def("delete_fake_prefix", [] { SocketEventHandle::set_fake_prefix(""); });
|
||||
m.def("wait_for_one_event", &Event::wait_for_one, nb::arg("events"), nb::arg("timeout") = -1, nb::call_guard<nb::gil_scoped_release>());
|
||||
|
||||
nb::class_<Event>(m, "Event")
|
||||
.def(nb::init<>())
|
||||
.def("set", &Event::set)
|
||||
.def("clear", &Event::clear)
|
||||
.def("wait", &Event::wait, nb::arg("timeout") = -1, nb::call_guard<nb::gil_scoped_release>())
|
||||
.def("peek", &Event::peek)
|
||||
.def_prop_ro("fd", &Event::fd)
|
||||
.def_prop_ro("ptr", [](Event &event) { return reinterpret_cast<uintptr_t>(&event); });
|
||||
|
||||
nb::class_<SocketEventHandle>(m, "SocketEventHandle")
|
||||
.def(nb::init<std::string, std::string, bool>(), nb::arg("endpoint"), nb::arg("identifier"), nb::arg("override"))
|
||||
.def_prop_rw("enabled", &SocketEventHandle::is_enabled, &SocketEventHandle::set_enabled)
|
||||
.def_prop_ro("recv_called_event", &SocketEventHandle::recv_called, nb::keep_alive<0, 1>())
|
||||
.def_prop_ro("recv_ready_event", &SocketEventHandle::recv_ready, nb::keep_alive<0, 1>());
|
||||
|
||||
nb::class_<SubSocket>(m, "SubSocket")
|
||||
.def(nb::init<>())
|
||||
.def("connect", [](SubSocket &socket, const std::string &name, bool conflate, size_t segment_size) {
|
||||
nb::gil_scoped_release release;
|
||||
int result = socket.connect(name, conflate, segment_size);
|
||||
check_error(result != 0, errno, name);
|
||||
}, nb::arg("endpoint"), nb::arg("conflate") = false, nb::arg("segment_size") = 0)
|
||||
.def("setTimeout", &SubSocket::setTimeout, nb::arg("timeout"), nb::call_guard<nb::gil_scoped_release>())
|
||||
.def("receive", [](SubSocket &socket, bool non_blocking) -> std::optional<nb::bytes> {
|
||||
std::unique_ptr<Message> message;
|
||||
{
|
||||
nb::gil_scoped_release release;
|
||||
message.reset(socket.receive(non_blocking));
|
||||
}
|
||||
if (!message) return std::nullopt;
|
||||
return nb::bytes(message->getData(), message->getSize());
|
||||
}, nb::arg("non_blocking") = false);
|
||||
|
||||
nb::class_<Poller>(m, "Poller")
|
||||
.def(nb::init<>())
|
||||
.def("registerSocket", &Poller::registerSocket, nb::arg("socket"), nb::keep_alive<1, 2>())
|
||||
.def("poll", &Poller::poll, nb::arg("timeout"), nb::rv_policy::reference, nb::call_guard<nb::gil_scoped_release>());
|
||||
|
||||
nb::class_<PubSocket>(m, "PubSocket")
|
||||
.def(nb::init<>())
|
||||
.def("connect", [](PubSocket &socket, const std::string &name, size_t segment_size) {
|
||||
int result = socket.connect(name, segment_size);
|
||||
check_error(result != 0, errno, name);
|
||||
}, nb::arg("endpoint"), nb::arg("segment_size") = 0)
|
||||
.def("send", [](PubSocket &socket, nb::bytes data) {
|
||||
int result = socket.send(const_cast<char *>(data.c_str()), data.size());
|
||||
check_error(result != static_cast<int>(data.size()), errno);
|
||||
}, nb::arg("data"))
|
||||
.def("all_readers_updated", &PubSocket::all_readers_updated)
|
||||
.def("wait_for_readers", [](PubSocket &socket, double timeout, double interval) {
|
||||
// Python's sleep releases the GIL and handles signals while waiting.
|
||||
nb::object sleep = nb::module_::import_("time").attr("sleep");
|
||||
auto deadline = std::chrono::steady_clock::now() + std::chrono::duration<double>(timeout);
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
if (socket.all_readers_updated()) return;
|
||||
sleep(interval);
|
||||
}
|
||||
PyErr_SetString(PyExc_TimeoutError, "subscriber did not connect");
|
||||
throw nb::python_error();
|
||||
}, nb::arg("timeout") = 1.0, nb::arg("interval") = 0.001);
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
# distutils: language = c++
|
||||
# cython: c_string_encoding=ascii, language_level=3
|
||||
|
||||
import time
|
||||
from libcpp.string cimport string
|
||||
from libcpp.vector cimport vector
|
||||
from libcpp cimport bool
|
||||
from libc cimport errno
|
||||
from libc.string cimport strerror
|
||||
from cython.operator import dereference
|
||||
|
||||
|
||||
from .ipc cimport SubSocket as cppSubSocket
|
||||
from .ipc cimport PubSocket as cppPubSocket
|
||||
from .ipc cimport Poller as cppPoller
|
||||
from .ipc cimport Message as cppMessage
|
||||
from .ipc cimport Event as cppEvent, SocketEventHandle as cppSocketEventHandle
|
||||
|
||||
|
||||
class IpcError(Exception):
|
||||
def __init__(self, endpoint=None):
|
||||
suffix = f"with {endpoint.decode('utf-8')}" if endpoint else ""
|
||||
message = f"Messaging failure {suffix}: {strerror(errno.errno).decode('utf-8')}"
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class MultiplePublishersError(IpcError):
|
||||
pass
|
||||
|
||||
|
||||
def toggle_fake_events(bool enabled):
|
||||
cppSocketEventHandle.toggle_fake_events(enabled)
|
||||
|
||||
|
||||
def set_fake_prefix(string prefix):
|
||||
cppSocketEventHandle.set_fake_prefix(prefix)
|
||||
|
||||
|
||||
def get_fake_prefix():
|
||||
return cppSocketEventHandle.fake_prefix()
|
||||
|
||||
|
||||
def delete_fake_prefix():
|
||||
cppSocketEventHandle.set_fake_prefix(b"")
|
||||
|
||||
|
||||
def wait_for_one_event(list events, int timeout=-1):
|
||||
cdef vector[cppEvent] items
|
||||
for event in events:
|
||||
items.push_back(dereference(<cppEvent*><size_t>event.ptr))
|
||||
return cppEvent.wait_for_one(items, timeout)
|
||||
|
||||
|
||||
cdef class Event:
|
||||
cdef cppEvent event;
|
||||
|
||||
cdef setEvent(self, cppEvent event):
|
||||
self.event = event
|
||||
|
||||
def set(self):
|
||||
self.event.set()
|
||||
|
||||
def clear(self):
|
||||
return self.event.clear()
|
||||
|
||||
def wait(self, int timeout=-1):
|
||||
self.event.wait(timeout)
|
||||
|
||||
def peek(self):
|
||||
return self.event.peek()
|
||||
|
||||
@property
|
||||
def fd(self):
|
||||
return self.event.fd()
|
||||
|
||||
@property
|
||||
def ptr(self):
|
||||
return <size_t><void*>&self.event
|
||||
|
||||
|
||||
cdef class SocketEventHandle:
|
||||
cdef cppSocketEventHandle * handle;
|
||||
|
||||
def __cinit__(self, string endpoint, string identifier, bool override):
|
||||
self.handle = new cppSocketEventHandle(endpoint, identifier, override)
|
||||
|
||||
def __dealloc__(self):
|
||||
del self.handle
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
return self.handle.is_enabled()
|
||||
|
||||
@enabled.setter
|
||||
def enabled(self, bool value):
|
||||
self.handle.set_enabled(value)
|
||||
|
||||
@property
|
||||
def recv_called_event(self):
|
||||
e = Event()
|
||||
e.setEvent(self.handle.recv_called())
|
||||
|
||||
return e
|
||||
|
||||
@property
|
||||
def recv_ready_event(self):
|
||||
e = Event()
|
||||
e.setEvent(self.handle.recv_ready())
|
||||
|
||||
return e
|
||||
|
||||
|
||||
cdef class Poller:
|
||||
cdef cppPoller * poller
|
||||
cdef list sub_sockets
|
||||
|
||||
def __cinit__(self):
|
||||
self.sub_sockets = []
|
||||
self.poller = new cppPoller()
|
||||
|
||||
def __dealloc__(self):
|
||||
del self.poller
|
||||
|
||||
def registerSocket(self, SubSocket socket):
|
||||
self.sub_sockets.append(socket)
|
||||
self.poller.registerSocket(socket.socket)
|
||||
|
||||
def poll(self, timeout):
|
||||
sockets = []
|
||||
cdef int t = timeout
|
||||
|
||||
with nogil:
|
||||
result = self.poller.poll(t)
|
||||
|
||||
for s in result:
|
||||
socket = SubSocket()
|
||||
socket.setPtr(s)
|
||||
sockets.append(socket)
|
||||
|
||||
return sockets
|
||||
|
||||
|
||||
cdef class SubSocket:
|
||||
cdef cppSubSocket * socket
|
||||
cdef bool is_owner
|
||||
def __cinit__(self):
|
||||
with nogil:
|
||||
self.socket = new cppSubSocket()
|
||||
|
||||
self.is_owner = True
|
||||
|
||||
def __dealloc__(self):
|
||||
if self.is_owner:
|
||||
with nogil:
|
||||
del self.socket
|
||||
|
||||
cdef setPtr(self, cppSubSocket * ptr):
|
||||
if self.is_owner:
|
||||
with nogil:
|
||||
del self.socket
|
||||
|
||||
self.is_owner = False
|
||||
self.socket = ptr
|
||||
|
||||
def connect(self, string endpoint, bool conflate=False, size_t segment_size=0):
|
||||
cdef int r
|
||||
with nogil:
|
||||
r = self.socket.connect(endpoint, conflate, segment_size)
|
||||
|
||||
if r != 0:
|
||||
if errno.errno == errno.EADDRINUSE:
|
||||
raise MultiplePublishersError(endpoint)
|
||||
else:
|
||||
raise IpcError(endpoint)
|
||||
|
||||
def setTimeout(self, int timeout):
|
||||
with nogil:
|
||||
self.socket.setTimeout(timeout)
|
||||
|
||||
def receive(self, bool non_blocking=False):
|
||||
cdef cppMessage *msg
|
||||
with nogil:
|
||||
msg = self.socket.receive(non_blocking)
|
||||
if msg == NULL:
|
||||
return None
|
||||
else:
|
||||
sz = msg.getSize()
|
||||
m = msg.getData()[:sz]
|
||||
with nogil:
|
||||
del msg
|
||||
return m
|
||||
|
||||
cdef class PubSocket:
|
||||
cdef cppPubSocket * socket
|
||||
|
||||
def __cinit__(self):
|
||||
self.socket = new cppPubSocket()
|
||||
|
||||
def __dealloc__(self):
|
||||
del self.socket
|
||||
|
||||
def connect(self, string endpoint, size_t segment_size=0):
|
||||
r = self.socket.connect(endpoint, segment_size)
|
||||
|
||||
if r != 0:
|
||||
if errno.errno == errno.EADDRINUSE:
|
||||
raise MultiplePublishersError(endpoint)
|
||||
else:
|
||||
raise IpcError(endpoint)
|
||||
|
||||
def send(self, bytes data):
|
||||
length = len(data)
|
||||
r = self.socket.send(<char*>data, length)
|
||||
|
||||
if r != length:
|
||||
if errno.errno == errno.EADDRINUSE:
|
||||
raise MultiplePublishersError
|
||||
else:
|
||||
raise IpcError
|
||||
|
||||
def all_readers_updated(self):
|
||||
return self.socket.all_readers_updated()
|
||||
|
||||
def wait_for_readers(self, double timeout=1.0, double interval=0.001):
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
if self.all_readers_updated():
|
||||
return
|
||||
time.sleep(interval)
|
||||
raise TimeoutError("subscriber did not connect")
|
||||
Binary file not shown.
@@ -569,3 +569,98 @@ TEST_CASE("32 active subscribers retain every message across publisher restart")
|
||||
msgq_close_queue(&pub);
|
||||
std::remove(file.c_str());
|
||||
}
|
||||
|
||||
static int64_t millis_since(std::chrono::steady_clock::time_point start)
|
||||
{
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start).count();
|
||||
}
|
||||
|
||||
static std::thread send_later(msgq_queue_t *q, int delay_ms)
|
||||
{
|
||||
return std::thread([=]() {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));
|
||||
uint64_t value = 1;
|
||||
msgq_msg_t msg;
|
||||
msgq_msg_init_data(&msg, (char *)&value, sizeof(value));
|
||||
msgq_msg_send(&msg, q);
|
||||
msgq_msg_close(&msg);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_CASE("msgq_poll times out")
|
||||
{
|
||||
cleanup_test_queue();
|
||||
msgq_queue_t writer, reader;
|
||||
msgq_new_queue(&writer, "test_queue", 1024);
|
||||
msgq_new_queue(&reader, "test_queue", 1024);
|
||||
msgq_init_publisher(&writer);
|
||||
msgq_init_subscriber(&reader);
|
||||
|
||||
msgq_pollitem_t items[] = {{&reader, 0}};
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
REQUIRE(msgq_poll(items, 1, 100) == 0);
|
||||
REQUIRE(millis_since(start) >= 100);
|
||||
REQUIRE(msgq_poll(items, 1, 0) == 0);
|
||||
|
||||
send_later(&writer, 0).join();
|
||||
REQUIRE(msgq_poll(items, 1, 0) == 1);
|
||||
|
||||
msgq_close_queue(&writer);
|
||||
msgq_close_queue(&reader);
|
||||
}
|
||||
|
||||
TEST_CASE("msgq_poll wakes up on send")
|
||||
{
|
||||
cleanup_test_queue();
|
||||
msgq_queue_t writer, reader;
|
||||
msgq_new_queue(&writer, "test_queue", 1024);
|
||||
msgq_new_queue(&reader, "test_queue", 1024);
|
||||
msgq_init_publisher(&writer);
|
||||
msgq_init_subscriber(&reader);
|
||||
|
||||
msgq_pollitem_t items[] = {{&reader, 0}};
|
||||
std::thread sender = send_later(&writer, 50);
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
int n = msgq_poll(items, 1, 5000);
|
||||
int64_t elapsed = millis_since(start);
|
||||
sender.join();
|
||||
|
||||
REQUIRE(n == 1);
|
||||
REQUIRE(elapsed < 1000);
|
||||
|
||||
msgq_close_queue(&writer);
|
||||
msgq_close_queue(&reader);
|
||||
}
|
||||
|
||||
TEST_CASE("msgq_poll multiple queues")
|
||||
{
|
||||
cleanup_test_queue();
|
||||
remove("/dev/shm/msgq_test_queue2");
|
||||
remove("/tmp/msgq_test_queue2");
|
||||
msgq_queue_t writer1, writer2, reader1, reader2;
|
||||
msgq_new_queue(&writer1, "test_queue", 1024);
|
||||
msgq_new_queue(&writer2, "test_queue2", 1024);
|
||||
msgq_new_queue(&reader1, "test_queue", 1024);
|
||||
msgq_new_queue(&reader2, "test_queue2", 1024);
|
||||
msgq_init_publisher(&writer1);
|
||||
msgq_init_publisher(&writer2);
|
||||
msgq_init_subscriber(&reader1);
|
||||
msgq_init_subscriber(&reader2);
|
||||
|
||||
msgq_pollitem_t items[] = {{&reader1, 0}, {&reader2, 0}};
|
||||
std::thread sender = send_later(&writer2, 50);
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
int n = msgq_poll(items, 2, 5000);
|
||||
int64_t elapsed = millis_since(start);
|
||||
sender.join();
|
||||
|
||||
REQUIRE(n == 1);
|
||||
REQUIRE(elapsed < 1000);
|
||||
REQUIRE(items[0].revents == 0);
|
||||
REQUIRE(items[1].revents == 1);
|
||||
|
||||
msgq_close_queue(&writer1);
|
||||
msgq_close_queue(&writer2);
|
||||
msgq_close_queue(&reader1);
|
||||
msgq_close_queue(&reader2);
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
visionipc_pyx.cpp
|
||||
*.so
|
||||
@@ -1,4 +1,4 @@
|
||||
from msgq.visionipc.visionipc_pyx import VisionBuf, VisionIpcClient, VisionIpcServer, get_endpoint_name
|
||||
from msgq.visionipc._visionipc import VisionBuf, VisionIpcClient, VisionIpcServer, get_endpoint_name
|
||||
|
||||
__all__ = [
|
||||
"VisionBuf",
|
||||
|
||||
Executable
BIN
Binary file not shown.
@@ -1,6 +1,7 @@
|
||||
import gc
|
||||
import uuid
|
||||
import struct
|
||||
import unittest
|
||||
import uuid
|
||||
from typing import Optional
|
||||
from msgq.visionipc import VisionIpcServer, VisionIpcClient
|
||||
|
||||
@@ -10,7 +11,6 @@ class TestVisionIpc(unittest.TestCase):
|
||||
client: Optional[VisionIpcClient]
|
||||
|
||||
def setUp(self):
|
||||
# Each test needs independent listener and publisher endpoints in parallel runs.
|
||||
self.name = f"test_{uuid.uuid4().hex}"
|
||||
self.server = None
|
||||
self.client = None
|
||||
@@ -19,88 +19,139 @@ class TestVisionIpc(unittest.TestCase):
|
||||
self.client = None
|
||||
self.server = None
|
||||
|
||||
def setup_vipc(self, *stream_types, num_buffers=1, width=100, height=100, conflate=False):
|
||||
self.server = VisionIpcServer(self.name)
|
||||
for stream_type in stream_types:
|
||||
self.server.create_buffers(stream_type, num_buffers, width, height)
|
||||
self.server.start_listener()
|
||||
def setup_vipc(
|
||||
self, name: str, stream_type: int, *other_stream_types: int,
|
||||
num_buffers: int = 1, width: int = 100, height: int = 100, conflate: bool = False,
|
||||
) -> tuple[VisionIpcServer, VisionIpcClient, int]:
|
||||
self.server = server = VisionIpcServer(self.name)
|
||||
for stream in (stream_type, *other_stream_types):
|
||||
server.create_buffers(stream, num_buffers, width, height)
|
||||
server.start_listener()
|
||||
|
||||
if len(stream_types):
|
||||
self.client = VisionIpcClient(self.name, stream_types[0], conflate)
|
||||
assert self.client.connect(True)
|
||||
else:
|
||||
self.client = None
|
||||
|
||||
return self.server, self.client
|
||||
self.client = client = VisionIpcClient(self.name, stream_type, conflate)
|
||||
assert client.connect(True)
|
||||
buffer_len = client.buffer_len
|
||||
assert buffer_len is not None
|
||||
return server, client, buffer_len
|
||||
|
||||
def test_connect(self):
|
||||
self.setup_vipc(0)
|
||||
assert self.client is not None
|
||||
assert self.client.is_connected()
|
||||
_, client, _ = self.setup_vipc("camerad", 0)
|
||||
assert client.is_connected()
|
||||
|
||||
def test_available_streams(self):
|
||||
stream_types = (0, 2)
|
||||
self.setup_vipc(*stream_types)
|
||||
self.setup_vipc("camerad", *stream_types)
|
||||
available_streams = VisionIpcClient.available_streams(self.name, True)
|
||||
assert available_streams == set(stream_types)
|
||||
|
||||
def test_buffers(self):
|
||||
width, height, num_buffers = 100, 200, 5
|
||||
self.setup_vipc(0, num_buffers=num_buffers, width=width, height=height)
|
||||
assert self.client is not None
|
||||
assert self.client.width == width
|
||||
assert self.client.height == height
|
||||
assert self.client.buffer_len is not None and self.client.buffer_len > 0
|
||||
assert self.client.num_buffers == num_buffers
|
||||
_, client, buffer_len = self.setup_vipc("camerad", 0, num_buffers=num_buffers, width=width, height=height)
|
||||
assert client.width == width
|
||||
assert client.height == height
|
||||
assert buffer_len > 0
|
||||
assert client.num_buffers == num_buffers
|
||||
|
||||
def test_send_single_buffer(self):
|
||||
self.setup_vipc(0)
|
||||
assert self.server is not None
|
||||
assert self.client is not None
|
||||
assert self.client.buffer_len is not None
|
||||
buf = bytearray(self.client.buffer_len)
|
||||
server, client, buffer_len = self.setup_vipc("camerad", 0)
|
||||
buf = bytearray(buffer_len)
|
||||
struct.pack_into("<Q", buf, 0, 1234)
|
||||
self.server.send(0, buf, frame_id=1337)
|
||||
server.send(0, buf, frame_id=1337)
|
||||
|
||||
recv_buf = self.client.recv()
|
||||
recv_buf = client.recv()
|
||||
assert recv_buf is not None
|
||||
data = recv_buf.data
|
||||
assert isinstance(data, memoryview)
|
||||
assert struct.unpack_from("<Q", data, 0)[0] == 1234
|
||||
assert len(data) == self.client.buffer_len
|
||||
assert data[8:].nbytes == self.client.buffer_len - 8
|
||||
assert self.client.frame_id == 1337
|
||||
assert len(data) == buffer_len
|
||||
assert data[8:].nbytes == buffer_len - 8
|
||||
assert client.frame_id == 1337
|
||||
assert recv_buf.frame_id == 1337
|
||||
|
||||
def test_no_conflate(self):
|
||||
self.setup_vipc(0)
|
||||
assert self.server is not None
|
||||
assert self.client is not None
|
||||
assert self.client.buffer_len is not None
|
||||
buf = bytearray(self.client.buffer_len)
|
||||
self.server.send(0, buf, frame_id=1)
|
||||
self.server.send(0, buf, frame_id=2)
|
||||
server, client, buffer_len = self.setup_vipc("camerad", 0)
|
||||
buf = bytearray(buffer_len)
|
||||
server.send(0, buf, frame_id=1)
|
||||
server.send(0, buf, frame_id=2)
|
||||
|
||||
recv_buf = self.client.recv()
|
||||
recv_buf = client.recv()
|
||||
assert recv_buf is not None
|
||||
assert self.client.frame_id == 1
|
||||
assert client.frame_id == 1
|
||||
|
||||
recv_buf = self.client.recv()
|
||||
recv_buf = client.recv()
|
||||
assert recv_buf is not None
|
||||
assert self.client.frame_id == 2
|
||||
assert client.frame_id == 2
|
||||
|
||||
def test_conflate(self):
|
||||
self.setup_vipc(0, conflate=True)
|
||||
assert self.server is not None
|
||||
assert self.client is not None
|
||||
assert self.client.buffer_len is not None
|
||||
buf = bytearray(self.client.buffer_len)
|
||||
self.server.send(0, buf, frame_id=1)
|
||||
self.server.send(0, buf, frame_id=2)
|
||||
server, client, buffer_len = self.setup_vipc("camerad", 0, conflate=True)
|
||||
buf = bytearray(buffer_len)
|
||||
server.send(0, buf, frame_id=1)
|
||||
server.send(0, buf, frame_id=2)
|
||||
|
||||
recv_buf = self.client.recv()
|
||||
recv_buf = client.recv()
|
||||
assert recv_buf is not None
|
||||
assert self.client.frame_id == 2
|
||||
assert client.frame_id == 2
|
||||
|
||||
recv_buf = self.client.recv(timeout_ms=5)
|
||||
recv_buf = client.recv(timeout_ms=5)
|
||||
assert recv_buf is None
|
||||
|
||||
def test_send_buffer_types(self):
|
||||
server, client, buffer_len = self.setup_vipc("camerad", 0)
|
||||
data = bytes(i % 256 for i in range(buffer_len))
|
||||
strided = bytearray(len(data) * 2)
|
||||
strided[::2] = data
|
||||
for payload in (data, bytearray(data), memoryview(data), memoryview(strided)[::2], memoryview(data[::-1])[::-1]):
|
||||
with self.subTest(type=type(payload)):
|
||||
server.send(0, payload, frame_id=42, timestamp_sof=123, timestamp_eof=456)
|
||||
buf = client.recv()
|
||||
assert buf is not None
|
||||
self.assertEqual(bytes(buf.data), data)
|
||||
self.assertEqual(buf.frame_id, 42)
|
||||
self.assertEqual(client.timestamp_sof, 123)
|
||||
self.assertEqual(client.timestamp_eof, 456)
|
||||
self.assertFalse(client.valid)
|
||||
|
||||
def test_send_wrong_size(self):
|
||||
server, _, _ = self.setup_vipc("camerad", 0)
|
||||
with self.assertRaises(AssertionError):
|
||||
server.send(0, b"short")
|
||||
|
||||
def test_buffer_keeps_client_alive(self):
|
||||
server, client, buffer_len = self.setup_vipc("camerad", 0)
|
||||
data = b"a" * buffer_len
|
||||
server.send(0, data, frame_id=42)
|
||||
buf = client.recv()
|
||||
assert buf is not None
|
||||
self.client = None
|
||||
del client
|
||||
gc.collect()
|
||||
self.assertEqual(buf.frame_id, 42)
|
||||
view = buf.data
|
||||
del buf
|
||||
gc.collect()
|
||||
self.assertEqual(bytes(view), data)
|
||||
self.assertFalse(view.readonly)
|
||||
view[0] = ord("b")
|
||||
self.assertEqual(view[0], ord("b"))
|
||||
|
||||
def test_unconnected_properties(self):
|
||||
client = VisionIpcClient("unconnected", 0, False)
|
||||
for value in (client.width, client.height, client.stride, client.uv_offset, client.buffer_len):
|
||||
self.assertIsNone(value)
|
||||
self.assertEqual(client.num_buffers, 0)
|
||||
self.assertFalse(client.is_connected())
|
||||
self.assertFalse(client.connect(False))
|
||||
self.assertIsNone(client.recv(timeout_ms=0))
|
||||
|
||||
def test_exact_uint64_timestamps_survive_receive_and_timeout(self):
|
||||
server, client, size = self.setup_vipc("clock", 0, width=32, height=18)
|
||||
payload = bytes(size)
|
||||
for frame, sof, eof in ((0, 0, 0), (1, 2**53 + 1, 2**53 + 19),
|
||||
(2**32 - 1, 2**63 + 17, 2**64 - 1)):
|
||||
with self.subTest(frame=frame, sof=sof, eof=eof):
|
||||
server.send(0, payload, frame_id=frame, timestamp_sof=sof, timestamp_eof=eof)
|
||||
buffer = client.recv(timeout_ms=1000)
|
||||
assert buffer is not None
|
||||
assert (client.frame_id, client.timestamp_sof, client.timestamp_eof) == (frame, sof, eof)
|
||||
assert client.recv(timeout_ms=0) is None
|
||||
assert (client.frame_id, client.timestamp_sof, client.timestamp_eof) == (frame, sof, eof)
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
# distutils: language = c++
|
||||
#cython: language_level=3
|
||||
|
||||
from libcpp.string cimport string
|
||||
from libcpp.vector cimport vector
|
||||
from libcpp.set cimport set
|
||||
from libc.stdint cimport uint32_t, uint64_t
|
||||
from libcpp cimport bool, int
|
||||
|
||||
cdef extern from "msgq/visionipc/visionbuf.h":
|
||||
ctypedef uint32_t VisionStreamType "VisionStreamType"
|
||||
|
||||
cdef cppclass VisionBuf:
|
||||
void * addr
|
||||
int fd
|
||||
size_t len
|
||||
size_t width
|
||||
size_t height
|
||||
size_t stride
|
||||
size_t uv_offset
|
||||
size_t idx
|
||||
void set_frame_id(uint64_t id)
|
||||
uint64_t get_frame_id()
|
||||
|
||||
cdef extern from "msgq/visionipc/visionipc.h":
|
||||
struct VisionIpcBufExtra:
|
||||
uint32_t frame_id
|
||||
uint64_t timestamp_sof
|
||||
uint64_t timestamp_eof
|
||||
bool valid
|
||||
|
||||
cdef extern from "msgq/visionipc/visionipc_server.h":
|
||||
string get_endpoint_name(string, VisionStreamType)
|
||||
|
||||
cdef cppclass VisionIpcServer:
|
||||
VisionIpcServer(string)
|
||||
void create_buffers(VisionStreamType, size_t, size_t, size_t)
|
||||
void create_buffers_with_sizes(VisionStreamType, size_t, size_t, size_t, size_t, size_t, size_t)
|
||||
VisionBuf * get_buffer(VisionStreamType)
|
||||
void send(VisionBuf *, VisionIpcBufExtra *, bool)
|
||||
void start_listener()
|
||||
|
||||
cdef extern from "msgq/visionipc/visionipc_client.h":
|
||||
cdef cppclass VisionIpcClient:
|
||||
int num_buffers
|
||||
VisionBuf buffers[1]
|
||||
VisionIpcClient(string, VisionStreamType, bool)
|
||||
VisionBuf * recv(VisionIpcBufExtra *, int) nogil
|
||||
bool connect(bool)
|
||||
bool is_connected()
|
||||
@staticmethod
|
||||
set[VisionStreamType] getAvailableStreams(string, bool)
|
||||
@@ -0,0 +1,109 @@
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include <nanobind/nanobind.h>
|
||||
#include <nanobind/ndarray.h>
|
||||
#include <nanobind/stl/optional.h>
|
||||
#include <nanobind/stl/set.h>
|
||||
#include <nanobind/stl/string.h>
|
||||
|
||||
#include "msgq/visionipc/visionbuf.h"
|
||||
#include "msgq/visionipc/visionipc.h"
|
||||
#include "msgq/visionipc/visionipc_client.h"
|
||||
#include "msgq/visionipc/visionipc_server.h"
|
||||
|
||||
namespace nb = nanobind;
|
||||
|
||||
namespace {
|
||||
|
||||
struct PythonVisionIpcClient : VisionIpcClient {
|
||||
using VisionIpcClient::VisionIpcClient;
|
||||
VisionIpcBufExtra extra = {};
|
||||
};
|
||||
|
||||
using Buffer = nb::ndarray<nb::memview, uint8_t, nb::ndim<1>, nb::device::cpu>;
|
||||
using InputBuffer = nb::ndarray<const uint8_t, nb::ndim<1>, nb::device::cpu>;
|
||||
|
||||
} // namespace
|
||||
|
||||
NB_MODULE(_visionipc, m) {
|
||||
// Daemon threads can still hold clients and servers at exit.
|
||||
nb::set_leak_warnings(false);
|
||||
|
||||
m.def("get_endpoint_name", &get_endpoint_name, nb::arg("name"), nb::arg("stream"));
|
||||
|
||||
nb::class_<VisionBuf>(m, "VisionBuf")
|
||||
.def_prop_ro("data", [](VisionBuf &buf) { return Buffer(buf.addr, {buf.len}); }, nb::rv_policy::reference_internal,
|
||||
nb::sig("def data(self) -> memoryview"))
|
||||
.def_ro("width", &VisionBuf::width)
|
||||
.def_ro("height", &VisionBuf::height)
|
||||
.def_ro("stride", &VisionBuf::stride)
|
||||
.def_ro("uv_offset", &VisionBuf::uv_offset)
|
||||
.def_ro("idx", &VisionBuf::idx)
|
||||
.def_ro("fd", &VisionBuf::fd)
|
||||
.def_prop_ro("frame_id", &VisionBuf::get_frame_id);
|
||||
|
||||
nb::class_<VisionIpcServer>(m, "VisionIpcServer")
|
||||
.def(nb::init<std::string>(), nb::arg("name"))
|
||||
.def("create_buffers", &VisionIpcServer::create_buffers, nb::arg("tp"), nb::arg("num_buffers"), nb::arg("width"), nb::arg("height"))
|
||||
.def("create_buffers_with_sizes", &VisionIpcServer::create_buffers_with_sizes,
|
||||
nb::arg("tp"), nb::arg("num_buffers"), nb::arg("width"), nb::arg("height"), nb::arg("size"), nb::arg("stride"), nb::arg("uv_offset"))
|
||||
.def("send", [](VisionIpcServer &server, VisionStreamType tp, InputBuffer data,
|
||||
uint32_t frame_id, uint64_t timestamp_sof, uint64_t timestamp_eof) {
|
||||
VisionBuf *buf = server.get_buffer(tp);
|
||||
if (buf->len != data.size()) {
|
||||
PyErr_SetString(PyExc_AssertionError, "buffer size does not match frame size");
|
||||
throw nb::python_error();
|
||||
}
|
||||
// The old binding accepted strided byte views as well as contiguous data.
|
||||
auto *source = data.data();
|
||||
if (data.stride(0) == 1) {
|
||||
std::memcpy(buf->addr, source, data.size());
|
||||
} else {
|
||||
auto *target = static_cast<uint8_t *>(buf->addr);
|
||||
for (size_t i = 0; i < data.size(); ++i) target[i] = source[static_cast<int64_t>(i) * data.stride(0)];
|
||||
}
|
||||
buf->set_frame_id(frame_id);
|
||||
VisionIpcBufExtra extra = {frame_id, timestamp_sof, timestamp_eof, false};
|
||||
server.send(buf, &extra, false);
|
||||
}, nb::arg("tp"), nb::arg("data").noconvert(), nb::arg("frame_id") = 0, nb::arg("timestamp_sof") = 0, nb::arg("timestamp_eof") = 0,
|
||||
nb::sig("def send(self, tp: int, data: bytes | bytearray | memoryview, frame_id: int = 0, timestamp_sof: int = 0, timestamp_eof: int = 0) -> None"))
|
||||
.def("start_listener", &VisionIpcServer::start_listener);
|
||||
|
||||
nb::class_<PythonVisionIpcClient>(m, "VisionIpcClient")
|
||||
.def(nb::init<std::string, VisionStreamType, bool>(), nb::arg("name"), nb::arg("stream"), nb::arg("conflate"))
|
||||
.def_prop_ro("width", [](PythonVisionIpcClient &client) -> std::optional<size_t> {
|
||||
return client.num_buffers ? std::make_optional(client.buffers[0].width) : std::nullopt;
|
||||
})
|
||||
.def_prop_ro("height", [](PythonVisionIpcClient &client) -> std::optional<size_t> {
|
||||
return client.num_buffers ? std::make_optional(client.buffers[0].height) : std::nullopt;
|
||||
})
|
||||
.def_prop_ro("stride", [](PythonVisionIpcClient &client) -> std::optional<size_t> {
|
||||
return client.num_buffers ? std::make_optional(client.buffers[0].stride) : std::nullopt;
|
||||
})
|
||||
.def_prop_ro("uv_offset", [](PythonVisionIpcClient &client) -> std::optional<size_t> {
|
||||
return client.num_buffers ? std::make_optional(client.buffers[0].uv_offset) : std::nullopt;
|
||||
})
|
||||
.def_prop_ro("buffer_len", [](PythonVisionIpcClient &client) -> std::optional<size_t> {
|
||||
return client.num_buffers ? std::make_optional(client.buffers[0].len) : std::nullopt;
|
||||
})
|
||||
.def_prop_ro("num_buffers", [](PythonVisionIpcClient &client) { return client.num_buffers; })
|
||||
.def_prop_ro("frame_id", [](PythonVisionIpcClient &client) { return client.extra.frame_id; })
|
||||
.def_prop_ro("timestamp_sof", [](PythonVisionIpcClient &client) { return client.extra.timestamp_sof; })
|
||||
.def_prop_ro("timestamp_eof", [](PythonVisionIpcClient &client) { return client.extra.timestamp_eof; })
|
||||
.def_prop_ro("valid", [](PythonVisionIpcClient &client) { return client.extra.valid; })
|
||||
.def("recv", [](PythonVisionIpcClient &client, int timeout_ms) {
|
||||
nb::gil_scoped_release release;
|
||||
return client.recv(&client.extra, timeout_ms);
|
||||
}, nb::arg("timeout_ms") = 100, nb::rv_policy::reference_internal,
|
||||
nb::sig("def recv(self, timeout_ms: int = 100) -> VisionBuf | None"))
|
||||
// Holds the GIL: connect remaps the buffers other threads may be reading.
|
||||
.def("connect", [](PythonVisionIpcClient &client, bool blocking) {
|
||||
return client.connect(blocking);
|
||||
}, nb::arg("blocking"))
|
||||
.def("is_connected", [](PythonVisionIpcClient &client) { return client.is_connected(); })
|
||||
.def_static("available_streams", &VisionIpcClient::getAvailableStreams, nb::arg("name"), nb::arg("block"),
|
||||
nb::call_guard<nb::gil_scoped_release>());
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
# distutils: language = c++
|
||||
#cython: language_level=3
|
||||
|
||||
from .visionipc cimport VisionBuf as cppVisionBuf
|
||||
|
||||
cdef class VisionBuf:
|
||||
cdef cppVisionBuf * buf
|
||||
|
||||
@staticmethod
|
||||
cdef create(cppVisionBuf*)
|
||||
@@ -1,163 +0,0 @@
|
||||
# distutils: language = c++
|
||||
# cython: c_string_encoding=ascii, language_level=3
|
||||
|
||||
from libc.string cimport memcpy
|
||||
from libc.stdint cimport uint32_t, uint64_t
|
||||
from libcpp cimport bool
|
||||
from libcpp.string cimport string
|
||||
|
||||
from .visionipc cimport VisionIpcServer as cppVisionIpcServer
|
||||
from .visionipc cimport VisionIpcClient as cppVisionIpcClient
|
||||
from .visionipc cimport VisionBuf as cppVisionBuf
|
||||
from .visionipc cimport VisionIpcBufExtra, VisionStreamType
|
||||
from .visionipc cimport get_endpoint_name as cpp_get_endpoint_name
|
||||
|
||||
|
||||
def get_endpoint_name(string name, VisionStreamType stream):
|
||||
return cpp_get_endpoint_name(name, stream).decode('utf-8')
|
||||
|
||||
|
||||
cdef class VisionBuf:
|
||||
@staticmethod
|
||||
cdef create(cppVisionBuf * cbuf):
|
||||
buf = VisionBuf()
|
||||
buf.buf = cbuf
|
||||
return buf
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
cdef unsigned char[:] data = <unsigned char[:self.buf.len]> self.buf.addr
|
||||
return memoryview(data)
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self.buf.width
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return self.buf.height
|
||||
|
||||
@property
|
||||
def stride(self):
|
||||
return self.buf.stride
|
||||
|
||||
@property
|
||||
def uv_offset(self):
|
||||
return self.buf.uv_offset
|
||||
|
||||
@property
|
||||
def idx(self):
|
||||
return self.buf.idx
|
||||
|
||||
@property
|
||||
def fd(self):
|
||||
return self.buf.fd
|
||||
|
||||
@property
|
||||
def frame_id(self):
|
||||
return self.buf.get_frame_id()
|
||||
|
||||
|
||||
cdef class VisionIpcServer:
|
||||
cdef cppVisionIpcServer * server
|
||||
|
||||
def __init__(self, string name):
|
||||
self.server = new cppVisionIpcServer(name)
|
||||
|
||||
def create_buffers(self, VisionStreamType tp, size_t num_buffers, size_t width, size_t height):
|
||||
self.server.create_buffers(tp, num_buffers, width, height)
|
||||
|
||||
def create_buffers_with_sizes(self, VisionStreamType tp, size_t num_buffers, size_t width, size_t height, size_t size, size_t stride, size_t uv_offset):
|
||||
self.server.create_buffers_with_sizes(tp, num_buffers, width, height, size, stride, uv_offset)
|
||||
|
||||
def send(self, VisionStreamType tp, const unsigned char[:] data, uint32_t frame_id=0, uint64_t timestamp_sof=0, uint64_t timestamp_eof=0):
|
||||
cdef cppVisionBuf * buf = self.server.get_buffer(tp)
|
||||
|
||||
# Populate buffer
|
||||
assert buf.len == len(data)
|
||||
memcpy(buf.addr, &data[0], len(data))
|
||||
buf.set_frame_id(frame_id)
|
||||
|
||||
cdef VisionIpcBufExtra extra
|
||||
extra.frame_id = frame_id
|
||||
extra.timestamp_sof = timestamp_sof
|
||||
extra.timestamp_eof = timestamp_eof
|
||||
extra.valid = False
|
||||
|
||||
self.server.send(buf, &extra, False)
|
||||
|
||||
def start_listener(self):
|
||||
self.server.start_listener()
|
||||
|
||||
def __dealloc__(self):
|
||||
del self.server
|
||||
|
||||
|
||||
cdef class VisionIpcClient:
|
||||
cdef cppVisionIpcClient * client
|
||||
cdef VisionIpcBufExtra extra
|
||||
|
||||
def __cinit__(self, string name, VisionStreamType stream, bool conflate):
|
||||
self.client = new cppVisionIpcClient(name, stream, conflate)
|
||||
|
||||
def __dealloc__(self):
|
||||
del self.client
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self.client.buffers[0].width if self.client.num_buffers else None
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return self.client.buffers[0].height if self.client.num_buffers else None
|
||||
|
||||
@property
|
||||
def stride(self):
|
||||
return self.client.buffers[0].stride if self.client.num_buffers else None
|
||||
|
||||
@property
|
||||
def uv_offset(self):
|
||||
return self.client.buffers[0].uv_offset if self.client.num_buffers else None
|
||||
|
||||
@property
|
||||
def buffer_len(self):
|
||||
return self.client.buffers[0].len if self.client.num_buffers else None
|
||||
|
||||
@property
|
||||
def num_buffers(self):
|
||||
return self.client.num_buffers
|
||||
|
||||
@property
|
||||
def frame_id(self):
|
||||
return self.extra.frame_id
|
||||
|
||||
@property
|
||||
def timestamp_sof(self):
|
||||
return self.extra.timestamp_sof
|
||||
|
||||
@property
|
||||
def timestamp_eof(self):
|
||||
return self.extra.timestamp_eof
|
||||
|
||||
@property
|
||||
def valid(self):
|
||||
return self.extra.valid
|
||||
|
||||
def recv(self, int timeout_ms=100):
|
||||
cdef cppVisionBuf * buf
|
||||
# release the GIL, this can block for timeout_ms
|
||||
with nogil:
|
||||
buf = self.client.recv(&self.extra, timeout_ms)
|
||||
if not buf:
|
||||
return None
|
||||
return VisionBuf.create(buf)
|
||||
|
||||
def connect(self, bool blocking):
|
||||
return self.client.connect(blocking)
|
||||
|
||||
def is_connected(self):
|
||||
return self.client.is_connected()
|
||||
|
||||
@staticmethod
|
||||
def available_streams(string name, bool block):
|
||||
return cppVisionIpcClient.getAvailableStreams(name, block)
|
||||
Binary file not shown.
+13
-13
@@ -1,5 +1,5 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=77", "Cython>=3.1", "scons"]
|
||||
requires = ["setuptools>=77", "nanobind>=2.12", "scons"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
@@ -21,10 +21,14 @@ dependencies = []
|
||||
Repository = "https://github.com/commaai/msgq"
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"setuptools", # for distutils
|
||||
"Cython",
|
||||
# for building from source with SCons
|
||||
build = [
|
||||
"nanobind>=2.12",
|
||||
"scons",
|
||||
]
|
||||
dev = [
|
||||
"msgq-ipc[build]",
|
||||
"setuptools", # for distutils
|
||||
"ruff",
|
||||
"cppcheck",
|
||||
"cpplint",
|
||||
@@ -41,13 +45,16 @@ include = ["msgq", "msgq.*"]
|
||||
namespaces = false
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
"msgq" = ["*.pxd", "*.h"]
|
||||
"msgq.visionipc" = ["*.pxd", "*.h"]
|
||||
"msgq" = ["*.pyi", "py.typed", "*.h"]
|
||||
"msgq.visionipc" = ["*.pyi", "*.h"]
|
||||
|
||||
[tool.cibuildwheel]
|
||||
build = "cp311-* cp312-* cp313-* cp314-*"
|
||||
test-command = "python -m unittest discover -s msgq"
|
||||
|
||||
[tool.cibuildwheel.macos.environment]
|
||||
MACOSX_DEPLOYMENT_TARGET = "10.13"
|
||||
|
||||
# https://beta.ruff.rs/docs/configuration/#using-pyprojecttoml
|
||||
[tool.ruff]
|
||||
lint.select = ["E", "F", "W", "PIE", "C4", "ISC", "RUF100", "A"]
|
||||
@@ -56,10 +63,3 @@ lint.flake8-implicit-str-concat.allow-multiline=false
|
||||
|
||||
line-length = 160
|
||||
target-version="py311"
|
||||
|
||||
[tool.ty.src]
|
||||
exclude = ["site_scons/"]
|
||||
|
||||
[tool.ty.rules]
|
||||
# Cython modules are compiled at build time, not available for static analysis
|
||||
unresolved-import = "ignore"
|
||||
|
||||
+7
-3
@@ -2,6 +2,7 @@ import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import sysconfig
|
||||
|
||||
from setuptools import Extension, setup
|
||||
from setuptools.command.build_ext import build_ext
|
||||
@@ -11,16 +12,19 @@ class SConsBuildExt(build_ext):
|
||||
def build_extensions(self):
|
||||
subprocess.check_call([sys.executable, "-m", "SCons", "--minimal", "-j", str(self.parallel or os.cpu_count() or 1)])
|
||||
for ext in self.extensions:
|
||||
source = Path(*ext.name.split(".")).with_suffix(".so")
|
||||
module = Path(*ext.name.split("."))
|
||||
source = module.with_suffix(sysconfig.get_config_var("EXT_SUFFIX"))
|
||||
target = Path(self.get_ext_fullpath(ext.name))
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.copy_file(str(source), str(target))
|
||||
stub = module.with_suffix(".pyi")
|
||||
self.copy_file(str(stub), str(target.parent / stub.name))
|
||||
|
||||
|
||||
setup(
|
||||
ext_modules=[
|
||||
Extension("msgq.ipc_pyx", sources=[]),
|
||||
Extension("msgq.visionipc.visionipc_pyx", sources=[]),
|
||||
Extension("msgq._ipc", sources=[]),
|
||||
Extension("msgq.visionipc._visionipc", sources=[]),
|
||||
],
|
||||
cmdclass={"build_ext": SConsBuildExt},
|
||||
)
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import re
|
||||
import SCons
|
||||
from SCons.Action import Action
|
||||
from SCons.Scanner import Scanner
|
||||
|
||||
pyx_from_import_re = re.compile(r'^from\s+(\S+)\s+cimport', re.M)
|
||||
pyx_import_re = re.compile(r'^cimport\s+(\S+)', re.M)
|
||||
cdef_import_re = re.compile(r'^cdef extern from\s+.(\S+).:', re.M)
|
||||
|
||||
|
||||
def pyx_scan(node, env, path, arg=None):
|
||||
contents = node.get_text_contents()
|
||||
|
||||
# from <module> cimport ...
|
||||
matches = pyx_from_import_re.findall(contents)
|
||||
# cimport <module>
|
||||
matches += pyx_import_re.findall(contents)
|
||||
|
||||
# Modules can be either .pxd or .pyx files
|
||||
files = [m.replace('.', '/') + '.pxd' for m in matches]
|
||||
files += [m.replace('.', '/') + '.pyx' for m in matches]
|
||||
|
||||
# cdef extern from <file>
|
||||
files += cdef_import_re.findall(contents)
|
||||
|
||||
# Handle relative imports
|
||||
cur_dir = str(node.get_dir())
|
||||
files = [cur_dir + f if f.startswith('/') else f for f in files]
|
||||
|
||||
# Filter out non-existing files (probably system imports)
|
||||
files = [f for f in files if env.File(f).exists()]
|
||||
return env.File(files)
|
||||
|
||||
|
||||
pyxscanner = Scanner(function=pyx_scan, skeys=['.pyx', '.pxd'], recursive=True)
|
||||
cythonAction = Action("$CYTHONCOM")
|
||||
|
||||
|
||||
def create_builder(env):
|
||||
try:
|
||||
cython = env['BUILDERS']['Cython']
|
||||
except KeyError:
|
||||
cython = SCons.Builder.Builder(
|
||||
action=cythonAction,
|
||||
emitter={},
|
||||
suffix=cython_suffix_emitter,
|
||||
single_source=1
|
||||
)
|
||||
env.Append(SCANNERS=pyxscanner)
|
||||
env['BUILDERS']['Cython'] = cython
|
||||
return cython
|
||||
|
||||
def cython_suffix_emitter(env, source):
|
||||
return "$CYTHONCFILESUFFIX"
|
||||
|
||||
def generate(env):
|
||||
env["CYTHON"] = "cythonize"
|
||||
env["CYTHONCOM"] = "$CYTHON $CYTHONFLAGS $SOURCE"
|
||||
env["CYTHONCFILESUFFIX"] = ".cpp"
|
||||
|
||||
c_file, _ = SCons.Tool.createCFileBuilders(env)
|
||||
|
||||
c_file.suffix['.pyx'] = cython_suffix_emitter
|
||||
c_file.add_action('.pyx', cythonAction)
|
||||
|
||||
c_file.suffix['.py'] = cython_suffix_emitter
|
||||
c_file.add_action('.py', cythonAction)
|
||||
|
||||
create_builder(env)
|
||||
|
||||
def exists(env):
|
||||
return True
|
||||
+2
-2
@@ -18,8 +18,8 @@ lefthook run test
|
||||
trap 'rm -rf "$TEST_DIR"' EXIT
|
||||
uv venv --python "$DIR/.venv/bin/python" "$TEST_DIR/.venv"
|
||||
uv pip install --python "$TEST_DIR/.venv/bin/python" "$DIR"
|
||||
cd "$TEST_DIR"
|
||||
"$TEST_DIR/.venv/bin/python" -m unittest msgq.tests.test_messaging
|
||||
cd "$("$TEST_DIR/.venv/bin/python" -c "import sysconfig; print(sysconfig.get_path('purelib'))")"
|
||||
"$TEST_DIR/.venv/bin/python" -m unittest discover -t . -s msgq
|
||||
)
|
||||
|
||||
# *** all done ***
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ body:
|
||||
attributes:
|
||||
label: Provide a route where the issue occurs
|
||||
description: Ensure the route is fully uploaded at https://useradmin.comma.ai. We cannot look into issues without routes, or at least a Dongle ID.
|
||||
placeholder: 77611a1fac303767|2020-05-11--16-37-07
|
||||
placeholder: 77611a1fac303767/2020-05-11--16-37-07
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ body:
|
||||
attributes:
|
||||
label: Provide a route where the issue occurs if applicable
|
||||
description: Ensure the route is fully uploaded at https://useradmin.comma.ai
|
||||
placeholder: 77611a1fac303767|2020-05-11--16-37-07
|
||||
placeholder: 77611a1fac303767/2020-05-11--16-37-07
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
|
||||
@@ -109,6 +109,10 @@ Start off by recording a route with lots of interesting events: enable LKAS and
|
||||
|
||||
Use the [longitudinal maneuvers](https://github.com/commaai/openpilot/tree/master/tools/longitudinal_maneuvers) report to evaluate your car's longitudinal control and tune it.
|
||||
|
||||
### Validation
|
||||
|
||||
Before submitting a port, go through the [car porting checklist](docs/CAR_PORTING_CHECKLIST.md).
|
||||
|
||||
## Contributing
|
||||
|
||||
All opendbc development is coordinated on GitHub and [Discord](https://discord.comma.ai). Check out the `#dev-opendbc-cars` channel and `Vehicle Specific` section.
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
# Car porting checklist
|
||||
|
||||
This checklist covers common issues we find in new car ports and many of the things we check before merging them. Use it as a guide to help you write a high-quality port and get it reviewed faster.
|
||||
|
||||
Not everything will apply to your car. If something can't be tested, let us know what and why. Where it applies, check each supported variant and both stock ACC and openpilot longitudinal. Some checks need the full openpilot stack, not just opendbc. Field names like `accFaulted` are defined in [`car.capnp`](../opendbc/car/car.capnp).
|
||||
|
||||
## Vehicle and setup
|
||||
|
||||
- Vehicle details
|
||||
|
||||
- [ ] Record make, model, year, trim, market, and relevant driver assistance options in `values.py`.
|
||||
- If all variants in the US market come equipped with the necessary ADAS package, no package documentation is required.
|
||||
- If a vehicle varies in features in another market, add a new [`CarDocs`](../opendbc/car/docs_definitions.py) entry.
|
||||
- [ ] Add every observed ECU firmware version to `fingerprints.py`.
|
||||
- [ ] Identify differences from related supported cars; verify reused signals and commands on this variant.
|
||||
- Gas, gear, and brake signals often move between ICE, hybrid, and EV variants (e.g. Hyundai, Subaru, Toyota SecOC).
|
||||
|
||||
- Harness and CAN topology
|
||||
|
||||
- [ ] Confirm harness pinout, CAN bus assignments, and CAN/CAN FD configuration.
|
||||
- [ ] Identify which ECUs send, receive, and forward each relevant message.
|
||||
- This is useful to know which messages we need to forward and block in safety.
|
||||
|
||||
- Stock behavior and power lifecycle
|
||||
|
||||
- [ ] Record baseline stock LKAS, ACC, stop-and-go, and driver override behavior for reference when writing the port.
|
||||
- Stock behavior should be matched when possible, e.g. how stock ACC commands standstill and resume.
|
||||
- [ ] Verify ignition detection, startup, shutdown, and vehicle sleep with the device connected.
|
||||
|
||||
## Identification and parameters
|
||||
|
||||
- [ ] The car identifies correctly on repeated cold starts without a manually forced fingerprint or fingerprint cache.
|
||||
- [ ] [Firmware queries](../opendbc/car/fw_versions.py) use the correct ECU addresses and buses without causing any DTCs on the dashboard.
|
||||
- [ ] Exact and [fuzzy fingerprinting](../opendbc/car/fw_versions.py) distinguish variants that need different control or safety behavior.
|
||||
- [ ] Unknown or ambiguous firmware does not silently select an incompatible platform.
|
||||
- A vehicle variant we do not support that has identical firmware (such as lacking ACC or LKAS hardware) should be detected with a CAN signal in `interface.py` or `carstate.py` depending on what we can check, and set `dashcamOnly`.
|
||||
- [ ] Reliable source for wheelbase, steering ratio, and other vehicle model parameters.
|
||||
- [ ] `minSteerSpeed`, `minEnableSpeed`, `autoResumeSng`, and `steerAtStandstill` in `carParams` match observed behavior.
|
||||
|
||||
## CAN parsing and message construction
|
||||
|
||||
- Signal definitions and input validation
|
||||
|
||||
- [ ] Message addresses, lengths, buses, frequencies, endianness, signedness, scales, and units are verified.
|
||||
- [ ] Checksums and counters validate against real captures, including counter wraparound.
|
||||
- [ ] Missing, stale, malformed, or checksum-invalid messages produce the expected invalid CAN state.
|
||||
- [ ] Variable-rate or intermittent messages have an [explicit frequency](../opendbc/can/parser.py) in `get_can_parsers` so they don't cause false CAN errors.
|
||||
- For example, VW `Blinkmodi_02`, `HCA_01`, and `AWV_03` are 1 Hz when inactive and 50 Hz when active.
|
||||
- [ ] Startup defaults and invalid signal values cannot look like valid engagement conditions.
|
||||
|
||||
- Outgoing messages and forwarding
|
||||
|
||||
- [ ] Copied stock messages preserve unrelated fields and use fresh source data.
|
||||
- [ ] Transmitted messages have the correct bus, timing, counters, checksums, and request bits.
|
||||
- [ ] Forwarding and replacement rules prevent duplicate actuation messages or bus loops.
|
||||
|
||||
- Diagnostics
|
||||
|
||||
- [ ] Diagnostic sessions, [ECU disabling](../opendbc/car/disable_ecu.py), and keepalives work across startup, restart, and shutdown, if used.
|
||||
- This is for disabling an ECU, such as to gain longitudinal control. Some ports do not require this.
|
||||
|
||||
## Vehicle state: basics
|
||||
|
||||
- Seatbelt and doors
|
||||
|
||||
- [ ] Driver seatbelt: buckle and unbuckle both update `seatbeltUnlatched` correctly.
|
||||
- [ ] Doors: open and close each door individually; `doorOpen` remains true until all monitored doors close.
|
||||
|
||||
- Gears
|
||||
|
||||
- [ ] Gear selection: ideally we parse park, reverse, neutral, drive, and alternate drive modes such as sport, B (brake) gear on hybrids, etc.
|
||||
- If we cannot parse all gears, ensure we can disengage cruise on non-drive/sport gears, or anything that stock ACC would normally disengage for.
|
||||
- [ ] Unknown gear values are handled explicitly instead of defaulting to drive.
|
||||
|
||||
- Pedals and regen
|
||||
|
||||
- [ ] Brake pedal: light press, firm press, hold, and release update `brakePressed` reliably.
|
||||
- [ ] Accelerator pedal: press and release update `gasPressed`, including while cruise is active.
|
||||
- [ ] Driver pedal signals are distinguished from braking or acceleration commanded by ACC/openpilot.
|
||||
- In the past, we found some accelerator signals either activate when stock ACC accelerates, or don't activate when the user's gas was under the stock ACC request. `gasPressed` must be true whenever the user is pressing the gas pedal any amount, in any state.
|
||||
- [ ] Regen paddles or other driver regen controls update `regenBraking`, if applicable.
|
||||
|
||||
- Parking brake and auto hold
|
||||
|
||||
- [ ] Parking brake engagement and release update `parkingBrake`.
|
||||
- [ ] Auto hold engagement and release update `brakeHoldActive`, independently of the brake pedal.
|
||||
|
||||
- Speed and standstill
|
||||
|
||||
- [ ] Vehicle speed and individual wheel speeds have correct units and plausible values.
|
||||
- [ ] Speed and `standstill` use the same signals as the safety model.
|
||||
- For example, Nissan and GM use the rear wheel speeds to match safety.
|
||||
- [ ] `standstill` distinguishes a complete stop from slow creep and updates promptly on drive-off.
|
||||
- [ ] Cluster speed and cruise set speed agree with the car in both metric and imperial settings.
|
||||
|
||||
- Steering
|
||||
|
||||
- [ ] Steering angle has the correct sign, scale, center offset, and behavior through its full range.
|
||||
- [ ] Steering torque has the correct sign and scale; driver torque and EPS torque are not confused.
|
||||
- [ ] `steeringPressed` detects driver input in both directions without triggering on normal commanded steering.
|
||||
- Use [`CS.update_steering_pressed`](../opendbc/car/interfaces.py) if the signal is noisy.
|
||||
|
||||
- Blinkers and blind spots
|
||||
|
||||
- [ ] Left/right blinkers are reported correctly.
|
||||
- If we only have lamp state, use [`CS.update_blinker_from_lamp`](../opendbc/car/interfaces.py) so it doesn't oscillate with the blinker.
|
||||
- If we only have stalk position, use [`CS.update_blinker_from_stalk`](../opendbc/car/interfaces.py) so short taps are held.
|
||||
- [ ] Left/right blind spot indications are mapped correctly and clear when the stock indication clears.
|
||||
|
||||
- Faults and stock system state
|
||||
|
||||
- [ ] Temporary and permanent steering faults are logged correctly and recover only when the fault clears.
|
||||
- EPS initializing states at startup are not permanent faults (e.g. VW, BYD).
|
||||
- Expected low-speed steering lockouts should set `lowSpeedAlert` instead of a fault, usually with hysteresis.
|
||||
- [ ] `accFaulted`, `carFaultedNonCritical`, `carNotReady`, and `vehicleSensorsInvalid` are reported correctly.
|
||||
- [ ] ESP disabled/active states and stock AEB/FCW states are parsed where available.
|
||||
- [ ] Stock LKAS settings (`invalidLkasSetting`) and conventional/non-adaptive cruise modes (`cruiseState.nonAdaptive`) are detected where relevant.
|
||||
- For example, Nissan requires stock LKAS off, Mazda requires it on, and Tesla requires Autosteer off.
|
||||
|
||||
## Buttons and cruise state
|
||||
|
||||
- [ ] Set, resume, cancel, main cruise, speed up/down, gap, and LKAS buttons are mapped where present.
|
||||
- [ ] Button press and release edges each occur once; held buttons do not cause unintended repeated engagement.
|
||||
- [ ] Short presses, long presses, and simultaneous button presses are handled correctly.
|
||||
- [ ] Cruise available (main), enabled, standstill, and set speed are parsed properly and match stock behavior.
|
||||
- Cruise enabled should stay true during driver accelerator override and at standstill. Some dash signals fall in these states (e.g. Subaru).
|
||||
- `cruiseState.standstill` should only be true if the car requires a resume press to drive off.
|
||||
- [ ] Set speed changes, unit changes, and minimum/maximum set speeds behave correctly.
|
||||
|
||||
## Engagement and cancellation
|
||||
|
||||
- Engagement conditions
|
||||
|
||||
- [ ] The intended set/resume action engages once, only when the car is ready (`carNotReady`).
|
||||
- [ ] Engagement refusal and disengagement for seatbelt, doors, gear, parking brake, and faults match the intended policy.
|
||||
|
||||
- Cancellation
|
||||
|
||||
- [ ] The physical cancel button disengages openpilot and cancels stock cruise as intended.
|
||||
- [ ] If stock ACC is controlling longitudinal, verify our cancel message stops commanding acceleration.
|
||||
- [ ] Software cancellation works in each supported cruise state, including standstill and driver accelerator override.
|
||||
- [ ] Cancel wins over simultaneous set/resume requests; repeated cancel requests do not resume or re-enable cruise.
|
||||
- If the cancel button can toggle pause/resume or the main button can engage, use `blockPcmEnable` to only allow PCM enable after a real user button press (e.g. Hyundai).
|
||||
- [ ] Cruise main off disengages and prevents unintended re-engagement.
|
||||
|
||||
- Disengagement and recovery
|
||||
|
||||
- [ ] Releasing a pedal, closing a door, buckling a belt, or clearing a fault does not unexpectedly re-engage unless we are still in the soft-disable grace period.
|
||||
- [ ] Stock ACC disengagement/fault causes the expected openpilot response when using stock longitudinal control.
|
||||
- [ ] Lateral-only and longitudinal-active states set their respective actuation request bits correctly.
|
||||
- For example, `CC.enabled` should be used when keeping the car's cruise engaged during a longitudinal override while `CC.longActive` falls.
|
||||
|
||||
## Lateral control
|
||||
|
||||
- Commands, limits, and driver override
|
||||
|
||||
- [ ] Small left/right commands produce the expected steering direction.
|
||||
- [ ] Command magnitude and rate limits match the platform and safety implementation.
|
||||
- [ ] Driver override reduces or cancels openpilot steering assistance as intended, in both directions.
|
||||
- Some platforms disengage both steering and cruise on a steering override (e.g. Tesla); use `steeringDisengage` for this.
|
||||
|
||||
- Low-speed behavior
|
||||
|
||||
- [ ] Steering behaves correctly around the minimum steering speed, including crossing it in both directions.
|
||||
- [ ] Check for low-speed oscillation and flag it if present. One solution is to low-pass filter the request below a certain speed.
|
||||
|
||||
- Faults and recovery
|
||||
|
||||
- [ ] Sustained steering, high steering rates, and request-bit transitions do not cause unexpected EPS faults.
|
||||
- [ ] Temporary steering unavailability stops active requests and produces the expected user alert.
|
||||
|
||||
- Tuning and reported output
|
||||
|
||||
- [ ] Tuning is checked across representative speeds and curves for oscillation, lag, and saturation.
|
||||
- [ ] Reported `actuatorsOutput` matches the command actually sent after clipping and rate limiting.
|
||||
|
||||
## Longitudinal control
|
||||
|
||||
- Supported control modes
|
||||
|
||||
- [ ] Stock ACC works correctly with openpilot lateral control, including cancel, override, and stop-and-go.
|
||||
- There are two levels of support with openpilot-controlled longitudinal:
|
||||
- [ ] Alpha longitudinal is chosen if we cannot maintain stock AEB or other safety features, or the car does not have a radar we can parse. openpilot longitudinal is then only available on development branches.
|
||||
- [ ] openpilot longitudinal is the only mode and available in all branches if both of the above requirements are met, plus we have a good understanding of the car's longitudinal API.
|
||||
|
||||
- Acceleration, braking, and driver override
|
||||
|
||||
- [ ] Acceleration and braking command signs, scales, limits, and rate limits are verified.
|
||||
- [ ] Acceleration, coasting, and braking transitions are smooth and free of conflicting requests.
|
||||
- Some cars accept gas and brake commands separately; never request both at once.
|
||||
- [ ] Brake press disengages while moving as well as while at a standstill.
|
||||
- [ ] Accelerator override and release behave correctly, i.e. we stop requesting acceleration (inactive, or 0 m/s^2) while continuing to steer.
|
||||
|
||||
- Stopping, holding, and resuming
|
||||
|
||||
- [ ] Low-speed braking reaches a complete stop without unexpected creep or brake release.
|
||||
- [ ] Stop hold works for short and extended stops; timeout and parking brake behavior are understood.
|
||||
- [ ] Resume/drive-off releases hold in the correct sequence without unintended acceleration.
|
||||
- [ ] Resume is sent only when requested and supported.
|
||||
- [ ] Stops and starts on grades behave correctly without unexpected rollback or parking brake/gear transitions.
|
||||
- [ ] `stopAccel`, `vEgoStopping`, `vEgoStarting`, `startingState`, and `startAccel` are tuned if needed.
|
||||
|
||||
- Powertrain states and faults
|
||||
|
||||
- [ ] Behavior is checked across relevant powertrain states, such as engine stop/start.
|
||||
- [ ] ACC faults and transient not-ready states inhibit requests appropriately and recover cleanly.
|
||||
|
||||
- Tuning and stock safety features
|
||||
|
||||
- [ ] `longitudinalActuatorDelay` and tuning are evaluated across representative speeds, following, stops, and starts.
|
||||
- [ ] Stock AEB/FCW interaction and any feature loss from ECU disabling are understood and documented.
|
||||
|
||||
## Radar, alerts, and stock features
|
||||
|
||||
- Radar
|
||||
|
||||
- [ ] Radar distance, lateral position, relative velocity, signs, units, and track IDs are verified, if supported.
|
||||
- [ ] Radar track creation, updates, disappearance, invalid targets, and timeouts behave correctly.
|
||||
- [ ] `radarDelay` is set if known.
|
||||
|
||||
- HUD and alerts
|
||||
|
||||
- [ ] HUD engaged/standby state, set speed, lane lines, lead indication, and gap bars match the requested state.
|
||||
- [ ] Takeover, steering unavailable, and other required alerts appear and clear correctly.
|
||||
|
||||
- Stock feature interaction
|
||||
|
||||
- [ ] Stock LKAS and ACC do not fight openpilot commands.
|
||||
- [ ] Stock safety messages are forwarded or arbitrated as intended; validate AEB interactions with replay/bench tests.
|
||||
- [ ] Normal operation does not introduce persistent dash warnings or diagnostic trouble codes.
|
||||
- Some cars may display warning messages on the dash or force a disengage if they detect a conflict between what their camera is sending and our request through read-back signals from the PCM or EPS. If we need to spoof a message to the camera/ADAS ECU, we need to verify it does not affect stock safety features such as LKAS or AEB.
|
||||
|
||||
## Safety and failure handling
|
||||
|
||||
- Message permissions and engagement
|
||||
|
||||
- [ ] The selected safety mode permits only intended transmit addresses, buses, and message lengths.
|
||||
- [ ] Safety engagement/disengagement agrees with the car interface for cruise, buttons, pedals, and motion state.
|
||||
- [ ] Non-zero actuation or active request bits are rejected when safety's [`controls_allowed`](../opendbc/safety/safety.h) is false.
|
||||
- [ ] Any messages permitted while disengaged are limited to their intended inactive/cancel behavior.
|
||||
|
||||
- Actuation limits
|
||||
|
||||
- [ ] Steering magnitude, rate, driver torque, measured torque, and request-bit checks are covered as applicable.
|
||||
- [ ] Longitudinal limits, inactive values, and cancel/resume restrictions are covered as applicable.
|
||||
- [ ] All actuation-capable fields in transmitted messages are constrained, including fields unused by the controller.
|
||||
|
||||
- Communication failures and recovery
|
||||
|
||||
- [ ] Invalid counters/checksums, missing required input messages, and stale state disable control as intended.
|
||||
- [ ] Relay malfunction detection and forwarding restrictions cover the actual harness topology.
|
||||
|
||||
- Test coverage
|
||||
|
||||
- [ ] Safety tests cover every new safety flag variant and reject disallowed commands.
|
||||
|
||||
## Regression tests
|
||||
|
||||
- Routes and automated checks
|
||||
|
||||
- [ ] Add representative routes to the [car test routes](../opendbc/car/tests/routes.py), covering relevant variants and modes.
|
||||
- [ ] Add or update tests for fingerprinting, parsing, command construction, and discovered edge cases.
|
||||
- [ ] Run applicable [car interface tests](../opendbc/car/tests/) and [safety tests](../opendbc/safety/tests/).
|
||||
- [ ] [`test_models.py`](../opendbc/car/tests/test_models.py) passes on the new routes; it replays them to check CAN validity and that safety agrees with carstate and accepts our commands.
|
||||
- [ ] Run the repository checks in [`test.sh`](../test.sh) and fix any failures.
|
||||
|
||||
- Regression validation
|
||||
|
||||
- [ ] Recheck related existing cars affected by shared DBC, controller, interface, or safety changes.
|
||||
- [ ] Repeat startup, engagement, cancellation, and shutdown over multiple drives and ignition cycles.
|
||||
@@ -0,0 +1 @@
|
||||
.github export-ignore
|
||||
@@ -0,0 +1 @@
|
||||
__pycache__/
|
||||
@@ -1,35 +1,53 @@
|
||||
# BluePilot angle source
|
||||
# Credits
|
||||
|
||||
Adapted from BluePilotDev/bluepilot bp-7.0 at
|
||||
[e1d051d7ba270261b4455068bd68f1a58db15a4a](https://github.com/BluePilotDev/bluepilot/commit/e1d051d7ba270261b4455068bd68f1a58db15a4a),
|
||||
principally Alan Polk, with John Christman, Jacob Neulight, Nathan Ingraham,
|
||||
Praeuner, tonesto7, Haibin Wen and other BluePilot, sunnypilot and comma contributors.
|
||||
Source paths: opendbc/sunnypilot/car/ford/lateral_angle_ext.py,
|
||||
fordcan_ext.py, human_turn.py and opendbc/safety/modes/ford.h.
|
||||
The simultaneous upstream LICENSE and LICENSE.md notices are retained.
|
||||
This software is licensed under a custom license requiring permission for use.
|
||||
This project uses software from Haibin Wen and SUNNYPILOT LLC and is licensed under a custom license requiring permission for use.
|
||||
The BluePilot Ford lateral library: Ford's four-signal lateral control in curvature and angle
|
||||
modes, the CAN messages that carry it, and the panda safety that checks it. Everything here is
|
||||
under the MIT terms in [LICENSE](LICENSE).
|
||||
|
||||
This is a StarPilot adapter to the September 2026 proposed library shape.
|
||||
It is not a released BluePilot library, tag, or claim of BluePilot validation.
|
||||
The existing curvature controller remains StarPilot-owned. Pinion curvature,
|
||||
curvature-primary settings and the unsafe native reset bypass are not imported.
|
||||
## Contributors
|
||||
|
||||
The exact upstream root notices are additionally retained as upstream-root-LICENSE
|
||||
and upstream-root-LICENSE.md; LICENSE/ LICENSE.md retain the opendbc notices.
|
||||
ORIGIN_HISTORY.tsv records the inspected path history with commit IDs/authors/dates.
|
||||
The private compact upstream root clone preserves its reachable root history.
|
||||
Pre-conversion opendbc submodule objects are not included in that root history.
|
||||
- **Alan Polk (`alan-polk`)**: principal author of the curvature and angle controllers, the
|
||||
human-turn behavior, the angle-mode side channel and its panda safety, and the lane centering
|
||||
trim.
|
||||
- **tonesto7**: the CAN message builders and BluePilot's early releases.
|
||||
- **John Christman**: lateral integration, platform data, and the angle-mode anti-stall work.
|
||||
- **Jacob Neulight**: the steering-pinion curvature measurement and its panda safety, and the
|
||||
shadow-curvature measurement.
|
||||
- **Blue Genie (`blue-genie`)**: the split of BluePilot's lateral control into opendbc that this
|
||||
library grew from.
|
||||
- **ghbarker**: the lane centering trim's confidence blend, fallback, filter and rate limit.
|
||||
- **Nathan Ingraham**: the angle-mode high-speed damping adjustment.
|
||||
- **Praeuner**: the angle-mode damping range.
|
||||
- **OneFast440**: the early Ford safety declarations.
|
||||
- **nitrobass24**: the library's structure, its inputs and settings plumbing, and the safety tests
|
||||
and CI.
|
||||
|
||||
Angle lane-centering trim is separately adapted from BluePilot bp-dev
|
||||
[e22afa6be9b881fa784c92ebb316db47728a3d81](https://github.com/BluePilotDev/bluepilot/commit/e22afa6be9b881fa784c92ebb316db47728a3d81),
|
||||
`opendbc/sunnypilot/car/ford/lane_center_trim.py` (blob
|
||||
`5eedebd79e99493624f9d6af0750d1dd5713306c`). Alan Polk authored the initial
|
||||
trim, model-position fallback, correction slew and default-strength changes;
|
||||
ghbarker contributed reference/NaN guards and tests. The source explicitly credits
|
||||
StarPilot/u/jc01rho for curvature-domain placement/geometry. The core adaptation
|
||||
adds immutable host projection and conservative stale/driver-pressure suppression.
|
||||
The pinned bp-7.0 controller baseline and default-disabled policy are unchanged.
|
||||
Both simultaneous root/opendbc MIT and Custom MIT notices also exist unchanged at
|
||||
this trim revision; all retained notices and permission acknowledgments apply
|
||||
without assigning an exclusive license or claiming a new permission grant.
|
||||
## Third-party code
|
||||
|
||||
### comma.ai's opendbc
|
||||
|
||||
MIT, Copyright (c) 2020, Comma.ai, Inc.
|
||||
|
||||
- `can/fordcan_ext.py`: `create_lat_ctl_msg` and `create_lat_ctl2_msg`, comma's Ford lateral
|
||||
message builders from `opendbc/car/ford/fordcan.py`, with ramp-type and precision parameters
|
||||
added.
|
||||
- `safety/lateral.h`: the measured-table rate check, curvature-error band and lateral-accel cap,
|
||||
from comma's earlier checks in `opendbc/safety/lateral.h`, and `ford_bp_curvature_cmd_checks`,
|
||||
adapted from comma's `steer_curvature_cmd_checks`.
|
||||
- `safety/ford.h`: the rate-of-change shape of the path angle and path offset checks, the
|
||||
curvature-error band in the shadow-curvature check, and the message-rate formula.
|
||||
- `core/lateral_curv_ext.py`: `apply_ford_curvature_limits_ext`, adapted from comma's
|
||||
`apply_ford_curvature_limits`, with its road-roll and lateral-accel constants.
|
||||
- Small pieces: the message packing and the yaw-rate curvature measurement in
|
||||
`core/carcontroller_ext.py`, and the model time-index formula in `core/values_ext.py`.
|
||||
- The BluePilot safety tests start from comma's Ford safety test skeletons.
|
||||
|
||||
### StarPilot
|
||||
|
||||
MIT, Copyright (c) 2026, firestar5683 and StarPilot contributors
|
||||
|
||||
- `core/lane_center_trim.py`: the lane-centering core (the correction formula, lookahead clip,
|
||||
lane-line probability, std and width gates, the 0.004 correction limit and the lane-line
|
||||
handling), ported from StarPilot's `selfdrive/controls/lib/lane_centering.py`
|
||||
(`LaneCenteringController`, originally by jc01rho). The confidence blend, fallback, filter and
|
||||
rate limit are BluePilot's.
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# Bringing BluePilot lateral to a fork
|
||||
|
||||
How an openpilot fork takes BluePilot's Ford lateral control. There are two ways to do it:
|
||||
|
||||
1. **Use a release on your own** (preferred). Pin a release of this library and write your own hooks and settings against [HOSTS.md](HOSTS.md). BluePilot isn't in the loop and nothing waits on BluePilot: you consume lateral code that has been driven on Fords.
|
||||
2. **Work with BluePilot.** BluePilot helps develop your hooks, keeps them in this repo, and tests the library on your fork in its CI, every PR and every night.
|
||||
|
||||
Either way the library is the same, and the same rules apply to it (below).
|
||||
|
||||
**This is a modified Ford safety model.** The panda safety allows signals and ranges that upstream's Ford safety blocks. Your fork decides whether to ship it, and whether BluePilot lateral is on or off by default for your users.
|
||||
|
||||
## The rules for either way
|
||||
|
||||
- **Pin a release, unmodified.** The library sits at `<pkg>/bluepilot_lateral/` in your opendbc (`<pkg>` is its package name) as one of:
|
||||
- a **git submodule** pointing at a release tag, if your fork uses submodules; updating is moving the pointer;
|
||||
- a **stamped copy** made with `tools/vendor.sh`, if it doesn't (see "The stamp tools").
|
||||
- **Don't edit inside the library folder**, carry it as a git subtree with its history, or copy parts of it elsewhere. A fix to the library is a PR to this repo; once it's released, every fork gets it.
|
||||
- **Everything else is yours:** the hooks in your opendbc, the code in your openpilot that fills the library's inputs every frame, and your settings UI.
|
||||
|
||||
## Option 1: use a release on your own
|
||||
|
||||
1. Pick a release from this repo's releases. Each one says what changed in the lateral control, the CAN messages and the panda safety, and what was validated on a car.
|
||||
2. Mount it (submodule or stamped copy, above).
|
||||
3. Write the hooks in your opendbc. [HOSTS.md](HOSTS.md) has the contract: the hook sites, the per-frame inputs and settings, the startup safety bits, and the host bindings to define if your safety function names differ from upstream opendbc's. Each release attaches the hook patch for sunnypilot/opendbc as a worked example.
|
||||
4. Write your adapter: the small module that hands the library your fork's types (`hosts/sunnypilot/` is the example to copy). It lives in your fork, outside the library folder.
|
||||
5. Run the library's tests in your tree ([HOSTS.md](HOSTS.md), "Running the tests") and drive it before you ship it.
|
||||
6. To update, move to a newer release and rerun your tests.
|
||||
|
||||
BluePilot doesn't review or approve any of it. Questions and bugs in the library are welcome as issues in this repo.
|
||||
|
||||
## Option 2: work with BluePilot
|
||||
|
||||
For a fork that wants BluePilot to help with the hooks and keep them working. Open an issue in this repo, "Host: <fork>": where the fork's source and integration branch are, how it carries its opendbc, and how it takes outside changes. From there, this repo keeps for that fork:
|
||||
|
||||
- **its adapter**, `hosts/<fork>/`, plus host bindings if its safety names differ from upstream's;
|
||||
- **its hooks**, `hosts/<fork>/hooks.patch`, until the fork carries them;
|
||||
- **`hosts/<fork>/host.sh`**: `tree <dir>` builds the fork's opendbc at a pinned commit with the hooks and the library, placed by `vendor.sh`; `test <dir>` runs the library tests, the fork's safety suite with the BluePilot safety tests, and coverage; `make <dir>` writes `hooks.patch` back after a hook is edited in `<dir>/host`;
|
||||
- **a CI job** that runs `host.sh tree` and `test` on every PR to this repo, so a library change that would break the fork fails here first;
|
||||
- **a nightly check** that tries the fork's latest integration branch: a pass moves the pinned commit, a failure opens one issue saying which step broke. Once the fork carries the hooks it tests the fork's tree as it is, and runs `check_vendored.sh` on its copy.
|
||||
|
||||
The steps:
|
||||
1. Develop and test the hooks and the fork's side in the fork, together.
|
||||
2. Add the adapter, `hooks.patch`, `host.sh`, the CI job and the nightly check here, in one PR.
|
||||
3. Release the library with them.
|
||||
4. BluePilot opens the PR to the fork: the hooks, its settings UI and the library at that release. Later releases arrive the same way, as PRs the fork takes when it chooses.
|
||||
|
||||
An adapter lives in this repo only if this repo's CI tests the library on that fork.
|
||||
|
||||
## The stamp tools
|
||||
|
||||
For a fork that takes no submodules. Both run from a clone of this repo that has the release tag (`git fetch --tags`).
|
||||
|
||||
**`tools/vendor.sh <tag> <folder>`** makes `<folder>` exactly the library at `<tag>`:
|
||||
- it empties the folder first, so files a release removed don't linger;
|
||||
- it copies the release's files, without the library's own CI (`.github/`);
|
||||
- it writes `<folder>/VERSION`: the tag and the commit, e.g. `v1.2.3 0a1b2c3d...`.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/BluePilotDev/bluepilot-lateral.git
|
||||
cd bluepilot-lateral
|
||||
tools/vendor.sh <tag> <fork checkout>/<path to its opendbc package>/bluepilot_lateral
|
||||
```
|
||||
|
||||
Commit the folder in the fork. To update, run it again with the new tag: the diff shows exactly what changed between the two releases.
|
||||
|
||||
**`tools/check_vendored.sh <folder>`** reads `<folder>/VERSION` and compares the folder with that release, file by file (Python caches ignored). It lists every added, removed or changed file, and exits non-zero if there is one:
|
||||
|
||||
```bash
|
||||
tools/check_vendored.sh <fork checkout>/<path to its opendbc package>/bluepilot_lateral
|
||||
```
|
||||
|
||||
Run it in your CI to catch an edit to the copy. For a fork working with BluePilot, this repo's nightly check runs it too.
|
||||
|
||||
## Installer note: nested submodule
|
||||
|
||||
A fork that embeds the library as a *nested* submodule (Option 2 — e.g. `fork → opendbc → opendbc/bluepilot_lateral`) can end up with the library missing after an end-user install. Some installers run a non-recursive `git submodule update --init`, which checks out the first-level submodule but not the library nested inside it, and the build then fails to import it. Installs that use `git submodule update --init --recursive` are unaffected.
|
||||
|
||||
If your fork is installed that way, self-heal on launch: before the build imports the library, when its directory is empty, run a recursive submodule update scoped to the embedding submodule — guard on the library's `__init__.py`, retry, and fail soft so an offline boot is no worse than before.
|
||||
|
||||
## How BluePilot releases
|
||||
|
||||
- Lateral changes and changes to the library's safety headers are driven on a car before they're released.
|
||||
- Every release says what changed in the lateral control, the CAN messages and the panda safety, and what was validated.
|
||||
@@ -0,0 +1,129 @@
|
||||
# Hosting the BluePilot lateral
|
||||
|
||||
A host is an opendbc fork with this library at `<pkg>/bluepilot_lateral/` and the hook patch applied, plus the openpilot on top of it that fills the library's inputs each frame. `<pkg>` is the fork's opendbc package: `opendbc` on sunnypilot/opendbc (ford-op/opendbc, under bp-sunnypilot), `iqdbc` on IQ.Pilot, `opendbc` on StarPilot. This page is what a host provides and what it gets back.
|
||||
|
||||
## Mounting the library
|
||||
|
||||
A host mounts this repo at `<pkg>/bluepilot_lateral/`, directly inside its opendbc package, as a git submodule (ford-op/opendbc) or, in a host that takes no submodules (IQPilot, at `artifacts/package_sources/iqdbc/iqdbc/bluepilot_lateral/`), as a stamped copy of a release. Updating is a bump of the submodule's commit, or vendoring the new release (below). The hook patch below carries every file ford-op/opendbc adds or changes outside the library, so since it mounts the library it also carries the mount's `.gitmodules` entry.
|
||||
|
||||
The library never names its host's package. Python imports are relative: the library's own modules by `.`/`..`, the host's opendbc by one more level (`from ...car.ford.values import CarControllerParams` in `core/`), and `tests/test_core_imports.py` fails on any absolute `opendbc`/`iqdbc`/`openpilot` import. Safety headers include each other as `../safety/<file>.h` and the host's declarations as `../../safety/declarations.h`, relative to the including file. The host includes the library's safety as `"<pkg>/bluepilot_lateral/safety/ford.h"`.
|
||||
|
||||
### Without submodules: a stamped copy
|
||||
|
||||
From a clone of this repo, `tools/vendor.sh <release tag> <host>/<pkg>/bluepilot_lateral` makes the folder exactly the library at that tag (files it no longer has are removed), without `.github/` (`.gitattributes`), plus a `VERSION` file: the tag and the commit. Nothing inside the folder is ever edited: a change goes into this repo, gets released, and the host vendors the new tag. `tools/check_vendored.sh <folder>` lists anything in the folder that differs from the release in its `VERSION` and fails if there is something; a host can run it in its CI, and this repo's IQPilot checks run it on IQPilot's copy.
|
||||
|
||||
### Host bindings
|
||||
|
||||
The safety headers are written against upstream opendbc's names. A host whose names differ defines these before it includes `safety/ford.h` (`safety/lateral_declarations.h` has the defaults):
|
||||
|
||||
| macro | default (upstream, sunnypilot) | IQ.Pilot |
|
||||
|---|---|---|
|
||||
| `BP_HOST_CURVATURE_LIMITS` | `CurvatureSteeringLimits` | `NativeCurvatureSteeringLimits` |
|
||||
| `BP_HOST_CURVATURE_STATE` | `curvature_state` | `native_curvature_state` |
|
||||
| `BP_HOST_RT_CURVATURE_RATE_CHECK` | `rt_curvature_rate_limit_check` | `rt_native_curvature_rate_limit_check` |
|
||||
| `BP_HOST_LAT_ALLOWED()` | `(controls_allowed \|\| controls_allowed_lateral)` | `is_lat_active()` |
|
||||
| `BP_HOST_SPEED_MISMATCH_CHECK()` | `speed_mismatch_check(vehicle_speed_2 ...)` | `((void)0)`: IQ's `ford_rx_hook` runs it |
|
||||
|
||||
The limits struct needs only `max_curvature`, `curvature_to_can`, `frequency`, `max_curvature_error` and `curvature_error_min_speed`.
|
||||
|
||||
## The hook patch
|
||||
|
||||
`tools/make_hook_patch.sh` prints it: every file outside the library that ford-op/opendbc changes from `master`, minus its own `.github/` (its CI). It fails unless the edited files are exactly the non-`.github` entries of `.github/bluepilot-hooks.txt`.
|
||||
|
||||
| file | what the hook does |
|
||||
|---|---|
|
||||
| `opendbc/car/ford/carcontroller.py` | Builds one `BluePilotLateral(CP, CP_SP, packer, CAN)`, the sunnypilot adapter (`hosts/sunnypilot/lateral.py`, below). Each frame `bp.update(CC_SP, frame)` says whether BluePilot lateral is active; if it is, `bp.step_lateral` sends the steering message at `STEER_STEP` (and returns the curvature to pass back next time) and `bp.step_lka` sends Lane_Assist_Data1 at `LKA_STEP`. Otherwise the stock path runs. |
|
||||
| `opendbc/car/structs.py` | Imports `LateralInputs` from `host_types.py` and adds `CarControlSP.lateralInputs` and `CarParamsSP.lateralInputsRequired`. |
|
||||
| `opendbc/sunnypilot/car/interfaces.py` | Calls `init_ford_safety_param_sp(CP, CP_SP, params_dict)` (`hosts/sunnypilot/safety_param.py`) at car init: the startup safety bits below, and `lateralInputsRequired` for every Ford. |
|
||||
| `opendbc/safety/modes/ford.h` | Includes `safety/ford.h` and makes one call per hook site: counter and quality checks for the pinion message, the yaw measurement skipped while the pinion source is on, `ford_bp_rx_hook` (pinion measurement), `ford_bp_lka_tx` (angle-mode state from Lane_Assist_Data1), `ford_bp_lmc_tx` in place of the stock steering check when `BP_LATERAL` is set (the stock check runs unchanged when it isn't), and the pinion RX check list from `ford_bp_init`. |
|
||||
| `opendbc/safety/tests/test_bluepilot_lateral.py` | New: hands the library's `safety/tests/` to upstream's test discovery, so upstream's `test.sh` and `mutation.py` run the BluePilot safety tests. |
|
||||
|
||||
## Every frame: inputs
|
||||
|
||||
A host fills these and the settings below for a car whose `CarParamsSP.lateralInputsRequired` is set: every Ford, BluePilot lateral on or off (`init_ford_safety_param_sp`).
|
||||
|
||||
`CarControlSP.lateralInputs` (`host_types.LateralInputs`):
|
||||
|
||||
| fields | from |
|
||||
|---|---|
|
||||
| `pathYawRate`, `pathX`, `pathY` | `modelV2.orientationRate.z`, `modelV2.position.x`, `.y` (33 points) |
|
||||
| `laneLineLeftX/Y`, `laneLineRightX/Y` | `modelV2.laneLines[1]`, `[2]` |
|
||||
| `laneLineProbs`, `laneLineStds` | `modelV2.laneLineProbs`, `.laneLineStds` |
|
||||
| `laneChangeState`, `laneChangeDirection` | `modelV2.meta` |
|
||||
| `steerRatio`, `stiffnessFactor`, `roll`, `angleOffsetDeg` | the learned vehicle parameters (`vehicleParameters`, paramsd) |
|
||||
| `lateralDelay` | `lateralDelay.lateralDelay` |
|
||||
| `valid` | true only when both the model and the vehicle parameters have been seen and every field above is filled |
|
||||
|
||||
Each path array has `host_types.MODEL_PATH_POINTS` (33) entries at the times in `host_types.MODEL_T_IDXS` (openpilot's `ModelConstants.T_IDXS`): the core interpolates over that grid. On a frame that isn't valid, or whose arrays aren't the full shape or contain a non-finite value, the library keeps the previous inputs.
|
||||
|
||||
## Every frame: settings
|
||||
|
||||
Every key in `opendbc.bluepilot_lateral.core.values_ext.BP_LATERAL_PARAMS` (the per-frame settings in `params/manifest.py`; hosts import the list from there), every frame, as (key, value) with the value as bytes (sunnypilot: `CarControlSP.params` entries): bools `"1"`/`"0"`, numbers as text. A missing, empty or unparsable value gives the manifest default. A host publishes its own registry default for a key nobody has set, so a host can carry its own defaults (BP7 uses 3.0 for `LC_PID_gain_UI_curv`). `python -m <pkg>.bluepilot_lateral.params.manifest` prints the keys as `params_keys.h` lines (IQ.Pilot drops `BACKUP`, which its params don't have).
|
||||
|
||||
## At startup: the safety bits
|
||||
|
||||
`values_ext.ford_safety_param(CP, params_dict)` reads the startup settings (`disable_BP_lat_UI`, `FordPrefSteerAngleCurvature`) and returns the bits. The host sends them to the panda in its second safety param (sunnypilot: the adapter ORs them into `CP_SP.safetyParam`). StarPilot has no second param: its adapter shifts them into bits 9-14 of its Ford `safetyParam` (`hosts/starpilot/safety_param.py`, clear of the bits StarPilot's Ford safety reads), and its `ford_init` hands `(param >> 9) & 0x3F` to `ford_bp_init`:
|
||||
|
||||
| bit | |
|
||||
|---|---|
|
||||
| 5 `BP_LATERAL` | the panda runs the BluePilot steering checks; the car controller sends the 4-signal messages only when it's set |
|
||||
| 0 `STEER_ANGLE_CURVATURE`, 1-4 geometry index | the pinion angle replaces the yaw rate as the curvature measurement, on platforms with a row in the pinion table |
|
||||
|
||||
The panda keeps what it was given at init: changing `disable_BP_lat_UI` while driving takes full effect at the next init.
|
||||
|
||||
## Reading status
|
||||
|
||||
`BluePilotLateral.status()` returns `host_types.LateralStatus`, this frame's values for a BluePilot UI, under the names BP7's `controllerStateBP` publisher reads off its car controller:
|
||||
|
||||
| field | |
|
||||
|---|---|
|
||||
| `lateralUncertainty` | from whichever strategy ran |
|
||||
| `angleRateLimited` | angle mode: the path angle soft rate limit bit this frame |
|
||||
| `curvatureRateLimited` | angle mode: the equivalent curvature would have been rate-limited |
|
||||
| `curvatureDeviationLimited` | either mode: the current-curvature error clip constrained the command |
|
||||
| `humanTurnLateralPaused` | angle mode: lateral sent inactive during a manual turn |
|
||||
| `stallBlipActive` | angle mode: the brief mode-0 pulse after a post-override stall |
|
||||
| `disable_BP_lat_UI` | BluePilot lateral off: the setting, or the panda bit not set at init |
|
||||
| `primary_lateral_control` | 0 = curvature, 1 = angle |
|
||||
|
||||
The four angle-mode values read false outside angle mode. bp-sunnypilot doesn't read status; it has no BluePilot UI.
|
||||
|
||||
## What the library uses from the host's opendbc
|
||||
|
||||
Every host is an opendbc fork, so the library uses these directly:
|
||||
|
||||
- `<pkg>.car`: `structs`, `DT_CTRL`, `ACCELERATION_DUE_TO_GRAVITY`, `scale_rot_inertia_per_mass` and `scale_tire_stiffness_per_mass` (or, on an opendbc from before the per-mass vehicle model, `scale_rot_inertia` and `scale_tire_stiffness`, at unit mass), `common.pid.PIDController`, `vehicle_model.VehicleModel` (either signature), `lateral` (`AngleSteeringLimits`, `apply_std_steer_angle_limits`, `ISO_LATERAL_ACCEL`), `ford.fordcan.CanBus`, `ford.values` (`CarControllerParams`, `FordFlags`).
|
||||
- `can/fordcan_ext.py` sets LateralMotionControl2's checksum itself: upstream's packer fills Ford checksums (and overwrites it with the same value), IQ.Pilot's doesn't.
|
||||
- The safety headers use `modes/ford.h`'s Ford constants and upstream's safety helpers, through the bindings above. `safety/lateral.h` and `can/fordcan_ext.py` copy four upstream functions, pinned per host package by `tests/test_bluepilot_upstream_copies.py`, so a host where one changed fails that test (a host with no pins skips it).
|
||||
- The safety tests use the host's test harness: `<pkg>.safety.tests.test_ford`, `common` and `libsafety.libsafety_py`. `tests/host.py` names the host's own structs, adapter and libsafety setters for the tests that touch them.
|
||||
|
||||
## The core's API, and the host adapters
|
||||
|
||||
The core takes only the library's types:
|
||||
|
||||
- `core.carcontroller_ext.BluePilotLateral(CP, safety_param, packer, CAN)`: `safety_param` is the startup bits (`ford_safety_param`).
|
||||
- `update(inputs, params, frame)`: `inputs` is a `host_types.LateralInputs`, `params` a `params.param_store.ParamStore` built from the frame's (key, value) settings. Then `step_lateral`, `step_lka` and `status()` as above.
|
||||
|
||||
Nothing under `core/`, `can/`, `params/` or `safety/` imports a host (`tests/test_core_imports.py`). `hosts/sunnypilot/`:
|
||||
|
||||
- `lateral.py`: a `BluePilotLateral` subclass with sunnypilot's types, `(CP, CP_SP, packer, CAN)` and `update(CC_SP, frame)`. It passes `CP_SP.safetyParam`, `CC_SP.lateralInputs` and the `CC_SP.params` entries as (key, value) pairs.
|
||||
- `safety_param.py`: `init_ford_safety_param_sp(CP, CP_SP, params_dict)` ORs `ford_safety_param` into `CP_SP.safetyParam`. Its own module, so car init imports only `values_ext`.
|
||||
|
||||
`hosts/iqpilot/`, the same for IQ.Pilot's types:
|
||||
|
||||
- `lateral.py`: `BluePilotLateral(CP, CP_IQ, packer, CAN)` and `update(CC_IQ, frame)`, passing `CP_IQ.iqSafetyFlags`, `CC_IQ.lateralInputs` and the `CC_IQ.params` entries.
|
||||
- `safety_param.py`: `init_ford_safety_param_iq(CP, CP_IQ, params_dict)` ORs `ford_safety_param` into `CP_IQ.iqSafetyFlags` (the panda's `current_safety_param_iq`) and sets `CP_IQ.lateralInputsRequired`.
|
||||
|
||||
`hosts/starpilot/`, for StarPilot, which has neither a second safety param nor CarControl / CarParams extension structs:
|
||||
|
||||
- `lateral.py`: `BluePilotLateral(CP, packer, CAN)`, reading the startup bits back out of the Ford `safetyParam`, and `update(frame_inputs, frame)`. `FrameInputs` carries `lateralInputs` and the `params` entries; StarPilot's openpilot side fills one each frame and hands it to the Ford CarController, in place of a CarControl extension.
|
||||
- `safety_param.py`: `init_ford_safety_param_starpilot(CP, params_dict)` ORs `ford_safety_param << 9` into `CP.safetyConfigs[-1].safetyParam`; `bp_safety_param(CP)` reads them back; `lateral_inputs_required(CP)` says whether to fill `FrameInputs`. StarPilot's Transit (`FordFlags.LKA_STEERING`, steering through Lane_Assist_Data1 alone) gets neither.
|
||||
|
||||
Another host writes its own adapter against the same API.
|
||||
|
||||
## Running the tests
|
||||
|
||||
- Python tests: `python -m unittest discover -s <pkg>/bluepilot_lateral/tests -t .`, from the directory that holds `<pkg>`.
|
||||
- Safety tests: the host's `<pkg>/safety/tests/test.sh` runs them, through a loader (ford-op/opendbc's `test_bluepilot_lateral.py`) or by naming `../../bluepilot_lateral/safety/tests` (IQ.Pilot: its cross-mode TX sweep reads `TX_MSGS` off every `Test*` class in its directory, so a loader there breaks it). Coverage of the BluePilot headers and `modes/ford.h`: `safety/tests/bluepilot_coverage.sh`, which runs the library's tests itself where the host has no loader; mutation: `safety/tests/bluepilot_mutation.py` (needs the host's `mutation.py`, which IQ.Pilot doesn't have).
|
||||
- On a clean sunnypilot/opendbc checkout: `tools/test_on_host.sh <checkout>`, from a ford-op/opendbc checkout. It copies the library as committed and applies the hook patch to a temporary clone, then runs both.
|
||||
- On IQPilot: `hosts/iqpilot/host.sh tree <dir>` builds iqdbc from IQ.Lvbs's repo at the pinned commit with `hosts/iqpilot/hooks.patch` (IQPilot's hooks, kept here until IQPilot carries them) and the library, vendored; `host.sh test <dir>` runs the library tests, IQPilot's safety suite with the BluePilot ones (minus `known_failures.txt`, IQPilot's own) and coverage; `host.sh make <dir>` writes the hook patch back after editing the hooks in `<dir>/host`. CI's `iqpilot` job runs tree and test. Every night `iqpilot pin check` tries IQ.Lvbs's latest `beta`: green moves the pin in a PR it merges itself (dropping `hooks.patch` once IQPilot carries the hooks), red keeps one issue open saying which step failed.
|
||||
@@ -1,4 +1,6 @@
|
||||
Copyright (c) 2020, Comma.ai, Inc.
|
||||
Copyright (c) 2026, firestar5683 and StarPilot contributors
|
||||
Copyright (c) 2024-2026, BluePilot contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
# Custom MIT License
|
||||
|
||||
Copyright (c) 2024, Haibin Wen, SUNNYPILOT LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to view and modify the Software, subject to the following conditions:
|
||||
|
||||
1. **Permission Required**: Permission Required for Commercial, For-Profit, or Closed Source Use: Use of the Software, in whole or in part, for any commercial purposes, for-profit projects, or in closed source projects requires explicit written permission from the original author(s).
|
||||
|
||||
2. **Redistribution**: Any redistribution of the Software, modified or unmodified, must retain this license notice and the following acknowledgment:
|
||||
"This software is licensed under a custom license requiring permission for use."
|
||||
|
||||
3. **Visibility**: Any project that uses the Software must visibly mention the following acknowledgment:
|
||||
"This project uses software from Haibin Wen and SUNNYPILOT LLC and is licensed under a custom license requiring permission for use."
|
||||
|
||||
4. **No Warranty**: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Contact sunnypilot Support <support@sunnypilot.ai> for permission requests.
|
||||
|
||||
---
|
||||
|
||||
Haibin Wen, SUNNYPILOT LLC
|
||||
@@ -1,38 +0,0 @@
|
||||
9d190417bf74563abfa6f36996f1e673e9ee0a1b Praeuner 2026-08-13T22:26:15-05:00 Update dampening factor to .25
|
||||
a881646ea5edc21c0db4c4e74076e0dc14671a4e alan-polk 2026-08-10T09:04:52-05:00 Merge pull request #176 from BluePilotDev/bp-no-stall
|
||||
9012f76666a5c90764fcaec40832b9a607488c27 John Christman 2026-08-05T17:49:59-04:00 anti-stall
|
||||
3610e3f18a0ad37d793d7ada61dbc739ab1077a3 Nathan Ingraham 2026-07-23T22:26:29-05:00 self.path_angle_gain_lowC_highV remains the platform-specific base gain. The toggle now controls separate self.user_dampening_factor. l ow_gain_calc uses: self.path_angle_gain_lowC_highV * self.user_dampening_factor UI placement, description, default, and 0.75–1.25 range remain unchanged.
|
||||
1db52bc79e607ddb00214254dd4d732615e27fe4 Nathan Ingraham 2026-07-23T00:00:51-05:00 -Added “High Speed Dampening” directly below “High Speed Adjustment Factor”, range 0.75–1.25, step 0.01, and default 1.0.
|
||||
5d994c24edf63f3b9004b2963f18ef4199e5d368 Jacob Neulight 2026-07-18T20:17:04-06:00 Ford safety: optional pinion-sourced angle_meas behind STEER_ANGLE_CURVATURE SP flag
|
||||
699c17d9fde62c690eed9d68eed7d731744d5137 Jacob Neulight 2026-07-18T19:53:46-06:00 Ford: publish angle-mode shadow curvature from measured curvature during overrides
|
||||
b7428fee4b116d4832f91c850d54cac2730de2ef alan-polk 2026-07-18T08:52:24-05:00 restore curvature safety checks to bp-6.0 logic.
|
||||
97867c1eb57b7472f6fc3de62f0fef576e5a5497 alan-polk 2026-07-16T19:18:05-05:00 Human Turn Reset for Angle (#118)
|
||||
7efee67e3e643d52b973895d6c6765c5cc12e314 alan-polk 2026-07-16T08:19:43-05:00 angle mode: rescale exit-detection threshold for 20Hz cadence
|
||||
1958a3501ead10ae1f496f0d11e1e8cda6e2dbcc alan-polk 2026-07-14T15:22:38-05:00 Bp 7.0 variable cleanup (#111)
|
||||
8f8d6d15f0a590f42b78de964ffb0d0af7f5d63d alan-polk 2026-07-14T08:53:28-05:00 Bp 7.0 panda2 (#109)
|
||||
11ac3b760671b489ca449d522f1edf240a232884 alan-polk 2026-07-09T07:53:21-05:00 angle mode: fix ROC values tuned for 100Hz cadence running at actual 20Hz
|
||||
dbdcea747e65f574d326a55c9bf1a9d6c9485e22 alan-polk 2026-07-08T09:21:32-05:00 Revert "ford: add current-curvature deviation check to angle mode (panda-side)"
|
||||
69a9714563622729ba304ac75354ba3fd37b0b39 alan-polk 2026-07-08T09:17:09-05:00 ford: add current-curvature deviation check to angle mode (panda-side)
|
||||
be7f86a5d0dc1ea3b3f0052d4b54ff0b8a70f843 John Christman 2026-07-06T21:53:55-04:00 cast on debug
|
||||
5c1fbb07dd4d7d950a01cb2cd1faddd90d41e632 alan-polk 2026-07-06T14:20:49-05:00 controllerStateBP: add angleRateLimited + curvatureRateLimited diagnostics
|
||||
06d654e5f48ea8fcffce6e98e48e3a30450b5097 alan-polk 2026-06-25T23:28:51-05:00 Revert "temp blend ratio change" — restore predicted-curvature blend to 0.50
|
||||
540e69fcf4861a2ce5d7ca41467effc231638e27 alan-polk 2026-06-22T08:35:44-05:00 temp blend ratio change
|
||||
e3819c7e630d85cfb44858840124fe7f72f1762d alan-polk 2026-06-13T20:47:09-05:00 ford.h: don't block angle mode on the inactive curvature signal
|
||||
5d343738e30267688d0ffa6eebe2ec3ca57b10ee alan-polk 2026-06-13T18:28:30-05:00 ford.h: widen path_angle ROC to 2% looser than lateral_angle_ext _soft_roc
|
||||
d0aac605f99d37e9da205e419f7989c1e9eaa386 alan-polk 2026-06-13T11:15:50-05:00 BluePilot: add angle-primary lateral control mode
|
||||
51d5de8991508c862e7c51333e5055425b2b8d23 alan-polk 2026-04-11T12:00:10-05:00 fixing missing punctation in ford.h
|
||||
8194ceb7e0bbc112c73d393a8f0add3261f6592d alan-polk 2026-04-11T11:51:49-05:00 make safety limits symetrical but control limits asymetrical. Add 1.5s duration to detect human turn
|
||||
db2bdff05df103d71df62f45c2a3cb5211aba6e6 alan-polk 2026-04-09T23:47:30-05:00 move all external logic to lateral_curv_ext.py. Apply some order of operations fixes to try and eleminate panda blocking messages.
|
||||
1a8ac23b5bd5d7952c15eb913c2b1e39f14ad92e Blue Genie 2026-04-08T15:10:24-05:00 Adjust panda limits
|
||||
892be076daf898d806983f847c72357accff7312 Blue Genie 2026-04-08T11:45:28-05:00 Add logging functionality
|
||||
ce7d27230657159332f64a253a0faea6cf8dcc95 alan-polk 2026-04-08T11:40:02-05:00 fix rate of change limits
|
||||
021b5146e80ac58fe2e81dbd848bc7cd409ef08e alan-polk 2026-04-07T16:24:38-05:00 align ford.h limits with dbc and carcontroller
|
||||
7f9212f7f8dc22c4a0a22443158554f4d3652a3c tonesto7 2026-04-05T21:28:37-04:00 Merge bp-6.1 upstream changes into bp-6.0-dev (#124)
|
||||
dd1a9d3fd8f60462d3d8ac6de3e7860e70e1f667 alan-polk 2025-12-12T12:22:31-06:00 convert opendbc from a submodule to a subfolder
|
||||
10101012d8a4cc37951a557023824cdd995443a6 alan-polk 2026-08-10T17:23:04Z BluePilot: angle-mode lane centering trim (advanced lane positioning)
|
||||
560e80e9d593badca08aa60e8ddb692e58033425 alan-polk 2026-08-11T12:50:00Z BluePilot: angle-mode lane centering falls back to model position, not zero
|
||||
94bf8144d4a683e9f3408f3babf892878f3b81f4 ghbarker 2026-08-12T01:31:33Z ford ALP: restore StarPilot guards (narrow lane, laneline std, NaN params) and cap applied correction at 0.0015
|
||||
80825b81568ad684fe4ec37a797d3cbaa004fdcf ghbarker 2026-08-13T01:04:02Z split applied-correction ceiling into its own PR per review
|
||||
e5b92cc48b34ea43ac756a96a96687c9851a188d ghbarker 2026-08-13T01:05:07Z add tests for the restored guards
|
||||
af4bc410c9ef417ba723212ceaa2a77758dd8ecb alan-polk 2026-08-14T03:24:28Z BluePilot: rate-limit the lane centering correction, not just filter it
|
||||
8eea9eb68e0c4126ee1c52f251e274d5865b6743 alan-polk 2026-08-19T13:33:00Z BluePilot: lower angle-mode lane centering default strength to 0.25
|
||||
|
Can't render this file because it contains an unexpected character in line 14 and column 85.
|
@@ -1,81 +1,14 @@
|
||||
# BluePilot Ford angle control
|
||||
# BluePilot lateral
|
||||
|
||||
StarPilot adaptation of BluePilotDev/bluepilot `bp-7.0`, pinned to
|
||||
[e1d051d7ba270261b4455068bd68f1a58db15a4a](https://github.com/BluePilotDev/bluepilot/commit/e1d051d7ba270261b4455068bd68f1a58db15a4a).
|
||||
The layout follows Alan Polk's September 2026 library proposal. This is an
|
||||
adaptation, not an upstream library release.
|
||||
BluePilot's Ford lateral control as a library an opendbc fork mounts at `opendbc/bluepilot_lateral/`: both modes (curvature and angle), the CAN messages that carry them, the panda safety that checks them, and a settings manifest.
|
||||
|
||||
Existing curvature control remains the default. Explicit `FordLateralMode=1`
|
||||
selects angle control next drive; the saved choice persists when unavailable.
|
||||
Only this selector is exposed. Fixed defaults and bounds live in
|
||||
`params/manifest.json`; pinion-yaw correction and curvature-primary tuning are
|
||||
outside this library. Angle lane-centering trim is available as a default-disabled
|
||||
core capability, separately sourced from BluePilot bp-dev
|
||||
`e22afa6be9b881fa784c92ebb316db47728a3d81`; it adds no public settings.
|
||||
**This is a modified Ford safety model.** The panda safety allows signals and ranges that upstream's Ford safety blocks. A fork chooses whether to ship it.
|
||||
|
||||
`core/` accepts scalar/model inputs without openpilot, Params or messaging
|
||||
imports. `can/` owns encoding, `safety/ford_bp.h` owns native enforcement, and
|
||||
`hosts/starpilot.py` connects the host. `params/protocol.json` generates the wire
|
||||
constants. The folder is vendored without submodules, LFS or an external runtime.
|
||||
**Status:** `v0.2.0`. It runs on bp-sunnypilot, a sunnypilot fork, through ford-op/opendbc (sunnypilot's opendbc plus the hooks).
|
||||
|
||||
## Admission
|
||||
- [FORKS.md](FORKS.md): how a fork takes BluePilot lateral and works with BluePilot: carrying the library (a submodule or a stamped copy), the stamp tools, and adding a fork.
|
||||
- [HOSTS.md](HOSTS.md): what a host provides, the hook patch, and how to run the tests.
|
||||
- [can/LANE_ASSIST_DATA1.md](can/LANE_ASSIST_DATA1.md): the Lane_Assist_Data1 bits angle mode uses.
|
||||
- [CREDITS.md](CREDITS.md) and [LICENSE](LICENSE) (MIT).
|
||||
|
||||
Classic: Bronco Sport Mk1, Escape Mk4, Focus Mk4, Maverick Mk1, Explorer Mk6.
|
||||
CAN FD: Escape Mk4.5, Expedition Mk4, F150 Mk14, F150 Lightning Mk1, Ranger Mk2,
|
||||
Mustang Mach-E Mk1.
|
||||
|
||||
Exact classic safety parameters 32/33 map to angle 128/129; CAN FD 18/19 and
|
||||
66/67 map to 130/131. Parameter 131 requires DEBUG; RELEASE retains stock 130.
|
||||
Longitudinal, radar, fingerprint and parser policy remain host-owned. Admission
|
||||
uses the final CarParams contract, not brand or steering-control enum alone.
|
||||
|
||||
AOL, alternative experiences, Transit, Edge, Mondeo, SecOC, passive, dashcam,
|
||||
notCar and unsupported static flags are excluded. Auxiliary noOutput requires
|
||||
parameter zero; production deployments with two Pandas remain unqualified until
|
||||
actual startup, bus routing and public acknowledgments are verified.
|
||||
|
||||
## Safety design
|
||||
|
||||
The extension uses small Ford hooks without changing shared safety headers or
|
||||
existing curvature namespaces. Active steering requires ordinary controls
|
||||
permission, healthy physical RX and a fresh matching public Panda acknowledgment
|
||||
of safety parameter, alternative experience, controls, RX validity, heartbeat and
|
||||
faults. Cruise, gas/brake, checksum, counter, quality and freshness checks apply.
|
||||
Neutral mode cannot grant permission.
|
||||
|
||||
The BluePilot reset-bypass latch is omitted. Rejected frames preserve accepted
|
||||
actuator history and withdraw the shadow lease; accepted neutral commands or
|
||||
permission withdrawal reset history. Lost counters recover through neutral
|
||||
resynchronization. Version or checksum failure cannot fall through to curvature
|
||||
control.
|
||||
|
||||
Active frames carry neutral curvature, offset and curvature rate, with checked
|
||||
mode, ramp, precision, path angle, counter, checksum and cadence. Native enforcement
|
||||
checks the shadow against the actual path across gains 0.95–1.3, bounds both by
|
||||
`min(0.02, 2.4114/v²)`, and bounds the full actual-curvature interval against
|
||||
measured curvature above 10 m/s. Actual path also obeys the 3.5886 m/s³ equivalent
|
||||
jerk limit and rolling message-rate limit. Core quantization and timing margins
|
||||
keep packed commands inside these bounds; an impossible intersection is neutral.
|
||||
|
||||
The versioned 0x3CA announcement preserves StarPilot's existing byte 4 bit 1
|
||||
curvature flag and requires it clear in angle mode. It carries a counter, signed
|
||||
shadow curvature, protocol version and checksum; its lease expires after 60 ms.
|
||||
The shadow derives from the final path after rate limiting and saturation.
|
||||
|
||||
Control computes at 20 Hz, announces through 33 Hz LKA, then sends the latest
|
||||
announced result at 20 Hz, adding at most one steering tick. Permission withdrawal
|
||||
sends neutral immediately. Human-turn recovery uses bounded neutral blips without
|
||||
a native bypass. The retained Dom factory steering delay is 0.22 s; pinned
|
||||
BluePilot uses 0.20 s.
|
||||
|
||||
These stricter native limits and versioned metadata intentionally differ from
|
||||
BluePilot 7.0. Identical bytes or safety decisions are not claimed for affected
|
||||
commands. Runtime and physical ECU qualification remain pending.
|
||||
|
||||
## Attribution and notices
|
||||
|
||||
See `CREDITS.md`, `ORIGIN_HISTORY.tsv` and the project `THIRD_PARTY_NOTICES.md`.
|
||||
Both upstream MIT and Custom MIT notices are retained verbatim in `LICENSE`,
|
||||
`LICENSE.md`, `upstream-root-LICENSE` and `upstream-root-LICENSE.md`. The maintainer
|
||||
reports author permission to use this adaptation under MIT. Both published notices
|
||||
remain intact.
|
||||
History before 2026-09-15 lives in [BluePilotDev/bluepilot](https://github.com/BluePilotDev/bluepilot).
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
v0.2.1 2b9dc6cc784faac47a1885d0bda0561f7b76f74d
|
||||
@@ -1,34 +0,0 @@
|
||||
"""Adapter to the proposed BluePilot library; angle only, exact source pinned."""
|
||||
from opendbc.bluepilot_lateral.can.ford import lateral, lka
|
||||
from opendbc.bluepilot_lateral.core.angle import Command
|
||||
from opendbc.bluepilot_lateral.hosts.starpilot import Host, qualified
|
||||
|
||||
|
||||
class BluePilotLateral:
|
||||
def __init__(self, CP, *, host=None):
|
||||
if not qualified(CP):
|
||||
raise ValueError("Angle mode is not configured")
|
||||
self.host = host or Host(CP)
|
||||
self.canfd = bool(CP.flags & 1)
|
||||
self.pending = Command()
|
||||
self.announced = Command()
|
||||
self.announced_ns = 0
|
||||
self.lka_counter = 0
|
||||
self.apply_curvature_last = 0.0
|
||||
|
||||
def step_lateral(self, CC, CS, packer, CAN, frame, now_ns):
|
||||
self.pending = self.host.update(CC, CS)
|
||||
command = self.announced
|
||||
# A pending permission withdrawal is immediate; an increase waits until
|
||||
# the 33Hz side channel has announced the matching final actuator.
|
||||
if not self.pending.active or not 0 < self.announced_ns <= now_ns <= self.announced_ns + 60_000_000:
|
||||
command = Command()
|
||||
self.announced = command
|
||||
return [lateral(packer, CAN.main, command, canfd=self.canfd, counter=frame // 5)]
|
||||
|
||||
def step_lka(self, packer, CAN, now_ns):
|
||||
self.announced = self.pending
|
||||
self.announced_ns = now_ns
|
||||
message = lka(packer, CAN.main, self.announced.shadow_curvature, self.lka_counter)
|
||||
self.lka_counter = (self.lka_counter + 1) % 8
|
||||
return [message]
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# Lane_Assist_Data1: BluePilot's angle-mode side channel
|
||||
|
||||
In angle mode the curvature signal in `LateralMotionControl2` is held at 0 and path angle does the
|
||||
steering, so the panda has no commanded curvature to check against the car's measured curvature.
|
||||
BluePilot sends that curvature, the *shadow curvature*, plus an "angle mode engaged" flag, in bits of
|
||||
`Lane_Assist_Data1` (0x3CA) that carry no DBC signal. openpilot sends this message itself at 33 Hz,
|
||||
and the panda reads it in the TX hook as it goes out. A separate message was tried first: the panda
|
||||
never receives its own TX, and an RX check for it blocked controls car-wide (2026-07-09).
|
||||
|
||||
Packed by `create_lka_msg` in `fordcan_ext.py`; read by `ford_bp_lka_tx` in `../safety/ford.h`. The
|
||||
byte positions, mask and scale are named once on each side (`BP_LKA_*` in `fordcan_ext.py`,
|
||||
`FORD_BP_LKA_*` in `../safety/ford_declarations.h`), and `../tests/test_lane_assist_data1.py` checks
|
||||
that they agree with each other and with the DBC.
|
||||
|
||||
## Layout
|
||||
|
||||
Bit numbers are within the byte, 7 = most significant. Stock signals are from `ford_lincoln_base_pt.dbc`.
|
||||
|
||||
| byte | bits | stock Ford signal | BluePilot |
|
||||
|---|---|---|---|
|
||||
| 0 | 7-5 | `LkaActvStats_D2_Req` | |
|
||||
| 0 | 4-2 | `LdwActvStats_D_Req` | |
|
||||
| 0 | 1-0 | `LdwActvIntns_D_Req` | |
|
||||
| 1-2 | 12 bits | `LaCurvature_No_Calc` | |
|
||||
| 2-3 | 12 bits | `LaRefAng_No_Req` | |
|
||||
| 4 | 7 | `LaRampType_B_Req` | |
|
||||
| 4 | 6-5 | `LkaDrvOvrrd_D_Rq` | |
|
||||
| 4 | 4-1 | none | free |
|
||||
| 4 | 0 | none | **angle mode engaged** |
|
||||
| 5-6 | all | none | **shadow curvature** |
|
||||
| 7 | all | none | free |
|
||||
|
||||
## Fields
|
||||
|
||||
- **Angle mode engaged** (byte 4 bit 0): 1 while angle mode is the selected lateral mode and
|
||||
BluePilot lateral is on.
|
||||
- **Shadow curvature** (bytes 5-6): int16, big-endian, 1e-6 1/m per unit, clamped to
|
||||
-32768..32767 (about ±0.0328 1/m). It is the curvature angle mode's path angle was derived from,
|
||||
after the same error clip curvature mode applies; while the driver is pressing, inactive, or
|
||||
during a human-turn or stall-blip pulse, it is the car's measured curvature instead. It is sent
|
||||
negated, in Ford's sign convention, like the curvature and path angle in `LateralMotionControl2`.
|
||||
|
||||
## What the panda does
|
||||
|
||||
- It latches both fields from every `Lane_Assist_Data1` frame that goes through its TX hook,
|
||||
including one it blocks for another reason.
|
||||
- **Angle mode engaged widens the path-angle range.** While the flag is set, the lateral command's
|
||||
path angle may use the DBC's full range (-0.5 to 0.5235 rad); otherwise it is held to ±0.25 rad
|
||||
(`ford_lmc_checks`). The flag alone does it: curvature 0 without the flag keeps the tight range.
|
||||
- **The shadow curvature check.** For a lateral command (`LateralMotionControl` on CAN or
|
||||
`LateralMotionControl2` on CAN FD) with curvature 0, steering enabled and the flag set, the
|
||||
panda scales the shadow curvature by 0.05 to curvature CAN units (2e-5 1/m, truncated toward
|
||||
zero) and, above 10 m/s, blocks the frame if it is outside the measured curvature ± the curvature
|
||||
error band + 1 (`ford_shadow_curvature_error_check`). It has no rate limit: path angle has its own.
|
||||
- `ford_bp_init` clears the flag.
|
||||
|
||||
## Not here yet
|
||||
|
||||
A protocol-version byte and a shared bit allocation for other forks (StarPilot) are Phase 3 (the StarPilot host issue, #16):
|
||||
they change bytes on the wire.
|
||||
@@ -1,37 +0,0 @@
|
||||
"""Ford angle codecs, adapted from BluePilot fordcan_ext.py at e1d051d7.
|
||||
|
||||
Core commands use planner signs; path and shadow are negated on the wire.
|
||||
The v1 side channel retains byte4 bit1 for StarPilot's curvature announcement.
|
||||
"""
|
||||
from opendbc.bluepilot_lateral.params import protocol as p
|
||||
|
||||
|
||||
def checksum4(dat):
|
||||
return (15 - (sum(dat[:7]) & 15)) & 15
|
||||
|
||||
|
||||
def lka(packer, bus, shadow, counter):
|
||||
addr, original, _ = packer.make_can_msg("Lane_Assist_Data1", bus, {"LaRefAng_No_Req": 0., "LaCurvature_No_Calc": 0.})
|
||||
dat = bytearray(original)
|
||||
raw = max(-20000, min(20000, round(-shadow / p.SHADOW_SCALE))) & 0xFFFF
|
||||
dat[4] |= p.ANGLE_MASK | ((counter % p.COUNTER_MODULUS) << p.COUNTER_SHIFT)
|
||||
dat[5], dat[6] = raw >> 8, raw & 255
|
||||
dat[7] = p.VERSION | (checksum4(dat) << p.CHECKSUM_SHIFT)
|
||||
return addr, bytes(dat), bus
|
||||
|
||||
|
||||
def lateral(packer, bus, command, *, canfd, counter):
|
||||
values = {"HandsOffCnfm_B_Rq": 0, "LatCtlRampType_D_Rq": 2 if command.active else 0,
|
||||
"LatCtlPrecision_D_Rq": command.precision, "LatCtlPathOffst_L_Actl": 0.0,
|
||||
"LatCtlPath_An_Actl": -command.path_angle, "LatCtlCurv_No_Actl": 0.0}
|
||||
if canfd:
|
||||
values.update({"LatCtl_D2_Rq": int(command.active), "LatCtlCrv_NoRate2_Actl": 0.0,
|
||||
"LatCtlPath_No_Cnt": counter % 16, "LatCtlPath_No_Cs": 0})
|
||||
dat = packer.make_can_msg("LateralMotionControl2", bus, values)[1]
|
||||
fields = ((dat[2] << 3) | (dat[3] >> 5), (dat[6] << 3) | (dat[7] >> 5),
|
||||
((dat[3] & 31) << 6) | (dat[4] >> 2), ((dat[4] & 3) << 8) | dat[5])
|
||||
total = int(command.active) + counter % 16 + sum(v + (v >> 8) for v in fields)
|
||||
values["LatCtlPath_No_Cs"] = 255 - (total & 255)
|
||||
return packer.make_can_msg("LateralMotionControl2", bus, values)
|
||||
values.update({"LatCtl_D_Rq": int(command.active), "LatCtlRng_L_Max": 0, "LatCtlCurv_NoRate_Actl": 0.0})
|
||||
return packer.make_can_msg("LateralMotionControl", bus, values)
|
||||
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
BluePilot Ford CAN message builder extensions (lateral only).
|
||||
|
||||
Extended versions of the stock fordcan.py lateral builders:
|
||||
- Dynamic ramp_type and precision_type for the 4-signal lateral commands
|
||||
- Lane_Assist_Data1 carries the angle-mode side channel read by bluepilot_lateral/safety/ford.h
|
||||
|
||||
Trimmed for the lateral proof-of-concept: the longitudinal, HUD and ICBM builders from
|
||||
BluePilot's fordcan_ext.py are out of scope here.
|
||||
"""
|
||||
|
||||
from ...car.ford.fordcan import CanBus
|
||||
|
||||
|
||||
# BluePilot: the Lane_Assist_Data1 side channel, see LANE_ASSIST_DATA1.md. ford_declarations.h has the
|
||||
# same values (tests/test_lane_assist_data1.py).
|
||||
BP_LKA_ANGLE_MODE_BYTE = 4
|
||||
BP_LKA_ANGLE_MODE_MASK = 0x1
|
||||
BP_LKA_SHADOW_MSB_BYTE = 5 # shadow curvature: int16, big-endian, bytes 5-6
|
||||
BP_LKA_SHADOW_LSB_BYTE = 6
|
||||
BP_LKA_SHADOW_CURVATURE_SCALE = 1e-6 # 1/meter per raw unit
|
||||
|
||||
|
||||
def create_lka_msg(packer, CAN: CanBus, lat_active: bool, hud_control,
|
||||
angle_mode_engaged: bool = False, shadow_curvature: float = 0.0):
|
||||
"""
|
||||
Creates a CAN message for the Ford LKA Command.
|
||||
|
||||
BluePilot extension: accepts lat_active and hud_control parameters for future
|
||||
lane departure warning integration (unused so far).
|
||||
|
||||
Also carries angle_mode_engaged + shadow_curvature (Ford sign) in bits with no DBC signal: the
|
||||
layout and what the panda does with them are in LANE_ASSIST_DATA1.md.
|
||||
|
||||
Frequency is 33Hz.
|
||||
"""
|
||||
addr, dat, bus = packer.make_can_msg("Lane_Assist_Data1", CAN.main, {})
|
||||
dat = bytearray(dat)
|
||||
|
||||
shadow_curvature_raw = int(round(shadow_curvature / BP_LKA_SHADOW_CURVATURE_SCALE))
|
||||
shadow_curvature_raw = max(-32768, min(32767, shadow_curvature_raw)) & 0xFFFF
|
||||
|
||||
dat[BP_LKA_ANGLE_MODE_BYTE] |= BP_LKA_ANGLE_MODE_MASK if angle_mode_engaged else 0
|
||||
dat[BP_LKA_SHADOW_MSB_BYTE] = (shadow_curvature_raw >> 8) & 0xFF
|
||||
dat[BP_LKA_SHADOW_LSB_BYTE] = shadow_curvature_raw & 0xFF
|
||||
|
||||
return addr, bytes(dat), bus
|
||||
|
||||
|
||||
def create_lat_ctl_msg(packer, CAN: CanBus, lat_active: bool, ramp_type: int, precision_type: int,
|
||||
path_offset: float, path_angle: float, curvature: float, curvature_rate: float):
|
||||
"""
|
||||
Creates a CAN message for the Ford TJA/LCA Command (non-CAN FD).
|
||||
|
||||
BluePilot extension: dynamic ramp_type and precision_type parameters.
|
||||
Stock hardcodes ramp_type=0 (Slow) and precision_type=1 (Precise).
|
||||
|
||||
Ford lane centering uses a third-order polynomial to describe the road centerline:
|
||||
c0 (path_offset): lateral offset between vehicle and centerline (positive is right)
|
||||
c1 (path_angle): heading angle between vehicle and centerline (positive is right)
|
||||
c2 (curvature): curvature of the centerline (positive is left)
|
||||
c3 (curvature_rate): rate of change of curvature
|
||||
|
||||
Frequency is 20Hz.
|
||||
"""
|
||||
values = {
|
||||
"LatCtlRng_L_Max": 0, # Unknown [0|126] meter
|
||||
"HandsOffCnfm_B_Rq": 0, # Unknown: 0=Inactive, 1=Active [0|1]
|
||||
"LatCtl_D_Rq": 1 if lat_active else 0, # Mode: 0=None, 1=ContinuousPathFollowing, 2=InterventionLeft,
|
||||
# 3=InterventionRight, 4-7=NotUsed [0|7]
|
||||
"LatCtlRampType_D_Rq": ramp_type, # Ramp speed: 0=Slow, 1=Medium, 2=Fast, 3=Immediate [0|3]
|
||||
"LatCtlPrecision_D_Rq": precision_type, # Precision: 0=Comfortable, 1=Precise, 2/3=NotUsed [0|3]
|
||||
"LatCtlPathOffst_L_Actl": path_offset, # Path offset [-5.12|5.11] meter
|
||||
"LatCtlPath_An_Actl": path_angle, # Path angle [-0.5|0.5235] radians
|
||||
"LatCtlCurv_NoRate_Actl": curvature_rate, # Curvature rate [-0.001024|0.00102375] 1/meter^2
|
||||
"LatCtlCurv_No_Actl": curvature, # Curvature [-0.02|0.02094] 1/meter
|
||||
}
|
||||
return packer.make_can_msg("LateralMotionControl", CAN.main, values)
|
||||
|
||||
|
||||
def create_lat_ctl2_msg(packer, CAN: CanBus, mode: int, ramp_type: int, precision_type: int,
|
||||
path_offset: float, path_angle: float, curvature: float,
|
||||
curvature_rate: float, counter: int):
|
||||
"""
|
||||
Creates a CAN message for the Ford Lane Centering command (CAN FD).
|
||||
|
||||
BluePilot extension: dynamic ramp_type and precision_type parameters.
|
||||
Stock hardcodes ramp_type=0 (Slow) and precision_type=1 (Precise).
|
||||
|
||||
This message replaces LateralMotionControl on CAN FD platforms and includes
|
||||
counter and checksum fields.
|
||||
|
||||
Frequency is 20Hz.
|
||||
"""
|
||||
values = {
|
||||
"LatCtl_D2_Rq": mode, # Mode: 0=None, 1=PathFollowingLimitedMode, 2=PathFollowingExtendedMode,
|
||||
# 3=SafeRampOut, 4-7=NotUsed [0|7]
|
||||
"LatCtlRampType_D_Rq": ramp_type, # 0=Slow, 1=Medium, 2=Fast, 3=Immediate [0|3]
|
||||
"LatCtlPrecision_D_Rq": precision_type, # 0=Comfortable, 1=Precise, 2/3=NotUsed [0|3]
|
||||
"LatCtlPathOffst_L_Actl": path_offset, # [-5.12|5.11] meter
|
||||
"LatCtlPath_An_Actl": path_angle, # [-0.5|0.5235] radians
|
||||
"LatCtlCurv_No_Actl": curvature, # [-0.02|0.02094] 1/meter
|
||||
"LatCtlCrv_NoRate2_Actl": curvature_rate, # [-0.001024|0.001023] 1/meter^2
|
||||
"HandsOffCnfm_B_Rq": 0, # 0=Inactive, 1=Active [0|1]
|
||||
"LatCtlPath_No_Cnt": counter, # [0|15]
|
||||
"LatCtlPath_No_Cs": 0, # [0|255]
|
||||
}
|
||||
|
||||
# Computed here, not left to the packer: upstream's packer fills Ford checksums itself (and overwrites
|
||||
# this with the same value), but a host's packer may not (IQ.Pilot's doesn't), and the PSCM drops a
|
||||
# frame whose checksum is wrong.
|
||||
dat = packer.make_can_msg("LateralMotionControl2", CAN.main, values)[1]
|
||||
values["LatCtlPath_No_Cs"] = lat_ctl2_checksum(mode, counter, dat)
|
||||
return packer.make_can_msg("LateralMotionControl2", CAN.main, values)
|
||||
|
||||
|
||||
def lat_ctl2_checksum(mode: int, counter: int, dat: bytes) -> int:
|
||||
"""LatCtlPath_No_Cs of a packed LateralMotionControl2: upstream's ford_checksum over the message's
|
||||
checksum fields (mode, counter, curvature, curvature rate, path angle, path offset)."""
|
||||
curvature = (dat[2] << 3) | (dat[3] >> 5)
|
||||
curvature_rate = (dat[6] << 3) | (dat[7] >> 5)
|
||||
path_angle = ((dat[3] & 0x1F) << 6) | (dat[4] >> 2)
|
||||
path_offset = ((dat[4] & 0x3) << 8) | dat[5]
|
||||
|
||||
checksum = mode + counter
|
||||
for sig_val in (curvature, curvature_rate, path_angle, path_offset):
|
||||
checksum += sig_val + (sig_val >> 8)
|
||||
return 0xFF - (checksum & 0xFF)
|
||||
@@ -1,208 +0,0 @@
|
||||
"""Angle-primary Ford steering adapted from Alan Polk's BluePilot bp-7.0.
|
||||
|
||||
Source e1d051d7ba270261b4455068bd68f1a58db15a4a lateral_angle_ext.py.
|
||||
See ../LICENSE, ../LICENSE.md and ../CREDITS.md. This core has no vehicle,
|
||||
openpilot, Params, messaging or model-constant imports.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
import math
|
||||
from ..params import limits as bounds
|
||||
from .trim import LaneCenterTrim, LaneGeometry, TrimConfig
|
||||
|
||||
|
||||
def clip(value, low, high):
|
||||
return max(low, min(high, value))
|
||||
|
||||
|
||||
def interp(value, xs, ys):
|
||||
if value <= xs[0]:
|
||||
return ys[0]
|
||||
for i in range(1, len(xs)):
|
||||
if value < xs[i]:
|
||||
return ys[i - 1] + (ys[i] - ys[i - 1]) * (value - xs[i - 1]) / (xs[i] - xs[i - 1])
|
||||
return ys[-1]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Inputs:
|
||||
active: bool
|
||||
speed: float
|
||||
measured_curvature: float
|
||||
desired_curvature: float
|
||||
steering_pressed: bool = False
|
||||
steering_angle: float = 0.0
|
||||
yaw_predictions: tuple = ()
|
||||
time_indices: tuple = ()
|
||||
lateral_delay: float = 0.15
|
||||
lane_change: bool = False
|
||||
lane_direction: int = 0
|
||||
limit_status: int = 0
|
||||
geometry: LaneGeometry | None = None
|
||||
speed_min: float | None = None
|
||||
speed_max: float | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Command:
|
||||
active: bool = False
|
||||
path_angle: float = 0.0
|
||||
shadow_curvature: float = 0.0
|
||||
precision: int = 1
|
||||
|
||||
|
||||
class AngleController:
|
||||
def __init__(self, *, low_gain=1.0, high_gain=1.15, trim_config=None):
|
||||
self.trim_config = TrimConfig() if trim_config is None else trim_config
|
||||
self.trim = LaneCenterTrim()
|
||||
self.low_gain = low_gain
|
||||
self.high_gain = high_gain
|
||||
self.path_last = 0.0
|
||||
self.speed_last = 0.1
|
||||
self.desired_last = 0.0
|
||||
self.human_timer = 0.0
|
||||
self.press_last = False
|
||||
self.preturned = False
|
||||
self.press_timer = 0.0
|
||||
self.blip_frames = 0
|
||||
self.blip_cooldown = 0.0
|
||||
self.blip_hold = 0.0
|
||||
self.blip_count = 0
|
||||
|
||||
def reset(self):
|
||||
self.trim.reset()
|
||||
self.path_last = 0.0
|
||||
self.speed_last = 0.1
|
||||
self.desired_last = 0.0
|
||||
self.human_timer = 0.0
|
||||
self.press_last = False
|
||||
self.preturned = False
|
||||
self.press_timer = 0.0
|
||||
self.blip_frames = 0
|
||||
self.blip_cooldown = 0.0
|
||||
self.blip_hold = 0.0
|
||||
self.blip_count = 0
|
||||
|
||||
def step(self, i: Inputs) -> Command:
|
||||
values = (i.speed, i.measured_curvature, i.desired_curvature, i.steering_angle, i.lateral_delay)
|
||||
if not i.active or not all(math.isfinite(v) for v in values) or i.speed < 0.0:
|
||||
self.reset()
|
||||
return Command()
|
||||
speed_min = max(i.speed, 0.1) if i.speed_min is None else i.speed_min
|
||||
speed_max = max(i.speed, 0.1) if i.speed_max is None else i.speed_max
|
||||
if (not all(math.isfinite(v) for v in (speed_min, speed_max)) or
|
||||
not 0 < speed_min <= max(i.speed, 0.1) <= speed_max):
|
||||
self.reset()
|
||||
return Command()
|
||||
if i.steering_pressed and not self.press_last:
|
||||
self.preturned = abs(i.steering_angle) > 45.0
|
||||
self.press_last = i.steering_pressed
|
||||
self.human_timer = self.human_timer + 0.05 if i.steering_pressed and abs(i.steering_angle) > 45.0 else 0.0
|
||||
if self.human_timer + 1e-9 >= (3.0 if self.preturned else 1.5):
|
||||
self.trim.reset()
|
||||
self.path_last = 0.0
|
||||
self.desired_last = i.desired_curvature
|
||||
self.blip_frames = self.blip_count = 0
|
||||
self.blip_hold = self.blip_cooldown = self.press_timer = 0.0
|
||||
return Command()
|
||||
if i.steering_pressed:
|
||||
self.press_timer += 0.05
|
||||
else:
|
||||
if self.press_timer >= 0.5 and self.blip_cooldown <= 0.0 and self.blip_frames <= 0 and abs(self.path_last) < 0.1:
|
||||
self.blip_frames = 6
|
||||
self.press_timer = 0.0
|
||||
if self.blip_frames > 0:
|
||||
self.blip_frames -= 1
|
||||
self.trim.reset()
|
||||
self.path_last = 0.0
|
||||
self.desired_last = i.desired_curvature
|
||||
if self.blip_frames == 0:
|
||||
self.blip_cooldown = 2.0
|
||||
return Command()
|
||||
|
||||
base = clip(i.lateral_delay, 0.1, 0.15) + 0.05
|
||||
predictions_valid = (len(i.yaw_predictions) == len(i.time_indices) and len(i.time_indices) >= 17 and
|
||||
all(math.isfinite(v) for v in (*i.yaw_predictions, *i.time_indices)) and
|
||||
all(b > a for a, b in zip(i.time_indices, i.time_indices[1:], strict=False)))
|
||||
curves = tuple(y / max(0.01, i.speed) for y in i.yaw_predictions) if predictions_valid else ()
|
||||
entering = bool(curves) and abs(interp(base, i.time_indices, curves)) > abs(i.desired_curvature)
|
||||
factor = 1.0 if entering else interp(abs(i.desired_curvature), (0.005, 0.020), (1.0, 0.0))
|
||||
lookup = base + bounds.VLT_EXTRA * interp(i.speed, (11.176, 24.5872), (1.0, 0.0)) * factor
|
||||
predicted = interp(lookup, i.time_indices, curves) if curves else i.desired_curvature
|
||||
saturated = i.limit_status >= 2 or self.path_last >= 0.5235 * 0.9 or self.path_last <= -0.5 * 0.9
|
||||
falling = abs(i.desired_curvature) < abs(self.desired_last) - 0.010
|
||||
blend = bounds.BLEND * 0.25 if not entering and (i.limit_status >= 1 or saturated or falling) else bounds.BLEND
|
||||
requested = predicted * blend + i.desired_curvature * (1.0 - blend)
|
||||
self.desired_last = i.desired_curvature
|
||||
precision = 0 if i.lane_change and ((i.lane_direction == 1 and requested < 0) or
|
||||
(i.lane_direction == 2 and requested > 0)) else 1
|
||||
# BluePilot's default lane-change factors are both 1.0.
|
||||
# Trim requested curvature before every existing actuator/safety limiter.
|
||||
requested = self.trim.update(requested, i.speed, i.geometry, self.trim_config,
|
||||
active=i.active, lane_change=i.lane_change, steering_pressed=i.steering_pressed)
|
||||
kappa = clip(requested, i.measured_curvature - 0.002, i.measured_curvature + 0.002) if i.speed > 9.0 else requested
|
||||
low = interp(i.speed, (13.5, 26.82), (1.0, self.low_gain))
|
||||
high = interp(i.speed, (13.5, 26.82), (1.3, self.high_gain))
|
||||
gain = interp(abs(kappa), (0.0007, 0.001), (low, high))
|
||||
path = kappa * i.speed * gain
|
||||
if saturated:
|
||||
last_mag, magnitude = abs(self.path_last), abs(path)
|
||||
if magnitude > last_mag:
|
||||
path = self.path_last
|
||||
elif last_mag - magnitude > 0.02:
|
||||
path = math.copysign(last_mag - 0.02, self.path_last)
|
||||
elif i.limit_status >= 1:
|
||||
path = clip(path, -abs(self.path_last), abs(self.path_last))
|
||||
roc = interp(i.speed, (10.0, 15.0, 25.0), (0.055, 0.0425, 0.009))
|
||||
path = clip(clip(path, -0.5235, 0.5), self.path_last - roc, self.path_last + roc)
|
||||
|
||||
# Tightening relative to bp-7.0: the shadow describes the final actuator,
|
||||
# including ROC/saturation. A truthful derived curvature must fit both the
|
||||
# measured-curvature interval and the modern ISO envelope. If those bands
|
||||
# cannot meet the previous actuator ROC, send a neutral mode-0 frame.
|
||||
effective_speed = max(i.speed, 0.1)
|
||||
cap = min(bounds.CURVATURE_MAX, bounds.LATERAL_ACCEL / max(speed_max, 1.0) ** 2)
|
||||
low_kappa, high_kappa = -cap, cap
|
||||
if i.speed > 9.0:
|
||||
low_kappa = max(low_kappa, i.measured_curvature - 0.0018)
|
||||
high_kappa = min(high_kappa, i.measured_curvature + 0.0018)
|
||||
# Native cannot trust a host-selected feel gain. Intersect every admitted
|
||||
# gain (0.95..1.3), so the worst equivalent curvature remains bounded.
|
||||
lower_gain = bounds.GAIN_MAX if low_kappa >= 0.0 else bounds.GAIN_MIN
|
||||
upper_gain = bounds.GAIN_MIN if high_kappa >= 0.0 else bounds.GAIN_MAX
|
||||
lower_speed = speed_max if low_kappa >= 0.0 else speed_min
|
||||
upper_speed = speed_min if high_kappa >= 0.0 else speed_max
|
||||
lower = max(-bounds.PATH_MAX, low_kappa * lower_speed * lower_gain, self.path_last - roc)
|
||||
upper = min(-bounds.PATH_MIN, high_kappa * upper_speed * upper_gain, self.path_last + roc)
|
||||
# Bound the actual-angle equivalent derivative at the worst admitted gain.
|
||||
# 45ms core margin remains inside the native elapsed-time/50ms cap.
|
||||
jerk_delta = bounds.LATERAL_JERK / max(i.speed - 1.0, 1.0) ** 2 * bounds.CORE_STEER_PERIOD_S * bounds.GAIN_MIN * effective_speed
|
||||
jerk_center = self.path_last / self.speed_last * effective_speed
|
||||
lower = max(lower, jerk_center - jerk_delta)
|
||||
upper = min(upper, jerk_center + jerk_delta)
|
||||
lower += bounds.PATH_ROUNDING_MARGIN
|
||||
upper -= bounds.PATH_ROUNDING_MARGIN
|
||||
if lower > upper:
|
||||
self.trim.reset()
|
||||
self.path_last = 0.0
|
||||
return Command()
|
||||
path = clip(path, lower, upper)
|
||||
self.path_last = path
|
||||
self.speed_last = effective_speed
|
||||
shadow = path / (effective_speed * gain)
|
||||
|
||||
self.blip_cooldown = max(0.0, self.blip_cooldown - 0.05)
|
||||
gap = i.desired_curvature - i.measured_curvature
|
||||
stalled = (not i.steering_pressed and not i.lane_change and i.speed > 9.0 and abs(gap) > 0.004 and
|
||||
abs(i.desired_curvature) > abs(i.measured_curvature))
|
||||
if stalled:
|
||||
if abs(kappa - requested) > 1e-9 and self.blip_cooldown <= 0.0:
|
||||
self.blip_hold += 0.05
|
||||
if self.blip_hold >= 0.5 and self.blip_count < 3 and abs(path) < 0.1:
|
||||
self.blip_frames = 6
|
||||
self.blip_hold = 0.0
|
||||
self.blip_count += 1
|
||||
else:
|
||||
self.blip_hold = 0.0
|
||||
if i.steering_pressed or abs(gap) < 0.002:
|
||||
self.blip_count = 0
|
||||
return Command(True, path, shadow, precision)
|
||||
@@ -0,0 +1,234 @@
|
||||
"""
|
||||
BluePilot Ford lateral: the one object the Ford CarController calls.
|
||||
|
||||
Holds the two strategies, LateralCurvExt and LateralAngleExt, with the inputs and state they
|
||||
share, and does the strategy dispatch and message assembly. Lives above both strategy modules
|
||||
(rather than inside either) so it can call into both without the circular import that would
|
||||
result from one depending on the other.
|
||||
|
||||
The CarController owns apply_curvature_last, which the stock and BluePilot paths share: it is
|
||||
passed into step_lateral and the result returned, so switching BluePilot lateral off mid-drive
|
||||
continues from the last command, as before.
|
||||
"""
|
||||
|
||||
import math
|
||||
from types import SimpleNamespace
|
||||
|
||||
try:
|
||||
from ...car import scale_rot_inertia_per_mass, scale_tire_stiffness_per_mass
|
||||
except ImportError:
|
||||
# a host on opendbc from before the per-mass vehicle model (IQ.Pilot): its mass-scaled helpers at unit
|
||||
# mass are the per-mass values
|
||||
from ...car import scale_rot_inertia, scale_tire_stiffness
|
||||
|
||||
def scale_rot_inertia_per_mass(wheelbase):
|
||||
return scale_rot_inertia(1.0, wheelbase)
|
||||
|
||||
def scale_tire_stiffness_per_mass(wheelbase, center_to_front, tire_stiffness_factor):
|
||||
return scale_tire_stiffness(1.0, wheelbase, center_to_front, tire_stiffness_factor)
|
||||
from ...car.ford.values import CarControllerParams, FordFlags
|
||||
from ...car.vehicle_model import VehicleModel
|
||||
from ..can import fordcan_ext
|
||||
from .lateral_angle_ext import LateralAngleExt
|
||||
from .lateral_curv_ext import LateralCurvExt, PrimaryLateralControl
|
||||
from ..params.manifest import value
|
||||
from ..params.param_store import ParamStore
|
||||
from .values_ext import FORD_PINION_WHEELBASE, FordSafetyFlagsSP
|
||||
from .human_turn import HumanTurnDetector
|
||||
from ..host_types import LateralInputs, LateralStatus
|
||||
from .lateral_inputs import ModelView, VehicleParamsView, lateral_inputs_complete
|
||||
|
||||
|
||||
def pinion_vehicle_model(CP):
|
||||
"""Vehicle model for the pinion-angle curvature measurement: CarSpecs, except where BluePilot's
|
||||
pinion geometry uses another wheelbase (FORD_PINION_WHEELBASE, the same value as the C table),
|
||||
keeping the platform's weight split and tire model."""
|
||||
wheelbase = FORD_PINION_WHEELBASE.get(CP.carFingerprint)
|
||||
if wheelbase is None:
|
||||
return VehicleModel(CP)
|
||||
center_to_front = wheelbase * CP.centerToFront / CP.wheelbase
|
||||
front, rear = scale_tire_stiffness_per_mass(wheelbase, center_to_front, CP.tireStiffnessFactor)
|
||||
inertia = scale_rot_inertia_per_mass(wheelbase)
|
||||
# Both VehicleModel signatures: upstream's per-mass fields, and the mass-scaled ones an older host's
|
||||
# model reads, at unit mass (mass cancels out of the model, so the two are the same model).
|
||||
return VehicleModel(SimpleNamespace(
|
||||
rotationalInertiaPerMass=inertia, wheelbase=wheelbase,
|
||||
centerToFront=center_to_front, steerRatio=CP.steerRatio,
|
||||
tireStiffnessFrontPerMass=front, tireStiffnessRearPerMass=rear,
|
||||
mass=1.0, rotationalInertia=inertia, tireStiffnessFront=front, tireStiffnessRear=rear, steerRatioRear=0.))
|
||||
|
||||
|
||||
class InputTracker:
|
||||
"""The model / vehicle-parameter inputs both strategies read, and the car's measured curvature.
|
||||
Owned by BluePilotLateral and updated once per frame from the host's LateralInputs, before either
|
||||
strategy runs."""
|
||||
|
||||
def __init__(self, CP, safety_param: int):
|
||||
# Model / vehicle-parameter inputs arrive per frame from the host (see update)
|
||||
self.VM = pinion_vehicle_model(CP) # used only for the pinion-angle curvature measurement
|
||||
self.model = None
|
||||
self.lp = None
|
||||
self.lateral_delay = 0.0
|
||||
|
||||
# BluePilot: steering-angle curvature measurement (bad-yaw-sensor workaround).
|
||||
# Mirrors the STEER_ANGLE_CURVATURE flag the safety firmware reads from
|
||||
# current_safety_param_sp -- both layers must always agree, so this is init-time
|
||||
# state from the startup safety bits (values_ext.ford_safety_param), never a live settings read:
|
||||
# a live flip against stale firmware would fight the panda.
|
||||
self.bp_pinion_curvature_enabled = bool(safety_param & FordSafetyFlagsSP.STEER_ANGLE_CURVATURE)
|
||||
|
||||
def get_current_curvature(self, CS):
|
||||
"""Measured curvature of the car right now (OP sign convention).
|
||||
|
||||
The single measurement source for every BluePilot lateral consumer: the deviation
|
||||
clip, the stall detector, and the shadow curvature published to ford.h's angle-mode
|
||||
deviation check. The default source is the RCM yaw rate -- the same family stock
|
||||
ford.h derives its angle_meas from. The shadow value judged against that check must
|
||||
always come from the same measurement as the check's own reference, so route all
|
||||
reads through here.
|
||||
|
||||
With the steering-angle curvature measurement enabled (FordPrefSteerAngleCurvature
|
||||
toggle -> FordSafetyFlagsSP.STEER_ANGLE_CURVATURE), the pinion angle via the vehicle
|
||||
model is used instead: some vehicles (e.g. a 2021 Explorer with a faulty RCM)
|
||||
broadcast implausible VehYaw_W_Actl (sign-inverted vs IMU and steering geometry)
|
||||
while its CAN quality flag still reads OK. The pinion angle (SteeringPinion_Data,
|
||||
PSCM) is an equivalent measurement, independently validated against the comma IMU
|
||||
(corr +0.99), and the panda safety angle_meas switches to the same source (see
|
||||
bluepilot_lateral/safety/ford.h) -- the layers always agree. angleOffsetDeg/roll come from
|
||||
liveParameters (paramsd, IMU-derived, not the car yaw sensor).
|
||||
"""
|
||||
if self.bp_pinion_curvature_enabled:
|
||||
angle_offset_deg = self.lp.angleOffsetDeg if self.lp is not None else 0.0
|
||||
roll = self.lp.roll if self.lp is not None else 0.0
|
||||
return -self.VM.calc_curvature(math.radians(CS.out.steeringAngleDeg - angle_offset_deg),
|
||||
CS.out.vEgoRaw, roll)
|
||||
return -CS.out.yawRate / max(CS.out.vEgoRaw, 0.1)
|
||||
|
||||
def update(self, li: LateralInputs):
|
||||
"""Take this frame's model / vehicle-parameter inputs from the host and update the vehicle model.
|
||||
|
||||
A frame is used only if it has the full modelV2 shape this code indexes into (33 path points,
|
||||
both inner lane lines, 3+ probs/stds); anything else is ignored and the previous inputs are
|
||||
kept, exactly as a stale SubMaster message was. That also makes fuzzed or partial inputs
|
||||
safe: the producer's valid flag is necessary but not trusted on its own."""
|
||||
if li.valid and lateral_inputs_complete(li):
|
||||
self.model = ModelView(li)
|
||||
self.lp = VehicleParamsView(li)
|
||||
self.lateral_delay = li.lateralDelay
|
||||
|
||||
if self.lp is not None:
|
||||
x = max(self.lp.stiffnessFactor, 0.1)
|
||||
sr = max(self.lp.steerRatio, 0.1)
|
||||
self.VM.update_params(x, sr)
|
||||
|
||||
|
||||
class SharedLateralState:
|
||||
"""The values both strategies read and write, so either mode sees what the other left, as when
|
||||
the strategies were mixins on one object. bp_curvature_deviation_limited is the BluePilot UI's
|
||||
deviation flag, from whichever mode ran."""
|
||||
|
||||
def __init__(self):
|
||||
self.path_angle_last = 0.0
|
||||
self.precision_type = 1 # 1=Precise, 0=Comfortable
|
||||
self.bp_curvature_deviation_limited = False
|
||||
self.human_turn_detector = HumanTurnDetector()
|
||||
|
||||
|
||||
class BluePilotLateral:
|
||||
def __init__(self, CP, safety_param: int, packer, CAN):
|
||||
"""safety_param: the startup safety bits the panda was given (values_ext.ford_safety_param)."""
|
||||
self.CP = CP
|
||||
self.frame = 0
|
||||
self.inputs = InputTracker(CP, safety_param)
|
||||
self.shared = SharedLateralState()
|
||||
self.curv = LateralCurvExt(self.inputs, self.shared)
|
||||
self.angle = LateralAngleExt(CP, self.inputs, self.shared)
|
||||
self.packer = packer
|
||||
self.CAN = CAN
|
||||
|
||||
self.disable_BP_lat_UI = False
|
||||
self.lateral_uncertainty = 0.0 # from whichever strategy ran last, for status()
|
||||
# the panda only accepts 4-signal messages when this bit was set at init
|
||||
self.bp_lateral_allowed = bool(safety_param & FordSafetyFlagsSP.BP_LATERAL)
|
||||
|
||||
def update(self, inputs: LateralInputs, params: ParamStore, frame):
|
||||
"""Every frame: take this frame's model / vehicle-parameter inputs and settings, both from the host
|
||||
(HOSTS.md). Returns whether BluePilot lateral is active."""
|
||||
self.frame = frame
|
||||
self.inputs.update(inputs)
|
||||
self.curv.update_lateral_params(params)
|
||||
self.angle.update_angle_params(params)
|
||||
# One flag for every BP consumer (LatCtl dispatch, LKA angle-mode bits, mode reporting): the UI
|
||||
# toggle, and the panda bit that was set at init. No 4-signal traffic to a stock-configured panda.
|
||||
self.disable_BP_lat_UI = value(params, "disable_BP_lat_UI") or not self.bp_lateral_allowed
|
||||
return not self.disable_BP_lat_UI
|
||||
|
||||
def step_lateral(self, CC, CS, actuators, apply_curvature_last):
|
||||
"""At STEER_STEP: the steer message(s). Returns (can_sends, apply_curvature): the CarController
|
||||
keeps it and passes it back next time as apply_curvature_last."""
|
||||
# BluePilot: select the BP lateral strategy by primary control variable.
|
||||
# angle -> LateralAngleExt: kappa -> path_angle (c1), apply_curvature held at 0.
|
||||
# curvature -> LateralCurvExt: full 4-signal curvature-primary (default).
|
||||
# Both return a LateralResult packed identically below.
|
||||
# Do not run the stock curvature limits here or overwrite apply_curvature_last before the
|
||||
# strategy runs. Panda rate-checks desired_curvature vs the last TX on the bus; that must match
|
||||
# the prior frame's lat.apply_curvature only (not an intermediate stock-limited value).
|
||||
angle_mode = self.curv.primary_lateral_control == PrimaryLateralControl.angle
|
||||
if angle_mode:
|
||||
lat = self.angle.update_angle_strategy(CC, CS, actuators)
|
||||
else:
|
||||
lat = self.curv.update(CC, CS, actuators, apply_curvature_last, self.CP)
|
||||
apply_curvature = float(lat.apply_curvature)
|
||||
self.lateral_uncertainty = float(lat.lateralUncertainty)
|
||||
|
||||
# BluePilot: angle-mode human-turn override -- send lateral inactive (mode 0) while the
|
||||
# driver manually turns, so the PSCM releases cleanly instead of stalling 2-3 s on
|
||||
# re-engage (observed on Mach-E). Panda-clean: every ford.h check has a legitimate
|
||||
# !steer_control_enabled branch for the zeroed frames; on release, path_angle ramps back
|
||||
# from 0 through the soft ROC. Curvature mode keeps its own reset_steering path (zeroed
|
||||
# signals with mode still active) in LateralCurvExt.
|
||||
# The stall blip (lateral_angle_ext.py) rides the same mode-0 path: a short pulse that
|
||||
# resets the PSCM's post-override attenuation when the deviation clip deadlocks hands-free.
|
||||
lat_active = CC.latActive and not (angle_mode and (self.angle.angle_human_turn_active or self.angle.angle_stall_blip_active))
|
||||
|
||||
can_sends = []
|
||||
if self.CP.flags & FordFlags.CANFD:
|
||||
mode = 1 if lat_active else 0
|
||||
counter = (self.frame // CarControllerParams.STEER_STEP) % 0x10
|
||||
can_sends.append(fordcan_ext.create_lat_ctl2_msg(
|
||||
self.packer, self.CAN, mode, lat.ramp_type, lat.precision_type,
|
||||
-lat.path_offset, -lat.path_angle, -lat.apply_curvature, -lat.curvature_rate, counter
|
||||
))
|
||||
else:
|
||||
can_sends.append(fordcan_ext.create_lat_ctl_msg(
|
||||
self.packer, self.CAN, lat_active, lat.ramp_type, lat.precision_type,
|
||||
-lat.path_offset, -lat.path_angle, -lat.apply_curvature, -lat.curvature_rate
|
||||
))
|
||||
return can_sends, apply_curvature
|
||||
|
||||
def status(self) -> LateralStatus:
|
||||
"""This frame's values for a BluePilot UI (HOSTS.md), as BP7 sets them on its car controller."""
|
||||
angle_mode = self.curv.primary_lateral_control == PrimaryLateralControl.angle
|
||||
return LateralStatus(
|
||||
lateralUncertainty=self.lateral_uncertainty,
|
||||
angleRateLimited=bool(angle_mode and self.angle.bp_angle_rate_limited),
|
||||
curvatureRateLimited=bool(angle_mode and self.angle.bp_curvature_rate_limited),
|
||||
curvatureDeviationLimited=bool(self.shared.bp_curvature_deviation_limited),
|
||||
humanTurnLateralPaused=bool(angle_mode and self.angle.angle_human_turn_active),
|
||||
stallBlipActive=bool(angle_mode and self.angle.angle_stall_blip_active),
|
||||
disable_BP_lat_UI=bool(self.disable_BP_lat_UI),
|
||||
primary_lateral_control=int(self.curv.primary_lateral_control),
|
||||
)
|
||||
|
||||
def step_lka(self, CC, hud_control):
|
||||
"""At LKA_STEP: the Lane_Assist_Data1 message."""
|
||||
# BluePilot: tell bluepilot_lateral/safety/ford.h whether angle mode is engaged, packed into
|
||||
# Lane_Assist_Data1's unused bits. shadow_curvature is negated to match the
|
||||
# path_angle/apply_curvature wire convention that ford.h's angle_meas is calibrated against.
|
||||
# Only called while BluePilot lateral is active (update returned True this frame), so the mode
|
||||
# alone decides.
|
||||
angle_mode_engaged = self.curv.primary_lateral_control == PrimaryLateralControl.angle
|
||||
shadow_curvature = -self.angle.bp_kappa_cmd if angle_mode_engaged else 0.0
|
||||
return fordcan_ext.create_lka_msg(
|
||||
self.packer, self.CAN, CC.latActive, hud_control, angle_mode_engaged, shadow_curvature
|
||||
)
|
||||
+2
-2
@@ -7,8 +7,8 @@ each mode DOES with the signal differs: curvature-primary (``lateral_curv_ext``)
|
||||
(reset_steering + post-reset ramp); angle-primary (``lateral_angle_ext``) forces lateral inactive
|
||||
(mode 0 on the wire) and ramps path_angle back in from zero through its soft ROC on release.
|
||||
"""
|
||||
from opendbc.car import DT_CTRL
|
||||
from opendbc.car.ford.values import CarControllerParams
|
||||
from ...car import DT_CTRL
|
||||
from ...car.ford.values import CarControllerParams
|
||||
|
||||
# Require sustained hands-on AND a large angle (avoids resetting on small wheel nudges in a curve).
|
||||
HUMAN_TURN_ANGLE_DEG = 45.0
|
||||
+12
-5
@@ -14,10 +14,10 @@ for a lane-centering nudge, and it bypasses every limiter angle mode already app
|
||||
|
||||
This class instead computes a small correction in the **curvature domain**, meant to be added to
|
||||
``kappa_cmd`` before any of those limiters run -- so the correction automatically inherits all of
|
||||
them, with no new safety-relevant code path. This mirrors where a GM-focused fork (StarPilot,
|
||||
u/jc01rho -- ``selfdrive/controls/lib/drive_helpers.py::LaneCenteringController`` on
|
||||
``feat/lane-centering-camera-offset``) places their own lane-centering trim: on the universal
|
||||
``desired_curvature`` signal, upstream of any car-specific actuator conversion. The geometry here
|
||||
them, with no new safety-relevant code path. The core is ported from StarPilot's
|
||||
``selfdrive/controls/lib/lane_centering.py`` (``LaneCenteringController``, by jc01rho; see
|
||||
CREDITS.md), which applies its trim the same way: on the universal ``desired_curvature`` signal,
|
||||
upstream of any car-specific actuator conversion. The geometry here
|
||||
(``raw_correction = 2*error / lookahead**2``) is the same ``y ~= 1/2 * kappa * x**2`` relation
|
||||
BluePilot already uses for PSCM d_ref / path_angle elsewhere in this package, solved for the
|
||||
curvature that would eliminate a lateral position error at the lookahead distance.
|
||||
@@ -184,6 +184,12 @@ class LaneCenterTrim:
|
||||
return 0.0, 0.0
|
||||
if not (np.all(np.diff(left_x) > 0) and np.all(np.diff(right_x) > 0)):
|
||||
return 0.0, 0.0
|
||||
if not all(np.isfinite(v) for v in (probs[1], probs[2], stds[1], stds[2])):
|
||||
# Unlike the arrays above, a NaN here doesn't raise -- it flows straight into min()/
|
||||
# np.interp() and poisons confidence (and therefore the correction) with a NaN that
|
||||
# survives every subsequent frame until a hard reset, instead of erroring or getting
|
||||
# clipped like everything else in this function.
|
||||
return 0.0, 0.0
|
||||
|
||||
left = float(np.interp(lookahead, left_x, left_y))
|
||||
right = float(np.interp(lookahead, right_x, right_y))
|
||||
@@ -194,7 +200,8 @@ class LaneCenterTrim:
|
||||
# probability via min() -- a single missing/unreliable line (e.g. no line on the curb
|
||||
# side, only a center stripe) drags confidence toward 0 on its own.
|
||||
width_tolerance = float(np.interp(width, _WIDTH_TOLERANCE_BP, _WIDTH_TOLERANCE_V))
|
||||
std_tolerance = float(np.interp(max(float(stds[1]), float(stds[2])), _STD_TOLERANCE_BP, _STD_TOLERANCE_V)) #StarPilot stopped at 0.3, this fades the std through the table
|
||||
# StarPilot stopped at 0.3, this fades the std through the table
|
||||
std_tolerance = float(np.interp(max(float(stds[1]), float(stds[2])), _STD_TOLERANCE_BP, _STD_TOLERANCE_V))
|
||||
confidence = min(float(probs[1]), float(probs[2]), width_tolerance, std_tolerance) #confidence is the weakest signal
|
||||
scale = float(np.clip(np.interp(confidence, _CONFIDENCE_BP, _CONFIDENCE_V), 0.0, 1.0))
|
||||
center_y = 0.5 * (left + right)
|
||||
+138
-146
@@ -2,55 +2,49 @@
|
||||
BluePilot: Ford CAN-FD path-angle–primary lateral control (developer).
|
||||
|
||||
Steering intent is c1 from κ → θ. Converts planner/model curvature into ``path_angle`` using
|
||||
PSCM short lookahead d_ref and y ≈ ½κ x² ⇒ path_angle = ½ κ d_ref (see
|
||||
``bluepilot/agent_info/20_FORD_PSCM_KNOWLEDGE_PACK.md``). Predicted curvature (modelV2) is
|
||||
blended with ``actuators.curvature`` per ``FordPathAngleBlendRatio`` (0 = planner only,
|
||||
PSCM short lookahead d_ref and y ≈ ½κ x² ⇒ path_angle = ½ κ d_ref. Predicted curvature
|
||||
(modelV2) is blended with ``actuators.curvature`` by ``path_angle_blend_ratio`` (0 = planner only,
|
||||
1 = model only).
|
||||
|
||||
**c0 (path_offset) is always zero on the wire, unconditionally.** Angle mode has no centering
|
||||
trim -- an earlier port attempt piped a small additive trim onto path_angle through the
|
||||
curv-mode ``LC_PID_controller``, but it never actually tracked lane center correctly in this
|
||||
mode and was removed; only the DBC-required zero c0 remains.
|
||||
**c0 (path_offset) is always zero on the wire, unconditionally.** An earlier port attempt piped
|
||||
a small additive trim onto path_angle through the curv-mode ``LC_PID_controller``, but it never
|
||||
actually tracked lane center correctly in this mode: path_angle here is a derived quantity
|
||||
(``kappa_cmd * v_ego * curvature_factor``), so an additive trim in that domain has the wrong
|
||||
(inverted) speed-dependence for a lane-centering nudge, and it bypassed every limiter this file
|
||||
applies to ``kappa_cmd``. That attempt was removed; only the DBC-required zero c0 remains.
|
||||
|
||||
**Lane centering trim (``lane_center_trim.py``)** replaces it: a small correction applied to
|
||||
``kappa_cmd`` itself (see ``LaneCenterTrim``), before the deviation clip / gain table / PSCM
|
||||
clamp / soft ROC below -- so it inherits every one of those limiters automatically instead of
|
||||
bypassing them. Blends toward lane-line center by confidence (same formula as
|
||||
``lateral_curv_ext``'s ``path_offset``) and falls back to the model's own predicted path -- not
|
||||
to zero -- when lines are missing/unreliable, so the user's left/right offset still applies on
|
||||
center-stripe-only roads. Disabled during lane changes, user-tunable (enable, offset, authority)
|
||||
via ``enable_lane_positioning_ang`` / ``custom_path_offset_ang`` / ``lane_centering_strength_ang``.
|
||||
|
||||
**Human-turn override**: while the driver manually turns (same sustained-press + angle criteria
|
||||
as ``lateral_curv_ext``, via the shared ``HumanTurnDetector``), lateral is forced inactive (mode
|
||||
0, all-zero signals) instead of winding path_angle into a stale command the PSCM has to reconcile
|
||||
on release -- on the Mach-E's PSCM that reconciliation cost 2-3 s of dead time before control
|
||||
resumed. Mode 0 is panda-clean by construction: every ford.h check has a legitimate
|
||||
!steer_control_enabled branch, so no reset-bypass latch involvement. On release, path_angle ramps
|
||||
back in from zero through the soft ROC below (no jump seed) -- generous at human-turn speeds, and
|
||||
admitted by ford.h's path_angle ROC check (2% looser) without any bypass.
|
||||
!steer_control_enabled branch. On release, path_angle ramps back in from zero through the soft
|
||||
ROC below (no jump seed) -- generous at human-turn speeds, and admitted by ford.h's path_angle
|
||||
ROC check (2% looser).
|
||||
"""
|
||||
import numpy as np
|
||||
from numpy import clip, interp
|
||||
|
||||
from opendbc.car import DT_CTRL
|
||||
from opendbc.car.lateral import apply_std_steer_angle_limits
|
||||
from opendbc.car.ford.values import CAR, CarControllerParams
|
||||
from opendbc.sunnypilot.car.ford.lateral_curv_ext import LateralResult
|
||||
from opendbc.sunnypilot.car.ford.human_turn import HumanTurnDetector
|
||||
from opendbc.sunnypilot.car.ford.values_ext import BP_ANGLE_LIMITS
|
||||
from selfdrive.modeld.constants import ModelConstants
|
||||
|
||||
# Hard-coded per-platform gain defaults.
|
||||
# CAN vehicles (Escape MK4, Bronco Sport, Explorer, Maverick, Edge)
|
||||
_GAIN_CAN = (1.00, 1.15)
|
||||
# CAN-FD body-on-frame trucks (F-150, Lightning, Expedition, Ranger)
|
||||
_GAIN_CANFD_BOF = (0.95, 0.95)
|
||||
# CAN-FD unibody SUVs (Mustang Mach-E, Escape MK4.5)
|
||||
_GAIN_CANFD_SUV = (1.00, 1.05)
|
||||
|
||||
_CANFD_BOF_CARS = frozenset({
|
||||
CAR.FORD_F_150_MK14,
|
||||
CAR.FORD_F_150_LIGHTNING_MK1,
|
||||
CAR.FORD_EXPEDITION_MK4,
|
||||
CAR.FORD_RANGER_MK2,
|
||||
})
|
||||
_CANFD_SUV_CARS = frozenset({
|
||||
CAR.FORD_MUSTANG_MACH_E_MK1,
|
||||
CAR.FORD_ESCAPE_MK4_5,
|
||||
})
|
||||
|
||||
from ...car import DT_CTRL
|
||||
from ...car.lateral import apply_std_steer_angle_limits
|
||||
from ...car.ford.values import CarControllerParams
|
||||
from .lateral_curv_ext import LateralResult
|
||||
from .lane_center_trim import LaneCenterTrim
|
||||
from .values_ext import (
|
||||
ANGLE_GAIN_CAN, ANGLE_GAIN_CANFD_BOF, ANGLE_GAIN_CANFD_BOF_CARS, ANGLE_GAIN_CANFD_SUV, ANGLE_GAIN_CANFD_SUV_CARS,
|
||||
BP_ANGLE_LIMITS, LANE_CHANGE_FACTOR_BP, LANE_CHANGE_FACTOR_LOW,
|
||||
)
|
||||
from .values_ext import MODEL_T_IDXS
|
||||
from ..params.manifest import default, value
|
||||
|
||||
# DBC ``LatCtlPath_An_Actl`` (rad) — panda safety uses the same in ``ford.h``; PSCM enforces in firmware.
|
||||
FORD_DBC_PATH_ANGLE_MIN = -0.5
|
||||
@@ -125,49 +119,62 @@ def pscm_d_ref_m(v_ego_ms: float) -> float:
|
||||
return d
|
||||
|
||||
|
||||
# the settings angle mode reads (params/manifest.py): (attribute, key)
|
||||
ANGLE_SETTINGS = (
|
||||
("low_speed_curv_factor", "FordLowSpeedFactor_ang"),
|
||||
("high_speed_curv_factor", "FordHighSpeedFactor_ang"),
|
||||
("user_dampening_factor", "FordHighSpeedDampening_ang"),
|
||||
("lane_change_factor_high_ang", "lane_change_factor_high_ang"),
|
||||
("enable_lane_positioning_ang", "enable_lane_positioning_ang"), # the lane centering trim (lane_center_trim.py)
|
||||
("custom_path_offset_ang", "custom_path_offset_ang"),
|
||||
("lane_centering_strength_ang", "lane_centering_strength_ang"),
|
||||
)
|
||||
|
||||
|
||||
class LateralAngleExt:
|
||||
def __init__(self, CP=None, CP_SP=None):
|
||||
# Predicted-curvature blend for path_angle: pred * b + desired * (1-b); b from ``FordPathAngleBlendRatio``
|
||||
def __init__(self, CP, inputs, shared):
|
||||
self.CP = CP
|
||||
# the user settings (updated from the host's params each frame), at their defaults until then
|
||||
for attr, key in ANGLE_SETTINGS:
|
||||
setattr(self, attr, default(key))
|
||||
# BluePilotLateral's LateralInputs (read) and SharedLateralState (read and written), common to both strategies
|
||||
self.inputs = inputs
|
||||
self.shared = shared
|
||||
# Predicted-curvature blend for path_angle: pred * b + desired * (1-b); b is this fixed default
|
||||
self.path_angle_blend_ratio = _FORD_PATH_ANGLE_BLEND_RATIO_DEFAULT
|
||||
# Max extra VLT above t_base; from ``FordVLTExtraMax`` param
|
||||
# Max extra VLT above t_base (fixed)
|
||||
self.vlt_extra_max = _VLT_T_EXTRA_MAX
|
||||
# Telemetry: final path_angle (rad) after limits (see bp_card_publisher)
|
||||
# Telemetry: final path_angle (rad) after limits (for the BluePilot UI's bp_card_publisher)
|
||||
self.bp_path_angle_final = 0.0
|
||||
# High-speed gain factors: set per-platform via carFingerprint in update_angle_params.
|
||||
self.path_angle_gain_lowC_highV = 1.0 # dampening at high speed, low curvature
|
||||
self.path_angle_gain_highC_highV = 1.0 # gain at high speed, high curvature
|
||||
self.bp_path_angle_gain_lowC_highV = 1.0
|
||||
self.bp_path_angle_gain_highC_highV = 1.0
|
||||
# User-tunable "feel" multipliers: read from the angle-tuning Params below.
|
||||
self.low_speed_curv_factor = 1.0
|
||||
self.high_speed_curv_factor = 1.0
|
||||
self.user_dampening_factor = 1.0
|
||||
# Telemetry copies of the user "feel" multipliers (ANGLE_SETTINGS)
|
||||
self.bp_low_speed_curv_factor = 1.0
|
||||
self.bp_high_speed_curv_factor = 1.0
|
||||
# BluePilot: angle mode's own lane-change scaling factor, independent of curvature mode's
|
||||
# lane_change_factor_high_curv -- angle needs a boost (>1) where curvature needs a cut (<1).
|
||||
self.lane_change_factor_high_ang = 1.0
|
||||
# BluePilot: angle-mode lane centering trim (advanced lane positioning) -- see
|
||||
# lane_center_trim.py and the module docstring above.
|
||||
self.lane_center_trim = LaneCenterTrim()
|
||||
# Telemetry: variable curvature lookup time used this frame (s)
|
||||
self.bp_curvature_lookup_time = _VLT_T_EXTRA_MAX + 0.3725 # warm start at ~0.5s
|
||||
# BluePilot: error-clipped kappa path_angle was derived from -- carcontroller.py reads this as
|
||||
# BluePilot: error-clipped kappa path_angle was derived from -- carcontroller_ext.py reads this as
|
||||
# shadow_curvature for ford.h's angle-mode deviation check. Actively consumed, not telemetry.
|
||||
self.bp_kappa_cmd = 0.0
|
||||
# BluePilot: rate-limit diagnostics (controllerStateBP)
|
||||
# BluePilot: rate-limit diagnostics (the BluePilot UI's controllerStateBP)
|
||||
self.bp_angle_rate_limited = False # path_angle soft-ROC clip actually bit this frame
|
||||
self.bp_curvature_rate_limited = False # equivalent curvature would be rate-limited by curv-mode logic (sim)
|
||||
self.bp_curvature_deviation_limited = False # current_curvature error-clip constrained kappa_cmd this frame
|
||||
self.sim_curvature_last = 0.0 # shadow curvature-mode last for the curvatureRateLimited sim
|
||||
# Exit detection: track previous desired curvature to sense when planner is actively reducing
|
||||
self._desired_curvature_last = 0.0
|
||||
# Human-turn override: while the driver manually turns, lateral is forced inactive (mode 0,
|
||||
# all-zero signals) instead of winding path_angle into a stale command the PSCM can't cleanly
|
||||
# reconcile on release (2-3 s re-engage dead time observed on Mach-E). See module docstring.
|
||||
# Note: in CarController this attribute is shared with LateralCurvExt (same mixin instance) --
|
||||
# only one lateral strategy runs per frame, so a single detector serves both.
|
||||
self.human_turn_detector = HumanTurnDetector()
|
||||
self.angle_human_turn_active = False # read by carcontroller to force mode 0
|
||||
# The detector itself is in BluePilotLateral's shared state: one detector serves both modes.
|
||||
self.angle_human_turn_active = False # read by BluePilotLateral.step_lateral to force mode 0
|
||||
# Post-override stall blip state (see module constants). angle_stall_blip_active is read by
|
||||
# carcontroller to force mode 0, exactly like angle_human_turn_active.
|
||||
# BluePilotLateral.step_lateral to force mode 0, exactly like angle_human_turn_active.
|
||||
self.stall_blip_hold_s = 0.0 # accumulated deviation-clip-binding time toward a pulse
|
||||
self.stall_blip_frames_left = 0 # remaining pulse frames; > 0 -> mode 0 on the wire
|
||||
self.stall_blip_cooldown_s = 0.0 # re-arm delay after a pulse
|
||||
@@ -177,47 +184,28 @@ class LateralAngleExt:
|
||||
|
||||
def update_angle_params(self, params):
|
||||
"""Sets per-platform gain defaults and reads user angle-tuning params."""
|
||||
self._ensure_lateral_curv_initialized(self.CP)
|
||||
fp = getattr(self.CP, 'carFingerprint', '')
|
||||
if fp in _CANFD_BOF_CARS:
|
||||
low, high = _GAIN_CANFD_BOF
|
||||
elif fp in _CANFD_SUV_CARS:
|
||||
low, high = _GAIN_CANFD_SUV
|
||||
if fp in ANGLE_GAIN_CANFD_BOF_CARS:
|
||||
low, high = ANGLE_GAIN_CANFD_BOF
|
||||
elif fp in ANGLE_GAIN_CANFD_SUV_CARS:
|
||||
low, high = ANGLE_GAIN_CANFD_SUV
|
||||
else:
|
||||
low, high = _GAIN_CAN
|
||||
low, high = ANGLE_GAIN_CAN
|
||||
self.path_angle_gain_lowC_highV = low
|
||||
self.path_angle_gain_highC_highV = high
|
||||
if params is not None and hasattr(params, "get"):
|
||||
for attr, key, min_value, max_value in (
|
||||
("low_speed_curv_factor", "FordLowSpeedFactor_ang", 0.5, 1.5),
|
||||
("high_speed_curv_factor", "FordHighSpeedFactor_ang", 0.5, 1.5),
|
||||
("user_dampening_factor", "FordHighSpeedDampening_ang", 0.25, 1.25),
|
||||
):
|
||||
try:
|
||||
raw = params.get(key, return_default=True)
|
||||
if raw is not None and raw != b"":
|
||||
setattr(self, attr, float(clip(
|
||||
float(raw.decode("utf-8", errors="replace") if isinstance(raw, bytes) else raw), min_value, max_value)))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
raw = params.get("lane_change_factor_high_ang", return_default=True)
|
||||
if raw is not None and raw != b"":
|
||||
self.lane_change_factor_high_ang = float(clip(
|
||||
float(raw.decode("utf-8", errors="replace") if isinstance(raw, bytes) else raw), 0.85, 1.50))
|
||||
except Exception:
|
||||
pass
|
||||
if params is not None:
|
||||
for attr, key in ANGLE_SETTINGS:
|
||||
setattr(self, attr, value(params, key))
|
||||
|
||||
def update_angle_strategy(self, CC, CS, actuators, CP):
|
||||
def update_angle_strategy(self, CC, CS, actuators):
|
||||
"""
|
||||
Curvature from planner (+ optional predicted blend) → path_angle via ½·κ·d_ref.
|
||||
c0 (path_offset) is always zero on the wire -- no centering trim in angle mode. c2 and c3 are zero.
|
||||
Curvature from planner (+ optional predicted blend, + lane centering trim) → path_angle via
|
||||
½·κ·d_ref. c0 (path_offset) is always zero on the wire; the lane centering trim lives entirely
|
||||
in the curvature domain (kappa_cmd), not on c0. c2 and c3 are zero.
|
||||
Blended κ is not passed through Ford c2 rate / DBC limits (those target the curvature actuator).
|
||||
"""
|
||||
self._ensure_lateral_curv_initialized(CP)
|
||||
|
||||
v_ego = float(CS.out.vEgoRaw)
|
||||
d_ref = pscm_d_ref_m(v_ego)
|
||||
d_ref = pscm_d_ref_m(v_ego) # noqa: F841 # BluePilot: unused today; kept verbatim from bp-dev
|
||||
|
||||
curvature_rate = 0.0
|
||||
path_offset = 0.0
|
||||
@@ -227,12 +215,11 @@ class LateralAngleExt:
|
||||
precision = 1
|
||||
|
||||
if not CC.latActive:
|
||||
self.path_angle_last = 0.0
|
||||
self.shared.path_angle_last = 0.0
|
||||
self.bp_path_angle_final = 0.0
|
||||
self.apply_curvature_last = 0.0
|
||||
self.bp_angle_rate_limited = False
|
||||
self.bp_curvature_rate_limited = False
|
||||
self.bp_curvature_deviation_limited = False
|
||||
self.shared.bp_curvature_deviation_limited = False
|
||||
self.sim_curvature_last = 0.0
|
||||
# Publish the shadow curvature from the measured curvature while inactive. LKA keeps
|
||||
# carrying angle_mode_engaged whenever angle mode is configured (independent of
|
||||
@@ -242,16 +229,17 @@ class LateralAngleExt:
|
||||
# ford.h's deviation check compares a zero shadow against real measured curvature.
|
||||
# (ford.h skips the check while steer_control_enabled is 0, so the value is free to
|
||||
# follow the measurement during the inactive period itself.)
|
||||
self.bp_kappa_cmd = self.get_current_curvature(CS)
|
||||
self.human_turn_detector.reset()
|
||||
self.bp_kappa_cmd = self.inputs.get_current_curvature(CS)
|
||||
self.shared.human_turn_detector.reset()
|
||||
self.angle_human_turn_active = False
|
||||
self.lane_center_trim.reset()
|
||||
self.stall_blip_hold_s = 0.0
|
||||
self.stall_blip_frames_left = 0
|
||||
self.stall_blip_cooldown_s = 0.0
|
||||
self.stall_blip_count = 0
|
||||
self.angle_stall_blip_active = False
|
||||
self.press_timer_s = 0.0
|
||||
self.precision_type = 1
|
||||
self.shared.precision_type = 1
|
||||
return LateralResult(
|
||||
apply_curvature=0.0,
|
||||
curvature_rate=0.0,
|
||||
@@ -263,28 +251,28 @@ class LateralAngleExt:
|
||||
)
|
||||
|
||||
# Human-turn override: sustained driver press + large wheel angle → force lateral inactive
|
||||
# (carcontroller drops mode to 0; all signals are zero on the wire) so path_angle can't wind
|
||||
# (BluePilotLateral.step_lateral drops mode to 0; all signals are zero on the wire) so path_angle can't wind
|
||||
# into a stale command while the driver turns. Always on in angle mode (no param gate) -- the
|
||||
# curv-suffixed human-turn toggle belongs to curvature mode's reset strategy, and the Mach-E
|
||||
# PSCM re-engage stall this prevents is not something a user should be able to opt out of.
|
||||
# On release, no jump seed: path_angle_last is 0, so the normal flow below ramps the command
|
||||
# back in through the soft ROC -- generous at human-turn speeds, no panda bypass involved.
|
||||
self.angle_human_turn_active = self.human_turn_detector.update(
|
||||
# back in through the soft ROC -- generous at human-turn speeds.
|
||||
self.angle_human_turn_active = self.shared.human_turn_detector.update(
|
||||
True, CS.out.steeringPressed, CS.out.steeringAngleDeg)
|
||||
if self.angle_human_turn_active:
|
||||
self.path_angle_last = 0.0
|
||||
self.shared.path_angle_last = 0.0
|
||||
self.bp_path_angle_final = 0.0
|
||||
self.apply_curvature_last = 0.0
|
||||
self.bp_angle_rate_limited = False
|
||||
self.bp_curvature_rate_limited = False
|
||||
self.bp_curvature_deviation_limited = False
|
||||
self.shared.bp_curvature_deviation_limited = False
|
||||
self.sim_curvature_last = 0.0
|
||||
# Truthful shadow during the override (mirrors the inactive path -- see the comment
|
||||
# there): the driver is steering, so the honest command is the car's actual curvature,
|
||||
# and the panda-latched shadow stays current for the re-engage frame.
|
||||
self.bp_kappa_cmd = self.get_current_curvature(CS)
|
||||
self.bp_kappa_cmd = self.inputs.get_current_curvature(CS)
|
||||
# Keep exit detection current so resume doesn't compare against a stale pre-turn value.
|
||||
self._desired_curvature_last = float(actuators.curvature)
|
||||
self.lane_center_trim.reset()
|
||||
# A human turn ends any stall episode -- its own mode 0 does the PSCM reset job. That also
|
||||
# covers the press so far: only press time accumulated AFTER the latch releases should earn
|
||||
# a hand-off pulse.
|
||||
@@ -294,7 +282,7 @@ class LateralAngleExt:
|
||||
self.stall_blip_count = 0
|
||||
self.angle_stall_blip_active = False
|
||||
self.press_timer_s = 0.0
|
||||
self.precision_type = 1
|
||||
self.shared.precision_type = 1
|
||||
return LateralResult(
|
||||
apply_curvature=0.0,
|
||||
curvature_rate=0.0,
|
||||
@@ -314,7 +302,7 @@ class LateralAngleExt:
|
||||
else:
|
||||
if (self.press_timer_s >= _PRESS_BLIP_MIN_S and self.stall_blip_cooldown_s <= 0.0
|
||||
and self.stall_blip_frames_left <= 0
|
||||
and abs(self.path_angle_last) < _BLIP_MAX_PATH_ANGLE):
|
||||
and abs(self.shared.path_angle_last) < _BLIP_MAX_PATH_ANGLE):
|
||||
self.stall_blip_frames_left = _STALL_BLIP_FRAMES
|
||||
self.press_timer_s = 0.0
|
||||
|
||||
@@ -326,17 +314,17 @@ class LateralAngleExt:
|
||||
if self.stall_blip_frames_left > 0:
|
||||
self.stall_blip_frames_left -= 1
|
||||
self.angle_stall_blip_active = True
|
||||
self.path_angle_last = 0.0
|
||||
self.shared.path_angle_last = 0.0
|
||||
self.bp_path_angle_final = 0.0
|
||||
self.apply_curvature_last = 0.0
|
||||
self.bp_angle_rate_limited = False
|
||||
self.bp_curvature_rate_limited = False
|
||||
self.bp_curvature_deviation_limited = False
|
||||
self.shared.bp_curvature_deviation_limited = False
|
||||
self.sim_curvature_last = 0.0
|
||||
# Truthful shadow during the blip (see the inactive-path comment).
|
||||
self.bp_kappa_cmd = self.get_current_curvature(CS)
|
||||
self.bp_kappa_cmd = self.inputs.get_current_curvature(CS)
|
||||
self._desired_curvature_last = float(actuators.curvature)
|
||||
self.precision_type = 1
|
||||
self.lane_center_trim.reset()
|
||||
self.shared.precision_type = 1
|
||||
if self.stall_blip_frames_left <= 0:
|
||||
self.stall_blip_cooldown_s = _STALL_COOLDOWN_S
|
||||
return LateralResult(
|
||||
@@ -350,9 +338,9 @@ class LateralAngleExt:
|
||||
)
|
||||
self.angle_stall_blip_active = False
|
||||
|
||||
self.precision_type = 1
|
||||
self.shared.precision_type = 1
|
||||
precision = 1
|
||||
LP = self.lp
|
||||
LP = self.inputs.lp # noqa: F841 # BluePilot: unused today; kept verbatim from bp-dev
|
||||
desired_curvature = float(actuators.curvature)
|
||||
|
||||
# Variable lookup time: t_base tracks planner pre-compensation; extra tapers on high speed and large curves.
|
||||
@@ -361,14 +349,14 @@ class LateralAngleExt:
|
||||
# curvature, kappa_entering stays True, and the exit-biased blend is permanently disabled — causing the car
|
||||
# to command max path_angle through the entire apex. 0.15s gives t_base ≤ 0.20s and VLT ≤ 0.33s, restoring
|
||||
# the 2.8m lookahead that kept kappa_entering False at the apex in successful earlier runs.
|
||||
_t_base = float(clip(self.sm['liveDelay'].lateralDelay, 0.1, 0.15)) + _DT_MDL
|
||||
_t_base = float(clip(self.inputs.lateral_delay, 0.1, 0.15)) + _DT_MDL
|
||||
_speed_factor = float(interp(v_ego, [_VLT_V_LOW_MS, _VLT_V_HIGH_MS], [1.0, 0.0]))
|
||||
# Direction-aware kappa factor: on curve ENTRY (model shows more curvature at t_base than planner now),
|
||||
# keep full lookahead so pre-steering begins early. On exit/apex, taper by magnitude to prevent unwind.
|
||||
_kappa_at_t_base = 0.0
|
||||
if self.model is not None and len(self.model.orientationRate.z) >= 17:
|
||||
_curvatures_ref = np.array(self.model.orientationRate.z) / max(0.01, v_ego)
|
||||
_kappa_at_t_base = abs(float(interp(_t_base, ModelConstants.T_IDXS, _curvatures_ref)))
|
||||
if self.inputs.model is not None and len(self.inputs.model.orientationRate.z) >= 17:
|
||||
_curvatures_ref = np.array(self.inputs.model.orientationRate.z) / max(0.01, v_ego)
|
||||
_kappa_at_t_base = abs(float(interp(_t_base, MODEL_T_IDXS, _curvatures_ref)))
|
||||
_kappa_entering = _kappa_at_t_base > abs(desired_curvature)
|
||||
if _kappa_entering:
|
||||
_kappa_factor = 1.0 # curve deepening ahead: full extra lookahead for gradual entry
|
||||
@@ -378,10 +366,10 @@ class LateralAngleExt:
|
||||
self.bp_curvature_lookup_time = curvature_lookup_time
|
||||
|
||||
predicted_curvature = 0.0
|
||||
if self.model is not None and len(self.model.orientationRate.z) >= 17:
|
||||
curvatures = np.array(self.model.orientationRate.z) / max(0.01, v_ego)
|
||||
if self.inputs.model is not None and len(self.inputs.model.orientationRate.z) >= 17:
|
||||
curvatures = np.array(self.inputs.model.orientationRate.z) / max(0.01, v_ego)
|
||||
predicted_curvature = float(
|
||||
interp(curvature_lookup_time, ModelConstants.T_IDXS, curvatures)
|
||||
interp(curvature_lookup_time, MODEL_T_IDXS, curvatures)
|
||||
)
|
||||
|
||||
b = float(self.path_angle_blend_ratio)
|
||||
@@ -400,8 +388,8 @@ class LateralAngleExt:
|
||||
# That caused a positive-feedback flat-line: under-steer → CtrSat → path_angle frozen → more under-steer.
|
||||
# Use DBC-limit proximity instead: only block when path_angle is already near the ±0.5 rad CAN limits,
|
||||
# which is the only condition where the anti-snap unwind rate cap makes physical sense.
|
||||
_dbc_sat = (self.path_angle_last >= FORD_DBC_PATH_ANGLE_MAX * 0.90 or
|
||||
self.path_angle_last <= FORD_DBC_PATH_ANGLE_MIN * 0.90)
|
||||
_dbc_sat = (self.shared.path_angle_last >= FORD_DBC_PATH_ANGLE_MAX * 0.90 or
|
||||
self.shared.path_angle_last <= FORD_DBC_PATH_ANGLE_MIN * 0.90)
|
||||
_in_hard_sat = _pscm_lim >= 2 or _dbc_sat
|
||||
# BluePilot: per-call delta threshold. The original 0.002 was authored 2026-05-07 on
|
||||
# bp-sid-simple (9c3d000fd), which ran STEER_STEP=1 (true 100Hz, switched 2026-04-22) -- so it
|
||||
@@ -415,26 +403,36 @@ class LateralAngleExt:
|
||||
requested_curvature = predicted_curvature * b_blend + desired_curvature * (1.0 - b_blend)
|
||||
self._desired_curvature_last = desired_curvature
|
||||
|
||||
if self.model is not None:
|
||||
self.lane_change = self.model.meta.laneChangeState in (1, 2, 3)
|
||||
if self.inputs.model is not None:
|
||||
self.lane_change = self.inputs.model.meta.laneChangeState in (1, 2, 3)
|
||||
else:
|
||||
self.lane_change = False
|
||||
|
||||
lane_change_factor = interp(
|
||||
v_ego, self.lane_change_factor_bp, [self.lane_change_factor_low, self.lane_change_factor_high_ang]
|
||||
v_ego, LANE_CHANGE_FACTOR_BP, [LANE_CHANGE_FACTOR_LOW, self.lane_change_factor_high_ang]
|
||||
)
|
||||
if self.lane_change and self.model is not None:
|
||||
if self.model.meta.laneChangeDirection == 1 and requested_curvature < 0:
|
||||
if self.lane_change and self.inputs.model is not None:
|
||||
if self.inputs.model.meta.laneChangeDirection == 1 and requested_curvature < 0:
|
||||
requested_curvature *= lane_change_factor
|
||||
precision = 0
|
||||
elif self.model.meta.laneChangeDirection == 2 and requested_curvature > 0:
|
||||
elif self.inputs.model.meta.laneChangeDirection == 2 and requested_curvature > 0:
|
||||
requested_curvature *= lane_change_factor
|
||||
precision = 0
|
||||
self.precision_type = precision
|
||||
self.shared.precision_type = precision
|
||||
|
||||
# Use planner / predicted κ directly for the κ → path_angle map; we are not sending κ on CAN.
|
||||
kappa_cmd = float(requested_curvature)
|
||||
|
||||
# BluePilot: lane centering trim (advanced lane positioning) -- nudges kappa_cmd toward true
|
||||
# lane-line center + user offset, gated on lane-line confidence and disabled during lane
|
||||
# changes (see lane_center_trim.py). Applied here, before the deviation clip below, so the
|
||||
# trimmed value inherits every limiter this file already applies to kappa_cmd instead of
|
||||
# bypassing them.
|
||||
kappa_cmd = self.lane_center_trim.update(
|
||||
kappa_cmd, self.inputs.model, v_ego, self.enable_lane_positioning_ang,
|
||||
self.custom_path_offset_ang, self.lane_centering_strength_ang,
|
||||
CC.latActive, self.lane_change)
|
||||
|
||||
# BluePilot: clip kappa_cmd to current_curvature (measured, from yaw rate) +- CURVATURE_ERROR,
|
||||
# mirroring lateral_curv_ext.py's apply_ford_curvature_limits_ext exactly (same formula, same
|
||||
# v_ego > 9 gate, same CarControllerParams.CURVATURE_ERROR tolerance). Without this, kappa_cmd
|
||||
@@ -444,20 +442,18 @@ class LateralAngleExt:
|
||||
# routinely, not just on genuine pothole/override divergence. Curvature mode has always clipped
|
||||
# here; this brings angle mode's actual steering intent in line with that proven behavior rather
|
||||
# than only clipping the value reported to panda (which would make the check a no-op).
|
||||
current_curvature = self.get_current_curvature(CS)
|
||||
self.bp_curvature_deviation_limited = False
|
||||
current_curvature = self.inputs.get_current_curvature(CS)
|
||||
self.shared.bp_curvature_deviation_limited = False
|
||||
if v_ego > 9:
|
||||
_kappa_cmd_pre_error_clip = kappa_cmd
|
||||
kappa_cmd = float(clip(kappa_cmd, current_curvature - CarControllerParams.CURVATURE_ERROR,
|
||||
current_curvature + CarControllerParams.CURVATURE_ERROR))
|
||||
# BluePilot: did this clip actually constrain kappa_cmd this frame (deviation from measured,
|
||||
# not rate-of-change -- see carcontroller.py)?
|
||||
self.bp_curvature_deviation_limited = bool(abs(kappa_cmd - _kappa_cmd_pre_error_clip) > 1e-9)
|
||||
# not rate-of-change)? The BluePilot UI's deviation flag.
|
||||
self.shared.bp_curvature_deviation_limited = bool(abs(kappa_cmd - _kappa_cmd_pre_error_clip) > 1e-9)
|
||||
|
||||
lateral_uncertainty = 0.0 # no curvature-limit ladder until angle-mode torque display is defined
|
||||
|
||||
|
||||
|
||||
# Speed-interpolated gain: at low speed both curves use 1.0; at high speed the params take effect.
|
||||
self.low_gain_calc = interp(
|
||||
v_ego, [13.5, 26.82], [1.0, (self.path_angle_gain_lowC_highV * self.user_dampening_factor)]
|
||||
@@ -470,7 +466,6 @@ class LateralAngleExt:
|
||||
path_angle_calc = kappa_cmd * v_ego * self.curvature_factor
|
||||
path_angle = path_angle_calc
|
||||
|
||||
|
||||
# PSCM authority limit clamp.
|
||||
# On CANFD Fords in angle mode, LatCtlLim_D_Stat does not fire, so _pscm_lim stays 0.
|
||||
# _in_hard_sat (computed above) combines _pscm_lim >= 2 with _dbc_sat (path_angle near ±0.5 rad limit).
|
||||
@@ -479,7 +474,7 @@ class LateralAngleExt:
|
||||
# Without the decrease cap, model+planner drop path_angle at ~0.36 rad/s at a sharp apex,
|
||||
# driving desired steering 30°+ ahead of actual while the PSCM is pinned, causing a snap when released.
|
||||
if _in_hard_sat:
|
||||
_last = self.path_angle_last
|
||||
_last = self.shared.path_angle_last
|
||||
_last_mag = abs(_last)
|
||||
_curr_mag = abs(path_angle)
|
||||
if _curr_mag > _last_mag: # magnitude growing — block
|
||||
@@ -488,7 +483,7 @@ class LateralAngleExt:
|
||||
_limited_mag = _last_mag - _PSCM_SAT_UNWIND_RATE
|
||||
path_angle = float(_limited_mag if _last >= 0 else -_limited_mag)
|
||||
elif _pscm_lim >= 1: # LimitClose (F150/non-angle-mode only): block increases only
|
||||
path_angle = float(clip(path_angle, -abs(self.path_angle_last), abs(self.path_angle_last)))
|
||||
path_angle = float(clip(path_angle, -abs(self.shared.path_angle_last), abs(self.shared.path_angle_last)))
|
||||
|
||||
path_angle = min(FORD_DBC_PATH_ANGLE_MAX, max(FORD_DBC_PATH_ANGLE_MIN, path_angle))
|
||||
|
||||
@@ -504,12 +499,11 @@ class LateralAngleExt:
|
||||
_soft_roc = float(interp(v_ego, [9., 10., 15., 25.], [0.055, 0.055, 0.0425, 0.009]))
|
||||
_path_angle_pre_roc = path_angle
|
||||
path_angle = float(clip(path_angle,
|
||||
self.path_angle_last - _soft_roc,
|
||||
self.path_angle_last + _soft_roc))
|
||||
self.shared.path_angle_last - _soft_roc,
|
||||
self.shared.path_angle_last + _soft_roc))
|
||||
# BluePilot: did the soft ROC clip actually limit the path_angle we wanted to send this frame?
|
||||
self.bp_angle_rate_limited = bool(abs(path_angle - _path_angle_pre_roc) > 1e-9)
|
||||
|
||||
|
||||
# c0 always zero -- no centering trim in angle mode.
|
||||
path_offset = 0.0
|
||||
|
||||
@@ -518,10 +512,9 @@ class LateralAngleExt:
|
||||
self.bp_path_angle_gain_highC_highV = self.path_angle_gain_highC_highV
|
||||
self.bp_low_speed_curv_factor = self.low_speed_curv_factor
|
||||
self.bp_high_speed_curv_factor = self.high_speed_curv_factor
|
||||
self.path_angle_last = path_angle
|
||||
self.shared.path_angle_last = path_angle
|
||||
self.bp_path_angle_final = path_angle
|
||||
self.apply_curvature_last = 0.0
|
||||
# BluePilot: the error-clipped kappa path_angle was derived from -- carcontroller.py reads this
|
||||
# BluePilot: the error-clipped kappa path_angle was derived from -- carcontroller_ext.py reads this
|
||||
# as shadow_curvature for ford.h's angle-mode deviation check (see fordcan_ext.create_lka_msg).
|
||||
# Not just telemetry: an actively-consumed value, unlike the removed *_kappa_cmd_raw stubs.
|
||||
# While the driver is pressing (before the human-turn override latches), the clipped planner
|
||||
@@ -530,7 +523,7 @@ class LateralAngleExt:
|
||||
# in-drive lateral safety block observed across ~3h of replayed road-test routes was exactly
|
||||
# this (driver fighting a sustained curve with the mode still enabled). The honest command
|
||||
# during a press is the driver's actual curvature.
|
||||
self.bp_kappa_cmd = self.get_current_curvature(CS) if CS.out.steeringPressed else kappa_cmd
|
||||
self.bp_kappa_cmd = self.inputs.get_current_curvature(CS) if CS.out.steeringPressed else kappa_cmd
|
||||
|
||||
# BluePilot: would the equivalent curvature (kappa_cmd) have been rate-limited by curvature-mode's
|
||||
# ROC (apply_std_steer_angle_limits)? kappa_cmd is already error-clipped above (same clip
|
||||
@@ -551,10 +544,10 @@ class LateralAngleExt:
|
||||
and abs(_stall_gap) > _STALL_GAP_MIN
|
||||
and abs(desired_curvature) > abs(current_curvature))
|
||||
if _stalled:
|
||||
if self.bp_curvature_deviation_limited and self.stall_blip_cooldown_s <= 0.0:
|
||||
if self.shared.bp_curvature_deviation_limited and self.stall_blip_cooldown_s <= 0.0:
|
||||
self.stall_blip_hold_s += _STEER_DT
|
||||
if (self.stall_blip_hold_s >= _STALL_HOLD_S and self.stall_blip_count < _STALL_MAX_BLIPS
|
||||
and abs(self.path_angle_last) < _BLIP_MAX_PATH_ANGLE):
|
||||
and abs(self.shared.path_angle_last) < _BLIP_MAX_PATH_ANGLE):
|
||||
self.stall_blip_frames_left = _STALL_BLIP_FRAMES
|
||||
self.stall_blip_hold_s = 0.0
|
||||
self.stall_blip_count += 1
|
||||
@@ -565,13 +558,12 @@ class LateralAngleExt:
|
||||
|
||||
ramp_type = 2
|
||||
|
||||
|
||||
return LateralResult(
|
||||
apply_curvature=0.0,
|
||||
curvature_rate=curvature_rate,
|
||||
path_offset=path_offset,
|
||||
path_angle=path_angle,
|
||||
ramp_type=ramp_type,
|
||||
precision_type=self.precision_type,
|
||||
precision_type=self.shared.precision_type,
|
||||
lateralUncertainty=lateral_uncertainty,
|
||||
)
|
||||
@@ -0,0 +1,448 @@
|
||||
"""
|
||||
BluePilot Ford lateral curvature extension.
|
||||
|
||||
Implements Ford's 4-signal lateral control (curvature, curvature_rate, path_offset, path_angle)
|
||||
using predicted curvature from modelV2, PID-based lane centering, and laneline-aware path offset.
|
||||
Held by BluePilotLateral (carcontroller_ext.py) as LateralCurvExt.
|
||||
|
||||
Ford uses four signals to control steering:
|
||||
- curvature: primary steering command (also used in upstream, limited to 0.02 m^-1)
|
||||
- curvature_rate: always 0, like upstream and stock Ford
|
||||
- path_offset: lateral offset for lane centering (blends model + laneline data)
|
||||
- path_angle: heading angle correction via PID controller
|
||||
|
||||
A detailed explanation of Ford's control protocol:
|
||||
https://www.f150gen14.com/forum/threads/introducing-bluepilot-a-ford-specific-fork-for-comma3x-openpilot.24241/#post-457706
|
||||
"""
|
||||
|
||||
from collections import namedtuple, deque
|
||||
from enum import IntEnum
|
||||
|
||||
import numpy as np
|
||||
from numpy import clip, interp
|
||||
|
||||
from ...car.common.pid import PIDController
|
||||
from ...car import ACCELERATION_DUE_TO_GRAVITY
|
||||
from ...car.lateral import ISO_LATERAL_ACCEL, apply_std_steer_angle_limits
|
||||
from ...car.ford.values import CarControllerParams, FordFlags
|
||||
from .values_ext import BP_ANGLE_LIMITS, CURVATURE_MAX, LANE_CHANGE_FACTOR_BP, LANE_CHANGE_FACTOR_LOW
|
||||
from .values_ext import MODEL_T_IDXS
|
||||
from ..params.manifest import default, value
|
||||
|
||||
|
||||
# the settings curvature mode reads (params/manifest.py), each kept as an attribute of the same name
|
||||
CURV_SETTINGS = ("enable_human_turn_detection_curv", "lane_change_factor_high_curv", "pc_blend_ratio_high_C_UI_curv",
|
||||
"pc_blend_ratio_low_C_UI_curv", "enable_lane_positioning_curv", "custom_path_offset_curv",
|
||||
"enable_lane_full_mode_curv", "custom_profile_curv", "LC_PID_gain_UI_curv")
|
||||
|
||||
|
||||
class PrimaryLateralControl(IntEnum):
|
||||
curvature = 0
|
||||
angle = 1
|
||||
|
||||
|
||||
# CAN FD lateral-accel cap (match opendbc/car/ford/carcontroller.py apply_ford_curvature_limits)
|
||||
AVERAGE_ROAD_ROLL = 0.06 # ~3.4 degrees, 6% superelevation
|
||||
MAX_LATERAL_ACCEL = ISO_LATERAL_ACCEL - (ACCELERATION_DUE_TO_GRAVITY * AVERAGE_ROAD_ROLL)
|
||||
|
||||
|
||||
# Result namedtuple returned by LateralCurvExt.update()
|
||||
LateralResult = namedtuple('LateralResult', [
|
||||
'apply_curvature',
|
||||
'curvature_rate',
|
||||
'path_offset',
|
||||
'path_angle',
|
||||
'ramp_type',
|
||||
'precision_type',
|
||||
'lateralUncertainty',
|
||||
])
|
||||
|
||||
|
||||
def apply_ford_curvature_limits_ext(apply_curvature, apply_curvature_last, current_curvature,
|
||||
v_ego_raw, steering_angle, lat_active, CP):
|
||||
"""Extended version of apply_ford_curvature_limits that returns
|
||||
(apply_curvature, max_curvature, curvature_deviation_limited).
|
||||
|
||||
The max_curvature value is used by _calculate_lateral_uncertainty() for the steering
|
||||
torque bar visualization. Stock version returns only apply_curvature.
|
||||
|
||||
curvature_deviation_limited (BluePilot): True when the current_curvature +- CURVATURE_ERROR
|
||||
clip below actually constrained the commanded curvature this frame -- i.e. the desired
|
||||
maneuver was limited by deviation from measured current curvature, not by rate-of-change
|
||||
(apply_std_steer_angle_limits below is a separate limiter with its own telemetry).
|
||||
"""
|
||||
max_curvature = 1 # large initial value
|
||||
curvature_deviation_limited = False
|
||||
|
||||
# No blending at low speed due to lack of torque wind-up and inaccurate current curvature
|
||||
if v_ego_raw > 9:
|
||||
apply_curvature_pre_error_clip = apply_curvature
|
||||
apply_curvature = np.clip(apply_curvature, current_curvature - CarControllerParams.CURVATURE_ERROR,
|
||||
current_curvature + CarControllerParams.CURVATURE_ERROR)
|
||||
curvature_deviation_limited = bool(abs(apply_curvature - apply_curvature_pre_error_clip) > 1e-9)
|
||||
max_curvature = abs(current_curvature) + CarControllerParams.CURVATURE_ERROR
|
||||
|
||||
# Curvature rate limit after driver torque limit (same inputs/order as apply_ford_curvature_limits)
|
||||
apply_curvature_before_std = apply_curvature
|
||||
apply_curvature = apply_std_steer_angle_limits(apply_curvature_before_std, apply_curvature_last, v_ego_raw,
|
||||
steering_angle, lat_active, BP_ANGLE_LIMITS)
|
||||
|
||||
# Max one-step envelope for UI: steer_up must use pre-limit curvature (see apply_std_steer_angle_limits)
|
||||
steer_up = (apply_curvature_last * apply_curvature_before_std >= 0.0
|
||||
and abs(apply_curvature_before_std) > abs(apply_curvature_last))
|
||||
rate_limits = BP_ANGLE_LIMITS.ANGLE_RATE_LIMIT_UP if steer_up else BP_ANGLE_LIMITS.ANGLE_RATE_LIMIT_DOWN
|
||||
std_steer_angle_rate_limit = np.interp(v_ego_raw, rate_limits[0], rate_limits[1])
|
||||
std_steer_angle_limit = abs(apply_curvature_last) + abs(std_steer_angle_rate_limit)
|
||||
max_curvature = np.minimum(max_curvature, std_steer_angle_limit)
|
||||
|
||||
# Ford CAN FD lateral acceleration limit (more torque available than CAN)
|
||||
if CP.flags & FordFlags.CANFD:
|
||||
curvature_accel_limit = MAX_LATERAL_ACCEL / (max(v_ego_raw, 1) ** 2)
|
||||
apply_curvature = float(np.clip(apply_curvature, -curvature_accel_limit, curvature_accel_limit))
|
||||
max_curvature = np.minimum(max_curvature, abs(curvature_accel_limit))
|
||||
|
||||
return apply_curvature, max_curvature, curvature_deviation_limited
|
||||
|
||||
|
||||
class LateralCurvExt:
|
||||
"""
|
||||
BluePilot lateral control extension for Ford vehicles.
|
||||
|
||||
Held by BluePilotLateral (carcontroller_ext.py), which calls
|
||||
update() during the lateral control loop (20Hz) to get the full
|
||||
4-signal steering command instead of the upstream curvature-only approach.
|
||||
"""
|
||||
|
||||
def __init__(self, inputs, shared):
|
||||
# BluePilotLateral's LateralInputs (read) and SharedLateralState (read and written), common to both strategies
|
||||
self.inputs = inputs
|
||||
self.shared = shared
|
||||
|
||||
# Primary lateral control variable: consumed by BluePilotLateral's lateral dispatch.
|
||||
self.primary_lateral_control = PrimaryLateralControl.curvature
|
||||
|
||||
# Settings (updated from the host's params each frame), at their defaults until then
|
||||
for key in CURV_SETTINGS:
|
||||
setattr(self, key, default(key))
|
||||
|
||||
# Precision/ramp control (precision_type is in the shared state)
|
||||
self.lateralUncertainty = 0.0
|
||||
|
||||
# Predicted curvature blending
|
||||
self.curvature_lookup_time = 0.2 # seconds into the future for curvature extraction
|
||||
self.pc_blend_ratio = 0.5
|
||||
self.pc_blend_ratio_bp = [0.0, 0.001] # curvature breakpoints (1/m)
|
||||
self.pc_blend_ratio_low = 0.40
|
||||
self.pc_blend_ratio_high = 0.40
|
||||
self.pc_blend_ratio_low_C = 0.40 # from UI when custom_profile_curv == 1
|
||||
self.pc_blend_ratio_high_C = 0.40 # from UI when custom_profile_curv == 1
|
||||
|
||||
# Lane change smoothing
|
||||
self.lane_change = False
|
||||
self.lane_change_last = False
|
||||
|
||||
# Post lane change transition
|
||||
self.post_lane_change_timer = 0
|
||||
self.post_lane_change_active = False
|
||||
self.pre_lane_change_values = {'path_angle': 0.0, 'path_offset': 0.0}
|
||||
self.max_path_angle_change = 0.00125
|
||||
self.max_path_offset_change = 0.00125
|
||||
|
||||
# Human turn detection (the detector is in the shared state, used by both modes)
|
||||
self.human_turn = False
|
||||
self.post_reset_ramp_active = False
|
||||
self.reset_steering_last = False
|
||||
|
||||
# Path offset
|
||||
self.path_offset_lookup_time = 0.2 # seconds
|
||||
self.min_laneline_confidence_bp = [0.6, 0.8]
|
||||
|
||||
# PID-based path angle for lane centering
|
||||
self.path_angle_filter_samples = 3
|
||||
self.path_angle_deque = deque(maxlen=self.path_angle_filter_samples)
|
||||
self.LC_PID_k_p = 0.25
|
||||
self.LC_PID_k_i = 0.05
|
||||
self.LC_PID_controller = PIDController(k_p=self.LC_PID_k_p, k_i=self.LC_PID_k_i, rate=20)
|
||||
self.LC_PID_speed_bp = [0.0, 9.0, 15.0] # m/s
|
||||
self.LC_PID_speed_v = [0.0, 0.0, 1.0]
|
||||
self.LC_path_angle_ROC_bp = [5, 15, 25] # m/s
|
||||
self.LC_path_angle_ROC_v = [0.003, 0.0015, 0.002] # match panda limits
|
||||
self.LC_path_angle_reset_counter = 0
|
||||
self.LC_path_angle_reset_duration = 1.5 # seconds
|
||||
|
||||
# DBC signal max limits
|
||||
self.path_angle_max = 0.5
|
||||
self.path_offset_max = 2.0
|
||||
self.curvature_max = CURVATURE_MAX # 0.02
|
||||
|
||||
# Previous frame values
|
||||
self.path_offset_last = 0.0
|
||||
|
||||
def update_lateral_params(self, params):
|
||||
"""Read lateral-related Params from the UI. Called each frame."""
|
||||
for key in CURV_SETTINGS:
|
||||
setattr(self, key, value(params, key))
|
||||
|
||||
try:
|
||||
self.primary_lateral_control = PrimaryLateralControl(value(params, "FordPrefLateralControl"))
|
||||
except ValueError: # a mode other than 0 or 1: curvature mode, the default (#66)
|
||||
self.primary_lateral_control = PrimaryLateralControl.curvature
|
||||
|
||||
def update(self, CC, CS, actuators, apply_curvature_last, CP):
|
||||
"""
|
||||
Compute lateral steering signals for the current frame.
|
||||
|
||||
Called at 20Hz from BluePilotLateral.step_lateral() when inside the STEER_STEP block.
|
||||
|
||||
Args:
|
||||
CC: CarControl with latActive, hudControl
|
||||
CS: CarState with vEgoRaw, yawRate, steeringPressed, steeringAngleDeg
|
||||
actuators: CC.actuators with desired curvature from planner
|
||||
apply_curvature_last: previous frame's applied curvature
|
||||
CP: CarParams with flags for CANFD etc.
|
||||
|
||||
Returns:
|
||||
LateralResult namedtuple with all signals needed for CAN message construction.
|
||||
"""
|
||||
apply_curvature = 0.0
|
||||
path_offset = 0.0
|
||||
path_angle = 0.0
|
||||
reset_steering = 0
|
||||
ramp_type = 2
|
||||
lateralUncertainty = 0.0
|
||||
curvature_deviation_limited = False
|
||||
|
||||
if CC.latActive:
|
||||
self.shared.precision_type = 1
|
||||
steeringPressed = CS.out.steeringPressed
|
||||
steeringAngleDeg_PV = CS.out.steeringAngleDeg
|
||||
|
||||
# Select tuning profile
|
||||
if self.custom_profile_curv == 1:
|
||||
self.pc_blend_ratio_low_C = self.pc_blend_ratio_low_C_UI_curv
|
||||
self.pc_blend_ratio_high_C = self.pc_blend_ratio_high_C_UI_curv
|
||||
|
||||
self.pc_blend_ratio_v = [self.pc_blend_ratio_low_C, self.pc_blend_ratio_high_C]
|
||||
|
||||
# Current and desired curvature
|
||||
current_curvature = self.inputs.get_current_curvature(CS)
|
||||
desired_curvature = actuators.curvature
|
||||
|
||||
# Extract predicted curvature from modelV2
|
||||
if self.inputs.model is not None and len(self.inputs.model.orientationRate.z) >= 17:
|
||||
curvatures = np.array(self.inputs.model.orientationRate.z) / max(0.01, CS.out.vEgoRaw)
|
||||
predicted_curvature = interp(self.curvature_lookup_time, MODEL_T_IDXS, curvatures)
|
||||
else:
|
||||
predicted_curvature = 0.0
|
||||
|
||||
# Blend predicted and desired curvature
|
||||
self.pc_blend_ratio = interp(abs(desired_curvature), self.pc_blend_ratio_bp, self.pc_blend_ratio_v)
|
||||
requested_curvature = (predicted_curvature * self.pc_blend_ratio) + (desired_curvature * (1 - self.pc_blend_ratio))
|
||||
|
||||
# Lane change detection
|
||||
if self.inputs.model is not None:
|
||||
self.lane_change = self.inputs.model.meta.laneChangeState in (1, 2, 3)
|
||||
else:
|
||||
self.lane_change = False
|
||||
|
||||
# Lane change curvature smoothing
|
||||
lane_change_factor = interp(CS.out.vEgoRaw, LANE_CHANGE_FACTOR_BP,
|
||||
[LANE_CHANGE_FACTOR_LOW, self.lane_change_factor_high_curv])
|
||||
|
||||
if self.lane_change and self.inputs.model is not None:
|
||||
if self.inputs.model.meta.laneChangeDirection == 1 and requested_curvature < 0:
|
||||
requested_curvature *= lane_change_factor
|
||||
self.shared.precision_type = 0
|
||||
elif self.inputs.model.meta.laneChangeDirection == 2 and requested_curvature > 0:
|
||||
requested_curvature *= lane_change_factor
|
||||
self.shared.precision_type = 0
|
||||
|
||||
# Human turn: sustained hands-on + large angle (not just a nudge) — see HumanTurnDetector
|
||||
self.human_turn = self.shared.human_turn_detector.update(
|
||||
self.enable_human_turn_detection_curv, steeringPressed, steeringAngleDeg_PV)
|
||||
|
||||
# Steering reset logic
|
||||
if self.human_turn or (CS.out.vEgoRaw < 0.1):
|
||||
reset_steering = 1
|
||||
if reset_steering == 1:
|
||||
requested_curvature = 0.0
|
||||
|
||||
# Apply curvature limits (extended version returning max_curvature)
|
||||
apply_curvature, max_curvature, curvature_deviation_limited = apply_ford_curvature_limits_ext(
|
||||
requested_curvature, apply_curvature_last, current_curvature,
|
||||
CS.out.vEgoRaw, 0, CC.latActive, CP)
|
||||
|
||||
# Lateral uncertainty for torque bar visualization
|
||||
lateralUncertainty = self._calculate_lateral_uncertainty(requested_curvature, apply_curvature, max_curvature)
|
||||
|
||||
# Human turn / standstill reset: after apply_ford_curvature_limits_ext (for torque-bar max_curvature),
|
||||
# force κ=0 and (below) path_angle=0 on the bus. ford.h accepts that frame (curvature 0 skips the
|
||||
# curvature checks), but checks the frames after it as usual: a ramp back that exceeds the rate
|
||||
# limit or sits outside the curvature error band is blocked.
|
||||
if reset_steering == 1:
|
||||
apply_curvature = 0.0
|
||||
self.post_reset_ramp_active = False
|
||||
else:
|
||||
# Detect transition from reset to normal
|
||||
if self.reset_steering_last and not reset_steering:
|
||||
self.post_reset_ramp_active = True
|
||||
apply_curvature_last = 0.0 # Reset to ensure clean ramp from 0
|
||||
|
||||
# Post-reset ramp: gradually ramp from 0 to avoid safety limit trips
|
||||
if self.post_reset_ramp_active:
|
||||
apply_curvature = apply_std_steer_angle_limits(
|
||||
requested_curvature, apply_curvature_last,
|
||||
CS.out.vEgoRaw, 0, CC.latActive, BP_ANGLE_LIMITS)
|
||||
|
||||
curvature_error = abs(requested_curvature - apply_curvature)
|
||||
curvature_threshold = max(abs(requested_curvature) * 0.1, 0.001)
|
||||
if curvature_error < curvature_threshold:
|
||||
self.post_reset_ramp_active = False
|
||||
|
||||
self.reset_steering_last = (reset_steering == 1)
|
||||
|
||||
# Path offset: blend model position with laneline data
|
||||
if self.inputs.model is not None:
|
||||
path_offset_position = interp(self.path_offset_lookup_time, MODEL_T_IDXS, self.inputs.model.position.y)
|
||||
path_offset_lanelines = (self.inputs.model.laneLines[1].y[0] + self.inputs.model.laneLines[2].y[0]) / 2
|
||||
|
||||
# Laneline width tolerance (prevents jumps when lanes merge/diverge)
|
||||
laneline_width = self.inputs.model.laneLines[2].y[0] + (-self.inputs.model.laneLines[1].y[0])
|
||||
laneline_width_tolerance = interp(laneline_width, [3.75, 4.25], [0.81, 0.59])
|
||||
|
||||
# Laneline confidence
|
||||
laneline_confidence = min(self.inputs.model.laneLineProbs[1], self.inputs.model.laneLineProbs[2], laneline_width_tolerance)
|
||||
if not self.enable_lane_full_mode_curv:
|
||||
laneline_confidence = 0.0
|
||||
|
||||
laneline_path_offset_scale = interp(laneline_confidence, self.min_laneline_confidence_bp, [0.0, 1.0])
|
||||
path_offset = ((path_offset_position * (1 - laneline_path_offset_scale)) +
|
||||
(path_offset_lanelines * laneline_path_offset_scale)) + self.custom_path_offset_curv
|
||||
|
||||
# No path offset during lane changes
|
||||
if self.lane_change:
|
||||
path_offset = 0
|
||||
|
||||
# PID-based path angle for lane centering
|
||||
path_offset_error = path_offset * (self.LC_PID_gain_UI_curv / 100)
|
||||
LC_PID_speed_factor = interp(CS.out.vEgoRaw, self.LC_PID_speed_bp, self.LC_PID_speed_v)
|
||||
path_offset_error_adj = path_offset_error * LC_PID_speed_factor
|
||||
|
||||
if not self.enable_lane_positioning_curv:
|
||||
path_offset_error_adj = 0.0
|
||||
|
||||
path_angle_low_c = self.LC_PID_controller.update(path_offset_error_adj)
|
||||
|
||||
if not self.enable_lane_positioning_curv:
|
||||
path_angle_low_c = 0.0
|
||||
if reset_steering == 1:
|
||||
path_angle_low_c = 0.0
|
||||
|
||||
# Rate limit path angle for comfort
|
||||
path_angle_roc = interp(abs(CS.out.vEgoRaw), self.LC_path_angle_ROC_bp, self.LC_path_angle_ROC_v)
|
||||
path_angle_low_c = clip(path_angle_low_c, self.shared.path_angle_last - path_angle_roc, self.shared.path_angle_last + path_angle_roc)
|
||||
|
||||
# Reset PID if driver applies consistent steering pressure
|
||||
if steeringPressed:
|
||||
self.LC_path_angle_reset_counter += 1
|
||||
else:
|
||||
self.LC_path_angle_reset_counter = 0
|
||||
if self.LC_path_angle_reset_counter > self.LC_path_angle_reset_duration * 20:
|
||||
self.LC_PID_controller.reset()
|
||||
|
||||
path_angle = path_angle_low_c # path_angle_high_c not used currently
|
||||
|
||||
# Post lane change transition smoothing
|
||||
path_angle, path_offset = self._handle_post_lane_change_transition(path_angle, path_offset)
|
||||
|
||||
# Final reset handling
|
||||
if reset_steering == 1:
|
||||
path_angle = 0.0
|
||||
|
||||
# Clip all signals to DBC limits
|
||||
# float() is required: numpy's clip returns np.float64, and capnp rejects that type when
|
||||
# apply_curvature reaches actuators.curvature via apply_curvature_last in carcontroller.py.
|
||||
# The cast is exact (both are IEEE 754 doubles), so no output value changes.
|
||||
apply_curvature = float(clip(apply_curvature, -self.curvature_max, self.curvature_max))
|
||||
path_offset = float(clip(path_offset, -self.path_offset_max, self.path_offset_max))
|
||||
path_angle = float(clip(path_angle, -self.path_angle_max, self.path_angle_max))
|
||||
|
||||
# Zero path_offset before CAN send (path_offset and path_angle can conflict, causing discomfort)
|
||||
path_offset = 0.0
|
||||
|
||||
# Ramp type selection
|
||||
if reset_steering == 1:
|
||||
ramp_type = 3 # Immediate
|
||||
self.path_angle_deque.clear()
|
||||
self.LC_PID_controller.reset()
|
||||
else:
|
||||
ramp_type = 2 # Fast
|
||||
|
||||
else:
|
||||
# Lateral control off — zero everything
|
||||
apply_curvature = 0.0
|
||||
path_offset = 0.0
|
||||
path_angle = 0.0
|
||||
self.path_angle_deque.clear()
|
||||
self.LC_PID_controller.reset()
|
||||
ramp_type = 0
|
||||
lateralUncertainty = 0.0
|
||||
|
||||
# Update state for next frame
|
||||
self.lateralUncertainty = lateralUncertainty
|
||||
self.path_offset_last = path_offset
|
||||
self.shared.path_angle_last = path_angle
|
||||
# BluePilot: did the current_curvature +- CURVATURE_ERROR clip constrain the commanded
|
||||
# curvature this frame (deviation from measured, not rate-of-change)? The BluePilot UI's deviation flag.
|
||||
self.shared.bp_curvature_deviation_limited = curvature_deviation_limited
|
||||
|
||||
return LateralResult(
|
||||
apply_curvature=apply_curvature,
|
||||
curvature_rate=0.0, # always 0, like stock Ford
|
||||
path_offset=path_offset,
|
||||
path_angle=path_angle,
|
||||
ramp_type=ramp_type,
|
||||
precision_type=self.shared.precision_type,
|
||||
lateralUncertainty=lateralUncertainty,
|
||||
)
|
||||
|
||||
def _handle_post_lane_change_transition(self, path_angle, path_offset):
|
||||
"""Smooth transition of control variables after lane change completes.
|
||||
|
||||
Rate-limits path_angle and path_offset back to target values
|
||||
over 160 frames (~8 seconds at 20Hz).
|
||||
"""
|
||||
# Detect lane change completion (True → False transition)
|
||||
if self.lane_change_last and not self.lane_change:
|
||||
self.post_lane_change_active = True
|
||||
self.post_lane_change_timer = 0
|
||||
self.pre_lane_change_values = {'path_angle': 0.0, 'path_offset': 0.0}
|
||||
|
||||
self.lane_change_last = self.lane_change
|
||||
|
||||
if self.post_lane_change_active:
|
||||
self.post_lane_change_timer += 1
|
||||
|
||||
new_path_angle = clip(path_angle,
|
||||
self.pre_lane_change_values['path_angle'] - self.max_path_angle_change,
|
||||
self.pre_lane_change_values['path_angle'] + self.max_path_angle_change)
|
||||
new_path_offset = clip(path_offset,
|
||||
self.pre_lane_change_values['path_offset'] - self.max_path_offset_change,
|
||||
self.pre_lane_change_values['path_offset'] + self.max_path_offset_change)
|
||||
|
||||
self.pre_lane_change_values = {
|
||||
'path_angle': new_path_angle,
|
||||
'path_offset': new_path_offset,
|
||||
}
|
||||
|
||||
if self.post_lane_change_timer >= 160:
|
||||
self.post_lane_change_active = False
|
||||
|
||||
return (new_path_angle, new_path_offset)
|
||||
|
||||
return (path_angle, path_offset)
|
||||
|
||||
def _calculate_lateral_uncertainty(self, requested_curvature, apply_curvature, max_curvature):
|
||||
"""Compute ratio of requested to max achievable curvature for the torque bar UI."""
|
||||
max_curvature = np.clip(max_curvature, apply_curvature, self.curvature_max)
|
||||
return float(requested_curvature / max_curvature)
|
||||
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
BluePilot: views over the host's LateralInputs (host_types.py) in the modelV2 / vehicleParameters shape
|
||||
the lateral code reads, and the check that a frame is complete. The host fills LateralInputs once per frame.
|
||||
"""
|
||||
import math
|
||||
from itertools import chain
|
||||
from types import SimpleNamespace
|
||||
|
||||
from ..host_types import MODEL_PATH_POINTS
|
||||
|
||||
|
||||
def lateral_inputs_complete(li) -> bool:
|
||||
"""True when every array the lateral indexes into has the shape modelV2 always provides, and
|
||||
every value is finite. A NaN from the model doesn't raise in the lateral math; it reaches the
|
||||
CAN encoders, which do, so the whole frame is dropped here and the previous inputs are kept."""
|
||||
return (len(li.pathYawRate) == MODEL_PATH_POINTS and len(li.pathX) == MODEL_PATH_POINTS and len(li.pathY) == MODEL_PATH_POINTS
|
||||
and len(li.laneLineLeftX) == MODEL_PATH_POINTS and len(li.laneLineLeftY) == MODEL_PATH_POINTS
|
||||
and len(li.laneLineRightX) == MODEL_PATH_POINTS and len(li.laneLineRightY) == MODEL_PATH_POINTS
|
||||
and len(li.laneLineProbs) >= 3 and len(li.laneLineStds) >= 3
|
||||
and all(map(math.isfinite, chain(li.pathYawRate, li.pathX, li.pathY, li.laneLineLeftX, li.laneLineLeftY,
|
||||
li.laneLineRightX, li.laneLineRightY, li.laneLineProbs, li.laneLineStds,
|
||||
(li.steerRatio, li.stiffnessFactor, li.roll, li.angleOffsetDeg, li.lateralDelay)))))
|
||||
|
||||
|
||||
class _LaneLine:
|
||||
def __init__(self, x, y):
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
|
||||
class ModelView:
|
||||
"""The subset of modelV2 the lateral code uses, with the same attribute paths."""
|
||||
|
||||
def __init__(self, li):
|
||||
self.orientationRate = SimpleNamespace(z=li.pathYawRate)
|
||||
self.position = SimpleNamespace(x=li.pathX, y=li.pathY)
|
||||
# indices match modelV2.laneLines: 1 = left, 2 = right; 0 and 3 are not used
|
||||
self.laneLines = [_LaneLine([], []),
|
||||
_LaneLine(li.laneLineLeftX, li.laneLineLeftY),
|
||||
_LaneLine(li.laneLineRightX, li.laneLineRightY),
|
||||
_LaneLine([], [])]
|
||||
self.laneLineProbs = li.laneLineProbs
|
||||
self.laneLineStds = li.laneLineStds
|
||||
self.meta = SimpleNamespace(laneChangeState=li.laneChangeState, laneChangeDirection=li.laneChangeDirection)
|
||||
|
||||
|
||||
class VehicleParamsView:
|
||||
"""The subset of vehicleParameters the lateral code uses."""
|
||||
|
||||
def __init__(self, li):
|
||||
self.steerRatio = li.steerRatio
|
||||
self.stiffnessFactor = li.stiffnessFactor
|
||||
self.roll = li.roll
|
||||
self.angleOffsetDeg = li.angleOffsetDeg
|
||||
@@ -1,104 +0,0 @@
|
||||
"""Pure angle lane-centering trim adapted from BluePilot bp-dev e22afa6b.
|
||||
|
||||
Alan Polk and ghbarker; the source also credits StarPilot/u/jc01rho.
|
||||
See ../CREDITS.md, ../LICENSE and ../LICENSE.md. Geometry/confidence/fallback,
|
||||
filter and slew follow lane_center_trim.py; this module receives immutable arrays
|
||||
instead of a host model. Stale geometry and driver pressure additionally suppress
|
||||
trim, and nonfinite confidence cannot create correction.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
import math
|
||||
from ..params.trim import DEFAULTS, BOUNDS
|
||||
|
||||
|
||||
def clip(value, low, high):
|
||||
return max(low, min(high, value))
|
||||
|
||||
|
||||
def interp(value, xs, ys):
|
||||
if value <= xs[0]:
|
||||
return ys[0]
|
||||
for index in range(1, len(xs)):
|
||||
if value < xs[index]:
|
||||
return ys[index - 1] + (ys[index] - ys[index - 1]) * (value - xs[index - 1]) / (xs[index] - xs[index - 1])
|
||||
return ys[-1]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TrimConfig:
|
||||
enabled: bool = DEFAULTS["enabled"]
|
||||
offset_m: float = DEFAULTS["offset_m"]
|
||||
gain: float = DEFAULTS["gain"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LaneGeometry:
|
||||
position_x: tuple = ()
|
||||
position_y: tuple = ()
|
||||
left_x: tuple = ()
|
||||
left_y: tuple = ()
|
||||
right_x: tuple = ()
|
||||
right_y: tuple = ()
|
||||
probabilities: tuple = ()
|
||||
standard_deviations: tuple = ()
|
||||
fresh: bool = False
|
||||
|
||||
|
||||
def sample(xs, ys, distance):
|
||||
if (not isinstance(xs, tuple) or not isinstance(ys, tuple) or len(xs) < 2 or len(xs) != len(ys) or
|
||||
not all(math.isfinite(v) for v in (*xs, *ys)) or not all(b > a for a, b in zip(xs, xs[1:], strict=False))):
|
||||
return None
|
||||
return interp(distance, xs, ys)
|
||||
|
||||
|
||||
class LaneCenterTrim:
|
||||
def __init__(self):
|
||||
self.correction = 0.0
|
||||
|
||||
def reset(self):
|
||||
self.correction = 0.0
|
||||
|
||||
def update(self, requested, speed, geometry, config, *, active, lane_change, steering_pressed=False):
|
||||
try:
|
||||
valid = (isinstance(config, TrimConfig) and config.enabled is True and active and not lane_change and
|
||||
not steering_pressed and isinstance(geometry, LaneGeometry) and geometry.fresh is True and
|
||||
math.isfinite(requested) and math.isfinite(speed) and speed >= 0.0 and
|
||||
math.isfinite(config.offset_m) and BOUNDS["offset_m"][0] <= config.offset_m <= BOUNDS["offset_m"][1] and
|
||||
math.isfinite(config.gain) and BOUNDS["gain"][0] <= config.gain <= BOUNDS["gain"][1])
|
||||
authority = interp(speed, BOUNDS["speed_mps"], BOUNDS["speed_authority"]) if valid else 0.0
|
||||
if not valid or authority <= 0.0:
|
||||
self.reset()
|
||||
return requested
|
||||
distance = clip(speed, *BOUNDS["lookahead_m"])
|
||||
model_y = sample(geometry.position_x, geometry.position_y, distance)
|
||||
if model_y is None:
|
||||
self.reset()
|
||||
return requested
|
||||
# Bad lane-only geometry retains the source's model-position/offset fallback.
|
||||
scale, center = self.lane_blend(geometry, distance)
|
||||
error = scale * (center - model_y) + config.offset_m
|
||||
raw = 2.0 * error / distance ** 2
|
||||
target = clip(raw, -BOUNDS["raw_correction"], BOUNDS["raw_correction"]) * config.gain * authority
|
||||
alpha = 1.0 - math.exp(-BOUNDS["period_s"] / BOUNDS["tau_s"])
|
||||
filtered = alpha * target + (1.0 - alpha) * self.correction
|
||||
self.correction = clip(filtered, self.correction - BOUNDS["correction_delta"], self.correction + BOUNDS["correction_delta"])
|
||||
return requested + self.correction
|
||||
except (AttributeError, IndexError, TypeError, ValueError, OverflowError):
|
||||
self.reset()
|
||||
return requested
|
||||
|
||||
def lane_blend(self, geometry, distance):
|
||||
try:
|
||||
left = sample(geometry.left_x, geometry.left_y, distance)
|
||||
right = sample(geometry.right_x, geometry.right_y, distance)
|
||||
probs, stds = geometry.probabilities, geometry.standard_deviations
|
||||
if (left is None or right is None or not isinstance(probs, tuple) or not isinstance(stds, tuple) or
|
||||
len(probs) != 2 or len(stds) != 2 or not all(math.isfinite(p) and 0.0 <= p <= 1.0 for p in probs) or
|
||||
not all(math.isfinite(s) and s >= 0.0 for s in stds)):
|
||||
return 0.0, 0.0
|
||||
confidence = min(*probs, interp(right - left, BOUNDS["width_m"], BOUNDS["width_tolerance"]),
|
||||
interp(max(stds), BOUNDS["std"], BOUNDS["std_tolerance"]))
|
||||
scale = clip(interp(confidence, BOUNDS["confidence"], (0.0, 1.0)), 0.0, 1.0)
|
||||
return scale, 0.5 * (left + right)
|
||||
except (AttributeError, IndexError, TypeError, ValueError, OverflowError):
|
||||
return 0.0, 0.0
|
||||
@@ -0,0 +1,156 @@
|
||||
from ...car import structs
|
||||
from ...car.lateral import AngleSteeringLimits
|
||||
from .. import host_types
|
||||
from ..params.manifest import PER_FRAME_KEYS, startup_value
|
||||
|
||||
|
||||
class FordSafetyFlagsSP:
|
||||
"""BluePilot's Ford safety flags: the bits of the second safety param a host sends the panda
|
||||
(current_safety_param_sp, USB control 0xdf), not the main safetyConfigs[].safetyParam (HOSTS.md).
|
||||
ford_bp_init reads it with GET_FLAG(current_safety_param_sp, ...), same pattern as Subaru
|
||||
STOP_AND_GO (subaru_common.h). Plain int constants, not IntFlag: the param must stay a plain int
|
||||
through the host's serialization.
|
||||
"""
|
||||
# Bit layout of the SP safety param (mirrored in ford_bp_init, bluepilot_lateral/safety/ford.h; pinned by
|
||||
# tests/test_safety_param_bits.py):
|
||||
# bit 0 STEER_ANGLE_CURVATURE (pinion-sourced curvature measurement)
|
||||
# bits 1-4 pinion geometry index (FORD_PINION_GEOMETRY_SHIFT / FORD_PINION_GEOMETRY_INDEX)
|
||||
# bit 5 BP_LATERAL: the panda runs the 4-signal checks (ford_lmc_checks) instead of
|
||||
# upstream's stock curvature-only checks. Clear = upstream safety, verbatim.
|
||||
STEER_ANGLE_CURVATURE = 1
|
||||
BP_LATERAL = 1 << 5
|
||||
|
||||
|
||||
# Geometry-table index for the steering-angle curvature measurement, packed into
|
||||
# the startup safety bits 1-4 when STEER_ANGLE_CURVATURE is set. Must match the
|
||||
# ford_pinion_geometry table in bluepilot_lateral/safety/ford.h row for row.
|
||||
# Index 0 is reserved as invalid: the firmware treats flag-set-but-no-index as feature
|
||||
# off, so a half-configured param can never select the wrong geometry silently.
|
||||
# Platforms with no row are in FORD_NO_PINION_PLATFORMS. Indices stay aligned with the C table.
|
||||
# BluePilot: the per-platform tables key on the platform name (CP.carFingerprint), not the CAR enum,
|
||||
# so a row for a platform some host lacks never breaks import there; it just never matches.
|
||||
# FORD_HOST_ONLY_PLATFORMS lists the names with rows here that upstream opendbc's CAR doesn't have; any
|
||||
# name a host's CAR lacks must be on it (tests/test_platform_tables.py).
|
||||
FORD_HOST_ONLY_PLATFORMS: frozenset[str] = frozenset({
|
||||
"FORD_EDGE_MK2", # StarPilot
|
||||
"FORD_MONDEO_MK5", # BP7, StarPilot
|
||||
"FORD_TRANSIT_MK5", # StarPilot
|
||||
})
|
||||
# No pinion geometry row, so no STEER_ANGLE_CURVATURE (the setting no-ops and the yaw source is kept):
|
||||
FORD_NO_PINION_PLATFORMS: frozenset[str] = frozenset({
|
||||
# ALT_STEER_ANGLE: reads a RELATIVE pinion angle (SteeringPinion_Data_Alt + learned offset) and lacks the
|
||||
# absolute measurement this needs
|
||||
"FORD_EDGE_MK2",
|
||||
# LKA steering through Lane_Assist_Data1 alone: no BluePilot lateral at all
|
||||
"FORD_TRANSIT_MK5",
|
||||
})
|
||||
|
||||
FORD_PINION_GEOMETRY_SHIFT = 1
|
||||
FORD_PINION_GEOMETRY_INDEX = {
|
||||
"FORD_BRONCO_SPORT_MK1": 1,
|
||||
"FORD_ESCAPE_MK4": 2,
|
||||
"FORD_ESCAPE_MK4_5": 3,
|
||||
"FORD_EXPEDITION_MK4": 4,
|
||||
"FORD_EXPLORER_MK6": 5,
|
||||
"FORD_FOCUS_MK4": 6,
|
||||
"FORD_F_150_LIGHTNING_MK1": 7,
|
||||
"FORD_F_150_MK14": 8,
|
||||
"FORD_MAVERICK_MK1": 9,
|
||||
"FORD_MONDEO_MK5": 10,
|
||||
"FORD_MUSTANG_MACH_E_MK1": 11,
|
||||
"FORD_RANGER_MK2": 12,
|
||||
}
|
||||
|
||||
|
||||
def ford_safety_param(CP: structs.CarParams, params_dict: dict[str, str]) -> int:
|
||||
"""The startup safety bits for this car, from the startup settings (HOSTS.md). A host ORs them into the
|
||||
second safety param it sends the panda (current_safety_param_sp, USB 0xdf) at car init."""
|
||||
# Steering-angle curvature measurement (bad-yaw-sensor workaround): the STEER_ANGLE_CURVATURE flag +
|
||||
# the platform geometry-table index; the control side mirrors the same flag
|
||||
# (carcontroller_ext.InputTracker.get_current_curvature). Platforms without a geometry row silently
|
||||
# keep stock yaw behavior -- the toggle no-ops rather than half-configuring.
|
||||
bits = 0
|
||||
if CP.brand == 'ford':
|
||||
# BluePilot 4-signal lateral: tell the panda to run the BP checks. Read once here, at init;
|
||||
# the carcontroller sends 4-signal messages only when this bit is set. Changing the setting
|
||||
# while driving does not change the panda: stock messages are then judged by the BP
|
||||
# envelope (which is not identical to the stock one), so the setting takes full effect at
|
||||
# the next init. Same property as openpilot longitudinal.
|
||||
bp_lateral = not startup_value(params_dict, "disable_BP_lat_UI")
|
||||
if bp_lateral:
|
||||
bits |= FordSafetyFlagsSP.BP_LATERAL
|
||||
|
||||
# The pinion-sourced measurement is a BP lateral feature: its wider error band and the
|
||||
# Python side's yaw-vs-pinion handling only line up on the BP path, so it is off whenever
|
||||
# BP lateral is off (ford_bp_init enforces the same on the panda).
|
||||
steer_angle_curvature = bp_lateral and startup_value(params_dict, "FordPrefSteerAngleCurvature")
|
||||
if steer_angle_curvature:
|
||||
geometry_index = FORD_PINION_GEOMETRY_INDEX.get(CP.carFingerprint)
|
||||
if geometry_index is not None:
|
||||
bits |= FordSafetyFlagsSP.STEER_ANGLE_CURVATURE | (geometry_index << FORD_PINION_GEOMETRY_SHIFT)
|
||||
return bits
|
||||
|
||||
|
||||
# Pinion geometry wheelbase (m) where BluePilot deliberately differs from CarSpecs. Used by both the
|
||||
# C table (ford_pinion_geometry in bluepilot_lateral/safety/ford.h) and the Python pinion path
|
||||
# (carcontroller_ext.pinion_vehicle_model), so the two layers convert pinion angle the same way.
|
||||
# FORD_F_150_MK14: the trucks with lane centering come in 3.68 and 3.99 m wheelbases and nothing on
|
||||
# the car tells them apart; the average keeps either within about 4% (#21).
|
||||
FORD_PINION_WHEELBASE = {
|
||||
"FORD_F_150_MK14": 3.84,
|
||||
}
|
||||
|
||||
|
||||
# BluePilot: Max curvature for steering command (m^-1), from DBC file limits
|
||||
CURVATURE_MAX = 0.02
|
||||
|
||||
# BluePilot: Curvature rate limits — 3-point breakpoints for smoother lateral control.
|
||||
# Upstream opendbc uses 2-point ([5, 25]) with more conservative values.
|
||||
# These allow higher rates at low speed for responsiveness, lower rates at mid-speed
|
||||
# for comfort, and very low rates at highway speed for stability.
|
||||
#
|
||||
# Control (Python) uses stricter windup than unwind so OP stays inside panda when apply_std
|
||||
# picks the wrong table vs ford_bp_curvature_cmd_checks. Safety firmware is looser than both: its
|
||||
# up table is the control's down table, and its down table is 1% looser again — see
|
||||
# bluepilot_lateral/safety/ford.h FORD_LIMITS.
|
||||
_BP_ANGLE_RATE_UP = ([5, 16, 25], [0.0025, 0.0012, 0.00008])
|
||||
_BP_ANGLE_RATE_DOWN = ([5, 16, 25], [0.0025, 0.0014, 0.00018])
|
||||
BP_ANGLE_LIMITS = AngleSteeringLimits(
|
||||
0.02, # Max curvature for steering command, m^-1
|
||||
_BP_ANGLE_RATE_UP,
|
||||
_BP_ANGLE_RATE_DOWN,
|
||||
)
|
||||
|
||||
|
||||
# The model's prediction times, part of the host contract (host_types.py); the strategies read it from here.
|
||||
MODEL_T_IDXS = host_types.MODEL_T_IDXS
|
||||
|
||||
|
||||
# The per-frame settings a host publishes every frame (HOSTS.md; hosts import this name from here). The
|
||||
# settings, their defaults and how they are read are in params/manifest.py.
|
||||
BP_LATERAL_PARAMS = PER_FRAME_KEYS
|
||||
|
||||
# Lane change smoothing, both modes: speed breakpoints (m/s) and the low-speed factor
|
||||
LANE_CHANGE_FACTOR_BP = (4.4, 40.23)
|
||||
LANE_CHANGE_FACTOR_LOW = 0.95
|
||||
|
||||
|
||||
# Angle mode's per-platform high-speed gain defaults (low curvature, high curvature); any other
|
||||
# platform gets ANGLE_GAIN_CAN. Read by lateral_angle_ext.update_angle_params.
|
||||
# CAN vehicles (Escape MK4, Bronco Sport, Explorer, Maverick, Edge)
|
||||
ANGLE_GAIN_CAN = (1.00, 1.15)
|
||||
# CAN-FD body-on-frame trucks (F-150, Lightning, Expedition, Ranger)
|
||||
ANGLE_GAIN_CANFD_BOF = (0.95, 0.95)
|
||||
# CAN-FD unibody SUVs (Mustang Mach-E, Escape MK4.5)
|
||||
ANGLE_GAIN_CANFD_SUV = (1.00, 1.05)
|
||||
|
||||
ANGLE_GAIN_CANFD_BOF_CARS = frozenset({
|
||||
"FORD_F_150_MK14",
|
||||
"FORD_F_150_LIGHTNING_MK1",
|
||||
"FORD_EXPEDITION_MK4",
|
||||
"FORD_RANGER_MK2",
|
||||
})
|
||||
ANGLE_GAIN_CANFD_SUV_CARS = frozenset({
|
||||
"FORD_MUSTANG_MACH_E_MK1",
|
||||
"FORD_ESCAPE_MK4_5",
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
"""BluePilot: the types a host hands the library and reads back from it (HOSTS.md).
|
||||
|
||||
Standard library only: opendbc/car/structs.py imports LateralInputs for CarControlSP, so an import of
|
||||
anything in opendbc here would be an import cycle."""
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
# The model's prediction grid: every path array in LateralInputs has MODEL_PATH_POINTS entries at these
|
||||
# times, openpilot's ModelConstants.T_IDXS (t = 10 * (i / 32)^2). The core interpolates over it.
|
||||
MODEL_PATH_POINTS = 33
|
||||
MODEL_T_IDXS = [10.0 * (i / 32) ** 2 for i in range(MODEL_PATH_POINTS)]
|
||||
|
||||
|
||||
@dataclass
|
||||
class LateralInputs:
|
||||
"""Per-frame model / vehicle inputs for the Ford 4-signal lateral control. The host fills them from
|
||||
modelV2 / vehicleParameters / lateralDelay, so opendbc never subscribes to messaging."""
|
||||
valid: bool = False # producer saw model + vehicle params and filled every field below
|
||||
pathYawRate: list[float] = field(default_factory=list) # modelV2.orientationRate.z, 33 points over T_IDXS
|
||||
pathX: list[float] = field(default_factory=list) # modelV2.position.x
|
||||
pathY: list[float] = field(default_factory=list) # modelV2.position.y
|
||||
laneLineLeftX: list[float] = field(default_factory=list) # modelV2.laneLines[1]
|
||||
laneLineLeftY: list[float] = field(default_factory=list)
|
||||
laneLineRightX: list[float] = field(default_factory=list) # modelV2.laneLines[2]
|
||||
laneLineRightY: list[float] = field(default_factory=list)
|
||||
laneLineProbs: list[float] = field(default_factory=list) # 4 entries
|
||||
laneLineStds: list[float] = field(default_factory=list)
|
||||
laneChangeState: int = 0 # modelV2.meta.laneChangeState
|
||||
laneChangeDirection: int = 0 # modelV2.meta.laneChangeDirection
|
||||
steerRatio: float = 0.0 # vehicleParameters
|
||||
stiffnessFactor: float = 0.0
|
||||
roll: float = 0.0
|
||||
angleOffsetDeg: float = 0.0
|
||||
lateralDelay: float = 0.0 # lateralDelay.lateralDelay
|
||||
|
||||
|
||||
@dataclass
|
||||
class LateralStatus:
|
||||
"""This frame's values for a BluePilot UI, from BluePilotLateral.status(). The names are the ones
|
||||
BP7's controllerStateBP publisher reads off its car controller; the four angle-mode values read
|
||||
False outside angle mode, as BP7 sets them."""
|
||||
lateralUncertainty: float = 0.0 # from whichever strategy ran last
|
||||
angleRateLimited: bool = False # angle mode: the path_angle soft-ROC clip bit this frame
|
||||
curvatureRateLimited: bool = False # angle mode: the equivalent curvature would have been rate-limited
|
||||
curvatureDeviationLimited: bool = False # either mode: the current-curvature error clip constrained the command
|
||||
humanTurnLateralPaused: bool = False # angle mode: lateral sent inactive during a manual turn
|
||||
stallBlipActive: bool = False # angle mode: the brief mode-0 pulse after a post-override stall
|
||||
disable_BP_lat_UI: bool = False # BluePilot lateral off: the UI toggle, or the panda bit not set at init
|
||||
primary_lateral_control: int = 0 # 0 = curvature, 1 = angle
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user