MZTC thermal camera integration — merged + resolved against maintenance-10.x (supersedes PR #11005) - #11837
Conversation
Signed-off-by: William Dunn <wdunn001@gmail.com>
…r Dependency, Remove Duplicate OSD Header, Add Safe Reconnection API
Signed-off-by: William Dunn <wdunn001@gmail.com>
Signed-off-by: wdunn001 <your-email@example.com>
…ht#11005 # Conflicts: # .gitignore # src/main/CMakeLists.txt # src/main/config/parameter_group_ids.h # src/main/fc/cli.c # src/main/fc/fc_msp.c
The PR added /src/main/target and assorted CMake/CLion build artifacts (CMakeFiles, Makefile, CMakeCache.txt, eeprom.bin, inav_9.0.0_SITL) to .gitignore. /src/main/target would hide all future target boards from git status (962 files tracked there), and the rest are artifacts of a different project's build layout. maintenance-10.x already covers build output dirs (/build_*/, /build-*/, /build_hw/, build_sitl/), so the base version is correct on its own.
The 1.78 MB compiled firmware image was committed in the feature PR
("adding presets and updating docs") and is referenced nowhere in the
repo. Compiled binaries do not belong in the source tree.
|
ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing |
The settings_md CI check failed: the committed docs/Settings.md was generated against an older settings.yaml (author's branch, March) and didn't match the generator output for the merged settings (enum tables now use the "Allowed Values" format). Regenerated with src/utils/update_cli_docs.py; verified stable (re-running produces no further changes).
PR Summary by QodoIntegrate MassZero thermal camera support with flash-safe gating
AI Description
Diagram
High-Level Assessment
Files changed (26)
|
Code Review by Qodo
1. Camera packets omit terminator
|
| // Size field is N+4 per protocol (addr..data..checksum), where N = 3(command bytes)+1(flags)+data_len | ||
| packet.size = (uint8_t)(4 + 3 + 1 + data_len); | ||
|
|
||
| // Total bytes on wire = 1(begin) + 1(size) + (size) + 1(end) | ||
| const uint8_t totalLen = (uint8_t)(1 + 1 + packet.size + 1); |
There was a problem hiding this comment.
1. Camera packets omit terminator 🐞 Bug ≡ Correctness
mztcSendPacket() declares size = data_len + 8 and writes only that prefix of a struct whose checksum and terminator follow a fixed 14-byte data array, so commands contain uninitialized data rather than their checksum and 0xFF terminator. The included hardware test vector shows the intended zero-data packet is eight bytes with size 4, while this function emits eleven malformed bytes.
Agent Prompt
## Issue description
MZTC commands are serialized from a fixed-layout struct using a variable prefix, which omits the checksum/end fields and emits uninitialized data.
## Issue Context
Build the wire packet contiguously as begin, size, address, command bytes, flags, exactly `data_len` data bytes, checksum, and end marker. Keep the size/total-length calculation consistent with the receive parser and protocol test vector.
## Fix Focus Areas
- src/main/io/mztc_camera.c[490-526]
- src/utils/thermal_bridge_debug.py[85-89]
- src/utils/test_thermal_camera.py[18-31]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| // Check flags | ||
| uint8_t flags = data[5]; | ||
| if (flags == MZTC_FLAG_SUCCESS) { | ||
| // Command executed successfully | ||
| mztcStatus.error_flags &= ~MZTC_ERROR_COMMUNICATION; |
There was a problem hiding this comment.
2. Successful replies are discarded 🐞 Bug ≡ Correctness
mztcProcessResponse() only clears an error bit for successful replies and never decodes any response payload into temperatures, status, or frame data. Consequently the new status/MSP/OSD surfaces cannot expose actual camera measurements even after serial framing is fixed.
Agent Prompt
## Issue description
Successful camera responses are acknowledged but their payloads are ignored, leaving all measurement fields at defaults.
## Issue Context
Dispatch responses by class/subclass, validate each payload length, decode values into durable status/frame state, and update timestamps only when corresponding data is received.
## Fix Focus Areas
- src/main/io/mztc_camera.c[534-570]
- src/main/io/mztc_camera.c[573-592]
- src/main/config/mztc_camera.h[120-148]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| if (mztcSerialPort != NULL) { | ||
| // Successfully opened port | ||
| mztcStatus.connected = true; | ||
| mztcStatus.status = MZTC_STATUS_READY; |
There was a problem hiding this comment.
3. Port open fakes connection 🐞 Bug ☼ Reliability
Opening the configured serial port immediately marks the camera connected and ready without receiving or validating any camera reply. mztcUpdateStatus() then forces connection quality to 100 and no timeout consumes mztcLastDataReceived, so an absent or disconnected camera remains reported healthy indefinitely.
Agent Prompt
## Issue description
Serial-port availability is treated as proof that a camera is connected, and connectivity never expires when replies stop.
## Issue Context
Keep the state initializing after opening the port, send a supported probe, transition to ready only after a valid response, and disconnect/retry after a receive timeout.
## Fix Focus Areas
- src/main/io/mztc_camera.c[219-258]
- src/main/io/mztc_camera.c[447-486]
- src/main/io/mztc_camera.c[573-592]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| // Process the response data here | ||
| // This is where we'd extract the actual thermal information | ||
|
|
||
| return true; |
There was a problem hiding this comment.
4. Frame request leaks stack 🐞 Bug ⛨ Security
mztcReadThermalFrame() returns success immediately after transmitting while explicitly leaving frameData untouched, so MSP2_MZTC_FRAME_DATA serializes an uninitialized stack object. This exposes stack contents in the MSP response and can also derive an unsafe copy length from uninitialized width and height.
Agent Prompt
## Issue description
The frame-read function reports success without populating its output, causing the MSP handler to serialize uninitialized stack memory.
## Issue Context
Store decoded frames asynchronously in initialized driver state. Return true only when a complete validated frame has been copied to the caller; otherwise return false, and initialize the MSP response before writing it.
## Fix Focus Areas
- src/main/io/mztc_camera.c[651-701]
- src/main/fc/fc_msp.c[2061-2085]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| cfgMutable->baudrate = config->baudrate; | ||
| cfgMutable->mode = config->mode; | ||
| cfgMutable->update_rate = config->update_rate; | ||
| cfgMutable->temperature_unit = config->temperature_unit; |
There was a problem hiding this comment.
5. Zero rate crashes scheduler 🐞 Bug ≡ Correctness
MSP2_SET_MZTC_CONFIG copies update_rate without validation, allowing a client to set it to zero. The periodic camera task then evaluates 1000 / mztcConfig()->update_rate, causing an integer divide-by-zero fault.
Agent Prompt
## Issue description
The active MSP config handler accepts an update rate of zero, which is later used as a divisor by the scheduler task.
## Issue Context
Validate the entire request against the declared MZTC limits before changing any parameter-group field. Reject invalid enum, boolean, serial-port, baud, rate, interval, percentage, and temperature combinations atomically.
## Fix Focus Areas
- src/main/fc/fc_msp.c[4075-4105]
- src/main/io/mztc_camera.c[261-263]
- src/main/config/mztc_camera.h[168-172]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const mztcConfig_t *cfg = mztcConfig(); | ||
| msp_mztc_config_t *config = (msp_mztc_config_t*)sbufPtr(dst); | ||
|
|
||
| config->enabled = cfg->enabled; |
There was a problem hiding this comment.
6. Msp wire layout is unstable 🐞 Bug ≡ Correctness
The new MSP handlers cast stream buffers to mixed-width C structs and use sizeof(struct) as the payload length, making protocol offsets and lengths depend on compiler padding. For example, padding is inserted between the config's 17 byte fields and its first float, so clients serializing the documented fields contiguously cannot interoperate reliably.
Agent Prompt
## Issue description
MZTC MSP payloads expose native C struct layout, including alignment padding, as the wire protocol.
## Issue Context
Define fixed field order and widths, then use `sbufRead*`/`sbufWrite*` helpers for every request and response. Encode floating-point values through an explicitly specified representation rather than unaligned struct casts.
## Fix Focus Areas
- src/main/fc/fc_msp.c[1995-2088]
- src/main/fc/fc_msp.c[4073-4184]
- src/main/msp/msp_mztc.h[50-105]
- src/main/msp/msp_mztc.c[48-140]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| char temp_str[32]; | ||
| snprintf(temp_str, sizeof(temp_str), "TEMP: %.1fC", (double)status->ambient_temperature); | ||
|
|
||
| // Note: In a real implementation, you would use the OSD drawing functions |
There was a problem hiding this comment.
7. Mztc osd renders nothing 🐞 Bug ≡ Correctness
The only MZTC OSD update function has no call site, and each draw routine merely formats a local string or contains placeholder comments without invoking an OSD API. Enabling the new OSD settings therefore displays none of the advertised temperature, status, alert, calibration, or connection elements.
Agent Prompt
## Issue description
The MZTC OSD module is initialized but never updated, and its draw functions do not render output.
## Issue Context
Integrate MZTC values with INAV's existing OSD element/render pipeline, schedule updates through that pipeline, honor configured positions/visibility, and remove placeholder formatting that has no output.
## Fix Focus Areas
- src/main/io/osd/mztc_camera_osd.c[70-219]
- src/main/fc/fc_init.c[567-572]
- src/main/io/osd/mztc_camera_osd.h[48-61]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| # Add our thermal camera test | ||
| set_property(SOURCE mztc_camera_unittest.cc PROPERTY depends | ||
| "io/mztc_camera.c" "drivers/serial.c" "drivers/time.c" "common/parameter_group.c") | ||
| set_property(SOURCE mztc_camera_unittest.cc PROPERTY definitions UNIT_TEST USE_MZTC) |
There was a problem hiding this comment.
8. Mztc tests compile empty 🐞 Bug ⚙ Maintainability
The PR applies the MZTC test's source dependencies and USE_MZTC definition after the loop that has already created every test target. The test is therefore built without USE_MZTC, causing its entire body to be removed by the file-level preprocessor guard and providing no coverage.
Agent Prompt
## Issue description
MZTC unit-test source properties are assigned after the generic loop consumes those properties and creates the test target.
## Issue Context
Move the MZTC source properties alongside the other per-test declarations before `file(GLOB TEST_PROGRAMS ...)` and the `foreach(unit_test)` loop, then verify the test target contains actual tests.
## Fix Focus Areas
- src/test/unit/CMakeLists.txt[1-60]
- src/test/unit/CMakeLists.txt[151-203]
- src/test/unit/mztc_camera_unittest.cc[1-6]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
RAM / Flash usage vs. base branch — commit
See RAM/flash optimization guide for techniques to reduce usage. |
|
Test firmware build ready — commit Download firmware for PR #11837 247 targets built. Find your board's
|
MCU_FLASH_SIZE is defined per-MCU in cmake and, for SITL, only in src/main/target/SITL/target.h — which platform.h includes AFTER target/common.h. At the gate, SITL's MCU_FLASH_SIZE was therefore undefined (0 > 512 = false), silently compiling out USE_MZTC and the feature's SITL tooling (mztc_simulate CLI, thermal_bridge.py, simulated frame path). SITL has no flash constraint, so include SITL_BUILD (set by cmake/sitl.cmake) in the gate.
Review notes from the merge-resolution passFirst — thank you for the MZTC feature; the integration is substantial and the merge onto current A few notes came out of a close look at the merged result. I took a look at this with an AI-based tool I have — it may well be wrong on any of the points below, so please treat them as questions to check rather than a verdict, and correct me if I've misread something: Questions for the author1. MSP2 command IDs — do the 0x3000/0x3001 values collide with the Betaflight-compat range? 2. MSP2_MZTC_FRAME_DATA — could it be returning uninitialized stack data? 3. OSD module — is it scaffolding for a follow-up, or should it be wired up? 4. Serial framing — could the packet length math be off by one? 5. MSP2_SET_MZTC_CONFIG — should it validate ranges like the CLI settings do? 6. Dead code — mztc_camera_cli.c, msp_mztc.c, and the unit test?
7. Defaults — do the C reset template and settings.yaml agree? 8. Smaller items (for awareness):
What the merge itself changed (for transparency)For anyone reviewing: the merge-resolution commits did not alter the feature's behavior — they (1) merged |
Enabling the feature in SITL (previous commit) surfaced two latent issues in the feature code that only manifest when USE_MZTC is actually compiled in: - mztc_camera.c used fprintf(stderr, ...) via the SD() macro without including <stdio.h> (6 'stderr undeclared' compile errors). - mztc_camera_osd.c declared mztcOsdConfig via PG_DECLARE and used PG_MZTC_OSD_CONFIG (1047) but never registered the parameter group (undefined reference to mztcOsdConfig_System at link time). Added PG_REGISTER_WITH_RESET_TEMPLATE + PG_RESET_TEMPLATE following the mztcConfig pattern, with the MZTC_OSD_DEFAULT_* defines moved above the template so the macros resolve. SITL now compiles and links with the camera code active (verified via fresh cmake -DSITL=ON + make SITL build).
|
Update on the two build-blockers — resolved in the merge so SITL CI is green: Enabling the feature in SITL (the gate fix) surfaced two latent issues in
These are the author's code, completed minimally to make the feature build — Everything else from the earlier comment (MSP2 ID range, frame-data stub, |
…camera manual Answers all eight Qodo findings and all eight questions from the review comment on iNavFlight#11837, then corrects the driver against the BJ core serial protocol manual that ships with the camera. Serial protocol The old sender wrote a variable-length prefix of a fixed-layout struct. The checksum and the 0xFF terminator sat past that prefix. Neither was ever transmitted and uninitialized bytes went out in their place. The size field was payload+8 where the protocol says payload+4. For a full payload the computed length ran past the end of the struct. The packet is now built contiguously and matches the manual's own worked example byte for byte: brightness 100 is F0 05 36 78 02 00 64 14 FF. The receive parser is length driven. A 0xF0 or 0xFF byte inside a payload can no longer split or truncate a packet. Responses are decoded by class and subclass with per-payload length checks. Corrections from the camera manual Auto shutter values were off by one. The camera takes 0x01 temperature only, 0x02 time only and 0x03 time and temperature. It answers 0x00 with a threshold error. The driver sent the zero-based setting straight through. The default sent an out of range value. The shutter interval belongs to the camera. 0x7C/0x05 takes two bytes of minutes and the camera runs the schedule from it. The driver never sent that command and ran a competing host-side timer instead. The interval is pushed on connect now. mztc_ffc_interval accepts 1 to 60. TEMP_ONLY is how time-driven correction is turned off. The initialization status reply arrives on class 0x7D subclass 0x06. The host asks on 0x7C/0x14. The decoder matched the request address and never fired. Connection state Opening the UART no longer counts as a connected camera. The driver probes for the device model and reports connected once the camera answers. An established link that stops answering for three seconds is closed and retried. connection_quality is the share of recent probes answered. It was hardcoded to 100. The configuration burst moved out of the serial receive interrupt. Removed surfaces The camera exposes a UART for control and a composite video output for the picture. It has no digital data interface. Its manual defines 26 class and subclass pairs. None of them read a frame or a temperature. 0x78/0x01 appears only in the manual's invalid-subclass error example. 0x74/0x0C reads the ISP parameter version number. Frame data is removed. mztcFrameData_t, mztcGetFrameData(), MSP2_MZTC_FRAME_DATA, the undocumented 0x78/0x30 request and the rand() based simulator are all gone, along with frame_count and last_frame_time. Temperature is removed. camera_temperature, ambient_temperature, mztc_temperature_unit, mztc_temperature_alerts, mztc_alert_high_temp, mztc_alert_low_temp and MSP2_SET_MZTC_ALERTS are gone. Three settings were stored and validated but never transmitted. They did nothing. mztc_bad_pixel_removal drove an interactive on-screen cursor that a flight controller cannot walk. mztc_vignetting_correction is a one-shot action at 0x7C/0x0C that needs the lens on a uniform surface first. It becomes the mztc_vignetting command and MSP2_SET_MZTC_VIGNETTING. mztc_crosshair_enabled had no camera command at all. MSP The commands move out of 0x3000, where MSP2_BETAFLIGHT_BIND and MSP2_RX_BIND already live, into a contiguous block from 0x2240 to 0x2249. The identifiers nothing handled are dropped along with the duplicate aliases. Every field is serialized with sbufRead and sbufWrite. No struct is cast over the stream buffer. Padding and alignment stay off the wire. The config payload is 15 bytes and the status payload 7. SET_MZTC_CONFIG validates the whole request before applying any of it. An update_rate of zero was a reachable divide-by-zero in the camera task and an unbounded baudrate indexed past baudRates[]. OSD The module formatted strings and drew nothing. OSD_MZTC_STATUS replaces it as a real element driven by INAV's own layout and render pipeline. That grows OSD_ITEM_COUNT. Adding items moves the per-layout offsets in osdLayoutsConfig. PG_OSD_LAYOUTS_CONFIG therefore goes to version 4. Dead code mztc_camera_cli.c was not in CMakeLists, was never initialized and called APIs that do not exist. msp_mztc.c had a dispatch that was never invoked. Both are removed. msp_mztc.h stays for the command IDs and the payload layouts. mztc_shutter was identical to mztc_calibrate and mztc_simulate faked link liveness. mztc_save and mztc_defaults are added so the camera flash commands are reachable. Configuration The reset template is driven from the SETTING_*_DEFAULT macros. The fresh-EEPROM defaults and the CLI defaults cannot diverge. That settles the baudrate at 115200 and the mode at STANDBY. The limits live in the settings.yaml constants block. Compile-time assertions check that the C limits still match. A value the CLI rejects cannot be accepted over MSP. last_calibration widens to uint16 after wrapping at about 4.25 hours. The PG version goes to 1 because the PR test builds are already out. Tests The unit test properties were applied after the loop that creates every target. USE_MZTC never reached the compiler. The whole test body was preprocessed away. They move ahead of the loop. The tautological assertions are replaced with 40 tests across three suites. They cover the wire format against the hardware capture, the configuration validator, and the receive path driven through the serial callback. The receive tests exercise framing resynchronisation, payloads containing framing markers, checksum rejection, response dispatch, and the exact bytes the configuration burst puts on the wire. Five mutations confirmed the tests fail when each fixed bug is reintroduced. Docs The feature doc is rewritten around commands that exist. Its preset, camera management, video input and OSD settings were never implemented. docs/Settings.md is regenerated. The MSP messages are added by hand to msp_messages.json per docs/development/msp/README.md, with the MSP docs regenerated. Validated on three builds. SPEEDYBEEF405AIO with MZTC on, MATEKF722SE with MZTC off, and SITL. All three compile with no warnings in any MZTC file. 556 of 556 unit tests pass.
Follows the firmware changes in iNavFlight/inav#11837. MSP codes The commands move from 0x3000 into INAV's own range and now sit contiguously from 0x2240 to 0x2249. 0x3000 and 0x3001 are MSP2_BETAFLIGHT_BIND and MSP2_RX_BIND. The old block collided with them. The codes nothing implemented are dropped. CALIBRATE, INIT_STATUS, SAVE_CONFIG, RESTORE_DEFAULTS and RECONNECT had no firmware handler. MSP2_SET_MZTC_VIGNETTING is added. Payloads MSP2_MZTC_CONFIG is a fixed 15 byte payload read one field at a time. The old parser assumed an unpadded C struct and read the thresholds at offsets 17 and 21. The firmware struct had alignment padding there. Both offsets were wrong on the wire in both directions. The send path no longer writes float32. MSP2_MZTC_STATUS is a fixed 7 byte payload and is parsed at all now. It was never handled. connected is a real field, set only after the camera answers a command. Removed fields The five RC channel fields were never in the payload. The temperature fields, the frame fields, bad pixel removal, vignetting correction and the crosshair flag are gone from the firmware. The camera reports no temperature and no frame over its serial protocol. The other three were stored but never transmitted. Serial port index mztc_port is the zero-based serialPortIdentifier_e value that the firmware hands to openSerialPort(). Three places added 1 to it and special-cased UART6. That pointed the driver at the wrong UART. OSD The ten elements at invented ids 200 to 209 are replaced with the one the firmware provides, MZTC_STATUS at id 171. The old entries were gated on FC.FEATURES.MZTC. That does not exist. A localization string is added for the name. State FC.MZTC_CONFIG and FC.MZTC_STATUS are declared in fc.js resetState like every other FC block. MZTC_CONFIG was previously created ad-hoc by the MSP handler. Localization The MassZero block had been inserted into messages.json twice. All 17 keys were duplicated. JSON keeps the last definition, leaving the earlier copy as dead weight. The earlier copy is removed along with the strings for the settings that no longer exist. The operating mode help text described frame capture modes the firmware does not have. Dependency electron-prebuilt-compile is reverted along with the 13385 lines of package-lock churn it pulled in. Nothing in the tree references it.
…camera manual Answers all eight Qodo findings and all eight questions from the review comment on iNavFlight#11837, then corrects the driver against the BJ core serial protocol manual that ships with the camera. Serial protocol The old sender wrote a variable-length prefix of a fixed-layout struct. The checksum and the 0xFF terminator sat past that prefix. Neither was ever transmitted and uninitialized bytes went out in their place. The size field was payload+8 where the protocol says payload+4. For a full payload the computed length ran past the end of the struct. The packet is now built contiguously and matches the manual's own worked example byte for byte: brightness 100 is F0 05 36 78 02 00 64 14 FF. The receive parser is length driven. A 0xF0 or 0xFF byte inside a payload can no longer split or truncate a packet. Responses are decoded by class and subclass with per-payload length checks. Corrections from the camera manual Auto shutter values were off by one. The camera takes 0x01 temperature only, 0x02 time only and 0x03 time and temperature. It answers 0x00 with a threshold error. The driver sent the zero-based setting straight through. The default sent an out of range value. The shutter interval belongs to the camera. 0x7C/0x05 takes two bytes of minutes and the camera runs the schedule from it. The driver never sent that command and ran a competing host-side timer instead. The interval is pushed on connect now. mztc_ffc_interval accepts 1 to 60. TEMP_ONLY is how time-driven correction is turned off. The initialization status reply arrives on class 0x7D subclass 0x06. The host asks on 0x7C/0x14. The decoder matched the request address and never fired. Connection state Opening the UART no longer counts as a connected camera. The driver probes for the device model and reports connected once the camera answers. An established link that stops answering for three seconds is closed and retried. connection_quality is the share of recent probes answered. It was hardcoded to 100. The configuration burst moved out of the serial receive interrupt. Removed surfaces The camera exposes a UART for control and a composite video output for the picture. It has no digital data interface. Its manual defines 26 class and subclass pairs. None of them read a frame or a temperature. 0x78/0x01 appears only in the manual's invalid-subclass error example. 0x74/0x0C reads the ISP parameter version number. Frame data is removed. mztcFrameData_t, mztcGetFrameData(), MSP2_MZTC_FRAME_DATA, the undocumented 0x78/0x30 request and the rand() based simulator are all gone, along with frame_count and last_frame_time. Temperature is removed. camera_temperature, ambient_temperature, mztc_temperature_unit, mztc_temperature_alerts, mztc_alert_high_temp, mztc_alert_low_temp and MSP2_SET_MZTC_ALERTS are gone. The port and its baud rate now come from the Ports tab through findSerialPortConfig(FUNCTION_MZTC_CAMERA), the way every other serial peripheral in INAV works. Assigning the function is what enables the camera. mztc_enabled, mztc_port and mztc_baudrate are all gone. That removes the class of bug where the setting and the Ports tab could disagree about which UART the camera is on. Three further settings were stored and validated but never transmitted. They did nothing. mztc_bad_pixel_removal drove an interactive on-screen cursor that a flight controller cannot walk. mztc_vignetting_correction is a one-shot action at 0x7C/0x0C that needs the lens on a uniform surface first. It becomes the mztc_vignetting command and MSP2_SET_MZTC_VIGNETTING. mztc_crosshair_enabled had no camera command at all. MSP The commands move out of 0x3000, where MSP2_BETAFLIGHT_BIND and MSP2_RX_BIND already live, into a contiguous block from 0x2240 to 0x2249. The identifiers nothing handled are dropped along with the duplicate aliases. Every field is serialized with sbufRead and sbufWrite. No struct is cast over the stream buffer. Padding and alignment stay off the wire. The config payload is 12 bytes and the status payload 7. SET_MZTC_CONFIG validates the whole request before applying any of it. An update_rate of zero was a reachable divide-by-zero in the camera task and an unbounded baudrate indexed past baudRates[]. OSD The module formatted strings and drew nothing. OSD_MZTC_STATUS replaces it as a real element driven by INAV's own layout and render pipeline. That grows OSD_ITEM_COUNT. Adding items moves the per-layout offsets in osdLayoutsConfig. PG_OSD_LAYOUTS_CONFIG therefore goes to version 4. Dead code mztc_camera_cli.c was not in CMakeLists, was never initialized and called APIs that do not exist. msp_mztc.c had a dispatch that was never invoked. Both are removed. msp_mztc.h stays for the command IDs and the payload layouts. mztc_shutter was identical to mztc_calibrate and mztc_simulate faked link liveness. mztc_save and mztc_defaults are added so the camera flash commands are reachable. Configuration The reset template is driven from the SETTING_*_DEFAULT macros. The fresh-EEPROM defaults and the CLI defaults cannot diverge. That settles the baudrate at 115200 and the mode at STANDBY. The limits live in the settings.yaml constants block. Compile-time assertions check that the C limits still match. A value the CLI rejects cannot be accepted over MSP. last_calibration widens to uint16 after wrapping at about 4.25 hours. The PG version goes to 1 because the PR test builds are already out. Tests The unit test properties were applied after the loop that creates every target. USE_MZTC never reached the compiler. The whole test body was preprocessed away. They move ahead of the loop. The tautological assertions are replaced with 38 tests across three suites. They cover the wire format against the hardware capture, the configuration validator, and the receive path driven through the serial callback. The receive tests exercise framing resynchronisation, payloads containing framing markers, checksum rejection, response dispatch, and the exact bytes the configuration burst puts on the wire. Five mutations confirmed the tests fail when each fixed bug is reintroduced. Docs The feature doc is rewritten around commands that exist. Its preset, camera management, video input and OSD settings were never implemented. docs/Settings.md is regenerated. The MSP messages are added by hand to msp_messages.json per docs/development/msp/README.md, with the MSP docs regenerated. Validated on three builds. SPEEDYBEEF405AIO with MZTC on, MATEKF722SE with MZTC off, and SITL. All three compile with no warnings in any MZTC file. 554 of 554 unit tests pass.
…camera manual Answers all eight Qodo findings and all eight questions from the review comment on iNavFlight#11837, then corrects the driver against the BJ core serial protocol manual that ships with the camera. Serial protocol The old sender wrote a variable-length prefix of a fixed-layout struct. The checksum and the 0xFF terminator sat past that prefix. Neither was ever transmitted and uninitialized bytes went out in their place. The size field was payload+8 where the protocol says payload+4. For a full payload the computed length ran past the end of the struct. The packet is now built contiguously and matches the manual's own worked example byte for byte: brightness 100 is F0 05 36 78 02 00 64 14 FF. The receive parser is length driven. A 0xF0 or 0xFF byte inside a payload can no longer split or truncate a packet. Responses are decoded by class and subclass with per-payload length checks. Corrections from the camera manual Auto shutter values were off by one. The camera takes 0x01 temperature only, 0x02 time only and 0x03 time and temperature. It answers 0x00 with a threshold error. The driver sent the zero-based setting straight through. The default sent an out of range value. The shutter interval belongs to the camera. 0x7C/0x05 takes two bytes of minutes and the camera runs the schedule from it. The driver never sent that command and ran a competing host-side timer instead. The interval is pushed on connect now. mztc_ffc_interval accepts 1 to 60. TEMP_ONLY is how time-driven correction is turned off. The initialization status reply arrives on class 0x7D subclass 0x06. The host asks on 0x7C/0x14. The decoder matched the request address and never fired. Connection state Opening the UART no longer counts as a connected camera. The driver probes for the device model and reports connected once the camera answers. An established link that stops answering for three seconds is closed and retried. connection_quality is the share of recent probes answered. It was hardcoded to 100. The configuration burst moved out of the serial receive interrupt. Removed surfaces The camera exposes a UART for control and a composite video output for the picture. It has no digital data interface. Its manual defines 26 class and subclass pairs. None of them read a frame or a temperature. 0x78/0x01 appears only in the manual's invalid-subclass error example. 0x74/0x0C reads the ISP parameter version number. Frame data is removed. mztcFrameData_t, mztcGetFrameData(), MSP2_MZTC_FRAME_DATA, the undocumented 0x78/0x30 request and the rand() based simulator are all gone, along with frame_count and last_frame_time. Temperature is removed. camera_temperature, ambient_temperature, mztc_temperature_unit, mztc_temperature_alerts, mztc_alert_high_temp, mztc_alert_low_temp and MSP2_SET_MZTC_ALERTS are gone. The port and its baud rate now come from the Ports tab through findSerialPortConfig(FUNCTION_MZTC_CAMERA), the way every other serial peripheral in INAV works. Assigning the function is what enables the camera. mztc_enabled, mztc_port and mztc_baudrate are all gone. That removes the class of bug where the setting and the Ports tab could disagree about which UART the camera is on. Three further settings were stored and validated but never transmitted. They did nothing. mztc_bad_pixel_removal drove an interactive on-screen cursor that a flight controller cannot walk. mztc_vignetting_correction is a one-shot action at 0x7C/0x0C that needs the lens on a uniform surface first. It becomes the mztc_vignetting command and MSP2_SET_MZTC_VIGNETTING. mztc_crosshair_enabled had no camera command at all. MSP The commands move out of 0x3000, where MSP2_BETAFLIGHT_BIND and MSP2_RX_BIND already live, into a contiguous block from 0x2240 to 0x2249. The identifiers nothing handled are dropped along with the duplicate aliases. Every field is serialized with sbufRead and sbufWrite. No struct is cast over the stream buffer. Padding and alignment stay off the wire. The config payload is 12 bytes and the status payload 7. SET_MZTC_CONFIG validates the whole request before applying any of it. An update_rate of zero was a reachable divide-by-zero in the camera task and an unbounded baudrate indexed past baudRates[]. OSD The module formatted strings and drew nothing. OSD_MZTC_STATUS replaces it as a real element driven by INAV's own layout and render pipeline. That grows OSD_ITEM_COUNT. Adding items moves the per-layout offsets in osdLayoutsConfig. PG_OSD_LAYOUTS_CONFIG therefore goes to version 4. Dead code mztc_camera_cli.c was not in CMakeLists, was never initialized and called APIs that do not exist. msp_mztc.c had a dispatch that was never invoked. Both are removed. msp_mztc.h stays for the command IDs and the payload layouts. mztc_shutter was identical to mztc_calibrate and mztc_simulate faked link liveness. mztc_save and mztc_defaults are added so the camera flash commands are reachable. Configuration The reset template is driven from the SETTING_*_DEFAULT macros. The fresh-EEPROM defaults and the CLI defaults cannot diverge. That settles the baudrate at 115200 and the mode at STANDBY. The limits live in the settings.yaml constants block. Compile-time assertions check that the C limits still match. A value the CLI rejects cannot be accepted over MSP. last_calibration widens to uint16 after wrapping at about 4.25 hours. The PG version goes to 1 because the PR test builds are already out. Tests The unit test properties were applied after the loop that creates every target. USE_MZTC never reached the compiler. The whole test body was preprocessed away. They move ahead of the loop. The tautological assertions are replaced with 38 tests across three suites. They cover the wire format against the hardware capture, the configuration validator, and the receive path driven through the serial callback. The receive tests exercise framing resynchronisation, payloads containing framing markers, checksum rejection, response dispatch, and the exact bytes the configuration burst puts on the wire. Five mutations confirmed the tests fail when each fixed bug is reintroduced. Docs The feature doc is rewritten around commands that exist. Its preset, camera management, video input and OSD settings were never implemented. docs/Settings.md is regenerated. The MSP messages are added by hand to msp_messages.json per docs/development/msp/README.md, with the MSP docs regenerated. Validated on three builds. SPEEDYBEEF405AIO with MZTC on, MATEKF722SE with MZTC off, and SITL. All three compile with no warnings in any MZTC file. 554 of 554 unit tests pass.
Summary
This is an automated merged + conflict-resolved version of PR #11005 (Mass Zero
Thermal Camera integration, author wdunn001). It shows exactly what the
feature would look like once PR #11005's branch is brought up to date with
maintenance-10.x— a clean, reviewable feature diff (26 files, allMZTC-related or its flash-gating).
Why this PR exists: PR #11005's branch is 5 months old (last commit
2026-03-26) and is CONFLICTING/DIRTY against
maintenance-10.x. This PRcarries the author's feature merged onto current maintenance-10.x with
conflicts resolved and two fixes applied, so reviewers can see the real
change without a 1245-commit branch delta.
What it contains
to them): MZTC camera drivers, MSP2_MZTC_* commands (0x3000–0x3007),
CLI commands, OSD elements, settings, docs, unit tests.
.gitignore— base version taken verbatim (PR's entries dropped:/src/main/targetwould hide future target boards; the rest wereforeign CMake/CLion artifacts)
src/main/CMakeLists.txt— union (mztc files + mavlink module files)src/main/config/parameter_group_ids.h— MZTC PGs renumbered1045/1046 → 1046/1047 (maintenance-10.x now uses 1045 for
PG_DRONECAN_CONFIG);PG_INAV_ENDconditional on USE_MZTCsrc/main/fc/cli.c— base'stimer_output_modeargs kept (featureadds no timer output modes); mztc CLI entries guarded by USE_MZTC
src/main/fc/fc_msp.c— union of MSP2_MZTC_* SET cases + base's newMSP2_INAV_* cases
USE_MZTCgated#if (MCU_FLASH_SIZE > 512)incommon.h(wasunconditional), and
#ifdef USE_MZTCguards completed incli.c,fc_init.c,fc_tasks.c(the author's guard work was incomplete —prototypes were under
USE_ASSERT; function bodies/command table/initcalls/task entry were unguarded).
inav_9.0.0_SPEEDYBEEF405AIO.hex(1.78 MB compiled binary,unreferenced).
Validation
FLASH 78.6%, RAM 96.4%
clean; FLASH 95.1%, RAM 51.3%; zero warnings, zero missing symbols
Relationship to PR #11005
original discussion.
superseded. The author is credited for the feature via the preserved
commit history.
master).
Notes for review
src/main/io/mztc_camera_cli.cis dead code (not in CMakeLists.txt,mztcCliInit()never called) — flagged for the author to remove;left as-is to keep the author's content intact.
condition: USE_MZTC, unit-test USE_MZTC define) were verified sound.