mirror of
https://gitee.com/mirrors_PX4/PX4-Autopilot.git
synced 2026-06-02 17:50:05 +08:00
Compare commits
712 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63cec47786 | |||
| 8bb863f417 | |||
| 2ae4ca4bf4 | |||
| 2fc70dea3e | |||
| 082beb885d | |||
| 6b5147110b | |||
| 576e336849 | |||
| 74408c0558 | |||
| 0e9d563570 | |||
| 50dabd8fae | |||
| f5d9491c6a | |||
| 8316d026e1 | |||
| e303e4ccfb | |||
| 616b25a280 | |||
| f11e2106af | |||
| 2f3b7b7967 | |||
| 4820a7d936 | |||
| ff1e898b72 | |||
| 73884312da | |||
| b8610ca6f4 | |||
| cdcdd1096f | |||
| acab9fdceb | |||
| 074e787a91 | |||
| 643c6fec24 | |||
| 2d79b9ea38 | |||
| afd327b322 | |||
| 1009268d31 | |||
| 4e6e2c059c | |||
| 42bedcb753 | |||
| 3f04b7a95a | |||
| bf4fac7e61 | |||
| e8e86a2e0f | |||
| a9f2e0e44e | |||
| 59ded6affd | |||
| 4a33fb169f | |||
| 11700382f6 | |||
| 3f0ddf9793 | |||
| 400bb253bd | |||
| d6e31f59cf | |||
| 3ed2f23d9c | |||
| ab6c9b7909 | |||
| eeb251aa52 | |||
| 7b3fe3478b | |||
| 7aa28de922 | |||
| a9461c4d1a | |||
| fb9f8d1835 | |||
| 6361b4cd7e | |||
| 8bb82c70ee | |||
| 0071699348 | |||
| 54df6d64a6 | |||
| 7207c34c5b | |||
| 270ad06e5f | |||
| 8bafcfbac7 | |||
| 2ff83e7e7c | |||
| 035ccc8395 | |||
| 7d84911668 | |||
| 4e279b16c2 | |||
| c5652b2084 | |||
| 03fc051c29 | |||
| 96c5c7ba02 | |||
| e9874b6f05 | |||
| 15f5a18629 | |||
| b2ea7ffab6 | |||
| 9f978b05f3 | |||
| aa998d88b8 | |||
| 7e776a7b9c | |||
| 57cf570bb4 | |||
| 55b62e5f2b | |||
| 8d99569643 | |||
| 7c1dee0b41 | |||
| 70e98f17ff | |||
| e3e26b4bfd | |||
| 51b56a7390 | |||
| 05d94b9820 | |||
| a38cf4d9e6 | |||
| d72d99f2d8 | |||
| a2808a991c | |||
| 20ded97d8a | |||
| e5071beaa3 | |||
| 2c337b77ab | |||
| a36334de50 | |||
| 02d9c32645 | |||
| 10e3c15c54 | |||
| 359b43e575 | |||
| 107b708918 | |||
| b0cc29319f | |||
| 454b690c4b | |||
| 377bec1e85 | |||
| 358574f9f6 | |||
| a32b43af0a | |||
| 48b6ec84bd | |||
| 1a0b7dae9d | |||
| 0640cc9e35 | |||
| 6fd3c88bb0 | |||
| cdacb01f55 | |||
| 029edb50b3 | |||
| b6164107d1 | |||
| af3cfaea25 | |||
| 44b2d8f845 | |||
| ee636a0e3d | |||
| a631716265 | |||
| 1dadd92a86 | |||
| 339882c6ad | |||
| 3e396f65e5 | |||
| 3aa499dfce | |||
| 4da97eb4fd | |||
| 343fd01e19 | |||
| ec56d2d83b | |||
| 26969c25ff | |||
| 4429c53f93 | |||
| 5cdf5ac482 | |||
| 7ee02968ac | |||
| ce828af85c | |||
| b5deafdc92 | |||
| 6558928069 | |||
| c85b3cdd61 | |||
| 8340415962 | |||
| 1a67c3d50a | |||
| 5500ebb1d0 | |||
| 52203a6bb7 | |||
| 844ba41a28 | |||
| 010f6dcbae | |||
| 61d2173524 | |||
| 16d938cda9 | |||
| 7c4c773858 | |||
| 19b5292dff | |||
| fa2d1c3662 | |||
| 7f3e0e9679 | |||
| 9228dca9bd | |||
| 40133e0b2c | |||
| c677cb75df | |||
| b79ed50615 | |||
| 94c3765712 | |||
| 30b6938f5e | |||
| 62d0620eff | |||
| 102b64f604 | |||
| 41b40e34fa | |||
| b37733459d | |||
| 5528ebec64 | |||
| b4d5eca3c0 | |||
| 9fcb6bcc0a | |||
| ab318cb636 | |||
| f11329784f | |||
| 4d85c1ad93 | |||
| 040b885dbd | |||
| 84e7c8e681 | |||
| ddb83e8d4d | |||
| 31977d8d19 | |||
| 1cb2debbb9 | |||
| 2cb9b9bfbe | |||
| 85ca947e09 | |||
| 88a57c5b65 | |||
| 8ed35be826 | |||
| de9698e7fa | |||
| c5d22f5fea | |||
| 192ac7bb54 | |||
| 7c78efe0c4 | |||
| ebc2093e52 | |||
| 6b51eddecc | |||
| 60872afd90 | |||
| 906b87581c | |||
| 38eb03c91f | |||
| 6bf73d9d89 | |||
| 4ac8ceff31 | |||
| c7aaacc8b4 | |||
| b9093340e2 | |||
| f8157f9308 | |||
| 3be015c3cb | |||
| a01044655b | |||
| 2fd131d3cf | |||
| 67c4256c08 | |||
| 16e6036536 | |||
| 18a07d2d7c | |||
| defab5114d | |||
| 24197831e6 | |||
| 2d69eaee74 | |||
| 7f5de5d141 | |||
| 43aa8de22b | |||
| f53a96be5d | |||
| 9c2e8aff0f | |||
| 6bf4745144 | |||
| 9cb3ea44fb | |||
| 764e621b63 | |||
| a38e0405f1 | |||
| d9a7c75ae5 | |||
| 963a776fa6 | |||
| d5a47925ab | |||
| c2ea4e6121 | |||
| 12babb33cb | |||
| 02e73e3b6f | |||
| 9e41ffd537 | |||
| e5a483e4b7 | |||
| 209c971c1b | |||
| a74724193e | |||
| 1550c8d245 | |||
| d495014878 | |||
| affc18a056 | |||
| 25cfb4f790 | |||
| 344bb6af83 | |||
| cccff98454 | |||
| fab9874183 | |||
| 726cb2c6eb | |||
| 57a380d8ec | |||
| 11378639ae | |||
| 9ff373c325 | |||
| d317113dc8 | |||
| c424edd055 | |||
| 4bfc5bce78 | |||
| 59358ae60d | |||
| 2450eb3d95 | |||
| 14e3a2da03 | |||
| bea52104b7 | |||
| c6a2fce9a3 | |||
| c0af8b9952 | |||
| 444897081e | |||
| 1beb6d70f0 | |||
| 106276978d | |||
| 539b1def78 | |||
| 3c961de3fe | |||
| ce564b4448 | |||
| 4a4c15765c | |||
| 5395b3823a | |||
| c1938b8a32 | |||
| 8a8496d57e | |||
| 48525073aa | |||
| cf51a72846 | |||
| fef12c9038 | |||
| 8fbe4a4f7d | |||
| 50c2055efe | |||
| b0b6771118 | |||
| 4226b7d4ec | |||
| 04f4ca8966 | |||
| 54c1d121e4 | |||
| a5c67b90a9 | |||
| 4a0e257fc9 | |||
| bd76832f34 | |||
| 6e418096b7 | |||
| b0b99de767 | |||
| 55d35ddf37 | |||
| 207456fd35 | |||
| a4f9786c3d | |||
| 8fd3d3268a | |||
| af25a31861 | |||
| 77a3ab7aad | |||
| 51be1e3fb9 | |||
| 637cece115 | |||
| d965df930c | |||
| 845a7efd58 | |||
| 30cbf84fd8 | |||
| 76fbac4dee | |||
| 24833f41e5 | |||
| 8d352cd8e0 | |||
| e2708705a8 | |||
| 039ac8c4c1 | |||
| 4880bd5d8c | |||
| 8f870a1346 | |||
| 9048a40277 | |||
| 9fb0ff0e80 | |||
| 7297364484 | |||
| 9e796daee8 | |||
| 17242bc1a4 | |||
| 8a9be9a8f0 | |||
| b346fcfa00 | |||
| 1d852907a2 | |||
| dd177ac8cf | |||
| 68b533f79f | |||
| 7daac63809 | |||
| 2ef5b0a20f | |||
| 2e2067173a | |||
| 93f06f6a23 | |||
| ca0dec5a33 | |||
| 790c2d3369 | |||
| 8478503349 | |||
| 6a18fd045f | |||
| 06942bbfcc | |||
| 89c4980e55 | |||
| e756574420 | |||
| df00901bfa | |||
| 918efc8f97 | |||
| 8602849847 | |||
| d564c5b4c2 | |||
| 245ae58264 | |||
| 996060f581 | |||
| a120773793 | |||
| 6cf8d80bdd | |||
| b4601278db | |||
| 0d66981dcc | |||
| 62f5f5267e | |||
| fd7edaa4fe | |||
| b343eb6a11 | |||
| b8877c4cfc | |||
| 64e996b475 | |||
| 1d45f699be | |||
| be126454c0 | |||
| ce3e62841f | |||
| 657854ae1b | |||
| 5613313107 | |||
| eafb6c396b | |||
| 2ebfd40bba | |||
| 368dd362c5 | |||
| 7332f264f0 | |||
| 06d6c31614 | |||
| d9448f3e99 | |||
| 50a42680d7 | |||
| 70a84a0c1b | |||
| 05de941399 | |||
| adc9a6d35d | |||
| fe91ace0bb | |||
| 977777f40a | |||
| 4afdf38378 | |||
| d0004403a3 | |||
| 2e651117e8 | |||
| 18c176beef | |||
| 602add3ec1 | |||
| 47d5971f42 | |||
| 7ef57f6262 | |||
| 2a0b795760 | |||
| 911fc81c59 | |||
| b2fc5993cc | |||
| d5ddc9135d | |||
| 6b67ccb0ad | |||
| 0e6b904e80 | |||
| 864df9fc7b | |||
| d17a5b2c26 | |||
| d46a9266ce | |||
| 3a6f566e80 | |||
| d84903d520 | |||
| 4331f880f5 | |||
| 11007dc893 | |||
| fd4b958790 | |||
| a06f062bf7 | |||
| 79bf7810d4 | |||
| 066e8f7fea | |||
| 08dc2a776e | |||
| 061fe4806e | |||
| 4bebbbae93 | |||
| e52ce5c43b | |||
| 32c94bd3b1 | |||
| b08fefa903 | |||
| 302d0601bf | |||
| c90811a277 | |||
| 79a7ef2869 | |||
| b9f4de0b51 | |||
| 14cbcee49f | |||
| f38aba3c5b | |||
| 84933cfbdf | |||
| be3e1fb2ef | |||
| 82850cb149 | |||
| 3d457528d2 | |||
| d74007dc87 | |||
| 47b3f5f6f9 | |||
| 6d8441dc89 | |||
| f29afe1342 | |||
| 9849d90877 | |||
| 497704f3b9 | |||
| b60aa5dd2b | |||
| 5d151c54a4 | |||
| 83a4d648e3 | |||
| 0646fa6c9d | |||
| d9b3e48ec5 | |||
| 29fefeeada | |||
| 618a6aa98f | |||
| 8a007d38e7 | |||
| e831c66ae1 | |||
| 021eee0c5c | |||
| 385450ca37 | |||
| 23c9af20da | |||
| d196d37ef2 | |||
| dc4aa749d3 | |||
| 767eb75662 | |||
| c29630f6ae | |||
| c511e72d4f | |||
| a235b5c87f | |||
| 87163c1578 | |||
| 841fccf6b9 | |||
| 8a3e227dc0 | |||
| ad0b6bdc6b | |||
| aecd1461d7 | |||
| 8017baa6e6 | |||
| 103a61450e | |||
| df242827d2 | |||
| d9996742be | |||
| f518f87d0f | |||
| 6a123298e5 | |||
| a38b10c9d0 | |||
| d641cc3986 | |||
| 731d754a15 | |||
| 7edf21414e | |||
| ec278758ed | |||
| 53a14d10cd | |||
| d6c4dd22da | |||
| 4117912506 | |||
| e239c017d1 | |||
| 5f0e3f600f | |||
| 61da2505fe | |||
| 27d831fbcd | |||
| 71fbc63d67 | |||
| a6d50d02bc | |||
| c56bc4208d | |||
| 741892c30f | |||
| ad6ee2aadf | |||
| b3c05bf4da | |||
| 0134d7dd3b | |||
| 07d9167d66 | |||
| e945c91f88 | |||
| 8bb1e44c10 | |||
| 36b5e1e1bb | |||
| 01b348c86b | |||
| 907823f9c5 | |||
| d886db8aab | |||
| 1c929da8ba | |||
| 6362a44cc4 | |||
| 42b0061217 | |||
| caf9feba73 | |||
| f751974b41 | |||
| 3e1b7d4d0a | |||
| 6ef0e57eeb | |||
| 4c866fc9b2 | |||
| 93fd9d4bae | |||
| 2ec9937369 | |||
| 3c80be8e48 | |||
| 89b5a61f79 | |||
| 06bb7a6e1b | |||
| d63f47b5b3 | |||
| 61a67b3a09 | |||
| 0966e92e36 | |||
| 273d3cedac | |||
| 2290096e25 | |||
| 0efded2fa7 | |||
| 1c13f28121 | |||
| 94514bd811 | |||
| 85aa863c56 | |||
| 380ae3047d | |||
| ba72434fe4 | |||
| 60df456168 | |||
| f11ea548da | |||
| 3fdd3e3e43 | |||
| f50d8b258c | |||
| dfcde09899 | |||
| b3c27bc91b | |||
| c66584879c | |||
| 7aca05a531 | |||
| c032138a34 | |||
| bc75b5cc0d | |||
| b70da3968c | |||
| 853fa088de | |||
| e80abe4e29 | |||
| 593192df54 | |||
| 166e7a2fd7 | |||
| 57843adb48 | |||
| 7be82b08c4 | |||
| d530bd5a7f | |||
| b6ace98962 | |||
| 75e2a38a05 | |||
| f73ac7a257 | |||
| 1a46de3a25 | |||
| 239a7bc4f1 | |||
| c5c66ee261 | |||
| 87f1eb69b1 | |||
| 86c38ae384 | |||
| b43a842bc8 | |||
| d227cfc174 | |||
| 24ac02fbac | |||
| bc6cc5f82a | |||
| 90169b932b | |||
| 7f010c1430 | |||
| d018551047 | |||
| 509b4ba96d | |||
| 64e3cb16fa | |||
| 77d8e4f213 | |||
| ecf8191aad | |||
| 3fb1459c33 | |||
| ee196fadb8 | |||
| fd9abf76fd | |||
| a98d68f919 | |||
| 7d2d85e6ac | |||
| 34845a62b0 | |||
| 7715a4ab4c | |||
| ea2ca45cf9 | |||
| 14864814ef | |||
| 3a6a57bd45 | |||
| e0af9f4ef4 | |||
| 0294d4a794 | |||
| d411e1c40d | |||
| 833e4536b7 | |||
| 1b7e12cf90 | |||
| aacb7e35dd | |||
| 1dbee4100a | |||
| dbb00d500f | |||
| 61a8ae80a6 | |||
| f8329ff80d | |||
| b4b1b44c6a | |||
| f5a56ae42f | |||
| c4535683a7 | |||
| f9cdd095b8 | |||
| 61c990c5ee | |||
| 8115cf2597 | |||
| eb43d21730 | |||
| 5e54d727fc | |||
| ecb222c7e7 | |||
| a5a7dd802c | |||
| 7b72335876 | |||
| 446895fdc0 | |||
| 3eb0255922 | |||
| 271d3f01a3 | |||
| e8fca6e991 | |||
| de1314f995 | |||
| 1bfc0da258 | |||
| 41966774c2 | |||
| 82e3322e0c | |||
| e2e89def7e | |||
| d5c4ace615 | |||
| 390c9d6ccf | |||
| 63c4f4ac3e | |||
| 40dc011d82 | |||
| 8d97013822 | |||
| 4e59a060a8 | |||
| f8c1e8b81f | |||
| 6be1a14e06 | |||
| 3075724f9e | |||
| e37a216393 | |||
| 90fec17427 | |||
| 03264ce1a7 | |||
| ac4f419b50 | |||
| 193a4478ed | |||
| 744548e9f2 | |||
| b9bd820186 | |||
| e0f1022681 | |||
| 4c184f309c | |||
| adf1bab518 | |||
| f5c5f2ed8c | |||
| 09d3f05bcd | |||
| b7d9876cd9 | |||
| bd6b0699cc | |||
| 684ba28fbf | |||
| 1797ce4e88 | |||
| 05517935dc | |||
| 4af33cef43 | |||
| c90095e8b4 | |||
| 65cedc8bf8 | |||
| 9be7585add | |||
| 2282330102 | |||
| 0676647d8a | |||
| 50bb31491b | |||
| cac3c3c133 | |||
| c2490e01a5 | |||
| 01d8113f8b | |||
| 091ac918b1 | |||
| c0c265cd1f | |||
| fad4450d7d | |||
| 1cfab8feb2 | |||
| e2864e521f | |||
| 9460625c99 | |||
| 32fc5cb5b9 | |||
| 7a6506f2dd | |||
| a416437561 | |||
| 554b52c6a1 | |||
| fc992385a9 | |||
| 0577a40440 | |||
| 2239c10192 | |||
| 8117fce790 | |||
| 18c3d889fe | |||
| 31c7d70342 | |||
| 6386f10ba2 | |||
| deb9a1ad4e | |||
| f685df32bc | |||
| 432b0e8c58 | |||
| 024b3d27ac | |||
| 5d5e1db97f | |||
| d3da4fe608 | |||
| 76b58b6f0b | |||
| 75d7395daa | |||
| 11f4d5c4e7 | |||
| 522c15284f | |||
| 93ab802202 | |||
| 3381b270ea | |||
| aed8a78c1d | |||
| 9cf07c2452 | |||
| 230276540f | |||
| 52308735a7 | |||
| b1c2820d69 | |||
| 1df5e62cc3 | |||
| 2ad25570ee | |||
| b8577c753f | |||
| c9e3118fea | |||
| 55e0810d8d | |||
| 4ac7853479 | |||
| 06db25c078 | |||
| 5635d639fe | |||
| bbce142129 | |||
| 475715818b | |||
| 02103b9100 | |||
| 25de111a4a | |||
| 9169f9cd44 | |||
| 273766a4ea | |||
| f98fdbc452 | |||
| edc2536bdd | |||
| e90f8b500f | |||
| fbdc31b60c | |||
| 392002f671 | |||
| c0c7f6ec40 | |||
| 8c5c4a0504 | |||
| 0da6efa52d | |||
| 9de10d672c | |||
| 0fa5a83409 | |||
| aed175451a | |||
| df42ef84f1 | |||
| fe30ef7f16 | |||
| 0b834dd0e7 | |||
| 8e658a6e2d | |||
| db2c6b2abe | |||
| 95b8328162 | |||
| 6de6abfb64 | |||
| e371c4edd9 | |||
| 473ef5fd06 | |||
| 46d9b14ba0 | |||
| 66e21497a6 | |||
| de49edc428 | |||
| b5846fd8c2 | |||
| ec6dd286fc | |||
| cf50ecf41b | |||
| 9fe69d4f33 | |||
| f4247aee58 | |||
| ec8f34325e | |||
| 0e615816b3 | |||
| 4cf4f82233 | |||
| 5f83c186ee | |||
| e761297003 | |||
| 27181619fc | |||
| 210239324d | |||
| c71e2d41d6 | |||
| 7c318a3296 | |||
| 0375f1a6f0 | |||
| 0ef4f1b7bd | |||
| 598066653a | |||
| 86e1356e0a | |||
| 450cf79fc8 | |||
| 1ff36422c9 | |||
| 38b6a9abf3 | |||
| 819c783b6c | |||
| 14558e9304 | |||
| 6bc24c8cd1 | |||
| d8220d4948 | |||
| 435040f2d5 | |||
| dad58f31a6 | |||
| 6ecbfb2262 | |||
| 9563f4c8c6 | |||
| cea6c10864 | |||
| 1e34c80115 | |||
| 076b673977 | |||
| 00f2c149d1 | |||
| e370d15c72 | |||
| aa850d5c1c | |||
| 1e618a7eb0 | |||
| f46fd89057 | |||
| 33c7eed5f1 | |||
| 96f428b683 | |||
| 722133f79a | |||
| 66c5644f1b | |||
| 3ef13da044 | |||
| 161b530247 | |||
| b5c0867101 | |||
| ca6d257ed8 | |||
| c51502781f | |||
| 339a0b40af | |||
| d2aa1b801c | |||
| 26d847e6e7 | |||
| 1cf2dc8791 | |||
| 209a9935e7 | |||
| 0fa667fd92 | |||
| bbf32a537e | |||
| 3e425210e0 | |||
| f25997a15b | |||
| e132568430 | |||
| 6195629373 | |||
| 2c044b327e | |||
| b7ffd6ea2c | |||
| b26db22c1a | |||
| 6f18ff8ff9 | |||
| f224b81eec | |||
| b537601b7a | |||
| e01c4b0692 | |||
| ca43fc8f5a | |||
| 11cac778ba | |||
| 441892b33f | |||
| 1d5a8531e4 | |||
| e71348967d | |||
| fbe49db571 | |||
| 4b36cfccfc | |||
| 699ec30c9c | |||
| e6c49edd20 | |||
| cb6bdee4c2 | |||
| 3438d593a1 | |||
| 38f89a8b69 | |||
| 3e290695ef | |||
| 11bf3cffde | |||
| ba4437ae60 | |||
| b64860f9f8 | |||
| e51d09612f | |||
| eb1c0322e3 | |||
| e822da74b2 | |||
| ccaad82f61 | |||
| 4e5c1140b7 | |||
| 4acb8ec799 | |||
| 89d43185c7 | |||
| df0a197050 | |||
| c726c7e4f4 | |||
| 24d06047bd | |||
| cb682006fe | |||
| 5632728467 | |||
| 778ad160f2 | |||
| 8393f46100 |
+75
@@ -105,6 +105,81 @@ Checks: '*,
|
||||
-readability-redundant-declaration,
|
||||
-readability-static-accessed-through-instance,
|
||||
-readability-static-definition-in-anonymous-namespace,
|
||||
-altera-struct-pack-align,
|
||||
-bugprone-easily-swappable-parameters,
|
||||
-concurrency-mt-unsafe,
|
||||
-cppcoreguidelines-avoid-const-or-ref-data-members,
|
||||
-cppcoreguidelines-macro-usage,
|
||||
-cppcoreguidelines-non-private-member-variables-in-classes,
|
||||
-hicpp-uppercase-literal-suffix,
|
||||
-llvm-qualified-auto,
|
||||
-misc-non-private-member-variables-in-classes,
|
||||
-misc-use-anonymous-namespace,
|
||||
-modernize-concat-nested-namespaces,
|
||||
-readability-const-return-type,
|
||||
-readability-identifier-length,
|
||||
-readability-isolate-declaration,
|
||||
-readability-qualified-auto,
|
||||
-readability-redundant-access-specifiers,
|
||||
-cppcoreguidelines-avoid-do-while,
|
||||
-misc-include-cleaner,
|
||||
-misc-const-correctness,
|
||||
-llvm-else-after-return,
|
||||
-readability-function-cognitive-complexity,
|
||||
-cppcoreguidelines-init-variables,
|
||||
-bugprone-reserved-identifier,
|
||||
-cert-dcl37-c,
|
||||
-cert-dcl51-cpp,
|
||||
-modernize-use-nodiscard,
|
||||
-misc-confusable-identifiers,
|
||||
-cert-err33-c,
|
||||
-readability-redundant-inline-specifier,
|
||||
-readability-uppercase-literal-suffix,
|
||||
-bugprone-narrowing-conversions,
|
||||
-cppcoreguidelines-narrowing-conversions,
|
||||
-bugprone-switch-missing-default-case,
|
||||
-cppcoreguidelines-avoid-goto,
|
||||
-hicpp-avoid-goto,
|
||||
-bugprone-branch-clone,
|
||||
-bugprone-unhandled-self-assignment,
|
||||
-cert-oop54-cpp,
|
||||
-performance-enum-size,
|
||||
-readability-avoid-nested-conditional-operator,
|
||||
-cppcoreguidelines-prefer-member-initializer,
|
||||
-cppcoreguidelines-explicit-virtual-functions,
|
||||
-readability-convert-member-functions-to-static,
|
||||
-readability-make-member-function-const,
|
||||
-bugprone-implicit-widening-of-multiplication-result,
|
||||
-bugprone-multi-level-implicit-pointer-conversion,
|
||||
-bugprone-signed-char-misuse,
|
||||
-cppcoreguidelines-avoid-non-const-global-variables,
|
||||
-cppcoreguidelines-use-default-member-init,
|
||||
-hicpp-multiway-paths-covered,
|
||||
-hicpp-named-parameter,
|
||||
-misc-header-include-cycle,
|
||||
-misc-no-recursion,
|
||||
-performance-no-int-to-ptr,
|
||||
-readability-avoid-return-with-void-value,
|
||||
-readability-avoid-unconditional-preprocessor-if,
|
||||
-readability-delete-null-pointer,
|
||||
-readability-redundant-casting,
|
||||
-readability-redundant-member-init,
|
||||
-readability-reference-to-constructed-temporary,
|
||||
-readability-simplify-boolean-expr,
|
||||
-cert-msc32-c,
|
||||
-cert-msc33-c,
|
||||
-cert-msc51-cpp,
|
||||
-cert-str34-c,
|
||||
-cppcoreguidelines-macro-to-enum,
|
||||
-modernize-macro-to-enum,
|
||||
-abseil-string-find-str-contains,
|
||||
-bugprone-suspicious-include,
|
||||
-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,
|
||||
-clang-analyzer-optin.core.EnumCastOutOfRange,
|
||||
-modernize-type-traits,
|
||||
-misc-definitions-in-headers,
|
||||
-bugprone-casting-through-void,
|
||||
-readability-redundant-string-init,
|
||||
'
|
||||
WarningsAsErrors: '*'
|
||||
CheckOptions:
|
||||
|
||||
@@ -3,92 +3,45 @@ description: Create a report to help us improve
|
||||
title: "[Bug] "
|
||||
labels: ["bug-report"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Tips for a great bug report:**
|
||||
- Describe what went wrong and what you expected
|
||||
- Include a flight log link from [logs.px4.io](http://logs.px4.io/) if possible
|
||||
- Mention your PX4 version, flight controller, and vehicle type if relevant
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of the bug.
|
||||
description: A clear description of the bug and what you expected to happen.
|
||||
placeholder: |
|
||||
What happened and what did you expect instead?
|
||||
|
||||
Steps to reproduce (if applicable):
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: To Reproduce
|
||||
label: Flight Log / Additional Information
|
||||
description: |
|
||||
Steps to reproduce the behavior.
|
||||
1. Drone switched on '...'
|
||||
2. Uploaded mission '....' (attach QGC mission file)
|
||||
3. Took off '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: false
|
||||
**Flight log** (highly recommended for flight-related issues):
|
||||
- Upload to [PX4 Flight Review](http://logs.px4.io/) and paste the link
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Screenshot / Media
|
||||
description: Add screenshot / media if you have them
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Flight Log
|
||||
description: |
|
||||
*Always* provide a link to the flight log file:
|
||||
- Download the flight log file from the vehicle ([tutorial](https://docs.px4.io/main/en/getting_started/flight_reporting.html)).
|
||||
- Upload the log to the [PX4 Flight Review](http://logs.px4.io/)
|
||||
- Share the link to the log (Copy and paste the URL of the log)
|
||||
**Additional details** (if relevant):
|
||||
- PX4 version (output of `ver all` in MAVLink Shell)
|
||||
- Flight controller model
|
||||
- Vehicle type (multicopter, fixed-wing, VTOL, etc.)
|
||||
- Screenshots or media
|
||||
placeholder: |
|
||||
# PASTE HERE THE LINK TO THE LOG
|
||||
Flight log link:
|
||||
|
||||
Version:
|
||||
|
||||
Hardware:
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Setup
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Software Version
|
||||
description: |
|
||||
Which version of PX4 are you using?
|
||||
placeholder: |
|
||||
# If you don't know the version, paste the output of `ver all` in the MAVLink Shell of QGC
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Flight controller
|
||||
description: Specify your flight controller model (what type is it, where was it bought from, ...).
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Vehicle type
|
||||
options:
|
||||
- Multicopter
|
||||
- Helicopter
|
||||
- Fixed Wing
|
||||
- Hybrid VTOL
|
||||
- Airship/Balloon
|
||||
- Rover
|
||||
- Boat
|
||||
- Submarine
|
||||
- Other
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: How are the different components wired up (including port information)
|
||||
description: Details about how all is wired.
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
blank_issues_enabled: false
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Support Question
|
||||
url: https://docs.px4.io/main/en/contribute/support.html#forums-and-chat
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
applyTo: "docs/en/**"
|
||||
---
|
||||
|
||||
# Review Guidelines docs/en Tree
|
||||
|
||||
## File System & Structure
|
||||
|
||||
- **Naming:** Use `lowercase_with_underscores` for all filenames. No spaces.
|
||||
- **Hierarchy:** Markdown files must reside exactly in a first-level category folder.
|
||||
- Valid: `docs/en/category/file.md`
|
||||
- Invalid: `docs/en/category/subcategory/file.md`
|
||||
- **Text Files:** Any `.txt` or `.text` files must start with an underscore (e.g., `_notes.txt`).
|
||||
- **Assets:** All images/non-docs must be in `/docs/assets/`. Deep nesting is permitted here.
|
||||
- **Formats:** Prefer **SVG** for diagrams and **PNG** for screenshots. Flag JPG files.
|
||||
|
||||
## Markdown & Style
|
||||
|
||||
- **Headings:** Use Title Case ("First Letter Capitalisation").
|
||||
- The Page Title must be the only H1 (`#`). All others must be `##` or lower.
|
||||
- Do not apply bold or italic styling inside a heading.
|
||||
- **Formatting:**
|
||||
- **Bold:** Only for UI elements (buttons, menu items).
|
||||
- **Inline Code:** Use backticks for file paths, parameters, and CLI commands (e.g., `prettier`).
|
||||
Backticks are optional for hyperlinked CLI commands and tool names.
|
||||
- **Italics (Emphasis):** Use for application names (e.g., *QGroundControl*).
|
||||
Emphasis is optional for hyperlinked applications.
|
||||
- **Structure:** End every line at the end of a sentence (Semantic Line Breaks).
|
||||
|
||||
## Linking & Navigation
|
||||
|
||||
- **Standard Links:** Use standard inline syntax: `[link text](../category/filename.md)`.
|
||||
Note relative link.
|
||||
- **Table Links:** To keep tables readable, use reference-style links.
|
||||
- Definition: `[Link Name]: https://example.com` (placed below the table).
|
||||
- Usage: `[Link Name]` within the table cell.
|
||||
- **Images:** All image links must include a descriptive, accessible alt-text in the brackets: ``.
|
||||
Note that all images should be relative references to images stored in the assets folder, which should be two folders below the any markdown file (as they are stored in a "category" subfolder)
|
||||
|
||||
- **Standard Links:** Use standard inline syntax: `[link text](../category/filename.md)`. Note the use of relative links.
|
||||
- **Table Links:** To keep tables easier to edit, prefer reference-style links.
|
||||
- Definition: `[Link Name]: https://example.com` (placed below the table).
|
||||
- Usage: `[Link Name]` within the table cell.
|
||||
- **Images:** All image links must include a descriptive, accessible alt-text: ``.
|
||||
- **Note:** All images must be relative references to the `/docs/assets/` folder. Since documents are nested in a category folder, this is usually two levels up (`../../assets/`).
|
||||
|
||||
## Quality Control
|
||||
|
||||
- **Prettier Check:** Ensure Prettier rules have been applied. If there is evidence of inconsistent indentation or spacing, request the author run `npx prettier --write .` before merging.
|
||||
- **Language:** Enforce **UK English** spelling and grammar.
|
||||
@@ -2,6 +2,37 @@
|
||||
# - If you want to keep the tests running in GitHub Actions you need to uncomment the "runs-on: ubuntu-latest" lines
|
||||
# and comment the "runs-on: [runs-on,runner=..." lines.
|
||||
# - If you would like to duplicate this setup try setting up "RunsOn" on your own AWS account try https://runs-on.com
|
||||
#
|
||||
# ===================================================================================
|
||||
# RELEASE UPLOAD LOGIC
|
||||
# ===================================================================================
|
||||
# This workflow handles building firmware and uploading to S3 + GitHub Releases.
|
||||
#
|
||||
# S3 Bucket Structure (s3://px4-travis/Firmware/):
|
||||
# - master/ <- Latest main branch build (for QGC compatibility)
|
||||
# - stable/ <- Latest stable release, controlled by 'stable' branch
|
||||
# - beta/ <- Latest pre-release, controlled by 'beta' branch
|
||||
# - vX.Y.Z/ <- Archived stable release
|
||||
# - vX.Y.Z-beta1/ <- Archived pre-release
|
||||
#
|
||||
# Trigger Behavior:
|
||||
# - Tag v1.16.1 -> Upload to: v1.16.1/ only (versioned archive)
|
||||
# - Tag v1.17.0-beta1 -> Upload to: v1.17.0-beta1/ only (versioned archive)
|
||||
# - Branch main -> Upload to: master/ (for QGC compatibility)
|
||||
# - Branch stable -> Upload to: stable/ (QGC stable firmware)
|
||||
# - Branch beta -> Upload to: beta/ (QGC beta firmware)
|
||||
# - Branch release/** -> Build only, no S3 upload (CI validation)
|
||||
# - Pull requests -> Build only, no S3 upload (CI validation)
|
||||
#
|
||||
# GitHub Releases:
|
||||
# - All version tags create a draft GitHub Release
|
||||
# - Pre-releases (alpha/beta/rc suffixes) are automatically marked as such
|
||||
#
|
||||
# IMPORTANT: Version tags do NOT upload to stable/ or beta/. Only the
|
||||
# corresponding branch pushes control those directories. This prevents
|
||||
# pre-release tags from accidentally overwriting stable firmware (#26340)
|
||||
# and avoids race conditions between tag and branch builds.
|
||||
# ===================================================================================
|
||||
|
||||
name: Build all targets
|
||||
|
||||
@@ -29,6 +60,7 @@ concurrency:
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
packages: read
|
||||
|
||||
jobs:
|
||||
group_targets:
|
||||
@@ -94,6 +126,9 @@ jobs:
|
||||
fail-fast: false
|
||||
container:
|
||||
image: ${{ matrix.container }}
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: runs-on/action@v2
|
||||
- uses: actions/checkout@v4
|
||||
@@ -159,6 +194,13 @@ jobs:
|
||||
path: ~/.ccache
|
||||
key: ${{ steps.cc_restore.outputs.cache-primary-key }}
|
||||
|
||||
# ===========================================================================
|
||||
# ARTIFACT UPLOAD JOB
|
||||
# ===========================================================================
|
||||
# Uploads build artifacts to S3 and creates GitHub Releases.
|
||||
# Runs for version tags (v*), main, stable, and beta branch pushes.
|
||||
# See header comments for full upload logic documentation.
|
||||
# ===========================================================================
|
||||
artifacts:
|
||||
name: Upload Artifacts
|
||||
# runs-on: ubuntu-latest
|
||||
@@ -177,31 +219,31 @@ jobs:
|
||||
- name: Choose Upload Location
|
||||
id: upload-location
|
||||
run: |
|
||||
# Determine upload location based on branch or tag with the following considerations:
|
||||
# Destination: AWS S3 bucket px4-travis in folder Firmware/
|
||||
# - If branch is main -> upload to master/
|
||||
# - Older versions of QGC are hardocded to look for master/
|
||||
# - If branch is stable or beta -> upload to stable/ or beta/
|
||||
# - If a tag vX.Y.Z -> upload to vX.Y.Z/
|
||||
# - Also update stable/ to point to the same version
|
||||
#. - Older versions of QGC are hardocded to look for stable/
|
||||
# - If a pull request -> do not upload
|
||||
set -euo pipefail
|
||||
|
||||
ref="${GITHUB_REF}"
|
||||
branch=${{ needs.group_targets.outputs.branchname }}
|
||||
location="$branch"
|
||||
is_prerelease="false"
|
||||
|
||||
# Main branch uploads to "master" for QGC backward compatibility
|
||||
if [[ "$branch" == "main" ]]; then
|
||||
location="master"
|
||||
fi
|
||||
|
||||
# Version tags: upload to versioned directory (e.g., v1.16.1/)
|
||||
if [[ "$ref" == refs/tags/v[0-9]* ]]; then
|
||||
tag="${ref#refs/tags/}"
|
||||
location="$tag"
|
||||
|
||||
# Pre-release tags contain -alpha, -beta, or -rc suffix
|
||||
if [[ "$tag" =~ -(alpha|beta|rc) ]]; then
|
||||
is_prerelease="true"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "uploadlocation=$location" >> $GITHUB_OUTPUT
|
||||
echo "is_prerelease=$is_prerelease" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Uploading Artifacts to S3 [${{ steps.upload-location.outputs.uploadlocation }}]
|
||||
uses: jakejarvis/s3-sync-action@master
|
||||
@@ -215,28 +257,13 @@ jobs:
|
||||
SOURCE_DIR: artifacts/
|
||||
DEST_DIR: Firmware/${{ steps.upload-location.outputs.uploadlocation }}/
|
||||
|
||||
# if we are uploading artifacts to a versioned folder
|
||||
# we should also update the stable folder in the s3 bucket
|
||||
- name: Uploading Artifacts to S3 [stable]
|
||||
uses: jakejarvis/s3-sync-action@master
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
args: --acl public-read
|
||||
env:
|
||||
AWS_S3_BUCKET: 'px4-travis'
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: 'us-west-1'
|
||||
SOURCE_DIR: artifacts/
|
||||
DEST_DIR: Firmware/stable/
|
||||
|
||||
# if build is a release triggered by a versioned tag then create a github release
|
||||
# and upload the build artifacts. A draft release is created so that the release
|
||||
# can be reviewed before publishing
|
||||
# Create a draft GitHub Release for all version tags
|
||||
# Pre-releases are automatically marked as such
|
||||
- name: Upload Artifacts to GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ steps.upload-location.outputs.is_prerelease == 'true' }}
|
||||
files: artifacts/*.px4
|
||||
name: ${{ steps.upload-location.outputs.uploadlocation }}
|
||||
|
||||
@@ -19,6 +19,10 @@ concurrency:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
container:
|
||||
image: px4io/px4-dev:v1.16.0-rc1-258-g0369abd556
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -35,20 +39,17 @@ jobs:
|
||||
"px4_sitl_allyes",
|
||||
"module_documentation",
|
||||
]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Building [${{ matrix.check }}]
|
||||
uses: addnab/docker-run-action@v3
|
||||
with:
|
||||
image: px4io/px4-dev:v1.16.0-rc1-258-g0369abd556
|
||||
options: -v ${{ github.workspace }}:/workspace
|
||||
run: |
|
||||
cd /workspace
|
||||
git config --global --add safe.directory /workspace
|
||||
make ${{ matrix.check }}
|
||||
run: |
|
||||
cd "$GITHUB_WORKSPACE"
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
make ${{ matrix.check }}
|
||||
|
||||
- name: Uploading Coverage to Codecov.io
|
||||
if: contains(matrix.check, 'coverage')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Clang Tidy
|
||||
name: Static Analysis
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -11,20 +11,59 @@ on:
|
||||
- '**'
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
clang_tidy:
|
||||
name: Clang-Tidy
|
||||
runs-on: [runs-on, runner=16cpu-linux-x64, "run-id=${{ github.run_id }}", "extras=s3-cache"]
|
||||
container:
|
||||
image: px4io/px4-dev:v1.17.0-beta1
|
||||
steps:
|
||||
- uses: runs-on/action@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Testing (clang-tidy)
|
||||
uses: addnab/docker-run-action@v3
|
||||
- name: Configure Git Safe Directory
|
||||
run: git config --system --add safe.directory '*'
|
||||
|
||||
- name: Restore Compiler Cache
|
||||
id: cc_restore
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
image: px4io/px4-dev-clang:2021-09-08
|
||||
options: -v ${{ github.workspace }}:/workspace
|
||||
run: |
|
||||
cd /workspace
|
||||
git config --global --add safe.directory /workspace
|
||||
make clang-tidy
|
||||
path: ~/.ccache
|
||||
key: ccache-clang-tidy-${{ github.head_ref || github.ref_name }}
|
||||
restore-keys: |
|
||||
ccache-clang-tidy-${{ github.head_ref || github.ref_name }}-
|
||||
ccache-clang-tidy-main-
|
||||
ccache-clang-tidy-
|
||||
|
||||
- name: Configure Compiler Cache
|
||||
run: |
|
||||
mkdir -p ~/.ccache
|
||||
echo "base_dir = ${GITHUB_WORKSPACE}" > ~/.ccache/ccache.conf
|
||||
echo "compression = true" >> ~/.ccache/ccache.conf
|
||||
echo "compression_level = 6" >> ~/.ccache/ccache.conf
|
||||
echo "max_size = 120M" >> ~/.ccache/ccache.conf
|
||||
echo "hash_dir = false" >> ~/.ccache/ccache.conf
|
||||
echo "compiler_check = content" >> ~/.ccache/ccache.conf
|
||||
ccache -s
|
||||
ccache -z
|
||||
|
||||
- name: Run Clang-Tidy Analysis
|
||||
run: make -j16 clang-tidy
|
||||
|
||||
- name: Compiler Cache Stats
|
||||
if: always()
|
||||
run: ccache -s
|
||||
|
||||
- name: Save Compiler Cache
|
||||
if: always()
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: ~/.ccache
|
||||
key: ${{ steps.cc_restore.outputs.cache-primary-key }}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
name: Commit Quality
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
|
||||
|
||||
jobs:
|
||||
pr-title:
|
||||
name: PR Title
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: Tools/ci
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Check PR title
|
||||
id: check
|
||||
run: |
|
||||
python3 Tools/ci/check_pr_title.py "${{ github.event.pull_request.title }}" --markdown-file comment.md && rc=0 || rc=$?
|
||||
echo "exit_code=$rc" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Post or clear comment
|
||||
if: env.IS_FORK == 'false'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
if [ "${{ steps.check.outputs.exit_code }}" != "0" ]; then
|
||||
python3 Tools/ci/pr_comment.py --marker pr-title --pr "$PR_NUMBER" --result fail < comment.md
|
||||
else
|
||||
python3 Tools/ci/pr_comment.py --marker pr-title --pr "$PR_NUMBER" --result pass
|
||||
fi
|
||||
|
||||
- name: Result
|
||||
if: steps.check.outputs.exit_code != '0'
|
||||
run: |
|
||||
echo "::error::PR title does not follow conventional commits format. See the PR comment for details."
|
||||
exit 1
|
||||
|
||||
commit-messages:
|
||||
name: Commit Messages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: Tools/ci
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Check commit messages
|
||||
id: check
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh api \
|
||||
"repos/${{ github.repository }}/pulls/${PR_NUMBER}/commits?per_page=100" \
|
||||
| python3 Tools/ci/check_commit_messages.py --markdown-file comment.md && rc=0 || rc=$?
|
||||
echo "exit_code=$rc" >> "$GITHUB_OUTPUT"
|
||||
# Check for warnings (non-empty markdown on exit 0)
|
||||
if [ "$rc" -eq 0 ] && [ -s comment.md ]; then
|
||||
echo "has_warnings=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "has_warnings=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Post or clear comment
|
||||
if: env.IS_FORK == 'false'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
if [ "${{ steps.check.outputs.exit_code }}" != "0" ]; then
|
||||
python3 Tools/ci/pr_comment.py --marker commit-msgs --pr "$PR_NUMBER" --result fail < comment.md
|
||||
elif [ "${{ steps.check.outputs.has_warnings }}" == "true" ]; then
|
||||
python3 Tools/ci/pr_comment.py --marker commit-msgs --pr "$PR_NUMBER" --result warn < comment.md
|
||||
else
|
||||
python3 Tools/ci/pr_comment.py --marker commit-msgs --pr "$PR_NUMBER" --result pass
|
||||
fi
|
||||
|
||||
- name: Result
|
||||
if: steps.check.outputs.exit_code != '0'
|
||||
run: |
|
||||
echo "::error::Commit message errors found. See the PR comment for details."
|
||||
exit 1
|
||||
|
||||
pr-body:
|
||||
name: PR Description
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
if: env.IS_FORK == 'false'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: Tools/ci
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Check PR body
|
||||
id: check
|
||||
env:
|
||||
PR_BODY: ${{ github.event.pull_request.body }}
|
||||
run: |
|
||||
message=""
|
||||
if [ -z "$PR_BODY" ]; then
|
||||
message="PR description is empty. Please add a summary of the changes."
|
||||
echo "::warning::PR description is empty."
|
||||
else
|
||||
cleaned=$(echo "$PR_BODY" | sed 's/<!--.*-->//g' | tr -d '[:space:]')
|
||||
if [ -z "$cleaned" ]; then
|
||||
message="PR description contains only template comments. Please fill in the details."
|
||||
echo "::warning::PR description contains only template comments."
|
||||
fi
|
||||
fi
|
||||
echo "message=$message" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Post or clear comment
|
||||
if: env.IS_FORK == 'false'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
if [ -n "${{ steps.check.outputs.message }}" ]; then
|
||||
printf '%s\n' \
|
||||
"## PR Description (advisory)" \
|
||||
"" \
|
||||
"This is **not blocking**, but your PR description appears to be empty or incomplete." \
|
||||
"" \
|
||||
"${{ steps.check.outputs.message }}" \
|
||||
"" \
|
||||
"A good PR description helps reviewers understand what changed and why." \
|
||||
"" \
|
||||
"---" \
|
||||
"*This comment will be automatically removed once the issue is resolved.*" \
|
||||
| python3 Tools/ci/pr_comment.py --marker pr-body --pr "$PR_NUMBER" --result warn
|
||||
else
|
||||
python3 Tools/ci/pr_comment.py --marker pr-body --pr "$PR_NUMBER" --result pass
|
||||
fi
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
shell: cmake -P {0}
|
||||
run: |
|
||||
string(TIMESTAMP current_date "%Y-%m-%d-%H;%M;%S" UTC)
|
||||
message("::set-output name=timestamp::${current_date}")
|
||||
file(APPEND "$ENV{GITHUB_OUTPUT}" "timestamp=${current_date}\n")
|
||||
- name: ccache cache files
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
|
||||
@@ -130,8 +130,8 @@ jobs:
|
||||
load: false
|
||||
push: ${{ startsWith(github.ref, 'refs/tags/') || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_registry) }}
|
||||
provenance: false
|
||||
cache-from: type=gha,version=1,scope=${{ matrix.arch }}
|
||||
cache-to: type=gha,version=1,mode=max,scope=${{ matrix.arch }}
|
||||
cache-from: type=gha,scope=${{ matrix.arch }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
|
||||
|
||||
deploy:
|
||||
name: Deploy To Registry
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
# Docs - Orchestrator
|
||||
#
|
||||
# Trigger paths:
|
||||
# push (main, release/**) → metadata-regen → build-site → deploy-aws
|
||||
# pull_request → detect-changes → pr-metadata-regen → link-check → build-site (if docs/source changed)
|
||||
# workflow_dispatch → metadata-regen → build-site → deploy-aws
|
||||
#
|
||||
# Container jobs (pr-metadata-regen, metadata-regen) run in px4-dev image and
|
||||
# require safe.directory + fetch-depth: 0 for git operations.
|
||||
|
||||
name: Docs - Orchestrator
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/**"
|
||||
paths:
|
||||
- "docs/**"
|
||||
- "src/**"
|
||||
- "msg/**"
|
||||
- "ROMFS/**"
|
||||
- "Tools/module_config/**"
|
||||
- ".github/workflows/docs-orchestrator.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "docs/**"
|
||||
- ".github/workflows/docs-orchestrator.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: docs-orchestrator-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# =============================================================================
|
||||
# Detect Changes (PR only)
|
||||
# =============================================================================
|
||||
detect-changes:
|
||||
name: "T1: Detect Changes"
|
||||
if: github.event_name == 'pull_request'
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
source_changed: ${{ steps.changes.outputs.source }}
|
||||
docs_changed: ${{ steps.changes.outputs.docs }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: changes
|
||||
with:
|
||||
filters: |
|
||||
source:
|
||||
- 'src/**'
|
||||
- 'msg/**'
|
||||
- 'ROMFS/**'
|
||||
- 'Tools/module_config/**'
|
||||
docs:
|
||||
- 'docs/**'
|
||||
|
||||
# =============================================================================
|
||||
# PR Metadata Regen (conditional - only when PR touches source files)
|
||||
# =============================================================================
|
||||
pr-metadata-regen:
|
||||
name: "T2: PR Metadata"
|
||||
needs: [detect-changes]
|
||||
if: github.event_name == 'pull_request' && needs.detect-changes.outputs.source_changed == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: [runs-on,runner=4cpu-linux-x64,image=ubuntu24-full-x64,"run-id=${{ github.run_id }}",spot=false,extras=s3-cache]
|
||||
container:
|
||||
image: px4io/px4-dev:v1.17.0-beta1
|
||||
steps:
|
||||
- uses: runs-on/action@v1
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Git ownership workaround
|
||||
run: git config --system --add safe.directory '*'
|
||||
|
||||
- name: Cache Restore - ccache
|
||||
id: cache-ccache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: ~/.ccache
|
||||
key: ccache-docs-metadata-${{ github.sha }}
|
||||
restore-keys: |
|
||||
ccache-docs-metadata-
|
||||
|
||||
- name: Setup ccache
|
||||
run: |
|
||||
mkdir -p ~/.ccache
|
||||
echo "max_size = 1G" > ~/.ccache/ccache.conf
|
||||
|
||||
- name: Build px4_sitl_default
|
||||
run: |
|
||||
make px4_sitl_default
|
||||
env:
|
||||
CCACHE_DIR: ~/.ccache
|
||||
|
||||
- name: Cache Save - ccache
|
||||
uses: actions/cache/save@v4
|
||||
if: always()
|
||||
with:
|
||||
path: ~/.ccache
|
||||
key: ccache-docs-metadata-${{ github.sha }}
|
||||
|
||||
- name: Generate and sync metadata
|
||||
run: Tools/ci/metadata_sync.sh --generate --sync parameters airframes modules msg_docs failsafe_web
|
||||
env:
|
||||
CCACHE_DIR: ~/.ccache
|
||||
|
||||
- name: Upload metadata artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pr-metadata
|
||||
path: docs/
|
||||
retention-days: 1
|
||||
|
||||
# =============================================================================
|
||||
# Push Metadata Regen (main/release branches)
|
||||
# =============================================================================
|
||||
metadata-regen:
|
||||
name: "T2: Metadata Sync"
|
||||
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: [runs-on,runner=4cpu-linux-x64,image=ubuntu24-full-x64,"run-id=${{ github.run_id }}",spot=false,extras=s3-cache]
|
||||
container:
|
||||
image: px4io/px4-dev:v1.17.0-beta1
|
||||
steps:
|
||||
- uses: runs-on/action@v1
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
token: ${{ secrets.PX4BUILTBOT_PERSONAL_ACCESS_TOKEN }}
|
||||
|
||||
- name: Git ownership workaround
|
||||
run: git config --system --add safe.directory '*'
|
||||
|
||||
- name: Cache Restore - ccache
|
||||
id: cache-ccache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: ~/.ccache
|
||||
key: ccache-docs-metadata-${{ github.sha }}
|
||||
restore-keys: |
|
||||
ccache-docs-metadata-
|
||||
|
||||
- name: Setup ccache
|
||||
run: |
|
||||
mkdir -p ~/.ccache
|
||||
echo "max_size = 1G" > ~/.ccache/ccache.conf
|
||||
|
||||
- name: Build px4_sitl_default
|
||||
run: |
|
||||
make px4_sitl_default
|
||||
env:
|
||||
CCACHE_DIR: ~/.ccache
|
||||
|
||||
- name: Cache Save - ccache
|
||||
uses: actions/cache/save@v4
|
||||
if: always()
|
||||
with:
|
||||
path: ~/.ccache
|
||||
key: ccache-docs-metadata-${{ github.sha }}
|
||||
|
||||
- name: Generate and sync metadata
|
||||
run: Tools/ci/metadata_sync.sh --generate --sync parameters airframes modules msg_docs failsafe_web
|
||||
env:
|
||||
CCACHE_DIR: ~/.ccache
|
||||
|
||||
- name: Install Node.js and Yarn
|
||||
run: |
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
||||
apt-get install -y nodejs
|
||||
corepack enable
|
||||
|
||||
- name: Format markdown with Prettier
|
||||
run: |
|
||||
cd docs
|
||||
yarn install --frozen-lockfile
|
||||
yarn prettier --write "en/**/*.md"
|
||||
|
||||
- name: Commit and push changes
|
||||
run: |
|
||||
git config --global user.name "${{ secrets.PX4BUILDBOT_USER }}"
|
||||
git config --global user.email "${{ secrets.PX4BUILDBOT_EMAIL }}"
|
||||
git add docs/
|
||||
if git diff --staged --quiet; then
|
||||
echo "No changes to commit"
|
||||
else
|
||||
git commit -m "docs: auto-sync metadata [skip ci]
|
||||
|
||||
Co-Authored-By: PX4 BuildBot <${{ secrets.PX4BUILDBOT_EMAIL }}>"
|
||||
git push
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Link Check
|
||||
# =============================================================================
|
||||
link-check:
|
||||
name: "T2: Link Check"
|
||||
needs: [detect-changes, pr-metadata-regen]
|
||||
if: always() && (github.event_name == 'pull_request')
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download metadata artifact
|
||||
if: needs.pr-metadata-regen.result == 'success'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: pr-metadata
|
||||
path: docs/
|
||||
|
||||
- name: Get changed doc files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v46.0.5
|
||||
with:
|
||||
json: true
|
||||
write_output_files: true
|
||||
output_dir: ./logs
|
||||
base_sha: ${{ github.event.pull_request.base.sha }}
|
||||
sha: ${{ github.event.pull_request.head.sha }}
|
||||
files: |
|
||||
docs/en/**/*.md
|
||||
|
||||
- name: Save changed files list
|
||||
run: |
|
||||
mv ./logs/all_changed_files.json ./logs/prFiles.json
|
||||
echo "Changed files:"
|
||||
cat ./logs/prFiles.json
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Run filtered link checker (changed files)
|
||||
run: |
|
||||
npm -g install markdown_link_checker_sc@0.0.138
|
||||
if [ "$(jq length ./logs/prFiles.json)" -gt 0 ]; then
|
||||
markdown_link_checker_sc \
|
||||
-r "$GITHUB_WORKSPACE" \
|
||||
-d docs \
|
||||
-e en \
|
||||
-f ./logs/prFiles.json \
|
||||
-i assets \
|
||||
-u docs.px4.io/main/ \
|
||||
> ./logs/filtered-link-check-results.md || true
|
||||
fi
|
||||
if [ ! -s ./logs/filtered-link-check-results.md ]; then
|
||||
echo "No broken links found in changed files." > ./logs/filtered-link-check-results.md
|
||||
fi
|
||||
cat ./logs/filtered-link-check-results.md
|
||||
|
||||
- name: Run full link checker
|
||||
run: |
|
||||
markdown_link_checker_sc \
|
||||
-r "$GITHUB_WORKSPACE" \
|
||||
-d docs \
|
||||
-e en \
|
||||
-i assets \
|
||||
-u docs.px4.io/main/ \
|
||||
> ./logs/link-check-results.md || true
|
||||
cat ./logs/link-check-results.md
|
||||
|
||||
- name: Post PR comment with link check results
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: flaws
|
||||
path: ./logs/filtered-link-check-results.md
|
||||
|
||||
- name: Upload link check results
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: link-check-results
|
||||
path: logs/
|
||||
retention-days: 7
|
||||
|
||||
# =============================================================================
|
||||
# Build Site
|
||||
# =============================================================================
|
||||
build-site:
|
||||
name: "T3: Build Site"
|
||||
needs: [detect-changes, pr-metadata-regen, metadata-regen, link-check]
|
||||
if: >-
|
||||
always() &&
|
||||
(needs.metadata-regen.result == 'success' || needs.metadata-regen.result == 'skipped') &&
|
||||
(needs.link-check.result == 'success' || needs.link-check.result == 'skipped') &&
|
||||
(github.event_name != 'pull_request' || needs.detect-changes.outputs.docs_changed == 'true' || needs.detect-changes.outputs.source_changed == 'true')
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: [runs-on,runner=4cpu-linux-x64,image=ubuntu24-full-x64,"run-id=${{ github.run_id }}",spot=false,extras=s3-cache]
|
||||
outputs:
|
||||
branchname: ${{ steps.set-branch.outputs.branchname }}
|
||||
releaseversion: ${{ steps.set-version.outputs.releaseversion }}
|
||||
steps:
|
||||
- uses: runs-on/action@v1
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Download metadata artifact (PR)
|
||||
if: github.event_name == 'pull_request' && needs.pr-metadata-regen.result == 'success'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: pr-metadata
|
||||
path: docs/
|
||||
|
||||
- id: set-branch
|
||||
run: echo "branchname=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
|
||||
|
||||
- id: set-version
|
||||
run: |
|
||||
branch="${{ steps.set-branch.outputs.branchname }}"
|
||||
if [[ "$branch" == "main" ]]; then
|
||||
version="main"
|
||||
elif [[ "$branch" =~ ^release/ ]]; then
|
||||
version="v${branch#release/}"
|
||||
elif [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
version="main"
|
||||
else
|
||||
echo "::error::Unsupported branch for docs deploy: $branch (expected main or release/*)"
|
||||
exit 1
|
||||
fi
|
||||
echo "releaseversion=$version" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
cache-dependency-path: ./docs/yarn.lock
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile --cwd ./docs
|
||||
|
||||
- name: Build with VitePress
|
||||
working-directory: ./docs
|
||||
env:
|
||||
BRANCH_NAME: ${{ steps.set-version.outputs.releaseversion }}
|
||||
run: |
|
||||
npm run docs:build_ubuntu
|
||||
touch .vitepress/dist/.nojekyll
|
||||
npm run docs:sitemap
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: px4_docs_build
|
||||
path: docs/.vitepress/dist/
|
||||
retention-days: 1
|
||||
|
||||
# =============================================================================
|
||||
# Deploy to AWS (push + workflow_dispatch)
|
||||
# =============================================================================
|
||||
deploy-aws:
|
||||
name: "T4: Deploy"
|
||||
needs: [metadata-regen, build-site]
|
||||
if: >-
|
||||
always() &&
|
||||
needs.metadata-regen.result == 'success' &&
|
||||
needs.build-site.result == 'success' &&
|
||||
(github.event_name == 'push' || github.event_name == 'workflow_dispatch')
|
||||
permissions:
|
||||
id-token: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download Artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: px4_docs_build
|
||||
path: ~/_book
|
||||
|
||||
- name: Configure AWS from OIDC
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
|
||||
aws-region: us-west-2
|
||||
|
||||
- name: Sanity check AWS credentials
|
||||
run: aws sts get-caller-identity
|
||||
|
||||
- name: Upload HTML with short cache
|
||||
run: |
|
||||
aws s3 sync ~/_book/ s3://px4-docs/${{ needs.build-site.outputs.releaseversion }}/ \
|
||||
--delete \
|
||||
--exclude "*" --include "*.html" \
|
||||
--cache-control "public, max-age=60"
|
||||
|
||||
- name: Upload assets with long cache
|
||||
run: |
|
||||
aws s3 sync ~/_book/ s3://px4-docs/${{ needs.build-site.outputs.releaseversion }}/ \
|
||||
--delete \
|
||||
--exclude "*.html" \
|
||||
--cache-control "public, max-age=86400, immutable"
|
||||
@@ -34,13 +34,13 @@ jobs:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
commit_message: New Crowdin translations - ${{ matrix.lc }}
|
||||
commit_message: 'docs(i18n): PX4 guide translations (Crowdin) - ${{ matrix.lc }}'
|
||||
localization_branch_name: l10n_crowdin_docs_translations_${{ matrix.lc }}
|
||||
crowdin_branch_name: main
|
||||
create_pull_request: true
|
||||
pull_request_base_branch_name: 'main'
|
||||
pull_request_title: New PX4 guide translations (Crowdin) - ${{ matrix.lc }}
|
||||
pull_request_body: 'New PX4 guide Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action) for ${{ matrix.lc }}'
|
||||
pull_request_title: 'docs(i18n): PX4 guide translations (Crowdin) - ${{ matrix.lc }}'
|
||||
pull_request_body: 'docs(i18n): PX4 guide Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action) for ${{ matrix.lc }}'
|
||||
pull_request_labels: 'Documentation 📑'
|
||||
pull_request_reviewers: hamishwillee
|
||||
download_language: ${{ matrix.lc }}
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
name: Docs - Deploy PX4 User Guide to AWS
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/**"
|
||||
paths:
|
||||
- "docs/en/**"
|
||||
- "docs/zh/**"
|
||||
- "docs/uk/**"
|
||||
- "docs/ko/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- "docs/en/**"
|
||||
- "docs/zh/**"
|
||||
- "docs/uk/**"
|
||||
- "docs/ko/**"
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
id-token: write # for AWS OIDC
|
||||
|
||||
concurrency:
|
||||
group: docs-deploy
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [runs-on,runner=4cpu-linux-x64,image=ubuntu24-full-x64,"run-id=${{ github.run_id }}",spot=false,extras=s3-cache]
|
||||
outputs:
|
||||
branchname: ${{ steps.set-branch.outputs.branchname }}
|
||||
releaseversion: ${{ steps.set-version.outputs.releaseversion }}
|
||||
steps:
|
||||
- uses: runs-on/action@v1
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- id: set-branch
|
||||
run: echo "branchname=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
|
||||
|
||||
- id: set-version
|
||||
run: |
|
||||
branch="${{ steps.set-branch.outputs.branchname }}"
|
||||
if [[ "$branch" == "main" ]]; then
|
||||
version="main"
|
||||
else
|
||||
version="v${branch#release/}"
|
||||
fi
|
||||
echo "releaseversion=$version" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
cache-dependency-path: ./docs/yarn.lock
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile --cwd ./docs
|
||||
|
||||
- name: Build with VitePress
|
||||
working-directory: ./docs
|
||||
env:
|
||||
BRANCH_NAME: ${{ steps.set-version.outputs.releaseversion }}
|
||||
run: |
|
||||
npm run docs:build_ubuntu
|
||||
touch .vitepress/dist/.nojekyll
|
||||
npm run docs:sitemap
|
||||
|
||||
- name: Upload artifact
|
||||
if: ${{ github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.merged) || github.event_name == 'workflow_dispatch' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: px4_docs_build
|
||||
path: docs/.vitepress/dist/
|
||||
retention-days: 1
|
||||
|
||||
deploy:
|
||||
if: ${{ github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.merged) || github.event_name == 'workflow_dispatch' }}
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Download Artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: px4_docs_build
|
||||
path: ~/_book
|
||||
|
||||
- name: Configure AWS from OIDC
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
|
||||
aws-region: us-west-2
|
||||
|
||||
- name: Sanity check AWS credentials
|
||||
run: aws sts get-caller-identity
|
||||
|
||||
- name: Upload HTML with short cache
|
||||
run: |
|
||||
aws s3 sync ~/_book/ s3://px4-docs/${{ needs.build.outputs.releaseversion }}/ \
|
||||
--delete \
|
||||
--exclude "*" --include "*.html" \
|
||||
--cache-control "public, max-age=60"
|
||||
|
||||
- name: Upload assets with long cache
|
||||
run: |
|
||||
aws s3 sync ~/_book/ s3://px4-docs/${{ needs.build.outputs.releaseversion }}/ \
|
||||
--delete \
|
||||
--exclude "*.html" \
|
||||
--cache-control "public, max-age=86400, immutable"
|
||||
@@ -1,85 +0,0 @@
|
||||
name: Docs - Check for flaws in PX4 Guide Source
|
||||
# So far:
|
||||
# Modifications of translations files
|
||||
# Broken internal links
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, edited, synchronize]
|
||||
paths:
|
||||
- 'docs/en/**'
|
||||
|
||||
jobs:
|
||||
check_flaws:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Create logs directory
|
||||
run: |
|
||||
mkdir logs
|
||||
|
||||
- name: Get changed english doc files
|
||||
id: get_changed_markdown_english
|
||||
uses: tj-actions/changed-files@v46.0.5
|
||||
with:
|
||||
json: true
|
||||
base_sha: "${{ github.event.pull_request.base.sha }}"
|
||||
sha: "${{ github.event.pull_request.head.sha }}"
|
||||
# Below are used to output files to a directory. May use in flaw checker.
|
||||
# write_output_files: true
|
||||
# output_dir: "./logs"
|
||||
files: |
|
||||
docs/en/**/*.md
|
||||
|
||||
- name: Save JSON file containing files to link check
|
||||
run: |
|
||||
echo "$ALL_CHANGED_FILES"
|
||||
# echo "$ALL_CHANGED_FILES" > ./logs/prFiles.json
|
||||
echo "$ALL_CHANGED_FILES" | sed 's/\\//g' | jq '.' > ./logs/prFiles.json
|
||||
env:
|
||||
ALL_CHANGED_FILES: ${{ steps.get_changed_markdown_english.outputs.all_changed_files }}
|
||||
|
||||
- name: Run link checker
|
||||
id: link-check
|
||||
run: |
|
||||
npm -g install markdown_link_checker_sc@0.0.138
|
||||
markdown_link_checker_sc \
|
||||
-r "$GITHUB_WORKSPACE" \
|
||||
-d docs \
|
||||
-e en \
|
||||
-f ./logs/prFiles.json \
|
||||
-i assets \
|
||||
-u docs.px4.io/main/ \
|
||||
> ./logs/errorsFilteredByPrPages.md
|
||||
mkdir -p ./pr
|
||||
cp ./logs/errorsFilteredByPrPages.md ./pr/errorsFilteredByPrPages.md
|
||||
|
||||
- name: Read errorsFilteredByPrPages.md file
|
||||
id: read-errors-by-page
|
||||
uses: juliangruber/read-file-action@v1
|
||||
with:
|
||||
path: ./logs/errorsFilteredByPrPages.md
|
||||
|
||||
- name: Echo Errors by Page
|
||||
run: echo "$ERRORS"
|
||||
env:
|
||||
ERRORS: ${{ steps.read-errors-by-page.outputs.content }}
|
||||
|
||||
- name: Save PR number
|
||||
run: echo "$PR_NUMBER" > ./pr/pr_number
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pr_number
|
||||
path: pr/
|
||||
@@ -1,111 +0,0 @@
|
||||
name: Docs - Comment Workflow
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Docs - Check for flaws in PX4 Guide Source"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
permissions:
|
||||
pull-requests: write # for marocchino/sticky-pull-request-comment
|
||||
name: Comments
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Download PR artifact'
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: context.payload.workflow_run.id,
|
||||
});
|
||||
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == "pr_number"
|
||||
})[0];
|
||||
let download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
let fs = require('fs');
|
||||
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/pr_number.zip`, Buffer.from(download.data));
|
||||
|
||||
- name: 'Unzip artifact'
|
||||
run: unzip pr_number.zip
|
||||
|
||||
# Doesn't work across workflows
|
||||
#- name: Get artifacts from flaw checker workflow
|
||||
# uses: actions/download-artifact@v3
|
||||
# with:
|
||||
# name: logs_and_errors
|
||||
# #path: ./logs
|
||||
|
||||
- name: Read errorsFilteredByPrPages.md file
|
||||
id: read-errors-by-page
|
||||
uses: juliangruber/read-file-action@v1
|
||||
with:
|
||||
path: ./errorsFilteredByPrPages.md
|
||||
|
||||
- name: Read PR number
|
||||
id: read-error-pr-number
|
||||
uses: juliangruber/read-file-action@v1
|
||||
with:
|
||||
path: ./pr_number
|
||||
|
||||
- name: File detail info
|
||||
run: |
|
||||
echo "$ERRORS"
|
||||
echo "$PRNUM"
|
||||
env:
|
||||
ERRORS: ${{ steps.read-errors-by-page.outputs.content }}
|
||||
PRNUM: ${{ steps.read-error-pr-number.outputs.content }}
|
||||
|
||||
- name: Create or update comment
|
||||
id: comment_to_pr
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
recreate: true
|
||||
number: ${{ steps.read-error-pr-number.outputs.content }}
|
||||
header: flaws
|
||||
message: ${{ steps.read-errors-by-page.outputs.content || 'No flaws found' }}
|
||||
|
||||
#- name: Dump GitHub context
|
||||
# env:
|
||||
# GITHUB_CONTEXT: ${{ toJSON(github) }}
|
||||
# run: echo "$GITHUB_CONTEXT"
|
||||
|
||||
# Would like to do this, but it doesn't work (for me).
|
||||
# Moving to time-based, or triggering on workflow
|
||||
#- name: Wait for artifacts upload to succeed
|
||||
# uses: lewagon/wait-on-check-action@v1.3.1
|
||||
# with:
|
||||
# ref: ${{ github.ref }}
|
||||
# check-name: 'Archive production artifacts'
|
||||
# repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# wait-interval: 80
|
||||
|
||||
# Not needed for now - trying to trigger off the workflow
|
||||
#- name: Sleep for 80 seconds
|
||||
# run: sleep 80s
|
||||
# shell: bash
|
||||
#- name: Find Comment
|
||||
# uses: peter-evans/find-comment@v2
|
||||
# id: fc
|
||||
# with:
|
||||
# issue-number: ${{ steps.read-error-pr-number.outputs.content }}
|
||||
# comment-author: 'github-actions[bot]'
|
||||
# body-includes: Flaws (may be none)
|
||||
|
||||
#- name: Create or update comment
|
||||
# uses: peter-evans/create-or-update-comment@v3
|
||||
# with:
|
||||
# comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||
# issue-number: ${{ steps.read-error-pr-number.outputs.content }}
|
||||
# body: |
|
||||
# Flaws (may be none)
|
||||
# ${{ steps.read-errors-by-page.outputs.content }}
|
||||
# edit-mode: replace
|
||||
@@ -15,21 +15,21 @@ concurrency:
|
||||
jobs:
|
||||
unit_tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: main test
|
||||
uses: addnab/docker-run-action@v3
|
||||
with:
|
||||
image: px4io/px4-dev:v1.16.0-rc1-258-g0369abd556
|
||||
options: -v ${{ github.workspace }}:/workspace
|
||||
container:
|
||||
image: px4io/px4-dev:v1.16.0-rc1-258-g0369abd556
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: main test
|
||||
run: |
|
||||
cd /workspace
|
||||
git config --global --add safe.directory /workspace
|
||||
cd "$GITHUB_WORKSPACE"
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
make tests TESTFILTER=EKF
|
||||
|
||||
- name: Check if there is a functional change
|
||||
run: git diff --exit-code
|
||||
working-directory: src/modules/ekf2/test/change_indication
|
||||
- name: Check if there is a functional change
|
||||
run: git diff --exit-code
|
||||
working-directory: src/modules/ekf2/test/change_indication
|
||||
|
||||
@@ -8,40 +8,47 @@ on:
|
||||
jobs:
|
||||
unit_tests:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
container:
|
||||
image: px4io/px4-dev:v1.16.0-rc1-258-g0369abd556
|
||||
|
||||
env:
|
||||
GIT_COMMITTER_EMAIL: bot@px4.io
|
||||
GIT_COMMITTER_NAME: PX4BuildBot
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: main test
|
||||
uses: addnab/docker-run-action@v3
|
||||
with:
|
||||
image: px4io/px4-dev:v1.16.0-rc1-258-g0369abd556
|
||||
options: -v ${{ github.workspace }}:/workspace
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: main test
|
||||
run: |
|
||||
cd /workspace
|
||||
git config --global --add safe.directory /workspace
|
||||
cd "$GITHUB_WORKSPACE"
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
make tests TESTFILTER=EKF
|
||||
|
||||
- name: Check if there exists diff and save result in variable
|
||||
id: diff-check
|
||||
run: echo "CHANGE_INDICATED=$(git diff --exit-code --output=/dev/null || echo $?)" >> $GITHUB_OUTPUT
|
||||
working-directory: src/modules/ekf2/test/change_indication
|
||||
- name: Check if there exists diff and save result in variable
|
||||
id: diff-check
|
||||
working-directory: src/modules/ekf2/test/change_indication
|
||||
run: |
|
||||
if git diff --quiet; then
|
||||
echo "CHANGE_INDICATED=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "CHANGE_INDICATED=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: auto-commit any changes to change indication
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
file_pattern: 'src/modules/ekf2/test/change_indication/*.csv'
|
||||
commit_user_name: ${GIT_COMMITTER_NAME}
|
||||
commit_user_email: ${GIT_COMMITTER_EMAIL}
|
||||
commit_message: |
|
||||
'[AUTO COMMIT] update change indication'
|
||||
- name: auto-commit any changes to change indication
|
||||
if: steps.diff-check.outputs.CHANGE_INDICATED == 'true'
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
file_pattern: 'src/modules/ekf2/test/change_indication/*.csv'
|
||||
commit_user_name: ${{ env.GIT_COMMITTER_NAME }}
|
||||
commit_user_email: ${{ env.GIT_COMMITTER_EMAIL }}
|
||||
commit_message: |
|
||||
[AUTO COMMIT] update change indication
|
||||
|
||||
See .github/workflopws/ekf_update_change_indicator.yml for more details
|
||||
See .github/workflows/ekf_update_change_indicator.yml for more details
|
||||
|
||||
- name: if there is a functional change, fail check
|
||||
if: ${{ steps.diff-check.outputs.CHANGE_INDICATED }}
|
||||
run: exit 1
|
||||
- name: if there is a functional change, fail check
|
||||
if: steps.diff-check.outputs.CHANGE_INDICATED == 'true'
|
||||
run: exit 1
|
||||
|
||||
@@ -19,25 +19,27 @@ concurrency:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
config:
|
||||
- {vehicle: "iris", mission: "MC_mission_box"}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Build SITL and Run Tests
|
||||
uses: addnab/docker-run-action@v3
|
||||
with:
|
||||
image: px4io/px4-dev-ros-melodic:2021-09-08
|
||||
options: -v ${{ github.workspace }}:/workspace
|
||||
- name: Build SITL and Run Tests (inside old ROS container)
|
||||
run: |
|
||||
cd /workspace
|
||||
git config --global --add safe.directory /workspace
|
||||
make px4_sitl_default
|
||||
make px4_sitl_default sitl_gazebo-classic
|
||||
./test/rostest_px4_run.sh mavros_posix_test_mission.test mission:=${{matrix.config.mission}} vehicle:=${{matrix.config.vehicle}}
|
||||
docker run --rm \
|
||||
-v "${GITHUB_WORKSPACE}:/workspace" \
|
||||
-w /workspace \
|
||||
px4io/px4-dev-ros-melodic:2021-09-08 \
|
||||
bash -c '
|
||||
git config --global --add safe.directory /workspace
|
||||
make px4_sitl_default
|
||||
make px4_sitl_default sitl_gazebo-classic
|
||||
./test/rostest_px4_run.sh \
|
||||
mavros_posix_test_mission.test \
|
||||
mission:=MC_mission_box \
|
||||
vehicle:=iris
|
||||
'
|
||||
|
||||
@@ -19,27 +19,26 @@ concurrency:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
config:
|
||||
- {test_file: "mavros_posix_tests_offboard_posctl.test", vehicle: "iris"}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Build PX4 and Run Tests
|
||||
uses: addnab/docker-run-action@v3
|
||||
with:
|
||||
image: px4io/px4-dev-ros-melodic:2021-09-08
|
||||
options: -v ${{ github.workspace }}:/workspace
|
||||
- name: Build SITL and Run Tests (inside old ROS container)
|
||||
run: |
|
||||
cd /workspace
|
||||
git config --global --add safe.directory /workspace
|
||||
make px4_sitl_default
|
||||
make px4_sitl_default sitl_gazebo-classic
|
||||
./test/rostest_px4_run.sh ${{matrix.config.test_file}} vehicle:=${{matrix.config.vehicle}}
|
||||
docker run --rm \
|
||||
-v "${GITHUB_WORKSPACE}:/workspace" \
|
||||
-w /workspace \
|
||||
px4io/px4-dev-ros-melodic:2021-09-08 \
|
||||
bash -c '
|
||||
git config --global --add safe.directory /workspace
|
||||
make px4_sitl_default
|
||||
make px4_sitl_default sitl_gazebo-classic
|
||||
./test/rostest_px4_run.sh \
|
||||
mavros_posix_tests_offboard_posctl.test \
|
||||
vehicle:=iris
|
||||
'
|
||||
|
||||
@@ -19,27 +19,28 @@ concurrency:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
container:
|
||||
image: px4io/px4-dev:v1.16.0-rc1-258-g0369abd556
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
config: [
|
||||
px4_fmu-v5_default,
|
||||
]
|
||||
config:
|
||||
- px4_fmu-v5_default
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Build PX4 and Run Test [${{ matrix.config }}]
|
||||
uses: addnab/docker-run-action@v3
|
||||
with:
|
||||
image: px4io/px4-dev:v1.16.0-rc1-258-g0369abd556
|
||||
options: -v ${{ github.workspace }}:/workspace
|
||||
- name: Build PX4 and Run Test [${{ matrix.config }}]
|
||||
run: |
|
||||
cd /workspace
|
||||
git config --global --add safe.directory /workspace
|
||||
export PX4_EXTRA_NUTTX_CONFIG="CONFIG_NSH_LOGIN_PASSWORD=\"test\";CONFIG_NSH_CONSOLE_LOGIN=y"
|
||||
cd "$GITHUB_WORKSPACE"
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
export PX4_EXTRA_NUTTX_CONFIG='CONFIG_NSH_LOGIN_PASSWORD="test";CONFIG_NSH_CONSOLE_LOGIN=y'
|
||||
echo "PX4_EXTRA_NUTTX_CONFIG: $PX4_EXTRA_NUTTX_CONFIG"
|
||||
|
||||
make ${{ matrix.config }} nuttx_context
|
||||
|
||||
echo "Check that the config option is set"
|
||||
grep CONFIG_NSH_LOGIN_PASSWORD build/${{ matrix.config }}/NuttX/nuttx/.config
|
||||
|
||||
@@ -90,6 +90,9 @@ jobs:
|
||||
mkdir -p /opt/px4_ws/src
|
||||
cd /opt/px4_ws/src
|
||||
git clone --recursive https://github.com/Auterion/px4-ros2-interface-lib.git
|
||||
# Ignore python packages due to compilation issue (can be enabled when updating ROS)
|
||||
touch px4-ros2-interface-lib/px4_ros2_py/COLCON_IGNORE || true
|
||||
touch px4-ros2-interface-lib/examples/python/COLCON_IGNORE || true
|
||||
cd ..
|
||||
# Copy messages to ROS workspace
|
||||
"${PX4_DIR}/Tools/copy_to_ros_ws.sh" "$(pwd)"
|
||||
|
||||
@@ -33,8 +33,10 @@ jobs:
|
||||
matrix:
|
||||
config:
|
||||
- {model: "iris", latitude: "59.617693", longitude: "-151.145316", altitude: "48", build_type: "RelWithDebInfo" } # Alaska
|
||||
- {model: "tailsitter" , latitude: "29.660316", longitude: "-82.316658", altitude: "30", build_type: "RelWithDebInfo" } # Florida
|
||||
- {model: "standard_vtol", latitude: "47.397742", longitude: "8.545594", altitude: "488", build_type: "Coverage" } # Zurich
|
||||
# VTOL/tailsitter disabled: persistent flaky CI failures (timeouts, erratic
|
||||
# transitions). Re-enable once the test infrastructure is stabilized.
|
||||
# - {model: "tailsitter" , latitude: "29.660316", longitude: "-82.316658", altitude: "30", build_type: "RelWithDebInfo" } # Florida
|
||||
# - {model: "standard_vtol", latitude: "47.397742", longitude: "8.545594", altitude: "488", build_type: "Coverage" } # Zurich
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
stale-issue-label: 'stale'
|
||||
stale-pr-label: 'stale'
|
||||
remove-stale-when-updated: true
|
||||
stale-issue-message: 'This issue has been marked as stale due to 90 days of inactivity. If no further activity occurs, it will be automatically closed in 30 days. Please leave a comment, add a reaction, make an update, or remove the stale label if you’d like to keep it open.'
|
||||
close-issue-message: 'This issue has been closed due to prolonged inactivity after being marked as stale. If you believe this was closed in error or the topic is still relevant, please feel free to reopen it or create a new issue.'
|
||||
stale-pr-message: 'This PR was identified as stale and it will be closed in 30 days unless any activity is detected.'
|
||||
close-pr-message: 'This pull request has been closed after being marked as stale with no further activity. Thank you for the time and effort you put into this contribution. If you’d like to continue the discussion or update the work, please feel free to reopen it or submit a new PR.'
|
||||
stale-issue-message: ''
|
||||
close-issue-message: 'This issue has been automatically closed due to 120 days of inactivity. If you believe this is still relevant, please feel free to reopen it or create a new issue.'
|
||||
stale-pr-message: ''
|
||||
close-pr-message: 'This pull request has been automatically closed due to 120 days of inactivity. If you would like to continue, please feel free to reopen it or submit a new PR.'
|
||||
|
||||
@@ -109,3 +109,6 @@ src/systemcmds/topic_listener/listener_generated.cpp
|
||||
# colcon
|
||||
log/
|
||||
keys/
|
||||
|
||||
# metadata
|
||||
_emscripten_sdk/
|
||||
|
||||
+12
@@ -103,3 +103,15 @@
|
||||
[submodule "src/drivers/ins/sbgecom/sbgECom"]
|
||||
path = src/drivers/ins/sbgecom/sbgECom
|
||||
url = https://github.com/PX4/sbgECom.git
|
||||
[submodule "src/modules/mc_raptor/blob"]
|
||||
path = src/modules/mc_raptor/blob
|
||||
url = https://github.com/rl-tools/px4-blob
|
||||
[submodule "src/lib/rl_tools/rl_tools"]
|
||||
path = src/lib/rl_tools/rl_tools
|
||||
url = https://github.com/rl-tools/rl-tools.git
|
||||
[submodule "libmodal-json"]
|
||||
path = boards/modalai/voxl2/src/lib/mpa/libmodal-json
|
||||
url = https://gitlab.com/voxl-public/voxl-sdk/core-libs/libmodal-json.git
|
||||
[submodule "libmodal-pipe"]
|
||||
path = boards/modalai/voxl2/src/lib/mpa/libmodal-pipe
|
||||
url = https://gitlab.com/voxl-public/voxl-sdk/core-libs/libmodal-pipe.git
|
||||
|
||||
Vendored
+15
@@ -6,6 +6,16 @@ CONFIG:
|
||||
buildType: RelWithDebInfo
|
||||
settings:
|
||||
CONFIG: px4_sitl_default
|
||||
px4_sitl_raptor:
|
||||
short: px4_sitl_raptor
|
||||
buildType: RelWithDebInfo
|
||||
settings:
|
||||
CONFIG: px4_sitl_raptor
|
||||
px4_sitl_raptor_debug:
|
||||
short: px4_sitl_raptor_debug
|
||||
buildType: Debug
|
||||
settings:
|
||||
CONFIG: px4_sitl_raptor
|
||||
px4_sitl_spacecraft:
|
||||
short: px4_sitl_spacecraft
|
||||
buildType: RelWithDebInfo
|
||||
@@ -326,6 +336,11 @@ CONFIG:
|
||||
buildType: MinSizeRel
|
||||
settings:
|
||||
CONFIG: cuav_x25-evo_default
|
||||
cuav_x25-super_default:
|
||||
short: cuav_x25-super
|
||||
buildType: MinSizeRel
|
||||
settings:
|
||||
CONFIG: cuav_x25-super_default
|
||||
cubepilot_cubeorange_test:
|
||||
short: cubepilot_cubeorange
|
||||
buildType: MinSizeRel
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
cff-version: 1.2.0
|
||||
title: "PX4 Autopilot"
|
||||
message: "If you use PX4 in your research, please cite it using this metadata."
|
||||
type: software
|
||||
authors:
|
||||
- family-names: Meier
|
||||
given-names: Lorenz
|
||||
- name: "The PX4 Contributors"
|
||||
repository-code: "https://github.com/PX4/PX4-Autopilot"
|
||||
url: "https://px4.io"
|
||||
abstract: >-
|
||||
PX4 is an open-source autopilot stack for drones and
|
||||
unmanned vehicles. It supports multirotors, fixed-wing,
|
||||
VTOL, rovers, and many more platforms. PX4 runs on both
|
||||
RTOS and POSIX-compatible operating systems.
|
||||
keywords:
|
||||
- autopilot
|
||||
- drone
|
||||
- uav
|
||||
- flight-controller
|
||||
- robotics
|
||||
- ros2
|
||||
license: BSD-3-Clause
|
||||
identifiers:
|
||||
- type: doi
|
||||
value: "10.5281/zenodo.595432"
|
||||
description: "Zenodo concept DOI (resolves to latest version)"
|
||||
+1
-1
@@ -267,7 +267,7 @@ endif()
|
||||
|
||||
set(package-contact "px4users@googlegroups.com")
|
||||
|
||||
set(CMAKE_CXX_STANDARD 14)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
|
||||
+147
-21
@@ -1,44 +1,170 @@
|
||||
# Contributing to PX4 Firmware
|
||||
# Contributing to PX4-Autopilot
|
||||
|
||||
We follow the [Github flow](https://guides.github.com/introduction/flow/) development model.
|
||||
We follow the [GitHub flow](https://guides.github.com/introduction/flow/) development model.
|
||||
|
||||
### Fork the project, then clone your repo
|
||||
## Fork the project, then clone your repo
|
||||
|
||||
First [fork and clone](https://help.github.com/articles/fork-a-repo) the project project.
|
||||
First [fork and clone](https://help.github.com/articles/fork-a-repo) the project.
|
||||
|
||||
### Create a feature branch
|
||||
## Create a feature branch
|
||||
|
||||
*Always* branch off main for new features.
|
||||
Always branch off `main` for new features.
|
||||
|
||||
```
|
||||
git checkout -b mydescriptivebranchname
|
||||
```
|
||||
|
||||
### Edit and build the code
|
||||
## Edit and build the code
|
||||
|
||||
The [developer guide](https://docs.px4.io/main/en/development/development.html) explains how to set up the development environment on Mac OS, Linux or Windows. Please take note of our [coding style](https://docs.px4.io/main/en/contribute/code.html) when editing files.
|
||||
The [developer guide](https://docs.px4.io/main/en/development/development.html) explains how to set up the development environment on Mac OS, Linux or Windows.
|
||||
|
||||
### Commit your changes
|
||||
### Coding standards
|
||||
|
||||
Always write descriptive commit messages and add a fixes or relates note to them with an [issue number](https://github.com/px4/Firmware/issues) (Github will link these then conveniently)
|
||||
All C/C++ code must follow the [PX4 coding style](https://docs.px4.io/main/en/contribute/code.html). Formatting is enforced by [astyle](http://astyle.sourceforge.net/) in CI (`make check_format`). Code quality checks run via [clang-tidy](https://clang.llvm.org/extra/clang-tidy/). Pull requests that fail either check will not be merged.
|
||||
|
||||
**Example:**
|
||||
Python code is checked with [mypy](https://mypy-lang.org/) and [flake8](https://flake8.pycqa.org/).
|
||||
|
||||
## Commit message convention
|
||||
|
||||
PX4 uses [conventional commits](https://www.conventionalcommits.org/) for all commit messages and PR titles.
|
||||
|
||||
### Format
|
||||
|
||||
```
|
||||
Change how the attitude controller works
|
||||
|
||||
- Fixes rate feed forward
|
||||
- Allows a local body rate override
|
||||
|
||||
Fixes issue #123
|
||||
type(scope): short description of the change
|
||||
```
|
||||
|
||||
### Test your changes
|
||||
| Part | Rule |
|
||||
|------|------|
|
||||
| **type** | Category of change (see types table below) |
|
||||
| **scope** | The module, driver, board, or area of PX4 affected |
|
||||
| **`!`** (optional) | Append before `:` to mark a breaking change |
|
||||
| **description** | What the change does, at least 5 characters, written in imperative form |
|
||||
|
||||
Since we care about safety, we will regularly ask you for test results. Best is to do a test flight (or bench test where it applies) and upload the logfile from it (on the microSD card in the logs directory) to Google Drive or Dropbox and share the link.
|
||||
### Types
|
||||
|
||||
### Push your changes
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `feat` | A new feature |
|
||||
| `fix` | A bug fix |
|
||||
| `docs` | Documentation only changes |
|
||||
| `style` | Formatting, whitespace, no code change |
|
||||
| `refactor` | Code change that neither fixes a bug nor adds a feature |
|
||||
| `perf` | Performance improvement |
|
||||
| `test` | Adding or correcting tests |
|
||||
| `build` | Build system or external dependencies |
|
||||
| `ci` | CI configuration files and scripts |
|
||||
| `chore` | Other changes that don't modify src or test files |
|
||||
| `revert` | Reverts a previous commit |
|
||||
|
||||
Push changes to your repo and send a [pull request](https://github.com/PX4/Firmware/compare/).
|
||||
### Scopes
|
||||
|
||||
The scope identifies which part of PX4 is affected. Common scopes:
|
||||
|
||||
| Scope | Area |
|
||||
|-------|------|
|
||||
| `ekf2` | Extended Kalman Filter (state estimation) |
|
||||
| `mavlink` | MAVLink messaging protocol |
|
||||
| `commander` | Commander and mode management |
|
||||
| `navigator` | Mission, RTL, Land, and other navigation modes |
|
||||
| `sensors` | Sensor drivers and processing |
|
||||
| `drivers` | Hardware drivers |
|
||||
| `boards/px4_fmu-v6x` | Board-specific changes (use the board name) |
|
||||
| `mc_att_control` | Multicopter attitude control |
|
||||
| `mc_pos_control` | Multicopter position control |
|
||||
| `fw_att_control` | Fixed-wing attitude control |
|
||||
| `vtol` | VTOL-specific logic |
|
||||
| `actuators` | Mixer and actuator output |
|
||||
| `battery` | Battery monitoring and estimation |
|
||||
| `logger` | On-board logging |
|
||||
| `param` | Parameter system |
|
||||
| `simulation` | SITL, Gazebo, SIH |
|
||||
| `ci` | Continuous integration and workflows |
|
||||
| `docs` | Documentation |
|
||||
| `build` | CMake, toolchain, build system |
|
||||
| `uorb` | Inter-module messaging |
|
||||
|
||||
For changes spanning multiple subsystems, use the primary one affected. Look at the directory path of the files you changed to find the right scope: `src/modules/ekf2/` uses `ekf2`, `src/drivers/imu/` uses `drivers/imu`, `.github/workflows/` uses `ci`.
|
||||
|
||||
### Breaking changes
|
||||
|
||||
Append `!` before the colon to indicate a breaking change:
|
||||
|
||||
```
|
||||
feat(ekf2)!: remove deprecated height fusion API
|
||||
```
|
||||
|
||||
### Good commit messages
|
||||
|
||||
```
|
||||
feat(ekf2): add height fusion timeout
|
||||
fix(mavlink): correct BATTERY_STATUS_V2 parsing
|
||||
refactor(navigator): simplify RTL altitude logic
|
||||
ci(workflows): migrate to reusable workflows
|
||||
docs(ekf2): update tuning guide
|
||||
feat(boards/px4_fmu-v6x)!: remove deprecated driver API
|
||||
perf(mc_rate_control): reduce loop latency
|
||||
```
|
||||
|
||||
### Commits to avoid
|
||||
|
||||
These will be flagged by CI and should be squashed or reworded before merging:
|
||||
|
||||
```
|
||||
fix # too vague, no type or scope
|
||||
update # too vague, no type or scope
|
||||
ekf2: fix something # missing type prefix
|
||||
apply suggestions from code review # squash into parent commit
|
||||
do make format # squash into parent commit
|
||||
WIP: trying something # not ready for main
|
||||
oops # not descriptive
|
||||
```
|
||||
|
||||
### PR titles
|
||||
|
||||
The PR title follows the same `type(scope): description` format. This is enforced by CI and is especially important because the PR title becomes the commit message when a PR is squash-merged.
|
||||
|
||||
### Merge policy
|
||||
|
||||
Commits should be atomic and independently revertable. Squash at reviewer discretion for obvious cases (multiple WIP commits, messy review-response history). When your commits are clean and logical, they will be preserved as individual commits on `main`.
|
||||
|
||||
### Cleaning up commits
|
||||
|
||||
If CI flags your commit messages, you can fix them with an interactive rebase:
|
||||
|
||||
```bash
|
||||
# Squash all commits into one:
|
||||
git rebase -i HEAD~N # replace N with the number of commits
|
||||
# mark all commits except the first as 'squash' or 'fixup'
|
||||
# reword the remaining commit to follow the format
|
||||
git push --force-with-lease
|
||||
|
||||
# Or reword specific commits:
|
||||
git rebase -i HEAD~N
|
||||
# mark the bad commits as 'reword'
|
||||
git push --force-with-lease
|
||||
```
|
||||
|
||||
## Test your changes
|
||||
|
||||
PX4 is safety-critical software. All contributions must include adequate testing where practical:
|
||||
|
||||
- **New features** must include unit tests and/or integration tests that exercise the new functionality, where practical. Hardware-dependent changes that cannot be tested in SITL should include bench test or flight test evidence.
|
||||
- **Bug fixes** must include a regression test where practical. When automated testing is not feasible (hardware-specific issues, race conditions, etc.), provide a link to a flight log demonstrating the fix and the reproduction steps for the original bug.
|
||||
- **Reviewers** will verify that tests or test evidence exist before approving a pull request.
|
||||
|
||||
### Types of tests
|
||||
|
||||
| Test type | When to use | How to run |
|
||||
|-----------|-------------|------------|
|
||||
| **Unit tests** (gtest) | Module-level logic, math, parsing | `make tests` |
|
||||
| **SITL integration tests** (MAVSDK) | Flight behavior, failsafes, missions | `test/mavsdk_tests/` |
|
||||
| **Bench tests / flight logs** | Hardware-dependent changes | Upload logs to [Flight Review](https://logs.px4.io) |
|
||||
|
||||
Since we care about safety, we will regularly ask you for test results. Best is to do a test flight (or bench test where it applies) and upload the log file from it (on the microSD card in the logs directory) to Google Drive or Dropbox and share the link.
|
||||
|
||||
## Push your changes
|
||||
|
||||
Push changes to your repo and send a [pull request](https://github.com/PX4/PX4-Autopilot/compare/).
|
||||
|
||||
Make sure to provide some testing feedback and if possible the link to a flight log file. Upload flight log files to [Flight Review](http://logs.px4.io) and link the resulting report.
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
## This file should be placed in the root directory of your project.
|
||||
## Then modify the CMakeLists.txt file in the root directory of your
|
||||
## project to incorporate the testing dashboard.
|
||||
##
|
||||
## # The following are required to submit to the CDash dashboard:
|
||||
## ENABLE_TESTING()
|
||||
## INCLUDE(CTest)
|
||||
|
||||
set(CTEST_PROJECT_NAME "PX4 Firmware")
|
||||
set(CTEST_NIGHTLY_START_TIME "00:00:00 EST")
|
||||
|
||||
set(CTEST_DROP_METHOD "http")
|
||||
set(CTEST_DROP_SITE "my.cdash.org")
|
||||
set(CTEST_DROP_LOCATION "/submit.php?project=PX4+Firmware")
|
||||
set(CTEST_DROP_SITE_CDASH TRUE)
|
||||
Vendored
+1
@@ -101,6 +101,7 @@ pipeline {
|
||||
echo $0;
|
||||
git clone https://github.com/emscripten-core/emsdk.git _emscripten_sdk;
|
||||
cd _emscripten_sdk;
|
||||
git checkout 4.0.15;
|
||||
./emsdk install latest;
|
||||
./emsdk activate latest;
|
||||
cd ..;
|
||||
|
||||
@@ -332,6 +332,7 @@ bootloaders_update: \
|
||||
cuav_7-nano_bootloader \
|
||||
cuav_fmu-v6x_bootloader \
|
||||
cuav_x25-evo_bootloader \
|
||||
cuav_x25-super_bootloader \
|
||||
cubepilot_cubeorange_bootloader \
|
||||
cubepilot_cubeorangeplus_bootloader \
|
||||
hkust_nxt-dual_bootloader \
|
||||
@@ -412,7 +413,7 @@ tests:
|
||||
$(call cmake-build,px4_sitl_test)
|
||||
|
||||
# work around lcov bug #316; remove once lcov is fixed (see https://github.com/linux-test-project/lcov/issues/316)
|
||||
LCOBUG = --ignore-errors mismatch
|
||||
LCOBUG = --ignore-errors mismatch,negative
|
||||
tests_coverage:
|
||||
@$(MAKE) clean
|
||||
@$(MAKE) --no-print-directory tests PX4_CMAKE_BUILD_TYPE=Coverage
|
||||
@@ -492,13 +493,29 @@ px4_sitl_default-clang:
|
||||
@cd "$(SRC_DIR)"/build/px4_sitl_default-clang && cmake "$(SRC_DIR)" $(CMAKE_ARGS) -G"$(PX4_CMAKE_GENERATOR)" -DCONFIG=px4_sitl_default -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
|
||||
@$(PX4_MAKE) -C "$(SRC_DIR)"/build/px4_sitl_default-clang
|
||||
|
||||
# Paths to exclude from clang-tidy (auto-generated from .gitmodules + manual additions):
|
||||
# - All submodules (external code we consume, not edit)
|
||||
# - Test code (allowed looser style)
|
||||
# - Example code (educational, not production)
|
||||
# - Vendored third-party code (e.g., CMSIS_5)
|
||||
# - NuttX-only drivers excluded at CMake level (mcp_common); I2C-dependent libs excluded here (smbus)
|
||||
# - GPIO excluded here (NuttX platform headers)
|
||||
# - Emscripten failsafe web build: source path + Unity build path (failsafe_test.dir)
|
||||
# because CMake Unity Builds merge sources into a generated .cxx under build/
|
||||
#
|
||||
# To add manual exclusions, append to CLANG_TIDY_EXCLUDE_EXTRA below.
|
||||
# Submodules are automatically excluded - no action needed when adding new ones.
|
||||
CLANG_TIDY_SUBMODULES := $(shell git config --file .gitmodules --get-regexp path | awk '{print $$2}' | tr '\n' '|' | sed 's/|$$//')
|
||||
CLANG_TIDY_EXCLUDE_EXTRA := src/systemcmds/tests|src/examples|src/modules/gyro_fft/CMSIS_5|src/lib/drivers/smbus|src/drivers/gpio|src/modules/commander/failsafe/emscripten|failsafe_test\.dir|\.pb\.cc
|
||||
CLANG_TIDY_EXCLUDE := $(CLANG_TIDY_SUBMODULES)|$(CLANG_TIDY_EXCLUDE_EXTRA)
|
||||
|
||||
clang-tidy: px4_sitl_default-clang
|
||||
@cd "$(SRC_DIR)"/build/px4_sitl_default-clang && "$(SRC_DIR)"/Tools/run-clang-tidy.py -header-filter=".*\.hpp" -j$(j_clang_tidy) -p .
|
||||
@cd "$(SRC_DIR)"/build/px4_sitl_default-clang && "$(SRC_DIR)"/Tools/run-clang-tidy.py -header-filter=".*\.hpp" -j$(j_clang_tidy) -exclude="$(CLANG_TIDY_EXCLUDE)" -p .
|
||||
|
||||
# to automatically fix a single check at a time, eg modernize-redundant-void-arg
|
||||
# % run-clang-tidy-4.0.py -fix -j4 -checks=-\*,modernize-redundant-void-arg -p .
|
||||
clang-tidy-fix: px4_sitl_default-clang
|
||||
@cd "$(SRC_DIR)"/build/px4_sitl_default-clang && "$(SRC_DIR)"/Tools/run-clang-tidy.py -header-filter=".*\.hpp" -j$(j_clang_tidy) -fix -p .
|
||||
@cd "$(SRC_DIR)"/build/px4_sitl_default-clang && "$(SRC_DIR)"/Tools/run-clang-tidy.py -header-filter=".*\.hpp" -j$(j_clang_tidy) -exclude="$(CLANG_TIDY_EXCLUDE)" -fix -p .
|
||||
|
||||
# TODO: Fix cppcheck errors then try --enable=warning,performance,portability,style,unusedFunction or --enable=all
|
||||
cppcheck: px4_sitl_default
|
||||
@@ -598,3 +615,10 @@ failsafe_web:
|
||||
run_failsafe_web_server: failsafe_web
|
||||
@cd build/px4_sitl_default_failsafe_web && \
|
||||
python3 -m http.server
|
||||
|
||||
# Generate reference documentation for uORB messages
|
||||
.PHONY: msg_docs
|
||||
msg_docs:
|
||||
$(call colorecho,'Generating uORB message reference docs')
|
||||
@mkdir -p build/msg_docs
|
||||
@./Tools/msg/generate_msg_docs.py -d build/msg_docs
|
||||
|
||||
@@ -1,62 +1,110 @@
|
||||
# PX4 Drone Autopilot
|
||||
<p align="center">
|
||||
<a href="https://px4.io">
|
||||
<img src="docs/assets/site/px4_logo.svg" alt="PX4 Autopilot" width="240">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
[](https://github.com/PX4/PX4-Autopilot/releases) [](https://zenodo.org/badge/latestdoi/22634/PX4/PX4-Autopilot)
|
||||
<p align="center">
|
||||
<em>The autopilot stack the industry builds on.</em>
|
||||
</p>
|
||||
|
||||
[](https://github.com/PX4/PX4-Autopilot/actions/workflows/build_all_targets.yml) [](https://github.com/PX4/PX4-Autopilot/actions?query=workflow%3A%22SITL+Tests%22)
|
||||
<p align="center">
|
||||
<a href="https://github.com/PX4/PX4-Autopilot/releases"><img src="https://img.shields.io/github/release/PX4/PX4-Autopilot.svg" alt="Releases"></a>
|
||||
<a href="https://www.bestpractices.dev/projects/6520"><img src="https://www.bestpractices.dev/projects/6520/badge" alt="OpenSSF Best Practices"></a>
|
||||
<a href="https://zenodo.org/badge/latestdoi/22634/PX4/PX4-Autopilot"><img src="https://zenodo.org/badge/22634/PX4/PX4-Autopilot.svg" alt="DOI"></a>
|
||||
<a href="https://github.com/PX4/PX4-Autopilot/actions/workflows/build_all_targets.yml"><img src="https://github.com/PX4/PX4-Autopilot/actions/workflows/build_all_targets.yml/badge.svg?branch=main" alt="Build Targets"></a>
|
||||
<a href="https://discord.gg/dronecode"><img src="https://discordapp.com/api/guilds/1022170275984457759/widget.png?style=shield" alt="Discord"></a>
|
||||
</p>
|
||||
|
||||
[](https://discord.gg/dronecode)
|
||||
---
|
||||
|
||||
This repository holds the [PX4](http://px4.io) flight control solution for drones, with the main applications located in the [src/modules](https://github.com/PX4/PX4-Autopilot/tree/main/src/modules) directory. It also contains the PX4 Drone Middleware Platform, which provides drivers and middleware to run drones.
|
||||
## About
|
||||
|
||||
PX4 is highly portable, OS-independent and supports Linux, NuttX and MacOS out of the box.
|
||||
PX4 is an open-source autopilot stack for drones and unmanned vehicles. It supports multirotors, fixed-wing, VTOL, rovers, and many more experimental platforms from racing quads to industrial survey aircraft. It runs on [NuttX](https://nuttx.apache.org/), Linux, and macOS. Licensed under [BSD 3-Clause](LICENSE).
|
||||
|
||||
* Official Website: http://px4.io (License: BSD 3-clause, [LICENSE](https://github.com/PX4/PX4-Autopilot/blob/main/LICENSE))
|
||||
* [Supported airframes](https://docs.px4.io/main/en/airframes/airframe_reference.html) ([portfolio](https://px4.io/ecosystem/commercial-systems/)):
|
||||
* [Multicopters](https://docs.px4.io/main/en/frames_multicopter/)
|
||||
* [Fixed wing](https://docs.px4.io/main/en/frames_plane/)
|
||||
* [VTOL](https://docs.px4.io/main/en/frames_vtol/)
|
||||
* [Autogyro](https://docs.px4.io/main/en/frames_autogyro/)
|
||||
* [Rover](https://docs.px4.io/main/en/frames_rover/)
|
||||
* many more experimental types (Blimps, Boats, Submarines, High Altitude Balloons, Spacecraft, etc)
|
||||
* Releases: [Downloads](https://github.com/PX4/PX4-Autopilot/releases)
|
||||
## Why PX4
|
||||
|
||||
## Releases
|
||||
**Modular architecture.** PX4 is built around [uORB](https://docs.px4.io/main/en/middleware/uorb.html), a [DDS](https://docs.px4.io/main/en/middleware/uxrce_dds.html)-compatible publish/subscribe middleware. Modules are fully parallelized and thread safe. You can build custom configurations and trim what you don't need.
|
||||
|
||||
Release notes and supporting information for PX4 releases can be found on the [Developer Guide](https://docs.px4.io/main/en/releases/).
|
||||
**Wide hardware support.** PX4 runs on a wide range of [autopilot boards](https://docs.px4.io/main/en/flight_controller/) and supports an extensive set of sensors, telemetry radios, and actuators through the [Pixhawk](https://pixhawk.org/) ecosystem.
|
||||
|
||||
## Building a PX4 based drone, rover, boat or robot
|
||||
**Developer friendly.** First-class support for [MAVLink](https://mavlink.io/) and [DDS / ROS 2](https://docs.px4.io/main/en/ros2/) integration. Comprehensive [SITL simulation](https://docs.px4.io/main/en/simulation/), hardware-in-the-loop testing, and [log analysis](https://docs.px4.io/main/en/log/flight_log_analysis.html) tools. An active developer community on [Discord](https://discord.gg/dronecode) and the [weekly dev call](https://docs.px4.io/main/en/contribute/).
|
||||
|
||||
The [PX4 User Guide](https://docs.px4.io/main/en/) explains how to assemble [supported vehicles](https://docs.px4.io/main/en/airframes/airframe_reference.html) and fly drones with PX4. See the [forum and chat](https://docs.px4.io/main/en/#getting-help) if you need help!
|
||||
**Vendor neutral governance.** PX4 is hosted under the [Dronecode Foundation](https://www.dronecode.org/), part of the Linux Foundation. Business-friendly BSD-3 license. No single vendor controls the roadmap.
|
||||
|
||||
## Supported Vehicles
|
||||
|
||||
## Changing Code and Contributing
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://docs.px4.io/main/en/frames_multicopter/">
|
||||
<img src="docs/assets/airframes/types/QuadRotorX.svg" width="50" alt="Multicopter"><br>
|
||||
<sub>Multicopter</sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://docs.px4.io/main/en/frames_plane/">
|
||||
<img src="docs/assets/airframes/types/Plane.svg" width="50" alt="Fixed Wing"><br>
|
||||
<sub>Fixed Wing</sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://docs.px4.io/main/en/frames_vtol/">
|
||||
<img src="docs/assets/airframes/types/VTOLPlane.svg" width="50" alt="VTOL"><br>
|
||||
<sub>VTOL</sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://docs.px4.io/main/en/frames_rover/">
|
||||
<img src="docs/assets/airframes/types/Rover.svg" width="50" alt="Rover"><br>
|
||||
<sub>Rover</sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
This [Developer Guide](https://docs.px4.io/main/en/development/development.html) is for software developers who want to modify the flight stack and middleware (e.g. to add new flight modes), hardware integrators who want to support new flight controller boards and peripherals, and anyone who wants to get PX4 working on a new (unsupported) airframe/vehicle.
|
||||
<sub>…and many more: helicopters, autogyros, airships, submarines, boats, and other experimental platforms. These frames have basic support but are not part of the regular flight-test program. See the <a href="https://docs.px4.io/main/en/airframes/airframe_reference.html">full airframe reference</a>.</sub>
|
||||
|
||||
Developers should read the [Guide for Contributions](https://docs.px4.io/main/en/contribute/).
|
||||
See the [forum and chat](https://docs.px4.io/main/en/#getting-help) if you need help!
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
git clone https://github.com/PX4/PX4-Autopilot.git --recursive
|
||||
cd PX4-Autopilot
|
||||
make px4_sitl
|
||||
```
|
||||
|
||||
## Weekly Dev Call
|
||||
> [!NOTE]
|
||||
> See the [Development Guide](https://docs.px4.io/main/en/development/development.html) for toolchain setup and build options.
|
||||
|
||||
The PX4 Dev Team syncs up on a [weekly dev call](https://docs.px4.io/main/en/contribute/).
|
||||
## Documentation & Resources
|
||||
|
||||
> **Note** The dev call is open to all interested developers (not just the core dev team). This is a great opportunity to meet the team and contribute to the ongoing development of the platform. It includes a QA session for newcomers. All regular calls are listed in the [Dronecode calendar](https://www.dronecode.org/calendar/).
|
||||
| Resource | Description |
|
||||
| --- | --- |
|
||||
| [User Guide](https://docs.px4.io/main/en/) | Build, configure, and fly with PX4 |
|
||||
| [Developer Guide](https://docs.px4.io/main/en/development/development.html) | Modify the flight stack, add peripherals, port to new hardware |
|
||||
| [Airframe Reference](https://docs.px4.io/main/en/airframes/airframe_reference.html) | Full list of supported frames |
|
||||
| [Autopilot Hardware](https://docs.px4.io/main/en/flight_controller/) | Compatible flight controllers |
|
||||
| [Release Notes](https://docs.px4.io/main/en/releases/) | What's new in each release |
|
||||
| [Contribution Guide](https://docs.px4.io/main/en/contribute/) | How to contribute to PX4 |
|
||||
|
||||
## Community
|
||||
|
||||
## Maintenance Team
|
||||
- **Weekly Dev Call** — open to all developers ([Dronecode calendar](https://www.dronecode.org/calendar/))
|
||||
- **Discord** — [Join the Dronecode server](https://discord.gg/dronecode)
|
||||
- **Discussion Forum** — [PX4 Discuss](https://discuss.px4.io/)
|
||||
- **Maintainers** — see [`MAINTAINERS.md`](MAINTAINERS.md)
|
||||
- **Contributor Stats** — [LFX Insights](https://insights.lfx.linuxfoundation.org/foundation/dronecode)
|
||||
|
||||
See the latest list of maintainers on [MAINTAINERS](MAINTAINERS.md) file at the root of the project.
|
||||
## Contributing
|
||||
|
||||
For the latest stats on contributors please see the latest stats for the Dronecode ecosystem in our project dashboard under [LFX Insights](https://insights.lfx.linuxfoundation.org/foundation/dronecode). For information on how to update your profile and affiliations please see the following support link on how to [Complete Your LFX Profile](https://docs.linuxfoundation.org/lfx/my-profile/complete-your-lfx-profile). Dronecode publishes a yearly snapshot of contributions and achievements on its [website under the Reports section](https://dronecode.org).
|
||||
We welcome contributions of all kinds — bug reports, documentation, new features, and code reviews. Please read the [Contribution Guide](https://docs.px4.io/main/en/contribute/) to get started.
|
||||
|
||||
## Supported Hardware
|
||||
## Governance
|
||||
|
||||
For the most up to date information, please visit [PX4 User Guide > Autopilot Hardware](https://docs.px4.io/main/en/flight_controller/).
|
||||
The PX4 Autopilot project is hosted by the [Dronecode Foundation](https://www.dronecode.org/), a [Linux Foundation](https://www.linuxfoundation.org/) Collaborative Project. Dronecode holds all PX4 trademarks and serves as the project's legal guardian, ensuring vendor-neutral stewardship — no single company owns the name or controls the roadmap. The source code is licensed under the [BSD 3-Clause](LICENSE) license, so you are free to use, modify, and distribute it in your own projects.
|
||||
|
||||
## Project Governance
|
||||
|
||||
The PX4 Autopilot project including all of its trademarks is hosted under [Dronecode](https://www.dronecode.org/), part of the Linux Foundation.
|
||||
|
||||
<a href="https://www.dronecode.org/" style="padding:20px" ><img src="https://dronecode.org/wp-content/uploads/sites/24/2020/08/dronecode_logo_default-1.png" alt="Dronecode Logo" width="110px"/></a>
|
||||
<div style="padding:10px"> </div>
|
||||
<p align="center">
|
||||
<a href="https://www.dronecode.org/">
|
||||
<img src="docs/assets/site/dronecode_logo.svg" alt="Dronecode Logo" width="180">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# @name QuadrotorX SITL for SIH (Swarm)
|
||||
#
|
||||
# @type Quadrotor
|
||||
#
|
||||
# @maintainer Ramon Roche <mrpollo@gmail.com>
|
||||
#
|
||||
|
||||
. ${R}etc/init.d/rc.mc_defaults
|
||||
|
||||
PX4_SIMULATOR=${PX4_SIMULATOR:=sihsim}
|
||||
PX4_SIM_MODEL=${PX4_SIM_MODEL:=quadx}
|
||||
|
||||
# Simulated sensors: GPS + baro + mag
|
||||
param set-default SENS_EN_GPSSIM 1
|
||||
param set-default SENS_EN_BAROSIM 1
|
||||
param set-default SENS_EN_MAGSIM 1
|
||||
|
||||
# Square quadrotor X PX4 numbering
|
||||
param set-default CA_ROTOR_COUNT 4
|
||||
param set-default CA_ROTOR0_PX 1
|
||||
param set-default CA_ROTOR0_PY 1
|
||||
param set-default CA_ROTOR1_PX -1
|
||||
param set-default CA_ROTOR1_PY -1
|
||||
param set-default CA_ROTOR2_PX 1
|
||||
param set-default CA_ROTOR2_PY -1
|
||||
param set-default CA_ROTOR2_KM -0.05
|
||||
param set-default CA_ROTOR3_PX -1
|
||||
param set-default CA_ROTOR3_PY 1
|
||||
param set-default CA_ROTOR3_KM -0.05
|
||||
|
||||
param set-default PWM_MAIN_FUNC1 101
|
||||
param set-default PWM_MAIN_FUNC2 102
|
||||
param set-default PWM_MAIN_FUNC3 103
|
||||
param set-default PWM_MAIN_FUNC4 104
|
||||
|
||||
param set SIH_VEHICLE_TYPE 0
|
||||
@@ -44,8 +44,6 @@ param set-default FW_T_SINK_MIN 3
|
||||
|
||||
param set-default FW_W_EN 1
|
||||
|
||||
param set-default FD_ESCS_EN 0
|
||||
|
||||
param set-default MIS_TAKEOFF_ALT 30
|
||||
|
||||
param set-default NAV_ACC_RAD 15
|
||||
|
||||
@@ -101,6 +101,6 @@ param set-default NAV_ACC_RAD 5
|
||||
param set-default NAV_DLL_ACT 2
|
||||
|
||||
param set-default VT_FWD_THRUST_EN 4
|
||||
param set-default VT_F_TRANS_THR 0.3
|
||||
param set-default VT_PITCH_MIN -5
|
||||
param set-default VT_F_TRANS_THR 1
|
||||
param set-default VT_TYPE 2
|
||||
param set-default FD_ESCS_EN 0
|
||||
|
||||
@@ -19,5 +19,6 @@ param set-default MNT_MAN_PITCH 2
|
||||
param set-default MNT_MAN_YAW 3
|
||||
|
||||
param set-default MNT_RANGE_ROLL 180
|
||||
param set-default MNT_RANGE_PITCH 180
|
||||
param set-default MNT_MAX_PITCH 45
|
||||
param set-default MNT_MIN_PITCH -135
|
||||
param set-default MNT_RANGE_YAW 720
|
||||
|
||||
@@ -26,7 +26,6 @@ param set-default SENS_EN_GPSSIM 1
|
||||
param set-default SENS_EN_BAROSIM 1
|
||||
param set-default SENS_EN_MAGSIM 1
|
||||
param set-default COM_ARM_CHK_ESCS 0 # We don't have ESCs
|
||||
param set-default FD_ESCS_EN 0 # We don't have ESCs - but maybe we need this later?
|
||||
|
||||
# Set proper failsafes
|
||||
param set-default COM_ACT_FAIL_ACT 0
|
||||
@@ -42,23 +41,21 @@ param set-default FD_FAIL_R 0
|
||||
param set-default CA_ROTOR_COUNT 8
|
||||
param set-default CA_R_REV 255
|
||||
|
||||
param set-default CA_ROTOR0_AX -1
|
||||
param set-default CA_ROTOR0_AY 1
|
||||
param set-default CA_ROTOR0_AX 1
|
||||
param set-default CA_ROTOR0_AY -1
|
||||
param set-default CA_ROTOR0_AZ 0
|
||||
param set-default CA_ROTOR0_KM 0
|
||||
param set-default CA_ROTOR0_PX 0.14
|
||||
param set-default CA_ROTOR0_PY 0.10
|
||||
param set-default CA_ROTOR0_PZ 0.06
|
||||
#param set-default CA_ROTOR0_PZ 0.0
|
||||
|
||||
param set-default CA_ROTOR1_AX -1
|
||||
param set-default CA_ROTOR1_AY -1
|
||||
param set-default CA_ROTOR1_AX 1
|
||||
param set-default CA_ROTOR1_AY 1
|
||||
param set-default CA_ROTOR1_AZ 0
|
||||
param set-default CA_ROTOR1_KM 0
|
||||
param set-default CA_ROTOR1_PX 0.14
|
||||
param set-default CA_ROTOR1_PY -0.10
|
||||
param set-default CA_ROTOR1_PZ 0.06
|
||||
#param set-default CA_ROTOR1_PZ 0.0
|
||||
|
||||
param set-default CA_ROTOR2_AX 1
|
||||
param set-default CA_ROTOR2_AY 1
|
||||
@@ -67,7 +64,6 @@ param set-default CA_ROTOR2_KM 0
|
||||
param set-default CA_ROTOR2_PX -0.14
|
||||
param set-default CA_ROTOR2_PY 0.10
|
||||
param set-default CA_ROTOR2_PZ 0.06
|
||||
#param set-default CA_ROTOR2_PZ 0.0
|
||||
|
||||
param set-default CA_ROTOR3_AX 1
|
||||
param set-default CA_ROTOR3_AY -1
|
||||
@@ -79,7 +75,7 @@ param set-default CA_ROTOR3_PZ 0.06
|
||||
|
||||
param set-default CA_ROTOR4_AX 0
|
||||
param set-default CA_ROTOR4_AY 0
|
||||
param set-default CA_ROTOR4_AZ 1
|
||||
param set-default CA_ROTOR4_AZ -1
|
||||
param set-default CA_ROTOR4_KM 0
|
||||
param set-default CA_ROTOR4_PX 0.12
|
||||
param set-default CA_ROTOR4_PY 0.22
|
||||
@@ -103,7 +99,7 @@ param set-default CA_ROTOR6_PZ 0
|
||||
|
||||
param set-default CA_ROTOR7_AX 0
|
||||
param set-default CA_ROTOR7_AY 0
|
||||
param set-default CA_ROTOR7_AZ 1
|
||||
param set-default CA_ROTOR7_AZ -1
|
||||
param set-default CA_ROTOR7_KM 0
|
||||
param set-default CA_ROTOR7_PX -0.12
|
||||
param set-default CA_ROTOR7_PY -0.22
|
||||
|
||||
@@ -28,7 +28,6 @@ param set-default SIM_GZ_EN 1
|
||||
|
||||
param set-default SENS_EN_MAGSIM 1
|
||||
param set-default COM_ARM_CHK_ESCS 0 # We don't have ESCs
|
||||
param set-default FD_ESCS_EN 0
|
||||
|
||||
param set-default CA_AIRFRAME 14
|
||||
param set-default MAV_TYPE 45
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# @name KTH-ATMOS
|
||||
#
|
||||
# @type Free-Flyer
|
||||
# @class Spacecraft
|
||||
#
|
||||
# @output Motor1 back left thruster, +x thrust
|
||||
# @output Motor2 front left thruster, -x thrust
|
||||
# @output Motor3 back right thruster, +x thrust
|
||||
# @output Motor4 front right thruster, -x thrust
|
||||
# @output Motor5 front left thruster, +y thrust
|
||||
# @output Motor6 front right thruster, -y thrust
|
||||
# @output Motor7 back left thruster, +y thrust
|
||||
# @output Motor8 back right thruster, -y thrust
|
||||
#
|
||||
# @maintainer discower-io
|
||||
# @url https://atmos.discower.io
|
||||
#
|
||||
|
||||
. ${R}etc/init.d/rc.sc_defaults
|
||||
|
||||
PX4_SIMULATOR=${PX4_SIMULATOR:=gz}
|
||||
PX4_GZ_WORLD=${PX4_GZ_WORLD:=default}
|
||||
PX4_SIM_MODEL=${PX4_SIM_MODEL:=atmos_dual}
|
||||
|
||||
param set-default SIM_GZ_EN 1
|
||||
|
||||
param set-default SENS_EN_MAGSIM 1
|
||||
param set-default COM_ARM_CHK_ESCS 0 # We don't have ESCs
|
||||
|
||||
param set-default CA_AIRFRAME 14
|
||||
param set-default MAV_TYPE 45
|
||||
|
||||
param set-default CA_ROTOR_COUNT 8
|
||||
param set-default CA_R_REV 0
|
||||
|
||||
# Auto to be provided by Custom Airframe
|
||||
param set-default CA_METHOD 0
|
||||
|
||||
# Set proper failsafes
|
||||
param set-default COM_ACT_FAIL_ACT 0
|
||||
param set-default COM_LOW_BAT_ACT 0
|
||||
param set-default NAV_DLL_ACT 0
|
||||
param set-default GF_ACTION 1
|
||||
param set-default NAV_RCL_ACT 1
|
||||
|
||||
# disable attitude failure detection
|
||||
param set-default FD_FAIL_P 0
|
||||
param set-default FD_FAIL_R 0
|
||||
|
||||
param set-default CA_ROTOR0_PX -0.12
|
||||
param set-default CA_ROTOR0_PY -0.12
|
||||
param set-default CA_ROTOR0_PZ 0.0
|
||||
param set-default CA_ROTOR0_CT 1.4
|
||||
param set-default CA_ROTOR0_AX 1.0
|
||||
param set-default CA_ROTOR0_AY 0.0
|
||||
param set-default CA_ROTOR0_AZ 0.0
|
||||
|
||||
param set-default CA_ROTOR1_PX 0.12
|
||||
param set-default CA_ROTOR1_PY -0.12
|
||||
param set-default CA_ROTOR1_PZ 0.0
|
||||
param set-default CA_ROTOR1_CT 1.4
|
||||
param set-default CA_ROTOR1_AX -1.0
|
||||
param set-default CA_ROTOR1_AY 0.0
|
||||
param set-default CA_ROTOR1_AZ 0.0
|
||||
|
||||
param set-default CA_ROTOR2_PX -0.12
|
||||
param set-default CA_ROTOR2_PY 0.12
|
||||
param set-default CA_ROTOR2_PZ 0.0
|
||||
param set-default CA_ROTOR2_CT 1.4
|
||||
param set-default CA_ROTOR2_AX 1.0
|
||||
param set-default CA_ROTOR2_AY 0.0
|
||||
param set-default CA_ROTOR2_AZ 0.0
|
||||
|
||||
param set-default CA_ROTOR3_PX 0.12
|
||||
param set-default CA_ROTOR3_PY 0.12
|
||||
param set-default CA_ROTOR3_PZ 0.0
|
||||
param set-default CA_ROTOR3_CT 1.4
|
||||
param set-default CA_ROTOR3_AX -1.0
|
||||
param set-default CA_ROTOR3_AY 0.0
|
||||
param set-default CA_ROTOR3_AZ 0.0
|
||||
|
||||
param set-default CA_ROTOR4_PX 0.12
|
||||
param set-default CA_ROTOR4_PY -0.12
|
||||
param set-default CA_ROTOR4_PZ 0.0
|
||||
param set-default CA_ROTOR4_CT 1.4
|
||||
param set-default CA_ROTOR4_AX 0.0
|
||||
param set-default CA_ROTOR4_AY 1.0
|
||||
param set-default CA_ROTOR4_AZ 0.0
|
||||
|
||||
param set-default CA_ROTOR5_PX 0.12
|
||||
param set-default CA_ROTOR5_PY 0.12
|
||||
param set-default CA_ROTOR5_PZ 0.0
|
||||
param set-default CA_ROTOR5_CT 1.4
|
||||
param set-default CA_ROTOR5_AX 0.0
|
||||
param set-default CA_ROTOR5_AY -1.0
|
||||
param set-default CA_ROTOR5_AZ 0.0
|
||||
|
||||
param set-default CA_ROTOR6_PX -0.12
|
||||
param set-default CA_ROTOR6_PY -0.12
|
||||
param set-default CA_ROTOR6_PZ 0.0
|
||||
param set-default CA_ROTOR6_CT 1.4
|
||||
param set-default CA_ROTOR6_AX 0.0
|
||||
param set-default CA_ROTOR6_AY 1.0
|
||||
param set-default CA_ROTOR6_AZ 0.0
|
||||
|
||||
param set-default CA_ROTOR7_PX -0.12
|
||||
param set-default CA_ROTOR7_PY 0.12
|
||||
param set-default CA_ROTOR7_PZ 0.0
|
||||
param set-default CA_ROTOR7_CT 1.4
|
||||
param set-default CA_ROTOR7_AX 0.0
|
||||
param set-default CA_ROTOR7_AY -1.0
|
||||
param set-default CA_ROTOR7_AZ 0.0
|
||||
|
||||
param set-default SIM_GZ_EC_FUNC1 101
|
||||
param set-default SIM_GZ_EC_FUNC2 102
|
||||
param set-default SIM_GZ_EC_FUNC3 103
|
||||
param set-default SIM_GZ_EC_FUNC4 104
|
||||
param set-default SIM_GZ_EC_FUNC5 105
|
||||
param set-default SIM_GZ_EC_FUNC6 106
|
||||
param set-default SIM_GZ_EC_FUNC7 107
|
||||
param set-default SIM_GZ_EC_FUNC8 108
|
||||
param set-default SIM_GZ_EC_FUNC9 301
|
||||
param set-default SIM_GZ_EC_FUNC10 302
|
||||
param set-default SIM_GZ_EC_FUNC11 303
|
||||
param set-default SIM_GZ_EC_FUNC12 304
|
||||
|
||||
param set-default SIM_GZ_EC_MIN1 0
|
||||
param set-default SIM_GZ_EC_MIN2 0
|
||||
param set-default SIM_GZ_EC_MIN3 0
|
||||
param set-default SIM_GZ_EC_MIN4 0
|
||||
param set-default SIM_GZ_EC_MIN5 0
|
||||
param set-default SIM_GZ_EC_MIN6 0
|
||||
param set-default SIM_GZ_EC_MIN7 0
|
||||
param set-default SIM_GZ_EC_MIN8 0
|
||||
param set-default SIM_GZ_EC_MIN9 1100
|
||||
param set-default SIM_GZ_EC_MIN10 1100
|
||||
param set-default SIM_GZ_EC_MIN11 1100
|
||||
param set-default SIM_GZ_EC_MIN12 1100
|
||||
|
||||
param set-default SIM_GZ_EC_MAX1 10000
|
||||
param set-default SIM_GZ_EC_MAX2 10000
|
||||
param set-default SIM_GZ_EC_MAX3 10000
|
||||
param set-default SIM_GZ_EC_MAX4 10000
|
||||
param set-default SIM_GZ_EC_MAX5 10000
|
||||
param set-default SIM_GZ_EC_MAX6 10000
|
||||
param set-default SIM_GZ_EC_MAX7 10000
|
||||
param set-default SIM_GZ_EC_MAX8 10000
|
||||
param set-default SIM_GZ_EC_MAX9 1900
|
||||
param set-default SIM_GZ_EC_MAX10 1900
|
||||
param set-default SIM_GZ_EC_MAX11 1900
|
||||
param set-default SIM_GZ_EC_MAX12 1900
|
||||
|
||||
# Controller Tunings
|
||||
param set SC_YAWRATE_P 3.335
|
||||
param set SC_YAWRATE_I 0.87
|
||||
param set SC_YAWRATE_D 0.15
|
||||
param set SC_YR_INT_LIM 0.2
|
||||
param set SC_YAW_P 3.0
|
||||
|
||||
param set SPC_POS_P 0.20
|
||||
param set SPC_VEL_P 6.55
|
||||
param set SPC_VEL_I 0.0
|
||||
param set SPC_VEL_D 0.0
|
||||
param set SPC_VEL_MAX 12.0
|
||||
@@ -107,6 +107,7 @@ px4_add_romfs_files(
|
||||
10043_sihsim_standard_vtol
|
||||
10044_sihsim_hex
|
||||
10045_sihsim_rover_ackermann
|
||||
10046_sihsim_quadx_vision
|
||||
|
||||
17001_flightgear_tf-g1
|
||||
17002_flightgear_tf-g2
|
||||
@@ -118,6 +119,7 @@ px4_add_romfs_files(
|
||||
60002_gz_uuv_bluerov2_heavy
|
||||
|
||||
70000_gz_atmos
|
||||
70001_gz_atmos_dual
|
||||
|
||||
# [22000, 22999] Reserve for custom models
|
||||
)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
udp_offboard_port_local=$((14580+px4_instance))
|
||||
udp_offboard_port_remote=$((14540+px4_instance))
|
||||
[ "$px4_instance" -gt 9 ] && udp_offboard_port_remote=14549 # use the same ports for more than 10 instances to avoid port overlaps
|
||||
udp_onboard_payload_port_local=$((14280+px4_instance))
|
||||
udp_onboard_payload_port_remote=$((14030+px4_instance))
|
||||
udp_onboard_gimbal_port_local=$((13030+px4_instance))
|
||||
|
||||
@@ -126,15 +126,6 @@ then
|
||||
set AUTOCNF yes
|
||||
fi
|
||||
|
||||
# Allow overriding parameters via env variables: export PX4_PARAM_{name}={value}
|
||||
env | while IFS='=' read -r line; do
|
||||
value=${line#*=}
|
||||
name=${line%%=*}
|
||||
case $name in
|
||||
"PX4_PARAM_"*) param set "${name#PX4_PARAM_}" "$value" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# multi-instance setup
|
||||
# shellcheck disable=SC2154
|
||||
param set MAV_SYS_ID $((px4_instance+1))
|
||||
@@ -238,6 +229,15 @@ then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Allow overriding parameters via env variables: export PX4_PARAM_{name}={value}
|
||||
env | while IFS='=' read -r line; do
|
||||
value=${line#*=}
|
||||
name=${line%%=*}
|
||||
case $name in
|
||||
"PX4_PARAM_"*) param set "${name#PX4_PARAM_}" "$value" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
dataman start
|
||||
|
||||
# only start the simulator if not in replay mode, as both control the lockstep time
|
||||
|
||||
@@ -123,3 +123,9 @@ if(CONFIG_MODULES_TEMPERATURE_COMPENSATION)
|
||||
rc.thermal_cal
|
||||
)
|
||||
endif()
|
||||
|
||||
if(CONFIG_DRIVERS_VTXTABLE)
|
||||
px4_add_romfs_files(
|
||||
rc.vtxtable
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
#
|
||||
# @board px4_fmu-v2 exclude
|
||||
# @board bitcraze_crazyflie exclude
|
||||
# @board px4_fmu-v6x exclude
|
||||
# @board ark_fmu-v6x exclude
|
||||
#
|
||||
|
||||
. ${R}etc/init.d/rc.fw_defaults
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
#
|
||||
# @board px4_fmu-v2 exclude
|
||||
# @board bitcraze_crazyflie exclude
|
||||
# @board px4_fmu-v6x exclude
|
||||
# @board ark_fmu-v6x exclude
|
||||
#
|
||||
|
||||
. ${R}etc/init.d/rc.fw_defaults
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#
|
||||
# @board px4_fmu-v2 exclude
|
||||
# @board bitcraze_crazyflie exclude
|
||||
# @board px4_fmu-v6x exclude
|
||||
# @board ark_fmu-v6x exclude
|
||||
#
|
||||
|
||||
. ${R}etc/init.d/rc.mc_defaults
|
||||
@@ -75,9 +77,6 @@ param set-default NAV_ACC_RAD 2
|
||||
param set-default RTL_DESCEND_ALT 5
|
||||
param set-default RTL_RETURN_ALT 5
|
||||
|
||||
# Logging Parameters
|
||||
param set-default SDLOG_PROFILE 131
|
||||
|
||||
# Sensors Parameters
|
||||
param set-default SENS_CM8JL65_CFG 104
|
||||
param set-default SENS_FLOW_MAXHGT 25
|
||||
|
||||
@@ -78,9 +78,6 @@ param set-default NAV_ACC_RAD 2
|
||||
param set-default RTL_DESCEND_ALT 5
|
||||
param set-default RTL_RETURN_ALT 5
|
||||
|
||||
# Logging Parameters
|
||||
param set-default SDLOG_PROFILE 131
|
||||
|
||||
# Sensors Parameters
|
||||
param set-default SENS_CM8JL65_CFG 202
|
||||
param set-default SENS_FLOW_MAXHGT 25
|
||||
|
||||
@@ -29,9 +29,6 @@ param set-default MPC_MAN_TILT_MAX 60
|
||||
|
||||
param set-default THR_MDL_FAC 0.3
|
||||
|
||||
# enable high-rate logging profile (helps with tuning)
|
||||
param set-default SDLOG_PROFILE 19
|
||||
|
||||
param set-default IMU_DGYRO_CUTOFF 50
|
||||
param set-default IMU_GYRO_CUTOFF 90
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# @name HolyBro QAV250
|
||||
#
|
||||
# @url https://docs.px4.io/main/en/frames_multicopter/holybro_qav250_pixhawk4_mini.html
|
||||
# @url https://docs.px4.io/main/en/frames_multicopter/holybro_qav250_pixhawk4_mini
|
||||
#
|
||||
# @type Quadrotor x
|
||||
# @class Copter
|
||||
@@ -11,6 +11,8 @@
|
||||
#
|
||||
# @board px4_fmu-v2 exclude
|
||||
# @board bitcraze_crazyflie exclude
|
||||
# @board px4_fmu-v6x exclude
|
||||
# @board ark_fmu-v6x exclude
|
||||
#
|
||||
|
||||
. ${R}etc/init.d/rc.mc_defaults
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#
|
||||
# @board px4_fmu-v2 exclude
|
||||
# @board bitcraze_crazyflie exclude
|
||||
# @board px4_fmu-v6x exclude
|
||||
# @board ark_fmu-v6x exclude
|
||||
#
|
||||
|
||||
. ${R}etc/init.d/rc.mc_defaults
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# @name Aion Robotics R1 UGV
|
||||
#
|
||||
# @url https://docs.px4.io/main/en/complete_vehicles_rover/aion_r1.html
|
||||
# @url https://docs.px4.io/main/en/complete_vehicles_rover/aion_r1
|
||||
#
|
||||
# @type Rover
|
||||
# @class Rover
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
|
||||
. ${R}etc/init.d/rc.uuv_defaults
|
||||
|
||||
# Overwrite DDS AG IP to `192.168.0.1`
|
||||
param set-default UXRCE_DDS_AG_IP -1062731775
|
||||
|
||||
# param set-default MAV_1_CONFIG 102
|
||||
|
||||
param set-default BAT1_A_PER_V 37.8798
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
# @board px4_fmu-v4pro exclude
|
||||
# @board px4_fmu-v5 exclude
|
||||
# @board px4_fmu-v5x exclude
|
||||
# @board px4_fmu-v6x exclude
|
||||
# @board bitcraze_crazyflie exclude
|
||||
# @board ark_fmu-v6x exclude
|
||||
#
|
||||
|
||||
. ${R}etc/init.d/rc.mc_defaults
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
|
||||
. ${R}etc/init.d/rc.sc_defaults
|
||||
|
||||
# Overwrite DDS AG IP to `192.168.0.1`
|
||||
param set-default UXRCE_DDS_AG_IP -1062731775
|
||||
|
||||
param set-default CA_AIRFRAME 14
|
||||
param set-default MAV_TYPE 45
|
||||
|
||||
|
||||
@@ -41,3 +41,9 @@ if param compare -s MC_NN_EN 1
|
||||
then
|
||||
mc_nn_control start
|
||||
fi
|
||||
|
||||
|
||||
if param compare -s MC_RAPTOR_ENABLE 1
|
||||
then
|
||||
mc_raptor start
|
||||
fi
|
||||
|
||||
@@ -8,9 +8,6 @@ set VEHICLE_TYPE spacecraft
|
||||
# MAV_TYPE_SPACECRAFT_ORBITTER
|
||||
param set-default MAV_TYPE 45
|
||||
|
||||
# Set micro-dds-client to use ethernet and IP-address 192.168.0.1
|
||||
param set-default UXRCE_DDS_AG_IP -1062731775
|
||||
|
||||
# Disable preflight disarm to not interfere with external launching
|
||||
param set-default COM_DISARM_PRFLT -1
|
||||
param set-default CBRK_SUPPLY_CHK 894281
|
||||
|
||||
@@ -237,6 +237,12 @@ then
|
||||
tla2528 start -X
|
||||
fi
|
||||
|
||||
# Start TMP102 temperature sensor
|
||||
if param compare SENS_EN_TMP102 1
|
||||
then
|
||||
tmp102 start -X
|
||||
fi
|
||||
|
||||
# probe for optional external I2C devices
|
||||
if param compare SENS_EXT_I2C_PRB 1
|
||||
then
|
||||
|
||||
@@ -10,9 +10,6 @@ set VEHICLE_TYPE uuv
|
||||
# MAV_TYPE_SUBMARINE 12
|
||||
param set-default MAV_TYPE 12
|
||||
|
||||
# Set micro-dds-client to use ethernet and IP-address 192.168.0.1
|
||||
param set-default UXRCE_DDS_AG_IP -1062731775
|
||||
|
||||
# Disable preflight disarm to not interfere with external launching
|
||||
param set-default COM_DISARM_PRFLT -1
|
||||
param set-default CBRK_SUPPLY_CHK 894281
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# VTX table loading script.
|
||||
#
|
||||
# NOTE: Script variables are declared/initialized/unset in the rcS script.
|
||||
#
|
||||
|
||||
vtxtable load
|
||||
@@ -479,6 +479,19 @@ else
|
||||
pwm_out start
|
||||
fi
|
||||
|
||||
#
|
||||
# Optional UAVCAN/DroneCAN or Cyphal
|
||||
#
|
||||
if param greater -s UAVCAN_ENABLE 0
|
||||
then
|
||||
uavcan start
|
||||
else
|
||||
if param greater -s CYPHAL_ENABLE 0
|
||||
then
|
||||
cyphal start
|
||||
fi
|
||||
fi
|
||||
|
||||
#
|
||||
# Configure vehicle type specific parameters.
|
||||
# Note: rc.vehicle_setup is the entry point for all vehicle type specific setup.
|
||||
@@ -507,6 +520,11 @@ else
|
||||
#
|
||||
. ${R}etc/init.d/rc.serial
|
||||
|
||||
if param greater -s ZENOH_ENABLE 0
|
||||
then
|
||||
zenoh start
|
||||
fi
|
||||
|
||||
# Must be started after the serial config is read
|
||||
rc_input start $RC_INPUT_ARGS
|
||||
|
||||
@@ -612,6 +630,16 @@ else
|
||||
fi
|
||||
unset RC_LOGGING
|
||||
|
||||
#
|
||||
# Start the VTX services.
|
||||
#
|
||||
set RC_VTXTABLE ${R}etc/init.d/rc.vtxtable
|
||||
if [ -f ${RC_VTXTABLE} ]
|
||||
then
|
||||
. ${RC_VTXTABLE}
|
||||
fi
|
||||
unset RC_VTXTABLE
|
||||
|
||||
#
|
||||
# Set additional parameters and env variables for selected AUTOSTART.
|
||||
#
|
||||
@@ -628,27 +656,6 @@ else
|
||||
fi
|
||||
unset BOARD_BOOTLOADER_UPGRADE
|
||||
|
||||
#
|
||||
# Check if UAVCAN is enabled, default to it for ESCs.
|
||||
#
|
||||
if param greater -s UAVCAN_ENABLE 0
|
||||
then
|
||||
# Start core UAVCAN module.
|
||||
if ! uavcan start
|
||||
then
|
||||
tune_control play error
|
||||
fi
|
||||
else
|
||||
if param greater -s CYPHAL_ENABLE 0
|
||||
then
|
||||
cyphal start
|
||||
fi
|
||||
fi
|
||||
if param greater -s ZENOH_ENABLE 0
|
||||
then
|
||||
zenoh start
|
||||
fi
|
||||
|
||||
#
|
||||
# End of autostart.
|
||||
#
|
||||
|
||||
+28
-12
@@ -2,24 +2,40 @@
|
||||
|
||||
## Supported Versions
|
||||
|
||||
The following is a list of versions the development team is currently supporting.
|
||||
The following versions receive security updates:
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 1.4.x | :white_check_mark: |
|
||||
| 1.3.3 | :white_check_mark: |
|
||||
| < 1.3 | :x: |
|
||||
| 1.16.x | :white_check_mark: |
|
||||
| < 1.16 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
We currently only receive security vulnerability reports through GitHub.
|
||||
We receive security vulnerability reports through GitHub Security Advisories.
|
||||
|
||||
To begin a report, please go to the top-level repository, for example, PX4/PX4-Autopilot,
|
||||
and click on the Security tab. If you are on mobile, click the ... dropdown menu, and then click Security.
|
||||
To begin a report, go to the [PX4/PX4-Autopilot](https://github.com/PX4/PX4-Autopilot) repository
|
||||
and click on the **Security** tab. If you are on mobile, click the **...** dropdown menu, then click **Security**.
|
||||
|
||||
Click Report a Vulnerability to open the advisory form. Fill in the advisory details form.
|
||||
Make sure your title is descriptive, and the development team can find all of the relevant details needed
|
||||
to verify on the description box. We recommend you add as much data as possible. We welcome logs,
|
||||
screenshots, photos, and videos, anything that can help us verify and identify the issues being reported.
|
||||
Click **Report a Vulnerability** to open the advisory form. Fill in the advisory details form.
|
||||
Make sure your title is descriptive and the description contains all relevant details needed
|
||||
to verify the issue. We welcome logs, screenshots, photos, and videos.
|
||||
|
||||
At the bottom of the form, click Submit report. The maintainer team will be notified and will get back to you ASAP.
|
||||
At the bottom of the form, click **Submit report**.
|
||||
|
||||
## Response Process
|
||||
|
||||
1. **Acknowledgment**: The maintainer team will acknowledge your report within **7 days**.
|
||||
2. **Triage**: We will assess severity and impact and communicate next steps.
|
||||
3. **Disclosure**: We coordinate disclosure with the reporter. We follow responsible disclosure practices and will credit reporters in the advisory unless they request anonymity.
|
||||
|
||||
If you do not receive acknowledgment within 7 days, please follow up by emailing the [release managers](MAINTAINERS.md).
|
||||
|
||||
## Secure Development Practices
|
||||
|
||||
The PX4 development team applies the following practices to reduce security risk:
|
||||
|
||||
- **Code review**: All changes require peer review before merging.
|
||||
- **Static analysis**: [clang-tidy](https://clang.llvm.org/extra/clang-tidy/) runs on every pull request with warnings treated as errors.
|
||||
- **Fuzzing**: A daily fuzzing pipeline using [Google fuzztest](https://github.com/google/fuzztest) tests MAVLink message handling and GNSS driver protocol parsing.
|
||||
- **Input validation**: All external inputs (MAVLink messages, RC signals, sensor data) are validated against expected ranges before use.
|
||||
- **Compiler hardening**: Builds use `-Wall -Werror`, stack protectors, and other hardening flags where supported by the target platform.
|
||||
|
||||
@@ -21,11 +21,13 @@ exec find boards msg src platforms test \
|
||||
-path src/lib/crypto/monocypher -prune -o \
|
||||
-path src/lib/events/libevents -prune -o \
|
||||
-path src/lib/parameters/uthash -prune -o \
|
||||
-path src/lib/rl_tools/rl_tools -prune -o \
|
||||
-path src/lib/wind_estimator/python/generated -prune -o \
|
||||
-path src/modules/ekf2/EKF/python/ekf_derivation/generated -prune -o \
|
||||
-path src/modules/ekf2/EKF/yaw_estimator/derivation/generated -prune -o \
|
||||
-path src/modules/gyro_fft/CMSIS_5 -prune -o \
|
||||
-path src/modules/mavlink/mavlink -prune -o \
|
||||
-path src/modules/mc_raptor/blob -prune -o \
|
||||
-path test/fuzztest -prune -o \
|
||||
-path test/mavsdk_tests/catch2 -prune -o \
|
||||
-path src/lib/crypto/monocypher -prune -o \
|
||||
@@ -37,6 +39,8 @@ exec find boards msg src platforms test \
|
||||
-path src/lib/cdrstream/rosidl -prune -o \
|
||||
-path src/modules/zenoh/zenoh-pico -prune -o \
|
||||
-path boards/modalai/voxl2/libfc-sensor-api -prune -o \
|
||||
-path boards/modalai/voxl2/src/lib/mpa/libmodal-json -prune -o \
|
||||
-path boards/modalai/voxl2/src/lib/mpa/libmodal-pipe -prune -o \
|
||||
-path src/drivers/actuators/vertiq_io/iq-module-communication-cpp -prune -o \
|
||||
-path src/lib/tensorflow_lite_micro/tflite_micro -prune -o \
|
||||
-path src/drivers/ins/sbgecom/sbgECom -prune -o \
|
||||
|
||||
@@ -17,37 +17,12 @@ if [[ -f $1"/.git" || -d $1"/.git" ]]; then
|
||||
SUBMODULE_STATUS=$(git submodule summary "$1")
|
||||
STATUSRETVAL=$(echo $SUBMODULE_STATUS | grep -A20 -i "$1")
|
||||
if ! [[ -z "$STATUSRETVAL" ]]; then
|
||||
echo -e "\033[31mChecked $1 submodule, ACTION REQUIRED:\033[0m"
|
||||
echo ""
|
||||
echo -e "Different commits:"
|
||||
echo -e "\033[33mWarning: $1 submodule has uncommitted changes:\033[0m"
|
||||
echo -e "$SUBMODULE_STATUS"
|
||||
echo ""
|
||||
echo -e "To update submodules to the expected version, run:"
|
||||
echo -e " \033[94mgit submodule sync --recursive && git submodule update --init --recursive\033[0m"
|
||||
echo ""
|
||||
echo -e " *******************************************************************************"
|
||||
echo -e " * \033[31mIF YOU DID NOT CHANGE THIS FILE (OR YOU DON'T KNOW WHAT A SUBMODULE IS):\033[0m *"
|
||||
echo -e " * \033[31mHit 'u' and <ENTER> to update ALL submodules and resolve this.\033[0m *"
|
||||
echo -e " * (performs \033[94mgit submodule sync --recursive\033[0m *"
|
||||
echo -e " * and \033[94mgit submodule update --init --recursive\033[0m ) *"
|
||||
echo -e " *******************************************************************************"
|
||||
echo ""
|
||||
echo ""
|
||||
echo -e " Only for EXPERTS:"
|
||||
echo -e " $1 submodule is not in the recommended version."
|
||||
echo -e " Hit 'y' and <ENTER> to continue the build with this version. Hit <ENTER> to resolve manually."
|
||||
echo -e " Use \033[94mgit add $1 && git commit -m 'Updated $1'\033[0m to choose this version (careful!)"
|
||||
echo ""
|
||||
read user_cmd
|
||||
if [ "$user_cmd" == "y" ]; then
|
||||
echo "Continuing build with manually overridden submodule.."
|
||||
elif [ "$user_cmd" == "u" ]; then
|
||||
git submodule sync --recursive -- $1
|
||||
git submodule update --init --recursive -- $1 || true
|
||||
git submodule update --init --recursive --force -- $1
|
||||
echo "Submodule fixed, continuing build.."
|
||||
else
|
||||
echo "Build aborted."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
git submodule --quiet sync --recursive --quiet -- $1
|
||||
|
||||
Executable
+331
@@ -0,0 +1,331 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate commit messages in a PR against conventional commits format.
|
||||
|
||||
Reads a JSON array of GitHub commit objects from stdin (as returned by the
|
||||
GitHub API's /pulls/{n}/commits endpoint) and checks each message for
|
||||
blocking errors and advisory warnings.
|
||||
|
||||
With --markdown, outputs a formatted PR comment body instead of plain text.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
from conventional_commits import (
|
||||
EXEMPT_PREFIXES,
|
||||
parse_header,
|
||||
)
|
||||
|
||||
# Blocking: prefixes that indicate unsquashed fixup commits
|
||||
FIXUP_PREFIXES = ('fixup!', 'squash!', 'amend!')
|
||||
|
||||
# Blocking: single-word throwaway messages (case-insensitive exact match)
|
||||
THROWAWAY_WORDS = frozenset({
|
||||
'fix', 'fixed', 'fixes',
|
||||
'update', 'updated', 'updates',
|
||||
'test', 'tests', 'testing',
|
||||
'tmp', 'temp',
|
||||
'oops', 'wip',
|
||||
'debug', 'cleanup',
|
||||
})
|
||||
|
||||
# Blocking: debug session leftovers
|
||||
DEBUG_KEYWORDS = ('tmate',)
|
||||
|
||||
# Warning: review-response messages (case-insensitive substring match)
|
||||
REVIEW_RESPONSE_PATTERNS = (
|
||||
'address review',
|
||||
'apply suggestions from code review',
|
||||
'code review',
|
||||
)
|
||||
|
||||
# Warning: formatter-only commits
|
||||
FORMATTER_PATTERNS = (
|
||||
'do make format',
|
||||
'make format',
|
||||
'run formatter',
|
||||
'apply format',
|
||||
)
|
||||
|
||||
MIN_MESSAGE_LENGTH = 5
|
||||
|
||||
|
||||
def check_commit(message: str) -> tuple[list[str], list[str]]:
|
||||
"""Return (errors, warnings) for a single commit message."""
|
||||
errors: list[str] = []
|
||||
warnings: list[str] = []
|
||||
|
||||
first_line = message.split('\n', 1)[0].strip()
|
||||
lower = first_line.lower()
|
||||
|
||||
# --- Blocking checks ---
|
||||
|
||||
for prefix in FIXUP_PREFIXES:
|
||||
if lower.startswith(prefix):
|
||||
errors.append(f'Unsquashed commit: starts with "{prefix}"')
|
||||
|
||||
if lower == 'wip' or lower.startswith('wip ') or lower.startswith('wip:'):
|
||||
errors.append('WIP commit should not be merged')
|
||||
|
||||
if len(first_line) < MIN_MESSAGE_LENGTH:
|
||||
errors.append(f'Message too short ({len(first_line)} chars, minimum {MIN_MESSAGE_LENGTH})')
|
||||
|
||||
if first_line.strip() and first_line.strip().lower() in THROWAWAY_WORDS:
|
||||
errors.append(f'Single-word throwaway message: "{first_line.strip()}"')
|
||||
|
||||
for kw in DEBUG_KEYWORDS:
|
||||
if kw in lower:
|
||||
errors.append(f'Debug session leftover: contains "{kw}"')
|
||||
|
||||
# --- Warning checks ---
|
||||
|
||||
for pattern in REVIEW_RESPONSE_PATTERNS:
|
||||
if pattern in lower:
|
||||
warnings.append('Review-response commit')
|
||||
break
|
||||
|
||||
for pattern in FORMATTER_PATTERNS:
|
||||
if pattern in lower:
|
||||
warnings.append('Formatter-only commit')
|
||||
break
|
||||
|
||||
if not parse_header(first_line):
|
||||
# Exempt merge commits
|
||||
for prefix in EXEMPT_PREFIXES:
|
||||
if first_line.startswith(prefix):
|
||||
break
|
||||
else:
|
||||
warnings.append(
|
||||
'Missing conventional commit format '
|
||||
'(e.g. "feat(ekf2): add something")'
|
||||
)
|
||||
|
||||
return errors, warnings
|
||||
|
||||
|
||||
def suggest_commit(message: str) -> str | None:
|
||||
"""Suggest how to fix a bad commit message."""
|
||||
first_line = message.split('\n', 1)[0].strip()
|
||||
lower = first_line.lower()
|
||||
|
||||
for prefix in FIXUP_PREFIXES:
|
||||
if lower.startswith(prefix):
|
||||
return 'Squash this into the commit it fixes'
|
||||
|
||||
if lower == 'wip' or lower.startswith('wip ') or lower.startswith('wip:'):
|
||||
return 'Reword with a descriptive message (e.g. "feat(scope): what changed")'
|
||||
|
||||
if len(first_line) < MIN_MESSAGE_LENGTH:
|
||||
return 'Reword with a descriptive message (e.g. "feat(ekf2): what changed")'
|
||||
|
||||
if first_line.strip().lower() in THROWAWAY_WORDS:
|
||||
return 'Reword with a descriptive message (e.g. "fix(scope): what changed")'
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def format_plain(data: list) -> tuple[bool, bool]:
|
||||
"""Print plain text output. Returns (has_blocking, has_warnings)."""
|
||||
has_blocking = False
|
||||
has_warnings = False
|
||||
|
||||
for commit in data:
|
||||
sha = commit.get('sha', '?')[:10]
|
||||
message = commit.get('commit', {}).get('message', '')
|
||||
first_line = message.split('\n', 1)[0].strip()
|
||||
|
||||
errors, warnings = check_commit(message)
|
||||
|
||||
if errors or warnings:
|
||||
print(f"\n {sha} {first_line}")
|
||||
|
||||
for err in errors:
|
||||
print(f" ERROR: {err}")
|
||||
has_blocking = True
|
||||
|
||||
for warn in warnings:
|
||||
print(f" WARNING: {warn}")
|
||||
has_warnings = True
|
||||
|
||||
if has_blocking:
|
||||
print(
|
||||
"\n"
|
||||
"ERROR = must fix before merging (CI will block the PR)\n"
|
||||
"WARNING = advisory, not blocking, but recommended to fix\n"
|
||||
"\n"
|
||||
"See the contributing guide for details:\n"
|
||||
" https://github.com/PX4/PX4-Autopilot/blob/main/CONTRIBUTING.md#commit-message-convention\n",
|
||||
)
|
||||
|
||||
elif has_warnings:
|
||||
print(
|
||||
"\n"
|
||||
"WARNING = advisory, not blocking, but recommended to fix\n"
|
||||
"\n"
|
||||
"See the contributing guide for details:\n"
|
||||
" https://github.com/PX4/PX4-Autopilot/blob/main/CONTRIBUTING.md#commit-message-convention\n",
|
||||
)
|
||||
|
||||
return has_blocking, has_warnings
|
||||
|
||||
|
||||
def format_markdown_blocking(data: list) -> str:
|
||||
"""Format a blocking error markdown comment."""
|
||||
error_groups: dict[str, list[str]] = {}
|
||||
unique_commits: list[tuple[str, str, list[str], str]] = []
|
||||
|
||||
for commit in data:
|
||||
sha = commit.get('sha', '?')[:10]
|
||||
message = commit.get('commit', {}).get('message', '')
|
||||
first_line = message.split('\n', 1)[0].strip()
|
||||
|
||||
errors, _ = check_commit(message)
|
||||
if not errors:
|
||||
continue
|
||||
|
||||
suggestion = suggest_commit(message) or ''
|
||||
unique_commits.append((sha, first_line, errors, suggestion))
|
||||
|
||||
for err in errors:
|
||||
error_groups.setdefault(err, []).append(sha)
|
||||
|
||||
lines = [
|
||||
"## \u274c Commit messages need attention before merging",
|
||||
"",
|
||||
]
|
||||
|
||||
has_large_group = any(len(shas) > 3 for shas in error_groups.values())
|
||||
|
||||
if has_large_group:
|
||||
lines.extend([
|
||||
"**Issues found:**",
|
||||
"",
|
||||
])
|
||||
for err_msg, shas in error_groups.items():
|
||||
if len(shas) > 3:
|
||||
lines.append(f"- **{len(shas)} commits**: {err_msg} "
|
||||
f"(`{shas[0]}`, `{shas[1]}`, ... `{shas[-1]}`)")
|
||||
else:
|
||||
sha_list = ', '.join(f'`{s}`' for s in shas)
|
||||
lines.append(f"- {err_msg}: {sha_list}")
|
||||
|
||||
distinct_messages = {msg for _, msg, _, _ in unique_commits}
|
||||
if len(distinct_messages) <= 5:
|
||||
lines.extend(["", "**Affected commits:**", ""])
|
||||
for sha, msg, errors, suggestion in unique_commits:
|
||||
safe_msg = msg.replace('|', '\\|')
|
||||
lines.append(f"- `{sha}` {safe_msg}")
|
||||
else:
|
||||
lines.extend([
|
||||
"| Commit | Message | Issue | Suggested fix |",
|
||||
"|--------|---------|-------|---------------|",
|
||||
])
|
||||
for sha, msg, errors, suggestion in unique_commits:
|
||||
issues = '; '.join(errors)
|
||||
safe_msg = msg.replace('|', '\\|')
|
||||
lines.append(f"| `{sha}` | {safe_msg} | {issues} | {suggestion} |")
|
||||
|
||||
lines.extend([
|
||||
"",
|
||||
"See [CONTRIBUTING.md](https://github.com/PX4/PX4-Autopilot/blob/main/CONTRIBUTING.md#commit-message-convention) "
|
||||
"for how to clean up commits.",
|
||||
"",
|
||||
"---",
|
||||
"*This comment will be automatically removed once the issues are resolved.*",
|
||||
])
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def format_markdown_advisory(data: list) -> str:
|
||||
"""Format an advisory warning markdown comment."""
|
||||
lines = [
|
||||
"## \U0001f4a1 Commit messages could be improved",
|
||||
"",
|
||||
"Not blocking, but these commit messages could use some cleanup.",
|
||||
"",
|
||||
"| Commit | Message | Suggestion |",
|
||||
"|--------|---------|------------|",
|
||||
]
|
||||
|
||||
for commit in data:
|
||||
sha = commit.get('sha', '?')[:10]
|
||||
message = commit.get('commit', {}).get('message', '')
|
||||
first_line = message.split('\n', 1)[0].strip()
|
||||
|
||||
_, warnings = check_commit(message)
|
||||
if not warnings:
|
||||
continue
|
||||
|
||||
suggestion = '; '.join(warnings)
|
||||
safe_msg = first_line.replace('|', '\\|')
|
||||
lines.append(f"| `{sha}` | {safe_msg} | {suggestion} |")
|
||||
|
||||
lines.extend([
|
||||
"",
|
||||
"See the [commit message convention](https://github.com/PX4/PX4-Autopilot/blob/main/CONTRIBUTING.md#commit-message-convention) "
|
||||
"for details.",
|
||||
"",
|
||||
"---",
|
||||
"*This comment will be automatically removed once the issues are resolved.*",
|
||||
])
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
markdown_stdout = '--markdown' in sys.argv
|
||||
markdown_file = None
|
||||
for i, a in enumerate(sys.argv):
|
||||
if a == '--markdown-file' and i + 1 < len(sys.argv):
|
||||
markdown_file = sys.argv[i + 1]
|
||||
elif a.startswith('--markdown-file='):
|
||||
markdown_file = a.split('=', 1)[1]
|
||||
|
||||
try:
|
||||
data = json.load(sys.stdin)
|
||||
except json.JSONDecodeError as exc:
|
||||
print(f"Failed to parse JSON input: {exc}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
if not isinstance(data, list):
|
||||
print("Expected a JSON array of commit objects.", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
# Always compute blocking/warning state
|
||||
has_blocking = False
|
||||
has_warnings = False
|
||||
for commit in data:
|
||||
message = commit.get('commit', {}).get('message', '')
|
||||
errors, warnings = check_commit(message)
|
||||
if errors:
|
||||
has_blocking = True
|
||||
if warnings:
|
||||
has_warnings = True
|
||||
|
||||
# Generate markdown if needed
|
||||
md = None
|
||||
if has_blocking:
|
||||
md = format_markdown_blocking(data)
|
||||
elif has_warnings:
|
||||
md = format_markdown_advisory(data)
|
||||
|
||||
if md:
|
||||
if markdown_stdout:
|
||||
print(md)
|
||||
if markdown_file:
|
||||
with open(markdown_file, 'w') as f:
|
||||
f.write(md + '\n')
|
||||
elif markdown_file:
|
||||
with open(markdown_file, 'w') as f:
|
||||
pass
|
||||
|
||||
# Plain text output to stderr for CI logs (always, unless --markdown only)
|
||||
if not markdown_stdout:
|
||||
has_blocking, _ = format_plain(data)
|
||||
|
||||
sys.exit(1 if has_blocking else 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+163
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate that a PR title follows conventional commits format.
|
||||
|
||||
Format: type(scope): description
|
||||
|
||||
Can output plain text for CI logs or markdown for PR comments.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
|
||||
from conventional_commits import (
|
||||
CONVENTIONAL_TYPES,
|
||||
EXEMPT_PREFIXES,
|
||||
parse_header,
|
||||
suggest_scope,
|
||||
suggest_type,
|
||||
)
|
||||
|
||||
|
||||
def suggest_title(title: str) -> str | None:
|
||||
"""Try to suggest a corrected title in conventional commits format."""
|
||||
stripped = title.strip()
|
||||
|
||||
# Remove common bracket prefixes like [docs], [CI], etc.
|
||||
bracket_match = re.match(r'^\[([^\]]+)\]\s*(.+)', stripped)
|
||||
if bracket_match:
|
||||
prefix = bracket_match.group(1).strip().lower()
|
||||
rest = bracket_match.group(2).strip()
|
||||
rest = re.sub(r'^[\-:]\s*', '', rest).strip()
|
||||
if len(rest) >= 5:
|
||||
# Try to map bracket content to a type
|
||||
commit_type = prefix if prefix in CONVENTIONAL_TYPES else suggest_type(rest)
|
||||
scope = suggest_scope(rest)
|
||||
if scope:
|
||||
return f"{commit_type}({scope}): {rest}"
|
||||
|
||||
# Already has old-style "subsystem: description" format - convert it
|
||||
colon_match = re.match(r'^([a-zA-Z][a-zA-Z0-9_/\-\. ]*): (.+)$', stripped)
|
||||
if colon_match:
|
||||
old_subsystem = colon_match.group(1).strip()
|
||||
desc = colon_match.group(2).strip()
|
||||
if len(desc) >= 5:
|
||||
commit_type = suggest_type(desc)
|
||||
# Use the old subsystem as scope (clean it up)
|
||||
scope = old_subsystem.lower().replace(' ', '_')
|
||||
return f"{commit_type}({scope}): {desc}"
|
||||
|
||||
# No format at all - try to guess both type and scope
|
||||
commit_type = suggest_type(stripped)
|
||||
scope = suggest_scope(stripped)
|
||||
if scope:
|
||||
desc = stripped[0].lower() + stripped[1:] if stripped else stripped
|
||||
return f"{commit_type}({scope}): {desc}"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def check_title(title: str) -> bool:
|
||||
title = title.strip()
|
||||
|
||||
if not title:
|
||||
print("PR title is empty.", file=sys.stderr)
|
||||
return False
|
||||
|
||||
for prefix in EXEMPT_PREFIXES:
|
||||
if title.startswith(prefix):
|
||||
return True
|
||||
|
||||
if parse_header(title):
|
||||
return True
|
||||
|
||||
types_str = ', '.join(f'`{t}`' for t in CONVENTIONAL_TYPES.keys())
|
||||
print(
|
||||
f"PR title does not match conventional commits format.\n"
|
||||
f"\n"
|
||||
f" Title: {title}\n"
|
||||
f"\n"
|
||||
f"Expected format: type(scope): description\n"
|
||||
f"\n"
|
||||
f"Valid types: {types_str}\n"
|
||||
f"\n"
|
||||
f"Good examples:\n"
|
||||
f" feat(ekf2): add height fusion timeout\n"
|
||||
f" fix(mavlink): correct BATTERY_STATUS_V2 parsing\n"
|
||||
f" ci(workflows): migrate to reusable workflows\n"
|
||||
f" feat(boards/px4_fmu-v6x)!: remove deprecated driver API\n"
|
||||
f"\n"
|
||||
f"Bad examples:\n"
|
||||
f" fix stuff\n"
|
||||
f" Update file\n"
|
||||
f" ekf2: fix something (missing type prefix)\n"
|
||||
f"\n"
|
||||
f"See the contributing guide for details:\n"
|
||||
f" https://github.com/PX4/PX4-Autopilot/blob/main/CONTRIBUTING.md#commit-message-convention\n",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def format_markdown(title: str) -> str:
|
||||
"""Format a markdown PR comment body for a bad title."""
|
||||
lines = [
|
||||
"## \u274c PR title needs conventional commit format",
|
||||
"",
|
||||
"Expected format: `type(scope): description` "
|
||||
"([conventional commits](https://www.conventionalcommits.org/)).",
|
||||
"",
|
||||
"**Your title:**",
|
||||
f"> {title}",
|
||||
"",
|
||||
]
|
||||
|
||||
suggestion = suggest_title(title)
|
||||
if suggestion:
|
||||
lines.extend([
|
||||
"**Suggested fix:**",
|
||||
f"> {suggestion}",
|
||||
"",
|
||||
])
|
||||
|
||||
lines.extend([
|
||||
"**To fix this:** click the ✏️ next to the PR title at the top "
|
||||
"of this page and update it.",
|
||||
"",
|
||||
"See [CONTRIBUTING.md](https://github.com/PX4/PX4-Autopilot/blob/main/CONTRIBUTING.md#commit-message-convention) "
|
||||
"for details.",
|
||||
"",
|
||||
"---",
|
||||
"*This comment will be automatically removed once the issue is resolved.*",
|
||||
])
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description='Check PR title format')
|
||||
parser.add_argument('title', help='The PR title to validate')
|
||||
parser.add_argument('--markdown', action='store_true',
|
||||
help='Output markdown to stdout on failure')
|
||||
parser.add_argument('--markdown-file', metavar='FILE',
|
||||
help='Write markdown to FILE on failure')
|
||||
args = parser.parse_args()
|
||||
|
||||
passed = check_title(args.title)
|
||||
|
||||
if not passed:
|
||||
md = format_markdown(args.title)
|
||||
if args.markdown:
|
||||
print(md)
|
||||
if args.markdown_file:
|
||||
with open(args.markdown_file, 'w') as f:
|
||||
f.write(md + '\n')
|
||||
elif args.markdown_file:
|
||||
with open(args.markdown_file, 'w') as f:
|
||||
pass
|
||||
|
||||
sys.exit(0 if passed else 1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,146 @@
|
||||
"""Shared constants and helpers for conventional commit validation.
|
||||
|
||||
Format: type(scope): description
|
||||
Optional breaking change marker: type(scope)!: description
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
CONVENTIONAL_TYPES = {
|
||||
'feat': 'A new feature',
|
||||
'fix': 'A bug fix',
|
||||
'docs': 'Documentation only changes',
|
||||
'style': 'Formatting, whitespace, no code change',
|
||||
'refactor': 'Code change that neither fixes a bug nor adds a feature',
|
||||
'perf': 'Performance improvement',
|
||||
'test': 'Adding or correcting tests',
|
||||
'build': 'Build system or external dependencies',
|
||||
'ci': 'CI configuration files and scripts',
|
||||
'chore': 'Other changes that don\'t modify src or test files',
|
||||
'revert': 'Reverts a previous commit',
|
||||
}
|
||||
|
||||
# type(scope)[!]: description
|
||||
# - type: one of CONVENTIONAL_TYPES keys
|
||||
# - scope: required, alphanumeric with _/-/.
|
||||
# - !: optional breaking change marker
|
||||
# - description: at least 5 chars
|
||||
HEADER_PATTERN = re.compile(
|
||||
r'^(' + '|'.join(CONVENTIONAL_TYPES.keys()) + r')'
|
||||
r'\(([a-zA-Z0-9_/\-\.]+)\)'
|
||||
r'(!)?'
|
||||
r': (.{5,})$'
|
||||
)
|
||||
|
||||
EXEMPT_PREFIXES = ('Merge ',)
|
||||
|
||||
# Common PX4 subsystem scopes for suggestions
|
||||
KNOWN_SCOPES = [
|
||||
'ekf2', 'mavlink', 'commander', 'navigator', 'sensors',
|
||||
'mc_att_control', 'mc_pos_control', 'mc_rate_control',
|
||||
'fw_att_control', 'fw_pos_control', 'fw_rate_control',
|
||||
'vtol', 'actuators', 'battery', 'param', 'logger',
|
||||
'uorb', 'drivers', 'boards', 'simulation', 'sitl',
|
||||
'gps', 'rc', 'safety', 'can', 'serial',
|
||||
'ci', 'docs', 'build', 'cmake', 'tools',
|
||||
'mixer', 'land_detector', 'airspeed', 'gyroscope',
|
||||
'accelerometer', 'magnetometer', 'barometer',
|
||||
]
|
||||
|
||||
# Keyword patterns to suggest scopes from description text
|
||||
KEYWORD_SCOPES = [
|
||||
(r'\b(ekf|estimator|height|fusion|imu|baro)\b', 'ekf2'),
|
||||
(r'\b(mavlink|MAVLink|MAVLINK|command_int|heartbeat)\b', 'mavlink'),
|
||||
(r'\b(uorb|orb|pub|sub|topic)\b', 'uorb'),
|
||||
(r'\b(board|fmu|nuttx|stm32)\b', 'boards'),
|
||||
(r'\b(mixer|actuator|motor|servo|pwm|dshot)\b', 'actuators'),
|
||||
(r'\b(battery|power)\b', 'battery'),
|
||||
(r'\b(param|parameter)\b', 'param'),
|
||||
(r'\b(log|logger|sdlog)\b', 'logger'),
|
||||
(r'\b(sensor|accel|gyro)\b', 'sensors'),
|
||||
(r'\b(land|takeoff|rtl|mission|navigator|geofence)\b', 'navigator'),
|
||||
(r'\b(position|velocity|attitude|rate)\s*(control|ctrl)\b', 'mc_att_control'),
|
||||
(r'\b(mc|multicopter|quad)\b', 'mc_att_control'),
|
||||
(r'\b(fw|fixedwing|fixed.wing|plane)\b', 'fw_att_control'),
|
||||
(r'\b(vtol|transition)\b', 'vtol'),
|
||||
(r'\b(ci|workflow|github.action|pipeline)\b', 'ci'),
|
||||
(r'\b(doc|docs|documentation|readme)\b', 'docs'),
|
||||
(r'\b(cmake|make|toolchain|compiler)\b', 'build'),
|
||||
(r'\b(sitl|simulation|gazebo|jmavsim|sih)\b', 'simulation'),
|
||||
(r'\b(can|uavcan|cyphal|dronecan)\b', 'can'),
|
||||
(r'\b(serial|uart|spi|i2c)\b', 'serial'),
|
||||
(r'\b(safety|failsafe|arm|disarm|kill)\b', 'safety'),
|
||||
(r'\b(rc|radio|sbus|crsf|elrs|dsm)\b', 'rc'),
|
||||
(r'\b(gps|gnss|rtk|ubx)\b', 'gps'),
|
||||
(r'\b(optical.flow|flow|rangefinder|lidar|distance)\b', 'sensors'),
|
||||
(r'\b(orbit|follow|offboard)\b', 'commander'),
|
||||
(r'\b(driver)\b', 'drivers'),
|
||||
]
|
||||
|
||||
# Verb patterns to suggest conventional commit type
|
||||
VERB_TYPE_MAP = [
|
||||
(r'^fix(e[ds])?[\s:]', 'fix'),
|
||||
(r'^bug[\s:]', 'fix'),
|
||||
(r'^add(s|ed|ing)?[\s:]', 'feat'),
|
||||
(r'^implement', 'feat'),
|
||||
(r'^introduce', 'feat'),
|
||||
(r'^support', 'feat'),
|
||||
(r'^enable', 'feat'),
|
||||
(r'^update[ds]?[\s:]', 'feat'),
|
||||
(r'^improv(e[ds]?|ing)', 'perf'),
|
||||
(r'^optimi[zs](e[ds]?|ing)', 'perf'),
|
||||
(r'^refactor', 'refactor'),
|
||||
(r'^clean\s*up', 'refactor'),
|
||||
(r'^restructure', 'refactor'),
|
||||
(r'^simplif(y|ied)', 'refactor'),
|
||||
(r'^remov(e[ds]?|ing)', 'refactor'),
|
||||
(r'^delet(e[ds]?|ing)', 'refactor'),
|
||||
(r'^deprecat', 'refactor'),
|
||||
(r'^replac(e[ds]?|ing)', 'refactor'),
|
||||
(r'^renam(e[ds]?|ing)', 'refactor'),
|
||||
(r'^migrat', 'refactor'),
|
||||
(r'^revert', 'revert'),
|
||||
(r'^doc(s|ument)', 'docs'),
|
||||
(r'^test', 'test'),
|
||||
(r'^format', 'style'),
|
||||
(r'^lint', 'style'),
|
||||
(r'^whitespace', 'style'),
|
||||
(r'^build', 'build'),
|
||||
(r'^ci[\s:]', 'ci'),
|
||||
]
|
||||
|
||||
|
||||
def parse_header(text: str) -> dict | None:
|
||||
"""Parse a conventional commit header into components.
|
||||
|
||||
Returns dict with keys {type, scope, breaking, subject} or None if
|
||||
the text doesn't match conventional commits format.
|
||||
"""
|
||||
text = text.strip()
|
||||
m = HEADER_PATTERN.match(text)
|
||||
if not m:
|
||||
return None
|
||||
return {
|
||||
'type': m.group(1),
|
||||
'scope': m.group(2),
|
||||
'breaking': m.group(3) == '!',
|
||||
'subject': m.group(4),
|
||||
}
|
||||
|
||||
|
||||
def suggest_type(text: str) -> str:
|
||||
"""Infer a conventional commit type from description text."""
|
||||
lower = text.strip().lower()
|
||||
for pattern, commit_type in VERB_TYPE_MAP:
|
||||
if re.search(pattern, lower):
|
||||
return commit_type
|
||||
return 'feat'
|
||||
|
||||
|
||||
def suggest_scope(text: str) -> str | None:
|
||||
"""Infer a scope from keywords in the text."""
|
||||
lower = text.strip().lower()
|
||||
for pattern, scope in KEYWORD_SCOPES:
|
||||
if re.search(pattern, lower, re.IGNORECASE):
|
||||
return scope
|
||||
return None
|
||||
@@ -36,11 +36,20 @@ if args.filter:
|
||||
target_filter.append(target)
|
||||
|
||||
default_container = 'ghcr.io/px4/px4-dev:v1.16.0-rc1-258-g0369abd556'
|
||||
voxl2_container = 'ghcr.io/px4/px4-dev-voxl2:v1.5'
|
||||
build_configs = []
|
||||
grouped_targets = {}
|
||||
excluded_boards = ['modalai_voxl2', 'px4_ros2', 'espressif_esp32'] # TODO: fix and enable
|
||||
excluded_boards = ['px4_ros2', 'espressif_esp32'] # TODO: fix and enable
|
||||
excluded_manufacturers = ['atlflight']
|
||||
excluded_platforms = ['qurt']
|
||||
excluded_platforms = []
|
||||
|
||||
# Container overrides for platforms/boards that need a non-default container
|
||||
platform_container_overrides = {
|
||||
'qurt': voxl2_container,
|
||||
}
|
||||
board_container_overrides = {
|
||||
'modalai_voxl2': voxl2_container,
|
||||
}
|
||||
excluded_labels = [
|
||||
'stackcheck',
|
||||
'nolockstep', 'replay', 'test',
|
||||
@@ -88,7 +97,20 @@ def process_target(px4board_file, target_name):
|
||||
|
||||
if platform not in excluded_platforms:
|
||||
container = default_container
|
||||
if platform == 'posix':
|
||||
|
||||
# Extract board name (manufacturer_board) from target name
|
||||
board_name = '_'.join(target_name.split('_')[:2])
|
||||
|
||||
# Apply container overrides for specific platforms or boards
|
||||
if platform in platform_container_overrides:
|
||||
container = platform_container_overrides[platform]
|
||||
if board_name in board_container_overrides:
|
||||
container = board_container_overrides[board_name]
|
||||
|
||||
# Boards with container overrides get their own group
|
||||
if board_name in board_container_overrides or platform in platform_container_overrides:
|
||||
group = 'voxl2'
|
||||
elif platform == 'posix':
|
||||
group = 'base'
|
||||
if toolchain:
|
||||
if toolchain.startswith('aarch64'):
|
||||
@@ -126,18 +148,18 @@ grouped_targets['base']['manufacturers'] = {}
|
||||
grouped_targets['base']['manufacturers']['px4'] = []
|
||||
grouped_targets['base']['manufacturers']['px4'] += metadata_targets
|
||||
|
||||
for manufacturer in os.scandir(os.path.join(source_dir, '../boards')):
|
||||
for manufacturer in sorted(os.scandir(os.path.join(source_dir, '../boards')), key=lambda e: e.name):
|
||||
if not manufacturer.is_dir():
|
||||
continue
|
||||
if manufacturer.name in excluded_manufacturers:
|
||||
if verbose: print(f'excluding manufacturer {manufacturer.name}')
|
||||
continue
|
||||
|
||||
for board in os.scandir(manufacturer.path):
|
||||
for board in sorted(os.scandir(manufacturer.path), key=lambda e: e.name):
|
||||
if not board.is_dir():
|
||||
continue
|
||||
|
||||
for files in os.scandir(board.path):
|
||||
for files in sorted(os.scandir(board.path), key=lambda e: e.name):
|
||||
if files.is_file() and files.name.endswith('.px4board'):
|
||||
|
||||
board_name = manufacturer.name + '_' + board.name
|
||||
@@ -203,7 +225,7 @@ if (args.group):
|
||||
if(verbose):
|
||||
print(f'=:Architectures: [{grouped_targets.keys()}]')
|
||||
for arch in grouped_targets:
|
||||
runner = 'x64' if arch == 'nuttx' else 'arm64'
|
||||
runner = 'x64' if arch in ('nuttx', 'voxl2') else 'arm64'
|
||||
if(verbose):
|
||||
print(f'=:Processing: [{arch}]')
|
||||
temp_group = []
|
||||
|
||||
Executable
+431
@@ -0,0 +1,431 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# metadata_sync.sh - Unified metadata generation and synchronization for PX4 docs
|
||||
#
|
||||
# Usage:
|
||||
# Tools/ci/metadata_sync.sh [OPTIONS] [TYPES...]
|
||||
#
|
||||
# Types:
|
||||
# parameters - Parameter reference (docs/en/advanced_config/parameter_reference.md)
|
||||
# airframes - Airframe reference (docs/en/airframes/airframe_reference.md)
|
||||
# modules - Module documentation (docs/en/modules/*.md)
|
||||
# msg_docs - uORB message docs (docs/en/msg_docs/*.md + docs/en/middleware/dds_topics.md)
|
||||
# uorb_graphs - uORB graph JSONs (docs/public/middleware/*.json)
|
||||
# failsafe_web - Failsafe simulator (docs/public/config/failsafe/*.{js,wasm,json})
|
||||
# all - All of the above (default)
|
||||
#
|
||||
# Options:
|
||||
# --generate Build the make targets to generate fresh metadata
|
||||
# --sync Copy generated files to docs/
|
||||
# --verbose Show detailed output
|
||||
# --help Show this help
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 - Success (files synced or already up-to-date)
|
||||
# 1 - Error (build failed, missing files, etc.)
|
||||
#
|
||||
# Examples:
|
||||
# # Full regeneration and sync (orchestrator use case)
|
||||
# Tools/ci/metadata_sync.sh --generate --sync all
|
||||
#
|
||||
# # Just sync specific type (assumes already built)
|
||||
# Tools/ci/metadata_sync.sh --sync parameters
|
||||
#
|
||||
# # Generate only, don't copy
|
||||
# Tools/ci/metadata_sync.sh --generate uorb_graphs
|
||||
#
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Configuration
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
EMSCRIPTEN_VERSION="3.1.64"
|
||||
EMSDK_DIR="${EMSDK_DIR:-_emscripten_sdk}"
|
||||
|
||||
# All available metadata types
|
||||
ALL_TYPES=(parameters airframes modules msg_docs uorb_graphs failsafe_web)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Logging
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
VERBOSE=false
|
||||
|
||||
log() {
|
||||
echo "[metadata_sync] $*"
|
||||
}
|
||||
|
||||
log_verbose() {
|
||||
if [[ "$VERBOSE" == "true" ]]; then
|
||||
echo "[metadata_sync] $*"
|
||||
fi
|
||||
}
|
||||
|
||||
die() {
|
||||
echo "[metadata_sync] ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Help
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
show_help() {
|
||||
head -n 35 "$0" | tail -n +2 | sed 's/^# \?//'
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Emscripten Setup
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
ensure_emscripten() {
|
||||
if command -v emcc >/dev/null 2>&1; then
|
||||
log_verbose "Emscripten already available: $(emcc --version | head -1)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "Setting up Emscripten ${EMSCRIPTEN_VERSION}..."
|
||||
|
||||
if [[ ! -d "$EMSDK_DIR" ]]; then
|
||||
log_verbose "Cloning emsdk to $EMSDK_DIR"
|
||||
if [[ "$VERBOSE" == "true" ]]; then
|
||||
git clone https://github.com/emscripten-core/emsdk.git "$EMSDK_DIR"
|
||||
else
|
||||
git clone https://github.com/emscripten-core/emsdk.git "$EMSDK_DIR" >/dev/null 2>&1
|
||||
fi
|
||||
fi
|
||||
|
||||
pushd "$EMSDK_DIR" >/dev/null
|
||||
if [[ "$VERBOSE" == "true" ]]; then
|
||||
./emsdk install "$EMSCRIPTEN_VERSION"
|
||||
./emsdk activate "$EMSCRIPTEN_VERSION"
|
||||
else
|
||||
./emsdk install "$EMSCRIPTEN_VERSION" >/dev/null 2>&1
|
||||
./emsdk activate "$EMSCRIPTEN_VERSION" >/dev/null 2>&1
|
||||
fi
|
||||
popd >/dev/null
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
source "${EMSDK_DIR}/emsdk_env.sh" >/dev/null 2>&1
|
||||
|
||||
log_verbose "Emscripten ready: $(emcc --version | head -1)"
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Generation Functions
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
generate_parameters() {
|
||||
log "Generating parameters metadata..."
|
||||
if [[ "$VERBOSE" == "true" ]]; then
|
||||
make parameters_metadata
|
||||
else
|
||||
make parameters_metadata >/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
generate_airframes() {
|
||||
log "Generating airframes metadata..."
|
||||
if [[ "$VERBOSE" == "true" ]]; then
|
||||
make airframe_metadata
|
||||
else
|
||||
make airframe_metadata >/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
generate_modules() {
|
||||
log "Generating modules documentation..."
|
||||
if [[ "$VERBOSE" == "true" ]]; then
|
||||
make module_documentation
|
||||
else
|
||||
make module_documentation >/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
generate_msg_docs() {
|
||||
log "Generating message documentation..."
|
||||
if [[ "$VERBOSE" == "true" ]]; then
|
||||
make msg_docs
|
||||
else
|
||||
make msg_docs >/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
generate_uorb_graphs() {
|
||||
log "Generating uORB graphs..."
|
||||
if [[ "$VERBOSE" == "true" ]]; then
|
||||
make uorb_graphs
|
||||
else
|
||||
make uorb_graphs >/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
generate_failsafe_web() {
|
||||
ensure_emscripten
|
||||
log "Generating failsafe web..."
|
||||
if [[ "$VERBOSE" == "true" ]]; then
|
||||
make failsafe_web
|
||||
else
|
||||
make failsafe_web >/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Sync Functions
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
sync_parameters() {
|
||||
local src="build/px4_sitl_default/docs/parameters.md"
|
||||
local dest="docs/en/advanced_config/parameter_reference.md"
|
||||
|
||||
log "Syncing parameters..."
|
||||
|
||||
if [[ ! -f "$src" ]]; then
|
||||
die "Source file not found: $src (did you run --generate?)"
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
cp "$src" "$dest"
|
||||
log_verbose " $src -> $dest"
|
||||
}
|
||||
|
||||
sync_airframes() {
|
||||
local src="build/px4_sitl_default/docs/airframes.md"
|
||||
local dest="docs/en/airframes/airframe_reference.md"
|
||||
|
||||
log "Syncing airframes..."
|
||||
|
||||
if [[ ! -f "$src" ]]; then
|
||||
die "Source file not found: $src (did you run --generate?)"
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
cp "$src" "$dest"
|
||||
log_verbose " $src -> $dest"
|
||||
}
|
||||
|
||||
sync_modules() {
|
||||
local src_dir="build/px4_sitl_default/docs/modules"
|
||||
local dest_dir="docs/en/modules"
|
||||
|
||||
log "Syncing modules..."
|
||||
|
||||
if [[ ! -d "$src_dir" ]]; then
|
||||
die "Source directory not found: $src_dir (did you run --generate?)"
|
||||
fi
|
||||
|
||||
local src_files=("$src_dir"/*.md)
|
||||
if [[ ${#src_files[@]} -eq 0 ]]; then
|
||||
die "No .md files found in $src_dir"
|
||||
fi
|
||||
|
||||
mkdir -p "$dest_dir"
|
||||
|
||||
for src in "${src_files[@]}"; do
|
||||
local name
|
||||
name=$(basename "$src")
|
||||
cp "$src" "$dest_dir/$name"
|
||||
log_verbose " $src -> $dest_dir/$name"
|
||||
done
|
||||
}
|
||||
|
||||
sync_msg_docs() {
|
||||
local src_dir="build/msg_docs"
|
||||
local dest_dir="docs/en/msg_docs"
|
||||
local middleware_dir="docs/en/middleware"
|
||||
|
||||
log "Syncing message docs..."
|
||||
|
||||
if [[ ! -d "$src_dir" ]]; then
|
||||
die "Source directory not found: $src_dir (did you run --generate?)"
|
||||
fi
|
||||
|
||||
local src_files=("$src_dir"/*.md)
|
||||
if [[ ${#src_files[@]} -eq 0 ]]; then
|
||||
die "No .md files found in $src_dir"
|
||||
fi
|
||||
|
||||
mkdir -p "$dest_dir"
|
||||
mkdir -p "$middleware_dir"
|
||||
|
||||
for src in "${src_files[@]}"; do
|
||||
local name
|
||||
name=$(basename "$src")
|
||||
|
||||
# dds_topics.md goes to middleware dir
|
||||
if [[ "$name" == "dds_topics.md" ]]; then
|
||||
cp "$src" "$middleware_dir/$name"
|
||||
log_verbose " $src -> $middleware_dir/$name"
|
||||
else
|
||||
cp "$src" "$dest_dir/$name"
|
||||
log_verbose " $src -> $dest_dir/$name"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
sync_uorb_graphs() {
|
||||
local src_dir="Tools/uorb_graph"
|
||||
local dest_dir="docs/public/middleware"
|
||||
|
||||
log "Syncing uORB graphs..."
|
||||
|
||||
local src_files=("$src_dir"/*.json)
|
||||
if [[ ${#src_files[@]} -eq 0 ]]; then
|
||||
die "No .json files found in $src_dir (did you run --generate?)"
|
||||
fi
|
||||
|
||||
mkdir -p "$dest_dir"
|
||||
|
||||
for src in "${src_files[@]}"; do
|
||||
local name
|
||||
name=$(basename "$src")
|
||||
cp "$src" "$dest_dir/$name"
|
||||
log_verbose " $src -> $dest_dir/$name"
|
||||
done
|
||||
}
|
||||
|
||||
sync_failsafe_web() {
|
||||
local src_dir="build/px4_sitl_default_failsafe_web"
|
||||
local dest_dir="docs/public/config/failsafe"
|
||||
|
||||
log "Syncing failsafe web..."
|
||||
|
||||
if [[ ! -d "$src_dir" ]]; then
|
||||
die "Source directory not found: $src_dir (did you run --generate?)"
|
||||
fi
|
||||
|
||||
# Gather js, wasm, json files
|
||||
local src_files=()
|
||||
for ext in js wasm json; do
|
||||
src_files+=("$src_dir"/*."$ext")
|
||||
done
|
||||
|
||||
if [[ ${#src_files[@]} -eq 0 ]]; then
|
||||
die "No .js/.wasm/.json files found in $src_dir"
|
||||
fi
|
||||
|
||||
mkdir -p "$dest_dir"
|
||||
|
||||
for src in "${src_files[@]}"; do
|
||||
local name
|
||||
name=$(basename "$src")
|
||||
cp "$src" "$dest_dir/$name"
|
||||
log_verbose " $src -> $dest_dir/$name"
|
||||
done
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Main Logic
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
DO_GENERATE=false
|
||||
DO_SYNC=false
|
||||
SELECTED_TYPES=()
|
||||
|
||||
parse_args() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--generate)
|
||||
DO_GENERATE=true
|
||||
shift
|
||||
;;
|
||||
--sync)
|
||||
DO_SYNC=true
|
||||
shift
|
||||
;;
|
||||
--verbose)
|
||||
VERBOSE=true
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
show_help
|
||||
;;
|
||||
-*)
|
||||
die "Unknown option: $1"
|
||||
;;
|
||||
*)
|
||||
# It's a type
|
||||
SELECTED_TYPES+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Default to all types if none specified
|
||||
if [[ ${#SELECTED_TYPES[@]} -eq 0 ]]; then
|
||||
SELECTED_TYPES=("all")
|
||||
fi
|
||||
|
||||
# Expand "all" to all types
|
||||
local expanded_types=()
|
||||
for t in "${SELECTED_TYPES[@]}"; do
|
||||
if [[ "$t" == "all" ]]; then
|
||||
expanded_types+=("${ALL_TYPES[@]}")
|
||||
else
|
||||
expanded_types+=("$t")
|
||||
fi
|
||||
done
|
||||
SELECTED_TYPES=("${expanded_types[@]}")
|
||||
|
||||
# Validate types
|
||||
for t in "${SELECTED_TYPES[@]}"; do
|
||||
local valid=false
|
||||
for valid_type in "${ALL_TYPES[@]}"; do
|
||||
if [[ "$t" == "$valid_type" ]]; then
|
||||
valid=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ "$valid" == "false" ]]; then
|
||||
die "Unknown type: $t (valid: ${ALL_TYPES[*]})"
|
||||
fi
|
||||
done
|
||||
|
||||
# Must specify at least one action
|
||||
if [[ "$DO_GENERATE" == "false" && "$DO_SYNC" == "false" ]]; then
|
||||
die "Must specify at least one of: --generate, --sync"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
parse_args "$@"
|
||||
|
||||
log "Selected types: ${SELECTED_TYPES[*]}"
|
||||
[[ "$DO_GENERATE" == "true" ]] && log "Actions: generate"
|
||||
[[ "$DO_SYNC" == "true" ]] && log "Actions: sync"
|
||||
|
||||
# Remove duplicates from SELECTED_TYPES
|
||||
local -A seen
|
||||
local unique_types=()
|
||||
for t in "${SELECTED_TYPES[@]}"; do
|
||||
if [[ -z "${seen[$t]:-}" ]]; then
|
||||
seen[$t]=1
|
||||
unique_types+=("$t")
|
||||
fi
|
||||
done
|
||||
SELECTED_TYPES=("${unique_types[@]}")
|
||||
|
||||
# Generate phase
|
||||
if [[ "$DO_GENERATE" == "true" ]]; then
|
||||
log "=== Generation Phase ==="
|
||||
for t in "${SELECTED_TYPES[@]}"; do
|
||||
"generate_$t"
|
||||
done
|
||||
fi
|
||||
|
||||
# Sync phase
|
||||
if [[ "$DO_SYNC" == "true" ]]; then
|
||||
log "=== Sync Phase ==="
|
||||
for t in "${SELECTED_TYPES[@]}"; do
|
||||
"sync_$t"
|
||||
done
|
||||
fi
|
||||
|
||||
log "Done."
|
||||
exit 0
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,37 +1,47 @@
|
||||
#!/bin/bash
|
||||
|
||||
mkdir artifacts
|
||||
cp **/**/*.px4 artifacts/
|
||||
cp **/**/*.elf artifacts/
|
||||
cp **/**/*.px4 artifacts/ 2>/dev/null || true
|
||||
cp **/**/*.elf artifacts/ 2>/dev/null || true
|
||||
for build_dir_path in build/*/ ; do
|
||||
build_dir_path=${build_dir_path::${#build_dir_path}-1}
|
||||
build_dir=${build_dir_path#*/}
|
||||
mkdir artifacts/$build_dir
|
||||
mkdir -p artifacts/$build_dir
|
||||
find artifacts/ -maxdepth 1 -type f -name "*$build_dir*"
|
||||
# Airframe
|
||||
cp $build_dir_path/airframes.xml artifacts/$build_dir/
|
||||
# Airframe (NuttX: build root, SITL: docs/ subdirectory)
|
||||
airframes_src=""
|
||||
if [ -f "$build_dir_path/airframes.xml" ]; then
|
||||
airframes_src="$build_dir_path/airframes.xml"
|
||||
elif [ -f "$build_dir_path/docs/airframes.xml" ]; then
|
||||
airframes_src="$build_dir_path/docs/airframes.xml"
|
||||
fi
|
||||
if [ -n "$airframes_src" ]; then
|
||||
cp "$airframes_src" "artifacts/$build_dir/"
|
||||
fi
|
||||
# Parameters
|
||||
cp $build_dir_path/parameters.xml artifacts/$build_dir/
|
||||
cp $build_dir_path/parameters.json artifacts/$build_dir/
|
||||
cp $build_dir_path/parameters.json.xz artifacts/$build_dir/
|
||||
cp $build_dir_path/parameters.xml artifacts/$build_dir/ 2>/dev/null || true
|
||||
cp $build_dir_path/parameters.json artifacts/$build_dir/ 2>/dev/null || true
|
||||
cp $build_dir_path/parameters.json.xz artifacts/$build_dir/ 2>/dev/null || true
|
||||
# Actuators
|
||||
cp $build_dir_path/actuators.json artifacts/$build_dir/
|
||||
cp $build_dir_path/actuators.json.xz artifacts/$build_dir/
|
||||
cp $build_dir_path/actuators.json artifacts/$build_dir/ 2>/dev/null || true
|
||||
cp $build_dir_path/actuators.json.xz artifacts/$build_dir/ 2>/dev/null || true
|
||||
# Events
|
||||
cp $build_dir_path/events/all_events.json.xz artifacts/$build_dir/
|
||||
# ROS 2 msgs
|
||||
cp $build_dir_path/events/all_events.json.xz artifacts/$build_dir/
|
||||
# Module Docs
|
||||
mkdir -p artifacts/$build_dir/events/
|
||||
cp $build_dir_path/events/all_events.json.xz artifacts/$build_dir/events/ 2>/dev/null || true
|
||||
ls -la artifacts/$build_dir
|
||||
echo "----------"
|
||||
done
|
||||
|
||||
if [ -d artifacts/px4_sitl_default ]; then
|
||||
# general metadata
|
||||
mkdir artifacts/_general/
|
||||
cp artifacts/px4_sitl_default/airframes.xml artifacts/_general/
|
||||
# general metadata (used by Flight Review and other downstream consumers)
|
||||
mkdir -p artifacts/_general/
|
||||
# Airframe
|
||||
cp artifacts/px4_sitl_default/airframes.xml artifacts/_general/
|
||||
if [ -f artifacts/px4_sitl_default/airframes.xml ]; then
|
||||
cp artifacts/px4_sitl_default/airframes.xml artifacts/_general/
|
||||
else
|
||||
echo "Error: expected 'artifacts/px4_sitl_default/airframes.xml' not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
# Parameters
|
||||
cp artifacts/px4_sitl_default/parameters.xml artifacts/_general/
|
||||
cp artifacts/px4_sitl_default/parameters.json artifacts/_general/
|
||||
@@ -40,9 +50,11 @@ if [ -d artifacts/px4_sitl_default ]; then
|
||||
cp artifacts/px4_sitl_default/actuators.json artifacts/_general/
|
||||
cp artifacts/px4_sitl_default/actuators.json.xz artifacts/_general/
|
||||
# Events
|
||||
cp artifacts/px4_sitl_default/events/all_events.json.xz artifacts/_general/
|
||||
# ROS 2 msgs
|
||||
cp artifacts/px4_sitl_default/events/all_events.json.xz artifacts/_general/
|
||||
# Module Docs
|
||||
if [ -f artifacts/px4_sitl_default/events/all_events.json.xz ]; then
|
||||
cp artifacts/px4_sitl_default/events/all_events.json.xz artifacts/_general/
|
||||
else
|
||||
echo "Error: expected 'artifacts/px4_sitl_default/events/all_events.json.xz' not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
ls -la artifacts/_general/
|
||||
fi
|
||||
|
||||
Executable
+95
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Post, update, or delete a PR comment with deduplication.
|
||||
|
||||
Uses hidden HTML markers to find existing comments and avoid duplicates.
|
||||
Reads comment body from stdin when posting or updating.
|
||||
|
||||
Usage:
|
||||
echo "comment body" | python3 pr_comment.py --marker pr-title --pr 123 --result fail
|
||||
python3 pr_comment.py --marker pr-title --pr 123 --result pass
|
||||
|
||||
Results:
|
||||
fail - post/update comment with body from stdin
|
||||
warn - post/update comment with body from stdin
|
||||
pass - delete existing comment if any
|
||||
|
||||
Requires GH_TOKEN and GITHUB_REPOSITORY environment variables.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def gh_api(endpoint: str, method: str = 'GET', body: dict | None = None) -> str:
|
||||
"""Call the GitHub API via gh cli."""
|
||||
cmd = ['gh', 'api', endpoint, '-X', method]
|
||||
if body:
|
||||
for key, value in body.items():
|
||||
cmd.extend(['-f', f'{key}={value}'])
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if result.returncode != 0 and method != 'DELETE':
|
||||
print(f"gh api error: {result.stderr}", file=sys.stderr)
|
||||
return result.stdout
|
||||
|
||||
|
||||
def find_comment(repo: str, pr: int, marker: str) -> str | None:
|
||||
"""Find an existing comment by its hidden marker. Returns comment ID or None."""
|
||||
response = gh_api(f"repos/{repo}/issues/{pr}/comments?per_page=100")
|
||||
try:
|
||||
comments = json.loads(response)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
if not isinstance(comments, list):
|
||||
return None
|
||||
|
||||
for comment in comments:
|
||||
if isinstance(comment, dict) and comment.get('body', '').startswith(marker):
|
||||
return str(comment['id'])
|
||||
return None
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description='Manage PR quality comments')
|
||||
parser.add_argument('--marker', required=True,
|
||||
help='Marker name (e.g. pr-title, commit-msgs, pr-body)')
|
||||
parser.add_argument('--pr', required=True, type=int,
|
||||
help='Pull request number')
|
||||
parser.add_argument('--result', required=True, choices=['pass', 'fail', 'warn'],
|
||||
help='Check result: pass deletes comment, fail/warn posts it')
|
||||
args = parser.parse_args()
|
||||
|
||||
repo = os.environ.get('GITHUB_REPOSITORY', '')
|
||||
if not repo:
|
||||
print("GITHUB_REPOSITORY not set", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
marker = f"<!-- commit-quality-{args.marker} -->"
|
||||
existing_id = find_comment(repo, args.pr, marker)
|
||||
|
||||
if args.result == 'pass':
|
||||
if existing_id:
|
||||
gh_api(f"repos/{repo}/issues/comments/{existing_id}", method='DELETE')
|
||||
return
|
||||
|
||||
# Read comment body from stdin
|
||||
body_content = sys.stdin.read().strip()
|
||||
if not body_content:
|
||||
print("No comment body provided on stdin", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
full_body = f"{marker}\n{body_content}"
|
||||
|
||||
if existing_id:
|
||||
gh_api(f"repos/{repo}/issues/comments/{existing_id}", method='PATCH',
|
||||
body={'body': full_body})
|
||||
else:
|
||||
gh_api(f"repos/{repo}/issues/{args.pr}/comments", method='POST',
|
||||
body={'body': full_body})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+163
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# test_metadata_sync.sh - Test metadata_sync.sh locally using Docker
|
||||
#
|
||||
# Usage:
|
||||
# Tools/ci/test_metadata_sync.sh [OPTIONS] [TYPES...]
|
||||
#
|
||||
# Options:
|
||||
# --shell Drop into interactive shell instead of running sync
|
||||
# --verbose Pass --verbose to metadata_sync.sh
|
||||
# --skip-build Skip SITL build (use existing build artifacts)
|
||||
# --help Show this help
|
||||
#
|
||||
# Types:
|
||||
# Same as metadata_sync.sh: parameters, airframes, modules, msg_docs, uorb_graphs, failsafe_web, all
|
||||
#
|
||||
# Examples:
|
||||
# # Test full regeneration
|
||||
# Tools/ci/test_metadata_sync.sh all
|
||||
#
|
||||
# # Test just parameters (faster)
|
||||
# Tools/ci/test_metadata_sync.sh parameters
|
||||
#
|
||||
# # Drop into shell for debugging
|
||||
# Tools/ci/test_metadata_sync.sh --shell
|
||||
#
|
||||
# # Skip build if you already have artifacts
|
||||
# Tools/ci/test_metadata_sync.sh --skip-build --verbose all
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
DOCKER_IMAGE="px4io/px4-dev:v1.17.0-alpha1"
|
||||
CONTAINER_NAME="px4-metadata-test-$$"
|
||||
|
||||
SHELL_MODE=false
|
||||
VERBOSE=""
|
||||
SKIP_BUILD=false
|
||||
TYPES=()
|
||||
|
||||
show_help() {
|
||||
head -n 28 "$0" | tail -n +2 | sed 's/^# \?//'
|
||||
exit 0
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
echo "[test] Cleaning up container..."
|
||||
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
|
||||
}
|
||||
|
||||
parse_args() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--shell)
|
||||
SHELL_MODE=true
|
||||
shift
|
||||
;;
|
||||
--verbose)
|
||||
VERBOSE="--verbose"
|
||||
shift
|
||||
;;
|
||||
--skip-build)
|
||||
SKIP_BUILD=true
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
show_help
|
||||
;;
|
||||
-*)
|
||||
echo "Unknown option: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
TYPES+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Default to all types
|
||||
if [[ ${#TYPES[@]} -eq 0 ]]; then
|
||||
TYPES=("all")
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
parse_args "$@"
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
echo "[test] Using Docker image: $DOCKER_IMAGE"
|
||||
echo "[test] Repository root: $REPO_ROOT"
|
||||
|
||||
# Pull image if not present
|
||||
if ! docker image inspect "$DOCKER_IMAGE" >/dev/null 2>&1; then
|
||||
echo "[test] Pulling Docker image..."
|
||||
docker pull "$DOCKER_IMAGE"
|
||||
fi
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
# Handle git worktrees: the .git file points to the main repo's .git directory
|
||||
# We need to mount that directory too so git works inside the container
|
||||
local git_mounts=()
|
||||
if [[ -f "$REPO_ROOT/.git" ]]; then
|
||||
# It's a worktree - read the gitdir path and mount it
|
||||
local gitdir
|
||||
gitdir=$(grep '^gitdir:' "$REPO_ROOT/.git" | cut -d' ' -f2)
|
||||
if [[ -n "$gitdir" ]]; then
|
||||
# Mount the gitdir at the same path so the .git file reference works
|
||||
git_mounts+=("-v" "$gitdir:$gitdir:ro")
|
||||
# Also need the main .git directory (parent of worktrees/)
|
||||
local main_git_dir
|
||||
main_git_dir=$(dirname "$(dirname "$gitdir")")
|
||||
git_mounts+=("-v" "$main_git_dir:$main_git_dir:ro")
|
||||
echo "[test] Detected git worktree, mounting git directories"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$SHELL_MODE" == "true" ]]; then
|
||||
echo "[test] Starting interactive shell..."
|
||||
echo "[test] Run: Tools/ci/metadata_sync.sh --generate --sync all"
|
||||
docker run -it --rm \
|
||||
--name "$CONTAINER_NAME" \
|
||||
-v "$REPO_ROOT:/src" \
|
||||
"${git_mounts[@]}" \
|
||||
-w /src \
|
||||
"$DOCKER_IMAGE" \
|
||||
/bin/bash
|
||||
else
|
||||
echo "[test] Running metadata sync for: ${TYPES[*]}"
|
||||
|
||||
# Build the command
|
||||
local cmd=""
|
||||
|
||||
if [[ "$SKIP_BUILD" == "false" ]]; then
|
||||
cmd="Tools/ci/metadata_sync.sh --generate --sync $VERBOSE ${TYPES[*]}"
|
||||
else
|
||||
cmd="Tools/ci/metadata_sync.sh --sync $VERBOSE ${TYPES[*]}"
|
||||
fi
|
||||
|
||||
echo "[test] Command: $cmd"
|
||||
|
||||
docker run --rm \
|
||||
--name "$CONTAINER_NAME" \
|
||||
-v "$REPO_ROOT:/src" \
|
||||
"${git_mounts[@]}" \
|
||||
-w /src \
|
||||
"$DOCKER_IMAGE" \
|
||||
/bin/bash -c "$cmd"
|
||||
|
||||
echo ""
|
||||
echo "[test] Done! Check git status for changes:"
|
||||
echo " git status -s docs/"
|
||||
echo ""
|
||||
echo "[test] To see what changed:"
|
||||
echo " git diff docs/"
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
+4
-24
@@ -1,47 +1,27 @@
|
||||
#! /bin/bash
|
||||
|
||||
if [ -z ${PX4_DOCKER_REPO+x} ]; then
|
||||
echo "guessing PX4_DOCKER_REPO based on input";
|
||||
if [[ $@ =~ .*clang.* ]] || [[ $@ =~ .*scan-build.* ]]; then
|
||||
# clang tools
|
||||
PX4_DOCKER_REPO="px4io/px4-dev-clang:2021-02-04"
|
||||
elif [[ $@ =~ .*tests* ]]; then
|
||||
# run all tests with simulation
|
||||
PX4_DOCKER_REPO="px4io/px4-dev-simulation-bionic:2021-12-11"
|
||||
fi
|
||||
PX4_DOCKER_REPO="px4io/px4-dev:v1.17.0-beta1"
|
||||
else
|
||||
echo "PX4_DOCKER_REPO is set to '$PX4_DOCKER_REPO'";
|
||||
fi
|
||||
|
||||
# otherwise default to nuttx
|
||||
if [ -z ${PX4_DOCKER_REPO+x} ]; then
|
||||
PX4_DOCKER_REPO="px4io/px4-dev:v1.16.0-rc1-258-g0369abd556"
|
||||
fi
|
||||
|
||||
echo "PX4_DOCKER_REPO: $PX4_DOCKER_REPO";
|
||||
|
||||
PWD=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
|
||||
SRC_DIR=$PWD/../
|
||||
SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
|
||||
SRC_DIR=${SCRIPT_DIR}/../
|
||||
|
||||
CCACHE_DIR=${HOME}/.ccache
|
||||
mkdir -p "${CCACHE_DIR}"
|
||||
|
||||
docker run -it --rm -w "${SRC_DIR}" \
|
||||
--user="$(id -u):$(id -g)" \
|
||||
--env=AWS_ACCESS_KEY_ID \
|
||||
--env=AWS_SECRET_ACCESS_KEY \
|
||||
--env=BRANCH_NAME \
|
||||
--env=CCACHE_DIR="${CCACHE_DIR}" \
|
||||
--env=CI \
|
||||
--env=CODECOV_TOKEN \
|
||||
--env=COVERALLS_REPO_TOKEN \
|
||||
--env=PX4_ASAN \
|
||||
--env=PX4_MSAN \
|
||||
--env=PX4_TSAN \
|
||||
--env=PX4_UBSAN \
|
||||
--env=TRAVIS_BRANCH \
|
||||
--env=TRAVIS_BUILD_ID \
|
||||
--publish 14556:14556/udp \
|
||||
--volume=${CCACHE_DIR}:${CCACHE_DIR}:rw \
|
||||
--volume=${SRC_DIR}:${SRC_DIR}:rw \
|
||||
${PX4_DOCKER_REPO} /bin/bash -c "$1 $2 $3"
|
||||
${PX4_DOCKER_REPO} /bin/bash -c "$@"
|
||||
|
||||
@@ -234,14 +234,14 @@ def get_actuator_output(yaml_config, output_functions, timer_config_file, verbos
|
||||
param_prefix = process_param_prefix(group['param_prefix'])
|
||||
standard_params = group.get('standard_params', {})
|
||||
standard_params_array = [
|
||||
( 'function', 'Function', 'FUNC', False ),
|
||||
( 'disarmed', 'Disarmed', 'DIS', False ),
|
||||
( 'min', 'Minimum', 'MIN', False ),
|
||||
( 'max', 'Maximum', 'MAX', False ),
|
||||
( 'center', 'Center\n(for Servos)', 'CENT', False ),
|
||||
( 'failsafe', 'Failsafe', 'FAIL', True ),
|
||||
( 'function', 'Function', 'FUNC', False, True ),
|
||||
( 'disarmed', 'Disarmed', 'DIS', False, True ),
|
||||
( 'min', 'Minimum', 'MIN', False, True ),
|
||||
( 'center', 'Center\n(for Servos)', 'CENT', False, False ),
|
||||
( 'max', 'Maximum', 'MAX', False, True ),
|
||||
( 'failsafe', 'Failsafe', 'FAIL', True, True ),
|
||||
]
|
||||
for key, label, param_suffix, advanced in standard_params_array:
|
||||
for key, label, param_suffix, advanced, has_function in standard_params_array:
|
||||
show_if = None
|
||||
if key in standard_params and 'show_if' in standard_params[key]:
|
||||
show_if = standard_params[key]['show_if']
|
||||
@@ -250,13 +250,12 @@ def get_actuator_output(yaml_config, output_functions, timer_config_file, verbos
|
||||
param = {
|
||||
'label': label,
|
||||
'name': param_prefix+'_'+param_suffix+'${i}',
|
||||
'function': key,
|
||||
}
|
||||
if has_function: param['function'] = key
|
||||
if advanced: param['advanced'] = True
|
||||
if show_if: param['show-if'] = show_if
|
||||
per_channel_params.append(param)
|
||||
|
||||
|
||||
param = {
|
||||
'label': 'Rev Range\n(for Servos)',
|
||||
'name': param_prefix+'_REV',
|
||||
|
||||
@@ -312,11 +312,7 @@ When set to -1 (default), the value depends on the function (see {:}).
|
||||
if standard_params[key]['max'] >= 1<<16:
|
||||
raise Exception('maximum value for {:} expected <= {:} (got {:})'.format(key, 1<<16, standard_params[key]['max']))
|
||||
|
||||
if key == 'failsafe':
|
||||
standard_params[key]['default'] = -1
|
||||
standard_params[key]['min'] = -1
|
||||
|
||||
if key == 'center':
|
||||
if key == 'failsafe' or key == 'center':
|
||||
standard_params[key]['default'] = -1
|
||||
standard_params[key]['min'] = -1
|
||||
|
||||
|
||||
+831
-68
@@ -8,6 +8,803 @@ Also generates docs/en/middleware/dds_topics.md from dds_topics.yaml
|
||||
import os
|
||||
import argparse
|
||||
import sys
|
||||
import re
|
||||
|
||||
VALID_FIELDS = { #Note, also have to add the message types as those can be fields
|
||||
'uint64',
|
||||
'uint16',
|
||||
'uint8',
|
||||
'uint32'
|
||||
}
|
||||
|
||||
ALLOWED_UNITS = set(["m", "m/s", "m/s^2", "(m/s)^2", "deg", "deg/s", "rad", "rad/s", "rad^2", "rpm" ,"V", "A", "mA", "mAh", "W", "Wh", "dBm", "h", "minutes", "s", "ms", "us", "Ohm", "MB", "Kb/s", "degC","Pa", "%", "norm", "-"])
|
||||
invalid_units = set()
|
||||
ALLOWED_FRAMES = set(["NED", "Body", "FRD", "ENU"])
|
||||
ALLOWED_INVALID_VALUES = set(["NaN", "0", "-1"])
|
||||
ALLOWED_CONSTANTS_NOT_IN_ENUM = set(["ORB_QUEUE_LENGTH","MESSAGE_VERSION"])
|
||||
|
||||
class Error:
|
||||
def __init__(self, type, message, linenumber=None, issueString = None, field = None):
|
||||
self.type = type
|
||||
self.message = message
|
||||
self.linenumber = linenumber
|
||||
self.issueString = issueString
|
||||
self.field = field
|
||||
|
||||
def display_error(self):
|
||||
#print(f"Debug: Error: display_error")
|
||||
|
||||
|
||||
if 'trailing_whitespace' == self.type:
|
||||
if self.issueString.strip():
|
||||
print(f"NOTE: Line has trailing whitespace ({self.message}: {self.linenumber}): {self.issueString}")
|
||||
else:
|
||||
print(f"NOTE: Line has trailing whitespace ({self.message}: {self.linenumber})")
|
||||
elif 'leading_whitespace_field_or_constant' == self.type:
|
||||
print(f"NOTE: Whitespace before field or constant ({self.message}: {self.linenumber}): {self.issueString}")
|
||||
elif 'field_or_constant_has_multiple_whitepsace' == self.type:
|
||||
print(f"NOTE: Field/constant has more than one sequential whitespace character ({self.message}: {self.linenumber}): {self.issueString}")
|
||||
elif 'empty_start_line' == self.type:
|
||||
print(f"NOTE: Empty line at start of file ({self.message}: {self.linenumber})")
|
||||
elif 'internal_comment' == self.type:
|
||||
print(f"NOTE: Internal Comment ({self.message}: {self.linenumber})\n {self.issueString}")
|
||||
elif 'internal_comment_empty' == self.type:
|
||||
print(f"NOTE: Empty Internal Comment ({self.message}: {self.linenumber})")
|
||||
elif 'summary_missing' == self.type:
|
||||
print(f"WARNING: No message description ({self.message})")
|
||||
elif 'topic_error' == self.type:
|
||||
print(f"NOTE: TOPIC ISSUE: {self.issueString}")
|
||||
elif 'unknown_unit' == self.type:
|
||||
print(f"WARNING: Unknown Unit: [{self.issueString}] on `{self.field}` ({self.message}: {self.linenumber})")
|
||||
elif 'constant_not_in_assigned_enum' == self.type:
|
||||
print(f"WARNING: `{self.issueString}` constant: Prefix not in `@enum` field metadata ({self.message}: {self.linenumber})")
|
||||
elif 'unknown_invalid_value' == self.type:
|
||||
print(f"WARNING: Unknown @invalid value: [{self.issueString}] on `{self.field}` ({self.message}: {self.linenumber})")
|
||||
elif 'unknown_frame' == self.type:
|
||||
print(f"WARNING: Unknown @frame: [{self.issueString}] on `{self.field}` ({self.message}: {self.linenumber})")
|
||||
elif 'command_no_params_pipes' == self.type:
|
||||
print(f"WARNING: `{self.field}` command has no parameters (pipes): [{self.issueString}] ({self.message}: {self.linenumber})")
|
||||
elif 'command_missing_params' == self.type:
|
||||
print(f"WARNING: `{self.field}` command missing params - should be 7 params surrounded by 8 pipes: [{self.issueString}] ({self.message}: {self.linenumber})")
|
||||
elif 'command_too_many_params' == self.type:
|
||||
print(f"WARNING: `{self.field}` command too many params (should be 7). Extras: [{self.issueString}] ({self.message}: {self.linenumber})")
|
||||
|
||||
|
||||
else:
|
||||
self.display_info()
|
||||
|
||||
def display_info(self):
|
||||
"""
|
||||
Display info about an error.
|
||||
Used as a fallback if error does not have specific printout in display_error()
|
||||
"""
|
||||
#print(f"Debug: Error: display_info")
|
||||
print(f" type: {self.type}, message: {self.message}, linenumber: {self.linenumber}, issueString: {self.issueString}, field: {self.field}")
|
||||
|
||||
class Enum:
|
||||
def __init__(self, name, parentMessage):
|
||||
self.name = name
|
||||
self.parent = parentMessage
|
||||
self.enumValues = dict()
|
||||
|
||||
def display_info(self):
|
||||
"""
|
||||
Display info about an enum
|
||||
"""
|
||||
print(f"Debug: Enum: display_info")
|
||||
print(f" name: {self.name}")
|
||||
for key, value in self.enumValues.items():
|
||||
value.display_info()
|
||||
|
||||
class ConstantValue:
|
||||
def __init__(self, name, type, value, comment, line_number):
|
||||
self.name = name.strip()
|
||||
self.type = type.strip()
|
||||
self.value = value.strip()
|
||||
self.comment = comment
|
||||
self.line_number = line_number
|
||||
|
||||
if not self.value:
|
||||
print(f"Debug WARNING: NO VALUE in ConstantValue: {self.name}") ## TODO make into ERROR
|
||||
exit()
|
||||
|
||||
# TODO if value or name are empty, error
|
||||
|
||||
def display_info(self):
|
||||
print(f"Debug: ConstantValue: display_info")
|
||||
print(f" name: {self.name}, type: {self.type}, value: {self.value}, comment: {self.comment}, line: {self.line_number}")
|
||||
|
||||
|
||||
class CommandParam:
|
||||
"""
|
||||
Represents an individual param in a command constant
|
||||
Encapsulates parsing of the param to extract units etc.
|
||||
"""
|
||||
|
||||
def __init__(self, num, paramText, line_number, parentCommand):
|
||||
self.paramNum = num
|
||||
self.paramText = paramText.strip()
|
||||
self.enum = None
|
||||
self.range = None
|
||||
#self.type = type
|
||||
self.units = []
|
||||
self.enums = []
|
||||
self.minValue = None
|
||||
self.maxValue = None
|
||||
self.invalidValue = None
|
||||
self.frameValue = None
|
||||
self.lineNumber = line_number
|
||||
self.parent = parentCommand
|
||||
self.parentMessage = self.parent.parent
|
||||
|
||||
match = None
|
||||
if self.paramText:
|
||||
match = re.match(r'^((?:\[[^\]]*\]\s*)+)(.*)$', paramText)
|
||||
self.description = paramText
|
||||
bracketed_part = None
|
||||
if match:
|
||||
bracketed_part = match.group(1).strip() # .strip() removes trailing whitespace from the bracketed part
|
||||
self.description = match.group(2).strip()
|
||||
if bracketed_part:
|
||||
# get units
|
||||
bracket_content_matches = re.findall(r'\[(.*?)\]', bracketed_part)
|
||||
#print(f"DEBUG: bracket_content_matches: {bracket_content_matches}")
|
||||
for item in bracket_content_matches:
|
||||
item = item.strip()
|
||||
if item.startswith('@'): # Not a unit:
|
||||
if item.startswith('@enum'):
|
||||
item = item.split(" ")
|
||||
enum = item[1].strip()
|
||||
if enum and enum not in self.enums:
|
||||
self.enums.append(enum)
|
||||
|
||||
# Create parent enum objects for any enums created in this step
|
||||
for enumName in self.enums:
|
||||
if not enumName in self.parentMessage.enums:
|
||||
self.parentMessage.enums[enumName]=Enum(enumName,self.parentMessage)
|
||||
|
||||
elif item.startswith('@range'):
|
||||
item = item[6:].strip().split(",")
|
||||
self.range = item
|
||||
self.minValue = item[0].strip()
|
||||
self.maxValue = item[1].strip()
|
||||
elif item.startswith('@invalid'):
|
||||
self.invalidValue = item[8:].strip()
|
||||
#TODO: Do we require a description? (not currently)
|
||||
if self.invalidValue.split(" ")[0] not in ALLOWED_INVALID_VALUES:
|
||||
print(f"TODO: Command param do not support @invalid: {self.invalidValue}")
|
||||
"""
|
||||
error = Error("unknown_invalid_value", self.parent.filename, self.lineNumber, self.invalidValue, self.name)
|
||||
#error.display_error()
|
||||
if not "unknown_invalid_value" in self.parent.errors:
|
||||
self.parent.errors["unknown_invalid_value"] = []
|
||||
self.parent.errors["unknown_invalid_value"].append(error)
|
||||
"""
|
||||
|
||||
elif item.startswith('@frame'):
|
||||
self.frameValue = item[6:].strip()
|
||||
print(f"TODO: Command param do not support @frame: {self.frameValue}")
|
||||
"""
|
||||
if self.frameValue not in ALLOWED_FRAMES:
|
||||
error = Error("unknown_frame", self.parent.filename, self.lineNumber, self.frameValue, self.name)
|
||||
#error.display_error()
|
||||
if not "unknown_frame" in self.parent.errors:
|
||||
self.parent.errors["unknown_frame"] = []
|
||||
self.parent.errors["unknown_frame"].append(error)
|
||||
"""
|
||||
else:
|
||||
print(f"WARNING: Unhandled metadata in message comment: {item}")
|
||||
# TODO - report errors for different kinds of metadata
|
||||
exit()
|
||||
|
||||
else: # bracket is a unit
|
||||
unit = item.strip()
|
||||
|
||||
if item == "-":
|
||||
unit = ""
|
||||
|
||||
if unit and unit not in self.units:
|
||||
self.units.append(unit)
|
||||
|
||||
if unit not in ALLOWED_UNITS:
|
||||
invalid_units.add(unit)
|
||||
error = Error("unknown_unit", self.parentMessage.filename, self.lineNumber, unit, self.parent.name)
|
||||
#error.display_error()
|
||||
if not "unknown_unit" in self.parentMessage.errors:
|
||||
self.parentMessage.errors["unknown_unit"] = []
|
||||
self.parentMessage.errors["unknown_unit"].append(error)
|
||||
|
||||
|
||||
def display_info(self):
|
||||
print(f"Debug: CommandParam: display_info")
|
||||
print(f" id: {self.paramNum}")
|
||||
print(f" paramText: {self.paramText}\n unit: {self.units}\n enums: {self.enums}\n lineNumber: {self.lineNumber}\n range: {self.range}\n minValue: {self.minValue}\n maxValue: {self.maxValue}\n invalidValue: {self.invalidValue}\n frameValue: {self.frameValue}\n parent: {self.parent}\n ")
|
||||
|
||||
|
||||
|
||||
class CommandConstant:
|
||||
"""
|
||||
Represents a constant that is a command definition.
|
||||
Encapsulates parsing of the command format.
|
||||
The individual params are further parsed in CommandParam
|
||||
"""
|
||||
def __init__(self, name, type, value, comment, line_number, parentMessage):
|
||||
self.name = name.strip()
|
||||
self.type = type.strip()
|
||||
self.value = value.strip()
|
||||
self.comment = comment
|
||||
self.line_number = line_number
|
||||
self.parent = parentMessage
|
||||
|
||||
self.description = self.comment
|
||||
self.param1 = None
|
||||
self.param2 = None
|
||||
self.param3 = None
|
||||
self.param4 = None
|
||||
self.param5 = None
|
||||
self.param6 = None
|
||||
self.param7 = None
|
||||
|
||||
if not self.value:
|
||||
print(f"Debug WARNING: NO VALUE in CommandConstant: {self.name}") ## TODO make into ERROR
|
||||
exit()
|
||||
|
||||
if not self.comment: # This is an bug for a command
|
||||
#print(f"Debug WARNING: NO COMMENT in CommandConstant: {self.name}") ## TODO make into ERROR
|
||||
return
|
||||
|
||||
# Parse command comment to get the description and parameters.
|
||||
# print(f"Debug CommandConstant: {self.comment}")
|
||||
if not "|" in self.comment:
|
||||
# This is an error for a command constant
|
||||
error = Error("command_no_params_pipes", self.parent.filename, self.line_number, self.comment, self.name)
|
||||
#error.display_error()
|
||||
if not "command_no_params_pipes" in self.parent.errors:
|
||||
self.parent.errors["command_no_params_pipes"] = []
|
||||
self.parent.errors["command_no_params_pipes"].append(error)
|
||||
return
|
||||
|
||||
# Split on pipes
|
||||
commandSplit = self.comment.split("|")
|
||||
if len(commandSplit) < 9:
|
||||
# Should 7 pipes, so each command is fully surrounded
|
||||
error = Error("command_missing_params", self.parent.filename, self.line_number, self.comment, self.name)
|
||||
#error.display_error()
|
||||
if not "command_missing_params" in self.parent.errors:
|
||||
self.parent.errors["command_missing_params"] = []
|
||||
self.parent.errors["command_missing_params"].append(error)
|
||||
|
||||
self.description = commandSplit[0].strip()
|
||||
self.description = self.description if self.description else None
|
||||
|
||||
params_to_update = commandSplit[1:8]
|
||||
|
||||
for i, value in enumerate(params_to_update, start=1):
|
||||
if value.strip():
|
||||
# parse the param
|
||||
param = CommandParam(i, value, self.line_number, self)
|
||||
#param.display_info() # DEBUG CODE XXX
|
||||
setattr(self, f"param{i}", param)
|
||||
# parse the param
|
||||
|
||||
if len(commandSplit) > 8:
|
||||
extras = commandSplit[8:]
|
||||
error = Error("command_too_many_params", self.parent.filename, self.line_number, extras, self.name)
|
||||
if not "command_too_many_params" in self.parent.errors:
|
||||
self.parent.errors["command_too_many_params"] = []
|
||||
self.parent.errors["command_too_many_params"].append(error)
|
||||
|
||||
|
||||
# TODO if value or name are empty, error
|
||||
|
||||
def markdown_out(self):
|
||||
#print("DEBUG: CommandConstant.markdown_out")
|
||||
output = f"""### {self.name} ({self.value})
|
||||
|
||||
{self.description}
|
||||
|
||||
Param | Units | Range/Enum | Description
|
||||
--- | --- | --- | ---
|
||||
"""
|
||||
for i in range(1, 8):
|
||||
attr_name = f"param{i}"
|
||||
# getattr returns None if the attribute doesn't exist
|
||||
val = getattr(self, attr_name, None)
|
||||
|
||||
if val is not None:
|
||||
rangeVal = ""
|
||||
if val.minValue or val.maxValue:
|
||||
rangeVal = f"[{val.minValue if val.minValue else '-'} : {val.maxValue if val.maxValue else '-' }]"
|
||||
|
||||
output+=f"{i} | {", ".join(val.units)}|{', '.join(f"[{e}](#{e})" for e in val.enums)}{rangeVal} | {val.description}\n"
|
||||
else:
|
||||
output+=f"{i} | | | ?\n"
|
||||
|
||||
output+=f"\n"
|
||||
return output
|
||||
|
||||
|
||||
def display_info(self):
|
||||
print(f"Debug: CommandConstant: display_info")
|
||||
print(f" name: {self.name}, type: {self.type}, value: {self.value}, comment: {self.comment}, line: {self.line_number}")
|
||||
print(f" description: {self.description}\n param1: {self.param1}\n param2: {self.param2}\n param3: {self.param3}\n param4: {self.param4}\n param5: {self.param5}\n param6: {self.param6}\n param7: {self.param7}")
|
||||
|
||||
class MessageField:
|
||||
"""
|
||||
Represents a field.
|
||||
Encapsulates parsing of the field information.
|
||||
"""
|
||||
def __init__(self, name, type, comment, line_number, parentMessage):
|
||||
self.name = name
|
||||
self.type = type
|
||||
self.comment = comment
|
||||
self.unit = None
|
||||
self.enums = None
|
||||
self.minValue = None
|
||||
self.maxValue = None
|
||||
self.invalidValue = None
|
||||
self.frameValue = None
|
||||
self.lineNumber = line_number
|
||||
self.parent = parentMessage
|
||||
|
||||
#print(f"MessageComment: {comment}")
|
||||
match = None
|
||||
if self.comment:
|
||||
match = re.match(r'^((?:\[[^\]]*\]\s*)+)(.*)$', comment)
|
||||
self.description = comment
|
||||
bracketed_part = None
|
||||
if match:
|
||||
bracketed_part = match.group(1).strip() # .strip() removes trailing whitespace from the bracketed part
|
||||
self.description = match.group(2).strip()
|
||||
if bracketed_part:
|
||||
# get units
|
||||
bracket_content_matches = re.findall(r'\[(.*?)\]', bracketed_part)
|
||||
#print(f"bracket_content_matches: {bracket_content_matches}")
|
||||
for item in bracket_content_matches:
|
||||
item = item.strip()
|
||||
if item.startswith('@'): # Not a unit:
|
||||
if item.startswith('@enum'):
|
||||
item = item.split(" ")
|
||||
self.enums = item[1:]
|
||||
# Create parent enum objects
|
||||
for enumName in self.enums:
|
||||
if not enumName in parentMessage.enums:
|
||||
parentMessage.enums[enumName]=Enum(enumName,parentMessage)
|
||||
elif item.startswith('@range'):
|
||||
item = item[6:].strip().split(",")
|
||||
self.minValue = item[0].strip()
|
||||
self.maxValue = item[1].strip()
|
||||
elif item.startswith('@invalid'):
|
||||
self.invalidValue = item[8:].strip()
|
||||
#TODO: Do we require a description? (not currently)
|
||||
if self.invalidValue.split(" ")[0] not in ALLOWED_INVALID_VALUES:
|
||||
error = Error("unknown_invalid_value", self.parent.filename, self.lineNumber, self.invalidValue, self.name)
|
||||
#error.display_error()
|
||||
if not "unknown_invalid_value" in self.parent.errors:
|
||||
self.parent.errors["unknown_invalid_value"] = []
|
||||
self.parent.errors["unknown_invalid_value"].append(error)
|
||||
elif item.startswith('@frame'):
|
||||
self.frameValue = item[6:].strip()
|
||||
if self.frameValue not in ALLOWED_FRAMES:
|
||||
error = Error("unknown_frame", self.parent.filename, self.lineNumber, self.frameValue, self.name)
|
||||
#error.display_error()
|
||||
if not "unknown_frame" in self.parent.errors:
|
||||
self.parent.errors["unknown_frame"] = []
|
||||
self.parent.errors["unknown_frame"].append(error)
|
||||
else:
|
||||
print(f"WARNING: Unhandled metadata in message comment: {item}")
|
||||
# TODO - report errors for different kinds of metadata
|
||||
exit()
|
||||
|
||||
else: # bracket is a unit
|
||||
self.unit = item
|
||||
|
||||
if self.unit not in ALLOWED_UNITS:
|
||||
invalid_units.add(self.unit)
|
||||
error = Error("unknown_unit", self.parent.filename, self.lineNumber, self.unit, self.name)
|
||||
#error.display_error()
|
||||
if not "unknown_unit" in self.parent.errors:
|
||||
self.parent.errors["unknown_unit"] = []
|
||||
self.parent.errors["unknown_unit"].append(error)
|
||||
|
||||
if item == "-":
|
||||
self.unit = ""
|
||||
|
||||
|
||||
def display_info(self):
|
||||
print(f"Debug: MessageField: display_info")
|
||||
print(f" name: {self.name}, type: {self.type}, description: {self.description}, enums: {self.enums}, minValue: {self.minValue}, maxValue: {self.maxValue}, invalidValue: {self.invalidValue}, frameValue: {self.frameValue}")
|
||||
|
||||
|
||||
class UORBMessage:
|
||||
"""
|
||||
Represents a whole message, including fields, enums, commands, constants.
|
||||
The parser function delegates the parsing of each part of the message to
|
||||
more appropriate classes, once the specific type of line has been identified.
|
||||
"""
|
||||
|
||||
def __init__(self, filename):
|
||||
|
||||
self.filename = filename
|
||||
msg_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"../../msg")
|
||||
self.msg_filename = os.path.join(msg_path, self.filename)
|
||||
self.name = os.path.splitext(os.path.basename(msg_file))[0]
|
||||
self.shortDescription = ""
|
||||
self.longDescription = ""
|
||||
self.fields = []
|
||||
self.constantFields = dict()
|
||||
self.commandConstants = dict()
|
||||
self.enums = dict()
|
||||
self.output_file = os.path.join(output_dir, f"{self.name}.md")
|
||||
self.topics = []
|
||||
self.errors = dict()
|
||||
|
||||
self.parseFile()
|
||||
|
||||
if args.errors:
|
||||
#print(f"DEBUG: args.errors: {args.errors}")
|
||||
if args.error_messages:
|
||||
messages = args.error_messages.split(" ")
|
||||
#print(f"DEBUG: args.errors: {messages},self.name: {self.name}")
|
||||
if self.name in messages:
|
||||
self.reportErrors()
|
||||
#print(f"Debug: {self.name} in {messages}")
|
||||
else:
|
||||
self.reportErrors()
|
||||
|
||||
def reportErrors(self):
|
||||
#print(f"Debug: UORBMessage: reportErrors()")
|
||||
for errorType, errors in self.errors.items():
|
||||
for error in errors:
|
||||
error.display_error()
|
||||
|
||||
def markdown_out(self):
|
||||
#print(f"Debug: UORBMessage: markdown_out()")
|
||||
|
||||
# Add page header (forces wide pages)
|
||||
markdown = f"""---
|
||||
pageClass: is-wide-page
|
||||
---
|
||||
|
||||
# {self.name} (UORB message)
|
||||
|
||||
"""
|
||||
## Append description info if present
|
||||
markdown += f"{self.shortDescription}\n\n" if self.shortDescription else ""
|
||||
markdown += f"{self.longDescription}\n\n" if self.longDescription else ""
|
||||
|
||||
topicList = " ".join(self.topics)
|
||||
markdown += f"**TOPICS:** {topicList}\n\n"
|
||||
|
||||
# Generate field docs
|
||||
markdown += f"## Fields\n\n"
|
||||
markdown += "Name | Type | Unit [Frame] | Range/Enum | Description\n"
|
||||
markdown += "--- | --- | --- | --- | ---\n"
|
||||
for field in self.fields:
|
||||
unit = f"{field.unit}" if field.unit else ""
|
||||
frame = f"[{field.frameValue}]" if field.frameValue else ""
|
||||
unit = f"{unit} {frame}"
|
||||
unit.strip()
|
||||
unit = f" {unit}"
|
||||
|
||||
value = " "
|
||||
if field.enums:
|
||||
value = ""
|
||||
for enum in field.enums:
|
||||
value += f"[{enum}](#{enum})"
|
||||
value = value.strip()
|
||||
value = f"{value}"
|
||||
elif field.minValue or field.maxValue:
|
||||
value = f"[{field.minValue if field.minValue else '-'} : {field.maxValue if field.maxValue else '-' }]"
|
||||
|
||||
description = f" {field.description}" if field.description else ""
|
||||
invalid = f" (Invalid: {field.invalidValue}) " if field.invalidValue else ""
|
||||
markdown += f"{field.name} | `{field.type}` |{unit}|{value}|{description}{invalid}\n"
|
||||
|
||||
# Generate table for command docs
|
||||
if len(self.commandConstants) > 0:
|
||||
#print("DEBUGCOMMAND")
|
||||
markdown += f"\n## Commands\n\n"
|
||||
|
||||
"""
|
||||
markdown += "Name | Type | Value | Description\n"
|
||||
markdown += "--- | --- | --- |---\n"
|
||||
for name, command in self.commandConstants.items():
|
||||
description = f" {command.comment} " if enum.comment else " "
|
||||
markdown += f'<a id="#{name}"></a> {name} | `{command.type}` | {command.value} |{description}\n'
|
||||
"""
|
||||
for commandConstant in self.commandConstants.values():
|
||||
#print(commandConstant)
|
||||
markdown += commandConstant.markdown_out()
|
||||
|
||||
# Generate enum docs
|
||||
if len(self.enums) > 0:
|
||||
markdown += f"\n## Enums\n"
|
||||
|
||||
for name, enum in self.enums.items():
|
||||
markdown += f"\n### {name} {{#{name}}}\n\n"
|
||||
|
||||
markdown += "Name | Type | Value | Description\n"
|
||||
markdown += "--- | --- | --- | ---\n"
|
||||
|
||||
for enumValueName, enumValue in enum.enumValues.items():
|
||||
description = f" {enumValue.comment} " if enumValue.comment else " "
|
||||
markdown += f'<a id="#{enumValueName}"></a> {enumValueName} | `{enumValue.type}` | {enumValue.value} |{description}\n'
|
||||
|
||||
# Generate table for constants docs
|
||||
if len(self.constantFields) > 0:
|
||||
markdown += f"\n## Constants\n\n"
|
||||
markdown += "Name | Type | Value | Description\n"
|
||||
markdown += "--- | --- | --- |---\n"
|
||||
for name, enum in self.constantFields.items():
|
||||
description = f" {enum.comment} " if enum.comment else " "
|
||||
markdown += f'<a id="#{name}"></a> {name} | `{enum.type}` | {enum.value} |{description}\n'
|
||||
|
||||
|
||||
|
||||
# Append msg contents to the end
|
||||
with open(self.msg_filename, 'r') as source_file:
|
||||
msg_contents = source_file.read()
|
||||
msg_contents = msg_contents.strip()
|
||||
|
||||
#Format markdown using msg name, comment, url, contents.
|
||||
markdown += f"""
|
||||
|
||||
## Source Message
|
||||
|
||||
[Source file (GitHub)](https://github.com/PX4/PX4-Autopilot/blob/main/msg/{self.filename})
|
||||
|
||||
::: details Click here to see original file
|
||||
|
||||
```c
|
||||
{msg_contents}
|
||||
```
|
||||
|
||||
:::
|
||||
"""
|
||||
|
||||
with open(self.output_file, 'w') as content_file:
|
||||
content_file.write(markdown)
|
||||
|
||||
#exit()
|
||||
|
||||
|
||||
def display_info(self):
|
||||
print(f"UORBMessage: display_info")
|
||||
print(f" name: {self.name}")
|
||||
print(f" filename: {self.filename}, ")
|
||||
print(f" msg_filename: {self.msg_filename}, ")
|
||||
print(f"self.shortDescription: {self.shortDescription}")
|
||||
print(f"self.longDescription: {self.longDescription}")
|
||||
print(f"self.enums: {self.enums}")
|
||||
|
||||
for enum, enumObject in self.enums.items():
|
||||
enumObject.display_info()
|
||||
|
||||
# Output our data so far
|
||||
for field in self.fields:
|
||||
field.display_info()
|
||||
|
||||
for enumvalue in self.constantFields:
|
||||
print(enumvalue)
|
||||
self.constantFields[enumvalue].display_info()
|
||||
|
||||
def handleField(self, line, line_number, parentMessage):
|
||||
#print(f"debug: handleField: (line): \n {line}")
|
||||
# Note, here we know we don't have a comment or a topic.
|
||||
# We expect it to be a field.
|
||||
|
||||
# Check field doesn't have leading whitespace (trailing spaces already checked)
|
||||
if line[:1].isspace(): # Returns True for ' ', '\t', '\n', '\r', etc.
|
||||
#print("First character is whitespace")
|
||||
error = Error("leading_whitespace_field_or_constant", self.filename, line_number, line)
|
||||
if not "leading_whitespace_field_or_constant" in self.errors:
|
||||
self.errors["leading_whitespace_field_or_constant"] = []
|
||||
self.errors["leading_whitespace_field_or_constant"].append(error)
|
||||
|
||||
# Now we can parse the stripped line
|
||||
fieldOrConstant = line.strip()
|
||||
|
||||
# Check that the field or constant has only single whitespace separators
|
||||
stripped_fieldOrConstant = re.sub(r'\s+', ' ', fieldOrConstant) # Collapse all spaces to a single space (LHS already stripped).
|
||||
if stripped_fieldOrConstant != fieldOrConstant:
|
||||
#print("Field/Constant has multiple whitespace characters") # Since the collapsed version shows them.
|
||||
error = Error("field_or_constant_has_multiple_whitepsace", self.filename, line_number, line)
|
||||
if not "field_or_constant_has_multiple_whitepsace" in self.errors:
|
||||
self.errors["field_or_constant_has_multiple_whitepsace"] = []
|
||||
self.errors["field_or_constant_has_multiple_whitepsace"].append(error)
|
||||
|
||||
fieldOrConstant = stripped_fieldOrConstant
|
||||
|
||||
|
||||
|
||||
comment = None
|
||||
if "#" in line:
|
||||
commentExtract = line.split("#", 1) # Split once on left-most '#'
|
||||
fieldOrConstant = commentExtract[0].strip()
|
||||
comment = commentExtract[-1].strip()
|
||||
|
||||
if "=" not in fieldOrConstant:
|
||||
# Is a field:
|
||||
field = fieldOrConstant.split(" ")
|
||||
type = field[0].strip()
|
||||
name = field[1].strip()
|
||||
field = MessageField(name, type, comment, line_number, parentMessage)
|
||||
self.fields.append(field)
|
||||
else:
|
||||
temp = fieldOrConstant.split("=")
|
||||
value = temp[-1]
|
||||
typeAndName = temp[0].split(" ")
|
||||
type = typeAndName[0].strip()
|
||||
name = typeAndName[1].strip()
|
||||
if name.startswith("VEHICLE_CMD_") and parentMessage.name == 'VehicleCommand': #it's a command.
|
||||
#print(f"DEBUG: startswith VEHICLE_CMD_ {name}")
|
||||
commandConstant = CommandConstant(name, type, value, comment, line_number, parentMessage)
|
||||
#commandConstant.display_info()
|
||||
self.commandConstants[name]=commandConstant
|
||||
else: #it's a constant (or part of an enum)
|
||||
constantField = ConstantValue(name, type, value, comment, line_number)
|
||||
self.constantFields[name]=constantField
|
||||
|
||||
|
||||
def parseFile(self):
|
||||
initial_block_lines = []
|
||||
#stopping_token = None
|
||||
found_first_relevant_content = False
|
||||
gettingInitialComments = False
|
||||
gettingFields = False
|
||||
|
||||
with open(self.msg_filename, 'r', encoding='utf-8') as uorbfile:
|
||||
lines = uorbfile.read().splitlines()
|
||||
for line_number, line in enumerate(lines, 1):
|
||||
|
||||
if line != line.rstrip():
|
||||
#print(f"[{self.filename}] Trailing whitespace on line {line_number}: XX{line}YY")
|
||||
error = Error("trailing_whitespace", self.filename, line_number, line)
|
||||
if not "trailing_whitespace" in self.errors:
|
||||
self.errors["trailing_whitespace"] = []
|
||||
self.errors["trailing_whitespace"].append(error)
|
||||
|
||||
#print(f"line: {line}")
|
||||
stripped_line = re.sub(r'\s+', ' ', line).strip() # Collapse all spaces to a single space and strip stuff off end.
|
||||
#print(f"stripped_line: {stripped_line}")
|
||||
# TODO? Perhaps report whitespace if the size of those two is different and it is empty
|
||||
# Or perhaps we just fix it on request
|
||||
|
||||
isEmptyLine = False if line.strip() else True
|
||||
if not found_first_relevant_content and isEmptyLine: #Empty line
|
||||
#print(f"{self.filename}: Empty line at start of file: [{line_number}]\n {line}")
|
||||
error = Error("empty_start_line", self.filename, line_number, line)
|
||||
if not "empty_start_line" in self.errors:
|
||||
self.errors["empty_start_line"] = []
|
||||
self.errors["empty_start_line"].append(error)
|
||||
#error.display_error()
|
||||
continue
|
||||
if not found_first_relevant_content and not isEmptyLine:
|
||||
found_first_relevant_content = True
|
||||
|
||||
if stripped_line.startswith("#"):
|
||||
gettingInitialComments = True
|
||||
else:
|
||||
gettingInitialComments = False
|
||||
gettingFields = True
|
||||
|
||||
if gettingInitialComments and stripped_line.startswith("#"):
|
||||
stripped_line=stripped_line[1:].strip()
|
||||
#print(f"DEBUG: gettingInitialComments: comment line: {stripped_line}")
|
||||
initial_block_lines.append(stripped_line)
|
||||
else:
|
||||
gettingInitialComments = False
|
||||
gettingFields = True #Getting fields and constants
|
||||
if gettingFields:
|
||||
if isEmptyLine:
|
||||
continue # empty line
|
||||
if stripped_line.startswith("# TOPICS "):
|
||||
stripped_line = stripped_line[9:]
|
||||
stripped_line = stripped_line.split(" ")
|
||||
self.topics+= stripped_line
|
||||
# Note, default topic and topic errors handled after all lines parsed
|
||||
continue
|
||||
if stripped_line.startswith("#"):
|
||||
# Its an internal comment
|
||||
stripped_line=stripped_line[1:].strip()
|
||||
|
||||
if stripped_line:
|
||||
#print(f"{self.filename}: Internal comment: [{line_number}]\n {line}")
|
||||
error = Error("internal_comment", self.filename, line_number, line)
|
||||
if not "internal_comment" in self.errors:
|
||||
self.errors["internal_comment"] = []
|
||||
self.errors["internal_comment"].append(error)
|
||||
else:
|
||||
#print(f"{self.filename}: Empty internal comment: [{line_number}]\n {line}")
|
||||
error = Error("internal_comment_empty", self.filename, line_number, line)
|
||||
if not "internal_comment_empty" in self.errors:
|
||||
self.errors["internal_comment_empty"] = []
|
||||
self.errors["internal_comment_empty"].append(error)
|
||||
#pass # Empty comment
|
||||
continue
|
||||
|
||||
# Must be a field or a comment.
|
||||
self.handleField(line, line_number, parentMessage=self)
|
||||
|
||||
# Fix up topics if the topic is empty
|
||||
def camel_to_snake(name):
|
||||
# Insert underscore between lowercase/digit and uppercase letter
|
||||
s1 = re.sub('([a-z0-9])([A-Z])', r'\1_\2', name)
|
||||
# Insert underscore between consecutive uppercase and uppercase+lowercase
|
||||
return re.sub('([A-Z]+)([A-Z][a-z])', r'\1_\2', s1).lower()
|
||||
|
||||
defaultTopic = camel_to_snake(self.name)
|
||||
if len(self.topics) == 0:
|
||||
# We have no topic declared, so set the default topic
|
||||
self.topics.append(defaultTopic)
|
||||
elif len(self.topics) == 1:
|
||||
# We have 1 topic declared - either it is default or there is some issue.
|
||||
if defaultTopic in self.topics:
|
||||
# Declared topic is default topic
|
||||
error = Error("topic_error", self.filename, "", f"WARNING: TOPIC {defaultTopic} unnecessarily declared for {self.name}")
|
||||
else:
|
||||
# Declared topic is not default topic
|
||||
error = Error("topic_error", self.filename, "", f"NOTE: TOPIC {self.topics[0]}: Only Declared topic is not default topic {defaultTopic} for {self.name}")
|
||||
if not "topic_error" in self.errors:
|
||||
self.errors["topic_error"] = []
|
||||
self.errors["topic_error"].append(error)
|
||||
elif len(self.topics) > 1:
|
||||
if defaultTopic not in self.topics:
|
||||
error = Error("topic_error", self.filename, "", f"NOTE: TOPIC - Default topic {defaultTopic} for {self.name} not in {self.topics}")
|
||||
|
||||
# Parse our short and long description
|
||||
#print(f"DEBUG: initial_block_lines: {initial_block_lines}")
|
||||
doingLongDescription = False
|
||||
for summaryline in initial_block_lines:
|
||||
if not self.shortDescription and summaryline.strip() == '':
|
||||
continue
|
||||
if not doingLongDescription and not summaryline.strip() == '':
|
||||
self.shortDescription += f" {summaryline}"
|
||||
self.shortDescription = self.shortDescription.strip()
|
||||
if not self.shortDescription[-1:] == ".": # Add terminating fullstop if not present.
|
||||
self.shortDescription += "."
|
||||
if not doingLongDescription and summaryline.strip() == '':
|
||||
doingLongDescription = True
|
||||
continue
|
||||
if doingLongDescription:
|
||||
self.longDescription += f"{summaryline}\n"
|
||||
|
||||
if self.longDescription:
|
||||
self.longDescription.strip()
|
||||
|
||||
if not self.shortDescription:
|
||||
# Summary has not been defined
|
||||
error = Error("summary_missing", self.filename)
|
||||
if not "summary_missing" in self.errors:
|
||||
self.errors["summary_missing"] = []
|
||||
self.errors["summary_missing"].append(error)
|
||||
|
||||
|
||||
# TODO Parse our constantValues into enums, leaving only constants
|
||||
constantValuesToRemove = []
|
||||
#print(f"DEBUG: Self.enums: {self.enums}")
|
||||
for enumName, enumObject in self.enums.items():
|
||||
for enumValueName, enumValueObject in self.constantFields.items():
|
||||
if enumValueName.startswith(enumName):
|
||||
# Copy this value into the object (cant be duplicate because parent is dict)
|
||||
enumObject.enumValues[enumValueName]=enumValueObject
|
||||
constantValuesToRemove.append(enumValueName)
|
||||
# Now delete the original enumvalues
|
||||
for enumValName in constantValuesToRemove:
|
||||
del self.constantFields[enumValName]
|
||||
constantsNotAssignedToEnums = len(self.constantFields)
|
||||
if constantsNotAssignedToEnums > 0:
|
||||
#print(f"Debug: WARNING constantsNotAssignedToEnums: {constantsNotAssignedToEnums}")
|
||||
for enumValueName, enumValue in self.constantFields.items():
|
||||
if enumValueName in ALLOWED_CONSTANTS_NOT_IN_ENUM: # Ignore constants
|
||||
pass
|
||||
else:
|
||||
error = Error("constant_not_in_assigned_enum", self.filename, enumValue.line_number, enumValueName)
|
||||
if not "constant_not_in_assigned_enum" in self.errors:
|
||||
self.errors["constant_not_in_assigned_enum"] = []
|
||||
self.errors["constant_not_in_assigned_enum"].append(error)
|
||||
# TODO Maybe present as list of possible enums.
|
||||
|
||||
|
||||
import yaml
|
||||
@@ -36,11 +833,9 @@ def generate_dds_yaml_doc(allMessageFiles, output_file = 'dds_topics.md'):
|
||||
for message in data["subscriptions"]:
|
||||
all_message_types.add(message['type'].split("::")[-1])
|
||||
all_topics.add(message['topic'].split('/')[-1])
|
||||
if data["subscriptions_multi"]: # There is none now
|
||||
dds_markdown += "None\n"
|
||||
for message in data["subscriptions_multi"]:
|
||||
all_message_types.add(message['type'].split("::")[-1])
|
||||
all_topics.add(message['topic'].split('/')[-1])
|
||||
for message in (data.get("subscriptions_multi") or []):
|
||||
all_message_types.add(message['type'].split("::")[-1])
|
||||
all_topics.add(message['topic'].split('/')[-1])
|
||||
for message in allMessageFiles:
|
||||
all_messages_in_source.add(message.split('/')[-1].split('.')[0])
|
||||
messagesNotExported = all_messages_in_source - all_message_types
|
||||
@@ -77,13 +872,17 @@ Topic | Type| Rate Limit
|
||||
|
||||
dds_markdown += "\n## Subscriptions Multi\n\n"
|
||||
|
||||
if not data["subscriptions_multi"]: # There is none now
|
||||
subscriptions_multi = data.get("subscriptions_multi") or []
|
||||
if not subscriptions_multi:
|
||||
dds_markdown += "None\n"
|
||||
else:
|
||||
print("Warning - we now have subscription_multi data - check format")
|
||||
dds_markdown += "Topic | Type\n--- | ---\n"
|
||||
for message in data["subscriptions_multi"]:
|
||||
dds_markdown += f"{message['topic']} | {message['type']}\n"
|
||||
dds_markdown += "Topic | Type | Route Field | Max Instances\n--- | --- | --- | ---\n"
|
||||
for message in subscriptions_multi:
|
||||
type = message['type']
|
||||
px4Type = type.split("::")[-1]
|
||||
route_field = f"`{message['route_field']}`" if 'route_field' in message else "-"
|
||||
max_instances = message.get('max_instances', '-')
|
||||
dds_markdown += f"{message['topic']} | [{type}](../msg_docs/{px4Type}.md) | {route_field} | {max_instances}\n"
|
||||
|
||||
if messagesNotExported:
|
||||
# Print the topics that are not exported to DDS
|
||||
@@ -127,83 +926,47 @@ if __name__ == "__main__":
|
||||
|
||||
parser = argparse.ArgumentParser(description='Generate docs from .msg files')
|
||||
parser.add_argument('-d', dest='dir', help='output directory', required=True)
|
||||
parser.add_argument('-e', dest='errors', action='store_true', help='Report errors')
|
||||
parser.add_argument('-m', dest='error_messages', help='Message to report errors against (by default all)')
|
||||
args = parser.parse_args()
|
||||
|
||||
output_dir = args.dir
|
||||
if not os.path.isdir(output_dir):
|
||||
print(f"making output_dir {output_dir}")
|
||||
os.mkdir(output_dir)
|
||||
|
||||
msg_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"../../msg")
|
||||
msg_files = get_msgs_list(msg_path)
|
||||
|
||||
msg_files.sort()
|
||||
|
||||
versioned_msgs_list = ''
|
||||
unversioned_msgs_list = ''
|
||||
msgTypes = set()
|
||||
|
||||
for msg_file in msg_files:
|
||||
# Add messages to set of allowed types (compound types)
|
||||
msg_name = os.path.splitext(os.path.basename(msg_file))[0]
|
||||
output_file = os.path.join(output_dir, msg_name+'.md')
|
||||
msg_filename = os.path.join(msg_path, msg_file)
|
||||
print("{:} -> {:}".format(msg_filename, output_file))
|
||||
msgTypes.add(msg_name)
|
||||
|
||||
#Format msg url
|
||||
msg_url="[source file](https://github.com/PX4/PX4-Autopilot/blob/main/msg/%s)" % msg_file
|
||||
|
||||
msg_description = ""
|
||||
summary_description = ""
|
||||
|
||||
#Get msg description (first non-empty comment line from top of msg)
|
||||
with open(msg_filename, 'r') as lineparser:
|
||||
line = lineparser.readline()
|
||||
while line.startswith('#') or (line.strip() == ''):
|
||||
print('DEBUG: line: %s' % line)
|
||||
line=line[1:].strip()+'\n'
|
||||
stripped_line=line.strip()
|
||||
if msg_description and not summary_description and stripped_line=='':
|
||||
summary_description = msg_description.strip()
|
||||
|
||||
msg_description+=line
|
||||
line = lineparser.readline()
|
||||
msg_description=msg_description.strip()
|
||||
if not summary_description and msg_description:
|
||||
summary_description = msg_description
|
||||
print('msg_description: Z%sZ' % msg_description)
|
||||
print('summary_description: Z%sZ' % summary_description)
|
||||
summary_description
|
||||
msg_contents = ""
|
||||
#Get msg contents (read the file)
|
||||
with open(msg_filename, 'r') as source_file:
|
||||
msg_contents = source_file.read()
|
||||
|
||||
#Format markdown using msg name, comment, url, contents.
|
||||
markdown_output="""# %s (UORB message)
|
||||
|
||||
%s
|
||||
|
||||
%s
|
||||
|
||||
```c
|
||||
%s
|
||||
```
|
||||
""" % (msg_name, msg_description, msg_url, msg_contents)
|
||||
|
||||
with open(output_file, 'w') as content_file:
|
||||
content_file.write(markdown_output)
|
||||
for msg_file in msg_files:
|
||||
message = UORBMessage(msg_file)
|
||||
# Any additional tests that can't be in UORBMessage parser go here.
|
||||
message.markdown_out()
|
||||
|
||||
# Categorize as versioned or unversioned
|
||||
if "versioned" in msg_file:
|
||||
versioned_msgs_list += '- [%s](%s.md)' % (msg_name, msg_name)
|
||||
if summary_description:
|
||||
versioned_msgs_list += " — %s" % summary_description
|
||||
versioned_msgs_list += f"- [{message.name}]({message.name}.md)"
|
||||
if message.shortDescription:
|
||||
versioned_msgs_list += f" — {message.shortDescription}"
|
||||
versioned_msgs_list += "\n"
|
||||
else:
|
||||
unversioned_msgs_list += '- [%s](%s.md)' % (msg_name, msg_name)
|
||||
if summary_description:
|
||||
unversioned_msgs_list += " — %s" % summary_description
|
||||
unversioned_msgs_list += f"- [{message.name}]({message.name}.md)"
|
||||
if message.shortDescription:
|
||||
unversioned_msgs_list += f" — {message.shortDescription}"
|
||||
unversioned_msgs_list += "\n"
|
||||
|
||||
# Write out the index.md file
|
||||
index_text="""# uORB Message Reference
|
||||
index_text=f"""# uORB Message Reference
|
||||
|
||||
::: info
|
||||
This list is [auto-generated](https://github.com/PX4/PX4-Autopilot/blob/main/Tools/msg/generate_msg_docs.py) from the source code.
|
||||
@@ -218,14 +981,14 @@ Graphs showing how these are used [can be found here](../middleware/uorb_graph.m
|
||||
|
||||
## Versioned Messages
|
||||
|
||||
%s
|
||||
{versioned_msgs_list}
|
||||
|
||||
## Unversioned Messages
|
||||
|
||||
%s
|
||||
""" % (versioned_msgs_list, unversioned_msgs_list)
|
||||
{unversioned_msgs_list}
|
||||
"""
|
||||
index_file = os.path.join(output_dir, 'index.md')
|
||||
with open(index_file, 'w') as content_file:
|
||||
with open(index_file, 'w', encoding='utf-8') as content_file:
|
||||
content_file.write(index_text)
|
||||
|
||||
generate_dds_yaml_doc(msg_files)
|
||||
|
||||
@@ -1138,6 +1138,8 @@ if num_mags >= 1:
|
||||
|
||||
if not math.isnan(sensor_mag_0['temperature'][0]):
|
||||
|
||||
mag_0_params['TC_M0_ID'] = int(np.median(sensor_mag_0['device_id']))
|
||||
|
||||
# find the min, max and reference temperature
|
||||
mag_0_params['TC_M0_TMIN'] = np.amin(sensor_mag_0['temperature'])
|
||||
mag_0_params['TC_M0_TMAX'] = np.amax(sensor_mag_0['temperature'])
|
||||
|
||||
Executable
+2113
File diff suppressed because it is too large
Load Diff
+8
-8
@@ -42,6 +42,7 @@
|
||||
import argparse
|
||||
import json
|
||||
import base64
|
||||
import os
|
||||
import zlib
|
||||
import time
|
||||
import subprocess
|
||||
@@ -99,14 +100,13 @@ if args.summary != None:
|
||||
if args.description != None:
|
||||
desc['description'] = str(args.description)
|
||||
if args.git_identity != None:
|
||||
cmd = "git --git-dir '{:}/.git' describe --exclude ext/* --always --tags".format(args.git_identity)
|
||||
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).stdout
|
||||
desc['git_identity'] = p.read().strip().decode('utf-8')
|
||||
p.close()
|
||||
cmd = "git --git-dir '{:}/.git' rev-parse --verify HEAD".format(args.git_identity)
|
||||
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).stdout
|
||||
desc['git_hash'] = p.read().strip().decode('utf-8')
|
||||
p.close()
|
||||
git_dir = os.path.join(args.git_identity, '.git')
|
||||
p = subprocess.run(["git", "--git-dir", git_dir, "describe", "--exclude", "ext/*", "--always", "--tags"],
|
||||
stdout=subprocess.PIPE, text=True)
|
||||
desc['git_identity'] = p.stdout.strip()
|
||||
p = subprocess.run(["git", "--git-dir", git_dir, "rev-parse", "--verify", "HEAD"],
|
||||
stdout=subprocess.PIPE, text=True)
|
||||
desc['git_hash'] = p.stdout.strip()
|
||||
if args.parameter_xml != None:
|
||||
f = open(args.parameter_xml, "rb")
|
||||
bytes = f.read()
|
||||
|
||||
@@ -1,983 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
############################################################################
|
||||
#
|
||||
# Copyright (c) 2012-2024 PX4 Development Team. All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in
|
||||
# the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# 3. Neither the name PX4 nor the names of its contributors may be
|
||||
# used to endorse or promote products derived from this software
|
||||
# without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
|
||||
# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
|
||||
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
############################################################################
|
||||
|
||||
#
|
||||
# Serial firmware uploader for the PX4FMU bootloader
|
||||
#
|
||||
# The PX4 firmware file is a JSON-encoded Python object, containing
|
||||
# metadata fields and a zlib-compressed base64-encoded firmware image.
|
||||
#
|
||||
# The uploader uses the following fields from the firmware file:
|
||||
#
|
||||
# image
|
||||
# The firmware that will be uploaded.
|
||||
# image_size
|
||||
# The size of the firmware in bytes.
|
||||
# board_id
|
||||
# The board for which the firmware is intended.
|
||||
# board_revision
|
||||
# Currently only used for informational purposes.
|
||||
#
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
import binascii
|
||||
import socket
|
||||
import struct
|
||||
import json
|
||||
import zlib
|
||||
import base64
|
||||
import time
|
||||
import array
|
||||
import os
|
||||
|
||||
from sys import platform as _platform
|
||||
|
||||
try:
|
||||
import serial
|
||||
except ImportError as e:
|
||||
print(f"Failed to import serial: {e}")
|
||||
print("")
|
||||
print("You may need to install it using:")
|
||||
print(" python -m pip install pyserial")
|
||||
print("")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Detect python version
|
||||
if sys.version_info[0] < 3:
|
||||
raise RuntimeError("Python 2 is not supported. Please try again using Python 3.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Use monotonic time where available
|
||||
def _time():
|
||||
try:
|
||||
return time.monotonic()
|
||||
except Exception:
|
||||
return time.time()
|
||||
|
||||
class FirmwareNotSuitableException(Exception):
|
||||
def __init__(self, message):
|
||||
super(FirmwareNotSuitableException, self).__init__(message)
|
||||
|
||||
|
||||
class firmware(object):
|
||||
'''Loads a firmware file'''
|
||||
|
||||
desc = {}
|
||||
image = bytes()
|
||||
crctab = array.array('I', [
|
||||
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
|
||||
0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
|
||||
0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
|
||||
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,
|
||||
0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
|
||||
0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
|
||||
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
|
||||
0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,
|
||||
0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
|
||||
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
|
||||
0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,
|
||||
0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
|
||||
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,
|
||||
0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
|
||||
0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
|
||||
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,
|
||||
0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683,
|
||||
0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
|
||||
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,
|
||||
0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
|
||||
0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
|
||||
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,
|
||||
0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,
|
||||
0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
|
||||
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
|
||||
0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21,
|
||||
0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
|
||||
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
|
||||
0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,
|
||||
0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
|
||||
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,
|
||||
0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d])
|
||||
crcpad = bytearray(b'\xff\xff\xff\xff')
|
||||
|
||||
def __init__(self, path):
|
||||
|
||||
# read the file
|
||||
f = open(path, "r")
|
||||
self.desc = json.load(f)
|
||||
f.close()
|
||||
|
||||
self.image = bytearray(zlib.decompress(base64.b64decode(self.desc['image'])))
|
||||
|
||||
# pad image to 4-byte length
|
||||
while ((len(self.image) % 4) != 0):
|
||||
self.image.extend(b'\xff')
|
||||
|
||||
def property(self, propname):
|
||||
return self.desc[propname]
|
||||
|
||||
def __crc32(self, bytes, state):
|
||||
for byte in bytes:
|
||||
index = (state ^ byte) & 0xff
|
||||
state = self.crctab[index] ^ (state >> 8)
|
||||
return state
|
||||
|
||||
def crc(self, padlen):
|
||||
state = self.__crc32(self.image, int(0))
|
||||
for _ in range(len(self.image), (padlen - 1), 4):
|
||||
state = self.__crc32(self.crcpad, state)
|
||||
return state
|
||||
|
||||
|
||||
class uploader:
|
||||
'''Uploads a firmware file to the PX4 bootloader'''
|
||||
|
||||
# protocol bytes
|
||||
INSYNC = b'\x12'
|
||||
EOC = b'\x20'
|
||||
|
||||
# reply bytes
|
||||
OK = b'\x10'
|
||||
FAILED = b'\x11'
|
||||
INVALID = b'\x13' # rev3+
|
||||
BAD_SILICON_REV = b'\x14' # rev5+
|
||||
|
||||
# command bytes
|
||||
NOP = b'\x00' # guaranteed to be discarded by the bootloader
|
||||
GET_SYNC = b'\x21'
|
||||
GET_DEVICE = b'\x22'
|
||||
CHIP_ERASE = b'\x23'
|
||||
CHIP_VERIFY = b'\x24' # rev2 only
|
||||
PROG_MULTI = b'\x27'
|
||||
READ_MULTI = b'\x28' # rev2 only
|
||||
GET_CRC = b'\x29' # rev3+
|
||||
GET_OTP = b'\x2a' # rev4+ , get a word from OTP area
|
||||
GET_SN = b'\x2b' # rev4+ , get a word from SN area
|
||||
GET_CHIP = b'\x2c' # rev5+ , get chip version
|
||||
SET_BOOT_DELAY = b'\x2d' # rev5+ , set boot delay
|
||||
GET_CHIP_DES = b'\x2e' # rev5+ , get chip description in ASCII
|
||||
GET_VERSION = b'\x2f' # rev5+ , get bootloader version in ASCII
|
||||
CHIP_FULL_ERASE = b'\x40' # full erase of flash, rev6+
|
||||
MAX_DES_LENGTH = 20
|
||||
|
||||
REBOOT = b'\x30'
|
||||
|
||||
INFO_BL_REV = b'\x01' # bootloader protocol revision
|
||||
BL_REV_MIN = 2 # minimum supported bootloader protocol
|
||||
BL_REV_MAX = 5 # maximum supported bootloader protocol
|
||||
INFO_BOARD_ID = b'\x02' # board type
|
||||
INFO_BOARD_REV = b'\x03' # board revision
|
||||
INFO_FLASH_SIZE = b'\x04' # max firmware size in bytes
|
||||
|
||||
PROG_MULTI_MAX = 252 # protocol max is 255, must be multiple of 4
|
||||
READ_MULTI_MAX = 252 # protocol max is 255
|
||||
|
||||
NSH_INIT = bytearray(b'\x0d\x0d\x0d')
|
||||
NSH_REBOOT_BL = b"reboot -b\n"
|
||||
NSH_REBOOT = b"reboot\n"
|
||||
MAVLINK_REBOOT_ID1 = bytearray(b'\xfe\x21\x72\xff\x00\x4c\x00\x00\x40\x40\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf6\x00\x01\x00\x00\x53\x6b')
|
||||
MAVLINK_REBOOT_ID0 = bytearray(b'\xfe\x21\x45\xff\x00\x4c\x00\x00\x40\x40\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf6\x00\x00\x00\x00\xcc\x37')
|
||||
|
||||
MAX_FLASH_PRGRAM_TIME = 0.001 # Time on an F7 to send SYNC, RESULT from last data in multi RXed
|
||||
|
||||
def __init__(self, portname, baudrate_bootloader, baudrate_flightstack):
|
||||
# Open the port, keep the default timeout short so we can poll quickly.
|
||||
# On some systems writes can suddenly get stuck without having a
|
||||
# write_timeout > 0 set.
|
||||
# chartime 8n1 * bit rate is us
|
||||
self.chartime = 10 * (1.0 / baudrate_bootloader)
|
||||
|
||||
# we use a window approche to SYNC,<result> gathring
|
||||
self.window = 0
|
||||
self.window_max = 256
|
||||
self.window_per = 2 # Sync,<result>
|
||||
self.ackWindowedMode = False # Assume Non Widowed mode for all USB CDC
|
||||
self.port = serial.Serial(portname, baudrate_bootloader, timeout=0.5, write_timeout=0)
|
||||
self.otp = b''
|
||||
self.sn = b''
|
||||
self.baudrate_bootloader = baudrate_bootloader
|
||||
self.baudrate_flightstack = baudrate_flightstack
|
||||
self.baudrate_flightstack_idx = -1
|
||||
self.force_erase = False
|
||||
|
||||
def close(self):
|
||||
if self.port is not None:
|
||||
self.port.close()
|
||||
|
||||
def open(self):
|
||||
# upload timeout
|
||||
timeout = _time() + 0.2
|
||||
|
||||
# attempt to open the port while it exists and until timeout occurs
|
||||
while self.port is not None:
|
||||
portopen = True
|
||||
try:
|
||||
portopen = self.port.is_open
|
||||
except AttributeError:
|
||||
portopen = self.port.isOpen()
|
||||
|
||||
if not portopen and _time() < timeout:
|
||||
try:
|
||||
self.port.open()
|
||||
except OSError:
|
||||
# wait for the port to be ready
|
||||
time.sleep(0.04)
|
||||
except serial.SerialException:
|
||||
# if open fails, try again later
|
||||
time.sleep(0.04)
|
||||
else:
|
||||
break
|
||||
|
||||
# debugging code
|
||||
def __probe(self, state):
|
||||
# self.port.setRTS(state)
|
||||
return
|
||||
|
||||
def __send(self, c):
|
||||
# print("send " + binascii.hexlify(c))
|
||||
self.port.write(c)
|
||||
|
||||
def __recv(self, count=1):
|
||||
c = self.port.read(count)
|
||||
if len(c) < 1:
|
||||
raise RuntimeError("timeout waiting for data (%u bytes)" % count)
|
||||
# print("recv " + binascii.hexlify(c))
|
||||
return c
|
||||
|
||||
def __recv_int(self):
|
||||
raw = self.__recv(4)
|
||||
val = struct.unpack("<I", raw)
|
||||
return val[0]
|
||||
|
||||
def __getSync(self, doFlush=True):
|
||||
if (doFlush):
|
||||
self.port.flush()
|
||||
c = bytes(self.__recv())
|
||||
if c != self.INSYNC:
|
||||
raise RuntimeError("unexpected %s instead of INSYNC" % c)
|
||||
c = self.__recv()
|
||||
if c == self.INVALID:
|
||||
raise RuntimeError("bootloader reports INVALID OPERATION")
|
||||
if c == self.FAILED:
|
||||
raise RuntimeError("bootloader reports OPERATION FAILED")
|
||||
if c != self.OK:
|
||||
raise RuntimeError("unexpected response 0x%x instead of OK" % ord(c))
|
||||
|
||||
# The control flow for receiving Sync is on the order of 16 Ms per Sync
|
||||
# This will validate all the SYNC,<results> for a window of programing
|
||||
# in about 13.81 Ms for 256 blocks written
|
||||
def __ackSyncWindow(self, count):
|
||||
if (count > 0):
|
||||
data = bytearray(bytes(self.__recv(count)))
|
||||
if (len(data) != count):
|
||||
raise RuntimeError("Ack Window %i not %i " % (len(data), count))
|
||||
for i in range(0, len(data), 2):
|
||||
if bytes([data[i]]) != self.INSYNC:
|
||||
raise RuntimeError("unexpected %s instead of INSYNC" % data[i])
|
||||
if bytes([data[i+1]]) == self.INVALID:
|
||||
raise RuntimeError("bootloader reports INVALID OPERATION")
|
||||
if bytes([data[i+1]]) == self.FAILED:
|
||||
raise RuntimeError("bootloader reports OPERATION FAILED")
|
||||
if bytes([data[i+1]]) != self.OK:
|
||||
raise RuntimeError("unexpected response 0x%x instead of OK" % ord(data[i+1]))
|
||||
|
||||
# attempt to get back into sync with the bootloader
|
||||
def __sync(self):
|
||||
# send a stream of ignored bytes longer than the longest possible conversation
|
||||
# that we might still have in progress
|
||||
# self.__send(uploader.NOP * (uploader.PROG_MULTI_MAX + 2))
|
||||
self.port.flushInput()
|
||||
self.__send(uploader.GET_SYNC +
|
||||
uploader.EOC)
|
||||
self.__getSync()
|
||||
|
||||
def __trySync(self):
|
||||
try:
|
||||
self.port.flush()
|
||||
if (self.__recv() != self.INSYNC):
|
||||
# print("unexpected 0x%x instead of INSYNC" % ord(c))
|
||||
return False
|
||||
c = self.__recv()
|
||||
if (c == self.BAD_SILICON_REV):
|
||||
raise NotImplementedError()
|
||||
if (c != self.OK):
|
||||
# print("unexpected 0x%x instead of OK" % ord(c))
|
||||
return False
|
||||
return True
|
||||
|
||||
except NotImplementedError:
|
||||
raise RuntimeError("Programing not supported for this version of silicon!\n"
|
||||
"See https://docs.px4.io/main/en/flight_controller/silicon_errata.html")
|
||||
except RuntimeError:
|
||||
# timeout, no response yet
|
||||
return False
|
||||
|
||||
# attempt to determins if the device is CDCACM or A FTDI
|
||||
def __determineInterface(self):
|
||||
self.port.flushInput()
|
||||
# Set a baudrate that can not work on a real serial port
|
||||
# in that it is 233% off.
|
||||
try:
|
||||
self.port.baudrate = self.baudrate_bootloader * 2.33
|
||||
except NotImplementedError as e:
|
||||
# This error can occur because pySerial on Windows does not support odd baudrates
|
||||
print(f"{e} -> could not check for FTDI device, assuming USB connection")
|
||||
return
|
||||
|
||||
self.__send(uploader.GET_SYNC +
|
||||
uploader.EOC)
|
||||
try:
|
||||
self.__getSync(False)
|
||||
except RuntimeError:
|
||||
# if it fails we are on a real serial port - only leave this enabled on Windows
|
||||
if sys.platform.startswith('win'):
|
||||
self.ackWindowedMode = True
|
||||
finally:
|
||||
try:
|
||||
self.port.baudrate = self.baudrate_bootloader
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# send the GET_DEVICE command and wait for an info parameter
|
||||
def __getInfo(self, param):
|
||||
self.__send(uploader.GET_DEVICE + param + uploader.EOC)
|
||||
value = self.__recv_int()
|
||||
self.__getSync()
|
||||
return value
|
||||
|
||||
# send the GET_OTP command and wait for an info parameter
|
||||
def __getOTP(self, param):
|
||||
t = struct.pack("I", param) # int param as 32bit ( 4 byte ) char array.
|
||||
self.__send(uploader.GET_OTP + t + uploader.EOC)
|
||||
value = self.__recv(4)
|
||||
self.__getSync()
|
||||
return value
|
||||
|
||||
# send the GET_SN command and wait for an info parameter
|
||||
def __getSN(self, param):
|
||||
t = struct.pack("I", param) # int param as 32bit ( 4 byte ) char array.
|
||||
self.__send(uploader.GET_SN + t + uploader.EOC)
|
||||
value = self.__recv(4)
|
||||
self.__getSync()
|
||||
return value
|
||||
|
||||
# send the GET_CHIP command
|
||||
def __getCHIP(self):
|
||||
self.__send(uploader.GET_CHIP + uploader.EOC)
|
||||
value = self.__recv_int()
|
||||
self.__getSync()
|
||||
return value
|
||||
|
||||
# send the GET_CHIP command
|
||||
def __getCHIPDes(self):
|
||||
self.__send(uploader.GET_CHIP_DES + uploader.EOC)
|
||||
length = self.__recv_int()
|
||||
value = self.__recv(length)
|
||||
self.__getSync()
|
||||
pieces = value.split(b",")
|
||||
return pieces
|
||||
|
||||
def __getVersion(self):
|
||||
self.__send(uploader.GET_VERSION + uploader.EOC)
|
||||
try:
|
||||
length = self.__recv_int()
|
||||
value = self.__recv(length)
|
||||
self.__getSync()
|
||||
except RuntimeError:
|
||||
# Bootloader doesn't support version call
|
||||
return "unknown"
|
||||
return value.decode()
|
||||
|
||||
def __drawProgressBar(self, label, progress, maxVal):
|
||||
if maxVal < progress:
|
||||
progress = maxVal
|
||||
|
||||
percent = (float(progress) / float(maxVal)) * 100.0
|
||||
|
||||
redraw = "\r" if sys.stdout.isatty() else "\n"
|
||||
sys.stdout.write("%s%s: [%-20s] %.1f%%" % (redraw, label, '='*int(percent/5.0), percent))
|
||||
sys.stdout.flush()
|
||||
|
||||
# send the CHIP_ERASE command and wait for the bootloader to become ready
|
||||
def __erase(self, label):
|
||||
print(f"Windowed mode: {self.ackWindowedMode}")
|
||||
print("\n", end='')
|
||||
|
||||
if self.force_erase:
|
||||
print("Trying force erase of full chip...\n")
|
||||
self.__send(uploader.CHIP_FULL_ERASE +
|
||||
uploader.EOC)
|
||||
else:
|
||||
self.__send(uploader.CHIP_ERASE +
|
||||
uploader.EOC)
|
||||
|
||||
# erase is very slow, give it 30s
|
||||
deadline = _time() + 30.0
|
||||
while _time() < deadline:
|
||||
|
||||
usualEraseDuration = 15.0
|
||||
estimatedTimeRemaining = deadline-_time()
|
||||
if estimatedTimeRemaining >= usualEraseDuration:
|
||||
self.__drawProgressBar(label, 30.0-estimatedTimeRemaining, usualEraseDuration)
|
||||
else:
|
||||
self.__drawProgressBar(label, 10.0, 10.0)
|
||||
sys.stdout.write(" (timeout: %d seconds) " % int(deadline-_time()))
|
||||
sys.stdout.flush()
|
||||
|
||||
if self.__trySync():
|
||||
self.__drawProgressBar(label, 10.0, 10.0)
|
||||
if self.force_erase:
|
||||
print("\nForce erase done.\n")
|
||||
return
|
||||
|
||||
if self.force_erase:
|
||||
raise RuntimeError("timed out waiting for erase, force erase is likely not supported by bootloader!")
|
||||
else:
|
||||
raise RuntimeError("timed out waiting for erase")
|
||||
|
||||
# send a PROG_MULTI command to write a collection of bytes
|
||||
def __program_multi(self, data, windowMode):
|
||||
|
||||
length = len(data).to_bytes(1, byteorder='big')
|
||||
|
||||
self.__send(uploader.PROG_MULTI)
|
||||
self.__send(length)
|
||||
self.__send(data)
|
||||
self.__send(uploader.EOC)
|
||||
if (not windowMode):
|
||||
self.__getSync(False)
|
||||
else:
|
||||
# The following is done to have minimum delay on the transmission
|
||||
# of the ne fw. The per block cost of __getSync was about 16 mS per.
|
||||
# Passively wait on Sync and Result using board rates and
|
||||
# N.B. attempts to activly wait on InWating still carried 8 mS of overhead
|
||||
self.__probe(False)
|
||||
self.__probe(True)
|
||||
time.sleep((ord(length) * self.chartime) + uploader.MAX_FLASH_PRGRAM_TIME)
|
||||
self.__probe(False)
|
||||
|
||||
# verify multiple bytes in flash
|
||||
def __verify_multi(self, data):
|
||||
|
||||
length = len(data).to_bytes(1, byteorder='big')
|
||||
|
||||
self.__send(uploader.READ_MULTI)
|
||||
self.__send(length)
|
||||
self.__send(uploader.EOC)
|
||||
self.port.flush()
|
||||
programmed = self.__recv(len(data))
|
||||
if programmed != data:
|
||||
print("got " + binascii.hexlify(programmed))
|
||||
print("expect " + binascii.hexlify(data))
|
||||
return False
|
||||
self.__getSync()
|
||||
return True
|
||||
|
||||
# send the reboot command
|
||||
def __reboot(self):
|
||||
self.__send(uploader.REBOOT +
|
||||
uploader.EOC)
|
||||
self.port.flush()
|
||||
|
||||
# v3+ can report failure if the first word flash fails
|
||||
if self.bl_rev >= 3:
|
||||
self.__getSync()
|
||||
|
||||
# split a sequence into a list of size-constrained pieces
|
||||
def __split_len(self, seq, length):
|
||||
return [seq[i:i+length] for i in range(0, len(seq), length)]
|
||||
|
||||
# upload code
|
||||
def __program(self, label, fw):
|
||||
self.__probe(False)
|
||||
print("\n", end='')
|
||||
code = fw.image
|
||||
groups = self.__split_len(code, uploader.PROG_MULTI_MAX)
|
||||
# Give imedate feedback
|
||||
self.__drawProgressBar(label, 0, len(groups))
|
||||
uploadProgress = 0
|
||||
for bytes in groups:
|
||||
self.__program_multi(bytes, self.ackWindowedMode)
|
||||
# If in Window mode, extend the window size for the __ackSyncWindow
|
||||
if self.ackWindowedMode:
|
||||
self.window += self.window_per
|
||||
|
||||
# Print upload progress (throttled, so it does not delay upload progress)
|
||||
uploadProgress += 1
|
||||
if uploadProgress % 256 == 0:
|
||||
self.__probe(True)
|
||||
self.__probe(False)
|
||||
self.__probe(True)
|
||||
self.__ackSyncWindow(self.window)
|
||||
self.__probe(False)
|
||||
self.window = 0
|
||||
self.__drawProgressBar(label, uploadProgress, len(groups))
|
||||
|
||||
# Do any remaining fragment
|
||||
self.__ackSyncWindow(self.window)
|
||||
self.window = 0
|
||||
self.__drawProgressBar(label, 100, 100)
|
||||
|
||||
# verify code
|
||||
def __verify_v2(self, label, fw):
|
||||
print("\n", end='')
|
||||
self.__send(uploader.CHIP_VERIFY +
|
||||
uploader.EOC)
|
||||
self.__getSync()
|
||||
code = fw.image
|
||||
groups = self.__split_len(code, uploader.READ_MULTI_MAX)
|
||||
verifyProgress = 0
|
||||
for bytes in groups:
|
||||
verifyProgress += 1
|
||||
if verifyProgress % 256 == 0:
|
||||
self.__drawProgressBar(label, verifyProgress, len(groups))
|
||||
if (not self.__verify_multi(bytes)):
|
||||
raise RuntimeError("Verification failed")
|
||||
self.__drawProgressBar(label, 100, 100)
|
||||
|
||||
def __verify_v3(self, label, fw):
|
||||
print("\n", end='')
|
||||
self.__drawProgressBar(label, 1, 100)
|
||||
expect_crc = fw.crc(self.fw_maxsize)
|
||||
self.__send(uploader.GET_CRC + uploader.EOC)
|
||||
time.sleep(0.5)
|
||||
report_crc = self.__recv_int()
|
||||
self.__getSync()
|
||||
if report_crc != expect_crc:
|
||||
print("Expected 0x%x" % expect_crc)
|
||||
print("Got 0x%x" % report_crc)
|
||||
raise RuntimeError("Program CRC failed")
|
||||
self.__drawProgressBar(label, 100, 100)
|
||||
|
||||
def __set_boot_delay(self, boot_delay):
|
||||
self.__send(uploader.SET_BOOT_DELAY +
|
||||
struct.pack("b", boot_delay) +
|
||||
uploader.EOC)
|
||||
self.__getSync()
|
||||
|
||||
# get basic data about the board
|
||||
def identify(self):
|
||||
self.__determineInterface()
|
||||
# make sure we are in sync before starting
|
||||
self.__sync()
|
||||
|
||||
# get the bootloader protocol ID first
|
||||
self.bl_rev = self.__getInfo(uploader.INFO_BL_REV)
|
||||
if (self.bl_rev < uploader.BL_REV_MIN) or (self.bl_rev > uploader.BL_REV_MAX):
|
||||
print("Unsupported bootloader protocol %d" % uploader.INFO_BL_REV)
|
||||
raise RuntimeError("Bootloader protocol mismatch")
|
||||
|
||||
self.board_type = self.__getInfo(uploader.INFO_BOARD_ID)
|
||||
self.board_rev = self.__getInfo(uploader.INFO_BOARD_REV)
|
||||
self.fw_maxsize = self.__getInfo(uploader.INFO_FLASH_SIZE)
|
||||
|
||||
self.version = self.__getVersion()
|
||||
|
||||
# upload the firmware
|
||||
def upload(self, fw_list, force=False, boot_delay=None, boot_check=False, force_erase=False):
|
||||
self.force_erase = force_erase
|
||||
# select correct binary
|
||||
found_suitable_firmware = False
|
||||
for file in fw_list:
|
||||
fw = firmware(file)
|
||||
if self.board_type == fw.property('board_id'):
|
||||
if len(fw_list) > 1: print("using firmware binary {}".format(file))
|
||||
found_suitable_firmware = True
|
||||
break
|
||||
|
||||
if not found_suitable_firmware:
|
||||
msg = "Firmware not suitable for this board (Firmware board_type=%u board_id=%u)" % (
|
||||
self.board_type, fw.property('board_id'))
|
||||
print("WARNING: %s" % msg)
|
||||
if force:
|
||||
if len(fw_list) > 1:
|
||||
raise FirmwareNotSuitableException("force flashing failed, more than one file provided, none suitable")
|
||||
print("FORCED WRITE, FLASHING ANYWAY!")
|
||||
else:
|
||||
raise FirmwareNotSuitableException(msg)
|
||||
|
||||
percent = fw.property('image_size') / fw.property('image_maxsize')
|
||||
binary_size = float(fw.property('image_size'))
|
||||
binary_max_size = float(fw.property('image_maxsize'))
|
||||
percent = (binary_size / binary_max_size) * 100
|
||||
|
||||
print("Loaded firmware for board id: %s,%s size: %d bytes (%.2f%%) " % (fw.property('board_id'), fw.property('board_revision'), fw.property('image_size'), percent))
|
||||
print()
|
||||
|
||||
print(f"Bootloader version: {self.version}")
|
||||
|
||||
# Make sure we are doing the right thing
|
||||
start = _time()
|
||||
if self.board_type != fw.property('board_id'):
|
||||
msg = "Firmware not suitable for this board (Firmware board_type=%u board_id=%u)" % (
|
||||
self.board_type, fw.property('board_id'))
|
||||
print("WARNING: %s" % msg)
|
||||
if force:
|
||||
print("FORCED WRITE, FLASHING ANYWAY!")
|
||||
else:
|
||||
raise FirmwareNotSuitableException(msg)
|
||||
|
||||
# Prevent uploads where the image would overflow the flash
|
||||
if self.fw_maxsize < fw.property('image_size'):
|
||||
raise RuntimeError("Firmware image is too large for this board")
|
||||
|
||||
# OTP added in v4:
|
||||
if self.bl_rev >= 4:
|
||||
for byte in range(0, 32*6, 4):
|
||||
x = self.__getOTP(byte)
|
||||
self.otp = self.otp + x
|
||||
# print(binascii.hexlify(x).decode('Latin-1') + ' ', end='')
|
||||
# see src/modules/systemlib/otp.h in px4 code:
|
||||
self.otp_id = self.otp[0:4]
|
||||
self.otp_idtype = self.otp[4:5]
|
||||
self.otp_vid = self.otp[8:4:-1]
|
||||
self.otp_pid = self.otp[12:8:-1]
|
||||
self.otp_coa = self.otp[32:160]
|
||||
# show user:
|
||||
try:
|
||||
print("Sn: ", end='')
|
||||
for byte in range(0, 12, 4):
|
||||
x = self.__getSN(byte)
|
||||
x = x[::-1] # reverse the bytes
|
||||
self.sn = self.sn + x
|
||||
print(binascii.hexlify(x).decode('Latin-1'), end='') # show user
|
||||
print('')
|
||||
print("Chip: %08x" % self.__getCHIP())
|
||||
|
||||
otp_id = self.otp_id.decode('Latin-1')
|
||||
if ("PX4" in otp_id):
|
||||
print("OTP id: " + otp_id)
|
||||
print("OTP idtype: " + binascii.b2a_qp(self.otp_idtype).decode('Latin-1'))
|
||||
print("OTP vid: " + binascii.hexlify(self.otp_vid).decode('Latin-1'))
|
||||
print("OTP pid: " + binascii.hexlify(self.otp_pid).decode('Latin-1'))
|
||||
print("OTP coa: " + binascii.b2a_base64(self.otp_coa).decode('Latin-1'))
|
||||
|
||||
except Exception as e:
|
||||
# ignore bad character encodings
|
||||
print(f"Exception ignored: {e}")
|
||||
pass
|
||||
|
||||
# Silicon errata check was added in v5
|
||||
if (self.bl_rev >= 5):
|
||||
des = self.__getCHIPDes()
|
||||
if (len(des) == 2):
|
||||
family, revision = des
|
||||
print(f"Family: {family.decode()}")
|
||||
print(f"Revision: {revision.decode()}")
|
||||
print(f"Flash: {self.fw_maxsize} bytes")
|
||||
|
||||
# Prevent uploads where the maximum image size of the board config is smaller than the flash
|
||||
# of the board. This is a hint the user chose the wrong config and will lack features
|
||||
# for this particular board.
|
||||
|
||||
# This check should also check if the revision is an unaffected revision
|
||||
# and thus can support the full flash, see
|
||||
# https://github.com/PX4/Firmware/blob/master/src/drivers/boards/common/stm32/board_mcu_version.c#L125-L144
|
||||
|
||||
if self.fw_maxsize > fw.property('image_maxsize') and not force:
|
||||
print(f"WARNING: Board can accept larger flash images ({self.fw_maxsize} bytes) than board config ({fw.property('image_maxsize')} bytes)")
|
||||
else:
|
||||
# If we're still on bootloader v4 on a Pixhawk, we don't know if we
|
||||
# have the silicon errata and therefore need to flash px4_fmu-v2
|
||||
# with 1MB flash or if it supports px4_fmu-v3 with 2MB flash.
|
||||
if fw.property('board_id') == 9 \
|
||||
and fw.property('image_size') > 1032192 \
|
||||
and not force:
|
||||
raise RuntimeError("\nThe Board uses bootloader revision 4 and can therefore not determine\n"
|
||||
"if flashing more than 1 MB (px4_fmu-v3_default) is safe, chances are\n"
|
||||
"high that it is not safe! If unsure, use px4_fmu-v2_default.\n"
|
||||
"\n"
|
||||
"If you know you that the board does not have the silicon errata, use\n"
|
||||
"this script with --force, or update the bootloader. If you are invoking\n"
|
||||
"upload using make, you can use force-upload target to force the upload.\n")
|
||||
self.__erase("Erase ")
|
||||
self.__program("Program", fw)
|
||||
|
||||
if self.bl_rev == 2:
|
||||
self.__verify_v2("Verify ", fw)
|
||||
else:
|
||||
self.__verify_v3("Verify ", fw)
|
||||
|
||||
if boot_delay is not None:
|
||||
self.__set_boot_delay(boot_delay)
|
||||
|
||||
print("\nRebooting.", end='')
|
||||
self.__reboot()
|
||||
self.port.close()
|
||||
print(" Elapsed Time %3.3f\n" % (_time() - start))
|
||||
|
||||
def __next_baud_flightstack(self):
|
||||
if self.baudrate_flightstack_idx + 1 >= len(self.baudrate_flightstack):
|
||||
return False
|
||||
try:
|
||||
self.port.baudrate = self.baudrate_flightstack[self.baudrate_flightstack_idx + 1]
|
||||
self.baudrate_flightstack_idx = self.baudrate_flightstack_idx + 1
|
||||
except serial.SerialException:
|
||||
# Sometimes _configure_port fails
|
||||
time.sleep(0.04)
|
||||
|
||||
return True
|
||||
|
||||
def send_protocol_splitter_format(self, data):
|
||||
# Header Structure:
|
||||
# bits: 1 2 3 4 5 6 7 8
|
||||
# header[0] - | Magic | (='S')
|
||||
# header[1] - |T| LenH | (T - 0: mavlink; 1: rtps)
|
||||
# header[2] - | LenL |
|
||||
# header[3] - | Checksum |
|
||||
|
||||
MAGIC = 83
|
||||
|
||||
len_bytes = len(data).to_bytes(2, "big")
|
||||
LEN_H = len_bytes[0] & 127
|
||||
LEN_L = len_bytes[1] & 255
|
||||
CHECKSUM = MAGIC ^ LEN_H ^ LEN_L
|
||||
|
||||
header_ints = [MAGIC, LEN_H, LEN_L, CHECKSUM]
|
||||
header_bytes = struct.pack("{}B".format(len(header_ints)), *header_ints)
|
||||
|
||||
self.__send(header_bytes)
|
||||
self.__send(data)
|
||||
|
||||
def send_reboot(self, use_protocol_splitter_format=False):
|
||||
if (not self.__next_baud_flightstack()):
|
||||
return False
|
||||
|
||||
print("Attempting reboot on %s with baudrate=%d..." % (self.port.port, self.port.baudrate), file=sys.stderr)
|
||||
if "ttyS" in self.port.port:
|
||||
print("If the board does not respond, check the connection to the Flight Controller")
|
||||
else:
|
||||
print("If the board does not respond, unplug and re-plug the USB connector.", file=sys.stderr)
|
||||
|
||||
try:
|
||||
send_fct = self.__send
|
||||
if use_protocol_splitter_format:
|
||||
send_fct = self.send_protocol_splitter_format
|
||||
|
||||
# try MAVLINK command first
|
||||
self.port.flush()
|
||||
send_fct(uploader.MAVLINK_REBOOT_ID1)
|
||||
send_fct(uploader.MAVLINK_REBOOT_ID0)
|
||||
# then try reboot via NSH
|
||||
send_fct(uploader.NSH_INIT)
|
||||
send_fct(uploader.NSH_REBOOT_BL)
|
||||
send_fct(uploader.NSH_INIT)
|
||||
send_fct(uploader.NSH_REBOOT)
|
||||
self.port.flush()
|
||||
self.port.baudrate = self.baudrate_bootloader
|
||||
except Exception:
|
||||
try:
|
||||
self.port.flush()
|
||||
self.port.baudrate = self.baudrate_bootloader
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
# Parse commandline arguments
|
||||
parser = argparse.ArgumentParser(description="Firmware uploader for the PX autopilot system.")
|
||||
parser.add_argument('--port', action="store", required=True, help="Comma-separated list of serial port(s) to which the FMU may be attached")
|
||||
parser.add_argument('--baud-bootloader', action="store", type=int, default=115200, help="Baud rate of the serial port (default is 115200) when communicating with bootloader, only required for true serial ports.")
|
||||
parser.add_argument('--baud-flightstack', action="store", default="57600", help="Comma-separated list of baud rate of the serial port (default is 57600) when communicating with flight stack (Mavlink or NSH), only required for true serial ports.")
|
||||
parser.add_argument('--force', action='store_true', default=False, help='Override board type check, or silicon errata checks and continue loading')
|
||||
parser.add_argument('--force-erase', action="store_true", help="Do not perform the blank check, always erase every sector of the application space")
|
||||
parser.add_argument('--boot-delay', type=int, default=None, help='minimum boot delay to store in flash')
|
||||
parser.add_argument('--use-protocol-splitter-format', action='store_true', help='use protocol splitter format for reboot')
|
||||
parser.add_argument('firmware', action="store", nargs='+', help="Firmware file(s)")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.use_protocol_splitter_format:
|
||||
print("Using protocol splitter format to reboot pixhawk!")
|
||||
|
||||
# warn people about ModemManager which interferes badly with Pixhawk
|
||||
if os.path.exists("/usr/sbin/ModemManager"):
|
||||
print("==========================================================================================================")
|
||||
print("WARNING: You should uninstall ModemManager as it conflicts with any non-modem serial device (like Pixhawk)")
|
||||
print("==========================================================================================================")
|
||||
|
||||
print("Waiting for bootloader...")
|
||||
# tell any GCS that might be connected to the autopilot to give up
|
||||
# control of the serial port
|
||||
|
||||
# send to localhost and default GCS port
|
||||
ipaddr = '127.0.0.1'
|
||||
portnum = 14550
|
||||
|
||||
# COMMAND_LONG in MAVLink 1
|
||||
heartbeatpacket = bytearray.fromhex('fe097001010000000100020c5103033c8a')
|
||||
commandpacket = bytearray.fromhex('fe210101014c00000000000000000000000000000000000000000000803f00000000f6000000008459')
|
||||
|
||||
# initialize an UDP socket
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
|
||||
# send heartbeat to initialize connection and command to free the link
|
||||
s.sendto(heartbeatpacket, (ipaddr, portnum))
|
||||
s.sendto(commandpacket, (ipaddr, portnum))
|
||||
|
||||
# close the socket
|
||||
s.close()
|
||||
|
||||
# Spin waiting for a device to show up
|
||||
try:
|
||||
while True:
|
||||
portlist = []
|
||||
patterns = args.port.split(",")
|
||||
# on unix-like platforms use glob to support wildcard ports. This allows
|
||||
# the use of /dev/serial/by-id/usb-3D_Robotics on Linux, which prevents the upload from
|
||||
# causing modem hangups etc
|
||||
if "linux" in _platform or "darwin" in _platform or "cygwin" in _platform:
|
||||
import glob
|
||||
for pattern in patterns:
|
||||
portlist += glob.glob(pattern)
|
||||
else:
|
||||
portlist = patterns
|
||||
|
||||
baud_flightstack = [int(x) for x in args.baud_flightstack.split(',')]
|
||||
|
||||
successful = False
|
||||
unsuitable_board = False
|
||||
for port in portlist:
|
||||
|
||||
# print("Trying %s" % port)
|
||||
|
||||
# create an uploader attached to the port
|
||||
try:
|
||||
if "linux" in _platform:
|
||||
# Linux, don't open Mac OS and Win ports
|
||||
if "COM" not in port and "tty.usb" not in port:
|
||||
up = uploader(port, args.baud_bootloader, baud_flightstack)
|
||||
elif "darwin" in _platform:
|
||||
# OS X, don't open Windows and Linux ports
|
||||
if "COM" not in port and "ACM" not in port:
|
||||
up = uploader(port, args.baud_bootloader, baud_flightstack)
|
||||
elif "cygwin" in _platform:
|
||||
# Cygwin, don't open native Windows COM and Linux ports
|
||||
if "COM" not in port and "ACM" not in port:
|
||||
up = uploader(port, args.baud_bootloader, baud_flightstack)
|
||||
elif "win" in _platform:
|
||||
# Windows, don't open POSIX ports
|
||||
if "/" not in port:
|
||||
up = uploader(port, args.baud_bootloader, baud_flightstack)
|
||||
except Exception as e:
|
||||
# open failed, rate-limit our attempts
|
||||
time.sleep(0.05)
|
||||
print(f"Exception ignored: {e}")
|
||||
|
||||
# and loop to the next port
|
||||
continue
|
||||
|
||||
found_bootloader = False
|
||||
while True:
|
||||
up.open()
|
||||
|
||||
# port is open, try talking to it
|
||||
try:
|
||||
# identify the bootloader
|
||||
up.identify()
|
||||
found_bootloader = True
|
||||
print()
|
||||
print(f"Found board id: {up.board_type},{up.board_rev} bootloader protocol revision {up.bl_rev} on {port}")
|
||||
break
|
||||
|
||||
except (RuntimeError, serial.SerialException):
|
||||
|
||||
if not up.send_reboot(args.use_protocol_splitter_format):
|
||||
break
|
||||
|
||||
# wait for the reboot, without we might run into Serial I/O Error 5
|
||||
time.sleep(0.25)
|
||||
|
||||
# always close the port
|
||||
up.close()
|
||||
|
||||
# wait for the close, without we might run into Serial I/O Error 6
|
||||
time.sleep(0.3)
|
||||
|
||||
if not found_bootloader:
|
||||
# Go to the next port
|
||||
continue
|
||||
|
||||
try:
|
||||
# ok, we have a bootloader, try flashing it
|
||||
up.upload(args.firmware, force=args.force, boot_delay=args.boot_delay, force_erase=args.force_erase)
|
||||
|
||||
# if we made this far without raising exceptions, the upload was successful
|
||||
successful = True
|
||||
|
||||
except RuntimeError as e:
|
||||
# print the error
|
||||
print(f"\n\nError: {e}")
|
||||
|
||||
except FirmwareNotSuitableException:
|
||||
unsuitable_board = True
|
||||
up.close()
|
||||
continue
|
||||
|
||||
except IOError:
|
||||
up.close()
|
||||
continue
|
||||
|
||||
finally:
|
||||
# always close the port
|
||||
up.close()
|
||||
|
||||
# we could loop here if we wanted to wait for more boards...
|
||||
if successful:
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
if unsuitable_board:
|
||||
# If we land here, we went through all ports, did not flash any
|
||||
# board and found at least one unsuitable board.
|
||||
# Exit with 2, so a caller can distinguish from other errors
|
||||
sys.exit(2)
|
||||
|
||||
# Delay retries to < 20 Hz to prevent spin-lock from hogging the CPU
|
||||
time.sleep(0.05)
|
||||
|
||||
# CTRL+C aborts the upload/spin-lock by interrupt mechanics
|
||||
except KeyboardInterrupt:
|
||||
print("\n Upload aborted by user.")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
|
||||
@@ -144,6 +144,8 @@ def main():
|
||||
help='number of tidy instances to be run in parallel.')
|
||||
parser.add_argument('files', nargs='*', default=['.*'],
|
||||
help='files to be processed (regex on path)')
|
||||
parser.add_argument('-exclude', dest='exclude', default=None,
|
||||
help='regular expression matching files to exclude')
|
||||
parser.add_argument('-fix', action='store_true', help='apply fix-its')
|
||||
parser.add_argument('-format', action='store_true', help='Reformat code '
|
||||
'after applying fixes')
|
||||
@@ -192,6 +194,7 @@ def main():
|
||||
|
||||
# Build up a big regexy filter from all command line arguments.
|
||||
file_name_re = re.compile('(' + ')|('.join(args.files) + ')')
|
||||
exclude_re = re.compile(args.exclude) if args.exclude else None
|
||||
|
||||
try:
|
||||
# Spin up a bunch of tidy-launching threads.
|
||||
@@ -205,6 +208,8 @@ def main():
|
||||
# Fill the queue with files.
|
||||
for name in files:
|
||||
if file_name_re.search(name):
|
||||
if exclude_re and exclude_re.search(name):
|
||||
continue
|
||||
queue.put(name)
|
||||
|
||||
# Wait for all threads to be done.
|
||||
|
||||
@@ -4,7 +4,7 @@ GREEN='\033[0;32m'
|
||||
NO_COLOR='\033[0m' # No Color
|
||||
SCRIPTID="${GREEN}[docker-entrypoint.sh]${NO_COLOR}"
|
||||
|
||||
echo -e "$SCRIPTID Starting"
|
||||
echo -e "$SCRIPTID $( uname -m ) | $(date -u +%FT%TZ)"
|
||||
|
||||
# Start virtual X server in the background
|
||||
# - DISPLAY default is :99, set in dockerfile
|
||||
@@ -22,6 +22,4 @@ if [ -n "${ROS_DISTRO}" ]; then
|
||||
source "/opt/ros/$ROS_DISTRO/setup.bash"
|
||||
fi
|
||||
|
||||
echo -e "$SCRIPTID ($( uname -m ))"
|
||||
|
||||
exec "$@"
|
||||
|
||||
@@ -74,7 +74,7 @@ python3 -m pip install --user -r ${DIR}/requirements.txt
|
||||
|
||||
# Optional, but recommended additional simulation tools:
|
||||
if [[ $INSTALL_SIM == "--sim-tools" ]]; then
|
||||
if brew ls --versions px4-sim > /dev/null; then
|
||||
if ! brew ls --versions px4-sim > /dev/null; then
|
||||
brew install px4-sim
|
||||
elif [[ $REINSTALL_FORMULAS == "--reinstall" ]]; then
|
||||
brew reinstall px4-sim
|
||||
|
||||
+21
-33
@@ -6,9 +6,9 @@ set -e
|
||||
## Can also be used in docker.
|
||||
##
|
||||
## Installs:
|
||||
## - Common dependencies and tools for nuttx, jMAVSim, Gazebo
|
||||
## - Common dependencies and tools for nuttx, Gazebo
|
||||
## - NuttX toolchain (omit with arg: --no-nuttx)
|
||||
## - jMAVSim and Gazebo9 simulator (omit with arg: --no-sim-tools)
|
||||
## - Gazebo Harmonic simulator (omit with arg: --no-sim-tools)
|
||||
##
|
||||
|
||||
INSTALL_NUTTX="true"
|
||||
@@ -176,8 +176,10 @@ if [[ $INSTALL_NUTTX == "true" ]]; then
|
||||
|
||||
echo
|
||||
echo "Fetching Xtensa compilers"
|
||||
wget -q -P $DIR https://github.com/espressif/crosstool-NG/releases/download/esp-13.2.0_20240530/xtensa-esp-elf-13.2.0_20240530-x86_64-linux-gnu.tar.xz
|
||||
sudo tar -xf $DIR/xtensa-esp-elf-13.2.0_20240530-x86_64-linux-gnu.tar.xz -C /opt
|
||||
XTENSA_FILE_NAME=xtensa-esp-elf-13.2.0_20240530-x86_64-linux-gnu.tar.xz
|
||||
wget -q -P $DIR https://github.com/espressif/crosstool-NG/releases/download/esp-13.2.0_20240530/$XTENSA_FILE_NAME
|
||||
sudo tar -xf $DIR/$XTENSA_FILE_NAME -C /opt
|
||||
rm $DIR/$XTENSA_FILE_NAME
|
||||
echo 'export PATH=$PATH:/opt/xtensa-esp-elf/bin/' >> /home/$USER/.bashrc
|
||||
fi
|
||||
|
||||
@@ -194,6 +196,11 @@ if [[ $INSTALL_NUTTX == "true" ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${UBUNTU_RELEASE}" == "25.10" ]]; then
|
||||
echo "[ubuntu.sh] Gazebo binaries are not available for 25.10, skipping installation"
|
||||
INSTALL_SIM="false"
|
||||
fi
|
||||
|
||||
# Simulation tools
|
||||
if [[ $INSTALL_SIM == "true" ]]; then
|
||||
|
||||
@@ -205,37 +212,18 @@ if [[ $INSTALL_SIM == "true" ]]; then
|
||||
bc \
|
||||
;
|
||||
|
||||
# Gazebo / Gazebo classic installation
|
||||
if [[ "${UBUNTU_RELEASE}" == "18.04" || "${UBUNTU_RELEASE}" == "20.04" ]]; then
|
||||
sudo sh -c 'echo "deb http://packages.osrfoundation.org/gazebo/ubuntu-stable `lsb_release -cs` main" > /etc/apt/sources.list.d/gazebo-stable.list'
|
||||
wget http://packages.osrfoundation.org/gazebo.key -O - | sudo apt-key add -
|
||||
# Update list, since new gazebo-stable.list has been added
|
||||
sudo apt-get update -y --quiet
|
||||
# Gazebo Harmonic installation (Ubuntu 22.04+)
|
||||
echo "[ubuntu.sh] Gazebo (Harmonic) will be installed"
|
||||
# Add Gazebo binary repository
|
||||
sudo wget https://packages.osrfoundation.org/gazebo.gpg -O /usr/share/keyrings/pkgs-osrf-archive-keyring.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/pkgs-osrf-archive-keyring.gpg] http://packages.osrfoundation.org/gazebo/ubuntu-stable $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/gazebo-stable.list > /dev/null
|
||||
sudo apt-get update -y --quiet
|
||||
|
||||
# Install Gazebo classic
|
||||
if [[ "${UBUNTU_RELEASE}" == "18.04" ]]; then
|
||||
gazebo_classic_version=9
|
||||
gazebo_packages="gazebo$gazebo_classic_version libgazebo$gazebo_classic_version-dev"
|
||||
else
|
||||
# default and Ubuntu 20.04
|
||||
gazebo_classic_version=11
|
||||
gazebo_packages="gazebo$gazebo_classic_version libgazebo$gazebo_classic_version-dev"
|
||||
fi
|
||||
else
|
||||
# Expects Ubuntu 22.04 > by default
|
||||
echo "[ubuntu.sh] Gazebo (Harmonic) will be installed"
|
||||
echo "[ubuntu.sh] Earlier versions will be removed"
|
||||
# Add Gazebo binary repository
|
||||
sudo wget https://packages.osrfoundation.org/gazebo.gpg -O /usr/share/keyrings/pkgs-osrf-archive-keyring.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/pkgs-osrf-archive-keyring.gpg] http://packages.osrfoundation.org/gazebo/ubuntu-stable $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/gazebo-stable.list > /dev/null
|
||||
sudo apt-get update -y --quiet
|
||||
# Install Gazebo
|
||||
gazebo_packages="gz-harmonic libunwind-dev"
|
||||
|
||||
# Install Gazebo
|
||||
gazebo_packages="gz-harmonic libunwind-dev"
|
||||
|
||||
if [[ "${UBUNTU_RELEASE}" == "24.04" ]]; then
|
||||
gazebo_packages="$gazebo_packages cppzmq-dev"
|
||||
fi
|
||||
if [[ "${UBUNTU_RELEASE}" == "24.04" ]]; then
|
||||
gazebo_packages="$gazebo_packages cppzmq-dev"
|
||||
fi
|
||||
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get -y --quiet --no-install-recommends install \
|
||||
|
||||
Executable
+142
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Collect ULog files from SIH swarm simulation instances.
|
||||
|
||||
Gathers .ulg files from per-instance log directories and copies them
|
||||
into a single output folder with filenames that identify the source instance.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def find_repo_root() -> Path:
|
||||
"""Walk up from script location to find the repository root."""
|
||||
path = Path(__file__).resolve().parent
|
||||
while path != path.parent:
|
||||
if (path / "ROMFS").is_dir() and (path / "src").is_dir():
|
||||
return path
|
||||
path = path.parent
|
||||
return Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
|
||||
def parse_instance_number(filepath: str) -> int | None:
|
||||
"""Extract the instance number from a path containing instance_N."""
|
||||
parts = Path(filepath).parts
|
||||
for part in parts:
|
||||
if part.startswith("instance_"):
|
||||
try:
|
||||
return int(part.split("_", 1)[1])
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def collect_ulogs(build_dir: Path, output_dir: Path, max_instances: int | None) -> None:
|
||||
"""Find and copy ULog files from swarm instance directories."""
|
||||
pattern = str(build_dir / "instance_*" / "log" / "**" / "*.ulg")
|
||||
ulog_files = sorted(glob.glob(pattern, recursive=True))
|
||||
|
||||
if not ulog_files:
|
||||
print(f"No ULog files found matching: {pattern}")
|
||||
print()
|
||||
print("Troubleshooting:")
|
||||
print(f" - Verify build directory exists: {build_dir}")
|
||||
print(f" - Check for instance_* subdirectories in {build_dir}")
|
||||
print(" - Ensure the swarm simulation has run and produced logs")
|
||||
sys.exit(1)
|
||||
|
||||
# Filter by instance count if specified.
|
||||
if max_instances is not None:
|
||||
ulog_files = [
|
||||
f for f in ulog_files
|
||||
if (n := parse_instance_number(f)) is not None and n < max_instances
|
||||
]
|
||||
|
||||
if not ulog_files:
|
||||
assert max_instances is not None
|
||||
print(f"No ULog files found for the requested instance range (0..{max_instances - 1}).")
|
||||
sys.exit(1)
|
||||
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
total_size = 0
|
||||
instances_seen: set[int] = set()
|
||||
copied = 0
|
||||
|
||||
for src in ulog_files:
|
||||
instance = parse_instance_number(src)
|
||||
if instance is None:
|
||||
print(f" skipping (could not determine instance): {src}")
|
||||
continue
|
||||
|
||||
instances_seen.add(instance)
|
||||
original_name = Path(src).name
|
||||
dest_name = f"drone_{instance:02d}_{original_name}"
|
||||
dest_path = output_dir / dest_name
|
||||
|
||||
shutil.copy2(src, dest_path)
|
||||
file_size = os.path.getsize(src)
|
||||
total_size += file_size
|
||||
copied += 1
|
||||
print(f" [{instance:02d}] {original_name} ({file_size / 1024:.1f} KB)")
|
||||
|
||||
print()
|
||||
print("Summary")
|
||||
print("-" * 40)
|
||||
print(f" Files collected : {copied}")
|
||||
print(f" Instances : {sorted(instances_seen)}")
|
||||
print(f" Total size : {total_size / (1024 * 1024):.2f} MB")
|
||||
print(f" Output directory: {output_dir.resolve()}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
repo_root = find_repo_root()
|
||||
default_build = str(repo_root / "build" / "px4_sitl_sih")
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Collect ULog files from SIH swarm simulation instances.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--build-dir",
|
||||
default=default_build,
|
||||
help=f"Path to the build directory (default: {default_build})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
default=None,
|
||||
help="Output directory (default: swarm_ulogs_YYYYMMDD_HHMMSS)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--instances",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Number of instances to collect from (default: auto-detect)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
build_dir = Path(args.build_dir)
|
||||
if not build_dir.is_dir():
|
||||
print(f"Error: build directory does not exist: {build_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
if args.output_dir is not None:
|
||||
output_dir = Path(args.output_dir)
|
||||
else:
|
||||
output_dir = Path(f"swarm_ulogs_{timestamp}")
|
||||
|
||||
print(f"Collecting ULog files from: {build_dir}")
|
||||
print(f"Output directory: {output_dir}")
|
||||
print()
|
||||
|
||||
collect_ulogs(build_dir, output_dir, args.instances)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Submodule Tools/simulation/gazebo-classic/sitl_gazebo-classic updated: ac718b6fb2...5b6966ed57
Executable
+487
@@ -0,0 +1,487 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate synthetic ULog files for testing mavsim-viewer swarm replay.
|
||||
|
||||
Creates N drone ULog files with crafted trajectories in valid ULog binary
|
||||
format, without requiring any real PX4 simulation.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import struct
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ULog binary writer
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ULOG_MAGIC = b'\x55\x4c\x6f\x67\x01\x12\x35'
|
||||
ULOG_VERSION = 1
|
||||
|
||||
|
||||
class ULogWriter:
|
||||
"""Write a minimal but valid ULog binary file."""
|
||||
|
||||
def __init__(self, path: str, timestamp_us: int = 0):
|
||||
self._path = path
|
||||
self._fp = open(path, 'wb')
|
||||
self._msg_id_counter = 0
|
||||
self._subscriptions: dict[str, int] = {} # topic_name -> msg_id
|
||||
self._write_header(timestamp_us)
|
||||
self._write_flag_bits()
|
||||
|
||||
# -- low-level helpers --------------------------------------------------
|
||||
|
||||
def _write_raw(self, data: bytes):
|
||||
self._fp.write(data)
|
||||
|
||||
def _write_message(self, msg_type: int, payload: bytes):
|
||||
"""Write a ULog message: uint16 size + uint8 type + payload."""
|
||||
self._write_raw(struct.pack('<HB', len(payload), msg_type))
|
||||
self._write_raw(payload)
|
||||
|
||||
def _write_header(self, timestamp_us: int):
|
||||
self._write_raw(ULOG_MAGIC)
|
||||
self._write_raw(struct.pack('<BQ', ULOG_VERSION, timestamp_us))
|
||||
|
||||
def _write_flag_bits(self):
|
||||
# 'B' message: 8 compat + 8 incompat + 3*uint64 appended offsets
|
||||
payload = b'\x00' * 8 + b'\x00' * 8 + struct.pack('<QQQ', 0, 0, 0)
|
||||
self._write_message(0x42, payload)
|
||||
|
||||
# -- definition messages ------------------------------------------------
|
||||
|
||||
def write_format(self, fmt_string: str):
|
||||
"""Write a Format ('F') definition message.
|
||||
|
||||
fmt_string example: "vehicle_local_position:uint64_t timestamp;float x;float y"
|
||||
"""
|
||||
payload = fmt_string.encode('ascii')
|
||||
self._write_message(0x46, payload)
|
||||
|
||||
def write_info(self, key: str, value: str):
|
||||
"""Write an Info ('I') message with a string value."""
|
||||
key_with_type = f"char[{len(value)}] {key}"
|
||||
key_bytes = key_with_type.encode('ascii')
|
||||
value_bytes = value.encode('ascii')
|
||||
payload = struct.pack('<B', len(key_bytes)) + key_bytes + value_bytes
|
||||
self._write_message(0x49, payload)
|
||||
|
||||
def add_subscription(self, topic_name: str, multi_id: int = 0) -> int:
|
||||
"""Write an AddSubscription ('A') message. Returns the assigned msg_id."""
|
||||
msg_id = self._msg_id_counter
|
||||
self._msg_id_counter += 1
|
||||
self._subscriptions[topic_name] = msg_id
|
||||
name_bytes = topic_name.encode('ascii')
|
||||
payload = struct.pack('<BH', multi_id, msg_id) + name_bytes
|
||||
self._write_message(0x41, payload)
|
||||
return msg_id
|
||||
|
||||
def write_data(self, topic_name: str, data_bytes: bytes):
|
||||
"""Write a Data ('D') message for a subscribed topic.
|
||||
|
||||
data_bytes must be the fully serialized payload (starting with
|
||||
uint64 timestamp) matching the format definition.
|
||||
"""
|
||||
msg_id = self._subscriptions[topic_name]
|
||||
payload = struct.pack('<H', msg_id) + data_bytes
|
||||
self._write_message(0x44, payload)
|
||||
|
||||
def close(self):
|
||||
self._fp.close()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Topic serialization helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def pack_vehicle_local_position(timestamp_us: int, x: float, y: float, z: float,
|
||||
vx: float, vy: float, vz: float,
|
||||
ref_lat: float, ref_lon: float, ref_alt: float) -> bytes:
|
||||
return struct.pack('<Qffffffddf',
|
||||
timestamp_us, x, y, z, vx, vy, vz,
|
||||
ref_lat, ref_lon, ref_alt)
|
||||
|
||||
|
||||
def pack_vehicle_attitude(timestamp_us: int, q: tuple[float, ...]) -> bytes:
|
||||
return struct.pack('<Qffff', timestamp_us, q[0], q[1], q[2], q[3])
|
||||
|
||||
|
||||
def pack_vehicle_global_position(timestamp_us: int, lat: float, lon: float, alt: float) -> bytes:
|
||||
return struct.pack('<Qddf', timestamp_us, lat, lon, alt)
|
||||
|
||||
|
||||
def pack_vehicle_status(timestamp_us: int, vehicle_type: int, nav_state: int) -> bytes:
|
||||
return struct.pack('<QBB', timestamp_us, vehicle_type, nav_state)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Format definition strings (must match pack functions above)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
FMT_LOCAL_POS = (
|
||||
"vehicle_local_position:"
|
||||
"uint64_t timestamp;"
|
||||
"float x;float y;float z;"
|
||||
"float vx;float vy;float vz;"
|
||||
"double ref_lat;double ref_lon;float ref_alt"
|
||||
)
|
||||
|
||||
FMT_ATTITUDE = (
|
||||
"vehicle_attitude:"
|
||||
"uint64_t timestamp;"
|
||||
"float[4] q"
|
||||
)
|
||||
|
||||
FMT_GLOBAL_POS = (
|
||||
"vehicle_global_position:"
|
||||
"uint64_t timestamp;"
|
||||
"double lat;double lon;float alt"
|
||||
)
|
||||
|
||||
FMT_STATUS = (
|
||||
"vehicle_status:"
|
||||
"uint64_t timestamp;"
|
||||
"uint8_t vehicle_type;uint8_t nav_state"
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Trajectory math
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
REF_LAT = 47.397742
|
||||
REF_LON = 8.545594
|
||||
REF_ALT = 488.0
|
||||
|
||||
NAV_TAKEOFF = 2
|
||||
NAV_OFFBOARD = 14
|
||||
NAV_LAND = 18
|
||||
|
||||
|
||||
def quat_from_yaw(yaw: float) -> tuple[float, float, float, float]:
|
||||
return (math.cos(yaw / 2.0), 0.0, 0.0, math.sin(yaw / 2.0))
|
||||
|
||||
|
||||
def smooth_interp(t: float) -> float:
|
||||
"""Cosine interpolation factor 0..1."""
|
||||
return 0.5 - 0.5 * math.cos(math.pi * max(0.0, min(1.0, t)))
|
||||
|
||||
|
||||
def grid_position(drone_id: int) -> tuple[float, float]:
|
||||
"""4x4 grid, 3m spacing, centered on origin."""
|
||||
row = drone_id // 4
|
||||
col = drone_id % 4
|
||||
x = (row - 1.5) * 3.0
|
||||
y = (col - 1.5) * 3.0
|
||||
return x, y
|
||||
|
||||
|
||||
def circle_position(drone_id: int, num_drones: int, radius: float = 30.0) -> tuple[float, float]:
|
||||
angle = 2.0 * math.pi * drone_id / num_drones
|
||||
return radius * math.cos(angle), radius * math.sin(angle)
|
||||
|
||||
|
||||
def local_to_global(x: float, y: float, z: float) -> tuple[float, float, float]:
|
||||
lat = REF_LAT + (x / 111320.0)
|
||||
lon = REF_LON + (y / (111320.0 * math.cos(math.radians(REF_LAT))))
|
||||
alt = REF_ALT - z
|
||||
return lat, lon, alt
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase computations
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
TARGET = (200.0, 0.0, 0.0) # NED target for kamikaze
|
||||
|
||||
|
||||
def compute_state(drone_id: int, t: float, num_drones: int, _duration: float):
|
||||
"""Return (x, y, z, vx, vy, vz, yaw, nav_state) for a drone at time t.
|
||||
|
||||
Returns None if the drone hasn't started yet or has impacted.
|
||||
"""
|
||||
start_time = drone_id * 0.5 # staggered start
|
||||
if t < start_time:
|
||||
return None
|
||||
|
||||
# Failure drone 12 -- crash at t=70, log ends
|
||||
if drone_id == 12 and t > 70.0:
|
||||
return None
|
||||
|
||||
# Default nav state
|
||||
nav_state = NAV_OFFBOARD
|
||||
yaw = 0.0
|
||||
vx, vy, vz = 0.0, 0.0, 0.0
|
||||
|
||||
gx, gy = grid_position(drone_id)
|
||||
cx, cy = circle_position(drone_id, num_drones)
|
||||
|
||||
if t < 10.0:
|
||||
# Phase 1: staggered start, on ground rising slightly
|
||||
frac = smooth_interp((t - start_time) / max(0.01, 10.0 - start_time))
|
||||
x, y = gx * frac, gy * frac
|
||||
z = 0.0
|
||||
nav_state = NAV_TAKEOFF
|
||||
|
||||
elif t < 25.0:
|
||||
# Phase 2: grid takeoff, ascend to -20 NED
|
||||
frac = smooth_interp((t - 10.0) / 15.0)
|
||||
x, y = gx, gy
|
||||
z = -20.0 * frac
|
||||
vz = -20.0 / 15.0 if frac < 1.0 else 0.0
|
||||
nav_state = NAV_TAKEOFF
|
||||
|
||||
elif t < 40.0:
|
||||
# Phase 3: grid -> ring morph
|
||||
frac = smooth_interp((t - 25.0) / 15.0)
|
||||
x = gx + (cx - gx) * frac
|
||||
y = gy + (cy - gy) * frac
|
||||
z = -20.0
|
||||
vx = (cx - gx) / 15.0
|
||||
vy = (cy - gy) / 15.0
|
||||
nav_state = NAV_OFFBOARD
|
||||
|
||||
elif t < 55.0:
|
||||
# Phase 4: ring cruise CW at 3 m/s
|
||||
angular_speed = 3.0 / 30.0 # omega = v/r
|
||||
base_angle = 2.0 * math.pi * drone_id / num_drones
|
||||
angle = base_angle - angular_speed * (t - 40.0) # CW -> negative
|
||||
x = 30.0 * math.cos(angle)
|
||||
y = 30.0 * math.sin(angle)
|
||||
z = -20.0
|
||||
vx = 30.0 * angular_speed * math.sin(angle)
|
||||
vy = -30.0 * angular_speed * math.cos(angle)
|
||||
yaw = math.atan2(-y, -x) # face center
|
||||
nav_state = NAV_OFFBOARD
|
||||
|
||||
else:
|
||||
# Phase 5/6: Kamikaze dive toward target
|
||||
# Starting position at t=55
|
||||
base_angle = 2.0 * math.pi * drone_id / num_drones
|
||||
angular_speed = 3.0 / 30.0
|
||||
angle55 = base_angle - angular_speed * 15.0
|
||||
start_x = 30.0 * math.cos(angle55)
|
||||
start_y = 30.0 * math.sin(angle55)
|
||||
start_z = -20.0
|
||||
|
||||
dx = TARGET[0] - start_x
|
||||
dy = TARGET[1] - start_y
|
||||
dz = TARGET[2] - start_z
|
||||
dist = math.sqrt(dx * dx + dy * dy + dz * dz)
|
||||
speed = 12.0 # m/s max
|
||||
nx, ny, nz = dx / dist, dy / dist, dz / dist
|
||||
|
||||
dt = t - 55.0
|
||||
x = start_x + nx * speed * dt
|
||||
y = start_y + ny * speed * dt
|
||||
z = start_z + nz * speed * dt
|
||||
vx = nx * speed
|
||||
vy = ny * speed
|
||||
vz = nz * speed
|
||||
yaw = math.atan2(ny, nx)
|
||||
nav_state = NAV_LAND
|
||||
|
||||
# Impact: close to target or above ground
|
||||
cur_dist = math.sqrt((TARGET[0] - x) ** 2 + (TARGET[1] - y) ** 2 + (TARGET[2] - z) ** 2)
|
||||
if cur_dist < 2.0 or z > -1.0:
|
||||
return None
|
||||
|
||||
# Drone 12 failure behavior (attitude wobble from t=60)
|
||||
if drone_id == 12 and t >= 60.0:
|
||||
wobble_t = t - 60.0
|
||||
wobble_amp = min(wobble_t * 0.1, 0.8) # growing oscillation
|
||||
roll = wobble_amp * math.sin(wobble_t * 5.0)
|
||||
pitch = wobble_amp * math.cos(wobble_t * 4.0)
|
||||
# drift sideways
|
||||
x += wobble_t * 2.0
|
||||
y += wobble_t * 1.5
|
||||
z += wobble_t * 0.5 # descending
|
||||
# Build quaternion with roll/pitch/yaw
|
||||
cr, sr = math.cos(roll / 2), math.sin(roll / 2)
|
||||
cp, sp = math.cos(pitch / 2), math.sin(pitch / 2)
|
||||
cy, sy = math.cos(yaw / 2), math.sin(yaw / 2)
|
||||
q = (
|
||||
cr * cp * cy + sr * sp * sy,
|
||||
sr * cp * cy - cr * sp * sy,
|
||||
cr * sp * cy + sr * cp * sy,
|
||||
cr * cp * sy - sr * sp * cy,
|
||||
)
|
||||
return (x, y, z, vx, vy, vz, q, nav_state)
|
||||
|
||||
return (x, y, z, vx, vy, vz, quat_from_yaw(yaw), nav_state)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Generate a single drone ULog
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def generate_drone_ulog(drone_id: int, num_drones: int, duration: float,
|
||||
output_dir: str, scenario: str):
|
||||
filename = os.path.join(output_dir, f"drone_{drone_id:02d}.ulg")
|
||||
start_us = drone_id * 500_000 # stagger by 0.5s in timestamps
|
||||
|
||||
writer = ULogWriter(filename, timestamp_us=start_us)
|
||||
|
||||
# Write format definitions
|
||||
writer.write_format(FMT_LOCAL_POS)
|
||||
writer.write_format(FMT_ATTITUDE)
|
||||
writer.write_format(FMT_GLOBAL_POS)
|
||||
writer.write_format(FMT_STATUS)
|
||||
|
||||
# Write info messages
|
||||
writer.write_info("sys_name", "PX4")
|
||||
writer.write_info("ver_hw", "SITL")
|
||||
writer.write_info("scenario", scenario)
|
||||
writer.write_info("drone_id", str(drone_id))
|
||||
|
||||
# Add subscriptions
|
||||
writer.add_subscription("vehicle_local_position")
|
||||
writer.add_subscription("vehicle_attitude")
|
||||
writer.add_subscription("vehicle_global_position")
|
||||
writer.add_subscription("vehicle_status")
|
||||
|
||||
# Determine rates per drone (edge cases)
|
||||
pos_dt = 0.02 # 50 Hz
|
||||
att_dt = 0.01 # 100 Hz
|
||||
if drone_id == 13:
|
||||
pos_dt = 0.1 # 10 Hz low rate
|
||||
if drone_id == 14:
|
||||
att_dt = 0.005 # 200 Hz high rate
|
||||
|
||||
# Generate data
|
||||
# We iterate at the finest resolution needed and emit when due
|
||||
fine_dt = min(pos_dt, att_dt, 0.001)
|
||||
# Use the finest rate needed for this drone
|
||||
fine_dt = min(pos_dt, att_dt)
|
||||
|
||||
status_interval = 1.0 # 1 Hz
|
||||
next_pos_t = 0.0
|
||||
next_att_t = 0.0
|
||||
next_status_t = 0.0
|
||||
last_nav_state = -1
|
||||
|
||||
t = 0.0
|
||||
samples_written = 0
|
||||
while t <= duration:
|
||||
state = compute_state(drone_id, t, num_drones, duration)
|
||||
|
||||
# Drone 15 data gap at t=50-52
|
||||
in_gap = (drone_id == 15 and 50.0 <= t <= 52.0)
|
||||
|
||||
if state is not None and not in_gap:
|
||||
x, y, z, vx, vy, vz, q, nav_state = state
|
||||
ts = int(t * 1_000_000)
|
||||
|
||||
# Position data
|
||||
if t >= next_pos_t:
|
||||
writer.write_data("vehicle_local_position",
|
||||
pack_vehicle_local_position(
|
||||
ts, x, y, z, vx, vy, vz,
|
||||
REF_LAT, REF_LON, REF_ALT))
|
||||
lat, lon, alt = local_to_global(x, y, z)
|
||||
writer.write_data("vehicle_global_position",
|
||||
pack_vehicle_global_position(ts, lat, lon, alt))
|
||||
next_pos_t = t + pos_dt
|
||||
samples_written += 2
|
||||
|
||||
# Attitude data
|
||||
if t >= next_att_t:
|
||||
writer.write_data("vehicle_attitude",
|
||||
pack_vehicle_attitude(ts, q))
|
||||
next_att_t = t + att_dt
|
||||
samples_written += 1
|
||||
|
||||
# Status data
|
||||
if t >= next_status_t or nav_state != last_nav_state:
|
||||
writer.write_data("vehicle_status",
|
||||
pack_vehicle_status(ts, 2, nav_state))
|
||||
next_status_t = t + status_interval
|
||||
last_nav_state = nav_state
|
||||
samples_written += 1
|
||||
|
||||
t += fine_dt
|
||||
|
||||
writer.close()
|
||||
return samples_written
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scenario metadata
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def write_scenario_json(output_dir: str, scenario: str, num_drones: int, duration: float):
|
||||
meta = {
|
||||
"scenario": scenario,
|
||||
"num_drones": num_drones,
|
||||
"duration_s": duration,
|
||||
"origin": {
|
||||
"lat": REF_LAT,
|
||||
"lon": REF_LON,
|
||||
"alt": REF_ALT,
|
||||
},
|
||||
"phases": [
|
||||
{"name": "staggered_start", "start_s": 0, "end_s": 10},
|
||||
{"name": "grid_takeoff", "start_s": 10, "end_s": 25},
|
||||
{"name": "grid_to_ring", "start_s": 25, "end_s": 40},
|
||||
{"name": "ring_cruise", "start_s": 40, "end_s": 55},
|
||||
{"name": "kamikaze_dive", "start_s": 55, "end_s": 75},
|
||||
],
|
||||
"edge_cases": {
|
||||
"drone_12": "failure at t=60, attitude oscillation, crash at t=70",
|
||||
"drone_13": "low-rate position data (10Hz)",
|
||||
"drone_14": "high-rate attitude data (200Hz)",
|
||||
"drone_15": "2-second data gap at t=50-52",
|
||||
},
|
||||
"files": [f"drone_{i:02d}.ulg" for i in range(num_drones)],
|
||||
}
|
||||
path = os.path.join(output_dir, "scenario.json")
|
||||
with open(path, 'w') as f:
|
||||
json.dump(meta, f, indent=2)
|
||||
print(f" Wrote {path}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate synthetic ULog files for swarm replay testing.")
|
||||
parser.add_argument("--output-dir", default="synthetic_swarm_ulogs",
|
||||
help="Output directory (default: synthetic_swarm_ulogs)")
|
||||
parser.add_argument("--num-drones", type=int, default=16,
|
||||
help="Number of drones (default: 16)")
|
||||
parser.add_argument("--duration", type=float, default=90.0,
|
||||
help="Scenario duration in seconds (default: 90)")
|
||||
parser.add_argument("--scenario", default="hawk_descent",
|
||||
help="Scenario name (default: hawk_descent)")
|
||||
args = parser.parse_args()
|
||||
|
||||
os.makedirs(args.output_dir, exist_ok=True)
|
||||
print(f"Generating {args.num_drones} drone ULog files for scenario '{args.scenario}'")
|
||||
print(f" Duration: {args.duration}s")
|
||||
print(f" Output: {os.path.abspath(args.output_dir)}/")
|
||||
print()
|
||||
|
||||
for i in range(args.num_drones):
|
||||
samples = generate_drone_ulog(i, args.num_drones, args.duration,
|
||||
args.output_dir, args.scenario)
|
||||
tag = ""
|
||||
if i == 12:
|
||||
tag = " [failure drone]"
|
||||
elif i == 13:
|
||||
tag = " [low-rate pos]"
|
||||
elif i == 14:
|
||||
tag = " [high-rate att]"
|
||||
elif i == 15:
|
||||
tag = " [data gap]"
|
||||
print(f" drone_{i:02d}.ulg ({samples} messages){tag}")
|
||||
|
||||
write_scenario_json(args.output_dir, args.scenario, args.num_drones, args.duration)
|
||||
print()
|
||||
print(f"Done. {args.num_drones} ULog files written to {os.path.abspath(args.output_dir)}/")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+1
-1
Submodule Tools/simulation/gz updated: 947f75b25d...3eb05f716a
Submodule Tools/simulation/jmavsim/jMAVSim updated: 66b764ada5...665b26d62d
Executable
+141
@@ -0,0 +1,141 @@
|
||||
#!/bin/bash
|
||||
# Launch N SIH quadrotor instances with XRCE-DDS agent.
|
||||
# Usage: ./sih_swarm_run.sh [CONFIG_YAML] [SPEED_FACTOR]
|
||||
#
|
||||
# Requires: build/px4_sitl_sih (make px4_sitl_sih sihsim_quadx_vision)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ulimit -n 4096
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
src_path="$SCRIPT_DIR/../../"
|
||||
build_path="${src_path}/build/px4_sitl_sih"
|
||||
|
||||
CONFIG_YAML="${1:-${SCRIPT_DIR}/swarm_formation.yaml}"
|
||||
SPEED_FACTOR="${2:-1}"
|
||||
|
||||
if [ ! -f "$CONFIG_YAML" ]; then
|
||||
echo "ERROR: config not found: $CONFIG_YAML"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$build_path" ]; then
|
||||
echo "ERROR: build directory not found: $build_path"
|
||||
echo "Run: make px4_sitl_sih"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Parse YAML config via Python ---
|
||||
eval "$(python3 -c "
|
||||
import yaml, sys
|
||||
with open('${CONFIG_YAML}') as f:
|
||||
cfg = yaml.safe_load(f)
|
||||
origin = cfg['origin']
|
||||
print(f\"ORIGIN_LAT={origin['lat']}\")
|
||||
print(f\"ORIGIN_LON={origin['lon']}\")
|
||||
print(f\"ORIGIN_ALT={origin['alt']}\")
|
||||
target = cfg['target']
|
||||
print(f\"TARGET_X={target['x']}\")
|
||||
print(f\"TARGET_Y={target['y']}\")
|
||||
print(f\"TARGET_Z={target['z']}\")
|
||||
drones = cfg['formation']
|
||||
print(f\"DRONE_COUNT={len(drones)}\")
|
||||
for i, d in enumerate(drones):
|
||||
print(f\"DRONE_{i}_ID={d['id']}\")
|
||||
print(f\"DRONE_{i}_X={d['x']}\")
|
||||
print(f\"DRONE_{i}_Y={d['y']}\")
|
||||
print(f\"DRONE_{i}_Z={d['z']}\")
|
||||
")"
|
||||
|
||||
echo "Swarm config: $DRONE_COUNT drones, origin ($ORIGIN_LAT, $ORIGIN_LON, ${ORIGIN_ALT}m)"
|
||||
echo "Target: ($TARGET_X, $TARGET_Y, $TARGET_Z)"
|
||||
echo "Speed factor: $SPEED_FACTOR"
|
||||
echo ""
|
||||
|
||||
# --- Cleanup ---
|
||||
CHILD_PIDS=()
|
||||
AGENT_PID=""
|
||||
|
||||
cleanup() {
|
||||
echo ""
|
||||
echo "Shutting down..."
|
||||
for pid in "${CHILD_PIDS[@]}"; do
|
||||
kill "$pid" 2>/dev/null || true
|
||||
done
|
||||
if [ -n "$AGENT_PID" ]; then
|
||||
kill "$AGENT_PID" 2>/dev/null || true
|
||||
fi
|
||||
pkill -x px4 2>/dev/null || true
|
||||
wait 2>/dev/null
|
||||
echo "All processes stopped."
|
||||
}
|
||||
|
||||
trap cleanup SIGINT SIGTERM
|
||||
|
||||
# --- Kill existing instances ---
|
||||
echo "Killing running instances..."
|
||||
pkill -x px4 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
# --- Start XRCE-DDS agent (if available) ---
|
||||
if command -v MicroXRCEAgent &>/dev/null; then
|
||||
echo "Starting MicroXRCEAgent on UDP port 8888..."
|
||||
MicroXRCEAgent udp4 -p 8888 > /dev/null 2>&1 &
|
||||
AGENT_PID=$!
|
||||
echo "XRCE-DDS agent PID: $AGENT_PID"
|
||||
else
|
||||
echo "WARNING: MicroXRCEAgent not found on host."
|
||||
echo " Start it separately (e.g. from Docker container):"
|
||||
echo " MicroXRCEAgent udp4 -p 8888"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# --- Port map ---
|
||||
printf "%-10s %-12s %-14s %-14s\n" "Instance" "MAV_SYS_ID" "DDS Namespace" "Offboard Port"
|
||||
printf "%-10s %-12s %-14s %-14s\n" "--------" "----------" "-------------" "-------------"
|
||||
n=0
|
||||
while [ $n -lt "$DRONE_COUNT" ]; do
|
||||
sysid=$((n + 1))
|
||||
printf "%-10s %-12s %-14s %-14s\n" "$n" "$sysid" "px4_${n}" "$((14540 + n))"
|
||||
n=$((n + 1))
|
||||
done
|
||||
echo ""
|
||||
|
||||
# --- Launch instances ---
|
||||
export PX4_SIM_MODEL=sihsim_quadx_vision
|
||||
|
||||
n=0
|
||||
while [ $n -lt "$DRONE_COUNT" ]; do
|
||||
eval "FX=\$DRONE_${n}_X"
|
||||
eval "FY=\$DRONE_${n}_Y"
|
||||
eval "FZ=\$DRONE_${n}_Z"
|
||||
|
||||
working_dir="$build_path/instance_$n"
|
||||
[ ! -d "$working_dir" ] && mkdir -p "$working_dir"
|
||||
|
||||
export PX4_HOME_LAT="$ORIGIN_LAT"
|
||||
export PX4_HOME_LON="$ORIGIN_LON"
|
||||
export PX4_HOME_ALT="$ORIGIN_ALT"
|
||||
export PX4_FORMATION_X="$FX"
|
||||
export PX4_FORMATION_Y="$FY"
|
||||
export PX4_FORMATION_Z="$FZ"
|
||||
export PX4_TARGET_X="$TARGET_X"
|
||||
export PX4_TARGET_Y="$TARGET_Y"
|
||||
export PX4_TARGET_Z="$TARGET_Z"
|
||||
export PX4_SIM_SPEED_FACTOR="$SPEED_FACTOR"
|
||||
# Force consistent DDS namespace for all instances (including 0)
|
||||
export PX4_UXRCE_DDS_NS="px4_$n"
|
||||
|
||||
pushd "$working_dir" &>/dev/null
|
||||
echo "Starting instance $n (formation: $FX, $FY, $FZ)"
|
||||
"$build_path/bin/px4" -i "$n" -d "$build_path/etc" >out.log 2>err.log &
|
||||
CHILD_PIDS+=($!)
|
||||
popd &>/dev/null
|
||||
|
||||
n=$((n + 1))
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "All $DRONE_COUNT instances launched. Press Ctrl-C to stop."
|
||||
wait
|
||||
@@ -0,0 +1,32 @@
|
||||
# Formation config for SIH swarm simulation
|
||||
# Used by sih_swarm_run.sh and swarm_ros2 coordinator
|
||||
|
||||
origin:
|
||||
lat: 47.397742
|
||||
lon: 8.545594
|
||||
alt: 488.0
|
||||
|
||||
formation:
|
||||
# NED offsets from origin (meters). 4x4 grid, 1m spacing, at 20m altitude
|
||||
- {id: 0, x: 0.0, y: 0.0, z: -20.0}
|
||||
- {id: 1, x: 0.0, y: 1.0, z: -20.0}
|
||||
- {id: 2, x: 0.0, y: 2.0, z: -20.0}
|
||||
- {id: 3, x: 0.0, y: 3.0, z: -20.0}
|
||||
- {id: 4, x: 1.0, y: 0.0, z: -20.0}
|
||||
- {id: 5, x: 1.0, y: 1.0, z: -20.0}
|
||||
- {id: 6, x: 1.0, y: 2.0, z: -20.0}
|
||||
- {id: 7, x: 1.0, y: 3.0, z: -20.0}
|
||||
- {id: 8, x: 2.0, y: 0.0, z: -20.0}
|
||||
- {id: 9, x: 2.0, y: 1.0, z: -20.0}
|
||||
- {id: 10, x: 2.0, y: 2.0, z: -20.0}
|
||||
- {id: 11, x: 2.0, y: 3.0, z: -20.0}
|
||||
- {id: 12, x: 3.0, y: 0.0, z: -20.0}
|
||||
- {id: 13, x: 3.0, y: 1.0, z: -20.0}
|
||||
- {id: 14, x: 3.0, y: 2.0, z: -20.0}
|
||||
- {id: 15, x: 3.0, y: 3.0, z: -20.0}
|
||||
|
||||
target:
|
||||
# Car position: 200m north of origin, ground level
|
||||
x: 200.0
|
||||
y: 0.0
|
||||
z: 0.0
|
||||
Executable
+485
@@ -0,0 +1,485 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Swarm Formation Designer - Create and preview formation YAML files for PX4 SIH swarm simulation.
|
||||
|
||||
Generates swarm_formation.yaml files with preset formation patterns (grid, circle, line,
|
||||
v, diamond, random) for use with sih_swarm_run.sh and swarm_ros2 coordinator.
|
||||
|
||||
Usage examples:
|
||||
# 16-drone grid with 2m spacing, preview in terminal
|
||||
python3 swarm_formation_designer.py --pattern grid --count 16 --spacing 2.0 --preview
|
||||
|
||||
# 8-drone circle, radius 10m, write to file
|
||||
python3 swarm_formation_designer.py --pattern circle --count 8 --radius 10 -o formation.yaml
|
||||
|
||||
# V formation with custom origin and target
|
||||
python3 swarm_formation_designer.py --pattern v --count 7 --spacing 3.0 \\
|
||||
--origin-lat 37.4 --origin-lon -122.0 --target-north 150 --target-east 50
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import random
|
||||
import sys
|
||||
from typing import List, Tuple
|
||||
|
||||
# Defaults matching PX4 SIH test site (Zurich area)
|
||||
DEFAULT_LAT = 47.397742
|
||||
DEFAULT_LON = 8.545594
|
||||
DEFAULT_ALT = 488.0
|
||||
DEFAULT_ALTITUDE = 20.0
|
||||
DEFAULT_SPACING = 1.0
|
||||
DEFAULT_RADIUS = 5.0
|
||||
DEFAULT_COUNT = 16
|
||||
DEFAULT_TARGET_NORTH = 200.0
|
||||
DEFAULT_TARGET_EAST = 0.0
|
||||
MIN_SEPARATION = 0.5 # meters, minimum allowed distance between any two drones
|
||||
|
||||
|
||||
def generate_grid(count: int, spacing: float, altitude: float) -> List[dict]:
|
||||
"""Arrange drones in a square grid pattern (ceil(sqrt(count)) columns)."""
|
||||
cols = math.ceil(math.sqrt(count))
|
||||
positions = []
|
||||
for i in range(count):
|
||||
row = i // cols
|
||||
col = i % cols
|
||||
positions.append({
|
||||
"id": i,
|
||||
"x": round(row * spacing, 2),
|
||||
"y": round(col * spacing, 2),
|
||||
"z": round(-altitude, 2),
|
||||
})
|
||||
return positions
|
||||
|
||||
|
||||
def generate_circle(count: int, radius: float, altitude: float) -> List[dict]:
|
||||
"""Arrange drones evenly spaced around a circle."""
|
||||
positions = []
|
||||
for i in range(count):
|
||||
angle = 2.0 * math.pi * i / count
|
||||
positions.append({
|
||||
"id": i,
|
||||
"x": round(radius * math.cos(angle), 2),
|
||||
"y": round(radius * math.sin(angle), 2),
|
||||
"z": round(-altitude, 2),
|
||||
})
|
||||
return positions
|
||||
|
||||
|
||||
def generate_line(count: int, spacing: float, altitude: float) -> List[dict]:
|
||||
"""Arrange drones in a line along the east (y) axis, centered at origin."""
|
||||
positions = []
|
||||
offset = (count - 1) * spacing / 2.0
|
||||
for i in range(count):
|
||||
positions.append({
|
||||
"id": i,
|
||||
"x": 0.0,
|
||||
"y": round(i * spacing - offset, 2),
|
||||
"z": round(-altitude, 2),
|
||||
})
|
||||
return positions
|
||||
|
||||
|
||||
def generate_v(count: int, spacing: float, altitude: float) -> List[dict]:
|
||||
"""Classic V formation with leader at front (northernmost position).
|
||||
|
||||
Leader is id 0 at the tip. Remaining drones alternate left/right,
|
||||
each row stepping back (south) and outward (east/west).
|
||||
"""
|
||||
positions = [{"id": 0, "x": 0.0, "y": 0.0, "z": round(-altitude, 2)}]
|
||||
wing_index = 1
|
||||
for i in range(1, count):
|
||||
side = 1 if (i % 2 == 1) else -1
|
||||
row = (i + 1) // 2
|
||||
positions.append({
|
||||
"id": wing_index,
|
||||
"x": round(-row * spacing, 2), # step back (south)
|
||||
"y": round(side * row * spacing, 2), # spread east/west
|
||||
"z": round(-altitude, 2),
|
||||
})
|
||||
wing_index += 1
|
||||
return positions
|
||||
|
||||
|
||||
def generate_diamond(count: int, spacing: float, altitude: float) -> List[dict]:
|
||||
"""Diamond/rhombus formation.
|
||||
|
||||
Builds concentric diamond rings outward from the center. Ring k has 4*k
|
||||
positions (except the center which has 1). Drones fill from center outward.
|
||||
"""
|
||||
# Generate diamond coordinates ring by ring
|
||||
coords: List[Tuple[float, float]] = [(0.0, 0.0)]
|
||||
ring = 1
|
||||
while len(coords) < count:
|
||||
# Each ring has 4 sides, each side has 'ring' segments
|
||||
for side in range(4):
|
||||
for step in range(ring):
|
||||
if side == 0: # top-right edge
|
||||
x = (ring - step) * spacing
|
||||
y = step * spacing
|
||||
elif side == 1: # bottom-right edge
|
||||
x = -step * spacing
|
||||
y = (ring - step) * spacing
|
||||
elif side == 2: # bottom-left edge
|
||||
x = -(ring - step) * spacing
|
||||
y = -step * spacing
|
||||
else: # top-left edge
|
||||
x = step * spacing
|
||||
y = -(ring - step) * spacing
|
||||
coords.append((round(x, 2), round(y, 2)))
|
||||
if len(coords) >= count:
|
||||
break
|
||||
if len(coords) >= count:
|
||||
break
|
||||
ring += 1
|
||||
|
||||
positions = []
|
||||
for i in range(count):
|
||||
positions.append({
|
||||
"id": i,
|
||||
"x": coords[i][0],
|
||||
"y": coords[i][1],
|
||||
"z": round(-altitude, 2),
|
||||
})
|
||||
return positions
|
||||
|
||||
|
||||
def generate_random(count: int, spacing: float, altitude: float) -> List[dict]:
|
||||
"""Random positions within a bounding box, enforcing minimum separation.
|
||||
|
||||
The bounding box size scales with count and spacing. Uses rejection
|
||||
sampling to guarantee minimum separation between all drones.
|
||||
"""
|
||||
box_size = max(spacing * math.sqrt(count) * 2, spacing * 4)
|
||||
positions = []
|
||||
coords: List[Tuple[float, float]] = []
|
||||
max_attempts = count * 1000
|
||||
|
||||
rng = random.Random(42) # deterministic for reproducibility
|
||||
attempts = 0
|
||||
while len(coords) < count and attempts < max_attempts:
|
||||
x = round(rng.uniform(-box_size / 2, box_size / 2), 2)
|
||||
y = round(rng.uniform(-box_size / 2, box_size / 2), 2)
|
||||
too_close = False
|
||||
for cx, cy in coords:
|
||||
if math.sqrt((x - cx) ** 2 + (y - cy) ** 2) < MIN_SEPARATION:
|
||||
too_close = True
|
||||
break
|
||||
if not too_close:
|
||||
coords.append((x, y))
|
||||
attempts += 1
|
||||
|
||||
if len(coords) < count:
|
||||
print(
|
||||
f"Warning: could only place {len(coords)}/{count} drones "
|
||||
f"with minimum separation {MIN_SEPARATION}m in bounding box {box_size:.1f}m",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
for i, (x, y) in enumerate(coords):
|
||||
positions.append({
|
||||
"id": i,
|
||||
"x": x,
|
||||
"y": y,
|
||||
"z": round(-altitude, 2),
|
||||
})
|
||||
return positions
|
||||
|
||||
|
||||
PATTERN_GENERATORS = {
|
||||
"grid": generate_grid,
|
||||
"circle": generate_circle,
|
||||
"line": generate_line,
|
||||
"v": generate_v,
|
||||
"diamond": generate_diamond,
|
||||
"random": generate_random,
|
||||
}
|
||||
|
||||
|
||||
def validate_no_overlap(positions: List[dict]) -> bool:
|
||||
"""Check that no two drones occupy the same horizontal position (within MIN_SEPARATION)."""
|
||||
for i in range(len(positions)):
|
||||
for j in range(i + 1, len(positions)):
|
||||
dx = positions[i]["x"] - positions[j]["x"]
|
||||
dy = positions[i]["y"] - positions[j]["y"]
|
||||
dist = math.sqrt(dx * dx + dy * dy)
|
||||
if dist < MIN_SEPARATION:
|
||||
print(
|
||||
f"Error: drones {positions[i]['id']} and {positions[j]['id']} "
|
||||
f"are only {dist:.2f}m apart (minimum: {MIN_SEPARATION}m)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def build_formation_dict(
|
||||
positions: List[dict],
|
||||
origin_lat: float,
|
||||
origin_lon: float,
|
||||
origin_alt: float,
|
||||
target_north: float,
|
||||
target_east: float,
|
||||
) -> dict:
|
||||
"""Build the formation dictionary matching swarm_formation.yaml structure."""
|
||||
return {
|
||||
"origin": {
|
||||
"lat": origin_lat,
|
||||
"lon": origin_lon,
|
||||
"alt": origin_alt,
|
||||
},
|
||||
"formation": positions,
|
||||
"target": {
|
||||
"x": target_north,
|
||||
"y": target_east,
|
||||
"z": 0.0,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def format_yaml(data: dict) -> str:
|
||||
"""Format the formation data as YAML with flow-style formation entries.
|
||||
|
||||
Produces output matching the established swarm_formation.yaml style with
|
||||
inline dicts for formation entries and block style for origin/target.
|
||||
"""
|
||||
lines = [
|
||||
"# Formation config for SIH swarm simulation",
|
||||
"# Generated by swarm_formation_designer.py",
|
||||
"",
|
||||
"origin:",
|
||||
f" lat: {data['origin']['lat']}",
|
||||
f" lon: {data['origin']['lon']}",
|
||||
f" alt: {data['origin']['alt']}",
|
||||
"",
|
||||
"formation:",
|
||||
]
|
||||
|
||||
# Determine padding width for aligned id field
|
||||
max_id = max(p["id"] for p in data["formation"])
|
||||
id_width = len(str(max_id))
|
||||
|
||||
for pos in data["formation"]:
|
||||
id_str = str(pos["id"]).rjust(id_width)
|
||||
lines.append(
|
||||
f" - {{id: {id_str}, "
|
||||
f"x: {pos['x']}, "
|
||||
f"y: {pos['y']}, "
|
||||
f"z: {pos['z']}}}"
|
||||
)
|
||||
|
||||
lines.extend([
|
||||
"",
|
||||
"target:",
|
||||
f" x: {data['target']['x']}",
|
||||
f" y: {data['target']['y']}",
|
||||
f" z: {data['target']['z']}",
|
||||
"",
|
||||
])
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def ascii_preview(positions: List[dict], target_north: float, target_east: float) -> str:
|
||||
"""Render an ASCII top-down map showing drone positions and target.
|
||||
|
||||
North is up, East is right. Axes are labeled. The target is shown as 'T'
|
||||
and drones as their id number (or '*' if id >= 10).
|
||||
"""
|
||||
width = 60
|
||||
height = 30
|
||||
|
||||
# Collect all points (drones + target)
|
||||
all_x = [p["x"] for p in positions] + [target_north]
|
||||
all_y = [p["y"] for p in positions] + [target_east]
|
||||
|
||||
min_x, max_x = min(all_x), max(all_x)
|
||||
min_y, max_y = min(all_y), max(all_y)
|
||||
|
||||
# Add margin
|
||||
range_x = max_x - min_x if max_x != min_x else 1.0
|
||||
range_y = max_y - min_y if max_y != min_y else 1.0
|
||||
margin = 0.1
|
||||
min_x -= range_x * margin
|
||||
max_x += range_x * margin
|
||||
min_y -= range_y * margin
|
||||
max_y += range_y * margin
|
||||
range_x = max_x - min_x
|
||||
range_y = max_y - min_y
|
||||
|
||||
def to_grid(north: float, east: float) -> Tuple[int, int]:
|
||||
col = int((east - min_y) / range_y * (width - 1))
|
||||
row = int((max_x - north) / range_x * (height - 1)) # north is up
|
||||
col = max(0, min(width - 1, col))
|
||||
row = max(0, min(height - 1, row))
|
||||
return row, col
|
||||
|
||||
# Initialize grid
|
||||
grid = [[" " for _ in range(width)] for _ in range(height)]
|
||||
|
||||
# Place target
|
||||
tr, tc = to_grid(target_north, target_east)
|
||||
grid[tr][tc] = "T"
|
||||
|
||||
# Place drones (overwrite target if on same cell)
|
||||
for pos in positions:
|
||||
r, c = to_grid(pos["x"], pos["y"])
|
||||
label = str(pos["id"]) if pos["id"] < 10 else "*"
|
||||
grid[r][c] = label
|
||||
|
||||
# Build output
|
||||
out = []
|
||||
out.append(f" Formation Preview ({len(positions)} drones)")
|
||||
out.append(f" North ^ (x range: {min_x:.1f} to {max_x:.1f} m)")
|
||||
out.append(" " + "-" * (width + 2))
|
||||
for row in grid:
|
||||
out.append(" |" + "".join(row) + "|")
|
||||
out.append(" " + "-" * (width + 2))
|
||||
out.append(f" East -> (y range: {min_y:.1f} to {max_y:.1f} m)")
|
||||
out.append("")
|
||||
out.append(" Legend: 0-9 = drone id, * = drone id >= 10, T = target")
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def parse_args(argv: "List[str] | None" = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Create and preview swarm formation YAML files for PX4 SIH simulation.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=(
|
||||
"Examples:\n"
|
||||
" %(prog)s --pattern grid --count 16 --spacing 2 --preview\n"
|
||||
" %(prog)s --pattern circle --count 8 --radius 10 -o my_formation.yaml\n"
|
||||
" %(prog)s --pattern v --count 7 --spacing 3 --preview -o v_formation.yaml\n"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pattern",
|
||||
choices=list(PATTERN_GENERATORS.keys()),
|
||||
default="grid",
|
||||
help="Formation pattern (default: grid)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--count",
|
||||
type=int,
|
||||
default=DEFAULT_COUNT,
|
||||
help=f"Number of drones (default: {DEFAULT_COUNT})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--spacing",
|
||||
type=float,
|
||||
default=DEFAULT_SPACING,
|
||||
help=f"Spacing between drones in meters (default: {DEFAULT_SPACING})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--altitude",
|
||||
type=float,
|
||||
default=DEFAULT_ALTITUDE,
|
||||
help=f"Flight altitude in meters AGL (default: {DEFAULT_ALTITUDE})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--radius",
|
||||
type=float,
|
||||
default=DEFAULT_RADIUS,
|
||||
help=f"Radius for circle pattern in meters (default: {DEFAULT_RADIUS})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--origin-lat",
|
||||
type=float,
|
||||
default=DEFAULT_LAT,
|
||||
help=f"Origin latitude (default: {DEFAULT_LAT})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--origin-lon",
|
||||
type=float,
|
||||
default=DEFAULT_LON,
|
||||
help=f"Origin longitude (default: {DEFAULT_LON})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--origin-alt",
|
||||
type=float,
|
||||
default=DEFAULT_ALT,
|
||||
help=f"Origin altitude AMSL in meters (default: {DEFAULT_ALT})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--target-north",
|
||||
type=float,
|
||||
default=DEFAULT_TARGET_NORTH,
|
||||
help=f"Target position north of origin in meters (default: {DEFAULT_TARGET_NORTH})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--target-east",
|
||||
type=float,
|
||||
default=DEFAULT_TARGET_EAST,
|
||||
help=f"Target position east of origin in meters (default: {DEFAULT_TARGET_EAST})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--preview",
|
||||
action="store_true",
|
||||
help="Show ASCII preview of the formation",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o", "--output",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Output YAML file path (default: print to stdout)",
|
||||
)
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.count < 1:
|
||||
parser.error("--count must be at least 1")
|
||||
if args.spacing <= 0:
|
||||
parser.error("--spacing must be positive")
|
||||
if args.altitude <= 0:
|
||||
parser.error("--altitude must be positive")
|
||||
if args.radius <= 0:
|
||||
parser.error("--radius must be positive")
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def main(argv: "List[str] | None" = None) -> int:
|
||||
args = parse_args(argv)
|
||||
|
||||
# Select generator and dispatch with appropriate arguments
|
||||
generator = PATTERN_GENERATORS[args.pattern]
|
||||
if args.pattern == "circle":
|
||||
positions = generator(args.count, args.radius, args.altitude)
|
||||
else:
|
||||
positions = generator(args.count, args.spacing, args.altitude)
|
||||
|
||||
# Validate
|
||||
if not validate_no_overlap(positions):
|
||||
return 1
|
||||
|
||||
# Build output
|
||||
data = build_formation_dict(
|
||||
positions,
|
||||
args.origin_lat,
|
||||
args.origin_lon,
|
||||
args.origin_alt,
|
||||
args.target_north,
|
||||
args.target_east,
|
||||
)
|
||||
yaml_text = format_yaml(data)
|
||||
|
||||
# Preview
|
||||
if args.preview:
|
||||
print(ascii_preview(positions, args.target_north, args.target_east))
|
||||
print()
|
||||
|
||||
# Output
|
||||
if args.output:
|
||||
with open(args.output, "w") as f:
|
||||
f.write(yaml_text)
|
||||
print(f"Wrote {len(positions)} drone formation to {args.output}")
|
||||
else:
|
||||
if not args.preview:
|
||||
print(yaml_text, end="")
|
||||
else:
|
||||
print(yaml_text, end="")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,2 @@
|
||||
# Staging directory created by build_docker.sh (ephemeral)
|
||||
_px4_msgs_defs/
|
||||
@@ -0,0 +1,95 @@
|
||||
# ROS 2 Jazzy layer on top of PX4 dev environment
|
||||
# For swarm simulation with px4-ros2-interface-lib
|
||||
#
|
||||
# Build (from PX4-Autopilot root):
|
||||
# Tools/simulation/swarm_ros2/build_docker.sh
|
||||
#
|
||||
# Run:
|
||||
# docker run -it --rm -v /path/to/PX4-Autopilot:/px4 px4-swarm-ros2
|
||||
#
|
||||
FROM px4io/px4-dev:v1.17.0-rc1
|
||||
LABEL maintainer="Ramon Roche <mrpollo@gmail.com>"
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV ROS_DISTRO=jazzy
|
||||
|
||||
# Add ROS 2 Jazzy repository (Ubuntu 24.04 = noble)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
gnupg \
|
||||
lsb-release \
|
||||
software-properties-common \
|
||||
&& curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key \
|
||||
-o /usr/share/keyrings/ros-archive-keyring.gpg \
|
||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] \
|
||||
http://packages.ros.org/ros2/ubuntu noble main" \
|
||||
> /etc/apt/sources.list.d/ros2.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ros-jazzy-ros-base \
|
||||
ros-jazzy-rmw-cyclonedds-cpp \
|
||||
python3-colcon-common-extensions \
|
||||
python3-rosdep \
|
||||
python3-numpy \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Initialize rosdep
|
||||
RUN rosdep init 2>/dev/null || true \
|
||||
&& su user -c "rosdep update --rosdistro jazzy" 2>/dev/null || true
|
||||
|
||||
# Set ROS 2 middleware to CycloneDDS (better multi-vehicle support)
|
||||
ENV RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
|
||||
|
||||
# Build Micro XRCE-DDS Agent from source
|
||||
RUN git clone --depth 1 https://github.com/eProsima/Micro-XRCE-DDS-Agent.git /tmp/xrce-agent \
|
||||
&& cd /tmp/xrce-agent \
|
||||
&& mkdir build && cd build \
|
||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
|
||||
&& make -j$(nproc) \
|
||||
&& make install \
|
||||
&& ldconfig \
|
||||
&& rm -rf /tmp/xrce-agent
|
||||
|
||||
# Build px4_msgs from PX4 source tree msg definitions (ensures type hash match).
|
||||
# The build_docker.sh script copies msg/ and srv/ into _px4_msgs_defs/ before build.
|
||||
ENV ROS2_WS=/opt/ros2_ws
|
||||
COPY _px4_msgs_defs/ /tmp/px4_msg_defs/
|
||||
RUN mkdir -p ${ROS2_WS}/src \
|
||||
&& cd ${ROS2_WS}/src \
|
||||
&& git clone --depth 1 https://github.com/PX4/px4_msgs.git \
|
||||
&& rm -rf px4_msgs/msg/*.msg px4_msgs/srv/*.srv \
|
||||
&& cp /tmp/px4_msg_defs/msg/*.msg px4_msgs/msg/ \
|
||||
&& cp /tmp/px4_msg_defs/srv/*.srv px4_msgs/srv/ \
|
||||
&& rm -rf /tmp/px4_msg_defs \
|
||||
&& cd ${ROS2_WS} \
|
||||
&& . /opt/ros/jazzy/setup.sh \
|
||||
&& colcon build --packages-select px4_msgs --cmake-args -DCMAKE_BUILD_TYPE=Release \
|
||||
&& rm -rf log
|
||||
|
||||
# Build px4-ros2-interface-lib (depends on px4_msgs built above)
|
||||
RUN cd ${ROS2_WS}/src \
|
||||
&& git clone --depth 1 --recursive https://github.com/Auterion/px4-ros2-interface-lib.git \
|
||||
&& cd ${ROS2_WS} \
|
||||
&& . /opt/ros/jazzy/setup.sh \
|
||||
&& . install/setup.sh \
|
||||
&& colcon build --packages-skip px4_msgs --cmake-args -DCMAKE_BUILD_TYPE=Release \
|
||||
&& rm -rf log src
|
||||
|
||||
# Copy swarm_ros2 package and build it
|
||||
COPY . ${ROS2_WS}/src/swarm_ros2/
|
||||
RUN cd ${ROS2_WS} \
|
||||
&& . /opt/ros/jazzy/setup.sh \
|
||||
&& . install/setup.sh \
|
||||
&& colcon build --packages-select swarm_ros2 \
|
||||
&& rm -rf log
|
||||
|
||||
# Source everything on shell entry
|
||||
RUN echo "source /opt/ros/jazzy/setup.bash" >> /etc/bash.bashrc \
|
||||
&& echo "source ${ROS2_WS}/install/setup.bash" >> /etc/bash.bashrc
|
||||
|
||||
# XRCE-DDS UDP port
|
||||
EXPOSE 8888/udp
|
||||
|
||||
WORKDIR ${ROS2_WS}
|
||||
|
||||
CMD ["/bin/bash"]
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
# Build the px4-swarm-ros2 Docker image with px4_msgs matching this PX4 commit.
|
||||
# Run from anywhere; the script locates PX4 root automatically.
|
||||
#
|
||||
# Usage: ./build_docker.sh
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
PX4_ROOT="$( cd "${SCRIPT_DIR}/../../.." && pwd )"
|
||||
|
||||
echo "PX4 root: ${PX4_ROOT}"
|
||||
echo "Staging px4_msgs definitions from this PX4 commit..."
|
||||
|
||||
# Stage msg/srv files into the Docker build context
|
||||
STAGING_DIR="${SCRIPT_DIR}/_px4_msgs_defs"
|
||||
rm -rf "${STAGING_DIR}"
|
||||
mkdir -p "${STAGING_DIR}/msg" "${STAGING_DIR}/srv"
|
||||
|
||||
cp "${PX4_ROOT}"/msg/*.msg "${STAGING_DIR}/msg/"
|
||||
cp "${PX4_ROOT}"/msg/versioned/*.msg "${STAGING_DIR}/msg/"
|
||||
cp "${PX4_ROOT}"/srv/*.srv "${STAGING_DIR}/srv/"
|
||||
|
||||
echo "Staged $(ls "${STAGING_DIR}/msg/" | wc -l | tr -d ' ') .msg files and $(ls "${STAGING_DIR}/srv/" | wc -l | tr -d ' ') .srv files"
|
||||
echo ""
|
||||
echo "Building Docker image..."
|
||||
|
||||
docker build -t px4-swarm-ros2 "${SCRIPT_DIR}"
|
||||
|
||||
# Clean up staged files
|
||||
rm -rf "${STAGING_DIR}"
|
||||
|
||||
echo ""
|
||||
echo "Done. Run with:"
|
||||
echo " docker run -it --rm -v ${PX4_ROOT}:/px4 px4-swarm-ros2 bash /px4/Tools/simulation/swarm_ros2/run_swarm.sh 4"
|
||||
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
Launch file for the PX4 swarm coordination stack.
|
||||
|
||||
Starts one vision_provider node per drone and a single swarm_controller node.
|
||||
|
||||
Usage:
|
||||
ros2 launch swarm_ros2 swarm_launch.py num_drones:=16 speed_factor:=1.0
|
||||
"""
|
||||
|
||||
from launch import LaunchDescription
|
||||
from launch.actions import DeclareLaunchArgument
|
||||
from launch.substitutions import LaunchConfiguration
|
||||
from launch_ros.actions import Node
|
||||
|
||||
|
||||
def generate_launch_description():
|
||||
# Declare launch arguments.
|
||||
num_drones_arg = DeclareLaunchArgument(
|
||||
'num_drones',
|
||||
default_value='16',
|
||||
description='Number of drones in the swarm',
|
||||
)
|
||||
speed_factor_arg = DeclareLaunchArgument(
|
||||
'speed_factor',
|
||||
default_value='1.0',
|
||||
description='Simulation speed factor',
|
||||
)
|
||||
formation_config_arg = DeclareLaunchArgument(
|
||||
'formation_config',
|
||||
default_value='',
|
||||
description='Path to YAML formation configuration file',
|
||||
)
|
||||
|
||||
num_drones = LaunchConfiguration('num_drones')
|
||||
speed_factor = LaunchConfiguration('speed_factor')
|
||||
|
||||
# We need a concrete integer to iterate, so we support a fixed max and
|
||||
# conditionally launch. ROS 2 launch does not natively support dynamic
|
||||
# loops over LaunchConfiguration integers, so we generate nodes for a
|
||||
# reasonable upper bound and rely on the controller's num_drones param
|
||||
# to limit actual usage. For simplicity, generate exactly 16 nodes
|
||||
# (the default). For other counts, users can call this launch file
|
||||
# programmatically or adjust the constant below.
|
||||
MAX_DRONES = 16
|
||||
|
||||
vision_nodes = []
|
||||
for i in range(MAX_DRONES):
|
||||
ns = f'px4_{i}'
|
||||
vision_nodes.append(
|
||||
Node(
|
||||
package='swarm_ros2',
|
||||
executable='vision_provider',
|
||||
name=f'vision_provider_{i}',
|
||||
parameters=[{
|
||||
'namespace': ns,
|
||||
'instance_id': i,
|
||||
}],
|
||||
output='screen',
|
||||
)
|
||||
)
|
||||
|
||||
# Single swarm controller.
|
||||
controller_node = Node(
|
||||
package='swarm_ros2',
|
||||
executable='swarm_controller',
|
||||
name='swarm_controller',
|
||||
parameters=[{
|
||||
'num_drones': num_drones,
|
||||
'speed_factor': speed_factor,
|
||||
}],
|
||||
output='screen',
|
||||
)
|
||||
|
||||
return LaunchDescription([
|
||||
num_drones_arg,
|
||||
speed_factor_arg,
|
||||
formation_config_arg,
|
||||
*vision_nodes,
|
||||
controller_node,
|
||||
])
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0"?>
|
||||
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
|
||||
<package format="3">
|
||||
<name>swarm_ros2</name>
|
||||
<version>0.1.0</version>
|
||||
<description>Swarm coordination for PX4 multi-vehicle simulation using px4-ros2-interface-lib concepts</description>
|
||||
<maintainer email="mrpollo@gmail.com">Ramon Roche</maintainer>
|
||||
<license>BSD-3-Clause</license>
|
||||
|
||||
<depend>rclpy</depend>
|
||||
<depend>px4_msgs</depend>
|
||||
<depend>px4_ros2_cpp</depend>
|
||||
<depend>std_msgs</depend>
|
||||
<depend>geometry_msgs</depend>
|
||||
|
||||
<test_depend>ament_copyright</test_depend>
|
||||
<test_depend>ament_flake8</test_depend>
|
||||
<test_depend>ament_pep257</test_depend>
|
||||
<test_depend>python3-pytest</test_depend>
|
||||
|
||||
<export>
|
||||
<build_type>ament_python</build_type>
|
||||
</export>
|
||||
</package>
|
||||
Executable
+99
@@ -0,0 +1,99 @@
|
||||
#!/bin/bash
|
||||
# Run the full swarm test inside Docker: PX4 SIH + XRCE-DDS + ROS 2
|
||||
# Usage: ./run_swarm.sh [NUM_DRONES] [SPEED_FACTOR]
|
||||
#
|
||||
# Must be run inside the px4-swarm-ros2 container with PX4 source mounted at /px4:
|
||||
# docker run -it --rm -v /path/to/PX4-Autopilot:/px4 px4-swarm-ros2 /opt/ros2_ws/src/swarm_ros2/run_swarm.sh 4
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
NUM_DRONES="${1:-4}"
|
||||
SPEED_FACTOR="${2:-1}"
|
||||
PX4_SRC="${PX4_SRC:-/px4}"
|
||||
BUILD_DIR="${PX4_SRC}/build/px4_sitl_sih"
|
||||
|
||||
echo "=== PX4 SIH Swarm Test ==="
|
||||
echo " Drones: $NUM_DRONES"
|
||||
echo " Speed factor: $SPEED_FACTOR"
|
||||
echo ""
|
||||
|
||||
# Check PX4 build exists
|
||||
if [ ! -f "$BUILD_DIR/bin/px4" ]; then
|
||||
echo "PX4 binary not found at $BUILD_DIR/bin/px4"
|
||||
echo "Building PX4 (this may take a few minutes)..."
|
||||
cd "$PX4_SRC"
|
||||
make px4_sitl_sih
|
||||
fi
|
||||
|
||||
# Source ROS 2
|
||||
source /opt/ros/jazzy/setup.bash
|
||||
source /opt/ros2_ws/install/setup.bash
|
||||
|
||||
# Cleanup handler
|
||||
PIDS=()
|
||||
cleanup() {
|
||||
echo ""
|
||||
echo "Shutting down..."
|
||||
for pid in "${PIDS[@]}"; do
|
||||
kill "$pid" 2>/dev/null || true
|
||||
done
|
||||
pkill -x px4 2>/dev/null || true
|
||||
pkill -x MicroXRCEAgent 2>/dev/null || true
|
||||
wait 2>/dev/null
|
||||
echo "Done."
|
||||
}
|
||||
trap cleanup EXIT SIGINT SIGTERM
|
||||
|
||||
# Kill any existing px4
|
||||
pkill -x px4 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
# Start XRCE-DDS agent
|
||||
echo "Starting XRCE-DDS agent on port 8888..."
|
||||
MicroXRCEAgent udp4 -p 8888 > /dev/null 2>&1 &
|
||||
PIDS+=($!)
|
||||
sleep 1
|
||||
|
||||
# Launch PX4 instances
|
||||
export PX4_SIM_MODEL=sihsim_quadx_vision
|
||||
export PX4_SIM_SPEED_FACTOR="$SPEED_FACTOR"
|
||||
|
||||
# Clean stale instance data (old parameters.bson can override param set-default)
|
||||
echo "Cleaning stale instance data..."
|
||||
for n in $(seq 0 $((NUM_DRONES - 1))); do
|
||||
rm -rf "$BUILD_DIR/instance_$n"
|
||||
done
|
||||
|
||||
echo "Launching $NUM_DRONES PX4 SIH instances..."
|
||||
for n in $(seq 0 $((NUM_DRONES - 1))); do
|
||||
working_dir="$BUILD_DIR/instance_$n"
|
||||
mkdir -p "$working_dir"
|
||||
|
||||
pushd "$working_dir" &>/dev/null
|
||||
# Force consistent DDS namespace for all instances (including 0)
|
||||
# so ROS 2 nodes can use px4_0, px4_1, ... uniformly
|
||||
PX4_UXRCE_DDS_NS="px4_$n" "$BUILD_DIR/bin/px4" -i "$n" -d "$BUILD_DIR/etc" >out.log 2>err.log &
|
||||
PIDS+=($!)
|
||||
popd &>/dev/null
|
||||
echo " Instance $n started (PID ${PIDS[-1]})"
|
||||
done
|
||||
|
||||
# Wait for PX4 instances to initialize
|
||||
echo ""
|
||||
echo "Waiting 5s for PX4 instances to initialize..."
|
||||
sleep 5
|
||||
|
||||
# Launch ROS 2 swarm (blocks until mission completes)
|
||||
echo ""
|
||||
echo "Launching ROS 2 swarm controller..."
|
||||
echo "=========================================="
|
||||
ros2 launch swarm_ros2 swarm_launch.py num_drones:="$NUM_DRONES"
|
||||
|
||||
echo ""
|
||||
echo "=== Mission complete ==="
|
||||
|
||||
# Collect ULogs
|
||||
echo "Collecting ULog files..."
|
||||
python3 "$PX4_SRC/Tools/simulation/collect_swarm_ulogs.py" \
|
||||
--build-dir "$BUILD_DIR" \
|
||||
--output-dir "$PX4_SRC/swarm_ulogs_$(date +%Y%m%d_%H%M%S)"
|
||||
@@ -0,0 +1,5 @@
|
||||
[develop]
|
||||
script_dir=$base/lib/swarm_ros2
|
||||
|
||||
[install]
|
||||
install_scripts=$base/lib/swarm_ros2
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user