From 35a734d49a52448b8b4357d078f0aa56f15a1234 Mon Sep 17 00:00:00 2001 From: Hamish Willee Date: Thu, 2 Apr 2026 09:10:59 +1100 Subject: [PATCH] docs(infrastructure): Add script and tests to generate FC doc sections/docs --- docs/package.json | 8 +- docs/scripts/fc_doc_generator/CLAUDE.md | 134 + .../fc_doc_generator/fc_doc_generator.py | 4461 +++ .../fc_doc_generator/metadata/.gitkeep | 0 .../3dr_ctrl-zero-h7-oem-revg_data.json | 214 + .../metadata/accton-godwit_ga1_data.json | 268 + .../metadata/airmind_mindpx-v2_data.json | 158 + .../metadata/ark_cannode_data.json | 249 + .../metadata/ark_fmu-v6x_data.json | 231 + .../metadata/ark_fpv_data.json | 203 + .../metadata/ark_pi6x_data.json | 208 + .../metadata/arkin_labs_a2m6x_wizard.json | 70 + .../arkin_labs_aeromind_6x_wizard.json | 72 + .../metadata/atl_mantis-edu_data.json | 152 + .../metadata/auterion_fmu-v6s_data.json | 253 + .../metadata/auterion_fmu-v6x_data.json | 264 + .../metadata/av_x-v1_data.json | 213 + .../metadata/bitcraze_crazyflie21_data.json | 104 + .../metadata/bitcraze_crazyflie_data.json | 122 + .../metadata/corvon_743v1_data.json | 230 + .../metadata/corvon_arse1_wizard.json | 59 + .../metadata/corvon_arse_wizard.json | 51 + .../metadata/cuav_7-nano_data.json | 247 + .../metadata/cuav_fmu-v6x_data.json | 266 + .../metadata/cuav_nora_data.json | 298 + .../metadata/cuav_x25-evo_data.json | 231 + .../metadata/cuav_x25-super_data.json | 230 + .../metadata/cuav_x7pro_data.json | 299 + .../metadata/cubepilot_cubeorange_data.json | 207 + .../cubepilot_cubeorangeplus_data.json | 224 + .../metadata/cubepilot_cubeyellow_data.json | 204 + .../metadata/cubepilot_io-v2_data.json | 108 + .../metadata/diatone_mamba-f405-mk2_data.json | 140 + .../metadata/espressif_esp32_data.json | 76 + .../metadata/flywoo_gn-f405_data.json | 120 + .../metadata/gearup_airbrainh743_data.json | 228 + .../metadata/hkust_nxt-dual_data.json | 212 + .../metadata/hkust_nxt-v1_data.json | 212 + .../metadata/holybro_durandal-v1_data.json | 236 + .../metadata/holybro_durandal_wizard.json | 49 + .../metadata/holybro_kakutef7_data.json | 198 + .../metadata/holybro_kakuteh7-wing_data.json | 266 + .../metadata/holybro_kakuteh7_data.json | 203 + .../holybro_kakuteh7_v1.18_wizard.json | 14 + .../metadata/holybro_kakuteh7_wizard.json | 36 + .../holybro_kakuteh7dualimu_data.json | 216 + .../metadata/holybro_kakuteh7mini_data.json | 203 + .../metadata/holybro_kakuteh7v2_data.json | 203 + .../metadata/holybro_pix32v5_data.json | 243 + .../metadata/matek_gnss-m9n-f4_data.json | 136 + .../metadata/matek_h743-mini_data.json | 246 + .../metadata/matek_h743-slim_data.json | 247 + .../metadata/matek_h743_data.json | 235 + .../metadata/micoair_h743-aio_data.json | 210 + .../metadata/micoair_h743-lite_data.json | 238 + .../metadata/micoair_h743-v2_data.json | 245 + .../metadata/micoair_h743_data.json | 230 + .../metadata/modalai_fc-v1_data.json | 215 + .../metadata/modalai_fc-v2_data.json | 209 + .../metadata/modalai_voxl2-io_data.json | 108 + .../metadata/mro_ctrl-zero-classic_data.json | 251 + .../metadata/mro_ctrl-zero-f7-oem_data.json | 203 + .../metadata/mro_ctrl-zero-f7_data.json | 203 + .../metadata/mro_ctrl-zero-h7-oem_data.json | 214 + .../metadata/mro_ctrl-zero-h7_data.json | 214 + .../metadata/mro_pixracerpro_data.json | 220 + .../metadata/mro_x21-777_data.json | 186 + .../metadata/mro_x21_data.json | 138 + .../metadata/narinfc_h7_data.json | 311 + .../metadata/nxp_fmuk66-e_data.json | 159 + .../metadata/nxp_fmuk66-v3_data.json | 139 + .../metadata/nxp_mr-canhubk3_data.json | 270 + .../metadata/nxp_mr-tropic_data.json | 227 + .../metadata/nxp_tropic-community_data.json | 209 + .../metadata/nxp_ucans32k146_data.json | 178 + .../metadata/omnibus_f4sd_data.json | 109 + .../metadata/px4_fmu-v2_data.json | 177 + .../metadata/px4_fmu-v3_data.json | 178 + .../metadata/px4_fmu-v4_data.json | 105 + .../metadata/px4_fmu-v4pro_data.json | 142 + .../metadata/px4_fmu-v5_data.json | 272 + .../metadata/px4_fmu-v5_wizard.json | 26 + .../metadata/px4_fmu-v5x_data.json | 292 + .../metadata/px4_fmu-v6c_data.json | 262 + .../metadata/px4_fmu-v6u_data.json | 240 + .../metadata/px4_fmu-v6x_data.json | 380 + .../metadata/px4_fmu-v6x_wizard.json | 14 + .../metadata/px4_fmu-v6xrt_data.json | 345 + .../metadata/px4_fmu_v6x_fmu_v6x_wizard.json | 14 + .../metadata/px4_fmu_v6x_wizard.json | 42 + .../metadata/px4_io-v2_data.json | 108 + .../metadata/radiolink_PIX6_data.json | 232 + .../metadata/raspberrypi_pico_data.json | 122 + .../metadata/siyi_n7_data.json | 201 + .../sky-drones_smartap-airlink_data.json | 278 + .../metadata/spracing_h7extreme_data.json | 208 + .../metadata/svehicle_e2_data.json | 268 + .../metadata/test_board_wizard.json | 21 + .../metadata/testmfr_testboard_wizard.json | 14 + .../metadata/thepeach_k1_data.json | 135 + .../metadata/thepeach_r1_data.json | 137 + .../metadata/uvify_core_data.json | 106 + .../metadata/x-mav_ap-h743r1_data.json | 223 + .../metadata/x-mav_ap-h743v2_data.json | 246 + .../metadata/xc-fly_xc-slam_data.json | 230 + .../metadata/xc-fly_xc-slim_data.json | 230 + .../metadata/zeroone_x6_data.json | 302 + docs/scripts/fc_doc_generator/pytest.ini | 2 + docs/scripts/fc_doc_generator/tests/.gitkeep | 0 .../fc_doc_generator/tests/__init__.py | 0 .../fc_doc_generator/tests/conftest.py | 122 + .../fixtures/imxrt_all_dshot/default.px4board | 9 + .../nuttx-config/nsh/defconfig | 12 + .../imxrt_all_dshot/src/board_config.h | 7 + .../imxrt_all_dshot/src/timer_config.cpp | 15 + .../stm32f4_no_dshot/default.px4board | 4 + .../nuttx-config/nsh/defconfig | 8 + .../stm32f4_no_dshot/src/board_config.h | 3 + .../stm32f4_no_dshot/src/timer_config.cpp | 16 + .../stm32h7_all_dshot/default.px4board | 5 + .../stm32h7_all_dshot/init/rc.board_sensors | 12 + .../nuttx-config/include/board.h | 20 + .../nuttx-config/nsh/defconfig | 10 + .../stm32h7_all_dshot/src/board_config.h | 4 + .../stm32h7_all_dshot/src/timer_config.cpp | 19 + .../init/rc.board_sensors | 22 + .../stm32h7_mixed_io/default.px4board | 7 + .../nuttx-config/nsh/defconfig | 12 + .../stm32h7_mixed_io/src/board_config.h | 10 + .../stm32h7_mixed_io/src/timer_config.cpp | 19 + .../stm32h7_ppm_shared/default.px4board | 4 + .../nuttx-config/nsh/defconfig | 6 + .../stm32h7_ppm_shared/src/board_config.h | 4 + .../stm32h7_ppm_shared/src/timer_config.cpp | 13 + .../stm32h7_variant/init/rc.board_sensors | 19 + .../tests/snapshots/fc_sections.md | 28892 ++++++++++++++++ .../tests/snapshots/gps_basic.md | 38 + .../tests/snapshots/gps_two_ports.md | 48 + .../tests/snapshots/gps_with_safety.md | 42 + .../tests/snapshots/gps_wizard_override.md | 44 + .../tests/snapshots/power_digital_ina238.md | 40 + .../tests/snapshots/power_dronecan_port.md | 48 + .../tests/snapshots/power_dual_redundant.md | 42 + .../snapshots/power_mixed_analog_dronecan.md | 54 + .../tests/snapshots/power_single_analog.md | 40 + .../tests/snapshots/power_wizard_override.md | 51 + .../tests/snapshots/pwm_imxrt_all_dshot.md | 12 + .../tests/snapshots/pwm_no_groups.md | 3 + .../tests/snapshots/pwm_stm32f4_no_dshot.md | 13 + .../tests/snapshots/pwm_stm32h7_all_dshot.md | 14 + .../tests/snapshots/pwm_stm32h7_mixed_io.md | 18 + .../tests/snapshots/rc_has_common_rc.md | 46 + .../tests/snapshots/rc_has_rc_input.md | 42 + .../tests/snapshots/rc_no_rc_data.md | 37 + .../tests/snapshots/rc_ppm_dedicated.md | 43 + .../tests/snapshots/rc_ppm_shared.md | 42 + .../tests/snapshots/sd_card_absent.md | 12 + .../tests/snapshots/sd_card_present.md | 24 + .../tests/snapshots/serial_empty.md | 3 + .../tests/snapshots/serial_imxrt.md | 11 + .../tests/snapshots/serial_stm32h7.md | 3 + .../snapshots/serial_with_flow_control.md | 12 + .../tests/snapshots/spec_i2c_sensors.md | 57 + .../snapshots/spec_io_board_rc_no_ppm.md | 56 + .../snapshots/spec_io_board_rc_with_ppm.md | 56 + .../tests/snapshots/spec_no_bus_info.md | 59 + .../tests/snapshots/spec_spi_sensors.md | 59 + .../snapshots/spec_with_variant_labels.md | 70 + .../tests/snapshots/spec_with_variants.md | 65 + .../tests/snapshots/spec_wizard_override.md | 58 + .../tests/snapshots/telemetry_multi.md | 26 + .../tests/snapshots/telemetry_single.md | 24 + .../fc_doc_generator/tests/test_compute.py | 150 + .../fc_doc_generator/tests/test_generators.py | 888 + .../fc_doc_generator/tests/test_helpers.py | 153 + .../fc_doc_generator/tests/test_parsers.py | 530 + .../fc_doc_generator/tests/test_wizard.py | 525 + 177 files changed, 56826 insertions(+), 1 deletion(-) create mode 100644 docs/scripts/fc_doc_generator/CLAUDE.md create mode 100644 docs/scripts/fc_doc_generator/fc_doc_generator.py create mode 100644 docs/scripts/fc_doc_generator/metadata/.gitkeep create mode 100644 docs/scripts/fc_doc_generator/metadata/3dr_ctrl-zero-h7-oem-revg_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/accton-godwit_ga1_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/airmind_mindpx-v2_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/ark_cannode_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/ark_fmu-v6x_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/ark_fpv_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/ark_pi6x_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/arkin_labs_a2m6x_wizard.json create mode 100644 docs/scripts/fc_doc_generator/metadata/arkin_labs_aeromind_6x_wizard.json create mode 100644 docs/scripts/fc_doc_generator/metadata/atl_mantis-edu_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/auterion_fmu-v6s_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/auterion_fmu-v6x_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/av_x-v1_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/bitcraze_crazyflie21_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/bitcraze_crazyflie_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/corvon_743v1_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/corvon_arse1_wizard.json create mode 100644 docs/scripts/fc_doc_generator/metadata/corvon_arse_wizard.json create mode 100644 docs/scripts/fc_doc_generator/metadata/cuav_7-nano_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/cuav_fmu-v6x_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/cuav_nora_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/cuav_x25-evo_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/cuav_x25-super_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/cuav_x7pro_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/cubepilot_cubeorange_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/cubepilot_cubeorangeplus_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/cubepilot_cubeyellow_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/cubepilot_io-v2_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/diatone_mamba-f405-mk2_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/espressif_esp32_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/flywoo_gn-f405_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/gearup_airbrainh743_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/hkust_nxt-dual_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/hkust_nxt-v1_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/holybro_durandal-v1_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/holybro_durandal_wizard.json create mode 100644 docs/scripts/fc_doc_generator/metadata/holybro_kakutef7_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/holybro_kakuteh7-wing_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/holybro_kakuteh7_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/holybro_kakuteh7_v1.18_wizard.json create mode 100644 docs/scripts/fc_doc_generator/metadata/holybro_kakuteh7_wizard.json create mode 100644 docs/scripts/fc_doc_generator/metadata/holybro_kakuteh7dualimu_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/holybro_kakuteh7mini_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/holybro_kakuteh7v2_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/holybro_pix32v5_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/matek_gnss-m9n-f4_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/matek_h743-mini_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/matek_h743-slim_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/matek_h743_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/micoair_h743-aio_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/micoair_h743-lite_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/micoair_h743-v2_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/micoair_h743_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/modalai_fc-v1_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/modalai_fc-v2_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/modalai_voxl2-io_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/mro_ctrl-zero-classic_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/mro_ctrl-zero-f7-oem_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/mro_ctrl-zero-f7_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/mro_ctrl-zero-h7-oem_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/mro_ctrl-zero-h7_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/mro_pixracerpro_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/mro_x21-777_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/mro_x21_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/narinfc_h7_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/nxp_fmuk66-e_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/nxp_fmuk66-v3_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/nxp_mr-canhubk3_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/nxp_mr-tropic_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/nxp_tropic-community_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/nxp_ucans32k146_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/omnibus_f4sd_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/px4_fmu-v2_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/px4_fmu-v3_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/px4_fmu-v4_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/px4_fmu-v4pro_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/px4_fmu-v5_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/px4_fmu-v5_wizard.json create mode 100644 docs/scripts/fc_doc_generator/metadata/px4_fmu-v5x_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/px4_fmu-v6c_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/px4_fmu-v6u_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/px4_fmu-v6x_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/px4_fmu-v6x_wizard.json create mode 100644 docs/scripts/fc_doc_generator/metadata/px4_fmu-v6xrt_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/px4_fmu_v6x_fmu_v6x_wizard.json create mode 100644 docs/scripts/fc_doc_generator/metadata/px4_fmu_v6x_wizard.json create mode 100644 docs/scripts/fc_doc_generator/metadata/px4_io-v2_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/radiolink_PIX6_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/raspberrypi_pico_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/siyi_n7_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/sky-drones_smartap-airlink_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/spracing_h7extreme_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/svehicle_e2_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/test_board_wizard.json create mode 100644 docs/scripts/fc_doc_generator/metadata/testmfr_testboard_wizard.json create mode 100644 docs/scripts/fc_doc_generator/metadata/thepeach_k1_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/thepeach_r1_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/uvify_core_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/x-mav_ap-h743r1_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/x-mav_ap-h743v2_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/xc-fly_xc-slam_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/xc-fly_xc-slim_data.json create mode 100644 docs/scripts/fc_doc_generator/metadata/zeroone_x6_data.json create mode 100644 docs/scripts/fc_doc_generator/pytest.ini create mode 100644 docs/scripts/fc_doc_generator/tests/.gitkeep create mode 100644 docs/scripts/fc_doc_generator/tests/__init__.py create mode 100644 docs/scripts/fc_doc_generator/tests/conftest.py create mode 100644 docs/scripts/fc_doc_generator/tests/fixtures/imxrt_all_dshot/default.px4board create mode 100644 docs/scripts/fc_doc_generator/tests/fixtures/imxrt_all_dshot/nuttx-config/nsh/defconfig create mode 100644 docs/scripts/fc_doc_generator/tests/fixtures/imxrt_all_dshot/src/board_config.h create mode 100644 docs/scripts/fc_doc_generator/tests/fixtures/imxrt_all_dshot/src/timer_config.cpp create mode 100644 docs/scripts/fc_doc_generator/tests/fixtures/stm32f4_no_dshot/default.px4board create mode 100644 docs/scripts/fc_doc_generator/tests/fixtures/stm32f4_no_dshot/nuttx-config/nsh/defconfig create mode 100644 docs/scripts/fc_doc_generator/tests/fixtures/stm32f4_no_dshot/src/board_config.h create mode 100644 docs/scripts/fc_doc_generator/tests/fixtures/stm32f4_no_dshot/src/timer_config.cpp create mode 100644 docs/scripts/fc_doc_generator/tests/fixtures/stm32h7_all_dshot/default.px4board create mode 100644 docs/scripts/fc_doc_generator/tests/fixtures/stm32h7_all_dshot/init/rc.board_sensors create mode 100644 docs/scripts/fc_doc_generator/tests/fixtures/stm32h7_all_dshot/nuttx-config/include/board.h create mode 100644 docs/scripts/fc_doc_generator/tests/fixtures/stm32h7_all_dshot/nuttx-config/nsh/defconfig create mode 100644 docs/scripts/fc_doc_generator/tests/fixtures/stm32h7_all_dshot/src/board_config.h create mode 100644 docs/scripts/fc_doc_generator/tests/fixtures/stm32h7_all_dshot/src/timer_config.cpp create mode 100644 docs/scripts/fc_doc_generator/tests/fixtures/stm32h7_graceful_fail/init/rc.board_sensors create mode 100644 docs/scripts/fc_doc_generator/tests/fixtures/stm32h7_mixed_io/default.px4board create mode 100644 docs/scripts/fc_doc_generator/tests/fixtures/stm32h7_mixed_io/nuttx-config/nsh/defconfig create mode 100644 docs/scripts/fc_doc_generator/tests/fixtures/stm32h7_mixed_io/src/board_config.h create mode 100644 docs/scripts/fc_doc_generator/tests/fixtures/stm32h7_mixed_io/src/timer_config.cpp create mode 100644 docs/scripts/fc_doc_generator/tests/fixtures/stm32h7_ppm_shared/default.px4board create mode 100644 docs/scripts/fc_doc_generator/tests/fixtures/stm32h7_ppm_shared/nuttx-config/nsh/defconfig create mode 100644 docs/scripts/fc_doc_generator/tests/fixtures/stm32h7_ppm_shared/src/board_config.h create mode 100644 docs/scripts/fc_doc_generator/tests/fixtures/stm32h7_ppm_shared/src/timer_config.cpp create mode 100644 docs/scripts/fc_doc_generator/tests/fixtures/stm32h7_variant/init/rc.board_sensors create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/fc_sections.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/gps_basic.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/gps_two_ports.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/gps_with_safety.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/gps_wizard_override.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/power_digital_ina238.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/power_dronecan_port.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/power_dual_redundant.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/power_mixed_analog_dronecan.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/power_single_analog.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/power_wizard_override.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/pwm_imxrt_all_dshot.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/pwm_no_groups.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/pwm_stm32f4_no_dshot.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/pwm_stm32h7_all_dshot.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/pwm_stm32h7_mixed_io.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/rc_has_common_rc.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/rc_has_rc_input.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/rc_no_rc_data.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/rc_ppm_dedicated.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/rc_ppm_shared.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/sd_card_absent.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/sd_card_present.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/serial_empty.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/serial_imxrt.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/serial_stm32h7.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/serial_with_flow_control.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/spec_i2c_sensors.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/spec_io_board_rc_no_ppm.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/spec_io_board_rc_with_ppm.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/spec_no_bus_info.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/spec_spi_sensors.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/spec_with_variant_labels.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/spec_with_variants.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/spec_wizard_override.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/telemetry_multi.md create mode 100644 docs/scripts/fc_doc_generator/tests/snapshots/telemetry_single.md create mode 100644 docs/scripts/fc_doc_generator/tests/test_compute.py create mode 100644 docs/scripts/fc_doc_generator/tests/test_generators.py create mode 100644 docs/scripts/fc_doc_generator/tests/test_helpers.py create mode 100644 docs/scripts/fc_doc_generator/tests/test_parsers.py create mode 100644 docs/scripts/fc_doc_generator/tests/test_wizard.py diff --git a/docs/package.json b/docs/package.json index 523efc3652..d8cdb57125 100644 --- a/docs/package.json +++ b/docs/package.json @@ -14,7 +14,13 @@ "docs:get_alt_sidebar_windows": "python ./scripts/gen_alt_sidebar.py", "start": "yarn docs:dev", "linkcheck": "markdown_link_checker_sc -r .. -d docs -e en -i assets", - "build_docs_metadata_ubuntu": "(cd .. && Tools/ci/metadata_sync.sh --generate && Tools/ci/metadata_sync.sh --sync) && echo 'NOTE: These metadata changes are for local testing only and do not need to be merged.'" + "build_docs_metadata_ubuntu": "(cd .. && Tools/ci/metadata_sync.sh --generate && Tools/ci/metadata_sync.sh --sync) && echo 'NOTE: These metadata changes are for local testing only and do not need to be merged.'", + "gen_fc_sections": "python3 ./scripts/fc_doc_generator/fc_doc_generator.py --apply", + "new_fc_doc": "python3 ./scripts/fc_doc_generator/fc_doc_generator.py --new-doc --since-version v1.18", + "check_fc_doc": "python3 ./scripts/fc_doc_generator/fc_doc_generator.py --check-doc", + "check_fc_docs": "python3 ./scripts/fc_doc_generator/fc_doc_generator.py --check-all", + "test_fc_doc": "python3 -m pytest scripts/fc_doc_generator/ -v", + "update_fc_snapshots": "python3 -m pytest scripts/fc_doc_generator/ -v --update-snapshots" }, "dependencies": { "@red-asuka/vitepress-plugin-tabs": "0.0.4", diff --git a/docs/scripts/fc_doc_generator/CLAUDE.md b/docs/scripts/fc_doc_generator/CLAUDE.md new file mode 100644 index 0000000000..8a741ec9f3 --- /dev/null +++ b/docs/scripts/fc_doc_generator/CLAUDE.md @@ -0,0 +1,134 @@ +# fc_doc_generator + +Auto-generates flight controller documentation sections for PX4-Autopilot +from board source files (`boards///`). + +## What it does + +Parses PX4 board C/Kconfig source files and generates Markdown sections +inserted into `docs/en/flight_controller/*.md` docs. Sections generated: + +- `## Specifications` — processor, sensors, interfaces +- `## PWM Outputs` — timer groups, DShot/BDShot capability per output +- `## Serial` — UART → /dev/ttyS* mapping with labels and flow control +- `## Radio Control` — RC input protocols and ports +- `## GPS & Compass` — GPS/safety connector info +- `## Power` — power input ports and monitor type +- `## Telemetry Radios` — TELEM port listing +- `## SD Card` — presence/absence + +## File layout + +``` +fc_doc_generator/ +├── fc_doc_generator.py # Main script (~3700 lines) +├── pytest.ini # testpaths = tests +├── metadata/ # Per-board cached JSON (data + wizard overrides) +│ ├── __data.json # Parsed board data +│ └── __wizard.json # User-supplied wizard overrides +└── tests/ + ├── conftest.py # snapshot fixture + board_* path fixtures + ├── fixtures/ # Minimal fake board trees (5 boards) + │ ├── stm32h7_all_dshot/ + │ ├── stm32h7_mixed_io/ + │ ├── stm32h7_ppm_shared/ + │ ├── stm32f4_no_dshot/ + │ └── imxrt_all_dshot/ + ├── snapshots/ # Expected markdown output (.md files) + ├── test_parsers.py # Unit tests for parse_* functions + ├── test_compute.py # Unit tests for compute_groups / compute_bdshot + ├── test_generators.py # Snapshot tests for generate_*_section functions + ├── test_helpers.py # Unit tests for helper functions + └── test_wizard.py # Tests for wizard cache and generate_full_template +``` + +## Running the script + +Run from the repo root (requires the PX4 `boards/` tree to be present): + +```sh +# Generate metadata JSON + fc_sections.md (no file edits): +python docs/scripts/fc_doc_generator/fc_doc_generator.py + +# Apply sections to existing FC docs: +python docs/scripts/fc_doc_generator/fc_doc_generator.py --apply + +# Apply a single section only: +python docs/scripts/fc_doc_generator/fc_doc_generator.py --apply --section pwm_outputs + +# Create a new stub FC doc (interactive wizard): +python docs/scripts/fc_doc_generator/fc_doc_generator.py --new-doc + +# Check a single doc against quality specs: +python docs/scripts/fc_doc_generator/fc_doc_generator.py --check-doc docs/en/flight_controller/holybro_kakuteh7.md + +# Check all FC docs: +python docs/scripts/fc_doc_generator/fc_doc_generator.py --check-all +``` + +Via yarn (from the `docs/` directory): `cd docs && yarn gen_fc_sections` + +## Running tests + +From `docs/scripts/fc_doc_generator/`: + +```sh +pytest # run all tests +pytest --update-snapshots # regenerate snapshot files after intentional changes +pytest tests/test_generators.py # specific test file +``` + +## Snapshot tests + +Tests in `test_generators.py` use the `snapshot` fixture from `conftest.py`. +- Snapshot files live in `tests/snapshots/*.md` +- To add a new snapshot test: call `snapshot("my_name.md", result)` — then run `pytest --update-snapshots` to create the file +- After intentional generator changes: run `pytest --update-snapshots` then review diffs with `git diff tests/snapshots/` + +## Extension pattern (adding a new section) + +1. Write `parse_(board_path: Path) -> dict` and call it in `gather_board_data()`, merging into the entry +2. Write `generate__section(board_key, entry) -> str` +3. Register both in `SECTION_GENERATORS` and `SECTION_ORDER` +4. Add snapshot tests in `test_generators.py` +5. Re-run `cd docs && yarn gen_fc_sections` to regenerate metadata JSON + `fc_sections.md` + +## Key architecture notes + +- **Parsers** read from `boards///` source files: + - `nuttx-config/nsh/defconfig` — chip family, enabled UARTs, SD card + - `src/board_config.h` — PWM count, IO board presence, GPIOs + - `src/timer_config.cpp` — timer groups and channels + - `default.px4board` — Kconfig board settings (serial labels, RC, GPS, drivers) + - `nuttx-config/include/board.h` — flow control GPIO definitions + - `init/rc.board_sensors` — sensor driver start commands + +- **Metadata JSON** in `metadata/` caches parsed data (`*_data.json`) and + wizard-supplied overrides (`*_wizard.json`). Wizard data persists across runs + and provides connector types, sensor names, dimensions, etc. + +- **`BOARD_TO_DOC`** — static mapping from `vendor/board` key to doc filename. + Boards mapped to `None` have no existing doc page yet. + +- **Section insertion** — `_apply_section()` finds existing headings and + replaces them, or inserts before anchor headings like `## Where to Buy`. + The `specifications` section is special: it preserves hand-written content + and appends generated content as a `` comment. + +- **Wizard** — `--new-doc` runs an interactive prompts session and caches + answers to `metadata/_wizard.json` for future re-use. + +## Conventions + +- British English in doc output +- Asset files lowercase with underscores; asset folder named after doc stem +- Section generators emit embedded `` JSON comments + so the raw parsed values are visible in the doc for manual verification +- `TODO:` placeholders are left wherever data cannot be auto-detected + +## Development rules + +**When modifying `fc_doc_generator.py`:** +1. All existing tests must pass: run `pytest` from `docs/scripts/fc_doc_generator/` +2. New functionality must have new tests (unit tests and/or snapshot tests as appropriate) +3. Update this `CLAUDE.md` if the change affects how to run the script, the architecture, extension patterns, or conventions diff --git a/docs/scripts/fc_doc_generator/fc_doc_generator.py b/docs/scripts/fc_doc_generator/fc_doc_generator.py new file mode 100644 index 0000000000..9a7cd8f29a --- /dev/null +++ b/docs/scripts/fc_doc_generator/fc_doc_generator.py @@ -0,0 +1,4461 @@ +#!/usr/bin/env python3 +""" +FC Doc Generator — parse PX4 board source files and generate flight controller +documentation sections. + +Currently generates: ## Specifications, ## PWM Outputs, ## Radio Control, ## GPS & Compass + +Extension pattern — to add a new section: + 1. Write parse_(board_path) -> dict and call it in gather_board_data(), + merging the result into the board entry. + 2. Write generate__section(board_key, entry) -> str. + 3. Register both in SECTION_GENERATORS and SECTION_ORDER below. + 4. Re-run fc_doc_generator.py (yarn gen_fc_sections) to regenerate metadata/ JSON + fc_sections.md and apply to docs. +""" + +import argparse +import re +import json +from pathlib import Path + + +def _find_repo_root(start: Path) -> Path: + """Walk up from start until a directory containing 'boards/' is found.""" + for p in [start, *start.parents]: + if (p / "boards").is_dir(): + return p + raise FileNotFoundError(f"Could not find PX4 repo root (no 'boards/' dir) from {start}") + + +def _make_slug(s: str) -> str: + """Lowercase, collapse spaces/hyphens to underscores, strip non-ASCII.""" + s = ''.join(c for c in s if ord(c) < 128) # ASCII-only (removes surrogates too) + return re.sub(r'[\s\-]+', '_', s.strip().lower()) + + +def _doc_filename(manufacturer: str, product: str, fmu_version: str = None) -> str: + """holybro + Pixhawk 6X + fmu-v6x → holybro_pixhawk_6x_fmu_v6x.md""" + parts = [_make_slug(manufacturer), _make_slug(product)] + if fmu_version: + parts.append(_make_slug(fmu_version)) + return "_".join(parts) + ".md" + + +SCRIPT_DIR = Path(__file__).resolve().parent +METADATA_DIR = SCRIPT_DIR / "metadata" +REPO = _find_repo_root(SCRIPT_DIR) +BOARDS = REPO / "boards" +FC_DOCS = REPO / "docs/en/flight_controller" + +# --------------------------------------------------------------------------- +# Serial UART hardware ordering by chip family. +# Matches the order NuttX registers devices: console → /dev/ttyS0, then the +# rest in this order as /dev/ttyS1, /dev/ttyS2, … +# --------------------------------------------------------------------------- +UART_HW_ORDER = { + 'stm32h7': ['USART1', 'USART2', 'USART3', 'UART4', 'UART5', 'USART6', 'UART7', 'UART8'], + 'stm32f7': ['USART1', 'USART2', 'USART3', 'UART4', 'UART5', 'USART6', 'UART7', 'UART8'], + 'stm32f4': ['USART1', 'USART2', 'USART3', 'UART4', 'UART5', 'USART6'], + 'stm32f1': ['USART1', 'USART2', 'USART3', 'UART4', 'UART5'], + 'imxrt': ['LPUART1', 'LPUART2', 'LPUART3', 'LPUART4', + 'LPUART5', 'LPUART6', 'LPUART7', 'LPUART8', + 'LPUART9', 'LPUART10', 'LPUART11'], +} + +# --------------------------------------------------------------------------- +# BDShot per-channel capture support by chip family and timer +# Source: hw_description.h getTimerChannelDMAMap() per family +# For each timer, list which channel indices (0-based) support capture DMA. +# None = timer not in map at all (no capture) +# --------------------------------------------------------------------------- +BDSHOT_CAPTURE_MAP = { + "stm32h7": { + # All 4 channels have dma_map_ch except Timer4 ch3 (index 3 = CH4) = 0 + "Timer1": [0, 1, 2, 3], + "Timer2": [0, 1, 2, 3], + "Timer3": [0, 1, 2, 3], + "Timer4": [0, 1, 2], # CH4 (index 3) = 0 + "Timer5": [0, 1, 2, 3], + "Timer8": [0, 1, 2, 3], + "Timer15": [0], + "Timer16": [0], + "Timer17": [0], + }, + "stm32f7": { + # Only Timer1 and Timer4 have channel DMA; Timer4 CH4 = 0 + "Timer1": [0, 1, 2, 3], + "Timer4": [0, 1, 2], # CH4 = 0 + }, + "stm32f4": { + # getTimerChannelDMAMap is explicitly "Not supported" — no BDShot at all + "_none": True, + }, + "stm32f1": { + # Not supported + "_none": True, + }, + "imxrt": { + # All outputs support BDShot (docs: "All FMU outputs" on iMXRT) + "_all": True, + }, +} + +# --------------------------------------------------------------------------- +# Chip model → hardware specification lookup +# Keyed by model number without package/revision suffix (e.g. "STM32H743"). +# Used by generate_specifications_section() to produce the Processor bullet. +# --------------------------------------------------------------------------- +CHIP_SPECS = { + # STM32H7 family — Cortex-M7, 480 MHz, 2MB flash, 1MB RAM + 'STM32H743': {'core': 'Cortex®-M7', 'mhz': 480, 'flash': '2MB', 'ram': '1MB'}, + 'STM32H753': {'core': 'Cortex®-M7', 'mhz': 480, 'flash': '2MB', 'ram': '1MB'}, + 'STM32H747': {'core': 'Cortex®-M7', 'mhz': 480, 'flash': '2MB', 'ram': '1MB'}, + 'STM32H757': {'core': 'Cortex®-M7', 'mhz': 480, 'flash': '2MB', 'ram': '1MB'}, + # STM32F7 family — Cortex-M7, 216 MHz + 'STM32F765': {'core': 'Cortex®-M7', 'mhz': 216, 'flash': '2MB', 'ram': '512KB'}, + 'STM32F767': {'core': 'Cortex®-M7', 'mhz': 216, 'flash': '2MB', 'ram': '512KB'}, + 'STM32F777': {'core': 'Cortex®-M7', 'mhz': 216, 'flash': '2MB', 'ram': '512KB'}, + # STM32F4 family — Cortex-M4, 168 MHz + 'STM32F427': {'core': 'Cortex®-M4', 'mhz': 168, 'flash': '2MB', 'ram': '256KB'}, + 'STM32F437': {'core': 'Cortex®-M4', 'mhz': 168, 'flash': '2MB', 'ram': '256KB'}, + 'STM32F407': {'core': 'Cortex®-M4', 'mhz': 168, 'flash': '1MB', 'ram': '192KB'}, + 'STM32F405': {'core': 'Cortex®-M4', 'mhz': 168, 'flash': '1MB', 'ram': '192KB'}, + # iMXRT family — Cortex-M7, 600 MHz + 'MIMXRT1062': {'core': 'Cortex®-M7', 'mhz': 600, 'flash': '8MB', 'ram': '1MB'}, +} + +# IO co-processor spec (always STM32F100 on PX4 io-v2) +_IO_PROC_SPEC = 'STM32F100 (32-bit Arm® Cortex®-M3, 24 MHz, 8KB SRAM)' + + +def get_chip_family(board_path: Path) -> str: + """Detect the MCU chip family for a board. + + Mechanism + --------- + Primary source: ``nuttx-config/nsh/defconfig`` + NuttX's Kconfig system sets exactly one ``CONFIG_ARCH_CHIP_*`` symbol for + the target MCU. The symbols are mutually exclusive, so the first match is + authoritative. Patterns checked (in order): + + CONFIG_ARCH_CHIP_STM32H7 → 'stm32h7' + CONFIG_ARCH_CHIP_STM32F7 → 'stm32f7' + CONFIG_ARCH_CHIP_STM32F → 'stm32f4' (F4/F3/F2 era, treated as F4) + CONFIG_ARCH_CHIP_STM32=y → 'stm32f1' + IMXRT (case-insensitive) → 'imxrt' + + Fallback: scan any ``CMakeLists.txt`` under the board directory for the + string "imxrt" (catches boards that configure the chip via cmake rather + than Kconfig). + + Returns 'unknown' when no pattern matches; callers that depend on family + (e.g. BDSHOT_CAPTURE_MAP, UART_HW_ORDER) will produce empty output rather + than incorrect data. + """ + defconfig = board_path / "nuttx-config/nsh/defconfig" + if defconfig.exists(): + text = defconfig.read_text(errors="ignore") + if "CONFIG_ARCH_CHIP_STM32H7" in text: + return "stm32h7" + if "CONFIG_ARCH_CHIP_STM32F7" in text: + return "stm32f7" + if re.search(r'CONFIG_ARCH_CHIP_STM32F[0-9]', text): + return "stm32f4" # F4 and F1 era + if re.search(r'CONFIG_ARCH_CHIP_STM32=y|CONFIG_ARCH_CHIP_STM32F1', text): + return "stm32f1" + if re.search(r'IMXRT|imxrt', text, re.IGNORECASE): + return "imxrt" + # Try cmake + for cmake in board_path.rglob("CMakeLists.txt"): + txt = cmake.read_text(errors="ignore") + if "imxrt" in txt.lower(): + return "imxrt" + return "unknown" + + +def parse_chip_variant(board_path: Path) -> dict: + """Extract the exact MCU chip model from nuttx-config/nsh/defconfig. + + Reads ``CONFIG_ARCH_CHIP_=y`` and strips the 2-character package + suffix to produce a model string suitable as a key into CHIP_SPECS. + + Examples + -------- + CONFIG_ARCH_CHIP_STM32H743VI=y → chip_model='STM32H743' + CONFIG_ARCH_CHIP_STM32F427II=y → chip_model='STM32F427' + CONFIG_ARCH_CHIP_MIMXRT1062=y → chip_model='MIMXRT1062' + + Returns + ------- + {'chip_model': str | None, 'chip_variant_full': str | None} + """ + defconfig = board_path / 'nuttx-config/nsh/defconfig' + if defconfig.exists(): + text = defconfig.read_text(errors='ignore') + # STM32 variants: CONFIG_ARCH_CHIP_STM32H743VI=y + m = re.search(r'CONFIG_ARCH_CHIP_(STM32[A-Z0-9]+)=y', text) + if m: + full = m.group(1) # e.g. "STM32H743VI" + # Strip 2-char package/revision suffix for model lookup + model = full[:-2] if len(full) > 8 else full + return {'chip_model': model, 'chip_variant_full': full} + # iMXRT variants: CONFIG_ARCH_CHIP_MIMXRT1062CVL5A=y + m = re.search(r'CONFIG_ARCH_CHIP_(MIMXRT[A-Z0-9]+)=y', text) + if m: + full = m.group(1) + # iMXRT model = first 10 chars (e.g. "MIMXRT1062") + model = full[:10] + return {'chip_model': model, 'chip_variant_full': full} + return {'chip_model': None, 'chip_variant_full': None} + + +def parse_board_config(board_path: Path) -> dict: + """Parse PWM output count and IO-board presence from board_config.h. + + Mechanism + --------- + Source file: ``src/board_config.h`` + + **Total FMU PWM outputs** — macro ``DIRECT_PWM_OUTPUT_CHANNELS``: + #define DIRECT_PWM_OUTPUT_CHANNELS 6 + This is the number of PWM lines driven directly by the FMU (i.e. the AUX + outputs on boards with an IO board, or MAIN outputs on boards without one). + + **IO board detection** — presence of any of: + PX4IO_SERIAL, HAVE_PX4IO, PX4IO_DEVICE_PATH + These are defined whenever the board routes a UART to a co-processor + running PX4IO firmware (the io-v2 board), which provides 8 additional + "MAIN" PWM outputs. The IO output count is therefore hardcoded to 8 + (the io-v2 standard) and does not need to be parsed. + + Returns a dict with keys: + total_outputs int — FMU PWM channel count (may be absent if not found) + has_io_board bool — True if a PX4IO co-processor is present + io_outputs int — 8 if has_io_board, else 0 + """ + cfg = board_path / "src/board_config.h" + if not cfg.exists(): + return {} + text = cfg.read_text(errors="ignore") + result = {} + m = re.search(r'#define\s+DIRECT_PWM_OUTPUT_CHANNELS\s+(\d+)', text) + if m: + result["total_outputs"] = int(m.group(1)) + m = re.search(r'#define\s+BOARD_NUM_IO_TIMERS\s+(\d+)', text) + if m: + result["num_timers"] = int(m.group(1)) + # Check for IO board (MAIN outputs) + if "PX4IO_SERIAL" in text or "HAVE_PX4IO" in text or "PX4IO_DEVICE_PATH" in text: + result["has_io_board"] = True + # IO board version 2 always has 8 outputs; detect version for future-proofing + m = re.search(r'#define\s+BOARD_USES_PX4IO_VERSION\s+(\d+)', text) + result["io_outputs"] = 8 # io-v2 standard + else: + result["has_io_board"] = False + result["io_outputs"] = 0 + return result + + +def parse_timer_config(board_path: Path) -> dict: + """Parse PWM timer groups and output channels from timer_config.cpp. + + Mechanism + --------- + Source file: ``src/timer_config.cpp`` + + PX4 defines PWM outputs through two compile-time ``constexpr`` arrays: + + **io_timers[]** — one entry per hardware timer used for PWM. + Each entry is constructed by one of: + initIOTimer(Timer::Timer5) — no DShot DMA + initIOTimer(Timer::Timer5, DMA{DMA::Index1, …}) — DShot capable + + The presence of a ``DMA{…}`` argument is the indicator that the entire + timer group supports DShot. The timer name (e.g. ``Timer5``) becomes the + group identifier. + + **timer_io_channels[]** — one entry per PWM output channel, in output order + (FMU_CH1, FMU_CH2, …). Each entry is constructed by one of: + initIOTimerChannel(io_timers, {Timer::Timer5, Timer::Channel1}, …) + initIOTimerChannelOutputClear(…) — same semantics, clears on disable + initIOTimerChannelDshot(…) — iMXRT only: marks channel as DShot + + The channel index (Channel1 = index 0, Channel2 = index 1, …) is extracted + and stored for use by compute_bdshot() when determining per-channel BDShot + DMA capability. Channels are counted sequentially to assign output numbers. + + **iMXRT special case** — NXP iMXRT boards use a PWM peripheral with a + different API: timer names are ``PWM1_PWM_A``, ``PWM1_PWM_B``, etc. + If io_timers[] is empty after parsing (no ``initIOTimer`` calls), the timer + list is reconstructed by grouping channels by their ``PWMn`` prefix. + DShot capability is inferred from any channel in the group having + ``initIOTimerChannelDshot``. + + Returns {'timers': [...], 'channels': [...]} + """ + tc = board_path / "src/timer_config.cpp" + if not tc.exists(): + return {} + + text = tc.read_text(errors="ignore") + + # Extract comment block for channel assignments (if present) + comment_assignments = [] + for line in text.split('\n'): + # e.g. "* TIM5_CH4 T FMU_CH1" + m = re.match(r'\s*\*\s+(TIM\w+_CH\w+)\s+\w+\s+(FMU_CH\d+)', line) + if m: + comment_assignments.append((m.group(1), m.group(2))) + + # Parse io_timers[] array entries + # Handle: initIOTimer(Timer::Timer5, DMA{DMA::Index1}) + # Handle: initIOTimer(Timer::Timer3, DMA{DMA::Index1, DMA::Stream2, DMA::Channel5}) + # Handle: initIOTimer(Timer::Timer12) -- no DMA + timers = [] + # Find the io_timers[] array block + io_timers_match = re.search( + r'constexpr\s+io_timers_t\s+io_timers\[.*?\]\s*=\s*\{(.*?)\};', + text, re.DOTALL + ) + if io_timers_match: + block = re.sub(r'//[^\n]*', '', io_timers_match.group(1)) + for entry in re.finditer(r'initIOTimer\s*\(\s*Timer::(\w+)(.*?)\)', block): + timer_name = entry.group(1) + rest = entry.group(2).strip() + has_dma = bool(re.search(r'DMA\s*\{', rest)) + timers.append({ + "timer": timer_name, + "dshot": has_dma, + }) + + # Parse timer_io_channels[] array entries + # Handle: initIOTimerChannel(io_timers, {Timer::Timer5, Timer::Channel1}, ...) + # Handle: initIOTimerChannelOutputClear(...), initIOTimerChannelDshot(...) + channels = [] + io_channels_match = re.search( + r'constexpr\s+timer_io_channels_t\s+timer_io_channels\[.*?\]\s*=\s*\{(.*?)\};', + text, re.DOTALL + ) + if io_channels_match: + # Strip C++ line comments to avoid parsing commented-out channels + block = re.sub(r'//[^\n]*', '', io_channels_match.group(1)) + output_idx = 1 + for entry in re.finditer( + r'(initIOTimerChannel(?:OutputClear|Dshot)?)\s*\(\s*io_timers\s*,\s*\{(?:Timer|PWM)::(\w+)\s*,\s*(?:Timer::|PWM::)?(\w+)\}', + block + ): + func_name = entry.group(1) + timer_name = entry.group(2) + channel_name = entry.group(3) + is_dshot = "Dshot" in func_name + # Skip channel mapping entries (not actual channels) + if "Mapping" in func_name: + continue + # Extract channel number + ch_num_match = re.search(r'Channel(\d+)|Submodule(\d+)|CH(\d+)', channel_name) + ch_idx = None + if ch_num_match: + ch_idx = int(next(g for g in ch_num_match.groups() if g is not None)) - 1 + channels.append({ + "output_index": output_idx, + "timer": timer_name, + "channel": channel_name, + "channel_index": ch_idx, + "is_dshot_channel": is_dshot, # iMXRT only + }) + output_idx += 1 + + # For NXP iMXRT boards: timers are derived from unique PWM module names in channels + if not timers and channels: + seen = {} + for ch in channels: + # e.g. timer = "PWM1_PWM_A" -> group key = "PWM1" + m = re.match(r'(PWM\d+)', ch["timer"]) + key = m.group(1) if m else ch["timer"] + if key not in seen: + seen[key] = ch.get("is_dshot_channel", False) + elif ch.get("is_dshot_channel"): + seen[key] = True + for tname, has_dshot in seen.items(): + timers.append({"timer": tname, "dshot": has_dshot}) + + return { + "timers": timers, + "channels": channels, + } + + +def _serial_label(raw: str) -> str: + """Normalise a CONFIG_BOARD_SERIAL_* key to a display label. + + TEL1 → TELEM1, TEL2 → TELEM2, etc. All other keys are returned as-is. + """ + m = re.fullmatch(r'TEL(\d+)', raw) + return f'TELEM{m.group(1)}' if m else raw + + +def parse_serial_config(board_path: Path) -> dict: + """Parse serial port configuration, producing a UART → device → label mapping. + + Mechanism + --------- + + **Labels** — ``default.px4board`` (primary, authoritative source) + Each board's Kconfig file contains ``CONFIG_BOARD_SERIAL_