diff --git a/artifacts/iqpilot_hephaestusd_private/manifest.json b/artifacts/iqpilot_hephaestusd_private/manifest.json index ef327ca68..3ea076f88 100644 --- a/artifacts/iqpilot_hephaestusd_private/manifest.json +++ b/artifacts/iqpilot_hephaestusd_private/manifest.json @@ -376,27 +376,27 @@ }, "python/iqpilot_private/konn3kt/backups/archive_codec.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "569652717895a42a6efd58fded1fdf6aa9c26badad96cf6c9a0a103a7972488d", + "sha256": "e5766a76611ae1e6a16f16bed9899c0a1617e9ec8390487ac1c76bd68102aec8", "size": 135552 }, "python/iqpilot_private/konn3kt/backups/backup_keys.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "fc2abcec7142e56b3c8c83414a49eaaec3922e8d56d49c9494ec5fa2c467c018", + "sha256": "f3434b919fbbcf9fc2bb29c3af817214b4b025eda7d8a50ab331637e41e3fc30", "size": 67664 }, "python/iqpilot_private/konn3kt/backups/backup_orchestrator.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "2d6763b947b92912313645d3e98a1ed31a6bfaba7f6c73269fa594e12fa2b525", + "sha256": "c2a3cbc4411bee79c6b9cf284385152f4c1d8a50fd4beb246872bf47a5059878", "size": 204640 }, "python/iqpilot_private/konn3kt/backups/cbc_vault.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "484c77fb10d314ac117f7a3914ed2f84c2b7afe2b466846f953aa41bf690d1e2", + "sha256": "13f241fc7fb7cdd3afc01ce65936e3054d60910461c384b6f8ccabb65e50e61b", "size": 69768 }, "python/iqpilot_private/konn3kt/backups/imahelper.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "85d79a68bd1fc98dc44131d765c55b3c06a7fc72b8901b3034c6ad54b56471dc", + "sha256": "c9d4a41687f057074f520b693c3d208cb8c78951f8c2e24b8cf6ea95c1690f14", "size": 203088 }, "python/iqpilot_private/konn3kt/flockd/__init__.py": { @@ -406,12 +406,12 @@ }, "python/iqpilot_private/konn3kt/flockd/flockd.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "3b70968cfe8bb9cc6faafaca46b9505a3a7c53062577f4e4d570bea7a6a84b21", + "sha256": "bd6e7bdcd9186240febe0cba2b66dc5ff92e5d7b65f91d8b146e81114a8c6090", "size": 202184 }, "python/iqpilot_private/konn3kt/flockd/signatures.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "45e697fc70a02b9f2ba939c5c7136958bef704e907e05d0c835ca585eab878fb", + "sha256": "9bea4ab85fdc103fc451031d99e1235ca7f84358a65424db902bbfd839278b30", "size": 68080 }, "python/iqpilot_private/konn3kt/hephaestus/__init__.py": { @@ -421,67 +421,67 @@ }, "python/iqpilot_private/konn3kt/hephaestus/_vendor/localapi_runtime.zip": { "mode": 420, - "sha256": "db892948057af888ff4587704b3f4ca70b714743a91c0f26d2ec85ccbff309b1", + "sha256": "5bccf8ab5fb8a3d648ba8be6f1e9a9263c1577b2a959f202763a6fcbe02eacb9", "size": 1379342 }, "python/iqpilot_private/konn3kt/hephaestus/ble_auth.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "7bbe4d5161fbb3cbe20f9ee9e52cb3c06b11254cbeaed1d3c64f8ac3bbf7ffc8", + "sha256": "6681689c2a20680be9a5c588fc6234a57eba1bc023ff209e9ba16534955d760c", "size": 268080 }, "python/iqpilot_private/konn3kt/hephaestus/ble_gatt.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "a6da1a1242c2817474d4d16ce156cf875f1ae18622c883af335fe8f344f8b083", + "sha256": "cc9c70a616928fe18e3ec7a49b80b127bb827438cd536146d57013ad006cb9ac", "size": 406176 }, "python/iqpilot_private/konn3kt/hephaestus/ble_rpc_dispatch.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "27741c9bec5652feb9da487489aa7c2cca2821887bc2fb36c28f9a79c05c52a7", + "sha256": "ac9f9b0467d64935e0a4f81a5e3525aacc8b243937eb339462086a447d7c6210", "size": 68048 }, "python/iqpilot_private/konn3kt/hephaestus/ble_transportd.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "6df244ee0a6fd26e4e9071c1907410485c8e2b5023fbd67831affadd943d301d", + "sha256": "33e3f6f65a0000d89c5b7e66054232a9773adc4d01fa28cb1ac658c20432e56b", "size": 335776 }, "python/iqpilot_private/konn3kt/hephaestus/bt_gamepad.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "0e9a3c8c59635fa516ab41c786f2f2cf162d3a9995cf00e7da46ca8c7b60c7b8", + "sha256": "c3306fc198b7b9cbf4b481debf4af79ff3d3e9dd23a6cc52137e763427d8f4cd", "size": 272112 }, "python/iqpilot_private/konn3kt/hephaestus/cloud_routes.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "140b254df8377104f3cb61e821e82d0a6885f63a61197dbad00a55c9c94641d6", + "sha256": "a3e3e45c6c9091620ee1d25f840bac287e166d7fb967d48f04d985c801d72711", "size": 134408 }, "python/iqpilot_private/konn3kt/hephaestus/hephaestusd.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "2324a4c85cd1b3489d7e87cb5c178c885b90fd0e1ad8a15e619dec05d264e4cf", + "sha256": "9151341dad54e3d27335a1e34592a35dc96e7f82431512cb9387a6ce35ac9661", "size": 3915272 }, "python/iqpilot_private/konn3kt/hephaestus/kwp2000.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "53b21e9789a609d1498653441629f99fbfa8051fe7f9e1da6cf7c24dbb20f509", + "sha256": "b3481836a4f580539882bf0781b6aa196940c247e6050f074727501599a04eaa", "size": 136472 }, "python/iqpilot_private/konn3kt/hephaestus/manage_hephaestusd.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "cea0fed8dbb76eb47c466c34491c8feb3847598b0235103a8c3f7897d28caa15", + "sha256": "7f557d6349f6feb313896fb063b7cdd1b81d13bc3adaa51072730b62e51f2199", "size": 68208 }, "python/iqpilot_private/konn3kt/hephaestus/motd.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "a60bd145da3d8dba8f55710eb6b36c2583ee485245e2a0d5d6b2230659157231", + "sha256": "32257f70289748828910bcdefeb689221a9b9f1b60b669aa454e0c4ed0514194", "size": 67840 }, "python/iqpilot_private/konn3kt/hephaestus/tp20.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "45d4c4cd8751b0d91efc90938c74148f29eec869a4523ef80dc7e0d471687e1a", + "sha256": "a3de8a57ec88a7a7900e360f274ff4a4a55cb6c693282f000ab9e1a804d1aadf", "size": 135664 }, "python/iqpilot_private/konn3kt/hephaestus/vw_pq_flasher.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "05445a115170b7ebe9355db42d2e44d4d6e323c4ddad10cd8d3c2ea975523793", + "sha256": "28a68065a70ceea382ac3773d79f0251f0bf862900a7a353d11fe9cb52561b08", "size": 269600 }, "python/iqpilot_private/konn3kt/uploaderd/__init__.py": { @@ -491,7 +491,7 @@ }, "python/iqpilot_private/konn3kt/uploaderd/iquploaderd.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "6fd0eb12cec35a9d0d8ac7b7e6005daccb5bb7fcc4cb53dd1bfd4bfbcde0a923", + "sha256": "beda75859f40628aaea1890d1207225bd633c76bc975f5a3cc741066508cc709", "size": 336280 }, "runtime": { @@ -535,25 +535,25 @@ } }, "signatures": { - "python/iqpilot_private/konn3kt/backups/archive_codec.cpython-312-aarch64-linux-gnu.so": "VxFp08PRJrvMSP23LRZa7qFujDFaUvn6tG4RlO7iJSDj8i+1wLZAcDiPGJTHZdY6YeExXgP6jSLtQy1BcbZ5Dg==", - "python/iqpilot_private/konn3kt/backups/backup_keys.cpython-312-aarch64-linux-gnu.so": "331zmBEB54CVkMicucclvnYGOhUpzEPhIS6Tnl3lURFjcwjjuc+r4uwgjvLDvfwaBCWTpwel0NNGC03aIyaGAA==", - "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/cbc_vault.cpython-312-aarch64-linux-gnu.so": "l3u8IUF/uaSXW4IimexnNX191B0nxLuu+PxdHQcjkSK+hOs0PJY8QVYE1oeY1CTy8mK0YTQP8sv6hp4RCVb7DA==", - "python/iqpilot_private/konn3kt/backups/imahelper.cpython-312-aarch64-linux-gnu.so": "kt7IQeKSbCNwia1YG2fHdJAO5DH+D+wNrJVTxGeSi+/c3qxOY9NgdXdRA9RROQcN7Zks95sNEcVXhR6I5ck4CQ==", - "python/iqpilot_private/konn3kt/flockd/flockd.cpython-312-aarch64-linux-gnu.so": "jeejVBJoO2+wtw/AYlbTGzPKvn+SLphtKhm9xuEkLTv8EmB5OkmM3AMqrH3G9czCRNBBSn2zjn4sxWimtH0fCQ==", - "python/iqpilot_private/konn3kt/flockd/signatures.cpython-312-aarch64-linux-gnu.so": "85xhiHsSocrlo2tm2m4tI/9wSjMMEzXYjJctL/SNk/MdHE1vSl7e85RGPp1r2xMBsibKi5vvJBaB66dDJikYAA==", - "python/iqpilot_private/konn3kt/hephaestus/ble_auth.cpython-312-aarch64-linux-gnu.so": "Arh8D4rzhmm8tIgJhOxDoOSGxZtrkkGFgWSkUPyFRxGv14cMoEoQzMEdU9UErkW4jO2U8vj93nieELi7SMNZDQ==", - "python/iqpilot_private/konn3kt/hephaestus/ble_gatt.cpython-312-aarch64-linux-gnu.so": "5JDxFY5itf0gGWDYKQbgpkBqE2Esf33v2i2cPDzQtDF/jpA+2C3Lev1rzmM2pN9y/fpCsW4urOmKB5IPRDh0Cg==", - "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_transportd.cpython-312-aarch64-linux-gnu.so": "w/1Xq9W4NVDyS24frgWfl58AbVPWEuq8k5CrC3uwyg0mLGAnr+UXqFzo9/E9xh9XJKbQdCfMdmo0OsSENpNgDA==", - "python/iqpilot_private/konn3kt/hephaestus/bt_gamepad.cpython-312-aarch64-linux-gnu.so": "ifpWpiuKSApIA3Q2q3p+Odw6d127qQfI64cJmr49ReS5ujyzKZqJMGo1vkgm1MsPf/CaBB5PLDA49dJxau1hCw==", - "python/iqpilot_private/konn3kt/hephaestus/cloud_routes.cpython-312-aarch64-linux-gnu.so": "HPAYWBMZJ8+mfyYR3lzg9jnkH+K5JEY/aYRCMTNlHPO7Wy1e2SLd8e5sqhc0AdOnVembXt06pdRNXrr5myMcDA==", - "python/iqpilot_private/konn3kt/hephaestus/hephaestusd.cpython-312-aarch64-linux-gnu.so": "6v9Zrjtw0bA5PLqZxZz03N1gwVoGHDzPLvolDWYnUziPwQ3ANhsjJ7puzun4r8s/Y2dyeZe5tjB3ht2YZBsOAQ==", - "python/iqpilot_private/konn3kt/hephaestus/kwp2000.cpython-312-aarch64-linux-gnu.so": "8LUrOg0CgdEaX04HvEAZrSWdovj/M/uhr54bN3XSWbE4qDeKnlsxDD430hHH/Bz17u+eSXzFPBrk33s7SXuyDg==", - "python/iqpilot_private/konn3kt/hephaestus/manage_hephaestusd.cpython-312-aarch64-linux-gnu.so": "TxU/vX6RG/aYvZnJc7plxaHcBiE7zzEmZGlcrRlojwIdUVR9MXwngKLkEBCDdgcXK99xS7JyeXuOxv7bJGwjCg==", - "python/iqpilot_private/konn3kt/hephaestus/motd.cpython-312-aarch64-linux-gnu.so": "VVwuzRtbygLWD0MFhPsRZLNm10Bep4Kde+PQbpHNmpdxXBNYN09cHnxGval7UBmpsgnOIcORBSNK0M2RkfZiBw==", - "python/iqpilot_private/konn3kt/hephaestus/tp20.cpython-312-aarch64-linux-gnu.so": "OVFsjEKAZNu/dx0Y6aTklTNL+kB+VgwdRFQEaBPkU4Aa3+w192FNSl7QgZYjGymBYk8t4TVkoEJAeIaQAOLBCQ==", - "python/iqpilot_private/konn3kt/hephaestus/vw_pq_flasher.cpython-312-aarch64-linux-gnu.so": "qQkWfrSS3J2CrMpWJ3YDvlg+QZJSLiFp+K6UJvEDOCpd2b9Vdu1jWrUzSVJ+jngsKnzyiwA28j15r5YW290pDA==", - "python/iqpilot_private/konn3kt/uploaderd/iquploaderd.cpython-312-aarch64-linux-gnu.so": "2OPtA7wcd78pbfj532oQKn07r4w+gVXXYq5SzH9Ely3etJJgtAMNGWF6kEY/St5UKHrXz0Ijkb243eyzNNVsBQ==" + "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": "skmbTy7FxaZggM1kT1qKUoQijD7s0TJjs7FZB+JSUt4ocf248pQMC55bbKM9P4yFX2QKk7LJ9P0NzXglwTi4CA==", + "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": "/N1l31Ks/riHSDMhyqBpguzrPNGNPg3WlNRBBnloST7kvAEvphcBnT5+yIQvPWtIZfDN6uKNFavUi06Q3/2JDA==", + "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": "YtzOVOh9rRQT3fbT4a4UDMbmKxPAUL/fLsA7WvQiI4YaUOgy5vx3i/FMx7N+2FswSbAL1Si+jab85zZbJbGaCQ==", + "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": "Uny9JAJkWPPCJbAkE9PkQvF2XNdHBKqZqdJraA6c2ywzbTx+F8Y+ge054N7Pfgj/oY2rZqHHwC66sz6zUXNbBg==", + "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": "Cmy6QWRx+jkndmCXb2Inr7KSWnB8MeN3IVYoR+GUR7Al2H28bTkB4aroCHc6JRIIIyiE+L3RHn8OZfbfszRICA==", + "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": "qA+C/uOh+uLHDi8j2HwLUa+Mx3CMOY5hVOjTaw86opkE8GFiCJEMnZnD1+gLfHhSw686VNswDgrp2oUmz97MBQ==", + "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": "oQC/Edkjh7OGSHjWyYdY0SKRJst5drk4GjXnMuFbZQu53INbrGEuZnpQ8x5Mjb6LxLOiKGU4Yu+ibhZTl7+oAw==", + "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": "yZY/mAzII1O/xMrmQueULxTgXf6Ze6P9PvMNM41LYEaLWlNSJLbYu+umhGQeru8LUGAO+xpuGahDloNMKa+dDg==", + "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": "10YGOLrImdCOB7LXlDkwkEdj6beXsFfc5AixI5Zq097mBiAcfoWFKiCSPeN3pk4CxuehNXtebw5UuLXsx2ABDg==", + "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": "CqJ0BMkC09FCG+XDybTNULBSobSDBWAPL8YMqXv7QVGCut/9pO1NON5s5zQPVsN6IHUOdt94Iz+h5Ehu1yPjAQ==" } } diff --git a/artifacts/iqpilot_hephaestusd_private/manifest.json.sig b/artifacts/iqpilot_hephaestusd_private/manifest.json.sig index 8d0d97157..e47824cd2 100644 --- a/artifacts/iqpilot_hephaestusd_private/manifest.json.sig +++ b/artifacts/iqpilot_hephaestusd_private/manifest.json.sig @@ -1 +1 @@ -1iB1q4SvZisVqE7ivgPA8wvxTvn7nSsXY5ZvPonciaVEx2/aLqhaXV9wA9sj3a+y1dp9J2Cg4AbrQqgyw+gvAg== +1J/pEtI6qoBjh397jGVMx0xaeG1QXhcxuiHCXKKiU497RGTTHNmgVpKkNngATL61HgbYa7dgj7mPwLs2u7epAQ== diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/archive_codec.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/archive_codec.cpython-312-aarch64-linux-gnu.so index 60af70382..7e9b21760 100755 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/archive_codec.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/archive_codec.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/backup_keys.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/backup_keys.cpython-312-aarch64-linux-gnu.so index 78967c291..c4e66618a 100755 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/backup_keys.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/backup_keys.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/backup_orchestrator.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/backup_orchestrator.cpython-312-aarch64-linux-gnu.so index ee02bc53f..223368c51 100755 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/backup_orchestrator.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/backup_orchestrator.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/cbc_vault.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/cbc_vault.cpython-312-aarch64-linux-gnu.so index c994427d0..0b6a1583f 100755 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/cbc_vault.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/cbc_vault.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/imahelper.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/imahelper.cpython-312-aarch64-linux-gnu.so index 4cec9da0b..30a9fb797 100755 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/imahelper.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/backups/imahelper.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/flockd/flockd.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/flockd/flockd.cpython-312-aarch64-linux-gnu.so index c511d5f38..89e90017d 100755 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/flockd/flockd.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/flockd/flockd.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/flockd/signatures.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/flockd/signatures.cpython-312-aarch64-linux-gnu.so index e024d77ba..7728235c4 100755 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/flockd/signatures.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/flockd/signatures.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/_vendor/localapi_runtime.zip b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/_vendor/localapi_runtime.zip index 8bbca605c..b3755d0e0 100644 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/_vendor/localapi_runtime.zip and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/_vendor/localapi_runtime.zip differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_auth.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_auth.cpython-312-aarch64-linux-gnu.so index f29e20401..a4f2a1b08 100755 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_auth.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_auth.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_gatt.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_gatt.cpython-312-aarch64-linux-gnu.so index cf416835f..ee57b01de 100755 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_gatt.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_gatt.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_rpc_dispatch.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_rpc_dispatch.cpython-312-aarch64-linux-gnu.so index 8a76a29bc..a3b0f4086 100755 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_rpc_dispatch.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_rpc_dispatch.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_transportd.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_transportd.cpython-312-aarch64-linux-gnu.so index 9fb2d63f1..d48134ccd 100755 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_transportd.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/ble_transportd.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/bt_gamepad.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/bt_gamepad.cpython-312-aarch64-linux-gnu.so index 663aeae47..3bce1b9c2 100755 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/bt_gamepad.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/bt_gamepad.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/cloud_routes.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/cloud_routes.cpython-312-aarch64-linux-gnu.so index 8f7a68a4f..e85c88c6d 100755 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/cloud_routes.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/cloud_routes.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/hephaestusd.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/hephaestusd.cpython-312-aarch64-linux-gnu.so index 1357288e4..cac85c726 100755 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/hephaestusd.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/hephaestusd.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/kwp2000.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/kwp2000.cpython-312-aarch64-linux-gnu.so index f75d650cf..42c1ec0c0 100755 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/kwp2000.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/kwp2000.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/manage_hephaestusd.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/manage_hephaestusd.cpython-312-aarch64-linux-gnu.so index 6642c1e6d..b546615ea 100755 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/manage_hephaestusd.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/manage_hephaestusd.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/motd.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/motd.cpython-312-aarch64-linux-gnu.so index fb9c7dd6f..b6204be1a 100755 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/motd.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/motd.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/tp20.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/tp20.cpython-312-aarch64-linux-gnu.so index 9567a2c07..784d66f7c 100755 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/tp20.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/tp20.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/vw_pq_flasher.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/vw_pq_flasher.cpython-312-aarch64-linux-gnu.so index 1007470d0..25b5ac94e 100755 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/vw_pq_flasher.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/hephaestus/vw_pq_flasher.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/uploaderd/iquploaderd.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/uploaderd/iquploaderd.cpython-312-aarch64-linux-gnu.so index df33c9a6d..92914bbe0 100755 Binary files a/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/uploaderd/iquploaderd.cpython-312-aarch64-linux-gnu.so and b/artifacts/iqpilot_hephaestusd_private/python/iqpilot_private/konn3kt/uploaderd/iquploaderd.cpython-312-aarch64-linux-gnu.so differ diff --git a/artifacts/package_sources/iqdbc/iqdbc/can/tests/test_checksums.py b/artifacts/package_sources/iqdbc/iqdbc/can/tests/test_checksums.py index 7c3e35405..45cb49ac5 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/can/tests/test_checksums.py +++ b/artifacts/package_sources/iqdbc/iqdbc/can/tests/test_checksums.py @@ -90,6 +90,44 @@ class TestCanChecksums: assert parser.vl['LKAS_HUD']['CHECKSUM'] == std 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'): """Test AUTOSAR E2E Profile 2 CRCs""" assert len(test_messages) == 16 # All counter values must be tested diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/disable_ecu.py b/artifacts/package_sources/iqdbc/iqdbc/car/disable_ecu.py index f5f325c7d..b5e49df81 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/disable_ecu.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/disable_ecu.py @@ -1,3 +1,4 @@ +from iqdbc.car.can_definitions import CanData from iqdbc.car.carlog import carlog from iqdbc.car.isotp_parallel_query import IsoTpParallelQuery @@ -6,9 +7,45 @@ EXT_DIAG_RESPONSE = b'\x50\x03' 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', - 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. 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) 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 ...") query = IsoTpParallelQuery(can_send, can_recv, bus, [(addr, sub_addr)], [com_cont_req], [COM_CONT_RESPONSE], response_offset) diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/honda/carcontroller.py b/artifacts/package_sources/iqdbc/iqdbc/car/honda/carcontroller.py index 3b6ce3739..6a1fc1655 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/honda/carcontroller.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/honda/carcontroller.py @@ -3,8 +3,9 @@ import math 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.honda import hondacan -from iqdbc.car.honda.values import CAR, CruiseButtons, HONDA_BOSCH, HONDA_BOSCH_CANFD, HONDA_BOSCH_RADARLESS, \ +from iqdbc.car.common.pid import PIDController +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 from iqdbc.car.interfaces import CarControllerBase @@ -102,6 +103,12 @@ class CarController(CarControllerBase, AolCarController, GasInterceptorCarContro self.CAN = hondacan.CanBus(CP) 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.brake_steady = 0. self.brake_last = 0. @@ -114,6 +121,15 @@ class CarController(CarControllerBase, AolCarController, GasInterceptorCarContro self.gas = 0.0 self.brake = 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_before_maxgas = 1.0 @@ -122,9 +138,13 @@ class CarController(CarControllerBase, AolCarController, GasInterceptorCarContro self.windfactor_before_brake = 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): AolCarController.update(self, self.CP, CC, CC_IQ) gas_pedal_force = 0.0 + min_gas = self.params.BOSCH_GAS_LOOKUP_BP[0] actuators = CC.actuators hud_control = CC.hudControl 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 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.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)) + # 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. 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)) # If using stock ACC, spam cancel command to kill gas when OP disengages. 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: - 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: # Send gas and brake commands. @@ -218,19 +303,38 @@ class CarController(CarControllerBase, AolCarController, GasInterceptorCarContro ts = self.frame * DT_CTRL if self.CP.carFingerprint in HONDA_BOSCH: - self.accel = float(np.clip(accel, self.params.BOSCH_ACCEL_MIN, self.params.BOSCH_ACCEL_MAX)) - gas_pedal_force = self.accel + wind_brake_ms2 * self.windfactor + hill_brake + # low-speed extra brake: the fixed accel command under-delivers approaching a stop, so an + # 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. if (actuators.longControlState == LongCtrlState.pid) and (not CS.out.gasPressed): - gas_error = self.accel - CS.out.aEgo - if gas_error != 0.0 and gas_pedal_force > 0.0: - learn_speed = 150 if (self.CP.carFingerprint == CAR.HONDA_INSIGHT) else 50 - self.gasfactor = np.clip(self.gasfactor + gas_error / learn_speed * gas_pedal_force, 0.1, 3.0) + gas_error = accel - CS.out.aEgo + if gas_error != 0.0 and gas_pedal_force > min_gas: + if self.CP.carFingerprint in (CAR.HONDA_INSIGHT, CAR.HONDA_CIVIC_BOSCH): # gas pedal reacts too slowly + 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): - 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) - if gas_pedal_force <= 0.0: + if gas_pedal_force <= min_gas: self.windfactor = max(self.windfactor, self.windfactor_before_brake) else: self.windfactor_before_brake = self.windfactor @@ -240,12 +344,21 @@ class CarController(CarControllerBase, AolCarController, GasInterceptorCarContro else: self.gasfactor_before_maxgas = self.gasfactor 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 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, - self.stopping_counter, self.CP.carFingerprint, gas_pedal_force)) + # CAN FD: never overlap the stock radar's own ACC_CONTROL stream; ours starts within a few + # 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: 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)) @@ -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)) - # 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.CP.openpilotLongitudinalControl: + if self.CP.openpilotLongitudinalControl and self.CP.carFingerprint not in HONDA_BOSCH_CANFD: # 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, hud_control, hud_v_cruise, CS.is_metric, CS.acc_hud)) steering_available = CS.out.cruiseState.available and CS.out.vEgo > self.CP.minSteerSpeed 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, - 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: # 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)) if self.CP.carFingerprint == CAR.HONDA_CIVIC_BOSCH: 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: 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 new_actuators = actuators.as_builder() new_actuators.speed = self.speed diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/honda/carstate.py b/artifacts/package_sources/iqdbc/iqdbc/car/honda/carstate.py index f8277716c..44ffa15c4 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/honda/carstate.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/honda/carstate.py @@ -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, \ HONDA_NIDEC_ALT_SCM_MESSAGES, HONDA_BOSCH_RADARLESS, HONDA_BOSCH_TJA_CONTROL, \ HondaFlags, CruiseButtons, CruiseSettings, GearShifter, CarControllerParams +from iqdbc.car.honda.dash_objects import CameraObjectTracker from iqdbc.car.interfaces import CarStateBase 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_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]: cp = can_parsers[Bus.pt] cp_cam = can_parsers[Bus.cam] if self.CP.enableBsm: cp_body = can_parsers[Bus.body] + if self.CP.carFingerprint in HONDA_BOSCH_CANFD: + cp_radar = can_parsers[Bus.radar] ret = structs.CarState() ret_iq = structs.IQCarState() @@ -76,6 +105,10 @@ class CarState(CarStateBase, IQCarState): prev_cruise_setting = self.cruise_setting self.cruise_setting = cp.vl["SCM_BUTTONS"]["CRUISE_SETTING"] 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 # 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"]] 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 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 @@ -115,7 +148,7 @@ class CarState(CarStateBase, IQCarState): # LOW_SPEED_LOCKOUT is not worth a warning # 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 - 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 # 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"] if self.CP.carFingerprint in (HONDA_BOSCH_RADARLESS | HONDA_BOSCH_CANFD): 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: # 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), ] - 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 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 = { - Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus(CP).pt), - Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus(CP).camera), + Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], pt_messages, CanBus(CP).pt), + Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], cam_messages, CanBus(CP).camera), } if CP.enableBsm: 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 diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/honda/dash_lane.py b/artifacts/package_sources/iqdbc/iqdbc/car/honda/dash_lane.py new file mode 100644 index 000000000..e5335e790 --- /dev/null +++ b/artifacts/package_sources/iqdbc/iqdbc/car/honda/dash_lane.py @@ -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) diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/honda/dash_objects.py b/artifacts/package_sources/iqdbc/iqdbc/car/honda/dash_objects.py new file mode 100644 index 000000000..bf228a214 --- /dev/null +++ b/artifacts/package_sources/iqdbc/iqdbc/car/honda/dash_objects.py @@ -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) diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/honda/hondacan.py b/artifacts/package_sources/iqdbc/iqdbc/car/honda/hondacan.py index 818c86ec5..9d35381ec 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/honda/hondacan.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/honda/hondacan.py @@ -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) -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 = [] 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 = { 'ACCEL_COMMAND': accel_command, 'STANDSTILL': standstill, - 'BRAKE_REQUEST': braking, } - if car_fingerprint in HONDA_BOSCH_RADARLESS: + if CP.flags & HondaFlags.BOSCH_RADARLESS: acc_control_values.update({ "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: acc_control_values.update({ + 'BRAKE_REQUEST': braking, # setting CONTROL_ON causes car to set POWERTRAIN_DATA->ACC_STATUS = 1 "CONTROL_ON": control_on, "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, } + 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: acc_hud_values['ACC_ON'] = int(enabled) - acc_hud_values['FCM_OFF'] = 1 - acc_hud_values['FCM_OFF_2'] = 1 + acc_hud_values['FCM_OFF'] = 0 + acc_hud_values['FCM_OFF_2'] = 0 else: # Shows the distance bars, TODO: stock camera shows updates temporarily while disabled 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) -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 = [] lkas_hud_values = { @@ -183,14 +190,28 @@ def create_lkas_hud(packer, bus, CP, hud_control, lat_active, steering_available '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): lkas_hud_values['LANE_LINES'] = 3 - lkas_hud_values['DASHED_LANES'] = lat_active - - # car likely needs to see LKAS_PROBLEM fall within a specific time frame, so forward from camera - # TODO: needed for Bosch CAN FD? + lkas_hud_values['LKAS_PROBLEM'] = steer_fault_permanent 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): 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, {}) -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 = { - 'CRUISE_BUTTONS': button_val, - 'CRUISE_SETTING': 0, + 'CRUISE_BUTTONS': cruise_button, + '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 - bus = CAN.camera if car_fingerprint in HONDA_BOSCH_RADARLESS else CAN.pt + if bus is None: + # 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) +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: s = 0 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 while addr: s += addr & 0xF @@ -249,5 +319,5 @@ def honda_checksum(address: int, sig, d: bytearray) -> int: s += (x & 0xF) + (x >> 4) s = 8 - s if extended: - s += 3 + s += 10 if high_extended else 3 return s & 0xF diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/honda/interface.py b/artifacts/package_sources/iqdbc/iqdbc/car/honda/interface.py index bc597abec..8c15c59ff 100755 --- a/artifacts/package_sources/iqdbc/iqdbc/car/honda/interface.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/honda/interface.py @@ -2,7 +2,7 @@ import numpy as np from iqdbc.car import get_safety_config, structs, uds 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.values import CarControllerParams, HondaFlags, CAR, HONDA_BOSCH, HONDA_BOSCH_CANFD, \ HONDA_NIDEC_ALT_SCM_MESSAGES, HONDA_BOSCH_RADARLESS, HondaSafetyFlags @@ -52,9 +52,8 @@ class CarInterface(CarInterfaceBase): # Disable the radar and let openpilot control longitudinal # WARNING: THIS DISABLES AEB! # If Bosch radarless, this blocks ACC messages from the camera - # TODO: get radar disable working on Bosch CANFD - ret.alphaLongitudinalAvailable = candidate not in HONDA_BOSCH_CANFD - ret.openpilotLongitudinalControl = alpha_long and (candidate not in HONDA_BOSCH_CANFD) + ret.alphaLongitudinalAvailable = True + ret.openpilotLongitudinalControl = alpha_long ret.pcmCruise = not ret.openpilotLongitudinalControl else: ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.hondaNidec)] @@ -91,8 +90,10 @@ class CarInterface(CarInterfaceBase): 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.longitudinalActuatorDelay = 0.25 # s + elif candidate in HONDA_BOSCH_CANFD: + ret.longitudinalActuatorDelay = 0.05 # near zero, canfd seems to have stock feedforward correction else: - ret.longitudinalActuatorDelay = 0.5 # s + ret.longitudinalActuatorDelay = 0.25 # s, per Bosch A log else: # default longitudinal tuning for all hondas 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): 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]] - if candidate == CAR.HONDA_CIVIC_BOSCH: - CarControllerParams.BOSCH_GAS_LOOKUP_V = [0, 750] 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 @@ -228,9 +227,13 @@ class CarInterface(CarInterfaceBase): if candidate == CAR.HONDA_PILOT_4G: 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) - 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 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 # to a negative value, so it won't matter. Otherwise, add 0.5 mph margin to not # 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: ret.minEnableSpeed = -1. elif candidate == CAR.HONDA_ODYSSEY_TWN: @@ -277,6 +283,9 @@ class CarInterface(CarInterfaceBase): if 0x223 in fingerprint[CAN.pt]: 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 ret.flags & HondaFlagsIQ.EPS_MODIFIED: # stock request input values: 0x0000, 0x00DE, 0x014D, 0x01EF, 0x0290, 0x0377, 0x0454, 0x0610, 0x06EE @@ -349,14 +358,32 @@ class CarInterface(CarInterfaceBase): @staticmethod def init(CP, CP_IQ, can_recv, can_send, communication_control=None): if CP.carFingerprint in (HONDA_BOSCH - HONDA_BOSCH_RADARLESS) and CP.openpilotLongitudinalControl: - # 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) + if communication_control is None and CP.carFingerprint in HONDA_BOSCH_CANFD: + # CAN FD: only clear DTCs here; the radar silencing itself is deferred to CarController until + # the comma relay is confirmed open. init() runs while the panda is still in the ELM327 safety + # mode, and silencing the radar from here raced the safety-mode switch: whenever the switch + # 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 def deinit(CP, can_recv, can_send): communication_control = bytes([uds.SERVICE_TYPE.COMMUNICATION_CONTROL, 0x80 | uds.CONTROL_TYPE.ENABLE_RX_ENABLE_TX, 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) diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/honda/tests/test_canfd_carcontroller.py b/artifacts/package_sources/iqdbc/iqdbc/car/honda/tests/test_canfd_carcontroller.py new file mode 100644 index 000000000..1117823ee --- /dev/null +++ b/artifacts/package_sources/iqdbc/iqdbc/car/honda/tests/test_canfd_carcontroller.py @@ -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") diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/honda/tests/test_canfd_radar_state.py b/artifacts/package_sources/iqdbc/iqdbc/car/honda/tests/test_canfd_radar_state.py new file mode 100644 index 000000000..c4b6f3239 --- /dev/null +++ b/artifacts/package_sources/iqdbc/iqdbc/car/honda/tests/test_canfd_radar_state.py @@ -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 diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/honda/tests/test_dash_rendering.py b/artifacts/package_sources/iqdbc/iqdbc/car/honda/tests/test_dash_rendering.py new file mode 100644 index 000000000..223902d7b --- /dev/null +++ b/artifacts/package_sources/iqdbc/iqdbc/car/honda/tests/test_dash_rendering.py @@ -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 diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/honda/values.py b/artifacts/package_sources/iqdbc/iqdbc/car/honda/values.py index 28f2d7e36..de27848f8 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/honda/values.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/honda/values.py @@ -32,7 +32,7 @@ class CarControllerParams: BOSCH_ACCEL_MIN = -3.5 # 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] STEER_STEP = 1 # 100 Hz @@ -132,7 +132,7 @@ class HondaBoschPlatformConfig(PlatformConfig): @dataclass 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): super().init() diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/interfaces.py b/artifacts/package_sources/iqdbc/iqdbc/car/interfaces.py index 7f68e4079..55d457019 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/interfaces.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/interfaces.py @@ -115,9 +115,12 @@ class CarInterfaceBase(ABC, CarInterfaceBaseIQ): dbc_names = {bus: cp.dbc_name for bus, cp in self.can_parsers.items()} 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: 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) @staticmethod @@ -432,6 +435,7 @@ class CarControllerBase(ABC): self.CP_IQ = CP_IQ self.frame = 0 self.secoc_key: bytes = b"00" * 16 + self.model = None @abstractmethod def update(self, CC: structs.CarControl, CC_IQ: structs.IQCarControl, CS: CarStateBase, now_nanos: int) -> tuple[structs.CarControl.Actuators, list[CanData]]: diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/tests/test_disable_ecu.py b/artifacts/package_sources/iqdbc/iqdbc/car/tests/test_disable_ecu.py new file mode 100644 index 000000000..5fd22afeb --- /dev/null +++ b/artifacts/package_sources/iqdbc/iqdbc/car/tests/test_disable_ecu.py @@ -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 diff --git a/artifacts/package_sources/iqdbc/iqdbc/dbc/generator/honda/_bosch_2018.dbc b/artifacts/package_sources/iqdbc/iqdbc/dbc/generator/honda/_bosch_2018.dbc index d5171fdef..ad2f61afc 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/dbc/generator/honda/_bosch_2018.dbc +++ b/artifacts/package_sources/iqdbc/iqdbc/dbc/generator/honda/_bosch_2018.dbc @@ -128,6 +128,7 @@ BO_ 586 ADJACENT_RIGHT_LANE_LINE_2: 8 CAM BO_ 662 SCM_BUTTONS: 4 SCM SG_ CRUISE_BUTTONS : 7|3@0+ (1,0) [0|7] "" 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_ 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 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_ 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"; diff --git a/artifacts/package_sources/iqdbc/iqdbc/dbc/generator/honda/_lkas_hud_8byte.dbc b/artifacts/package_sources/iqdbc/iqdbc/dbc/generator/honda/_lkas_hud_8byte.dbc index d2dbed64d..bbbc85f26 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/dbc/generator/honda/_lkas_hud_8byte.dbc +++ b/artifacts/package_sources/iqdbc/iqdbc/dbc/generator/honda/_lkas_hud_8byte.dbc @@ -21,9 +21,71 @@ BO_ 829 LKAS_HUD: 8 ADAS SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" 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 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_ 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 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"; diff --git a/artifacts/package_sources/iqdbc/iqdbc/dbc/generator/honda/honda_bosch_radarless.dbc b/artifacts/package_sources/iqdbc/iqdbc/dbc/generator/honda/honda_bosch_radarless.dbc index f57265f58..5af9d5903 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/dbc/generator/honda/honda_bosch_radarless.dbc +++ b/artifacts/package_sources/iqdbc/iqdbc/dbc/generator/honda/honda_bosch_radarless.dbc @@ -7,7 +7,7 @@ CM_ "IMPORT _gearbox_common.dbc"; BO_ 456 ACC_CONTROL: 8 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_ CONTROL_ON : 10|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_ 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_ 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 COMPUTER_BRAKE_ASSIST "on hybrid and alt-brake cars set whenever braking; otherwise allows engine idle stop at a standstill"; 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"; diff --git a/artifacts/package_sources/iqdbc/iqdbc/dbc/generator/honda/honda_common_canfd.dbc b/artifacts/package_sources/iqdbc/iqdbc/dbc/generator/honda/honda_common_canfd.dbc index cd7efe7be..4c19ed968 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/dbc/generator/honda/honda_common_canfd.dbc +++ b/artifacts/package_sources/iqdbc/iqdbc/dbc/generator/honda/honda_common_canfd.dbc @@ -5,3 +5,71 @@ CM_ "IMPORT _lkas_hud_8byte.dbc"; CM_ "IMPORT _bosch_standstill.dbc"; CM_ "IMPORT _steering_sensors_a.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"; diff --git a/artifacts/package_sources/iqdbc/iqdbc/lvbs/car/honda/iq_carstate.py b/artifacts/package_sources/iqdbc/iqdbc/lvbs/car/honda/iq_carstate.py index daba99a4d..8987e5ab0 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/lvbs/car/honda/iq_carstate.py +++ b/artifacts/package_sources/iqdbc/iqdbc/lvbs/car/honda/iq_carstate.py @@ -4,6 +4,8 @@ Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed from enum import StrEnum 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.lvbs.car.honda.iq_values import HondaFlagsIQ @@ -13,10 +15,15 @@ class IQCarState: self.CP = CP 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_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: 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) diff --git a/artifacts/package_sources/iqdbc/iqdbc/lvbs/car/honda/iq_values.py b/artifacts/package_sources/iqdbc/iqdbc/lvbs/car/honda/iq_values.py index 137d5686a..d04d32964 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/lvbs/car/honda/iq_values.py +++ b/artifacts/package_sources/iqdbc/iqdbc/lvbs/car/honda/iq_values.py @@ -9,10 +9,9 @@ class HondaFlagsIQ(IntFlag): NIDEC_HYBRID = 1 EPS_MODIFIED = 2 HYBRID_ALT_BRAKEHOLD = 4 + HAS_CAMERA_MESSAGES = 8 class HondaSafetyFlagsIQ: NIDEC_HYBRID = 1 GAS_INTERCEPTOR = 2 - - diff --git a/artifacts/package_sources/iqdbc/iqdbc/lvbs/car/honda/test_honda.py b/artifacts/package_sources/iqdbc/iqdbc/lvbs/car/honda/test_honda.py index 6d68faaa5..09e34c813 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/lvbs/car/honda/test_honda.py +++ b/artifacts/package_sources/iqdbc/iqdbc/lvbs/car/honda/test_honda.py @@ -6,7 +6,7 @@ from types import SimpleNamespace from iqdbc.can import CANParser 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.honda.values import CAR from iqdbc.lvbs.car.honda.iq_carstate import IQCarState @@ -36,12 +36,13 @@ class TestHondaGasInterceptor: parser = CANParser("acura_ilx_2016_can_generated", [], 0) state = IQCarState(CP, CP_IQ) 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 not ret.gasPressed parser.vl["GAS_SENSOR"]["INTERCEPTOR_GAS"] = 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 diff --git a/artifacts/package_sources/iqdbc/iqdbc/safety/modes/honda.h b/artifacts/package_sources/iqdbc/iqdbc/safety/modes/honda.h index 025556173..f477e3116 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/safety/modes/honda.h +++ b/artifacts/package_sources/iqdbc/iqdbc/safety/modes/honda.h @@ -43,6 +43,9 @@ static bool honda_bosch_long = false; static bool honda_bosch_radarless = false; static bool honda_bosch_canfd = 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; 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 // 0x1A6 for the ILX, 0x296 for the Civic Touring 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 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, .zero_accel = 0, - .max_gas = 2000, + .max_gas = 2200, .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 // ensuring that only the cancel button press is sent (VAL 2) when controls are off. // 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) { 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 + // 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 ((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; } } @@ -362,6 +391,7 @@ static safety_config honda_nidec_init(uint16_t param) { honda_bosch_long = false; honda_bosch_radarless = false; honda_bosch_canfd = false; + honda_op_buttons_fresh = 0; 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}, {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}, - {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; @@ -458,6 +505,7 @@ static safety_config honda_bosch_init(uint16_t param) { honda_hw = HONDA_BOSCH; honda_brake_switch_prev = false; + honda_op_buttons_fresh = 0; honda_bosch_radarless = GET_FLAG(param, HONDA_PARAM_RADARLESS); honda_bosch_canfd = GET_FLAG(param, HONDA_PARAM_BOSCH_CANFD); // 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); } } 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 { if (honda_bosch_long) { SET_TX_MSGS(HONDA_BOSCH_LONG_TX_MSGS, ret); @@ -524,10 +576,34 @@ const safety_hooks honda_nidec_hooks = { .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 = { .init = honda_bosch_init, .rx = honda_rx_hook, .tx = honda_tx_hook, + .fwd = honda_bosch_fwd_hook, .get_counter = honda_get_counter, .get_checksum = honda_get_checksum, .compute_checksum = honda_compute_checksum, diff --git a/artifacts/package_sources/iqdbc/iqdbc/safety/tests/common.py b/artifacts/package_sources/iqdbc/iqdbc/safety/tests/common.py index 8b16c5a18..cffc6de42 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/safety/tests/common.py +++ b/artifacts/package_sources/iqdbc/iqdbc/safety/tests/common.py @@ -843,7 +843,10 @@ class SafetyTest(SafetyTestBase): SCANNED_ADDRS = [*range(0x800), # Entire 11-bit CAN address space *range(0x18DA00F1, 0x18DB00F1, 0x100), # 29-bit UDS physical 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_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'): 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 if attr.startswith('TestHonda'): # 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'): # exceptions for common msgs across different Hyundai CAN platforms diff --git a/artifacts/package_sources/iqdbc/iqdbc/safety/tests/test_honda.py b/artifacts/package_sources/iqdbc/iqdbc/safety/tests/test_honda.py index a12ea521e..44e005e41 100755 --- a/artifacts/package_sources/iqdbc/iqdbc/safety/tests/test_honda.py +++ b/artifacts/package_sources/iqdbc/iqdbc/safety/tests/test_honda.py @@ -31,6 +31,8 @@ class Btn: # * Bosch with Longitudinal Support # * Bosch Radarless # * Bosch Radarless with Longitudinal Support +# * Bosch CANFD +# * Bosch CANFD with Longitudinal Support class HondaButtonEnableBase(common.CarSafetyTest): @@ -372,6 +374,14 @@ class TestHondaNidecSafetyBase(HondaBase): send = brake == 0 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): """ @@ -536,7 +546,7 @@ class TestHondaBoschLongSafety(HondaButtonEnableBase, TestHondaBoschSafetyBase): Covers the Honda Bosch safety mode with longitudinal control """ NO_GAS = -30000 - MAX_GAS = 2000 + MAX_GAS = 2200 MAX_ACCEL = 2.0 # accel is used for brakes, but openpilot can set positive values 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") 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): for controls_allowed in [True, False]: 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) 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)) @@ -593,14 +609,41 @@ class TestHondaBoschRadarlessSafetyBase(TestHondaBoschSafetyBase): STEER_BUS = 0 BUTTONS_BUS = 2 # camera controls ACC, need to send buttons on bus 2 - TX_MSGS = [[0xE4, 0], [0x296, 2], [0x33D, 0]] - FWD_BLACKLISTED_ADDRS = {2: [0xE4, 0x33D]} - RELAY_MALFUNCTION_ADDRS = {0: (0xE4, 0x33D)} # STEERING_CONTROL + TX_MSGS = [[0xE4, 0], [0x296, 2], [0x33D, 0], [0x6CD5554, 0], [0xF31AA54, 0], [0x6CD5557, 0]] + FWD_BLACKLISTED_ADDRS = {2: [0xE4, 0x33D, 0x6CD5554, 0xF31AA54, 0x6CD5557]} + # STEERING_CONTROL, LANE_PATH, LKAS_HUD_2, HUD_OBJECTS + RELAY_MALFUNCTION_ADDRS = {0: (0xE4, 0x33D, 0x6CD5554, 0xF31AA54, 0x6CD5557)} def setUp(self): self.packer = CANPackerSafety("honda_bosch_radarless_generated") 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): """ @@ -629,9 +672,9 @@ class TestHondaBoschRadarlessLongSafety(common.LongitudinalAccelSafetyTest, Hond """ Covers the Honda Bosch Radarless safety mode with longitudinal control """ - TX_MSGS = [[0xE4, 0], [0x33D, 0], [0x1C8, 0], [0x30C, 0]] - FWD_BLACKLISTED_ADDRS = {2: [0xE4, 0x33D, 0x1C8, 0x30C]} - RELAY_MALFUNCTION_ADDRS = {0: (0xE4, 0x1C8, 0x30C, 0x33D)} + 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, 0x6CD5554, 0xF31AA54, 0x6CD5557]} + RELAY_MALFUNCTION_ADDRS = {0: (0xE4, 0x1C8, 0x30C, 0x33D, 0x6CD5554, 0xF31AA54, 0x6CD5557)} def setUp(self): super().setUp() @@ -655,7 +698,7 @@ class TestHondaBoschCANFDSafetyBase(TestHondaBoschSafetyBase): STEER_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]} RELAY_MALFUNCTION_ADDRS = {0: (0xE4, 0x33D)} @@ -663,6 +706,42 @@ class TestHondaBoschCANFDSafetyBase(TestHondaBoschSafetyBase): self.packer = CANPackerSafety("honda_common_canfd_generated") 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): """ @@ -686,6 +765,48 @@ class TestHondaBoschCANFDAltBrakeSafety(HondaPcmEnableBase, TestHondaBoschCANFDS 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): """ Covers the Honda Nidec safety mode with hybrid brake diff --git a/iqpilot/common/params_keys.h b/iqpilot/common/params_keys.h index 472354f00..ffe56c284 100644 --- a/iqpilot/common/params_keys.h +++ b/iqpilot/common/params_keys.h @@ -174,6 +174,7 @@ inline static std::unordered_map keys = { {"IQCarParamsCache", {CLEAR_ON_MANAGER_START, BYTES}}, {"IQCarParamsPersistent", {PERSISTENT, BYTES}}, {"IQCarParamsPersistentV2", {PERSISTENT, BYTES}}, + {"IQLongLearnedFactors", {PERSISTENT, JSON}}, {"CarPlatformBundle", {PERSISTENT, JSON}}, {"Konn3ktVwOdometers", {PERSISTENT, JSON}}, {"Konn3ktVehicleOdometers", {PERSISTENT, JSON}}, diff --git a/iqpilot/selfdrive/car/card.py b/iqpilot/selfdrive/car/card.py index 786be2ffb..4e6e84a24 100755 --- a/iqpilot/selfdrive/car/card.py +++ b/iqpilot/selfdrive/car/card.py @@ -2,6 +2,8 @@ """ Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos """ +import json +import math import os import time import threading @@ -86,7 +88,7 @@ class Car: def __init__(self, CI=None, RI=None) -> None: 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.can_rcv_cum_timeout_counter = 0 @@ -178,6 +180,9 @@ class Car: else: cloudlog.warning("Saved SecOC key is invalid") + if controller_available: + self._seed_learned_factors() + # Write previous route's CarParams prev_cp = self.params.get("CarParamsPersistent") if prev_cp is not None: @@ -259,9 +264,43 @@ class Car: 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): """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) if self.sm.frame % int(50. / DT_CTRL) == 0: 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) convert_us = (time.monotonic_ns() - started) // 1000 + model = self.sm['modelV2'] if self.sm.valid['modelV2'] else None 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 started = time.monotonic_ns() diff --git a/iqpilot/selfdrive/car/tests/test_learned_factor_persistence.py b/iqpilot/selfdrive/car/tests/test_learned_factor_persistence.py new file mode 100644 index 000000000..603f31f8a --- /dev/null +++ b/iqpilot/selfdrive/car/tests/test_learned_factor_persistence.py @@ -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 diff --git a/iqpilot/selfdrive/controls/lib/latcontrol_torque.py b/iqpilot/selfdrive/controls/lib/latcontrol_torque.py index bda8d65e1..3ac71f5a9 100644 --- a/iqpilot/selfdrive/controls/lib/latcontrol_torque.py +++ b/iqpilot/selfdrive/controls/lib/latcontrol_torque.py @@ -11,6 +11,7 @@ import numpy as np from iqpilot.cereal import log, custom # noqa: F401 (custom kept available for downstream imports) from iqdbc.car import structs 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.iq_lateral import get_friction as get_friction_in_torque_space 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.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.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.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)) expected_lateral_accel = self.lat_accel_request_buffer[-delay_frames] 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 lookahead_idx = int(np.clip(-delay_frames + self.lookahead_frames, -self.lat_accel_request_buffer_len+1, -2))