IQ.Pilot Release Commit @ 589e633

This commit is contained in:
IQ.Lvbs CI [bot]
2026-08-31 02:37:13 -05:00
parent 90015b2835
commit 4b46753a27
51 changed files with 2304 additions and 142 deletions

View File

@@ -376,27 +376,27 @@
}, },
"python/iqpilot_private/konn3kt/backups/archive_codec.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/backups/archive_codec.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "569652717895a42a6efd58fded1fdf6aa9c26badad96cf6c9a0a103a7972488d", "sha256": "e5766a76611ae1e6a16f16bed9899c0a1617e9ec8390487ac1c76bd68102aec8",
"size": 135552 "size": 135552
}, },
"python/iqpilot_private/konn3kt/backups/backup_keys.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/backups/backup_keys.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "fc2abcec7142e56b3c8c83414a49eaaec3922e8d56d49c9494ec5fa2c467c018", "sha256": "f3434b919fbbcf9fc2bb29c3af817214b4b025eda7d8a50ab331637e41e3fc30",
"size": 67664 "size": 67664
}, },
"python/iqpilot_private/konn3kt/backups/backup_orchestrator.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/backups/backup_orchestrator.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "2d6763b947b92912313645d3e98a1ed31a6bfaba7f6c73269fa594e12fa2b525", "sha256": "c2a3cbc4411bee79c6b9cf284385152f4c1d8a50fd4beb246872bf47a5059878",
"size": 204640 "size": 204640
}, },
"python/iqpilot_private/konn3kt/backups/cbc_vault.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/backups/cbc_vault.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "484c77fb10d314ac117f7a3914ed2f84c2b7afe2b466846f953aa41bf690d1e2", "sha256": "13f241fc7fb7cdd3afc01ce65936e3054d60910461c384b6f8ccabb65e50e61b",
"size": 69768 "size": 69768
}, },
"python/iqpilot_private/konn3kt/backups/imahelper.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/backups/imahelper.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "85d79a68bd1fc98dc44131d765c55b3c06a7fc72b8901b3034c6ad54b56471dc", "sha256": "c9d4a41687f057074f520b693c3d208cb8c78951f8c2e24b8cf6ea95c1690f14",
"size": 203088 "size": 203088
}, },
"python/iqpilot_private/konn3kt/flockd/__init__.py": { "python/iqpilot_private/konn3kt/flockd/__init__.py": {
@@ -406,12 +406,12 @@
}, },
"python/iqpilot_private/konn3kt/flockd/flockd.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/flockd/flockd.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "3b70968cfe8bb9cc6faafaca46b9505a3a7c53062577f4e4d570bea7a6a84b21", "sha256": "bd6e7bdcd9186240febe0cba2b66dc5ff92e5d7b65f91d8b146e81114a8c6090",
"size": 202184 "size": 202184
}, },
"python/iqpilot_private/konn3kt/flockd/signatures.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/flockd/signatures.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "45e697fc70a02b9f2ba939c5c7136958bef704e907e05d0c835ca585eab878fb", "sha256": "9bea4ab85fdc103fc451031d99e1235ca7f84358a65424db902bbfd839278b30",
"size": 68080 "size": 68080
}, },
"python/iqpilot_private/konn3kt/hephaestus/__init__.py": { "python/iqpilot_private/konn3kt/hephaestus/__init__.py": {
@@ -421,67 +421,67 @@
}, },
"python/iqpilot_private/konn3kt/hephaestus/_vendor/localapi_runtime.zip": { "python/iqpilot_private/konn3kt/hephaestus/_vendor/localapi_runtime.zip": {
"mode": 420, "mode": 420,
"sha256": "db892948057af888ff4587704b3f4ca70b714743a91c0f26d2ec85ccbff309b1", "sha256": "5bccf8ab5fb8a3d648ba8be6f1e9a9263c1577b2a959f202763a6fcbe02eacb9",
"size": 1379342 "size": 1379342
}, },
"python/iqpilot_private/konn3kt/hephaestus/ble_auth.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/hephaestus/ble_auth.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "7bbe4d5161fbb3cbe20f9ee9e52cb3c06b11254cbeaed1d3c64f8ac3bbf7ffc8", "sha256": "6681689c2a20680be9a5c588fc6234a57eba1bc023ff209e9ba16534955d760c",
"size": 268080 "size": 268080
}, },
"python/iqpilot_private/konn3kt/hephaestus/ble_gatt.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/hephaestus/ble_gatt.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "a6da1a1242c2817474d4d16ce156cf875f1ae18622c883af335fe8f344f8b083", "sha256": "cc9c70a616928fe18e3ec7a49b80b127bb827438cd536146d57013ad006cb9ac",
"size": 406176 "size": 406176
}, },
"python/iqpilot_private/konn3kt/hephaestus/ble_rpc_dispatch.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/hephaestus/ble_rpc_dispatch.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "27741c9bec5652feb9da487489aa7c2cca2821887bc2fb36c28f9a79c05c52a7", "sha256": "ac9f9b0467d64935e0a4f81a5e3525aacc8b243937eb339462086a447d7c6210",
"size": 68048 "size": 68048
}, },
"python/iqpilot_private/konn3kt/hephaestus/ble_transportd.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/hephaestus/ble_transportd.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "6df244ee0a6fd26e4e9071c1907410485c8e2b5023fbd67831affadd943d301d", "sha256": "33e3f6f65a0000d89c5b7e66054232a9773adc4d01fa28cb1ac658c20432e56b",
"size": 335776 "size": 335776
}, },
"python/iqpilot_private/konn3kt/hephaestus/bt_gamepad.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/hephaestus/bt_gamepad.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "0e9a3c8c59635fa516ab41c786f2f2cf162d3a9995cf00e7da46ca8c7b60c7b8", "sha256": "c3306fc198b7b9cbf4b481debf4af79ff3d3e9dd23a6cc52137e763427d8f4cd",
"size": 272112 "size": 272112
}, },
"python/iqpilot_private/konn3kt/hephaestus/cloud_routes.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/hephaestus/cloud_routes.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "140b254df8377104f3cb61e821e82d0a6885f63a61197dbad00a55c9c94641d6", "sha256": "a3e3e45c6c9091620ee1d25f840bac287e166d7fb967d48f04d985c801d72711",
"size": 134408 "size": 134408
}, },
"python/iqpilot_private/konn3kt/hephaestus/hephaestusd.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/hephaestus/hephaestusd.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "2324a4c85cd1b3489d7e87cb5c178c885b90fd0e1ad8a15e619dec05d264e4cf", "sha256": "9151341dad54e3d27335a1e34592a35dc96e7f82431512cb9387a6ce35ac9661",
"size": 3915272 "size": 3915272
}, },
"python/iqpilot_private/konn3kt/hephaestus/kwp2000.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/hephaestus/kwp2000.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "53b21e9789a609d1498653441629f99fbfa8051fe7f9e1da6cf7c24dbb20f509", "sha256": "b3481836a4f580539882bf0781b6aa196940c247e6050f074727501599a04eaa",
"size": 136472 "size": 136472
}, },
"python/iqpilot_private/konn3kt/hephaestus/manage_hephaestusd.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/hephaestus/manage_hephaestusd.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "cea0fed8dbb76eb47c466c34491c8feb3847598b0235103a8c3f7897d28caa15", "sha256": "7f557d6349f6feb313896fb063b7cdd1b81d13bc3adaa51072730b62e51f2199",
"size": 68208 "size": 68208
}, },
"python/iqpilot_private/konn3kt/hephaestus/motd.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/hephaestus/motd.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "a60bd145da3d8dba8f55710eb6b36c2583ee485245e2a0d5d6b2230659157231", "sha256": "32257f70289748828910bcdefeb689221a9b9f1b60b669aa454e0c4ed0514194",
"size": 67840 "size": 67840
}, },
"python/iqpilot_private/konn3kt/hephaestus/tp20.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/hephaestus/tp20.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "45d4c4cd8751b0d91efc90938c74148f29eec869a4523ef80dc7e0d471687e1a", "sha256": "a3de8a57ec88a7a7900e360f274ff4a4a55cb6c693282f000ab9e1a804d1aadf",
"size": 135664 "size": 135664
}, },
"python/iqpilot_private/konn3kt/hephaestus/vw_pq_flasher.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/hephaestus/vw_pq_flasher.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "05445a115170b7ebe9355db42d2e44d4d6e323c4ddad10cd8d3c2ea975523793", "sha256": "28a68065a70ceea382ac3773d79f0251f0bf862900a7a353d11fe9cb52561b08",
"size": 269600 "size": 269600
}, },
"python/iqpilot_private/konn3kt/uploaderd/__init__.py": { "python/iqpilot_private/konn3kt/uploaderd/__init__.py": {
@@ -491,7 +491,7 @@
}, },
"python/iqpilot_private/konn3kt/uploaderd/iquploaderd.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/uploaderd/iquploaderd.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "6fd0eb12cec35a9d0d8ac7b7e6005daccb5bb7fcc4cb53dd1bfd4bfbcde0a923", "sha256": "beda75859f40628aaea1890d1207225bd633c76bc975f5a3cc741066508cc709",
"size": 336280 "size": 336280
}, },
"runtime": { "runtime": {
@@ -535,25 +535,25 @@
} }
}, },
"signatures": { "signatures": {
"python/iqpilot_private/konn3kt/backups/archive_codec.cpython-312-aarch64-linux-gnu.so": "VxFp08PRJrvMSP23LRZa7qFujDFaUvn6tG4RlO7iJSDj8i+1wLZAcDiPGJTHZdY6YeExXgP6jSLtQy1BcbZ5Dg==", "python/iqpilot_private/konn3kt/backups/archive_codec.cpython-312-aarch64-linux-gnu.so": "/uV0KncXN88hxSYogRunz1SJTaeiZtYzMfVXiOwr+QA3AwatE3ZHouzIiPxqYsH/AngmSPBqk9sBHOCmPuTiCw==",
"python/iqpilot_private/konn3kt/backups/backup_keys.cpython-312-aarch64-linux-gnu.so": "331zmBEB54CVkMicucclvnYGOhUpzEPhIS6Tnl3lURFjcwjjuc+r4uwgjvLDvfwaBCWTpwel0NNGC03aIyaGAA==", "python/iqpilot_private/konn3kt/backups/backup_keys.cpython-312-aarch64-linux-gnu.so": "skmbTy7FxaZggM1kT1qKUoQijD7s0TJjs7FZB+JSUt4ocf248pQMC55bbKM9P4yFX2QKk7LJ9P0NzXglwTi4CA==",
"python/iqpilot_private/konn3kt/backups/backup_orchestrator.cpython-312-aarch64-linux-gnu.so": "pHLBdX6X8gNM4sI3+NAFAUB5zB82F46GBpzzzWVwiy/pNDPa/60yJh0228Lo8/Z+79iOwr2rqpT+GtZIGaUHBw==", "python/iqpilot_private/konn3kt/backups/backup_orchestrator.cpython-312-aarch64-linux-gnu.so": "pVG+KU133RzRnnJ71ocvMO5c6lnAhXf3eq7wB3Vr1U3KeITBE/7dJemwbvVfN9bciD8/mUFeurVpBslBDlp1Bw==",
"python/iqpilot_private/konn3kt/backups/cbc_vault.cpython-312-aarch64-linux-gnu.so": "l3u8IUF/uaSXW4IimexnNX191B0nxLuu+PxdHQcjkSK+hOs0PJY8QVYE1oeY1CTy8mK0YTQP8sv6hp4RCVb7DA==", "python/iqpilot_private/konn3kt/backups/cbc_vault.cpython-312-aarch64-linux-gnu.so": "/N1l31Ks/riHSDMhyqBpguzrPNGNPg3WlNRBBnloST7kvAEvphcBnT5+yIQvPWtIZfDN6uKNFavUi06Q3/2JDA==",
"python/iqpilot_private/konn3kt/backups/imahelper.cpython-312-aarch64-linux-gnu.so": "kt7IQeKSbCNwia1YG2fHdJAO5DH+D+wNrJVTxGeSi+/c3qxOY9NgdXdRA9RROQcN7Zks95sNEcVXhR6I5ck4CQ==", "python/iqpilot_private/konn3kt/backups/imahelper.cpython-312-aarch64-linux-gnu.so": "2m5TXcCRT3Shxw5k8Ch3G3UtCIE5Lq5pZwp/R0Npn3hBIqAHi7lVU2q8YuaAPEuR09vbL+dStL3aqy/CbCqXBw==",
"python/iqpilot_private/konn3kt/flockd/flockd.cpython-312-aarch64-linux-gnu.so": "jeejVBJoO2+wtw/AYlbTGzPKvn+SLphtKhm9xuEkLTv8EmB5OkmM3AMqrH3G9czCRNBBSn2zjn4sxWimtH0fCQ==", "python/iqpilot_private/konn3kt/flockd/flockd.cpython-312-aarch64-linux-gnu.so": "YtzOVOh9rRQT3fbT4a4UDMbmKxPAUL/fLsA7WvQiI4YaUOgy5vx3i/FMx7N+2FswSbAL1Si+jab85zZbJbGaCQ==",
"python/iqpilot_private/konn3kt/flockd/signatures.cpython-312-aarch64-linux-gnu.so": "85xhiHsSocrlo2tm2m4tI/9wSjMMEzXYjJctL/SNk/MdHE1vSl7e85RGPp1r2xMBsibKi5vvJBaB66dDJikYAA==", "python/iqpilot_private/konn3kt/flockd/signatures.cpython-312-aarch64-linux-gnu.so": "+zB9FYrJAU6/equ/5EUmCTKV+ukSnPb3NdFuLVDxrbQ0wOKLpwU5fL+760Nk4anp8bpOVABQeS5LMSK+jvhqDA==",
"python/iqpilot_private/konn3kt/hephaestus/ble_auth.cpython-312-aarch64-linux-gnu.so": "Arh8D4rzhmm8tIgJhOxDoOSGxZtrkkGFgWSkUPyFRxGv14cMoEoQzMEdU9UErkW4jO2U8vj93nieELi7SMNZDQ==", "python/iqpilot_private/konn3kt/hephaestus/ble_auth.cpython-312-aarch64-linux-gnu.so": "Uny9JAJkWPPCJbAkE9PkQvF2XNdHBKqZqdJraA6c2ywzbTx+F8Y+ge054N7Pfgj/oY2rZqHHwC66sz6zUXNbBg==",
"python/iqpilot_private/konn3kt/hephaestus/ble_gatt.cpython-312-aarch64-linux-gnu.so": "5JDxFY5itf0gGWDYKQbgpkBqE2Esf33v2i2cPDzQtDF/jpA+2C3Lev1rzmM2pN9y/fpCsW4urOmKB5IPRDh0Cg==", "python/iqpilot_private/konn3kt/hephaestus/ble_gatt.cpython-312-aarch64-linux-gnu.so": "EGHn14z9HhVK4WDAiv1k3MKPm9YRkXThjee3PKte9hMB6wF0LWCMNul0pUWcmauPSjhBIU7h9DSIFg9GmgKiAw==",
"python/iqpilot_private/konn3kt/hephaestus/ble_rpc_dispatch.cpython-312-aarch64-linux-gnu.so": "T5D7W6WtK6K42GT/VkkD3/1AWp9xV9Ya5rgE6V+GX4+4sV5PgaFmEf68BlDr/Iwf7PKTEHKKM7eQ3gswOYNEAg==", "python/iqpilot_private/konn3kt/hephaestus/ble_rpc_dispatch.cpython-312-aarch64-linux-gnu.so": "Cmy6QWRx+jkndmCXb2Inr7KSWnB8MeN3IVYoR+GUR7Al2H28bTkB4aroCHc6JRIIIyiE+L3RHn8OZfbfszRICA==",
"python/iqpilot_private/konn3kt/hephaestus/ble_transportd.cpython-312-aarch64-linux-gnu.so": "w/1Xq9W4NVDyS24frgWfl58AbVPWEuq8k5CrC3uwyg0mLGAnr+UXqFzo9/E9xh9XJKbQdCfMdmo0OsSENpNgDA==", "python/iqpilot_private/konn3kt/hephaestus/ble_transportd.cpython-312-aarch64-linux-gnu.so": "vAEIgGKcJnr8NmZDkgoJlVQczr2JmQoORbj5jUmFIidDyw2biwH/hhnhAUEbd7DMs2AgcUKsaCozIdD2SYi8CQ==",
"python/iqpilot_private/konn3kt/hephaestus/bt_gamepad.cpython-312-aarch64-linux-gnu.so": "ifpWpiuKSApIA3Q2q3p+Odw6d127qQfI64cJmr49ReS5ujyzKZqJMGo1vkgm1MsPf/CaBB5PLDA49dJxau1hCw==", "python/iqpilot_private/konn3kt/hephaestus/bt_gamepad.cpython-312-aarch64-linux-gnu.so": "qA+C/uOh+uLHDi8j2HwLUa+Mx3CMOY5hVOjTaw86opkE8GFiCJEMnZnD1+gLfHhSw686VNswDgrp2oUmz97MBQ==",
"python/iqpilot_private/konn3kt/hephaestus/cloud_routes.cpython-312-aarch64-linux-gnu.so": "HPAYWBMZJ8+mfyYR3lzg9jnkH+K5JEY/aYRCMTNlHPO7Wy1e2SLd8e5sqhc0AdOnVembXt06pdRNXrr5myMcDA==", "python/iqpilot_private/konn3kt/hephaestus/cloud_routes.cpython-312-aarch64-linux-gnu.so": "A/t3xlcV3TPh3o9rfH1JhX7nhUVsDH9foqFKjwpqvpuOlm7X4pfgJXLlxCeEN/YAOkaFEL2ITxpTR/LxxI+sAQ==",
"python/iqpilot_private/konn3kt/hephaestus/hephaestusd.cpython-312-aarch64-linux-gnu.so": "6v9Zrjtw0bA5PLqZxZz03N1gwVoGHDzPLvolDWYnUziPwQ3ANhsjJ7puzun4r8s/Y2dyeZe5tjB3ht2YZBsOAQ==", "python/iqpilot_private/konn3kt/hephaestus/hephaestusd.cpython-312-aarch64-linux-gnu.so": "oQC/Edkjh7OGSHjWyYdY0SKRJst5drk4GjXnMuFbZQu53INbrGEuZnpQ8x5Mjb6LxLOiKGU4Yu+ibhZTl7+oAw==",
"python/iqpilot_private/konn3kt/hephaestus/kwp2000.cpython-312-aarch64-linux-gnu.so": "8LUrOg0CgdEaX04HvEAZrSWdovj/M/uhr54bN3XSWbE4qDeKnlsxDD430hHH/Bz17u+eSXzFPBrk33s7SXuyDg==", "python/iqpilot_private/konn3kt/hephaestus/kwp2000.cpython-312-aarch64-linux-gnu.so": "P1y+n8sFHjL+0EMD3+5dHYCUZ5fGLeznzf+1/zszr45fBsHJbSYqPDBepAEx+TrXwDrxEONo8aaVC6PA193pDg==",
"python/iqpilot_private/konn3kt/hephaestus/manage_hephaestusd.cpython-312-aarch64-linux-gnu.so": "TxU/vX6RG/aYvZnJc7plxaHcBiE7zzEmZGlcrRlojwIdUVR9MXwngKLkEBCDdgcXK99xS7JyeXuOxv7bJGwjCg==", "python/iqpilot_private/konn3kt/hephaestus/manage_hephaestusd.cpython-312-aarch64-linux-gnu.so": "yZY/mAzII1O/xMrmQueULxTgXf6Ze6P9PvMNM41LYEaLWlNSJLbYu+umhGQeru8LUGAO+xpuGahDloNMKa+dDg==",
"python/iqpilot_private/konn3kt/hephaestus/motd.cpython-312-aarch64-linux-gnu.so": "VVwuzRtbygLWD0MFhPsRZLNm10Bep4Kde+PQbpHNmpdxXBNYN09cHnxGval7UBmpsgnOIcORBSNK0M2RkfZiBw==", "python/iqpilot_private/konn3kt/hephaestus/motd.cpython-312-aarch64-linux-gnu.so": "zQoY1eHKdlMZW3ITM8G6+UwMBCGR+xt+0dx/ovz5u1uGkbrVHm6A8tfk6Zx27iBKle0qFdye6Al2PQK02kXMBQ==",
"python/iqpilot_private/konn3kt/hephaestus/tp20.cpython-312-aarch64-linux-gnu.so": "OVFsjEKAZNu/dx0Y6aTklTNL+kB+VgwdRFQEaBPkU4Aa3+w192FNSl7QgZYjGymBYk8t4TVkoEJAeIaQAOLBCQ==", "python/iqpilot_private/konn3kt/hephaestus/tp20.cpython-312-aarch64-linux-gnu.so": "10YGOLrImdCOB7LXlDkwkEdj6beXsFfc5AixI5Zq097mBiAcfoWFKiCSPeN3pk4CxuehNXtebw5UuLXsx2ABDg==",
"python/iqpilot_private/konn3kt/hephaestus/vw_pq_flasher.cpython-312-aarch64-linux-gnu.so": "qQkWfrSS3J2CrMpWJ3YDvlg+QZJSLiFp+K6UJvEDOCpd2b9Vdu1jWrUzSVJ+jngsKnzyiwA28j15r5YW290pDA==", "python/iqpilot_private/konn3kt/hephaestus/vw_pq_flasher.cpython-312-aarch64-linux-gnu.so": "QQ2CnLrT79NeQzODxK9/QbduXtCrp6u/Ca6U5KAdnKVPD86+TndxeiX01wgYUZMLmIFCJh4rPGiFBI2f+oS7AA==",
"python/iqpilot_private/konn3kt/uploaderd/iquploaderd.cpython-312-aarch64-linux-gnu.so": "2OPtA7wcd78pbfj532oQKn07r4w+gVXXYq5SzH9Ely3etJJgtAMNGWF6kEY/St5UKHrXz0Ijkb243eyzNNVsBQ==" "python/iqpilot_private/konn3kt/uploaderd/iquploaderd.cpython-312-aarch64-linux-gnu.so": "CqJ0BMkC09FCG+XDybTNULBSobSDBWAPL8YMqXv7QVGCut/9pO1NON5s5zQPVsN6IHUOdt94Iz+h5Ehu1yPjAQ=="
} }
} }

View File

@@ -1 +1 @@
1iB1q4SvZisVqE7ivgPA8wvxTvn7nSsXY5ZvPonciaVEx2/aLqhaXV9wA9sj3a+y1dp9J2Cg4AbrQqgyw+gvAg== 1J/pEtI6qoBjh397jGVMx0xaeG1QXhcxuiHCXKKiU497RGTTHNmgVpKkNngATL61HgbYa7dgj7mPwLs2u7epAQ==

View File

@@ -90,6 +90,44 @@ class TestCanChecksums:
assert parser.vl['LKAS_HUD']['CHECKSUM'] == std assert parser.vl['LKAS_HUD']['CHECKSUM'] == std
assert parser.vl['LKAS_HUD_A']['CHECKSUM'] == ext assert parser.vl['LKAS_HUD_A']['CHECKSUM'] == ext
def test_honda_checksum_high_extended(self):
"""Extended CAN ids above 0x100000 use a +10 checksum constant instead of +3"""
dbc_file = "honda_common_canfd_generated"
msgs = [("LANE_PATH", 0), ("RADAR_LEAD", 0)]
parser = CANParser(dbc_file, msgs, 0)
packer = CANPacker(dbc_file)
lane_path_values = {
'MUX': 1,
'PATH_OFFSET_1': 0,
'PATH_OFFSET_2': 0,
'PATH_OFFSET_3': 2047,
'PATH_OFFSET_4': 2047,
}
radar_lead_values = {
'CNTR_REF': 2,
'SET_ME_X01': 1,
'TARGET_SPEED_MAYBE': 140,
'LEFT_LANE': 3,
'RIGHT_LANE': 3,
'LANE_PATH_LENGTH': 6,
}
# known correct checksums according to the above values
checksum_lane_path = [14, 13, 12, 11]
checksum_radar_lead = [4, 3, 2, 1]
for lane_path, radar_lead in zip(checksum_lane_path, checksum_radar_lead, strict=True):
msgs = [
packer.make_can_msg("LANE_PATH", 0, lane_path_values),
packer.make_can_msg("RADAR_LEAD", 0, radar_lead_values),
]
parser.update([0, msgs])
assert parser.vl['LANE_PATH']['CHECKSUM'] == lane_path
assert parser.vl['RADAR_LEAD']['CHECKSUM'] == radar_lead
assert parser.can_valid
def verify_volkswagen_mqb_crc(self, subtests, msg_name: str, msg_addr: int, test_messages: list[bytes], counter_field: str = 'COUNTER'): def verify_volkswagen_mqb_crc(self, subtests, msg_name: str, msg_addr: int, test_messages: list[bytes], counter_field: str = 'COUNTER'):
"""Test AUTOSAR E2E Profile 2 CRCs""" """Test AUTOSAR E2E Profile 2 CRCs"""
assert len(test_messages) == 16 # All counter values must be tested assert len(test_messages) == 16 # All counter values must be tested

View File

@@ -1,3 +1,4 @@
from iqdbc.car.can_definitions import CanData
from iqdbc.car.carlog import carlog from iqdbc.car.carlog import carlog
from iqdbc.car.isotp_parallel_query import IsoTpParallelQuery from iqdbc.car.isotp_parallel_query import IsoTpParallelQuery
@@ -6,9 +7,45 @@ EXT_DIAG_RESPONSE = b'\x50\x03'
COM_CONT_RESPONSE = b'' COM_CONT_RESPONSE = b''
CLEAR_DTC_REQUEST = b'\x14\xff\xff\xff'
CLEAR_DTC_RESPONSE = b'\x54'
FUNCTIONAL_ADDR_29BIT = 0x18DB33F1
CLEAR_DTC_ISOTP_SF = bytes([len(CLEAR_DTC_REQUEST)]) + CLEAR_DTC_REQUEST + b'\x00' * (7 - len(CLEAR_DTC_REQUEST))
def clear_all_dtcs(can_send, buses, functional_addr=FUNCTIONAL_ADDR_29BIT):
# broadcast clears stored DTCs on every ECU on the bus, including safety-relevant modules
for bus in buses:
carlog.warning(f"clear all DTCs (functional) on bus {bus} ...")
can_send([CanData(functional_addr, CLEAR_DTC_ISOTP_SF, bus)])
def clear_ecu_dtcs(can_recv, can_send, bus=0, addr=0x7d0, sub_addr=None, timeout=0.1, retry=10, response_offset: int = 0x8):
carlog.warning(f"ecu clear DTCs {hex(addr), sub_addr} ...")
for i in range(retry):
try:
query = IsoTpParallelQuery(can_send, can_recv, bus, [(addr, sub_addr)], [EXT_DIAG_REQUEST], [EXT_DIAG_RESPONSE], response_offset)
for _, _ in query.get_data(timeout).items():
carlog.warning("clear diagnostic information ...")
query = IsoTpParallelQuery(can_send, can_recv, bus, [(addr, sub_addr)], [CLEAR_DTC_REQUEST], [CLEAR_DTC_RESPONSE], response_offset)
query.get_data(timeout)
carlog.warning("ecu DTCs cleared")
return True
except Exception:
carlog.exception("ecu clear DTCs exception")
carlog.error(f"ecu clear DTCs retry ({i + 1}) ...")
carlog.error("ecu clear DTCs failed")
return False
def disable_ecu(can_recv, can_send, bus=0, addr=0x7d0, sub_addr=None, com_cont_req=b'\x28\x83\x01', def disable_ecu(can_recv, can_send, bus=0, addr=0x7d0, sub_addr=None, com_cont_req=b'\x28\x83\x01',
timeout=0.1, retry=10, response_offset: int = 0x8): timeout=0.1, retry=10, response_offset: int = 0x8, clear_dtc=False):
"""Silence an ECU by disabling sending and receiving messages using UDS 0x28. """Silence an ECU by disabling sending and receiving messages using UDS 0x28.
The ECU will stay silent as long as openpilot keeps sending Tester Present. The ECU will stay silent as long as openpilot keeps sending Tester Present.
@@ -21,6 +58,12 @@ def disable_ecu(can_recv, can_send, bus=0, addr=0x7d0, sub_addr=None, com_cont_r
query = IsoTpParallelQuery(can_send, can_recv, bus, [(addr, sub_addr)], [EXT_DIAG_REQUEST], [EXT_DIAG_RESPONSE], response_offset) query = IsoTpParallelQuery(can_send, can_recv, bus, [(addr, sub_addr)], [EXT_DIAG_REQUEST], [EXT_DIAG_RESPONSE], response_offset)
for _, _ in query.get_data(timeout).items(): for _, _ in query.get_data(timeout).items():
# a DTC clear can take the ECU several hundred ms, so it must complete before comms go down
if clear_dtc:
carlog.warning("clear diagnostic information ...")
query = IsoTpParallelQuery(can_send, can_recv, bus, [(addr, sub_addr)], [CLEAR_DTC_REQUEST], [CLEAR_DTC_RESPONSE], response_offset)
query.get_data(timeout)
carlog.warning("communication control disable tx/rx ...") carlog.warning("communication control disable tx/rx ...")
query = IsoTpParallelQuery(can_send, can_recv, bus, [(addr, sub_addr)], [com_cont_req], [COM_CONT_RESPONSE], response_offset) query = IsoTpParallelQuery(can_send, can_recv, bus, [(addr, sub_addr)], [com_cont_req], [COM_CONT_RESPONSE], response_offset)

View File

@@ -3,8 +3,9 @@ import math
from iqdbc.can import CANPacker from iqdbc.can import CANPacker
from iqdbc.car import ACCELERATION_DUE_TO_GRAVITY, Bus, DT_CTRL, rate_limit, make_tester_present_msg, structs from iqdbc.car import ACCELERATION_DUE_TO_GRAVITY, Bus, DT_CTRL, rate_limit, make_tester_present_msg, structs
from iqdbc.car.honda import hondacan from iqdbc.car.common.pid import PIDController
from iqdbc.car.honda.values import CAR, CruiseButtons, HONDA_BOSCH, HONDA_BOSCH_CANFD, HONDA_BOSCH_RADARLESS, \ from iqdbc.car.honda import dash_lane, dash_objects, hondacan
from iqdbc.car.honda.values import CAR, CruiseButtons, CruiseSettings, HONDA_BOSCH, HONDA_BOSCH_CANFD, HONDA_BOSCH_RADARLESS, \
HONDA_BOSCH_TJA_CONTROL, HONDA_NIDEC_ALT_PCM_ACCEL, CarControllerParams HONDA_BOSCH_TJA_CONTROL, HONDA_NIDEC_ALT_PCM_ACCEL, CarControllerParams
from iqdbc.car.interfaces import CarControllerBase from iqdbc.car.interfaces import CarControllerBase
@@ -102,6 +103,12 @@ class CarController(CarControllerBase, AolCarController, GasInterceptorCarContro
self.CAN = hondacan.CanBus(CP) self.CAN = hondacan.CanBus(CP)
self.tja_control = CP.carFingerprint in HONDA_BOSCH_TJA_CONTROL self.tja_control = CP.carFingerprint in HONDA_BOSCH_TJA_CONTROL
self.lane_renderer = dash_lane.LanePathRenderer()
self.dash_object_author = dash_objects.DashObjectAuthor()
self.rendered_lane = dash_lane.RenderedLane()
self.lkas_hud_key = None
self.lkas_state_change_frames = 0
self.braking = False self.braking = False
self.brake_steady = 0. self.brake_steady = 0.
self.brake_last = 0. self.brake_last = 0.
@@ -114,6 +121,15 @@ class CarController(CarControllerBase, AolCarController, GasInterceptorCarContro
self.gas = 0.0 self.gas = 0.0
self.brake = 0.0 self.brake = 0.0
self.last_torque = 0.0 self.last_torque = 0.0
self.bosch_last_gas = 0
self.lkas_button_send_remaining = 0
self.last_lkas_button_frame = 0
self.radar_disable_counter = 0
self.radar_mux = 0
# stock RADAR_HUD_CANFD raises its CMBS bit only for a short burst after ACC engages; 10Hz hud ticks
self.radar_hud_pulse = 0
self.last_acc_enabled = False
self.gasfactor = 1.0 self.gasfactor = 1.0
self.gasfactor_before_maxgas = 1.0 self.gasfactor_before_maxgas = 1.0
@@ -122,9 +138,13 @@ class CarController(CarControllerBase, AolCarController, GasInterceptorCarContro
self.windfactor_before_brake = 0.0 self.windfactor_before_brake = 0.0
self.pitch = 0.0 self.pitch = 0.0
self.brake_pid = PIDController(k_p=0.0, k_i=1.0, pos_limit=0.0, neg_limit=-2.0, rate=50)
self.brake_pid.reset()
def update(self, CC, CC_IQ, CS, now_nanos): def update(self, CC, CC_IQ, CS, now_nanos):
AolCarController.update(self, self.CP, CC, CC_IQ) AolCarController.update(self, self.CP, CC, CC_IQ)
gas_pedal_force = 0.0 gas_pedal_force = 0.0
min_gas = self.params.BOSCH_GAS_LOOKUP_BP[0]
actuators = CC.actuators actuators = CC.actuators
hud_control = CC.hudControl hud_control = CC.hudControl
hud_v_cruise = hud_control.setSpeed / CS.v_cruise_factor if hud_control.speedVisible else 255 hud_v_cruise = hud_control.setSpeed / CS.v_cruise_factor if hud_control.speedVisible else 255
@@ -165,11 +185,74 @@ class CarController(CarControllerBase, AolCarController, GasInterceptorCarContro
# Send CAN commands # Send CAN commands
can_sends = [] can_sends = []
# tester present - w/ no response (keeps radar disabled)
if self.CP.carFingerprint in (HONDA_BOSCH - HONDA_BOSCH_RADARLESS) and self.CP.openpilotLongitudinalControl: if self.CP.carFingerprint in (HONDA_BOSCH - HONDA_BOSCH_RADARLESS) and self.CP.openpilotLongitudinalControl:
if self.frame % 10 == 0: if self.CP.carFingerprint in HONDA_BOSCH_CANFD and CS.stock_acc_alive:
# CAN FD: the radar is silenced from here rather than from CarInterface.init(), and only once
# the comma relay is confirmed open: init() ran under the ELM327 safety mode, so the
# replacement ACC_CONTROL stream was blocked until the safety-mode switch landed, and whenever
# that took longer than ~110ms after radar silence the brake module latched CRUISE_FAULT for
# the whole drive. With the relay open the replacement stream starts within a few frames of
# radar silence (see CS.stock_acc_alive), well inside the fault threshold
if CS.canfd_relay_open:
if self.radar_disable_counter % 50 == 0:
# UDS extended diagnostic session, required before CommunicationControl
can_sends.append((0x18DAB0F1, b'\x02\x10\x03\x00\x00\x00\x00\x00', self.CAN.pt))
elif self.radar_disable_counter % 50 == 5:
# UDS CommunicationControl disableRxAndTx (0x80 suppresses the response), retried every
# 0.5s until the radar goes silent
can_sends.append((0x18DAB0F1, b'\x03\x28\x83\x03\x00\x00\x00\x00', self.CAN.pt))
self.radar_disable_counter += 1
elif self.frame % 10 == 0:
# tester present - w/ no response (keeps radar disabled)
can_sends.append(make_tester_present_msg(0x18DAB0F1, self.CAN.pt, suppress_response=True)) can_sends.append(make_tester_present_msg(0x18DAB0F1, self.CAN.pt, suppress_response=True))
# simulate the disabled canfd radar to prevent faults. These look-alikes are consumed by both the
# camera (behind the relay, on the camera bus) and the powertrain: openpilot's own TX is not
# forwarded across the open relay, so each frame is packed exactly once (the packer's
# counter/checksum only advance once per cycle) and the identical bytes are mirrored onto both
# buses (re-packing would double-increment the counter and desync the buses). While the stock
# radar is still transmitting it authors all of these itself
if self.CP.carFingerprint in HONDA_BOSCH_CANFD and self.CP.openpilotLongitudinalControl and not CS.stock_acc_alive:
if CC.enabled and not self.last_acc_enabled:
self.radar_hud_pulse = 30 # ~3s at 10Hz, matching the stock 2-6s engage burst
self.last_acc_enabled = CC.enabled
radar_msgs = []
if CS.hud_tick:
radar_msgs.append(hondacan.create_radar_hud_canfd(self.packer, self.CAN.pt, CC.enabled, self.radar_hud_pulse > 0))
if self.radar_hud_pulse > 0:
self.radar_hud_pulse -= 1
if CS.supp_tick:
radar_msgs.append(hondacan.create_canfd_supplemental(self.packer, self.CAN.pt))
if CS.radar_50hz_tick:
# Cycle the radar MUX through the stock banks: 1-10, 17-26, 33-42, 49-58. This counter also
# drives the LANE_PATH/HUD_OBJECTS mux below: it advances exactly one step per transmitted
# frame, so the sweep stays contiguous even when a tick is missed (a frame-derived mux left
# holes in the sweep the stock radar never produces).
# These must be elif: a bare `if` at a bank start would fall through to the increment,
# skipping the bank-start values (17, 33, 49)
if self.radar_mux >= 58:
self.radar_mux = 1
elif self.radar_mux == 10:
self.radar_mux = 17
elif self.radar_mux == 26:
self.radar_mux = 33
elif self.radar_mux == 42:
self.radar_mux = 49
else:
self.radar_mux += 1
if CS.radar_5hz_tick:
# RADAR_LEAD's LANE_PATH_LENGTH must track the valid-point count of the LANE_PATH sweep being
# authored, and LEFT_LANE/RIGHT_LANE the per-side line-detected status, in lockstep with the
# stock radar's behavior or the dash won't draw the lane lines
radar_msgs.extend(hondacan.create_canfd_5hz_radar_messages(self.packer, self.CAN.pt, CS.radar_ref_counter,
dash_lane.canfd_lane_length(self.rendered_lane),
dash_lane.LANE_LINE_ON if self.rendered_lane.left_line else 0,
dash_lane.LANE_LINE_ON if self.rendered_lane.right_line else 0))
for addr, dat, _ in radar_msgs:
can_sends.append((addr, dat, self.CAN.pt))
can_sends.append((addr, dat, self.CAN.camera))
# Send steering command. # Send steering command.
can_sends.append(hondacan.create_steering_control(self.packer, self.CAN, apply_torque, CC.latActive, self.tja_control)) can_sends.append(hondacan.create_steering_control(self.packer, self.CAN, apply_torque, CC.latActive, self.tja_control))
@@ -208,9 +291,11 @@ class CarController(CarControllerBase, AolCarController, GasInterceptorCarContro
can_sends.append(hondacan.create_bosch_supplemental_1(self.packer, self.CAN)) can_sends.append(hondacan.create_bosch_supplemental_1(self.packer, self.CAN))
# If using stock ACC, spam cancel command to kill gas when OP disengages. # If using stock ACC, spam cancel command to kill gas when OP disengages.
if pcm_cancel_cmd: if pcm_cancel_cmd:
can_sends.append(hondacan.spam_buttons_command(self.packer, self.CAN, CruiseButtons.CANCEL, self.CP.carFingerprint)) can_sends.append(hondacan.spam_buttons_command(self.packer, self.CAN, CruiseButtons.CANCEL, 0, CS.scm_ambient_light,
self.CP.carFingerprint))
elif CC.cruiseControl.resume: elif CC.cruiseControl.resume:
can_sends.append(hondacan.spam_buttons_command(self.packer, self.CAN, CruiseButtons.RES_ACCEL, self.CP.carFingerprint)) can_sends.append(hondacan.spam_buttons_command(self.packer, self.CAN, CruiseButtons.RES_ACCEL, 0, CS.scm_ambient_light,
self.CP.carFingerprint))
else: else:
# Send gas and brake commands. # Send gas and brake commands.
@@ -218,19 +303,38 @@ class CarController(CarControllerBase, AolCarController, GasInterceptorCarContro
ts = self.frame * DT_CTRL ts = self.frame * DT_CTRL
if self.CP.carFingerprint in HONDA_BOSCH: if self.CP.carFingerprint in HONDA_BOSCH:
self.accel = float(np.clip(accel, self.params.BOSCH_ACCEL_MIN, self.params.BOSCH_ACCEL_MAX)) # low-speed extra brake: the fixed accel command under-delivers approaching a stop, so an
gas_pedal_force = self.accel + wind_brake_ms2 * self.windfactor + hill_brake # integral-only term closes the gap, releasing at 1 m/s^3 once out of the window
if (accel < min_gas) and (CS.out.vEgo < 3.0) and not (-1e-3 < CS.out.vEgo < 1e-3):
brake_addon = self.brake_pid.update(error=accel - CS.out.aEgo, speed=CS.out.vEgo)
target_accel = min(accel, accel + brake_addon)
else:
if (self.brake_pid.i < 0.0) and (accel < min_gas):
self.brake_pid.i = min(0.0, self.brake_pid.i + 0.02)
else:
self.brake_pid.reset()
target_accel = min(accel, accel + self.brake_pid.i)
self.accel = float(np.clip(target_accel, self.params.BOSCH_ACCEL_MIN, self.params.BOSCH_ACCEL_MAX))
# not using self.accel since the brake pid resets with the gas pedal
gas_pedal_force = accel + wind_brake_ms2 * self.windfactor + hill_brake
# Live-learn gas pedal adjustments when openpilot is controlling gas. # Live-learn gas pedal adjustments when openpilot is controlling gas.
if (actuators.longControlState == LongCtrlState.pid) and (not CS.out.gasPressed): if (actuators.longControlState == LongCtrlState.pid) and (not CS.out.gasPressed):
gas_error = self.accel - CS.out.aEgo gas_error = accel - CS.out.aEgo
if gas_error != 0.0 and gas_pedal_force > 0.0: if gas_error != 0.0 and gas_pedal_force > min_gas:
learn_speed = 150 if (self.CP.carFingerprint == CAR.HONDA_INSIGHT) else 50 if self.CP.carFingerprint in (CAR.HONDA_INSIGHT, CAR.HONDA_CIVIC_BOSCH): # gas pedal reacts too slowly
self.gasfactor = np.clip(self.gasfactor + gas_error / learn_speed * gas_pedal_force, 0.1, 3.0) learn_speed = 150
elif self.CP.carFingerprint == CAR.ACURA_RDX_3G: # prevent overreacting to turbo lag
learn_speed = 300
else:
learn_speed = 50
self.gasfactor = np.clip(self.gasfactor + gas_error / learn_speed * (gas_pedal_force - min_gas), 0.01, 3.0)
if gas_error != 0.0 and (not CS.out.brakePressed) and (CS.out.vEgo > 0.0): if gas_error != 0.0 and (not CS.out.brakePressed) and (CS.out.vEgo > 0.0):
wind_adjust = 1 + wind_brake_ms2 / 1000 wind_learn_speed = 100 if self.CP.carFingerprint == CAR.ACURA_RDX_3G else 1000
wind_adjust = 1 + wind_brake_ms2 / wind_learn_speed
self.windfactor = np.clip(self.windfactor * (wind_adjust if (gas_error > 0) else 1.0 / wind_adjust), 0.1, 3.0) self.windfactor = np.clip(self.windfactor * (wind_adjust if (gas_error > 0) else 1.0 / wind_adjust), 0.1, 3.0)
if gas_pedal_force <= 0.0: if gas_pedal_force <= min_gas:
self.windfactor = max(self.windfactor, self.windfactor_before_brake) self.windfactor = max(self.windfactor, self.windfactor_before_brake)
else: else:
self.windfactor_before_brake = self.windfactor self.windfactor_before_brake = self.windfactor
@@ -240,12 +344,21 @@ class CarController(CarControllerBase, AolCarController, GasInterceptorCarContro
else: else:
self.gasfactor_before_maxgas = self.gasfactor self.gasfactor_before_maxgas = self.gasfactor
self.windfactor_before_maxgas = self.windfactor self.windfactor_before_maxgas = self.windfactor
self.gas = float(np.interp(gas_pedal_force * self.gasfactor, self.params.BOSCH_GAS_LOOKUP_BP, self.params.BOSCH_GAS_LOOKUP_V)) self.gas = float(np.interp((gas_pedal_force - min_gas) * self.gasfactor + min_gas,
self.params.BOSCH_GAS_LOOKUP_BP, self.params.BOSCH_GAS_LOOKUP_V))
# limit gas ramp to 60 units per frame, matches stock; higher sometimes makes the powertrain ignore the command
max_gas = max(60, self.bosch_last_gas + 60)
self.gas = min(self.gas, max_gas)
self.bosch_last_gas = self.gas
stopping = actuators.longControlState == LongCtrlState.stopping stopping = actuators.longControlState == LongCtrlState.stopping
self.stopping_counter = self.stopping_counter + 1 if stopping else 0 self.stopping_counter = self.stopping_counter + 1 if stopping else 0
can_sends.extend(hondacan.create_acc_commands(self.packer, self.CAN, CC.enabled, CC.longActive, self.accel, self.gas, # CAN FD: never overlap the stock radar's own ACC_CONTROL stream; ours starts within a few
self.stopping_counter, self.CP.carFingerprint, gas_pedal_force)) # frames of the radar going silent (see the deferred radar disable above)
if not (self.CP.carFingerprint in HONDA_BOSCH_CANFD and CS.stock_acc_alive):
can_sends.extend(hondacan.create_acc_commands(self.packer, self.CAN, CC.enabled, CC.longActive, self.accel, self.gas,
self.stopping_counter, self.CP, gas_pedal_force))
else: else:
apply_brake = np.clip(self.brake_last - wind_brake, 0.0, 1.0) apply_brake = np.clip(self.brake_last - wind_brake, 0.0, 1.0)
apply_brake = int(np.clip(apply_brake * self.params.NIDEC_BRAKE_MAX, 0, self.params.NIDEC_BRAKE_MAX - 1)) apply_brake = int(np.clip(apply_brake * self.params.NIDEC_BRAKE_MAX, 0, self.params.NIDEC_BRAKE_MAX - 1))
@@ -272,21 +385,41 @@ class CarController(CarControllerBase, AolCarController, GasInterceptorCarContro
can_sends.extend(GasInterceptorCarController.update(self, CC, CS, gas * self.gasfactor, brake, wind_brake, self.packer, self.frame)) can_sends.extend(GasInterceptorCarController.update(self, CC, CS, gas * self.gasfactor, brake, wind_brake, self.packer, self.frame))
# Send dashboard UI commands. # Send dashboard UI commands. On CAN FD, ACC_HUD is a radar look-alike that openpilot only owns
# once it has disabled the radar; it rides the phase-locked 10Hz hud tick instead of frame % 10
if (self.CP.carFingerprint in HONDA_BOSCH_CANFD and CS.hud_tick and
self.CP.openpilotLongitudinalControl and not CS.stock_acc_alive):
can_sends.append(hondacan.create_acc_hud(self.packer, self.CAN.pt, self.CP, CC.enabled, pcm_speed, actuators.accel,
hud_control, hud_v_cruise, CS.is_metric, CS.acc_hud))
if self.frame % 10 == 0: if self.frame % 10 == 0:
if self.CP.openpilotLongitudinalControl: if self.CP.openpilotLongitudinalControl and self.CP.carFingerprint not in HONDA_BOSCH_CANFD:
# On Nidec, this also controls longitudinal positive acceleration # On Nidec, this also controls longitudinal positive acceleration
can_sends.append(hondacan.create_acc_hud(self.packer, self.CAN.pt, self.CP, CC.enabled, pcm_speed, pcm_accel, can_sends.append(hondacan.create_acc_hud(self.packer, self.CAN.pt, self.CP, CC.enabled, pcm_speed, pcm_accel,
hud_control, hud_v_cruise, CS.is_metric, CS.acc_hud)) hud_control, hud_v_cruise, CS.is_metric, CS.acc_hud))
steering_available = CS.out.cruiseState.available and CS.out.vEgo > self.CP.minSteerSpeed steering_available = CS.out.cruiseState.available and CS.out.vEgo > self.CP.minSteerSpeed
reduced_steering = CS.out.steeringPressed reduced_steering = CS.out.steeringPressed
lkas_state_change = None
if self.CP.carFingerprint in HONDA_BOSCH_CANFD:
# The key must contain exactly the signals that change the LKAS_HUD payload, nothing more:
# a flickering input (like steer saturation) re-triggers the pulse continuously, which keeps
# LKAS_STATE_CHANGE high and suppresses the dash lane lines entirely
hud_key = (bool(CC.latActive), bool(self.dashed_lanes), bool(alert_steer_required), bool(CS.out.steerFaultPermanent))
if hud_key != self.lkas_hud_key:
self.lkas_hud_key = hud_key
self.lkas_state_change_frames = 30 # 3s at the 10Hz LKAS_HUD rate, matching the stock pulse length
lkas_state_change = self.lkas_state_change_frames > 0
self.lkas_state_change_frames = max(0, self.lkas_state_change_frames - 1)
can_sends.extend(hondacan.create_lkas_hud(self.packer, self.CAN.lkas, self.CP, hud_control, CC.latActive, can_sends.extend(hondacan.create_lkas_hud(self.packer, self.CAN.lkas, self.CP, hud_control, CC.latActive,
steering_available, reduced_steering, alert_steer_required, CS.lkas_hud, self.dashed_lanes)) steering_available, reduced_steering, alert_steer_required, CS.lkas_hud, self.dashed_lanes,
steer_fault_permanent=CS.out.steerFaultPermanent, lkas_state_change=lkas_state_change))
if self.CP.openpilotLongitudinalControl: if self.CP.openpilotLongitudinalControl:
# TODO: combining with create_acc_hud block above will change message order and will need replay logs regenerated # TODO: combining with create_acc_hud block above will change message order and will need replay logs regenerated
if self.CP.carFingerprint in (HONDA_BOSCH - HONDA_BOSCH_RADARLESS): if self.CP.carFingerprint in (HONDA_BOSCH - HONDA_BOSCH_RADARLESS - HONDA_BOSCH_CANFD):
can_sends.append(hondacan.create_radar_hud(self.packer, self.CAN.pt)) can_sends.append(hondacan.create_radar_hud(self.packer, self.CAN.pt))
if self.CP.carFingerprint == CAR.HONDA_CIVIC_BOSCH: if self.CP.carFingerprint == CAR.HONDA_CIVIC_BOSCH:
can_sends.append(hondacan.create_legacy_brake_command(self.packer, self.CAN.pt)) can_sends.append(hondacan.create_legacy_brake_command(self.packer, self.CAN.pt))
@@ -295,6 +428,72 @@ class CarController(CarControllerBase, AolCarController, GasInterceptorCarContro
if not self.CP_IQ.enableGasInterceptor: if not self.CP_IQ.enableGasInterceptor:
self.gas = pcm_accel / self.params.NIDEC_GAS_MAX self.gas = pcm_accel / self.params.NIDEC_GAS_MAX
# Render OP's lane and lead cars on the dash. On CAN FD these are radar look-alikes that only
# exist (and are only allowed by panda safety) when the radar is disabled; in stock ACC the real
# radar still owns LANE_PATH/HUD_OBJECTS
if ((self.frame % 2 == 0 and self.CP.carFingerprint in HONDA_BOSCH_RADARLESS) or
(CS.radar_50hz_tick and self.CP.carFingerprint in HONDA_BOSCH_CANFD and self.CP.openpilotLongitudinalControl
and not CS.stock_acc_alive)):
leads = dash_objects.leads_from_model(self.model, CS.out.vEgo)
lead = leads[0]
lead_d = lead.dRel if lead.status else 0.0
self.rendered_lane = self.lane_renderer.update(self.model, CS.out.vEgo, lead_d)
# the dash freezes the lane display if LANE_PATH and HUD_OBJECTS muxes don't match
if self.CP.carFingerprint in HONDA_BOSCH_CANFD:
mux = self.radar_mux
# no LKAS_HUD_2 on CAN FD: the dash reads the lane length from the in-band terminator, so the
# path is reshaped into the terminated-prefix form
lane_offsets = dash_lane.canfd_lane_offsets(self.rendered_lane)
else:
mux = dash_lane.MUX_CYCLE[(self.frame // 2) % len(dash_lane.MUX_CYCLE)]
lane_offsets = self.rendered_lane.offsets
lane_msg = dash_lane.create_lane_path(self.packer, self.CAN.lkas, lane_offsets, mux)
can_sends.append(lane_msg)
# CAN FD cars have no camera HUD_OBJECTS to poll (the disabled radar owned it): author OP's
# lead in slot 0 with the other slots blank (tracks=None)
tracks = CS.camera_object_tracker.snapshot() if CS.camera_object_tracker is not None else None
if self.CP.openpilotLongitudinalControl:
hud_msg = self.dash_object_author.create(self.packer, self.CAN.lkas, lead, tracks, mux, now_nanos * 1e-9,
extra_leads=leads[1:])
else:
# for stock ACC, forward the camera's objects but with our mux
hud_msg = dash_objects.forward_hud_object(self.packer, self.CAN.lkas, mux, tracks)
can_sends.append(hud_msg)
# on CAN FD the camera (behind the relay) also consumes these; mirror the identical packed
# bytes onto the camera bus (packed once, so the counter/checksum stay in lockstep)
if self.CP.carFingerprint in HONDA_BOSCH_CANFD:
for addr, dat, _ in (lane_msg, hud_msg):
can_sends.append((addr, dat, self.CAN.camera))
if self.frame % 20 == 0 and self.CP.carFingerprint in HONDA_BOSCH_RADARLESS:
# COUNTER_2 trails the packer's COUNTER (frame//20 % 4) by one
rl = self.rendered_lane
can_sends.append(dash_lane.create_lkas_hud_2(self.packer, self.CAN.lkas, (self.frame // 20 - 1) % 4,
rl.reach, rl.lane_cross, rl.left_line, rl.right_line))
# Radarless + CAN FD: when stock LKAS is active, the touch-steering-wheel nag eventually forces an
# ACC disengagement (on CAN FD it shows up as a brake tap from the VSA). Disable LKAS automatically
# and block the driver's LKAS button by taking over SCM_BUTTONS on the camera bus while engaged
# (panda blocks the forwarded stock SCM_BUTTONS while this stream flows)
if self.CP.carFingerprint in (HONDA_BOSCH_RADARLESS | HONDA_BOSCH_CANFD) and CC.enabled and self.frame % 4 == 0 and \
not pcm_cancel_cmd and not CC.cruiseControl.resume:
if self.lkas_button_send_remaining == 0 and CS.lkas_hud["LKAS_READY"] and self.frame >= self.last_lkas_button_frame + 500:
self.lkas_button_send_remaining = 3
if self.lkas_button_send_remaining > 0:
self.last_lkas_button_frame = self.frame
self.lkas_button_send_remaining -= 1
cruise_setting = CruiseSettings.LKAS
elif CS.cruise_setting == CruiseSettings.LKAS:
cruise_setting = 0 # block the driver's LKAS button press
else:
cruise_setting = CS.cruise_setting
can_sends.append(hondacan.spam_buttons_command(self.packer, self.CAN, CS.cruise_buttons, cruise_setting,
CS.scm_ambient_light, self.CP.carFingerprint, bus=self.CAN.camera))
# Finalize actuator state for downstream consumers # Finalize actuator state for downstream consumers
new_actuators = actuators.as_builder() new_actuators = actuators.as_builder()
new_actuators.speed = self.speed new_actuators.speed = self.speed

View File

@@ -8,6 +8,7 @@ from iqdbc.car.honda.hondacan import CanBus
from iqdbc.car.honda.values import CAR, DBC, STEER_THRESHOLD, HONDA_BOSCH, HONDA_BOSCH_ALT_RADAR, HONDA_BOSCH_CANFD, \ from iqdbc.car.honda.values import CAR, DBC, STEER_THRESHOLD, HONDA_BOSCH, HONDA_BOSCH_ALT_RADAR, HONDA_BOSCH_CANFD, \
HONDA_NIDEC_ALT_SCM_MESSAGES, HONDA_BOSCH_RADARLESS, HONDA_BOSCH_TJA_CONTROL, \ HONDA_NIDEC_ALT_SCM_MESSAGES, HONDA_BOSCH_RADARLESS, HONDA_BOSCH_TJA_CONTROL, \
HondaFlags, CruiseButtons, CruiseSettings, GearShifter, CarControllerParams HondaFlags, CruiseButtons, CruiseSettings, GearShifter, CarControllerParams
from iqdbc.car.honda.dash_objects import CameraObjectTracker
from iqdbc.car.interfaces import CarStateBase from iqdbc.car.interfaces import CarStateBase
from iqdbc.lvbs.car.honda.iq_carstate import IQCarState from iqdbc.lvbs.car.honda.iq_carstate import IQCarState
@@ -58,11 +59,39 @@ class CarState(CarStateBase, IQCarState):
self.initial_accFault_cleared = False self.initial_accFault_cleared = False
self.initial_accFault_cleared_timer = int(10 / DT_CTRL) # 10 seconds after startup for initial faults to clear self.initial_accFault_cleared_timer = int(10 / DT_CTRL) # 10 seconds after startup for initial faults to clear
self.scm_ambient_light = 0
self.radar_ref_counter = 0
self.radar_5hz_tick_counter = 0
self.radar_5hz_tick = False
self.supp_tick_counter = 0
self.supp_tick = False
self.hud_tick_counter = 0
self.hud_tick = False
self.radar_50hz_tick_counter = 0
self.radar_50hz_tick = False
# CAN FD deferred radar disable (see carcontroller): the stock radar is assumed alive until it has
# been silent for a few frames, and the relay is detected open once the camera's STEERING_CONTROL
# stops being physically visible on the PT bus
self.stock_acc_counter = 0
self.stock_acc_alive = False
self.camera_steer_counter = 0
self.camera_steer_seen = False
self.canfd_frames = 0
self.canfd_relay_open = False
# only radarless cameras emit HUD_OBJECTS to poll for adjacent-car positions; on CAN FD the
# (disabled) radar owned it, so there is nothing to track
self.camera_object_tracker = CameraObjectTracker() if self.CP.carFingerprint in HONDA_BOSCH_RADARLESS else None
def update(self, can_parsers) -> tuple[structs.CarState, structs.IQCarState]: def update(self, can_parsers) -> tuple[structs.CarState, structs.IQCarState]:
cp = can_parsers[Bus.pt] cp = can_parsers[Bus.pt]
cp_cam = can_parsers[Bus.cam] cp_cam = can_parsers[Bus.cam]
if self.CP.enableBsm: if self.CP.enableBsm:
cp_body = can_parsers[Bus.body] cp_body = can_parsers[Bus.body]
if self.CP.carFingerprint in HONDA_BOSCH_CANFD:
cp_radar = can_parsers[Bus.radar]
ret = structs.CarState() ret = structs.CarState()
ret_iq = structs.IQCarState() ret_iq = structs.IQCarState()
@@ -76,6 +105,10 @@ class CarState(CarStateBase, IQCarState):
prev_cruise_setting = self.cruise_setting prev_cruise_setting = self.cruise_setting
self.cruise_setting = cp.vl["SCM_BUTTONS"]["CRUISE_SETTING"] self.cruise_setting = cp.vl["SCM_BUTTONS"]["CRUISE_SETTING"]
self.cruise_buttons = cp.vl["SCM_BUTTONS"]["CRUISE_BUTTONS"] self.cruise_buttons = cp.vl["SCM_BUTTONS"]["CRUISE_BUTTONS"]
if self.CP.carFingerprint in (HONDA_BOSCH_RADARLESS | HONDA_BOSCH_CANFD):
# The camera consumes SCM_BUTTONS content beyond the buttons (losing/zeroing this byte raises an
# adaptive high beam error), so it must be echoed on frames sent in the SCM's place
self.scm_ambient_light = cp.vl["SCM_BUTTONS"]["AMBIENT_LIGHT_MAYBE"]
# used for car hud message # used for car hud message
# TODO: find CAR_SPEED for HONDA_ODYSSEY_TWN or use ACC_HUD w/ detection # TODO: find CAR_SPEED for HONDA_ODYSSEY_TWN or use ACC_HUD w/ detection
@@ -106,7 +139,7 @@ class CarState(CarStateBase, IQCarState):
steer_status = self.steer_status_values[cp.vl["STEER_STATUS"]["STEER_STATUS"]] steer_status = self.steer_status_values[cp.vl["STEER_STATUS"]["STEER_STATUS"]]
ret.steerFaultPermanent = steer_status not in ("NORMAL", "NO_TORQUE_ALERT_1", "NO_TORQUE_ALERT_2", "LOW_SPEED_LOCKOUT", "TMP_FAULT") ret.steerFaultPermanent = steer_status not in ("NORMAL", "NO_TORQUE_ALERT_1", "NO_TORQUE_ALERT_2", "LOW_SPEED_LOCKOUT", "TMP_FAULT")
if self.CP.carFingerprint in HONDA_BOSCH_ALT_RADAR: if self.CP.carFingerprint in (HONDA_BOSCH_ALT_RADAR | HONDA_BOSCH_CANFD):
# TODO: See if this logic works for all other Honda # TODO: See if this logic works for all other Honda
min_steer_speed = max(CarControllerParams.STEER_GLOBAL_MIN_SPEED, self.CP.minSteerSpeed) min_steer_speed = max(CarControllerParams.STEER_GLOBAL_MIN_SPEED, self.CP.minSteerSpeed)
expected_low_speed_lockout = steer_status == "LOW_SPEED_LOCKOUT" and ret.vEgo < min_steer_speed expected_low_speed_lockout = steer_status == "LOW_SPEED_LOCKOUT" and ret.vEgo < min_steer_speed
@@ -115,7 +148,7 @@ class CarState(CarStateBase, IQCarState):
# LOW_SPEED_LOCKOUT is not worth a warning # LOW_SPEED_LOCKOUT is not worth a warning
# NO_TORQUE_ALERT_2 can be caused by bump or steering nudge from driver # NO_TORQUE_ALERT_2 can be caused by bump or steering nudge from driver
# FIXME: the stock camera stops steering on NO_TORQUE_ALERT_1 # FIXME: the stock camera stops steering on NO_TORQUE_ALERT_1
ret.steerFaultTemporary = steer_status not in ("NORMAL", "LOW_SPEED_LOCKOUT", "NO_TORQUE_ALERT_2") ret.steerFaultTemporary = steer_status not in ("NORMAL", "LOW_SPEED_LOCKOUT", "TJA_LOW_SPEED_LOCKOUT", "NO_TORQUE_ALERT_2")
# All Honda EPS cut off slightly above standstill, some much higher # All Honda EPS cut off slightly above standstill, some much higher
# Don't alert in the near-standstill range, but alert for per-vehicle configured minimums above that # Don't alert in the near-standstill range, but alert for per-vehicle configured minimums above that
@@ -234,6 +267,72 @@ class CarState(CarStateBase, IQCarState):
self.stock_brake = cp_cam.vl["BRAKE_COMMAND"] self.stock_brake = cp_cam.vl["BRAKE_COMMAND"]
if self.CP.carFingerprint in (HONDA_BOSCH_RADARLESS | HONDA_BOSCH_CANFD): if self.CP.carFingerprint in (HONDA_BOSCH_RADARLESS | HONDA_BOSCH_CANFD):
self.lkas_hud = cp_cam.vl["LKAS_HUD"] self.lkas_hud = cp_cam.vl["LKAS_HUD"]
if self.CP.carFingerprint in HONDA_BOSCH_CANFD:
# The radar emits low-rate tick reference messages that keep running even while its data
# messages are disabled, so the look-alikes are phased to the stock cadence off of them.
#
# There is a one-frame (10 ms) delay between reading a tick here in carstate and transmitting the
# response in carcontroller. The stock radar sends each data message in the SAME frame as its
# tick, so we pulse one frame BEFORE the next tick (counter == period-1): the +1 transmit delay
# then lands the message on the next tick frame, matching stock.
# period (frames @100Hz): 0x710=100, 0x730=10, 0x750=2, RADAR_REFERENCE=20
self.radar_ref_counter = cp.vl["RADAR_REFERENCE"]["COUNTER"]
# 5 Hz: RADAR_REFERENCE (0x3A1) is on the powertrain bus (cp), not the radar bus (cp_radar).
# RADAR_LEAD does NOT ride with the reference; stock sends it ~120 ms (12 frames) after, so fire
# at frame 11 (+1 transmit delay -> ~120 ms)
ref_tick_vals = cp.vl_all.get("RADAR_REFERENCE", {}).get("COUNTER", [])
if len(ref_tick_vals) > 0:
self.radar_5hz_tick_counter = 0
else:
self.radar_5hz_tick_counter += 1
self.radar_5hz_tick = (self.radar_5hz_tick_counter == 11)
supp_tick_vals = cp_radar.vl_all.get("RADAR_SUPP_TICK_REFERENCE", {}).get("IGNORE", [])
if len(supp_tick_vals) > 0:
self.supp_tick_counter = 0
else:
self.supp_tick_counter += 1
self.supp_tick = (self.supp_tick_counter == 99)
hud_tick_vals = cp_radar.vl_all.get("RADAR_HUD_TICK_REFERENCE", {}).get("IGNORE", [])
if len(hud_tick_vals) > 0:
self.hud_tick_counter = 0
else:
self.hud_tick_counter += 1
self.hud_tick = (self.hud_tick_counter == 9)
tick_50hz_vals = cp_radar.vl_all.get("RADAR_50HZ_TICK_REFERENCE", {}).get("IGNORE", [])
if len(tick_50hz_vals) > 0:
self.radar_50hz_tick_counter = 0
else:
self.radar_50hz_tick_counter += 1
self.radar_50hz_tick = (self.radar_50hz_tick_counter == 1)
# Deferred radar disable (see carcontroller). The stock radar transmits ACC_CONTROL every 2
# frames, so 4 missed frames means it has been silenced; assume alive until then so the
# replacement stream never overlaps it
self.canfd_frames += 1
if len(cp.vl_all.get("ACC_CONTROL", {}).get("COUNTER", [])) > 0:
self.stock_acc_counter = 0
else:
self.stock_acc_counter += 1
self.stock_acc_alive = self.stock_acc_counter < 4
# While the comma relay is closed the camera's STEERING_CONTROL is physically visible on the PT
# bus; when the relay opens it disappears (openpilot's own 0xE4 TX is not parsed as RX). As a
# fallback, assume the relay is open after 5 s of controls in case the camera was never seen
if len(cp.vl_all.get("STEERING_CONTROL", {}).get("COUNTER", [])) > 0:
self.camera_steer_counter = 0
self.camera_steer_seen = True
else:
self.camera_steer_counter += 1
self.canfd_relay_open = (self.camera_steer_seen and self.camera_steer_counter >= 5) or self.canfd_frames >= 500
else:
self.supp_tick = False
self.hud_tick = False
self.radar_5hz_tick = False
self.radar_50hz_tick = False
if self.CP.enableBsm: if self.CP.enableBsm:
# BSM messages are on B-CAN, requires a panda forwarding B-CAN messages to CAN 0 # BSM messages are on B-CAN, requires a panda forwarding B-CAN messages to CAN 0
@@ -246,16 +345,39 @@ class CarState(CarStateBase, IQCarState):
*create_button_events(self.cruise_setting, prev_cruise_setting, SETTINGS_BUTTONS_DICT), *create_button_events(self.cruise_setting, prev_cruise_setting, SETTINGS_BUTTONS_DICT),
] ]
IQCarState.update(self, ret, can_parsers) IQCarState.update(self, ret, ret_iq, can_parsers)
if self.camera_object_tracker is not None:
self.camera_object_tracker.update(cp_cam)
return ret, ret_iq return ret, ret_iq
def get_can_parsers(self, CP, CP_IQ): def get_can_parsers(self, CP, CP_IQ):
pt_messages = []
cam_messages = []
if CP.carFingerprint in HONDA_BOSCH_CANFD:
# Radar-alive and relay-open detection for the deferred radar disable (see carcontroller).
# Both messages intentionally go silent (the radar is disabled, the camera ends up behind the
# open relay), so subscribe with NaN frequency to skip the alive/timeout checks
pt_messages += [("ACC_CONTROL", float('nan')), ("STEERING_CONTROL", float('nan'))]
if CP.carFingerprint in HONDA_BOSCH_RADARLESS:
# polled by the CameraObjectTracker, but not every radarless camera emits it
cam_messages += [("HUD_OBJECTS", float('nan'))]
parsers = { parsers = {
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus(CP).pt), Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], pt_messages, CanBus(CP).pt),
Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus(CP).camera), Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], cam_messages, CanBus(CP).camera),
} }
if CP.enableBsm: if CP.enableBsm:
parsers[Bus.body] = CANParser(DBC[CP.carFingerprint][Bus.body], [], CanBus(CP).radar) parsers[Bus.body] = CANParser(DBC[CP.carFingerprint][Bus.body], [], CanBus(CP).radar)
if CP.carFingerprint in HONDA_BOSCH_CANFD:
# The tick references are only read via vl_all, which (unlike vl) does not auto-subscribe
# messages, so they must be listed explicitly or they are never parsed.
# 0x710 RADAR_SUPP_TICK_REFERENCE (1 Hz), 0x730 RADAR_HUD_TICK_REFERENCE (10 Hz),
# 0x750 RADAR_50HZ_TICK_REFERENCE (50 Hz)
parsers[Bus.radar] = CANParser(DBC[CP.carFingerprint][Bus.radar], [
("RADAR_SUPP_TICK_REFERENCE", 0),
("RADAR_HUD_TICK_REFERENCE", 0),
("RADAR_50HZ_TICK_REFERENCE", 0),
], CanBus(CP).radar)
return parsers return parsers

View File

@@ -0,0 +1,186 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""
from dataclasses import dataclass, field
import numpy as np
POINT_COUNT = 40
POINTS_PER_FRAME = 4
SWEEP_INDICES = POINT_COUNT // POINTS_PER_FRAME
# the camera repeats each sweep index across four redundant banks: mux = index + bank*16,
# giving mux values 1-10, 17-26, 33-42 and 49-58 for logical indices 0-9
MUX_CYCLE = tuple(index + bank * 16 for bank in range(4) for index in range(1, SWEEP_INDICES + 1))
OFFSET_UNAVAILABLE = 2047
OFFSET_VALID_MAX = 2046
NEAR_M = 2.0
FAR_M = 100.0
LOOKAHEAD_M = np.linspace(NEAR_M, FAR_M, POINT_COUNT)
# full swing center -> max turn is slewed over this long so model jumps can't teleport the dash lane
SLEW_RATE_HZ = 50.0
SLEW_FULL_SCALE_S = 2.0
SLEW_MAX_STEP = OFFSET_VALID_MAX / (SLEW_FULL_SCALE_S * SLEW_RATE_HZ)
def _stock_gain(d):
# raw offset units per meter of lateral, regressed from stock radar sweeps vs modelV2 lane centers
return 29.3 + 0.243 * d - 0.00228 * d ** 2
def _legacy_gain(d):
return 6.27 + 0.0106 * d + 0.000354 * d ** 2
GAIN = _stock_gain(LOOKAHEAD_M)
def gain_correction(d: float) -> float:
# the HUD lead marker's lateral scale was tuned against lanes drawn with the legacy (flatter) gain
# law, so the lead's lateral must ride this ratio to stay on the corrected lane rendering
d = min(max(float(d), NEAR_M), FAR_M)
return _stock_gain(d) / _legacy_gain(d)
LANE_LINE_ON = 3
LANE_LENGTH_MAX_VALUE = 33
LANE_WIDTH_DEFAULT = 32
LINE_PROB_ON = 0.25
LINE_PROB_OFF = 0.10
HALF_LANE_M = 1.65
FULL_REACH_SPEED = 27.0
FULL_REACH_LEAD_DIST = 70.0
MIN_REACH = 0.15
def encode_lane_path(x, y):
x = np.asarray(x, dtype=float)
y = np.asarray(y, dtype=float)
if x.size < 2 or x.max() < FAR_M:
return [OFFSET_UNAVAILABLE] * POINT_COUNT
lat = np.interp(LOOKAHEAD_M, x, y)
# stock encodes offsets with the opposite lateral sign to openpilot's +left convention
raw = np.clip(np.round(-GAIN * lat), -OFFSET_VALID_MAX, OFFSET_VALID_MAX)
return [int(v) for v in raw]
# The CAN FD dash has no LKAS_HUD_2 to carry the drawn length: it reads the path as a contiguous valid
# prefix ended by an in-band OFFSET_UNAVAILABLE terminator, idles at 6 valid zero offsets (never
# all-unavailable), and cross-checks the prefix length against RADAR_LEAD's LANE_PATH_LENGTH.
CANFD_MAX_VALID_PTS = 23
CANFD_MIN_VALID_PTS = 6
CANFD_IDLE_OFFSETS = [0] * CANFD_MIN_VALID_PTS + [OFFSET_UNAVAILABLE] * (POINT_COUNT - CANFD_MIN_VALID_PTS)
# stock valid-point count is a function of ego speed alone, fit from factory lanes-on RADAR_LEAD frames
CANFD_LEN_INTERCEPT = 6.74
CANFD_LEN_SLOPE = 0.862
@dataclass
class RenderedLane:
offsets: list[int] = field(default_factory=lambda: [OFFSET_UNAVAILABLE] * POINT_COUNT)
reach: float = 0.0
left_line: bool = False
right_line: bool = False
lane_cross: int = 0
v_ego: float = 0.0
@property
def blank(self) -> bool:
return self.reach <= 0.0 or self.offsets[0] == OFFSET_UNAVAILABLE
def canfd_lane_length(lane: RenderedLane) -> int:
if lane.blank:
return CANFD_MIN_VALID_PTS
n = round(CANFD_LEN_INTERCEPT + CANFD_LEN_SLOPE * lane.v_ego)
return max(CANFD_MIN_VALID_PTS, min(CANFD_MAX_VALID_PTS, n))
def canfd_lane_offsets(lane: RenderedLane) -> list[int]:
if lane.blank:
return CANFD_IDLE_OFFSETS
n_valid = canfd_lane_length(lane)
return list(lane.offsets[:n_valid]) + [OFFSET_UNAVAILABLE] * (POINT_COUNT - n_valid)
def create_lane_path(packer, bus, offsets, mux):
base = ((mux - 1) % 16) * POINTS_PER_FRAME
values = {"MUX": mux}
for i in range(POINTS_PER_FRAME):
values[f"PATH_OFFSET_{i + 1}"] = offsets[base + i]
return packer.make_can_msg("LANE_PATH", bus, values)
def create_lkas_hud_2(packer, bus, counter_2, reach=1.0, lane_cross=0, left_line=True, right_line=True):
lane_length = max(0, min(LANE_LENGTH_MAX_VALUE, round(reach * LANE_LENGTH_MAX_VALUE)))
shown = lane_length > 0
values = {
"COUNTER_2": counter_2,
"SET_ME_X01": 1,
"LANE_WIDTH": LANE_WIDTH_DEFAULT,
"LEFT_LANE": LANE_LINE_ON if (shown and left_line) else 0,
"RIGHT_LANE": LANE_LINE_ON if (shown and right_line) else 0,
"LEFT_LANE_CROSSED": 1 if (shown and lane_cross < 0) else 0,
"RIGHT_LANE_CROSSED": 1 if (shown and lane_cross > 0) else 0,
"LANE_LENGTH": lane_length,
}
return packer.make_can_msg("LKAS_HUD_2", bus, values)
class LanePathRenderer:
def __init__(self):
self._left_on = False
self._right_on = False
self._shown = None
def _lane_center(self, model):
lls, probs = model.laneLines, model.laneLineProbs
if len(lls) < 3 or len(probs) < 3 or len(lls[1].x) == 0:
return None, None, False, False
left = probs[1] >= (LINE_PROB_OFF if self._left_on else LINE_PROB_ON)
right = probs[2] >= (LINE_PROB_OFF if self._right_on else LINE_PROB_ON)
x = np.array(lls[1].x)
yl, yr = np.array(lls[1].y), np.array(lls[2].y)
if left and right:
y = (yl + yr) / 2.0
elif right:
y = yr - HALF_LANE_M
elif left:
y = yl + HALF_LANE_M
else:
return None, None, False, False
return x, y, left, right
def _slew(self, offsets):
# an all-sentinel fit draws nothing: pass through and reset so the next real fit shows unslewed
if offsets[0] == OFFSET_UNAVAILABLE:
self._shown = None
return offsets
target = np.asarray(offsets, dtype=float)
if self._shown is None:
self._shown = target
else:
self._shown = self._shown + np.clip(target - self._shown, -SLEW_MAX_STEP, SLEW_MAX_STEP)
return [int(v) for v in np.round(self._shown)]
def update(self, model, v_ego, lead_d) -> RenderedLane:
x = y = None
left_on = right_on = False
if model is not None:
x, y, left_on, right_on = self._lane_center(model)
if x is None:
self._shown = None
return RenderedLane()
self._left_on, self._right_on = left_on, right_on
reach = float(np.clip(max(v_ego / FULL_REACH_SPEED, lead_d / FULL_REACH_LEAD_DIST, MIN_REACH), 0.0, 1.0))
if round(reach * LANE_LENGTH_MAX_VALUE) <= 0:
self._shown = None
return RenderedLane()
return RenderedLane(self._slew(encode_lane_path(x, y)), reach, left_on, right_on, v_ego=v_ego)

View File

@@ -0,0 +1,314 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""
import math
from dataclasses import dataclass
from iqdbc.can.parser import CANParser
from iqdbc.car.honda import dash_lane
NUM_SLOTS = 10
LONG_DIST_CAP_M = 195.0
# byte-faithful empty-slot payload decoded from stock HUD_OBJECTS; an inconsistent frame risks the dash rejecting it
INACTIVE = {
"OBJECT_ID": 0,
"IS_LEAD_CAR": 0,
"CAR_TYPE": -1,
"ROTATION": -128,
"LONG_DIST": 196.9,
"LAT_DIST": 204.7,
}
CAR_TYPE_CAR = 7
LONG_DIST_MAX_M = 194.0
LAT_DIST_LIM_M = 204.7
# the dash under-scales LAT_DIST ~0.3x in the ego frame; tuned on-car so the lead marker lands on the lane
LAT_SCALE = 0.35
ROT_BAND_M = 1.5
ROT_MAX = 6
REID_GAP_M = 8.0
REID_TAU = 1.5
REID_REFRACTORY = 1.5
MAX_OBJECT_ID = 31
DREL_SMOOTH_TAU = 0.6
YREL_SMOOTH_TAU = 0.5
FF_VREL_MIN = 0.5
DREL_RESID_CLAMP = 1.5
LEAD_PROB_ON = 0.5
LEAD_PROB_OFF = 0.35
LEAD_HOLD_S = 0.6
# modelV2.leadsV3 entries are one car at three time horizons, not three cars: only render the extra
# horizons when spatially distinct from everything already rendered (a genuinely different vehicle)
EXTRA_LEAD_SLOTS = (1, 2)
EXTRA_LEAD_MIN_SEP_D = 5.0
EXTRA_LEAD_MIN_SEP_Y = 1.5
@dataclass
class CameraObject:
slot: int
object_id: int
d_rel: float
y_rel: float
is_lead_car: bool
valid: bool
car_type: int = -1
rotation: int = -128
class CameraObjectTracker:
def __init__(self):
self._tracks: list[CameraObject] = [
CameraObject(slot=i, object_id=0, d_rel=0.0, y_rel=0.0, is_lead_car=False, valid=False)
for i in range(NUM_SLOTS)
]
def update(self, cp_cam: CANParser) -> None:
vla = cp_cam.vl_all["HUD_OBJECTS"]
for mux, oid, ld, yd, lead, ct, rot in zip(vla["MUX"], vla["OBJECT_ID"], vla["LONG_DIST"], vla["LAT_DIST"],
vla["IS_LEAD_CAR"], vla["CAR_TYPE"], vla["ROTATION"], strict=True):
slot = (int(mux) - 1) % 16
if 0 <= slot < NUM_SLOTS:
self._tracks[slot] = CameraObject(
slot=slot,
object_id=int(oid),
d_rel=float(ld),
y_rel=float(yd),
is_lead_car=bool(lead),
valid=oid != 0 and ld < LONG_DIST_CAP_M,
car_type=int(ct),
rotation=int(rot),
)
def snapshot(self) -> list[CameraObject]:
return self._tracks
@dataclass
class ModelLead:
status: bool
dRel: float
yRel: float
vRel: float
prob: float = 0.0
def leads_from_model(model, v_ego, n=3):
# modelV2's lateral is +right; the dash convention is +left. v is made relative for the smoother.
# Data stays populated below LEAD_PROB_ON (status False, prob carried) so the author's hysteresis
# can keep an already-rendered lead alive down to LEAD_PROB_OFF instead of blinking it
out = []
for i in range(n):
if model is None or len(model.leadsV3) <= i or len(model.leadsV3[i].x) == 0:
out.append(ModelLead(False, 0.0, 0.0, 0.0))
continue
lead = model.leadsV3[i]
out.append(ModelLead(bool(lead.prob >= LEAD_PROB_ON), float(lead.x[0]), -float(lead.y[0]),
float(lead.v[0]) - v_ego, prob=float(lead.prob)))
return out
def lead_rotation(lateral_left_m: float) -> int:
magnitude = min(round(abs(lateral_left_m) / ROT_BAND_M), ROT_MAX)
return -magnitude if lateral_left_m > 0 else magnitude
class LeadIdentity:
"""Mints a stable OBJECT_ID for the rendered lead, re-IDing on a fresh lead or a range discontinuity.
dRel is noisy, so a leaky predictor (feed-forward vRel, leak toward dRel) accumulates the residual
instead of a per-sample range-rate test."""
def __init__(self):
self.object_id = 0
self._on = False
self._pred = 0.0
self._prev_t = 0.0
self._reid_t = -1e9
def update(self, status: bool, d_rel: float, v_rel: float, now: float) -> int:
if not status:
self.object_id = 0
self._on = False
return 0
new_lead = not self._on
if self._on:
dt = max(now - self._prev_t, 1e-3)
self._pred += v_rel * dt
self._pred += min(dt / REID_TAU, 1.0) * (d_rel - self._pred)
if abs(d_rel - self._pred) > REID_GAP_M and now - self._reid_t > REID_REFRACTORY:
new_lead = True
self._prev_t = now
if new_lead:
self.object_id = self.object_id % MAX_OBJECT_ID + 1
self._reid_t = now
self._pred = d_rel
self._on = True
return self.object_id
class MarkerSmoother:
"""Stabilizes a rendered marker without lagging real motion: vRel feed-forward on dRel with a
clamped leak toward the measurement, plain low-pass on yRel, snapping on an identity change."""
def __init__(self):
self._id = 0
self._d = 0.0
self._y = 0.0
self._t = 0.0
def update(self, d_rel: float, y_rel: float, v_rel: float, object_id: int, now: float) -> tuple[float, float]:
if object_id != self._id:
self._id, self._d, self._y, self._t = object_id, d_rel, y_rel, now
return d_rel, y_rel
dt = max(now - self._t, 1e-3)
self._t = now
if abs(v_rel) >= FF_VREL_MIN:
self._d += v_rel * dt
resid = min(max(d_rel - self._d, -DREL_RESID_CLAMP), DREL_RESID_CLAMP)
self._d += (1.0 - math.exp(-dt / DREL_SMOOTH_TAU)) * resid
self._y += (1.0 - math.exp(-dt / YREL_SMOOTH_TAU)) * (y_rel - self._y)
return self._d, self._y
def create_hud_object(packer, bus, mux, track):
values = {"MUX": mux}
if track is None:
values.update(INACTIVE)
else:
values.update({
"OBJECT_ID": int(track["object_id"]),
"IS_LEAD_CAR": int(track["is_lead_car"]),
"CAR_TYPE": int(track["car_type"]),
"ROTATION": int(track["rotation"]),
"LONG_DIST": min(max(track["d_rel"], 0.0), LONG_DIST_MAX_M),
"LAT_DIST": min(max(track["y_rel"], -LAT_DIST_LIM_M), LAT_DIST_LIM_M),
})
return packer.make_can_msg("HUD_OBJECTS", bus, values)
def forward_hud_object(packer, bus, mux, tracks):
slot = (mux - 1) % 16
st = tracks[slot] if (tracks and slot < len(tracks)) else None
track = ({"d_rel": st.d_rel, "y_rel": st.y_rel, "object_id": st.object_id, "is_lead_car": st.is_lead_car,
"car_type": st.car_type, "rotation": st.rotation} if (st is not None and st.valid) else None)
return create_hud_object(packer, bus, mux, track)
class DashObjectAuthor:
"""Authors HUD_OBJECTS: openpilot's lead in slot 0 with a stable identity and smoothed marker, the
camera's non-lead cars forwarded in slots 1-9 (or distinct extra model leads where there is no
camera to forward), one frame per mux tick."""
def __init__(self):
self._identity = LeadIdentity()
self._smoother = MarkerSmoother()
self._lead_id = 0
self._prev_op_id = 0
self._lead_on = False
self._lead_hold: ModelLead | None = None
self._lead_seen_t = -1e9
self._extra_ids = {slot: LeadIdentity() for slot in EXTRA_LEAD_SLOTS}
self._extra_smooth = {slot: MarkerSmoother() for slot in EXTRA_LEAD_SLOTS}
self._extra_emit = dict.fromkeys(EXTRA_LEAD_SLOTS, 0)
def _gate_lead(self, lead: ModelLead, now: float) -> ModelLead:
# leadsV3[0].prob hovers around 0.5 in traffic; hysteresis plus a short dead-reckoned hold keeps
# the marker from blinking at a cadence the stock radar never produces
if lead.prob >= (LEAD_PROB_OFF if self._lead_on else LEAD_PROB_ON):
self._lead_on = True
self._lead_hold = lead
self._lead_seen_t = now
return lead if lead.status else ModelLead(True, lead.dRel, lead.yRel, lead.vRel, lead.prob)
if self._lead_on and self._lead_hold is not None and now - self._lead_seen_t < LEAD_HOLD_S:
h = self._lead_hold
return ModelLead(True, h.dRel + h.vRel * (now - self._lead_seen_t), h.yRel, h.vRel, h.prob)
self._lead_on = False
self._lead_hold = None
return ModelLead(False, 0.0, 0.0, 0.0)
def _lead_object_id(self, status: bool, op_id: int, stock_lead_id: int | None, in_use: set[int]) -> int:
if not status:
self._lead_id = 0
elif stock_lead_id is not None:
self._lead_id = stock_lead_id
elif self._lead_id == 0 or op_id != self._prev_op_id or self._lead_id in in_use:
# advance from the current id rather than picking the lowest free one: with no camera ids in
# use a handoff would keep the same id and the id-keyed smoother would slide between two cars
# instead of snapping
nxt = self._lead_id % MAX_OBJECT_ID + 1
while nxt in in_use:
nxt = nxt % MAX_OBJECT_ID + 1
self._lead_id = nxt
self._prev_op_id = op_id
return self._lead_id
def _update_extras(self, extra_leads, lead, in_use, now):
rendered = [(lead.dRel, lead.yRel)] if lead.status else []
out = {}
for slot, ex in zip(EXTRA_LEAD_SLOTS, extra_leads or (), strict=False):
distinct = ex.status and all(abs(ex.dRel - d) >= EXTRA_LEAD_MIN_SEP_D or
abs(ex.yRel - y) >= EXTRA_LEAD_MIN_SEP_Y
for d, y in rendered)
op_id = self._extra_ids[slot].update(distinct, ex.dRel, ex.vRel, now)
if not distinct:
self._extra_emit[slot] = 0
out[slot] = None
continue
emit = self._extra_emit[slot]
if emit == 0 or emit in in_use:
emit = op_id
while emit in in_use:
emit = emit % MAX_OBJECT_ID + 1
self._extra_emit[slot] = emit
in_use.add(emit)
d_rel, y_rel = self._extra_smooth[slot].update(ex.dRel, LAT_SCALE * ex.yRel, ex.vRel, emit, now)
rendered.append((ex.dRel, ex.yRel))
out[slot] = {"d_rel": d_rel, "y_rel": y_rel, "object_id": emit, "is_lead_car": 0,
"car_type": CAR_TYPE_CAR, "rotation": lead_rotation(y_rel / LAT_SCALE)}
return out
def create(self, packer, bus, lead, tracks, mux: int, now: float, extra_leads=None):
lead = self._gate_lead(lead, now)
op_id = self._identity.update(lead.status, lead.dRel, lead.vRel, now)
stock_lead, in_use = None, set()
for t in (tracks or ()):
if not t.valid:
continue
if t.is_lead_car:
stock_lead = t
elif t.slot != 0:
in_use.add(t.object_id)
stock_lead_id = stock_lead.object_id if stock_lead is not None else None
lead_id = self._lead_object_id(lead.status, op_id, stock_lead_id, in_use)
if lead.status:
in_use.add(lead_id)
# ride the lane gain-law correction at the lead's distance so the marker tracks the lane rendering
lat_scale = LAT_SCALE * dash_lane.gain_correction(lead.dRel)
d_rel, y_rel = self._smoother.update(lead.dRel, lat_scale * lead.yRel, lead.vRel, lead_id, now)
extras = self._update_extras(extra_leads, lead, in_use, now) if tracks is None else {}
slot = (mux - 1) % 16
if slot == 0 and lead.status:
track = {"d_rel": d_rel, "y_rel": y_rel, "object_id": lead_id, "is_lead_car": 1,
"car_type": stock_lead.car_type if stock_lead is not None else CAR_TYPE_CAR,
"rotation": stock_lead.rotation if stock_lead is not None else lead_rotation(y_rel / lat_scale)}
elif slot in extras:
track = extras[slot]
else:
st = tracks[slot] if (tracks and slot < len(tracks)) else None
# never forward the camera's lead: if OP has no lead, the HUD must not flag one OP isn't acting on
track = ({"d_rel": st.d_rel, "y_rel": st.y_rel, "object_id": st.object_id, "is_lead_car": 0,
"car_type": st.car_type, "rotation": st.rotation}
if (st is not None and st.valid and not st.is_lead_car) else None)
return create_hud_object(packer, bus, mux, track)

View File

@@ -77,7 +77,7 @@ def create_brake_command(packer, CAN, apply_brake, pump_on, pcm_override, pcm_ca
return packer.make_can_msg("BRAKE_COMMAND", CAN.pt, values) return packer.make_can_msg("BRAKE_COMMAND", CAN.pt, values)
def create_acc_commands(packer, CAN, enabled, active, accel, gas, stopping_counter, car_fingerprint, gas_force): def create_acc_commands(packer, CAN, enabled, active, accel, gas, stopping_counter, CP, gas_force):
commands = [] commands = []
min_gas_accel = CarControllerParams.BOSCH_GAS_LOOKUP_BP[0] min_gas_accel = CarControllerParams.BOSCH_GAS_LOOKUP_BP[0]
@@ -92,15 +92,17 @@ def create_acc_commands(packer, CAN, enabled, active, accel, gas, stopping_count
acc_control_values = { acc_control_values = {
'ACCEL_COMMAND': accel_command, 'ACCEL_COMMAND': accel_command,
'STANDSTILL': standstill, 'STANDSTILL': standstill,
'BRAKE_REQUEST': braking,
} }
if car_fingerprint in HONDA_BOSCH_RADARLESS: if CP.flags & HondaFlags.BOSCH_RADARLESS:
acc_control_values.update({ acc_control_values.update({
"CONTROL_ON": enabled, "CONTROL_ON": enabled,
# hybrid and alt-brake cars require this bit whenever braking; others use it for idle stop after 4s at 50Hz
"COMPUTER_BRAKE_ASSIST": braking if CP.flags & (HondaFlags.HYBRID | HondaFlags.BOSCH_ALT_BRAKE) else stopping_counter > 200,
}) })
else: else:
acc_control_values.update({ acc_control_values.update({
'BRAKE_REQUEST': braking,
# setting CONTROL_ON causes car to set POWERTRAIN_DATA->ACC_STATUS = 1 # setting CONTROL_ON causes car to set POWERTRAIN_DATA->ACC_STATUS = 1
"CONTROL_ON": control_on, "CONTROL_ON": control_on,
"GAS_COMMAND": gas_command, # used for gas "GAS_COMMAND": gas_command, # used for gas
@@ -153,10 +155,14 @@ def create_acc_hud(packer, bus, CP, enabled, pcm_speed, pcm_accel, hud_control,
'SET_ME_X01_2': 1, 'SET_ME_X01_2': 1,
} }
if CP.flags & HondaFlags.BOSCH_CANFD:
acc_hud_values['SET_ME_X01'] = int(enabled and (bool(acc_hud_values['HUD_LEAD']) or (pcm_accel < 0.2)))
acc_hud_values['SET_ME_X01_2'] = int(enabled and (bool(acc_hud_values['HUD_LEAD']) or (pcm_accel < 0.2)))
if CP.carFingerprint in HONDA_BOSCH: if CP.carFingerprint in HONDA_BOSCH:
acc_hud_values['ACC_ON'] = int(enabled) acc_hud_values['ACC_ON'] = int(enabled)
acc_hud_values['FCM_OFF'] = 1 acc_hud_values['FCM_OFF'] = 0
acc_hud_values['FCM_OFF_2'] = 1 acc_hud_values['FCM_OFF_2'] = 0
else: else:
# Shows the distance bars, TODO: stock camera shows updates temporarily while disabled # Shows the distance bars, TODO: stock camera shows updates temporarily while disabled
acc_hud_values['ACC_ON'] = int(enabled) acc_hud_values['ACC_ON'] = int(enabled)
@@ -171,7 +177,8 @@ def create_acc_hud(packer, bus, CP, enabled, pcm_speed, pcm_accel, hud_control,
return packer.make_can_msg("ACC_HUD", bus, acc_hud_values) return packer.make_can_msg("ACC_HUD", bus, acc_hud_values)
def create_lkas_hud(packer, bus, CP, hud_control, lat_active, steering_available, reduced_steering, alert_steer_required, lkas_hud, dashed_lanes): def create_lkas_hud(packer, bus, CP, hud_control, lat_active, steering_available, reduced_steering, alert_steer_required, lkas_hud, dashed_lanes,
steer_fault_permanent=False, lkas_state_change=None):
commands = [] commands = []
lkas_hud_values = { lkas_hud_values = {
@@ -183,14 +190,28 @@ def create_lkas_hud(packer, bus, CP, hud_control, lat_active, steering_available
'BEEP': 0, 'BEEP': 0,
} }
# the stock camera holds LKAS_STATE_CHANGE low, pulsing it high ~3s around HUD state changes;
# holding it high permanently suppresses the dash lane-line rendering
if lkas_state_change is not None:
lkas_hud_values['LKAS_STATE_CHANGE'] = int(lkas_state_change)
if CP.carFingerprint in (HONDA_BOSCH_RADARLESS | HONDA_BOSCH_CANFD): if CP.carFingerprint in (HONDA_BOSCH_RADARLESS | HONDA_BOSCH_CANFD):
lkas_hud_values['LANE_LINES'] = 3 lkas_hud_values['LANE_LINES'] = 3
lkas_hud_values['DASHED_LANES'] = lat_active lkas_hud_values['LKAS_PROBLEM'] = steer_fault_permanent
# car likely needs to see LKAS_PROBLEM fall within a specific time frame, so forward from camera
# TODO: needed for Bosch CAN FD?
if CP.carFingerprint in HONDA_BOSCH_RADARLESS: if CP.carFingerprint in HONDA_BOSCH_RADARLESS:
lkas_hud_values['LKAS_PROBLEM'] = lkas_hud['LKAS_PROBLEM'] # gray lanes when disengaged
lkas_hud_values['DASHED_LANES'] = 1
else:
# CAN FD: dashed lanes are the AOL armed indication (dashed_lanes is aol.enabled and not
# latActive, which is not standstill-gated - so parked LKAS button presses produce cluster
# feedback). ORed with lat_active so the engaged payload keeps SOLID and DASHED set together,
# byte-matching the stock camera's lanes-on state
lkas_hud_values['DASHED_LANES'] = dashed_lanes or lat_active
if CP.carFingerprint in HONDA_BOSCH_CANFD:
# every payload change must coincide with an LKAS_STATE_CHANGE pulse (see carcontroller); keyed
# on lat_active, not lanesVisible, so the dash LKAS indication follows AOL's lateral state
lkas_hud_values['SOLID_LANES'] = lat_active
if not (CP.flags & HondaFlags.BOSCH_EXT_HUD): if not (CP.flags & HondaFlags.BOSCH_EXT_HUD):
lkas_hud_values['RDM_OFF'] = 1 lkas_hud_values['RDM_OFF'] = 1
@@ -225,19 +246,68 @@ def create_legacy_brake_command(packer, bus):
return packer.make_can_msg("LEGACY_BRAKE_COMMAND", bus, {}) return packer.make_can_msg("LEGACY_BRAKE_COMMAND", bus, {})
def spam_buttons_command(packer, CAN, button_val, car_fingerprint): def spam_buttons_command(packer, CAN, cruise_button, cruise_setting, ambient_light, car_fingerprint, bus=None):
values = { values = {
'CRUISE_BUTTONS': button_val, 'CRUISE_BUTTONS': cruise_button,
'CRUISE_SETTING': 0, 'CRUISE_SETTING': cruise_setting,
# the camera consumes this byte too (adaptive high beam); echo the SCM's live value
'AMBIENT_LIGHT_MAYBE': ambient_light,
} }
# send buttons to camera on radarless (camera does ACC) cars if bus is None:
bus = CAN.camera if car_fingerprint in HONDA_BOSCH_RADARLESS else CAN.pt # send buttons to camera on radarless (camera does ACC) cars
bus = CAN.camera if car_fingerprint in HONDA_BOSCH_RADARLESS else CAN.pt
return packer.make_can_msg("SCM_BUTTONS", bus, values) return packer.make_can_msg("SCM_BUTTONS", bus, values)
def create_radar_hud_canfd(packer, bus, acc, acc_pulse=False):
values = {
# the stock radar raises this bit only in short bursts right after ACC engages, never held
'CMBS_ENABLED_MAYBE': 1 if (acc and acc_pulse) else 0,
'ACC_ON': acc,
'SET_ME_X01': 0x01,
'SET_ME_X01_2': 0x01,
}
return packer.make_can_msg("RADAR_HUD_CANFD", bus, values)
def create_canfd_supplemental(packer, bus):
values = {
'SET_ME_X01': 0x01,
'SET_ME_X41': 0x41,
}
return packer.make_can_msg("BOSCH_SUPPLEMENTAL_CANFD", bus, values)
def create_canfd_5hz_radar_messages(packer, bus, radar_ref_cntr, lane_path_length=6, left_lane=0, right_lane=0):
commands = []
radar_lead_values = {
'CNTR_REF': radar_ref_cntr,
'SET_ME_X01': 0x01,
# stock radar transmits a constant 140 here; 120 causes a camera mismatch
'TARGET_SPEED_MAYBE': 140,
'LEFT_LANE': left_lane,
'RIGHT_LANE': right_lane,
# the dash cross-checks this against the LANE_PATH in-band terminator; a mismatch suppresses the lane lines
'LANE_PATH_LENGTH': lane_path_length,
}
commands.append(packer.make_can_msg('RADAR_LEAD', bus, radar_lead_values))
radar_lead2_values = {
'SET_ME_X88': 136,
'SET_ME_X78': 120,
'LEAD_DISTANCE_MAYBE': 0,
}
commands.append(packer.make_can_msg('RADAR_LEAD2', bus, radar_lead2_values))
return commands
def honda_checksum(address: int, sig, d: bytearray) -> int: def honda_checksum(address: int, sig, d: bytearray) -> int:
s = 0 s = 0
extended = address > 0x7FF extended = address > 0x7FF
# extended ids above 0x100000 use a different checksum constant, observed on Bosch CAN FD radar messages
high_extended = address > 0x100000
addr = address addr = address
while addr: while addr:
s += addr & 0xF s += addr & 0xF
@@ -249,5 +319,5 @@ def honda_checksum(address: int, sig, d: bytearray) -> int:
s += (x & 0xF) + (x >> 4) s += (x & 0xF) + (x >> 4)
s = 8 - s s = 8 - s
if extended: if extended:
s += 3 s += 10 if high_extended else 3
return s & 0xF return s & 0xF

View File

@@ -2,7 +2,7 @@
import numpy as np import numpy as np
from iqdbc.car import get_safety_config, structs, uds from iqdbc.car import get_safety_config, structs, uds
from iqdbc.car.common.conversions import Conversions as CV from iqdbc.car.common.conversions import Conversions as CV
from iqdbc.car.disable_ecu import disable_ecu from iqdbc.car.disable_ecu import disable_ecu, clear_all_dtcs, clear_ecu_dtcs
from iqdbc.car.honda.hondacan import CanBus from iqdbc.car.honda.hondacan import CanBus
from iqdbc.car.honda.values import CarControllerParams, HondaFlags, CAR, HONDA_BOSCH, HONDA_BOSCH_CANFD, \ from iqdbc.car.honda.values import CarControllerParams, HondaFlags, CAR, HONDA_BOSCH, HONDA_BOSCH_CANFD, \
HONDA_NIDEC_ALT_SCM_MESSAGES, HONDA_BOSCH_RADARLESS, HondaSafetyFlags HONDA_NIDEC_ALT_SCM_MESSAGES, HONDA_BOSCH_RADARLESS, HondaSafetyFlags
@@ -52,9 +52,8 @@ class CarInterface(CarInterfaceBase):
# Disable the radar and let openpilot control longitudinal # Disable the radar and let openpilot control longitudinal
# WARNING: THIS DISABLES AEB! # WARNING: THIS DISABLES AEB!
# If Bosch radarless, this blocks ACC messages from the camera # If Bosch radarless, this blocks ACC messages from the camera
# TODO: get radar disable working on Bosch CANFD ret.alphaLongitudinalAvailable = True
ret.alphaLongitudinalAvailable = candidate not in HONDA_BOSCH_CANFD ret.openpilotLongitudinalControl = alpha_long
ret.openpilotLongitudinalControl = alpha_long and (candidate not in HONDA_BOSCH_CANFD)
ret.pcmCruise = not ret.openpilotLongitudinalControl ret.pcmCruise = not ret.openpilotLongitudinalControl
else: else:
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.hondaNidec)] ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.hondaNidec)]
@@ -91,8 +90,10 @@ class CarInterface(CarInterfaceBase):
if candidate in HONDA_BOSCH_RADARLESS: if candidate in HONDA_BOSCH_RADARLESS:
ret.stopAccel = CarControllerParams.BOSCH_ACCEL_MIN # stock uses -4.0 m/s^2 once stopped but limited by safety model ret.stopAccel = CarControllerParams.BOSCH_ACCEL_MIN # stock uses -4.0 m/s^2 once stopped but limited by safety model
ret.longitudinalActuatorDelay = 0.25 # s ret.longitudinalActuatorDelay = 0.25 # s
elif candidate in HONDA_BOSCH_CANFD:
ret.longitudinalActuatorDelay = 0.05 # near zero, canfd seems to have stock feedforward correction
else: else:
ret.longitudinalActuatorDelay = 0.5 # s ret.longitudinalActuatorDelay = 0.25 # s, per Bosch A log
else: else:
# default longitudinal tuning for all hondas # default longitudinal tuning for all hondas
ret.longitudinalTuning.kiBP = [0., 5., 35.] ret.longitudinalTuning.kiBP = [0., 5., 35.]
@@ -110,8 +111,6 @@ class CarInterface(CarInterfaceBase):
elif candidate in (CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CIVIC_BOSCH_DIESEL): elif candidate in (CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CIVIC_BOSCH_DIESEL):
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.8], [0.24]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.8], [0.24]]
if candidate == CAR.HONDA_CIVIC_BOSCH:
CarControllerParams.BOSCH_GAS_LOOKUP_V = [0, 750]
elif candidate == CAR.HONDA_CIVIC_2022: elif candidate == CAR.HONDA_CIVIC_2022:
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 5120], [0, 5120]] # TODO: determine if there is a dead zone at the top end ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 5120], [0, 5120]] # TODO: determine if there is a dead zone at the top end
@@ -228,9 +227,13 @@ class CarInterface(CarInterfaceBase):
if candidate == CAR.HONDA_PILOT_4G: if candidate == CAR.HONDA_PILOT_4G:
CarControllerParams.BOSCH_GAS_LOOKUP_V = [0, 2200] CarControllerParams.BOSCH_GAS_LOOKUP_V = [0, 2200]
elif candidate == CAR.ACURA_RDX_3G:
CarControllerParams.BOSCH_GAS_LOOKUP_V = [0, 2200]
elif candidate == CAR.HONDA_CRV_6G and ret.flags & HondaFlags.HYBRID:
CarControllerParams.BOSCH_GAS_LOOKUP_BP = [-0.3, 2.0]
# These cars use alternate user brake msg (0x1BE) # These cars use alternate user brake msg (0x1BE)
if 0x1BE in fingerprint[CAN.pt] and candidate in (CAR.HONDA_ACCORD, CAR.HONDA_HRV_3G, CAR.ACURA_RDX_3G, *HONDA_BOSCH_CANFD): if 0x1BE in fingerprint[CAN.pt] and candidate in HONDA_BOSCH:
ret.flags |= HondaFlags.BOSCH_ALT_BRAKE.value ret.flags |= HondaFlags.BOSCH_ALT_BRAKE.value
if ret.flags & HondaFlags.BOSCH_ALT_BRAKE: if ret.flags & HondaFlags.BOSCH_ALT_BRAKE:
@@ -247,7 +250,10 @@ class CarInterface(CarInterfaceBase):
# min speed to enable ACC. if car can do stop and go, then set enabling speed # min speed to enable ACC. if car can do stop and go, then set enabling speed
# to a negative value, so it won't matter. Otherwise, add 0.5 mph margin to not # to a negative value, so it won't matter. Otherwise, add 0.5 mph margin to not
# conflict with PCM acc # conflict with PCM acc
ret.autoResumeSng = candidate in (HONDA_BOSCH | {CAR.HONDA_CIVIC}) if (ret.transmissionType == TransmissionType.manual) and (not ret.openpilotLongitudinalControl):
ret.autoResumeSng = False
else:
ret.autoResumeSng = candidate in (HONDA_BOSCH | {CAR.HONDA_CIVIC})
if ret.autoResumeSng: if ret.autoResumeSng:
ret.minEnableSpeed = -1. ret.minEnableSpeed = -1.
elif candidate == CAR.HONDA_ODYSSEY_TWN: elif candidate == CAR.HONDA_ODYSSEY_TWN:
@@ -277,6 +283,9 @@ class CarInterface(CarInterfaceBase):
if 0x223 in fingerprint[CAN.pt]: if 0x223 in fingerprint[CAN.pt]:
ret.flags |= HondaFlagsIQ.HYBRID_ALT_BRAKEHOLD.value ret.flags |= HondaFlagsIQ.HYBRID_ALT_BRAKEHOLD.value
if 0x35E in fingerprint[CAN.pt]:
ret.flags |= HondaFlagsIQ.HAS_CAMERA_MESSAGES.value
if candidate == CAR.HONDA_CIVIC: if candidate == CAR.HONDA_CIVIC:
if ret.flags & HondaFlagsIQ.EPS_MODIFIED: if ret.flags & HondaFlagsIQ.EPS_MODIFIED:
# stock request input values: 0x0000, 0x00DE, 0x014D, 0x01EF, 0x0290, 0x0377, 0x0454, 0x0610, 0x06EE # stock request input values: 0x0000, 0x00DE, 0x014D, 0x01EF, 0x0290, 0x0377, 0x0454, 0x0610, 0x06EE
@@ -349,14 +358,32 @@ class CarInterface(CarInterfaceBase):
@staticmethod @staticmethod
def init(CP, CP_IQ, can_recv, can_send, communication_control=None): def init(CP, CP_IQ, can_recv, can_send, communication_control=None):
if CP.carFingerprint in (HONDA_BOSCH - HONDA_BOSCH_RADARLESS) and CP.openpilotLongitudinalControl: if CP.carFingerprint in (HONDA_BOSCH - HONDA_BOSCH_RADARLESS) and CP.openpilotLongitudinalControl:
# 0x80 silences response if communication_control is None and CP.carFingerprint in HONDA_BOSCH_CANFD:
if communication_control is None: # CAN FD: only clear DTCs here; the radar silencing itself is deferred to CarController until
communication_control = bytes([uds.SERVICE_TYPE.COMMUNICATION_CONTROL, 0x80 | uds.CONTROL_TYPE.DISABLE_RX_DISABLE_TX, # the comma relay is confirmed open. init() runs while the panda is still in the ELM327 safety
uds.MESSAGE_TYPE.NORMAL_AND_NETWORK_MANAGEMENT]) # mode, and silencing the radar from here raced the safety-mode switch: whenever the switch
disable_ecu(can_recv, can_send, bus=CanBus(CP).pt, addr=0x18DAB0F1, com_cont_req=communication_control) # took longer than ~110 ms after radar silence, the brake module latched CRUISE_FAULT for the
# entire drive.
#
# The brake module's radar lost-communication DTC matures over trips (Honda two-trip
# detection): once confirmed from a previous drive, the very next comm-loss detection faults
# ~0.16 s after the radar goes silent. Broadcast-clear stored DTCs on the powertrain and
# camera buses every drive to reset the maturation counter, and clear the radar's own stored
# DTCs so codes accumulated while it was disabled don't re-fault a later drive. Clearing must
# precede the radar silence because a DTC clear can take an ECU several hundred ms.
# NOTE: ELM327 safety mode allows the 29-bit functional diagnostic address on every bus, so
# the broadcast needs no TX allowlist entry in the car safety mode
clear_all_dtcs(can_send, [CanBus(CP).pt, CanBus(CP).camera])
clear_ecu_dtcs(can_recv, can_send, bus=CanBus(CP).pt, addr=0x18DAB0F1)
else:
# 0x80 silences response
if communication_control is None:
communication_control = bytes([uds.SERVICE_TYPE.COMMUNICATION_CONTROL, 0x80 | uds.CONTROL_TYPE.DISABLE_RX_DISABLE_TX,
uds.MESSAGE_TYPE.NORMAL_AND_NETWORK_MANAGEMENT])
disable_ecu(can_recv, can_send, bus=CanBus(CP).pt, addr=0x18DAB0F1, com_cont_req=communication_control)
@staticmethod @staticmethod
def deinit(CP, can_recv, can_send): def deinit(CP, can_recv, can_send):
communication_control = bytes([uds.SERVICE_TYPE.COMMUNICATION_CONTROL, 0x80 | uds.CONTROL_TYPE.ENABLE_RX_ENABLE_TX, communication_control = bytes([uds.SERVICE_TYPE.COMMUNICATION_CONTROL, 0x80 | uds.CONTROL_TYPE.ENABLE_RX_ENABLE_TX,
uds.MESSAGE_TYPE.NORMAL_AND_NETWORK_MANAGEMENT]) uds.MESSAGE_TYPE.NORMAL_AND_NETWORK_MANAGEMENT])
CarInterface.init(CP, can_recv, can_send, communication_control) CarInterface.init(CP, None, can_recv, can_send, communication_control)

View File

@@ -0,0 +1,166 @@
from iqdbc.car import DT_CTRL, gen_empty_fingerprint, structs
from iqdbc.car.honda.interface import CarInterface
from iqdbc.car.honda.values import CAR
CANFD_CAR = CAR.HONDA_CRV_6G
RADAR_DIAG_ADDR = 0x18DAB0F1
ACC_CONTROL_ADDR = 0x1DF
ACC_HUD_ADDR = 0x30C
SCM_BUTTONS_ADDR = 0x296
RADAR_HUD_ADDR = 0x310
LANE_PATH_ADDR = 0x6CD5558
HUD_OBJECTS_ADDR = 0x6CD5559
RADAR_LEAD_ADDR = 0xF31AA5C
RADAR_LEAD2_ADDR = 0xF31AA52
SUPPLEMENTAL_ADDR = 0x1A45AA4E
LOOKALIKE_ADDRS = (RADAR_HUD_ADDR, LANE_PATH_ADDR, HUD_OBJECTS_ADDR, RADAR_LEAD_ADDR, RADAR_LEAD2_ADDR, SUPPLEMENTAL_ADDR)
EXT_DIAG_SESSION = b'\x02\x10\x03\x00\x00\x00\x00\x00'
COMM_CONTROL_DISABLE = b'\x03\x28\x83\x03\x00\x00\x00\x00'
def build_long_interface():
fingerprint = gen_empty_fingerprint()
CP = CarInterface.get_params(CANFD_CAR, fingerprint, [], False, False, False)
CP.openpilotLongitudinalControl = True
CP.pcmCruise = False
CP_IQ = CarInterface.get_params_iq(CP, CANFD_CAR, fingerprint, [], False, False, False)
return CarInterface(CP, CP_IQ)
def make_cc(enabled=True):
CC = structs.CarControl()
CC.enabled = enabled
CC.latActive = enabled
CC.longActive = enabled
return CC.as_reader()
class CanfdControllerHarness:
def __init__(self):
self.ci = build_long_interface()
self.cs = self.ci.CS
self.ci.update([])
self.now_nanos = 0
self.set_radar(alive=True, relay_open=False)
self.set_ticks()
def set_radar(self, alive, relay_open):
self.cs.stock_acc_alive = alive
self.cs.canfd_relay_open = relay_open
def set_ticks(self, hud=False, supp=False, five=False, fifty=False):
self.cs.hud_tick = hud
self.cs.supp_tick = supp
self.cs.radar_5hz_tick = five
self.cs.radar_50hz_tick = fifty
def step(self, CC=None, model=None):
self.now_nanos += int(DT_CTRL * 1e9)
_, can_sends = self.ci.apply(CC or make_cc(), structs.IQCarControl(), self.now_nanos, model)
return can_sends
@staticmethod
def by_addr(can_sends, addr):
return [m for m in can_sends if m[0] == addr]
class TestCanfdDeferredRadarDisable:
def setup_method(self):
self.h = CanfdControllerHarness()
def test_no_disable_requests_before_relay_open(self):
for _ in range(20):
sends = self.h.step()
assert not self.h.by_addr(sends, RADAR_DIAG_ADDR)
assert not self.h.by_addr(sends, ACC_CONTROL_ADDR)
assert not any(self.h.by_addr(sends, a) for a in LOOKALIKE_ADDRS)
def test_disable_handshake_after_relay_open(self):
self.h.set_radar(alive=True, relay_open=True)
payloads = []
for _ in range(101):
for msg in self.h.by_addr(self.h.step(), RADAR_DIAG_ADDR):
payloads.append(msg[1])
assert payloads == [EXT_DIAG_SESSION, COMM_CONTROL_DISABLE, EXT_DIAG_SESSION, COMM_CONTROL_DISABLE, EXT_DIAG_SESSION]
def test_tester_present_keeps_radar_down_once_silent(self):
self.h.set_radar(alive=False, relay_open=True)
payloads = []
for _ in range(60):
payloads += [m[1] for m in self.h.by_addr(self.h.step(), RADAR_DIAG_ADDR)]
assert payloads == [b'\x02\x3E\x80\x00\x00\x00\x00\x00'] * 6
class TestCanfdReplacementStream:
def setup_method(self):
self.h = CanfdControllerHarness()
self.h.set_radar(alive=False, relay_open=True)
def test_acc_control_every_second_frame(self):
seen = [bool(self.h.by_addr(self.h.step(), ACC_CONTROL_ADDR)) for _ in range(10)]
assert sum(seen) == 5
def test_no_acc_control_while_stock_alive(self):
self.h.set_radar(alive=True, relay_open=True)
for _ in range(10):
assert not self.h.by_addr(self.h.step(), ACC_CONTROL_ADDR)
def test_lookalikes_mirrored_byte_identical_on_both_buses(self):
self.h.set_ticks(hud=True, supp=True, five=True, fifty=True)
sends = self.h.step()
for addr in LOOKALIKE_ADDRS:
msgs = self.h.by_addr(sends, addr)
assert len(msgs) == 2, hex(addr)
buses = sorted(m[2] for m in msgs)
assert buses == [0, 2], hex(addr)
assert msgs[0][1] == msgs[1][1], hex(addr)
def test_no_lookalikes_without_ticks(self):
sends = self.h.step()
for addr in (RADAR_HUD_ADDR, RADAR_LEAD_ADDR, RADAR_LEAD2_ADDR, SUPPLEMENTAL_ADDR, LANE_PATH_ADDR, HUD_OBJECTS_ADDR):
assert not self.h.by_addr(sends, addr)
def test_mux_sweep_contiguous_across_banks(self):
self.h.set_ticks(fifty=True)
muxes = []
for _ in range(45):
msgs = self.h.by_addr(self.h.step(), LANE_PATH_ADDR)
muxes.append(msgs[0][1][0] >> 2)
sweep = list(range(1, 11)) + list(range(17, 27)) + list(range(33, 43)) + list(range(49, 59))
assert muxes == (sweep + sweep)[:45]
def test_acc_hud_rides_hud_tick(self):
assert not self.h.by_addr(self.h.step(), ACC_HUD_ADDR)
self.h.set_ticks(hud=True)
assert self.h.by_addr(self.h.step(), ACC_HUD_ADDR)
self.h.set_ticks()
assert not self.h.by_addr(self.h.step(), ACC_HUD_ADDR)
class TestCanfdButtonTakeover:
def setup_method(self):
self.h = CanfdControllerHarness()
self.h.set_radar(alive=False, relay_open=True)
def test_buttons_streamed_to_camera_while_engaged(self):
seen = 0
for _ in range(20):
for msg in self.h.by_addr(self.h.step(), SCM_BUTTONS_ADDR):
assert msg[2] == 2
seen += 1
assert seen == 5
def test_no_button_stream_when_disengaged(self):
for _ in range(20):
assert not self.h.by_addr(self.h.step(make_cc(enabled=False)), SCM_BUTTONS_ADDR)
def test_ambient_light_echoed(self):
self.h.cs.scm_ambient_light = 0x77
for _ in range(4):
msgs = self.h.by_addr(self.h.step(), SCM_BUTTONS_ADDR)
if msgs:
assert msgs[0][1][2] == 0x77
return
raise AssertionError("no SCM_BUTTONS takeover frame seen")

View File

@@ -0,0 +1,202 @@
import pytest
from iqdbc.can import CANPacker
from iqdbc.car import Bus, DT_CTRL, gen_empty_fingerprint
from iqdbc.car.honda.interface import CarInterface
from iqdbc.car.honda.values import CAR, DBC
from iqdbc.car.common.conversions import Conversions as CV
CANFD_CAR = CAR.HONDA_CRV_6G
RADARLESS_CAR = CAR.HONDA_CIVIC_2022
CAMERA_MESSAGES_ADDR = 0x35E
def build_car(candidate, extra_pt_addrs=()):
fingerprint = gen_empty_fingerprint()
for addr in extra_pt_addrs:
fingerprint[0][addr] = 8
CP = CarInterface.get_params(candidate, fingerprint, [], False, False, False)
CP_IQ = CarInterface.get_params_iq(CP, candidate, fingerprint, [], False, False, False)
return CarInterface(CP, CP_IQ)
class CanFeed:
def __init__(self, ci, dbc_name):
self.ci = ci
self.packer = CANPacker(dbc_name)
self.nanos = 0
# the first CarState.update lazily subscribes vl-read messages, so run one empty
# cycle before feeding data or the first fed frame of those messages is dropped
self.step()
self.ci.CS.update(self.ci.can_parsers)
def step(self, msgs=()):
self.nanos += int(DT_CTRL * 1e9)
packed = [self.packer.make_can_msg(name, bus, values) for name, bus, values in msgs]
for parser in self.ci.can_parsers.values():
parser.update([self.nanos, packed])
class TestHondaCanfdRadarState:
def setup_method(self):
self.ci = build_car(CANFD_CAR)
self.cs = self.ci.CS
self.feed = CanFeed(self.ci, DBC[CANFD_CAR][Bus.pt])
def update(self, msgs=()):
self.feed.step(msgs)
return self.cs.update(self.ci.can_parsers)
def test_parsers_include_radar_bus(self):
assert Bus.radar in self.ci.can_parsers
assert self.ci.can_parsers[Bus.radar].bus == 1
def test_50hz_tick_fires_one_frame_before_next_tick(self):
ticks = []
for frame in range(20):
msgs = [("RADAR_50HZ_TICK_REFERENCE", 1, {})] if frame % 2 == 0 else []
self.update(msgs)
ticks.append(self.cs.radar_50hz_tick)
assert ticks[2:] == [frame % 2 == 1 for frame in range(2, 20)]
def test_hud_tick_fires_one_frame_before_next_tick(self):
fired = []
for frame in range(40):
msgs = [("RADAR_HUD_TICK_REFERENCE", 1, {})] if frame % 10 == 0 else []
self.update(msgs)
if self.cs.hud_tick:
fired.append(frame)
assert fired == [9, 19, 29, 39]
def test_5hz_tick_fires_at_stock_radar_lead_offset(self):
fired = []
for frame in range(60):
msgs = [("RADAR_REFERENCE", 0, {})] if frame % 20 == 0 else []
self.update(msgs)
if self.cs.radar_5hz_tick:
fired.append(frame)
assert fired == [11, 31, 51]
def test_stock_acc_alive_until_four_silent_frames(self):
for frame in range(11):
msgs = [("ACC_CONTROL", 0, {})] if frame % 2 == 0 else []
self.update(msgs)
assert self.cs.stock_acc_alive
silent_state = []
for _ in range(6):
self.update()
silent_state.append(self.cs.stock_acc_alive)
assert silent_state == [True, True, True, False, False, False]
self.update([("ACC_CONTROL", 0, {})])
assert self.cs.stock_acc_alive
def test_relay_open_when_camera_steering_disappears(self):
for _ in range(10):
self.update([("STEERING_CONTROL", 0, {})])
assert not self.cs.canfd_relay_open
assert self.cs.camera_steer_seen
open_state = []
for _ in range(7):
self.update()
open_state.append(self.cs.canfd_relay_open)
assert open_state == [False, False, False, False, True, True, True]
def test_relay_open_fallback_without_camera(self):
primed_frames = self.cs.canfd_frames
for frame in range(510):
self.update()
assert self.cs.canfd_relay_open == (primed_frames + frame + 1 >= 500)
def test_ambient_light_echoed_from_scm_buttons(self):
self.update([("SCM_BUTTONS", 0, {"AMBIENT_LIGHT_MAYBE": 0x5A})])
assert self.cs.scm_ambient_light == 0x5A
class TestHondaNonCanfdRadarState:
def test_no_radar_parser_and_ticks_stay_low(self):
ci = build_car(RADARLESS_CAR)
feed = CanFeed(ci, DBC[RADARLESS_CAR][Bus.pt])
assert Bus.radar not in ci.can_parsers
for _ in range(5):
feed.step()
ci.CS.update(ci.can_parsers)
assert not ci.CS.radar_50hz_tick
assert not ci.CS.hud_tick
assert not ci.CS.supp_tick
assert not ci.CS.radar_5hz_tick
class TestCanfdLongInterface:
def test_alpha_long_available_on_canfd(self):
CP = CarInterface.get_params(CANFD_CAR, gen_empty_fingerprint(), [], False, False, False)
assert CP.alphaLongitudinalAvailable
assert not CP.openpilotLongitudinalControl
assert CP.pcmCruise
def test_alpha_long_enabled_on_canfd(self):
CP = CarInterface.get_params(CANFD_CAR, gen_empty_fingerprint(), [], True, False, False)
assert CP.openpilotLongitudinalControl
assert not CP.pcmCruise
assert CP.longitudinalActuatorDelay == pytest.approx(0.05)
def test_canfd_long_init_clears_dtcs_without_disabling_radar(self, mocker):
clear_all = mocker.patch("iqdbc.car.honda.interface.clear_all_dtcs")
clear_ecu = mocker.patch("iqdbc.car.honda.interface.clear_ecu_dtcs")
disable = mocker.patch("iqdbc.car.honda.interface.disable_ecu")
CP = CarInterface.get_params(CANFD_CAR, gen_empty_fingerprint(), [], True, False, False)
CarInterface.init(CP, None, None, None)
assert clear_all.call_count == 1
assert clear_all.call_args.args[1] == [0, 2]
assert clear_ecu.call_count == 1
assert disable.call_count == 0
def test_canfd_deinit_reenables_radar(self, mocker):
clear_all = mocker.patch("iqdbc.car.honda.interface.clear_all_dtcs")
disable = mocker.patch("iqdbc.car.honda.interface.disable_ecu")
CP = CarInterface.get_params(CANFD_CAR, gen_empty_fingerprint(), [], True, False, False)
CarInterface.deinit(CP, None, None)
assert clear_all.call_count == 0
assert disable.call_count == 1
def test_bosch_a_long_init_still_disables_radar(self, mocker):
clear_all = mocker.patch("iqdbc.car.honda.interface.clear_all_dtcs")
disable = mocker.patch("iqdbc.car.honda.interface.disable_ecu")
CP = CarInterface.get_params(CAR.HONDA_ACCORD, gen_empty_fingerprint(), [], True, False, False)
CarInterface.init(CP, None, None, None)
assert clear_all.call_count == 0
assert disable.call_count == 1
class TestHondaDashboardSpeedLimit:
def build(self, candidate, with_camera_messages):
extra = (CAMERA_MESSAGES_ADDR,) if with_camera_messages else ()
return build_car(candidate, extra_pt_addrs=extra)
@pytest.mark.parametrize("sign_value,expected_mph", [(101, 25), (97, 5), (113, 85)])
def test_speed_limit_sign_reported(self, sign_value, expected_mph):
ci = self.build(RADARLESS_CAR, True)
feed = CanFeed(ci, DBC[RADARLESS_CAR][Bus.pt])
feed.step([("CAMERA_MESSAGES", 2, {"SPEED_LIMIT_SIGN": sign_value})])
_, ret_iq = ci.CS.update(ci.can_parsers)
assert ret_iq.speedLimit == pytest.approx(expected_mph * CV.MPH_TO_MS)
@pytest.mark.parametrize("sign_value", [125, 0, 32])
def test_invalid_sign_reports_no_limit(self, sign_value):
ci = self.build(RADARLESS_CAR, True)
feed = CanFeed(ci, DBC[RADARLESS_CAR][Bus.pt])
feed.step([("CAMERA_MESSAGES", 2, {"SPEED_LIMIT_SIGN": sign_value})])
_, ret_iq = ci.CS.update(ci.can_parsers)
assert ret_iq.speedLimit == 0.0
def test_without_camera_messages_flag_no_limit(self):
ci = self.build(RADARLESS_CAR, False)
feed = CanFeed(ci, DBC[RADARLESS_CAR][Bus.pt])
feed.step([("CAMERA_MESSAGES", 2, {"SPEED_LIMIT_SIGN": 101})])
_, ret_iq = ci.CS.update(ci.can_parsers)
assert ret_iq.speedLimit == 0.0

View File

@@ -0,0 +1,235 @@
import math
from types import SimpleNamespace
import numpy as np
from iqdbc.can import CANPacker
from iqdbc.car.honda import dash_lane, dash_objects
V_EGO = 30.0
def model_at(center_y):
x = list(np.linspace(0.0, 110.0, 23))
def line(y):
return SimpleNamespace(x=x, y=[y] * len(x))
return SimpleNamespace(laneLines=[line(center_y + 3.3), line(center_y + 1.65), line(center_y - 1.65), line(center_y - 3.3)],
laneLineProbs=[0.0, 1.0, 1.0, 0.0],
leadsV3=[])
def lane_xy(center_y):
m = model_at(center_y)
return m.laneLines[1].x, [(a + b) / 2.0 for a, b in zip(m.laneLines[1].y, m.laneLines[2].y, strict=True)]
class TestLanePathSlew:
def test_first_fit_shown_unslewed(self):
renderer = dash_lane.LanePathRenderer()
lane = renderer.update(model_at(-2.0), V_EGO, 0.0)
assert lane.offsets == dash_lane.encode_lane_path(*lane_xy(-2.0))
def test_step_is_rate_limited(self):
renderer = dash_lane.LanePathRenderer()
prev = renderer.update(model_at(0.0), V_EGO, 0.0).offsets
assert all(o == 0 for o in prev)
target = dash_lane.encode_lane_path(*lane_xy(-2.0))
max_step = math.ceil(dash_lane.SLEW_MAX_STEP)
for _ in range(10):
cur = renderer.update(model_at(-2.0), V_EGO, 0.0).offsets
for p, c, t in zip(prev, cur, target, strict=True):
assert abs(c - p) <= max_step
assert abs(t - c) <= abs(t - p)
prev = cur
assert prev == target
def test_full_scale_takes_two_seconds(self):
renderer = dash_lane.LanePathRenderer()
renderer.update(model_at(0.0), V_EGO, 0.0)
target = dash_lane.encode_lane_path(*lane_xy(-100.0))
assert all(t == dash_lane.OFFSET_VALID_MAX for t in target)
n_updates = round(dash_lane.SLEW_FULL_SCALE_S * dash_lane.SLEW_RATE_HZ)
for i in range(n_updates):
lane = renderer.update(model_at(-100.0), V_EGO, 0.0)
if i < n_updates - 1:
assert lane.offsets != target
assert lane.offsets == target
def test_blank_resets_slew(self):
renderer = dash_lane.LanePathRenderer()
renderer.update(model_at(0.0), V_EGO, 0.0)
lane = renderer.update(None, V_EGO, 0.0)
assert lane.offsets == [dash_lane.OFFSET_UNAVAILABLE] * dash_lane.POINT_COUNT
lane = renderer.update(model_at(-2.0), V_EGO, 0.0)
assert lane.offsets == dash_lane.encode_lane_path(*lane_xy(-2.0))
def test_short_path_passthrough_and_reset(self):
renderer = dash_lane.LanePathRenderer()
renderer.update(model_at(0.0), V_EGO, 0.0)
short = model_at(-2.0)
for ll in short.laneLines:
ll.x = ll.x[:10]
ll.y = ll.y[:10]
lane = renderer.update(short, V_EGO, 0.0)
assert lane.offsets == [dash_lane.OFFSET_UNAVAILABLE] * dash_lane.POINT_COUNT
lane = renderer.update(model_at(-2.0), V_EGO, 0.0)
assert lane.offsets == dash_lane.encode_lane_path(*lane_xy(-2.0))
class TestLaneLineHysteresis:
def test_single_line_offset_and_hysteresis(self):
renderer = dash_lane.LanePathRenderer()
m = model_at(0.0)
m.laneLineProbs = [0.0, 0.0, 1.0, 0.0]
lane = renderer.update(m, V_EGO, 0.0)
assert not lane.left_line and lane.right_line
assert lane.offsets == dash_lane.encode_lane_path(m.laneLines[2].x, [y - dash_lane.HALF_LANE_M for y in m.laneLines[2].y])
# a left prob between OFF and ON must not switch the left line on
m.laneLineProbs = [0.0, (dash_lane.LINE_PROB_OFF + dash_lane.LINE_PROB_ON) / 2, 1.0, 0.0]
lane = renderer.update(m, V_EGO, 0.0)
assert not lane.left_line
# once on, the same mid prob keeps it on
m.laneLineProbs = [0.0, dash_lane.LINE_PROB_ON, 1.0, 0.0]
assert renderer.update(m, V_EGO, 0.0).left_line
m.laneLineProbs = [0.0, (dash_lane.LINE_PROB_OFF + dash_lane.LINE_PROB_ON) / 2, 1.0, 0.0]
assert renderer.update(m, V_EGO, 0.0).left_line
class TestCanfdReshape:
def test_idle_pattern_when_blank(self):
assert dash_lane.canfd_lane_offsets(dash_lane.RenderedLane()) == dash_lane.CANFD_IDLE_OFFSETS
assert dash_lane.canfd_lane_length(dash_lane.RenderedLane()) == dash_lane.CANFD_MIN_VALID_PTS
def test_terminated_prefix_matches_length_law(self):
for v_ego, expected in ((0.0, 7), (10.0, 15), (19.0, 23), (38.0, 23)):
lane = dash_lane.RenderedLane(offsets=[5] * dash_lane.POINT_COUNT, reach=1.0, v_ego=v_ego)
n = dash_lane.canfd_lane_length(lane)
assert n == expected
offs = dash_lane.canfd_lane_offsets(lane)
assert offs[:n] == [5] * n
assert offs[n:] == [dash_lane.OFFSET_UNAVAILABLE] * (dash_lane.POINT_COUNT - n)
class TestMuxMapping:
def test_mux_cycle_covers_all_banks(self):
assert len(dash_lane.MUX_CYCLE) == 40
assert set(dash_lane.MUX_CYCLE) == set(range(1, 11)) | set(range(17, 27)) | set(range(33, 43)) | set(range(49, 59))
def test_lane_path_frame_selects_offsets_by_mux(self):
packer = CANPacker("honda_bosch_radarless_generated")
offsets = list(range(40))
for mux in dash_lane.MUX_CYCLE:
addr, dat, bus = dash_lane.create_lane_path(packer, 0, offsets, mux)
base = ((mux - 1) % 16) * 4
raw_mux = dat[0] >> 2
assert raw_mux == mux
assert base < 40
class TestDashObjectAuthor:
def make_lead(self, prob=0.9, d=30.0, y=0.0, v=0.0):
status = prob >= dash_objects.LEAD_PROB_ON
return dash_objects.ModelLead(status, d, y, v, prob=prob)
def payload(self, msg):
return msg[1]
def test_inactive_slot_bytes_match_stock_sentinel(self):
packer = CANPacker("honda_common_canfd_generated")
author = dash_objects.DashObjectAuthor()
msg = author.create(packer, 0, self.make_lead(prob=0.0), None, 2, 0.0)
parsed_long = ((self.payload(msg)[4] << 2) | (self.payload(msg)[5] >> 6)) & 0x3FF
assert parsed_long == 1023
def test_lead_rendered_in_slot0_only(self):
packer = CANPacker("honda_common_canfd_generated")
author = dash_objects.DashObjectAuthor()
lead = self.make_lead()
slot0 = author.create(packer, 0, lead, None, 1, 0.0)
slot3 = author.create(packer, 0, lead, None, 4, 0.02)
assert self.payload(slot0)[1] != 0
assert self.payload(slot3)[1] & 0xF8 == 0
def test_lead_prob_hysteresis_and_hold(self):
packer = CANPacker("honda_common_canfd_generated")
author = dash_objects.DashObjectAuthor()
now = 0.0
def object_id(prob):
nonlocal now
now += 0.02
msg = author.create(packer, 0, self.make_lead(prob=prob), None, 1, now)
return self.payload(msg)[1] >> 3
assert object_id(0.6) != 0
# dips below ON but above OFF keep rendering
assert object_id(0.4) != 0
# a full drop is bridged for LEAD_HOLD_S
assert object_id(0.0) != 0
now += dash_objects.LEAD_HOLD_S
assert object_id(0.0) == 0
def test_reid_on_range_discontinuity(self):
ident = dash_objects.LeadIdentity()
now = 0.0
first = ident.update(True, 30.0, 0.0, now)
# stay steady past the re-id refractory window
for _ in range(int(dash_objects.REID_REFRACTORY / 0.02) + 10):
now += 0.02
same = ident.update(True, 30.0, 0.0, now)
assert same == first
now += 0.02
assert ident.update(True, 60.0, 0.0, now) != first
def test_camera_lead_never_forwarded(self):
packer = CANPacker("honda_bosch_radarless_generated")
author = dash_objects.DashObjectAuthor()
tracks = [dash_objects.CameraObject(slot=i, object_id=0, d_rel=0.0, y_rel=0.0, is_lead_car=False, valid=False)
for i in range(dash_objects.NUM_SLOTS)]
tracks[0] = dash_objects.CameraObject(slot=0, object_id=9, d_rel=40.0, y_rel=0.0, is_lead_car=True, valid=True,
car_type=7, rotation=0)
msg = author.create(packer, 0, self.make_lead(prob=0.0), tracks, 1, 0.0)
assert self.payload(msg)[1] >> 3 == 0
def test_adjacent_car_forwarded_with_own_mux(self):
packer = CANPacker("honda_bosch_radarless_generated")
tracks = [dash_objects.CameraObject(slot=i, object_id=0, d_rel=0.0, y_rel=0.0, is_lead_car=False, valid=False)
for i in range(dash_objects.NUM_SLOTS)]
tracks[3] = dash_objects.CameraObject(slot=3, object_id=12, d_rel=25.0, y_rel=3.0, is_lead_car=False, valid=True,
car_type=7, rotation=1)
msg = dash_objects.forward_hud_object(packer, 0, 20, tracks)
assert msg[1][0] >> 2 == 20
assert msg[1][1] >> 3 == 12
class TestCameraObjectTracker:
def test_tracks_persist_across_banks(self):
tracker = dash_objects.CameraObjectTracker()
class FakeParser:
vl_all = {"HUD_OBJECTS": {
"MUX": [2, 18], "OBJECT_ID": [5, 5], "LONG_DIST": [30.0, 31.0], "LAT_DIST": [1.0, 1.1],
"IS_LEAD_CAR": [0, 0], "CAR_TYPE": [7, 7], "ROTATION": [0, 0],
}}
tracker.update(FakeParser())
snap = tracker.snapshot()
assert snap[1].valid and snap[1].object_id == 5
assert snap[1].d_rel == 31.0
def test_empty_sentinel_invalid(self):
tracker = dash_objects.CameraObjectTracker()
class FakeParser:
vl_all = {"HUD_OBJECTS": {
"MUX": [1], "OBJECT_ID": [0], "LONG_DIST": [196.9], "LAT_DIST": [204.7],
"IS_LEAD_CAR": [0], "CAR_TYPE": [-1], "ROTATION": [-128],
}}
tracker.update(FakeParser())
assert not tracker.snapshot()[0].valid

View File

@@ -32,7 +32,7 @@ class CarControllerParams:
BOSCH_ACCEL_MIN = -3.5 # m/s^2 BOSCH_ACCEL_MIN = -3.5 # m/s^2
BOSCH_ACCEL_MAX = 2.0 # m/s^2 BOSCH_ACCEL_MAX = 2.0 # m/s^2
BOSCH_GAS_LOOKUP_BP = [-0.2, 2.0] # 2m/s^2 BOSCH_GAS_LOOKUP_BP = [0.0, 2.0] # 2m/s^2
BOSCH_GAS_LOOKUP_V = [0, 1600] BOSCH_GAS_LOOKUP_V = [0, 1600]
STEER_STEP = 1 # 100 Hz STEER_STEP = 1 # 100 Hz
@@ -132,7 +132,7 @@ class HondaBoschPlatformConfig(PlatformConfig):
@dataclass @dataclass
class HondaBoschCANFDPlatformConfig(HondaBoschPlatformConfig): class HondaBoschCANFDPlatformConfig(HondaBoschPlatformConfig):
dbc_dict: DbcDict = field(default_factory=lambda: {Bus.pt: 'honda_common_canfd_generated'}) dbc_dict: DbcDict = field(default_factory=lambda: {Bus.pt: 'honda_common_canfd_generated', Bus.radar: 'honda_common_canfd_generated'})
def init(self): def init(self):
super().init() super().init()

View File

@@ -115,9 +115,12 @@ class CarInterfaceBase(ABC, CarInterfaceBaseIQ):
dbc_names = {bus: cp.dbc_name for bus, cp in self.can_parsers.items()} dbc_names = {bus: cp.dbc_name for bus, cp in self.can_parsers.items()}
self.CC: CarControllerBase = self.CarController(dbc_names, CP, CP_IQ) self.CC: CarControllerBase = self.CarController(dbc_names, CP, CP_IQ)
def apply(self, c: structs.CarControl, c_iq: structs.IQCarControl, now_nanos: int | None = None) -> tuple[structs.CarControl.Actuators, list[CanData]]: def apply(self, c: structs.CarControl, c_iq: structs.IQCarControl, now_nanos: int | None = None,
model=None) -> tuple[structs.CarControl.Actuators, list[CanData]]:
if now_nanos is None: if now_nanos is None:
now_nanos = int(time.monotonic() * 1e9) now_nanos = int(time.monotonic() * 1e9)
# modelV2 for cars that render it on the dash; an attr so every CarController.update keeps its signature
self.CC.model = model
return self.CC.update(c, c_iq, self.CS, now_nanos) return self.CC.update(c, c_iq, self.CS, now_nanos)
@staticmethod @staticmethod
@@ -432,6 +435,7 @@ class CarControllerBase(ABC):
self.CP_IQ = CP_IQ self.CP_IQ = CP_IQ
self.frame = 0 self.frame = 0
self.secoc_key: bytes = b"00" * 16 self.secoc_key: bytes = b"00" * 16
self.model = None
@abstractmethod @abstractmethod
def update(self, CC: structs.CarControl, CC_IQ: structs.IQCarControl, CS: CarStateBase, now_nanos: int) -> tuple[structs.CarControl.Actuators, list[CanData]]: def update(self, CC: structs.CarControl, CC_IQ: structs.IQCarControl, CS: CarStateBase, now_nanos: int) -> tuple[structs.CarControl.Actuators, list[CanData]]:

View File

@@ -0,0 +1,93 @@
from iqdbc.car.can_definitions import CanData
from iqdbc.car.disable_ecu import (CLEAR_DTC_ISOTP_SF, CLEAR_DTC_REQUEST, EXT_DIAG_REQUEST,
FUNCTIONAL_ADDR_29BIT, clear_all_dtcs, clear_ecu_dtcs, disable_ecu)
RADAR_ADDR = 0x18DAB0F1
COM_CONT_REQUEST = b'\x28\x83\x03'
class QueryRecorder:
def __init__(self):
self.requests = []
def make_fake_query(self):
recorder = self
class FakeIsoTpParallelQuery:
def __init__(self, can_send, can_recv, bus, addrs, requests, responses, response_offset=0x8):
self.bus = bus
self.addrs = addrs
self.request = requests[0]
recorder.requests.append((bus, addrs[0][0], requests[0]))
def get_data(self, timeout):
return {(self.addrs[0][0], None): b''}
return FakeIsoTpParallelQuery
def test_clear_all_dtcs_broadcasts_single_frame():
sent = []
clear_all_dtcs(lambda msgs: sent.extend(msgs), [0, 2])
assert sent == [
CanData(FUNCTIONAL_ADDR_29BIT, CLEAR_DTC_ISOTP_SF, 0),
CanData(FUNCTIONAL_ADDR_29BIT, CLEAR_DTC_ISOTP_SF, 2),
]
def test_clear_dtc_isotp_framing():
assert len(CLEAR_DTC_ISOTP_SF) == 8
assert CLEAR_DTC_ISOTP_SF[0] == len(CLEAR_DTC_REQUEST)
assert CLEAR_DTC_ISOTP_SF[1:1 + len(CLEAR_DTC_REQUEST)] == CLEAR_DTC_REQUEST
assert CLEAR_DTC_REQUEST == b'\x14\xff\xff\xff'
def test_clear_ecu_dtcs_sequence(mocker):
recorder = QueryRecorder()
mocker.patch("iqdbc.car.disable_ecu.IsoTpParallelQuery", recorder.make_fake_query())
assert clear_ecu_dtcs(None, None, bus=0, addr=RADAR_ADDR)
assert recorder.requests == [
(0, RADAR_ADDR, EXT_DIAG_REQUEST),
(0, RADAR_ADDR, CLEAR_DTC_REQUEST),
]
def test_disable_ecu_sequence(mocker):
recorder = QueryRecorder()
mocker.patch("iqdbc.car.disable_ecu.IsoTpParallelQuery", recorder.make_fake_query())
assert disable_ecu(None, None, bus=1, addr=RADAR_ADDR, com_cont_req=COM_CONT_REQUEST)
assert recorder.requests == [
(1, RADAR_ADDR, EXT_DIAG_REQUEST),
(1, RADAR_ADDR, COM_CONT_REQUEST),
]
def test_disable_ecu_clears_dtcs_before_comm_control(mocker):
recorder = QueryRecorder()
mocker.patch("iqdbc.car.disable_ecu.IsoTpParallelQuery", recorder.make_fake_query())
assert disable_ecu(None, None, bus=1, addr=RADAR_ADDR, com_cont_req=COM_CONT_REQUEST, clear_dtc=True)
assert recorder.requests == [
(1, RADAR_ADDR, EXT_DIAG_REQUEST),
(1, RADAR_ADDR, CLEAR_DTC_REQUEST),
(1, RADAR_ADDR, COM_CONT_REQUEST),
]
def test_disable_ecu_retries_then_fails(mocker):
attempts = []
class NoResponseQuery:
def __init__(self, can_send, can_recv, bus, addrs, requests, responses, response_offset=0x8):
attempts.append(requests[0])
def get_data(self, timeout):
return {}
mocker.patch("iqdbc.car.disable_ecu.IsoTpParallelQuery", NoResponseQuery)
assert not disable_ecu(None, None, addr=RADAR_ADDR, retry=3)
assert attempts == [EXT_DIAG_REQUEST] * 3

View File

@@ -128,6 +128,7 @@ BO_ 586 ADJACENT_RIGHT_LANE_LINE_2: 8 CAM
BO_ 662 SCM_BUTTONS: 4 SCM BO_ 662 SCM_BUTTONS: 4 SCM
SG_ CRUISE_BUTTONS : 7|3@0+ (1,0) [0|7] "" EON SG_ CRUISE_BUTTONS : 7|3@0+ (1,0) [0|7] "" EON
SG_ CRUISE_SETTING : 3|2@0+ (1,0) [0|3] "" EON SG_ CRUISE_SETTING : 3|2@0+ (1,0) [0|3] "" EON
SG_ AMBIENT_LIGHT_MAYBE : 23|8@0+ (1,0) [0|255] "" EON
SG_ COUNTER : 29|2@0+ (1,0) [0|3] "" EON SG_ COUNTER : 29|2@0+ (1,0) [0|3] "" EON
SG_ CHECKSUM : 27|4@0+ (1,0) [0|15] "" EON SG_ CHECKSUM : 27|4@0+ (1,0) [0|15] "" EON
@@ -205,6 +206,7 @@ BO_ 13275 LKAS_HUD_B: 8 ADAS
SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" BDY SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" BDY
CM_ SG_ 228 DRIVER_OVERRIDE "Appears to acknowledge STEER_STATUS.NO_TORQUE_ALERT_1"; CM_ SG_ 228 DRIVER_OVERRIDE "Appears to acknowledge STEER_STATUS.NO_TORQUE_ALERT_1";
CM_ SG_ 662 AMBIENT_LIGHT_MAYBE "Slow-moving sensor value (possibly the ambient light input for adaptive high beam), undecoded. The camera consumes SCM_BUTTONS content beyond the buttons, so frames sent in its place must echo this byte";
CM_ SG_ 576 LINE_DISTANCE_VISIBLE "Length of line visible, undecoded"; CM_ SG_ 576 LINE_DISTANCE_VISIBLE "Length of line visible, undecoded";
CM_ SG_ 577 LINE_FAR_EDGE_POSITION "Appears to be a measure of line thickness, indicates location of the portion of the line furthest from the car, undecoded"; CM_ SG_ 577 LINE_FAR_EDGE_POSITION "Appears to be a measure of line thickness, indicates location of the portion of the line furthest from the car, undecoded";
CM_ SG_ 577 LINE_PARAMETER "Unclear if this is low quality line curvature rate or if this is something else, but it is correlated with line curvature, undecoded"; CM_ SG_ 577 LINE_PARAMETER "Unclear if this is low quality line curvature rate or if this is something else, but it is correlated with line curvature, undecoded";

View File

@@ -21,9 +21,71 @@ BO_ 829 LKAS_HUD: 8 ADAS
SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX
SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX
BO_ 254913108 LKAS_HUD_2: 8 ADAS
SG_ COUNTER_2 : 7|2@0+ (1,0) [0|3] "" XXX
SG_ SET_ME_X01 : 5|1@0+ (1,0) [0|1] "" XXX
SG_ LANE_WIDTH : 15|6@0+ (1,0) [0|63] "" XXX
SG_ LEFT_LANE : 23|2@0+ (1,0) [0|3] "" XXX
SG_ RIGHT_LANE : 21|2@0+ (1,0) [0|3] "" XXX
SG_ LEFT_LANE_CROSSED : 25|1@0+ (1,0) [0|1] "" XXX
SG_ RIGHT_LANE_CROSSED : 24|1@0+ (1,0) [0|1] "" XXX
SG_ LANE_LENGTH : 31|6@0+ (1,0) [0|63] "" XXX
SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX
SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX
BO_ 114120023 HUD_OBJECTS: 8 CAM
SG_ MUX M : 7|6@0+ (1,0) [0|63] "" XXX
SG_ OBJECT_ID m1 : 15|5@0+ (1,0) [0|31] "" XXX
SG_ IS_LEAD_CAR m1 : 17|1@0+ (1,0) [0|1] "" XXX
SG_ CAR_TYPE m1 : 23|4@0- (1,0) [-8|7] "" XXX
SG_ ROTATION m1 : 31|8@0- (1,0) [-128|127] "" XXX
SG_ LONG_DIST m1 : 39|10@0+ (0.209,-16.9) [-16.9|196.9] "m" XXX
SG_ LAT_DIST m1 : 43|12@0- (0.1,0) [-204.8|204.7] "m" XXX
SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX
SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX
BO_ 114120025 HUD_OBJECTS_B: 8 XXX
SG_ MUX M : 7|6@0+ (1,0) [0|63] "" XXX
SG_ OBJECT_ID m1 : 15|5@0+ (1,0) [0|31] "" XXX
SG_ IS_LEAD_CAR m1 : 17|1@0+ (1,0) [0|1] "" XXX
SG_ CAR_TYPE m1 : 23|4@0- (1,0) [-8|7] "" XXX
SG_ ROTATION m1 : 31|8@0- (1,0) [-128|127] "" XXX
SG_ LONG_DIST m1 : 39|10@0+ (0.209,-16.9) [-16.9|196.9] "m" XXX
SG_ LAT_DIST m1 : 43|12@0- (0.1,0) [-204.8|204.7] "m" XXX
SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX
SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX
BO_ 114120020 LANE_PATH: 8 CAM
SG_ MUX M : 7|6@0+ (1,0) [0|63] "" XXX
SG_ PATH_OFFSET_1 m1 : 15|12@0- (1,0) [-2048|2047] "" XXX
SG_ PATH_OFFSET_2 m1 : 19|12@0- (1,0) [-2048|2047] "" XXX
SG_ PATH_OFFSET_3 m1 : 39|12@0- (1,0) [-2048|2047] "" XXX
SG_ PATH_OFFSET_4 m1 : 43|12@0- (1,0) [-2048|2047] "" XXX
SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX
SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX
BO_ 114120024 LANE_PATH_B: 8 XXX
SG_ MUX M : 7|6@0+ (1,0) [0|63] "" XXX
SG_ PATH_OFFSET_1 m1 : 15|12@0- (1,0) [-2048|2047] "" XXX
SG_ PATH_OFFSET_2 m1 : 19|12@0- (1,0) [-2048|2047] "" XXX
SG_ PATH_OFFSET_3 m1 : 39|12@0- (1,0) [-2048|2047] "" XXX
SG_ PATH_OFFSET_4 m1 : 43|12@0- (1,0) [-2048|2047] "" XXX
SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX
SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX
CM_ SG_ 829 BEEP "beeps are pleasant, chimes are for warnings etc..."; CM_ SG_ 829 BEEP "beeps are pleasant, chimes are for warnings etc...";
CM_ SG_ 829 CAM_TEMP_HIGH "Some Driver Assist Systems Cannot Operate: Camera Temperature Too High"; CM_ SG_ 829 CAM_TEMP_HIGH "Some Driver Assist Systems Cannot Operate: Camera Temperature Too High";
CM_ SG_ 829 CAMERA_OVERHEAT "Lane Keeping Assist Cannot Operate: Camera Too Hot"; CM_ SG_ 829 CAMERA_OVERHEAT "Lane Keeping Assist Cannot Operate: Camera Too Hot";
CM_ SG_ 114120023 MUX "1-10, 17-26, 33-42, 49-58 map to the same 10 slots for 5Hz frequency each";
CM_ SG_ 114120023 IS_LEAD_CAR "1 when this track is the lead car; the lead, when present, is always track index 1";
CM_ SG_ 114120023 LAT_DIST "positive = left of ego, 204.7 (max value) when inactive";
CM_ SG_ 114120023 ROTATION "0 straight, negative left, positive right. Range of -3 to 3 seen so far. -128 when inactive.";
CM_ SG_ 114120025 MUX "1-10, 17-26, 33-42, 49-58 map to the same 10 slots for 5Hz frequency each";
CM_ SG_ 114120025 IS_LEAD_CAR "1 when this track is the lead car; the lead, when present, is always track index 1";
CM_ SG_ 114120025 LAT_DIST "positive = left of ego, 204.7 (max value) when inactive";
CM_ SG_ 114120025 ROTATION "0 straight, negative left, positive right. Range of -3 to 3 seen so far. -128 when inactive.";
VAL_ 829 BEEP 5 "solid_beep" 4 "double_beep" 3 "single_beep" 2 "triple_beep" 1 "repeated_beep" 0 "no_beep"; VAL_ 829 BEEP 5 "solid_beep" 4 "double_beep" 3 "single_beep" 2 "triple_beep" 1 "repeated_beep" 0 "no_beep";
VAL_ 829 LANE_LINES 7 "both_lines_green" 6 "both_lines_white" 2 "left_line_white" 0 "no_lines"; VAL_ 829 LANE_LINES 7 "both_lines_green" 6 "both_lines_white" 2 "left_line_white" 0 "no_lines";
VAL_ 114120023 CAR_TYPE 7 "CAR" 6 "MOTORCYCLE" -7 "TRUCK" -1 "INACTIVE" 0 "UNKNOWN";
VAL_ 114120025 CAR_TYPE 7 "CAR" 6 "MOTORCYCLE" -7 "TRUCK" -1 "INACTIVE" 0 "UNKNOWN";

View File

@@ -7,7 +7,7 @@ CM_ "IMPORT _gearbox_common.dbc";
BO_ 456 ACC_CONTROL: 8 XXX BO_ 456 ACC_CONTROL: 8 XXX
SG_ ACCEL_COMMAND : 7|12@0- (0.01,0) [0|0] "m/s^2" XXX SG_ ACCEL_COMMAND : 7|12@0- (0.01,0) [0|0] "m/s^2" XXX
SG_ BRAKE_REQUEST : 8|1@0+ (1,0) [0|1] "" XXX SG_ COMPUTER_BRAKE_ASSIST : 8|1@0+ (1,0) [0|1] "" XXX
SG_ STANDSTILL : 9|1@0+ (1,0) [0|1] "" XXX SG_ STANDSTILL : 9|1@0+ (1,0) [0|1] "" XXX
SG_ CONTROL_ON : 10|1@0+ (1,0) [0|1] "" XXX SG_ CONTROL_ON : 10|1@0+ (1,0) [0|1] "" XXX
SG_ BOH : 23|1@0+ (1,0) [0|1] "" XXX SG_ BOH : 23|1@0+ (1,0) [0|1] "" XXX
@@ -34,17 +34,7 @@ BO_ 495 SPEED_LIMIT_DASH_DISPLAY: 8 ADAS
SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX
SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX
BO_ 254913108 LKAS_HUD_2: 8 ADAS CM_ SG_ 456 COMPUTER_BRAKE_ASSIST "on hybrid and alt-brake cars set whenever braking; otherwise allows engine idle stop at a standstill";
SG_ COUNTER_2 : 7|2@0+ (1,0) [0|3] "" XXX
SG_ SET_ME_X01 : 5|1@0+ (1,0) [0|1] "" XXX
SG_ LKAS_BOH_1 : 15|6@0+ (1,0) [0|63] "" XXX
SG_ LEFT_LANE : 23|2@0+ (1,0) [0|3] "" XXX
SG_ RIGHT_LANE : 21|2@0+ (1,0) [0|3] "" XXX
SG_ LKAS_BOH_2 : 30|5@0+ (1,0) [0|31] "" XXX
SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX
SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX
CM_ SG_ 456 IDLESTOP_ALLOW "allows car to turn off engine at a standstill";
CM_ SG_ 456 STANDSTILL "set to 1 when camera requests -4.0 m/s^2"; CM_ SG_ 456 STANDSTILL "set to 1 when camera requests -4.0 m/s^2";
CM_ SG_ 495 SPEED_LIMIT "Defaults to 0xFF if no speed limit found"; CM_ SG_ 495 SPEED_LIMIT "Defaults to 0xFF if no speed limit found";

View File

@@ -5,3 +5,71 @@ CM_ "IMPORT _lkas_hud_8byte.dbc";
CM_ "IMPORT _bosch_standstill.dbc"; CM_ "IMPORT _bosch_standstill.dbc";
CM_ "IMPORT _steering_sensors_a.dbc"; CM_ "IMPORT _steering_sensors_a.dbc";
CM_ "IMPORT _gearbox_common.dbc"; CM_ "IMPORT _gearbox_common.dbc";
BO_ 929 RADAR_REFERENCE: 8 XXX
SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON
SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON
BO_ 784 RADAR_HUD_CANFD: 8 XXX
SG_ SET_ME_X01 : 11|1@0+ (1,0) [0|1] "" XXX
SG_ SET_ME_X01_2 : 48|1@0+ (1,0) [0|1] "" XXX
SG_ CMBS_ENABLED_MAYBE : 53|1@0+ (1,0) [0|1] "" XXX
SG_ ACC_ON : 55|1@0+ (1,0) [0|1] "" XXX
SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON
SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON
BO_ 114120024 LANE_PATH: 8 XXX
SG_ MUX M : 7|6@0+ (1,0) [0|63] "" XXX
SG_ PATH_OFFSET_1 m1 : 15|12@0- (1,0) [-2048|2047] "" XXX
SG_ PATH_OFFSET_2 m1 : 19|12@0- (1,0) [-2048|2047] "" XXX
SG_ PATH_OFFSET_3 m1 : 39|12@0- (1,0) [-2048|2047] "" XXX
SG_ PATH_OFFSET_4 m1 : 43|12@0- (1,0) [-2048|2047] "" XXX
SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX
SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX
BO_ 114120025 HUD_OBJECTS: 8 XXX
SG_ MUX M : 7|6@0+ (1,0) [0|63] "" XXX
SG_ OBJECT_ID m1 : 15|5@0+ (1,0) [0|31] "" XXX
SG_ IS_LEAD_CAR m1 : 17|1@0+ (1,0) [0|1] "" XXX
SG_ CAR_TYPE m1 : 23|4@0- (1,0) [-8|7] "" XXX
SG_ ROTATION m1 : 31|8@0- (1,0) [-128|127] "" XXX
SG_ LONG_DIST m1 : 39|10@0+ (0.209,-16.9) [-16.9|196.9] "m" XXX
SG_ LAT_DIST m1 : 43|12@0- (0.1,0) [-204.8|204.7] "m" XXX
SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX
SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX
BO_ 254913106 RADAR_LEAD2: 8 XXX
SG_ SET_ME_X88 : 7|8@0+ (1,0) [0|255] "" XXX
SG_ SET_ME_X78 : 15|8@0+ (1,0) [0|255] "" XXX
SG_ LEAD_DISTANCE_MAYBE : 23|11@0+ (1,0) [0|2047] "" XXX
SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX
SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX
BO_ 254913116 RADAR_LEAD: 8 XXX
SG_ SET_ME_X01 : 5|1@0+ (1,0) [0|1] "" XXX
SG_ CNTR_REF : 7|2@0+ (1,0) [0|3] "" XXX
SG_ TARGET_SPEED_MAYBE : 15|8@0+ (1,0) [0|255] "" XXX
SG_ LEFT_LANE : 23|2@0+ (1,0) [0|3] "" XXX
SG_ RIGHT_LANE : 21|2@0+ (1,0) [0|3] "" XXX
SG_ LANE_PATH_LENGTH : 31|6@0+ (1,0) [0|63] "" XXX
SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX
SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX
BO_ 440773198 BOSCH_SUPPLEMENTAL_CANFD: 8 XXX
SG_ SET_ME_X01 : 7|8@0+ (1,0) [0|255] "" XXX
SG_ SET_ME_X41 : 23|8@0+ (1,0) [0|255] "" XXX
SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX
SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX
BO_ 1808 RADAR_SUPP_TICK_REFERENCE: 32 XXX
SG_ IGNORE : 11|1@0+ (1,0) [0|1] "" XXX
BO_ 1840 RADAR_HUD_TICK_REFERENCE: 6 XXX
SG_ IGNORE : 11|1@0+ (1,0) [0|1] "" XXX
BO_ 1872 RADAR_50HZ_TICK_REFERENCE: 16 XXX
SG_ IGNORE : 11|1@0+ (1,0) [0|1] "" XXX
CM_ SG_ 254913116 LANE_PATH_LENGTH "number of valid LANE_PATH points in the current sweep (6 = idle/no lane, up to 23-24); the dash needs this to match the in-band 2047 terminator to draw the lane lines";
CM_ SG_ 254913116 LEFT_LANE "3 = left lane line detected, 0 = none; tracks the camera's LKAS_HUD LANE_LINES bit 1 exactly in factory logs. The CAN FD equivalent of radarless LKAS_HUD_2 LEFT_LANE; the dash won't draw the lane lines while both are 0";
CM_ SG_ 254913116 RIGHT_LANE "3 = right lane line detected, 0 = none; tracks the camera's LKAS_HUD LANE_LINES bit 0 exactly in factory logs. The CAN FD equivalent of radarless LKAS_HUD_2 RIGHT_LANE; the dash won't draw the lane lines while both are 0";

View File

@@ -4,6 +4,8 @@ Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed
from enum import StrEnum from enum import StrEnum
from iqdbc.car import Bus, structs from iqdbc.car import Bus, structs
from iqdbc.car.common.conversions import Conversions as CV
from iqdbc.car.honda.values import HONDA_BOSCH, HONDA_BOSCH_CANFD, HONDA_BOSCH_RADARLESS
from iqdbc.can.parser import CANParser from iqdbc.can.parser import CANParser
from iqdbc.lvbs.car.honda.iq_values import HondaFlagsIQ from iqdbc.lvbs.car.honda.iq_values import HondaFlagsIQ
@@ -13,10 +15,15 @@ class IQCarState:
self.CP = CP self.CP = CP
self.CP_IQ = CP_IQ self.CP_IQ = CP_IQ
def update(self, ret: structs.CarState, can_parsers: dict[StrEnum, CANParser]) -> None: def update(self, ret: structs.CarState, ret_iq: structs.IQCarState, can_parsers: dict[StrEnum, CANParser]) -> None:
cp = can_parsers[Bus.pt] cp = can_parsers[Bus.pt]
cp_cam = can_parsers[Bus.cam] cp_cam = can_parsers[Bus.cam]
if self.CP_IQ.flags & HondaFlagsIQ.HAS_CAMERA_MESSAGES:
speed_bus = cp if (self.CP.carFingerprint in (HONDA_BOSCH - HONDA_BOSCH_RADARLESS - HONDA_BOSCH_CANFD)) else cp_cam
speed_limit_raw = speed_bus.vl["CAMERA_MESSAGES"]["SPEED_LIMIT_SIGN"] % 32
ret_iq.speedLimit = speed_limit_raw * 5.0 * CV.MPH_TO_MS if (1 <= speed_limit_raw <= 17) else 0.0
if self.CP_IQ.flags & HondaFlagsIQ.NIDEC_HYBRID: if self.CP_IQ.flags & HondaFlagsIQ.NIDEC_HYBRID:
ret.accFaulted = bool(cp.vl["HYBRID_BRAKE_ERROR"]["BRAKE_ERROR_1"] or cp.vl["HYBRID_BRAKE_ERROR"]["BRAKE_ERROR_2"]) ret.accFaulted = bool(cp.vl["HYBRID_BRAKE_ERROR"]["BRAKE_ERROR_1"] or cp.vl["HYBRID_BRAKE_ERROR"]["BRAKE_ERROR_2"])
ret.stockAeb = bool(cp_cam.vl["BRAKE_COMMAND"]["AEB_REQ_1"] and cp_cam.vl["BRAKE_COMMAND"]["COMPUTER_BRAKE_HYBRID"] > 1e-5) ret.stockAeb = bool(cp_cam.vl["BRAKE_COMMAND"]["AEB_REQ_1"] and cp_cam.vl["BRAKE_COMMAND"]["COMPUTER_BRAKE_HYBRID"] > 1e-5)

View File

@@ -9,10 +9,9 @@ class HondaFlagsIQ(IntFlag):
NIDEC_HYBRID = 1 NIDEC_HYBRID = 1
EPS_MODIFIED = 2 EPS_MODIFIED = 2
HYBRID_ALT_BRAKEHOLD = 4 HYBRID_ALT_BRAKEHOLD = 4
HAS_CAMERA_MESSAGES = 8
class HondaSafetyFlagsIQ: class HondaSafetyFlagsIQ:
NIDEC_HYBRID = 1 NIDEC_HYBRID = 1
GAS_INTERCEPTOR = 2 GAS_INTERCEPTOR = 2

View File

@@ -6,7 +6,7 @@ from types import SimpleNamespace
from iqdbc.can import CANParser from iqdbc.can import CANParser
from iqdbc.car import Bus, gen_empty_fingerprint from iqdbc.car import Bus, gen_empty_fingerprint
from iqdbc.car.structs import CarParams, CarState from iqdbc.car.structs import CarParams, CarState, IQCarState as IQCarStateStruct
from iqdbc.car.car_helpers import interfaces from iqdbc.car.car_helpers import interfaces
from iqdbc.car.honda.values import CAR from iqdbc.car.honda.values import CAR
from iqdbc.lvbs.car.honda.iq_carstate import IQCarState from iqdbc.lvbs.car.honda.iq_carstate import IQCarState
@@ -36,12 +36,13 @@ class TestHondaGasInterceptor:
parser = CANParser("acura_ilx_2016_can_generated", [], 0) parser = CANParser("acura_ilx_2016_can_generated", [], 0)
state = IQCarState(CP, CP_IQ) state = IQCarState(CP, CP_IQ)
ret = CarState() ret = CarState()
ret_iq = IQCarStateStruct()
state.update(ret, {Bus.pt: parser, Bus.cam: parser}) state.update(ret, ret_iq, {Bus.pt: parser, Bus.cam: parser})
assert "GAS_SENSOR" in parser.vl assert "GAS_SENSOR" in parser.vl
assert not ret.gasPressed assert not ret.gasPressed
parser.vl["GAS_SENSOR"]["INTERCEPTOR_GAS"] = 493 parser.vl["GAS_SENSOR"]["INTERCEPTOR_GAS"] = 493
parser.vl["GAS_SENSOR"]["INTERCEPTOR_GAS2"] = 493 parser.vl["GAS_SENSOR"]["INTERCEPTOR_GAS2"] = 493
state.update(ret, {Bus.pt: parser, Bus.cam: parser}) state.update(ret, ret_iq, {Bus.pt: parser, Bus.cam: parser})
assert ret.gasPressed assert ret.gasPressed

View File

@@ -43,6 +43,9 @@ static bool honda_bosch_long = false;
static bool honda_bosch_radarless = false; static bool honda_bosch_radarless = false;
static bool honda_bosch_canfd = false; static bool honda_bosch_canfd = false;
static bool honda_nidec_hybrid = false; static bool honda_nidec_hybrid = false;
// counts down on each stock SCM_BUTTONS rx, topped up on each OP SCM_BUTTONS tx to the camera:
// the stock buttons are only blocked from forwarding while OP's replacement stream is actually flowing
static int honda_op_buttons_fresh = 0;
typedef enum {HONDA_NIDEC, HONDA_BOSCH} HondaHw; typedef enum {HONDA_NIDEC, HONDA_BOSCH} HondaHw;
static HondaHw honda_hw = HONDA_NIDEC; static HondaHw honda_hw = HONDA_NIDEC;
@@ -130,6 +133,11 @@ static void honda_rx_hook(const CANPacket_t *msg) {
// state machine to enter and exit controls for button enabling // state machine to enter and exit controls for button enabling
// 0x1A6 for the ILX, 0x296 for the Civic Touring // 0x1A6 for the ILX, 0x296 for the Civic Touring
if (((msg->addr == 0x1A6U) || (msg->addr == 0x296U)) && (msg->bus == pt_bus)) { if (((msg->addr == 0x1A6U) || (msg->addr == 0x296U)) && (msg->bus == pt_bus)) {
// stock buttons act as the clock for the OP button takeover freshness (see honda_bosch_fwd_hook)
if (honda_op_buttons_fresh > 0) {
honda_op_buttons_fresh--;
}
int button = (msg->data[0] & 0xE0U) >> 5; int button = (msg->data[0] & 0xE0U) >> 5;
int cruise_setting = (msg->data[(msg->addr == 0x296U) ? 0U : 5U] & 0x0CU) >> 2U; int cruise_setting = (msg->data[(msg->addr == 0x296U) ? 0U : 5U] & 0x0CU) >> 2U;
@@ -222,7 +230,7 @@ static bool honda_tx_hook(const CANPacket_t *msg) {
.min_accel = -350, .min_accel = -350,
.zero_accel = 0, .zero_accel = 0,
.max_gas = 2000, .max_gas = 2200,
.inactive_gas = -30000, .inactive_gas = -30000,
}; };
@@ -315,15 +323,36 @@ static bool honda_tx_hook(const CANPacket_t *msg) {
// FORCE CANCEL: safety check only relevant when spamming the cancel button in Bosch HW // FORCE CANCEL: safety check only relevant when spamming the cancel button in Bosch HW
// ensuring that only the cancel button press is sent (VAL 2) when controls are off. // ensuring that only the cancel button press is sent (VAL 2) when controls are off.
// This avoids unintended engagements while still allowing resume spam // This avoids unintended engagements while still allowing resume spam
if ((msg->addr == 0x296U) && !controls_allowed && (msg->bus == bus_buttons)) { // On CAN FD and radarless, buttons are also sent to the camera (bus 2) to take over SCM_BUTTONS
// while engaged, so the same check applies there
const bool is_buttons_bus = (msg->bus == bus_buttons) || ((honda_bosch_canfd || honda_bosch_radarless) && (msg->bus == 2U));
if ((msg->addr == 0x296U) && !controls_allowed && is_buttons_bus) {
if (((msg->data[0] >> 5) & 0x7U) != 2U) { if (((msg->data[0] >> 5) & 0x7U) != 2U) {
tx = false; tx = false;
} }
} }
// OP is streaming SCM_BUTTONS to the camera: block the stock buttons from forwarding while this
// stream stays fresh (see honda_bosch_fwd_hook). Topped up here so the block fails safe: if OP
// stops sending, the stock buttons resume forwarding within ~10 button frames (~0.4 s)
if (tx && (msg->addr == 0x296U) && (msg->bus == 2U)) {
honda_op_buttons_fresh = 10;
}
// Only tester present ("\x02\x3E\x80\x00\x00\x00\x00\x00") allowed on diagnostics address // Only tester present ("\x02\x3E\x80\x00\x00\x00\x00\x00") allowed on diagnostics address
// On CAN FD the radar is silenced from CarController after the relay opens (init() under the ELM327
// mode raced the safety-mode switch and latched CRUISE_FAULT), so additionally allow exactly the
// extended-diagnostic-session request and the suppressed-response CommunicationControl disableRxAndTx.
// The corresponding enable stays blocked: re-enabling the radar into OP's ACC_CONTROL stream would
// double up control messages while driving
if (msg->addr == 0x18DAB0F1U) { if (msg->addr == 0x18DAB0F1U) {
if ((GET_BYTES(msg, 0, 4) != 0x00803E02U) || (GET_BYTES(msg, 4, 4) != 0x0U)) { const uint32_t first_bytes = GET_BYTES(msg, 0, 4);
bool allowed = (first_bytes == 0x00803E02U);
if (honda_bosch_canfd) {
allowed = allowed || (first_bytes == 0x00031002U);
allowed = allowed || (first_bytes == 0x03832803U);
}
if (!allowed || (GET_BYTES(msg, 4, 4) != 0x0U)) {
tx = false; tx = false;
} }
} }
@@ -362,6 +391,7 @@ static safety_config honda_nidec_init(uint16_t param) {
honda_bosch_long = false; honda_bosch_long = false;
honda_bosch_radarless = false; honda_bosch_radarless = false;
honda_bosch_canfd = false; honda_bosch_canfd = false;
honda_op_buttons_fresh = 0;
safety_config ret; safety_config ret;
@@ -424,12 +454,29 @@ static safety_config honda_bosch_init(uint16_t param) {
{0x33DA, 1, 5, .check_relay = true}, {0x33DB, 1, 8, .check_relay = true}, {0x39F, 1, 8, .check_relay = false}, {0x33DA, 1, 5, .check_relay = true}, {0x33DB, 1, 8, .check_relay = true}, {0x39F, 1, 8, .check_relay = false},
{0x18DAB0F1, 1, 8, .check_relay = false}}; // Bosch w/ gas and brakes {0x18DAB0F1, 1, 8, .check_relay = false}}; // Bosch w/ gas and brakes
static CanMsg HONDA_RADARLESS_TX_MSGS[] = {{0xE4, 0, 5, .check_relay = true}, {0x296, 2, 4, .check_relay = false}, {0x33D, 0, 8, .check_relay = true}}; // Bosch radarless static CanMsg HONDA_RADARLESS_TX_MSGS[] = {{0xE4, 0, 5, .check_relay = true}, {0x296, 2, 4, .check_relay = false}, {0x33D, 0, 8, .check_relay = true},
{0x6CD5554, 0, 8, .check_relay = true}, {0xF31AA54, 0, 8, .check_relay = true},
{0x6CD5557, 0, 8, .check_relay = true}}; // Bosch radarless (LANE_PATH/LKAS_HUD_2/HUD_OBJECTS authored in stock ACC too)
static CanMsg HONDA_RADARLESS_LONG_TX_MSGS[] = {{0xE4, 0, 5, .check_relay = true}, {0x33D, 0, 8, .check_relay = true}, {0x1C8, 0, 8, .check_relay = true}, static CanMsg HONDA_RADARLESS_LONG_TX_MSGS[] = {{0xE4, 0, 5, .check_relay = true}, {0x33D, 0, 8, .check_relay = true}, {0x1C8, 0, 8, .check_relay = true},
{0x30C, 0, 8, .check_relay = true}}; // Bosch radarless w/ gas and brakes {0x30C, 0, 8, .check_relay = true}, {0x296, 2, 4, .check_relay = false}, {0x6CD5554, 0, 8, .check_relay = true},
{0xF31AA54, 0, 8, .check_relay = true}, {0x6CD5557, 0, 8, .check_relay = true}}; // Bosch radarless w/ gas and brakes
static CanMsg HONDA_CANFD_TX_MSGS[] = {{0xE4, 0, 5, .check_relay = true}, {0x296, 0, 4, .check_relay = false}, {0x33D, 0, 8, .check_relay = true}}; // 0x296 on bus 2: OP takes over SCM_BUTTONS towards the camera to auto-disable stock LKAS and to block
// the driver's LKAS button while engaged (the physical SCM_BUTTONS is blocked from forwarding, see fwd hook)
static CanMsg HONDA_CANFD_TX_MSGS[] = {{0xE4, 0, 5, .check_relay = true}, {0x296, 0, 4, .check_relay = false}, {0x296, 2, 4, .check_relay = false},
{0x33D, 0, 8, .check_relay = true}};
// The radar look-alikes (0x310, 0x6CD5558, 0x6CD5559, 0xF31AA52, 0xF31AA5C, 0x1A45AA4E) are consumed by both
// the camera (behind the relay on the camera bus, 2) and the powertrain (radar bus, 0). openpilot TX is not
// forwarded across the open relay, so each is sent on both buses; the control messages stay on bus 0
static CanMsg HONDA_CANFD_LONG_TX_MSGS[] = {{0xE4, 0, 5, .check_relay = true}, {0x1DF, 0, 8, .check_relay = true}, {0x1EF, 0, 8, .check_relay = false},
{0x30C, 0, 8, .check_relay = false}, {0x33D, 0, 8, .check_relay = true}, {0x296, 2, 4, .check_relay = false},
{0x39F, 0, 8, .check_relay = false}, {0x18DAB0F1, 0, 8, .check_relay = false},
{0x310, 0, 8, .check_relay = false}, {0x6CD5558, 0, 8, .check_relay = true}, {0x6CD5559, 0, 8, .check_relay = false},
{0xF31AA52, 0, 8, .check_relay = false}, {0xF31AA5C, 0, 8, .check_relay = true}, {0x1A45AA4E, 0, 8, .check_relay = false},
{0x310, 2, 8, .check_relay = false}, {0x6CD5558, 2, 8, .check_relay = true}, {0x6CD5559, 2, 8, .check_relay = false},
{0xF31AA52, 2, 8, .check_relay = false}, {0xF31AA5C, 2, 8, .check_relay = true}, {0x1A45AA4E, 2, 8, .check_relay = false}};
const uint16_t HONDA_PARAM_ALT_BRAKE = 1; const uint16_t HONDA_PARAM_ALT_BRAKE = 1;
@@ -458,6 +505,7 @@ static safety_config honda_bosch_init(uint16_t param) {
honda_hw = HONDA_BOSCH; honda_hw = HONDA_BOSCH;
honda_brake_switch_prev = false; honda_brake_switch_prev = false;
honda_op_buttons_fresh = 0;
honda_bosch_radarless = GET_FLAG(param, HONDA_PARAM_RADARLESS); honda_bosch_radarless = GET_FLAG(param, HONDA_PARAM_RADARLESS);
honda_bosch_canfd = GET_FLAG(param, HONDA_PARAM_BOSCH_CANFD); honda_bosch_canfd = GET_FLAG(param, HONDA_PARAM_BOSCH_CANFD);
// Checking for alternate brake override from safety parameter // Checking for alternate brake override from safety parameter
@@ -491,7 +539,11 @@ static safety_config honda_bosch_init(uint16_t param) {
SET_TX_MSGS(HONDA_RADARLESS_TX_MSGS, ret); SET_TX_MSGS(HONDA_RADARLESS_TX_MSGS, ret);
} }
} else if (honda_bosch_canfd) { } else if (honda_bosch_canfd) {
SET_TX_MSGS(HONDA_CANFD_TX_MSGS, ret); if (honda_bosch_long) {
SET_TX_MSGS(HONDA_CANFD_LONG_TX_MSGS, ret);
} else {
SET_TX_MSGS(HONDA_CANFD_TX_MSGS, ret);
}
} else { } else {
if (honda_bosch_long) { if (honda_bosch_long) {
SET_TX_MSGS(HONDA_BOSCH_LONG_TX_MSGS, ret); SET_TX_MSGS(HONDA_BOSCH_LONG_TX_MSGS, ret);
@@ -524,10 +576,34 @@ const safety_hooks honda_nidec_hooks = {
.compute_checksum = honda_compute_checksum, .compute_checksum = honda_compute_checksum,
}; };
static bool honda_bosch_fwd_hook(int bus_num, int addr) {
bool block_msg = false;
// On radarless and CAN FD, OP takes over SCM_BUTTONS (0x296) towards the camera when engaged, to
// auto-disable stock LKAS and block the driver's LKAS button (the touch-steering-wheel timer would
// otherwise force a disengagement). Only block the stock buttons while OP's replacement stream is
// actually flowing (honda_op_buttons_fresh): the camera needs SCM_BUTTONS content beyond the buttons
// (it raises an adaptive high beam error when the message goes missing), so a bare controls_allowed
// gate would starve it whenever the panda allows controls but OP refuses to engage
if ((honda_bosch_radarless || honda_bosch_canfd) && controls_allowed && (honda_op_buttons_fresh > 0) &&
(bus_num == 0) && (addr == 0x296)) {
block_msg = true;
}
// CAN FD: the radar disable handshake happens after the relay is open, so block the radar's UDS
// responses from forwarding to the camera (the camera doesn't need them)
if (honda_bosch_canfd && (bus_num == 0) && (addr == 0x18DAF1B0)) {
block_msg = true;
}
return block_msg;
}
const safety_hooks honda_bosch_hooks = { const safety_hooks honda_bosch_hooks = {
.init = honda_bosch_init, .init = honda_bosch_init,
.rx = honda_rx_hook, .rx = honda_rx_hook,
.tx = honda_tx_hook, .tx = honda_tx_hook,
.fwd = honda_bosch_fwd_hook,
.get_counter = honda_get_counter, .get_counter = honda_get_counter,
.get_checksum = honda_get_checksum, .get_checksum = honda_get_checksum,
.compute_checksum = honda_compute_checksum, .compute_checksum = honda_compute_checksum,

View File

@@ -843,7 +843,10 @@ class SafetyTest(SafetyTestBase):
SCANNED_ADDRS = [*range(0x800), # Entire 11-bit CAN address space SCANNED_ADDRS = [*range(0x800), # Entire 11-bit CAN address space
*range(0x18DA00F1, 0x18DB00F1, 0x100), # 29-bit UDS physical addressing *range(0x18DA00F1, 0x18DB00F1, 0x100), # 29-bit UDS physical addressing
*range(0x18DB00F1, 0x18DC00F1, 0x100), # 29-bit UDS functional addressing *range(0x18DB00F1, 0x18DC00F1, 0x100), # 29-bit UDS functional addressing
*range(0x3300, 0x3400)] # Honda *range(0x3300, 0x3400), # Honda
*range(0x6CD5554, 0x6CD555A), # Honda Bosch LANE_PATH, HUD_OBJECTS (camera and radar variants)
0xF31AA52, 0xF31AA54, 0xF31AA5C, # Honda Bosch RADAR_LEAD2, LKAS_HUD_2, RADAR_LEAD
0x1A45AA4E] # Honda Bosch BOSCH_SUPPLEMENTAL_CANFD
FWD_BLACKLISTED_ADDRS: dict[int, list[int]] = {} # {bus: [addr]} FWD_BLACKLISTED_ADDRS: dict[int, list[int]] = {} # {bus: [addr]}
FWD_BUS_LOOKUP: dict[int, int] = {0: 2, 2: 0} FWD_BUS_LOOKUP: dict[int, int] = {0: 2, 2: 0}
@@ -959,10 +962,14 @@ class SafetyTest(SafetyTestBase):
if attr in ('TestVolkswagenMqbLongSafety', 'TestVolkswagenMlbLongSafety') and current_test.startswith('TestHondaBoschRadarless'): if attr in ('TestVolkswagenMqbLongSafety', 'TestVolkswagenMlbLongSafety') and current_test.startswith('TestHondaBoschRadarless'):
tx = list(filter(lambda m: m[0] not in [0x30c, ], tx)) tx = list(filter(lambda m: m[0] not in [0x30c, ], tx))
# Volkswagen MQB/MLB and Honda Bosch CANFD ACC HUD messages overlap
if attr in ('TestVolkswagenMqbLongSafety', 'TestVolkswagenMlbLongSafety') and current_test.startswith('TestHondaBoschCANFD'):
tx = list(filter(lambda m: m[0] not in [0x30c, ], tx))
# TODO: Temporary, should be fixed in panda firmware, safety_honda.h # TODO: Temporary, should be fixed in panda firmware, safety_honda.h
if attr.startswith('TestHonda'): if attr.startswith('TestHonda'):
# exceptions for common msgs across different hondas # exceptions for common msgs across different hondas
tx = list(filter(lambda m: m[0] not in [0x1FA, 0x30C, 0x33D, 0x33DB], tx)) tx = list(filter(lambda m: m[0] not in [0x1FA, 0x30C, 0x33D, 0x33DB, 0x6CD5554, 0xF31AA54, 0x6CD5557], tx))
if attr.startswith('TestHyundaiLongitudinal'): if attr.startswith('TestHyundaiLongitudinal'):
# exceptions for common msgs across different Hyundai CAN platforms # exceptions for common msgs across different Hyundai CAN platforms

View File

@@ -31,6 +31,8 @@ class Btn:
# * Bosch with Longitudinal Support # * Bosch with Longitudinal Support
# * Bosch Radarless # * Bosch Radarless
# * Bosch Radarless with Longitudinal Support # * Bosch Radarless with Longitudinal Support
# * Bosch CANFD
# * Bosch CANFD with Longitudinal Support
class HondaButtonEnableBase(common.CarSafetyTest): class HondaButtonEnableBase(common.CarSafetyTest):
@@ -372,6 +374,14 @@ class TestHondaNidecSafetyBase(HondaBase):
send = brake == 0 send = brake == 0
self.assertEqual(send, self._tx(self._send_brake_msg(brake))) self.assertEqual(send, self._tx(self._send_brake_msg(brake)))
# Inactive brake must pass when gas blocks longitudinal actuation
self.safety.set_honda_fwd_brake(False)
self.safety.set_controls_allowed(True)
self.safety.set_gas_pressed_prev(True)
self.assertFalse(self.safety.get_longitudinal_allowed())
self.assertTrue(self._tx(self._send_brake_msg(0)))
self.assertFalse(self._tx(self._send_brake_msg(1)))
class TestHondaNidecPcmSafety(HondaPcmEnableBase, TestHondaNidecSafetyBase): class TestHondaNidecPcmSafety(HondaPcmEnableBase, TestHondaNidecSafetyBase):
""" """
@@ -536,7 +546,7 @@ class TestHondaBoschLongSafety(HondaButtonEnableBase, TestHondaBoschSafetyBase):
Covers the Honda Bosch safety mode with longitudinal control Covers the Honda Bosch safety mode with longitudinal control
""" """
NO_GAS = -30000 NO_GAS = -30000
MAX_GAS = 2000 MAX_GAS = 2200
MAX_ACCEL = 2.0 # accel is used for brakes, but openpilot can set positive values MAX_ACCEL = 2.0 # accel is used for brakes, but openpilot can set positive values
MIN_ACCEL = -3.5 MIN_ACCEL = -3.5
@@ -570,10 +580,16 @@ class TestHondaBoschLongSafety(HondaButtonEnableBase, TestHondaBoschSafetyBase):
not_tester_present = libsafety_py.make_CANPacket(0x18DAB0F1, self.PT_BUS, b"\x03\xAA\xAA\x00\x00\x00\x00\x00") not_tester_present = libsafety_py.make_CANPacket(0x18DAB0F1, self.PT_BUS, b"\x03\xAA\xAA\x00\x00\x00\x00\x00")
self.assertFalse(self._tx(not_tester_present)) self.assertFalse(self._tx(not_tester_present))
# the radar disable requests are only allowed on CANFD
ext_diag = libsafety_py.make_CANPacket(0x18DAB0F1, self.PT_BUS, b"\x02\x10\x03\x00\x00\x00\x00\x00")
self.assertFalse(self._tx(ext_diag))
comm_control_disable = libsafety_py.make_CANPacket(0x18DAB0F1, self.PT_BUS, b"\x03\x28\x83\x03\x00\x00\x00\x00")
self.assertFalse(self._tx(comm_control_disable))
def test_gas_safety_check(self): def test_gas_safety_check(self):
for controls_allowed in [True, False]: for controls_allowed in [True, False]:
for gas in np.arange(self.NO_GAS, self.MAX_GAS + 2000, 100): for gas in np.arange(self.NO_GAS, self.MAX_GAS + 2000, 100):
accel = 0 if gas < 0 else gas / 1000 accel = 0 if gas < 0 else min(gas / 1000, self.MAX_ACCEL)
self.safety.set_controls_allowed(controls_allowed) self.safety.set_controls_allowed(controls_allowed)
send = (controls_allowed and 0 <= gas <= self.MAX_GAS) or gas == self.NO_GAS send = (controls_allowed and 0 <= gas <= self.MAX_GAS) or gas == self.NO_GAS
self.assertEqual(send, self._tx(self._send_gas_brake_msg(gas, accel)), (controls_allowed, gas, accel)) self.assertEqual(send, self._tx(self._send_gas_brake_msg(gas, accel)), (controls_allowed, gas, accel))
@@ -593,14 +609,41 @@ class TestHondaBoschRadarlessSafetyBase(TestHondaBoschSafetyBase):
STEER_BUS = 0 STEER_BUS = 0
BUTTONS_BUS = 2 # camera controls ACC, need to send buttons on bus 2 BUTTONS_BUS = 2 # camera controls ACC, need to send buttons on bus 2
TX_MSGS = [[0xE4, 0], [0x296, 2], [0x33D, 0]] TX_MSGS = [[0xE4, 0], [0x296, 2], [0x33D, 0], [0x6CD5554, 0], [0xF31AA54, 0], [0x6CD5557, 0]]
FWD_BLACKLISTED_ADDRS = {2: [0xE4, 0x33D]} FWD_BLACKLISTED_ADDRS = {2: [0xE4, 0x33D, 0x6CD5554, 0xF31AA54, 0x6CD5557]}
RELAY_MALFUNCTION_ADDRS = {0: (0xE4, 0x33D)} # STEERING_CONTROL # STEERING_CONTROL, LANE_PATH, LKAS_HUD_2, HUD_OBJECTS
RELAY_MALFUNCTION_ADDRS = {0: (0xE4, 0x33D, 0x6CD5554, 0xF31AA54, 0x6CD5557)}
def setUp(self): def setUp(self):
self.packer = CANPackerSafety("honda_bosch_radarless_generated") self.packer = CANPackerSafety("honda_bosch_radarless_generated")
self.safety = libsafety_py.libsafety self.safety = libsafety_py.libsafety
def test_buttons_fwd(self):
# SCM_BUTTONS (0x296) forwards to the camera unless OP's replacement button stream is flowing
# (engaged + a recent OP SCM_BUTTONS tx on the camera bus). The camera needs the message content
# beyond the buttons, so the block fails safe back to forwarding when OP stops sending
self.safety.set_controls_allowed(False)
self.assertEqual(2, self.safety.safety_fwd_hook(0, 0x296))
# engaged but OP not sending buttons: keep forwarding
self.safety.set_controls_allowed(True)
self.assertEqual(2, self.safety.safety_fwd_hook(0, 0x296))
# OP button stream flowing: block the stock buttons
self.assertTrue(self._tx(self._button_msg(Btn.NONE, bus=2)))
self.assertEqual(-1, self.safety.safety_fwd_hook(0, 0x296))
# never blocked while disengaged
self.safety.set_controls_allowed(False)
self.assertEqual(2, self.safety.safety_fwd_hook(0, 0x296))
self.safety.set_controls_allowed(True)
self.assertEqual(-1, self.safety.safety_fwd_hook(0, 0x296))
# freshness decays after 10 stock button frames without an OP tx
for _ in range(10):
self._rx(self._button_msg(Btn.NONE, main_on=True))
self.assertEqual(2, self.safety.safety_fwd_hook(0, 0x296))
class TestHondaBoschRadarlessSafety(HondaPcmEnableBase, TestHondaBoschRadarlessSafetyBase): class TestHondaBoschRadarlessSafety(HondaPcmEnableBase, TestHondaBoschRadarlessSafetyBase):
""" """
@@ -629,9 +672,9 @@ class TestHondaBoschRadarlessLongSafety(common.LongitudinalAccelSafetyTest, Hond
""" """
Covers the Honda Bosch Radarless safety mode with longitudinal control Covers the Honda Bosch Radarless safety mode with longitudinal control
""" """
TX_MSGS = [[0xE4, 0], [0x33D, 0], [0x1C8, 0], [0x30C, 0]] TX_MSGS = [[0xE4, 0], [0x33D, 0], [0x1C8, 0], [0x30C, 0], [0x296, 2], [0x6CD5554, 0], [0xF31AA54, 0], [0x6CD5557, 0]]
FWD_BLACKLISTED_ADDRS = {2: [0xE4, 0x33D, 0x1C8, 0x30C]} FWD_BLACKLISTED_ADDRS = {2: [0xE4, 0x33D, 0x1C8, 0x30C, 0x6CD5554, 0xF31AA54, 0x6CD5557]}
RELAY_MALFUNCTION_ADDRS = {0: (0xE4, 0x1C8, 0x30C, 0x33D)} RELAY_MALFUNCTION_ADDRS = {0: (0xE4, 0x1C8, 0x30C, 0x33D, 0x6CD5554, 0xF31AA54, 0x6CD5557)}
def setUp(self): def setUp(self):
super().setUp() super().setUp()
@@ -655,7 +698,7 @@ class TestHondaBoschCANFDSafetyBase(TestHondaBoschSafetyBase):
STEER_BUS = 0 STEER_BUS = 0
BUTTONS_BUS = 0 BUTTONS_BUS = 0
TX_MSGS = [[0xE4, 0], [0x296, 0], [0x33D, 0]] TX_MSGS = [[0xE4, 0], [0x296, 0], [0x296, 2], [0x33D, 0]]
FWD_BLACKLISTED_ADDRS = {2: [0xE4, 0x33D]} FWD_BLACKLISTED_ADDRS = {2: [0xE4, 0x33D]}
RELAY_MALFUNCTION_ADDRS = {0: (0xE4, 0x33D)} RELAY_MALFUNCTION_ADDRS = {0: (0xE4, 0x33D)}
@@ -663,6 +706,42 @@ class TestHondaBoschCANFDSafetyBase(TestHondaBoschSafetyBase):
self.packer = CANPackerSafety("honda_common_canfd_generated") self.packer = CANPackerSafety("honda_common_canfd_generated")
self.safety = libsafety_py.libsafety self.safety = libsafety_py.libsafety
def test_buttons_fwd(self):
# SCM_BUTTONS (0x296) forwards to the camera unless OP's replacement button stream is flowing
# (engaged + a recent OP SCM_BUTTONS tx on the camera bus); see the radarless variant of this test
self.safety.set_controls_allowed(True)
self.assertEqual(2, self.safety.safety_fwd_hook(0, 0x296))
self.assertTrue(self._tx(self._button_msg(Btn.NONE, bus=2)))
self.assertEqual(-1, self.safety.safety_fwd_hook(0, 0x296))
self.safety.set_controls_allowed(False)
self.assertEqual(2, self.safety.safety_fwd_hook(0, 0x296))
self.safety.set_controls_allowed(True)
for _ in range(10):
self._rx(self._button_msg(Btn.NONE, main_on=True))
self.assertEqual(2, self.safety.safety_fwd_hook(0, 0x296))
def test_radar_diag_response_fwd(self):
# the radar's UDS responses (0x18DAF1B0) never forward to the camera: the radar disable handshake
# happens after the relay is open on CAN FD
self.safety.set_controls_allowed(False)
self.assertEqual(-1, self.safety.safety_fwd_hook(0, 0x18DAF1B0))
self.safety.set_controls_allowed(True)
self.assertEqual(-1, self.safety.safety_fwd_hook(0, 0x18DAF1B0))
def test_buttons_tx_camera_bus(self):
# Buttons to the camera (bus 2): cancel-only while disengaged, any button while engaged
# (OP takes over SCM_BUTTONS towards the camera when engaged)
self.safety.set_controls_allowed(0)
self.assertTrue(self._tx(self._button_msg(Btn.CANCEL, bus=2)))
self.assertFalse(self._tx(self._button_msg(Btn.RESUME, bus=2)))
self.assertFalse(self._tx(self._button_msg(Btn.SET, bus=2)))
self.safety.set_controls_allowed(1)
self.assertTrue(self._tx(self._button_msg(Btn.NONE, bus=2)))
self.assertTrue(self._tx(self._button_msg(Btn.RESUME, bus=2)))
class TestHondaBoschCANFDSafety(HondaPcmEnableBase, TestHondaBoschCANFDSafetyBase): class TestHondaBoschCANFDSafety(HondaPcmEnableBase, TestHondaBoschCANFDSafetyBase):
""" """
@@ -686,6 +765,48 @@ class TestHondaBoschCANFDAltBrakeSafety(HondaPcmEnableBase, TestHondaBoschCANFDS
self.safety.init_tests() self.safety.init_tests()
class TestHondaBoschCANFDLongSafety(TestHondaBoschLongSafety, TestHondaBoschCANFDSafetyBase):
"""
Covers the Honda Bosch CANFD safety mode with longitudinal control
"""
PT_BUS = 0
STEER_BUS = 0
BUTTONS_BUS = 0
# the radar look-alikes are sent on both the powertrain bus (0) and the camera bus (2)
TX_MSGS = [[0xE4, 0], [0x1DF, 0], [0x1EF, 0], [0x30C, 0], [0x33D, 0], [0x39F, 0], [0x296, 2], [0x18DAB0F1, 0],
[0x310, 0], [0x6CD5558, 0], [0x6CD5559, 0], [0xF31AA52, 0], [0xF31AA5C, 0], [0x1A45AA4E, 0],
[0x310, 2], [0x6CD5558, 2], [0x6CD5559, 2], [0xF31AA52, 2], [0xF31AA5C, 2], [0x1A45AA4E, 2]]
FWD_BLACKLISTED_ADDRS = {0: [0x6CD5558, 0xF31AA5C], 2: [0xE4, 0x1DF, 0x33D, 0x6CD5558, 0xF31AA5C]}
# STEERING_CONTROL, ACC_CONTROL, LKAS_HUD on the pt bus; the radar's LANE_PATH and RADAR_LEAD are
# additionally blocked from forwarding to the camera in both directions
RELAY_MALFUNCTION_ADDRS = {0: (0xE4, 0x1DF, 0x33D, 0x6CD5558, 0xF31AA5C), 2: (0x6CD5558, 0xF31AA5C)}
def setUp(self):
super().setUp()
self.safety.set_safety_hooks(CarParams.SafetyModel.hondaBosch, HondaSafetyFlags.BOSCH_CANFD | HondaSafetyFlags.BOSCH_LONG)
self.safety.init_tests()
def test_diagnostics(self):
# CAN FD silences the radar from CarController after the relay opens, so exactly the extended
# diagnostic session and the suppressed-response CommunicationControl disable are allowed too
tester_present = libsafety_py.make_CANPacket(0x18DAB0F1, self.PT_BUS, b"\x02\x3E\x80\x00\x00\x00\x00\x00")
self.assertTrue(self._tx(tester_present))
ext_diag = libsafety_py.make_CANPacket(0x18DAB0F1, self.PT_BUS, b"\x02\x10\x03\x00\x00\x00\x00\x00")
self.assertTrue(self._tx(ext_diag))
comm_control_disable = libsafety_py.make_CANPacket(0x18DAB0F1, self.PT_BUS, b"\x03\x28\x83\x03\x00\x00\x00\x00")
self.assertTrue(self._tx(comm_control_disable))
# anything else stays blocked, including re-enabling the radar and non-zero trailing bytes
comm_control_enable = libsafety_py.make_CANPacket(0x18DAB0F1, self.PT_BUS, b"\x03\x28\x80\x03\x00\x00\x00\x00")
self.assertFalse(self._tx(comm_control_enable))
not_tester_present = libsafety_py.make_CANPacket(0x18DAB0F1, self.PT_BUS, b"\x03\xAA\xAA\x00\x00\x00\x00\x00")
self.assertFalse(self._tx(not_tester_present))
trailing_bytes = libsafety_py.make_CANPacket(0x18DAB0F1, self.PT_BUS, b"\x02\x10\x03\x00\x00\x00\x00\x01")
self.assertFalse(self._tx(trailing_bytes))
class TestHondaNidecHybridSafety(TestHondaNidecPcmSafety): class TestHondaNidecHybridSafety(TestHondaNidecPcmSafety):
""" """
Covers the Honda Nidec safety mode with hybrid brake Covers the Honda Nidec safety mode with hybrid brake

View File

@@ -174,6 +174,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"IQCarParamsCache", {CLEAR_ON_MANAGER_START, BYTES}}, {"IQCarParamsCache", {CLEAR_ON_MANAGER_START, BYTES}},
{"IQCarParamsPersistent", {PERSISTENT, BYTES}}, {"IQCarParamsPersistent", {PERSISTENT, BYTES}},
{"IQCarParamsPersistentV2", {PERSISTENT, BYTES}}, {"IQCarParamsPersistentV2", {PERSISTENT, BYTES}},
{"IQLongLearnedFactors", {PERSISTENT, JSON}},
{"CarPlatformBundle", {PERSISTENT, JSON}}, {"CarPlatformBundle", {PERSISTENT, JSON}},
{"Konn3ktVwOdometers", {PERSISTENT, JSON}}, {"Konn3ktVwOdometers", {PERSISTENT, JSON}},
{"Konn3ktVehicleOdometers", {PERSISTENT, JSON}}, {"Konn3ktVehicleOdometers", {PERSISTENT, JSON}},

View File

@@ -2,6 +2,8 @@
""" """
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
""" """
import json
import math
import os import os
import time import time
import threading import threading
@@ -86,7 +88,7 @@ class Car:
def __init__(self, CI=None, RI=None) -> None: def __init__(self, CI=None, RI=None) -> None:
self.can_sock = messaging.sub_sock('can', timeout=20) self.can_sock = messaging.sub_sock('can', timeout=20)
self.sm = messaging.SubMaster(['pandaStates', 'carControl', 'onroadEvents', 'testJoystick'] + ['iqCarControl', 'iqPlan']) self.sm = messaging.SubMaster(['pandaStates', 'carControl', 'onroadEvents', 'testJoystick', 'modelV2'] + ['iqCarControl', 'iqPlan'])
self.pm = messaging.PubMaster(['sendcan', 'carState', 'carParams', 'carOutput', 'radarTracks', 'iqPerfTrace'] + ['iqCarParams', 'iqCarState']) self.pm = messaging.PubMaster(['sendcan', 'carState', 'carParams', 'carOutput', 'radarTracks', 'iqPerfTrace'] + ['iqCarParams', 'iqCarState'])
self.can_rcv_cum_timeout_counter = 0 self.can_rcv_cum_timeout_counter = 0
@@ -178,6 +180,9 @@ class Car:
else: else:
cloudlog.warning("Saved SecOC key is invalid") cloudlog.warning("Saved SecOC key is invalid")
if controller_available:
self._seed_learned_factors()
# Write previous route's CarParams # Write previous route's CarParams
prev_cp = self.params.get("CarParamsPersistent") prev_cp = self.params.get("CarParamsPersistent")
if prev_cp is not None: if prev_cp is not None:
@@ -259,9 +264,43 @@ class Car:
return CS, CS_IQ, RD return CS, CS_IQ, RD
def _learned_factor_attrs(self):
if self.CI.CC is None:
return ()
return tuple(attr for attr in ("gasfactor", "windfactor") if hasattr(self.CI.CC, attr))
def _stored_learned_factors(self) -> dict:
try:
stored = json.loads(self.params.get("IQLongLearnedFactors") or b"{}")
except ValueError:
stored = {}
return stored if isinstance(stored, dict) else {}
def _seed_learned_factors(self):
attrs = self._learned_factor_attrs()
if not attrs:
return
factors = self._stored_learned_factors().get(str(self.CP.carFingerprint), {})
for attr in attrs:
value = factors.get(attr)
if isinstance(value, int | float) and math.isfinite(value):
setattr(self.CI.CC, attr, float(value))
def _save_learned_factors(self):
attrs = self._learned_factor_attrs()
if not attrs:
return
stored = self._stored_learned_factors()
stored[str(self.CP.carFingerprint)] = {attr: float(getattr(self.CI.CC, attr)) for attr in attrs}
self.params.put_nonblocking("IQLongLearnedFactors", json.dumps(stored))
def state_publish(self, CS: car.CarState, CS_IQ: custom.IQCarState, RD: structs.RadarDataT | None): def state_publish(self, CS: car.CarState, CS_IQ: custom.IQCarState, RD: structs.RadarDataT | None):
"""carState and carParams publish loop""" """carState and carParams publish loop"""
# persist live-learned longitudinal factors so they survive across drives
if self.sm.frame > 0 and self.sm.frame % int(60. / DT_CTRL) == 0:
self._save_learned_factors()
# carParams - logged every 50 seconds (> 1 per segment) # carParams - logged every 50 seconds (> 1 per segment)
if self.sm.frame % int(50. / DT_CTRL) == 0: if self.sm.frame % int(50. / DT_CTRL) == 0:
cp_send = messaging.new_message('carParams') cp_send = messaging.new_message('carParams')
@@ -323,8 +362,9 @@ class Car:
cc_iq = convert_iq_car_control_compact(CC_IQ, include_leads=self._needs_iq_lead_data) cc_iq = convert_iq_car_control_compact(CC_IQ, include_leads=self._needs_iq_lead_data)
convert_us = (time.monotonic_ns() - started) // 1000 convert_us = (time.monotonic_ns() - started) // 1000
model = self.sm['modelV2'] if self.sm.valid['modelV2'] else None
started = time.monotonic_ns() started = time.monotonic_ns()
self.last_actuators_output, can_sends = self.CI.apply(CC, cc_iq, now_nanos) self.last_actuators_output, can_sends = self.CI.apply(CC, cc_iq, now_nanos, model)
apply_us = (time.monotonic_ns() - started) // 1000 apply_us = (time.monotonic_ns() - started) // 1000
started = time.monotonic_ns() started = time.monotonic_ns()

View File

@@ -0,0 +1,82 @@
import json
from types import SimpleNamespace
from iqpilot.selfdrive.car.card import Car
class DummyParams:
def __init__(self):
self.values: dict[str, object] = {}
def get(self, key: str):
return self.values.get(key)
def put_nonblocking(self, key: str, value) -> None:
self.values[key] = value
class HondaLikeController:
def __init__(self):
self.gasfactor = 1.0
self.windfactor = 1.0
def make_car(controller):
car = object.__new__(Car)
car.params = DummyParams()
car.CI = SimpleNamespace(CC=controller)
car.CP = SimpleNamespace(carFingerprint="HONDA_CRV_6G")
return car
class TestLearnedFactorPersistence:
def test_save_then_seed_round_trip(self):
car = make_car(HondaLikeController())
car.CI.CC.gasfactor = 1.37
car.CI.CC.windfactor = 0.84
car._save_learned_factors()
fresh = make_car(HondaLikeController())
fresh.params.values = car.params.values
fresh._seed_learned_factors()
assert fresh.CI.CC.gasfactor == 1.37
assert fresh.CI.CC.windfactor == 0.84
def test_factors_keyed_per_fingerprint(self):
car = make_car(HondaLikeController())
car.CI.CC.gasfactor = 2.0
car._save_learned_factors()
other = make_car(HondaLikeController())
other.params.values = car.params.values
other.CP = SimpleNamespace(carFingerprint="HONDA_CIVIC_BOSCH")
other._seed_learned_factors()
assert other.CI.CC.gasfactor == 1.0
other.CI.CC.gasfactor = 0.5
other._save_learned_factors()
stored = json.loads(other.params.values["IQLongLearnedFactors"])
assert stored["HONDA_CRV_6G"]["gasfactor"] == 2.0
assert stored["HONDA_CIVIC_BOSCH"]["gasfactor"] == 0.5
def test_seed_ignores_corrupt_or_nonfinite_values(self):
car = make_car(HondaLikeController())
car.params.values["IQLongLearnedFactors"] = "not json"
car._seed_learned_factors()
assert car.CI.CC.gasfactor == 1.0
car.params.values["IQLongLearnedFactors"] = json.dumps({"HONDA_CRV_6G": {"gasfactor": float("nan"), "windfactor": "x"}})
car._seed_learned_factors()
assert car.CI.CC.gasfactor == 1.0
assert car.CI.CC.windfactor == 1.0
def test_noop_for_controllers_without_factors(self):
car = make_car(SimpleNamespace())
car._seed_learned_factors()
car._save_learned_factors()
assert "IQLongLearnedFactors" not in car.params.values
car = make_car(None)
car._seed_learned_factors()
car._save_learned_factors()
assert "IQLongLearnedFactors" not in car.params.values

View File

@@ -11,6 +11,7 @@ import numpy as np
from iqpilot.cereal import log, custom # noqa: F401 (custom kept available for downstream imports) from iqpilot.cereal import log, custom # noqa: F401 (custom kept available for downstream imports)
from iqdbc.car import structs from iqdbc.car import structs
from iqdbc.car.lateral import FRICTION_THRESHOLD, get_friction from iqdbc.car.lateral import FRICTION_THRESHOLD, get_friction
from iqdbc.car.toyota.values import ToyotaFlags
from iqdbc.lvbs.car.interfaces import LatControlInputs from iqdbc.lvbs.car.interfaces import LatControlInputs
from iqdbc.lvbs.car.iq_lateral import get_friction as get_friction_in_torque_space from iqdbc.lvbs.car.iq_lateral import get_friction as get_friction_in_torque_space
from iqpilot.common.basedir import BASEDIR from iqpilot.common.basedir import BASEDIR
@@ -556,6 +557,8 @@ class LatControlTorque(LatControl):
self.lat_accel_request_buffer = deque([0.] * self.lat_accel_request_buffer_len , maxlen=self.lat_accel_request_buffer_len) self.lat_accel_request_buffer = deque([0.] * self.lat_accel_request_buffer_len , maxlen=self.lat_accel_request_buffer_len)
self.lookahead_frames = int(JERK_LOOKAHEAD_SECONDS / self.dt) self.lookahead_frames = int(JERK_LOOKAHEAD_SECONDS / self.dt)
self.jerk_filter = FirstOrderFilter(0.0, 1 / (2 * np.pi * LP_FILTER_CUTOFF_HZ), self.dt) self.jerk_filter = FirstOrderFilter(0.0, 1 / (2 * np.pi * LP_FILTER_CUTOFF_HZ), self.dt)
self.setpoint_lead_enabled = CP.brand == "toyota" and bool(CP.flags & ToyotaFlags.TSS2)
self.setpoint_lead_filter = FirstOrderFilter(0.0, 1 / (2 * np.pi * LP_FILTER_CUTOFF_HZ), self.dt)
self.lateral_acceleration_slew_limiter = LateralAccelerationSlewLimiter(Params().get_bool("IQLateralAccelSlew")) self.lateral_acceleration_slew_limiter = LateralAccelerationSlewLimiter(Params().get_bool("IQLateralAccelSlew"))
self.curvature_lookahead_enabled = Params().get_bool("IQLateralCurvatureLookahead") self.curvature_lookahead_enabled = Params().get_bool("IQLateralCurvatureLookahead")
@@ -593,6 +596,10 @@ class LatControlTorque(LatControl):
delay_frames = int(np.clip(lat_delay / self.dt + 1, 1, self.lat_accel_request_buffer_len)) delay_frames = int(np.clip(lat_delay / self.dt + 1, 1, self.lat_accel_request_buffer_len))
expected_lateral_accel = self.lat_accel_request_buffer[-delay_frames] expected_lateral_accel = self.lat_accel_request_buffer[-delay_frames]
setpoint = expected_lateral_accel setpoint = expected_lateral_accel
if self.setpoint_lead_enabled:
# the delayed setpoint mutes P/I for lat_delay after a ramp starts; lead by the filtered ramp rate so torque-capped TSS2 EPS turns in on time
request_ramp_rate = (future_desired_lateral_accel - expected_lateral_accel) / max(lat_delay, self.dt)
setpoint += self.setpoint_lead_filter.update(request_ramp_rate) * lat_delay
error = setpoint - measurement error = setpoint - measurement
lookahead_idx = int(np.clip(-delay_frames + self.lookahead_frames, -self.lat_accel_request_buffer_len+1, -2)) lookahead_idx = int(np.clip(-delay_frames + self.lookahead_frames, -self.lat_accel_request_buffer_len+1, -2))