Skip to content

MZTC thermal camera integration — merge conflicts resolved against maintenance-10.x (supersedes PR #11005) - #11837

Open
sensei-hacker wants to merge 24 commits into
iNavFlight:maintenance-10.xfrom
sensei-hacker:resolve-conflict-pr-11005
Open

sensei-hacker wants to merge 24 commits into
iNavFlight:maintenance-10.xfrom
sensei-hacker:resolve-conflict-pr-11005

Conversation

@sensei-hacker

@sensei-hacker sensei-hacker commented Aug 29, 2026 •

Copy link
Copy Markdown
Member

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, all
MZTC-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 PR
carries 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

  1. The author's feature unchanged (12 commits from wdunn001, credited
    to them): MZTC camera drivers, MSP2_MZTC_* commands (0x3000–0x3007),
    CLI commands, OSD elements, settings, docs, unit tests.
  2. Merge of maintenance-10.x with conflicts resolved:
    • .gitignore — base version taken verbatim (PR's entries dropped:
      /src/main/target would hide future target boards; the rest were
      foreign CMake/CLion artifacts)
    • src/main/CMakeLists.txt — union (mztc files + mavlink module files)
    • src/main/config/parameter_group_ids.h — MZTC PGs renumbered
      1045/1046 → 1046/1047 (maintenance-10.x now uses 1045 for
      PG_DRONECAN_CONFIG); PG_INAV_END conditional on USE_MZTC
    • src/main/fc/cli.c — base's timer_output_mode args kept (feature
      adds 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 new
      MSP2_INAV_* cases
  3. F722 flash-size fix (the open review finding from 2025-12-20):
    USE_MZTC gated #if (MCU_FLASH_SIZE > 512) in common.h (was
    unconditional), and #ifdef USE_MZTC guards completed in cli.c,
    fc_init.c, fc_tasks.c (the author's guard work was incomplete —
    prototypes were under USE_ASSERT; function bodies/command table/init
    calls/task entry were unguarded).
  4. Cleanup: removed the committed build artifact
    inav_9.0.0_SPEEDYBEEF405AIO.hex (1.78 MB compiled binary,
    unreferenced).

Validation

  • SPEEDYBEEF405AIO (F405, 1024 KB) — USE_MZTC=ON: builds clean;
    FLASH 78.6%, RAM 96.4%
  • MATEKF722SE (F722, 512 KB) — USE_MZTC=OFF: builds and links
    clean; FLASH 95.1%, RAM 51.3%; zero warnings, zero missing symbols

Relationship to PR #11005

Notes for review

  • src/main/io/mztc_camera_cli.c is 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.
  • The 03-27 "fixing some issues" commits (settings.yaml condition: USE_MZTC, unit-test USE_MZTC define) were verified sound.

wdunn001 and others added 15 commits August 20, 2025 08:57
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-code-review

Copy link
Copy Markdown
Contributor

ⓘ 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).
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Integrate MassZero thermal camera support with flash-safe gating

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🐞 Bug fix 🕐 40+ Minutes

Grey Divider

AI Description

• Adds MZTC UART control, configuration, MSP V2 access, CLI commands, and OSD scaffolding.
• Integrates initialization and scheduling while excluding 512 KB targets to protect flash.
• Adds generated settings documentation, hardware/SITL utilities, and unit coverage.
Diagram

graph TD
  Camera["MZTC Camera"] <--> Serial["UART Transport"] --> Core["MZTC Core"] --> Data["Status and Frames"]
  Config["Settings and CLI"] --> Core
  MSP["MSP V2 API"] <--> Core
  Scheduler["FC Scheduler"] --> Core
  Core --> OSD["OSD Layer"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Consolidate on native INAV CLI and MSP paths
  • ➕ Removes the unbuilt duplicate CLI registry implementation
  • ➕ Avoids two command models and duplicate MSP serialization logic
  • ➕ Keeps documentation aligned with actually reachable commands
  • ➖ Requires adapting or dropping presets and management subcommands
  • ➖ Changes more of the original contributor's implementation
2. Land a protocol-only first phase
  • ➕ Reduces review and flash-risk surface
  • ➕ Allows hardware protocol validation before exposing frames and OSD
  • ➖ Delays user-facing controls and configurator integration
  • ➖ Requires follow-up PRs and temporary API boundaries

Recommendation: Keep the feature-gated core integration, native scheduler wiring, settings, and MSP exposure, but consolidate all reachable commands into INAV's existing CLI/MSP paths before merge. Remove or wire mztc_camera_cli.c; leaving a second, unbuilt command framework makes documented presets and management commands misleading. A protocol-only split is safer if hardware frame/status behavior cannot yet be validated, especially because current frame and OSD paths include scaffolding or simulated data.

Files changed (26) +4898 / -2

Enhancement (15) +2812 / -0
mztc_camera.hDefine persistent MZTC configuration and runtime data models +192/-0

Define persistent MZTC configuration and runtime data models

• Introduces feature-gated enums and structures for camera modes, units, palettes, zoom, shutter behavior, settings, status, and reduced frame data. Declares parameter-group and camera-control interfaces plus status and error constants.

src/main/config/mztc_camera.h

cli.cExpose native CLI controls for MZTC operation +320/-0

Expose native CLI controls for MZTC operation

• Adds feature-gated commands for status, modes, image parameters, denoising, alerts, calibration, palette, zoom, shutter, reconnection, and SITL data simulation. Completes guards around declarations, implementations, and command-table entries so non-MZTC targets do not reference camera symbols.

src/main/fc/cli.c

fc_init.cInitialize MZTC subsystems during flight-controller startup +11/-0

Initialize MZTC subsystems during flight-controller startup

• Adds camera, OSD, and MSP initialization under 'USE_MZTC', preventing references on flash-constrained builds where the feature is disabled.

src/main/fc/fc_init.c

fc_msp.cRoute MZTC commands through the flight-controller MSP dispatcher +211/-0

Route MZTC commands through the flight-controller MSP dispatcher

• Adds feature-gated MSP output handling for configuration, status, and reduced frame data, plus input handling for configuration and camera controls. Integrates these cases alongside the maintenance branch's existing INAV commands.

src/main/fc/fc_msp.c

fc_tasks.cSchedule periodic MZTC camera processing +9/-0

Schedule periodic MZTC camera processing

• Registers a low-priority 10 Hz MZTC task that runs the camera update and reconnection logic only when the feature is compiled.

src/main/fc/fc_tasks.c

mztc_camera.cImplement the MZTC UART camera driver +849/-0

Implement the MZTC UART camera driver

• Implements persistent defaults, serial connection retries, packet framing and validation, response parsing, status tracking, calibration, image controls, reconnect handling, and camera management commands. It also provides a provisional frame path that requests an undocumented command and otherwise synthesizes reduced thermal data.

src/main/io/mztc_camera.c

mztc_camera.hExpose MZTC driver lifecycle and control APIs +58/-0

Expose MZTC driver lifecycle and control APIs

• Declares feature-gated camera initialization, scheduling, status, frame retrieval, calibration, image controls, reconnection, and camera configuration management interfaces.

src/main/io/mztc_camera.h

mztc_camera_cli.cAdd an alternate MZTC subcommand and preset implementation +325/-0

Add an alternate MZTC subcommand and preset implementation

• Defines status and management subcommands plus five application-specific camera presets using a separate CLI registration API. This file is not compiled or initialized by the current build, so the implementation is presently unreachable.

src/main/io/mztc_camera_cli.c

mztc_camera_cli.hDeclare alternate MZTC CLI registration +26/-0

Declare alternate MZTC CLI registration

• Adds the feature-gated declaration for initializing the standalone MZTC CLI command table. No current startup path calls this interface.

src/main/io/mztc_camera_cli.h

mztc_camera_osd.cAdd MZTC OSD state and rendering scaffolding +268/-0

Add MZTC OSD state and rendering scaffolding

• Introduces OSD initialization, update cadence, visibility controls, positioning, and element toggles for temperature, status, alerts, calibration, and connection state. Drawing functions currently format or inspect values but do not emit actual OSD elements.

src/main/io/osd/mztc_camera_osd.c

mztc_camera_osd.hDefine MZTC OSD configuration and element APIs +64/-0

Define MZTC OSD configuration and element APIs

• Adds OSD element identifiers, visibility flags, a parameter-group-backed configuration structure, and initialization and mutation interfaces.

src/main/io/osd/mztc_camera_osd.h

serial.hReserve a serial function for the MZTC camera +3/-0

Reserve a serial function for the MZTC camera

• Adds a feature-gated serial-port function bit so UART resources can be allocated specifically to the thermal camera.

src/main/io/serial.h

msp_mztc.cImplement MZTC-specific MSP V2 command processing +322/-0

Implement MZTC-specific MSP V2 command processing

• Adds bounded request and reply processing for configuration, status, frames, calibration, modes, palette, zoom, shutter, alerts, image parameters, and correction controls. This handler exists alongside direct cases in 'fc_msp.c', creating overlapping command-processing paths.

src/main/msp/msp_mztc.c

msp_mztc.hDefine MZTC MSP V2 commands and payloads +151/-0

Define MZTC MSP V2 commands and payloads

• Allocates MZTC command identifiers in the 0x3000 range and defines binary payload structures for configuration, status, reduced frames, and camera controls. Declares the dedicated MZTC MSP processor and initializer.

src/main/msp/msp_mztc.h

scheduler.hAdd a feature-gated MZTC scheduler task identifier +3/-0

Add a feature-gated MZTC scheduler task identifier

• Extends the scheduler task enumeration with a camera task only when MZTC support is enabled.

src/main/scheduler/scheduler.h

Bug fix (1) +5 / -0
common.hDisable MZTC on 512 KB flash targets +5/-0

Disable MZTC on 512 KB flash targets

• Enables 'USE_MZTC' only when MCU flash exceeds 512 KB, preserving build and link viability for constrained F722 targets.

src/main/target/common.h

Tests (2) +752 / -0
CMakeLists.txtConfigure the MZTC unit-test target +5/-0

Configure the MZTC unit-test target

• Associates the camera test with implementation dependencies and compiles it with unit-test and MZTC feature definitions.

src/test/unit/CMakeLists.txt

mztc_camera_unittest.ccCover MZTC constants, structures, ranges, and data models +747/-0

Cover MZTC constants, structures, ranges, and data models

• Adds GoogleTest coverage for enums, flags, limits, field sizes, configuration values, status and frame structures, alignment, and data integrity. Much of the suite validates declarations and assignments rather than runtime serial parsing or camera behavior.

src/test/unit/mztc_camera_unittest.cc

Documentation (2) +859 / -0
MassZero_Thermal_Camera.mdDocument MZTC installation, controls, presets, and troubleshooting +639/-0

Document MZTC installation, controls, presets, and troubleshooting

• Adds a comprehensive guide covering wiring, camera modes, image settings, application presets, MSP commands, OSD concepts, protocol details, troubleshooting, and safety. Some documented preset and management commands belong to the currently unbuilt alternate CLI implementation.

docs/MassZero_Thermal_Camera.md

Settings.mdPublish generated MZTC setting references +220/-0

Publish generated MZTC setting references

• Adds generated reference entries for camera enablement, serial configuration, modes, image processing, alerts, calibration, palette, zoom, and correction settings.

docs/Settings.md

Other (6) +470 / -2
CMakeLists.txtCompile MZTC core, OSD, and MSP modules +8/-2

Compile MZTC core, OSD, and MSP modules

• Adds the MZTC configuration header, camera driver, OSD module, and MSP implementation to common firmware sources while preserving existing MAVLink sources. The separate MZTC CLI module is intentionally not included.

src/main/CMakeLists.txt

parameter_group_ids.hReserve feature-gated MZTC parameter-group IDs +6/-0

Reserve feature-gated MZTC parameter-group IDs

• Allocates IDs 1046 and 1047 for camera and OSD configuration after DroneCAN. Makes the INAV parameter-group endpoint conditional on MZTC support to avoid the maintenance-branch ID collision.

src/main/config/parameter_group_ids.h

settings.yamlRegister configurable MZTC settings and enum tables +161/-0

Register configurable MZTC settings and enum tables

• Defines camera mode, unit, palette, shutter, zoom, and mirror lookup tables. Adds a 'USE_MZTC'-conditioned parameter group exposing persistent camera settings with defaults and bounds.

src/main/fc/settings.yaml

test_thermal_camera.pyAdd a direct serial camera protocol probe +86/-0

Add a direct serial camera protocol probe

• Adds a pyserial utility that builds MZTC packets and exercises model, firmware, initialization, and brightness commands against a configured Windows COM port.

src/utils/test_thermal_camera.py

thermal_bridge.pyBridge MZTC serial traffic into SITL over TCP +83/-0

Bridge MZTC serial traffic into SITL over TCP

• Adds a bidirectional threaded serial-to-TCP bridge for connecting physical camera hardware to an INAV SITL serial endpoint, with compact hexadecimal logging.

src/utils/thermal_bridge.py

thermal_bridge_debug.pyAdd a verbose MZTC serial-to-SITL bridge +126/-0

Add a verbose MZTC serial-to-SITL bridge

• Adds a diagnostic bridge variant with startup probing, full packet dumps, transfer counts, flushing, and detailed connection reporting.

src/utils/thermal_bridge_debug.py

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 29, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Camera packets omit terminator ✓ Resolved 🐞 Bug ≡ Correctness
Description
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.
Code

src/main/io/mztc_camera.c[R521-525]

+    // 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);
Evidence
The struct reserves 14 bytes before checksum/end, but the sender writes a data-dependent prefix. The
PR's own debug utility identifies F0 04 36 74 02 01 AD FF as a complete zero-data command,
directly contradicting the firmware's size and length calculations.

src/main/io/mztc_camera.c[73-84]
src/main/io/mztc_camera.c[501-526]
src/utils/thermal_bridge_debug.py[85-89]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


2. Successful replies are discarded ⊘ Outdated 🐞 Bug ≡ Correctness
Description
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.
Code

src/main/io/mztc_camera.c[R546-550]

+    // Check flags
+    uint8_t flags = data[5];
+    if (flags == MZTC_FLAG_SUCCESS) {
+        // Command executed successfully
+        mztcStatus.error_flags &= ~MZTC_ERROR_COMMUNICATION;
Evidence
The response handler inspects only flags/error code and returns, while the status updater fabricates
quality/count/time and contains no camera-temperature assignments. Repository search shows no
assignment to ambient_temperature in the driver.

src/main/io/mztc_camera.c[534-570]
src/main/io/mztc_camera.c[573-592]
src/main/config/mztc_camera.h[120-148]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


3. Port open fakes connection ✓ Resolved 🐞 Bug ☼ Reliability
Description
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.
Code

src/main/io/mztc_camera.c[R233-236]

+            if (mztcSerialPort != NULL) {
+                // Successfully opened port
+                mztcStatus.connected = true;
+                mztcStatus.status = MZTC_STATUS_READY;
Evidence
The open-success branch sets connected/ready before any callback, the callback records receive time,
but status updates unconditionally report 100% quality and never test that timestamp.

src/main/io/mztc_camera.c[219-250]
src/main/io/mztc_camera.c[447-486]
src/main/io/mztc_camera.c[573-592]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


View high (4)
4. Frame request leaks stack ⊘ Outdated 🐞 Bug ⛨ Security
Description
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.
Code

src/main/io/mztc_camera.c[R683-686]

+        // Process the response data here
+        // This is where we'd extract the actual thermal information
+        
+        return true;
Evidence
The function marks its output unused and returns true solely because send succeeded. Its caller
immediately returns success, after which the MSP handler copies every uninitialized field and
advances over the complete response struct.

src/main/io/mztc_camera.c[651-701]
src/main/fc/fc_msp.c[2061-2085]
src/main/config/mztc_camera.h[134-148]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


5. Zero rate crashes scheduler ✓ Resolved 🐞 Bug ≡ Correctness
Description
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.
Code

src/main/fc/fc_msp.c[R4082-4085]

+            cfgMutable->baudrate = config->baudrate;
+            cfgMutable->mode = config->mode;
+            cfgMutable->update_rate = config->update_rate;
+            cfgMutable->temperature_unit = config->temperature_unit;
Evidence
The real FC dispatch path performs a direct unchecked assignment. The driver subsequently divides by
this value, while the header explicitly declares a minimum update rate of one; validation in the
unused standalone MSP handler does not protect this path.

src/main/fc/fc_msp.c[4075-4105]
src/main/io/mztc_camera.c[261-263]
src/main/config/mztc_camera.h[168-172]
src/main/msp/msp_mztc.c[92-110]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


6. MSP wire layout is unstable ✓ Resolved 🐞 Bug ≡ Correctness
Description
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.
Code

src/main/fc/fc_msp.c[R1999-2002]

+            const mztcConfig_t *cfg = mztcConfig();
+            msp_mztc_config_t *config = (msp_mztc_config_t*)sbufPtr(dst);
+            
+            config->enabled = cfg->enabled;
Evidence
The FC handler writes through a struct pointer and advances by sizeof, and the input handler
requires the same native size. The protocol structs mix byte, float, 16-bit, and 32-bit members,
necessarily exposing target alignment gaps instead of an explicit MSP layout.

src/main/fc/fc_msp.c[1995-2025]
src/main/fc/fc_msp.c[2031-2083]
src/main/fc/fc_msp.c[4075-4105]
src/main/msp/msp_mztc.h[52-105]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


7. MZTC OSD renders nothing ✓ Resolved 🐞 Bug ≡ Correctness
Description
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.
Code

src/main/io/osd/mztc_camera_osd.c[R132-135]

+    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
Evidence
The update routine dispatches the draw functions, but repository search finds no caller for
mztcOsdUpdate. The temperature, status, and connection routines only format stack strings, while
alert/calibration branches contain comments instead of drawing calls.

src/main/io/osd/mztc_camera_osd.c[70-117]
src/main/io/osd/mztc_camera_osd.c[119-219]
src/main/fc/fc_init.c[567-572]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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



Remediation recommended

8. MZTC tests compile empty ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
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.
Code

src/test/unit/CMakeLists.txt[R200-203]

+# 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)
Evidence
The unit_test helper reads source properties while targets are created in the loop ending at line
196, but the new properties are not set until lines 200-203. The test source places every test under
#ifdef USE_MZTC.

src/test/unit/CMakeLists.txt[151-203]
src/test/unit/mztc_camera_unittest.cc[1-6]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


Grey Divider

Tip of the day
💡 Did you know, you can turn on the rule miner and Qodo learns your standards from review history

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/main/io/mztc_camera.c Outdated
Comment thread src/main/io/mztc_camera.c Outdated
Comment thread src/main/io/mztc_camera.c Outdated
Comment thread src/main/io/mztc_camera.c Outdated
Comment thread src/main/fc/fc_msp.c Outdated
Comment thread src/main/fc/fc_msp.c Outdated
Comment thread src/main/io/osd/mztc_camera_osd.c Outdated
Comment thread src/test/unit/CMakeLists.txt Outdated
@github-actions

github-actions Bot commented Aug 29, 2026 •

Copy link
Copy Markdown

RAM / Flash usage vs. base branch — commit 1faf409

No size baseline is available yet for this PR's base commit (no per-commit baseline has been published for it). This comment will show deltas once one exists — rebasing the PR refreshes its base commit.

Target Flash Δ RAM Δ
MATEKF405 725497 B (no baseline) 150720 B (no baseline)
MATEKF722 465475 B (no baseline) 125260 B (no baseline)
MATEKF765 756725 B (no baseline) 166620 B (no baseline)
MATEKH743 793865 B (no baseline) 169132 B (no baseline)

See RAM/flash optimization guide for techniques to reduce usage.

@github-actions

github-actions Bot commented Aug 29, 2026 •

Copy link
Copy Markdown

Test firmware build ready — commit 1faf409

Download firmware for PR #11837

247 targets built. Find your board's .hex file by name on that page (e.g. MATEKF405SE.hex). Files are individually downloadable — no GitHub login required.

Development build for testing only. Use Full Chip Erase when flashing.

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.
@sensei-hacker

Copy link
Copy Markdown
Member Author

Review notes from the merge-resolution pass

First — thank you for the MZTC feature; the integration is substantial and the merge onto current maintenance-10.x landed cleanly. This PR was produced by merging your branch with upstream and resolving the conflicts (plus gating the feature off 512 KB-flash targets, which was the outstanding review finding).

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 author

1. MSP2 command IDs — do the 0x3000/0x3001 values collide with the Betaflight-compat range?
msp_mztc.h comments "INAV-specific range 0x3000-0x3FFF", but msp_protocol_v2_common.h already defines MSP2_BETAFLIGHT_BIND = 0x3000 and MSP2_RX_BIND = 0x3001 (the Betaflight compatibility range). INAV's own range is 0x1000-0x1FFF. Could the MZTC IDs be renumbered into the INAV range to avoid a collision with RX_BIND in the in-command dispatch? Also, several IDs are defined (0x3004-0x300B) but never handled in the active fc_msp.c dispatch — intentional aliases for a future version, or droppable?

2. MSP2_MZTC_FRAME_DATA — could it be returning uninitialized stack data?
mztcReadThermalFrame() is currently a stub that returns true without filling frameData, so mztcGetFrameData() can reply over MSP with an uninitialized caller struct (width/height/temps/data). Should the stub return false (or fill the struct) until real frame reading is implemented?

3. OSD module — is it scaffolding for a follow-up, or should it be wired up?
mztcOsdConfig has a PG_DECLARE but no PG_REGISTER anywhere, and mztcOsdUpdate() is never called (only mztcOsdInit()). The draw functions also don't call any OSD drawing API yet. Should the PG be registered and the module invoked, or is it intentionally deferred?

4. Serial framing — could the packet length math be off by one?
The send path writes totalLen = 1 + 1 + packet.size + 1 wire bytes, while the receive path expects declaredSize + 4; and for data_len = 14, totalLen (25) exceeds sizeof(mztcPacket_t) (22), which would over-read the stack via serialWriteBufShim. Could you double-check the framing against the camera datasheet?

5. MSP2_SET_MZTC_CONFIG — should it validate ranges like the CLI settings do?
The MSP set path copies update_rate/baudrate/port without the bounds the settings.yaml definitions enforce. With update_rate = 0, mztcUpdate() would divide by zero (1000 / update_rate), and a large baudrate would index past the baudRates[] array. Would it make sense to validate in the MSP handler?

6. Dead code — mztc_camera_cli.c, msp_mztc.c, and the unit test?

  • mztc_camera_cli.c isn't in CMakeLists.txt, mztcCliInit() is never called, and it references APIs that don't appear to exist (cliAddCommand, cliCommand_t) — while the cliMztc* commands already live in cli.c. Remove it?
  • msp_mztc.c's mspMztcProcessCommand is never invoked from the MSP dispatch (the real handlers are in fc_msp.c). Keep one implementation?
  • The unit test (mztc_camera_unittest.cc) has its properties set after the target-creation foreach and depends on common/parameter_group.c (which I believe is config/parameter_group.c), and its assertions are largely tautological — does it actually run any meaningful tests?

7. Defaults — do the C reset template and settings.yaml agree?
C defaults say mztc_baudrate = 115200 (index 8) and mztc_mode = DISABLED, while settings.yaml says 38400 (index 6) and STANDBY. Which pair is intended? (Fresh-EEPROM vs defaults CLI would currently give different configs.)

8. Smaller items (for awareness):

  • last_calibration is "minutes since calibration" in a uint8 — wraps after ~4.25 h?
  • cliMztcShutter and MSP2_SET_MZTC_SHUTTER both call mztcTriggerCalibration — same command intended?
  • cliMztcAlerts accepts -40..300°C but settings.yaml caps at 200?
  • MSP structs with floats are cast over sbuf pointers — potential unaligned access and padding bytes on the wire; packed structs or byte-wise serialization might be safer?
  • zoom_channel/palette_channel/ffc_channel/brightness_channel/contrast_channel in mztcConfig_t are unused and not in settings.yaml — leftover?

What the merge itself changed (for transparency)

For anyone reviewing: the merge-resolution commits did not alter the feature's behavior — they (1) merged maintenance-10.x and resolved the 5 conflicts (.gitignore taken verbatim from base; CMakeLists/fc_msp.c unions; MZTC PGs renumbered 1045/1046 → 1046/1047 because base now uses 1045 for PG_DRONECAN_CONFIG; cli.c kept base's timer_output_mode + the mztc commands), (2) gated USE_MZTC with #if defined(SITL_BUILD) || (MCU_FLASH_SIZE > 512) in common.h and completed the #ifdef USE_MZTC guards (cli.c prototypes were under USE_ASSERT; fc_init.c/fc_tasks.c entries unguarded), (3) removed the committed inav_9.0.0_SPEEDYBEEF405AIO.hex artifact, and (4) regenerated docs/Settings.md. Validated: F405 (MZTC on) and F722 (MZTC off) both build; 23/23 CI checks green.

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).
@sensei-hacker

Copy link
Copy Markdown
Member Author

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
the feature code that only appear when USE_MZTC is actually compiled in.
Both were completed in the merge branch so the build passes (they were
needed for the gate change to be buildable):

  • mztc_camera.c now includes <stdio.h> (it used fprintf(stderr, ...)
    via the SD() macro without the include — 6 stderr undeclared errors).
  • mztc_camera_osd.c now registers the mztcOsdConfig parameter group
    (PG_REGISTER_WITH_RESET_TEMPLATE + reset template) — previously the PG
    was declared and its ID (1047) used, but never registered, which was an
    undefined reference at link time. Followed the existing mztcConfig
    pattern in mztc_camera.c.

These are the author's code, completed minimally to make the feature build —
happy to adjust if you'd prefer a different default set or structure.

Everything else from the earlier comment (MSP2 ID range, frame-data stub,
serial framing math, MSP validation, defaults mismatch, dead code) remains
open for your take. Validated: F405 (MZTC on), F722 (MZTC off), and SITL
(MZTC on) all build; awaiting CI on the updated branch.

wdunn001 added a commit to wdunn001/inav that referenced this pull request Aug 30, 2026
…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.
wdunn001 added a commit to wdunn001/inav-configurator that referenced this pull request Aug 30, 2026
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.
wdunn001 added a commit to wdunn001/inav that referenced this pull request Aug 30, 2026
…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.
mztc_mode offered eight values and none of them did anything. Every use across
the tree was a range check, a stored value or a status echo. No mode was ever
sent to the camera. The camera protocol has no matching command. Its ALERT
value needed a scene temperature that this core does not report.

It is replaced by mztc_preset, a named bundle of the image settings the camera
does have. Selecting one writes the palette, brightness, contrast, digital
enhancement, both denoise levels, the shutter mode and the correction interval.
CUSTOM is the default and writes nothing. A hand-tuned configuration therefore
survives. Zoom and mirror are never written. Zoom belongs to the pilot and
mirror describes how the camera is mounted.

Two constraints shape the values. Temporal denoising averages across frames. On
a moving airframe it therefore smears targets and leaves trails. Spatial
denoising trades noise for sharpness. A person at search range is a few pixels
wide. Both stay low except in SURVEILLANCE, where loiter leaves little motion
to smear.

mztc_update_rate is removed. It did not control serial traffic. The probe has
its own 500 ms gate. Its range allowed 30 Hz on a task that runs at 10 Hz. Its
description named a frame rate for frame data that was removed earlier.

The MSP config payload drops from 12 bytes to 11 and MSP2_SET_MZTC_MODE becomes
MSP2_SET_MZTC_PRESET. The parameter group version goes to 2. Removing two
fields and adding one shifts every offset. An older record read at the new
offsets would apply garbage to real camera settings.

The CLI gains mztc_preset. It lists the presets when called with no argument.
mztc_reconnect moves into the alphabetical block. mztc_config states its 0-100
range.

Tests go from 38 to 41. The new ones pin that every preset produces a valid
configuration, that CUSTOM writes nothing, and that a preset leaves zoom and
mirror alone. A fourth pins that CUSTOM does not repair an already invalid
configuration. That is a real consequence of writing nothing.

The feature doc is rewritten around presets. Two errors in its previous
application setups are corrected. Search and rescue used a temporal denoise of
70. That smears the small target it is meant to find. Industrial inspection
claimed absolute temperature accuracy from a camera that reports no
temperature. The MSP payload size in that doc was stale at 15 bytes.

Verified on 247 of 247 targets with warnings as errors, 557 of 557 unit tests,
and a cross-artifact audit of the header against settings.yaml, the MSP header,
fc_msp.c wire order, msp_messages.json, both generated docs and the CLI.
THERMAL CALIBRATE is a box mode that runs one flat field correction on the
rising edge of the switch. Holding it does not repeat the correction. The box is only offered when the camera has a UART assigned in the Ports tab. That is the same rule the Configuration and OSD tabs use.

Firing it is safe in any attitude. The correction uses the camera's own internal shutter as the reference. The sensor cannot see the scene while it runs. The camera already performs the same correction on its own timer.

Vignetting correction deliberately stays a CLI command. It has no protective
shutter and captures whatever the lens faces. The camera manual states that the
lens must face a uniform surface first. Otherwise the current scene is
superimposed on every later image. Manual background correction and the bad
pixel commands are not implemented for the same reason.

MZTC_ZOOM is adjustment function 61. It steps the digital zoom through 1x, 2x,
4x and 8x. It goes through mztcSetZoom. That writes the camera and stores the level together. The switch position and the saved setting therefore still agree after a reconnect. It logs a blackbox inflight adjustment event like every other
adjustment in that file.

Both appear in the configurator Modes tab with no configurator change. That tab builds from MSP_BOXIDS and the firmware box list.

The adjustment table in Inflight Adjustments.md was stale at 58 while the enum
already reached 60. The two missing upstream entries are added alongside the
new one.
mztc_preset could be written two ways with different results. The mztc_preset CLI command and MSP2_SET_MZTC_PRESET called mztcSetPreset. That writes the eight owned values. Writing the field through the settings framework, as
"set mztc_preset = SEARCH" does, stored a byte and applied nothing. The setting
then named a preset whose values were not in effect.

That is the defect the old mode enum had. A control that reads as doing
something has to do it.

The driver now tracks which preset the stored values reflect and applies any
change it sees. Every route agrees: the CLI command, the CLI set, the
configurator dropdown and MSP2_SET_MZTC_PRESET.

Boot deliberately does not reapply. mztcInit seeds the tracking value from the saved preset. The saved field values already reflect it. Reapplying at
boot would discard tuning done after the preset was chosen.

Three tests added, one of which found a gap in the existing suite.
IS_RC_MODE_ACTIVE was never stubbed. The calibrate switch added in the previous commit therefore had no unit coverage at all. It has coverage now, including that it fires once per flip and not on every task tick while held.

Each test was mutation checked. Removing the apply-on-change, seeding the
tracking value wrongly at boot, and turning the switch from edge to level
triggered each fail exactly one test.

The boot test failed its first mutation check by passing when it should not
have. mztcInit returns early while the driver is already initialised. The test never reached the code it claimed to cover. A mztcTestForceReinit hook
fixes that. This is the same shape as the USE_MZTC problem this branch already
fixed, where a whole test body was preprocessed away and every test still
passed.

560 of 560 unit tests pass. SITL builds with warnings as errors.
sensei-hacker pushed a commit to iNavFlight/inav-configurator that referenced this pull request Sep 4, 2026
Companion to the firmware in iNavFlight/inav#11837.

Serial port

MZTC_CAMERA is registered as a peripheral serial function. Assigning it to a
UART in the Ports tab is what enables the camera. The firmware finds the port
and its baud rate through findSerialPortConfig(), the way every other serial
peripheral works, so there is no separate port or enable setting to keep in
step. The camera ships at 115200.

MSP

Ten commands in INAV's own range, contiguous from 0x2240 to 0x2249. The
firmware answers MSP2_MZTC_CONFIG with a fixed 12 byte payload and
MSP2_MZTC_STATUS with 7 bytes. Both are read and written one field at a time,
so no compiler padding reaches the wire. connected is only set once the camera
has answered a command, so an open UART on its own does not report a camera.

Configuration tab

A declarative section carrying data-setting attributes, in the same style as
the gimbal and headtracker sections. The Settings framework handles load and
save, so the section needs no JavaScript. All twelve firmware settings are
exposed and nothing else is.

OSD

MZTC_STATUS at id 171, matching OSD_MZTC_STATUS in the firmware. It shows a
three letter link state.
@Sibre3

Sibre3 commented Sep 15, 2026

Copy link
Copy Markdown

@wdunn001 @sensei-hacker I have tried testing this with 1faf409. No later builds are available. Unfortunately the testing was to no avail. The camera would not communicate with the flight controller.
The camera is an Axisflying thermal camera TC640. Which looks identical and has the same specifications as the Mass Zero camera.
Much time was spent checking hardware connections, along with altering CLI commands and changing settings in the configuration tab.

I set the baud in the ports tab to 115200 and selected mass zero thermal camera and set that on the correct UART port I have the camera connected to. Surely that is what's required. But why are these settings here??
mztc_baudrate 1 to 10.
mztc_port 1 to 7

The other disappointment I observed is the lack of means to control the cameras functions by RC. The only way I could find is MSP commands. Which is not of much use if you don't use a ground station.
I expected this feature to provide something that would allow the cameras Pallet, Zoom etc to be controlled by an RC transmitter channel when inflight. Maybe a mode might be too extreme for a special use camera with a limited user base. But I thought it should at least have the commands in the Adjustments tab. Or the programming framework, with all the commands broken out there.

@wdunn001

Copy link
Copy Markdown

MZTC: address the review findings on iNavFlight#11837 and correct the driver against the camera manual
@sensei-hacker

sensei-hacker commented Sep 18, 2026 •

Copy link
Copy Markdown
Member Author

odd it does exactly as you say I made a pr while back not sure if its in yes sensei-hacker#50

Ah, I did not see that PR. I just merged it! Now we'll give Qodo and Sonarqube a few minutes to point anything out, and we can then look at the now-merged version.

@Sibre3

Sibre3 commented Sep 19, 2026

Copy link
Copy Markdown

Ah, I did not see that PR. I just merged it! Now we'll give Qodo and Sonarqube a few minutes to point anything out, and we can then look at the now-merged version.

May I request you update the test firmware? 9bf30b5 is not yet available.

@wdunn001 Does the firmware you wrote a while back also include inflight RC function control.

@sensei-hacker

sensei-hacker commented Sep 19, 2026 •

Copy link
Copy Markdown
Member Author

Ah, I did not see that PR. I just merged it! Now we'll give Qodo and Sonarqube a few minutes to point anything out, and we can then look at the now-merged version.

May I request you update the test firmware? 9bf30b5 is not yet available.

@wdunn001 Does the firmware you wrote a while back also include inflight RC function control.

It should rebuild as soon as the code is able to compile. It looks like sensei-hacker#50 may have created a some merge conflicts?

…ht#11837

# Conflicts:
#	docs/development/msp/README.md
#	docs/development/msp/inav_enums.json
#	docs/development/msp/inav_enums_ref.md
#	src/main/config/parameter_group_ids.h
#	src/main/fc/fc_msp_box.c
#	src/main/fc/rc_modes.h
#	src/main/io/osd.h
#	src/main/io/serial.h
@sensei-hacker

Copy link
Copy Markdown
Member Author

I reolved the merge conflicts again, but it's still failing to build on F722. Milestoning to inav 10.1

@sensei-hacker sensei-hacker modified the milestones: Future, 10.1 Sep 21, 2026
Raffi1202 pushed a commit to Raffi1202/inav-configurator that referenced this pull request Sep 23, 2026
maintenance-10.x now assigns id 172 to MZTC_STATUS (4e7526d), matching
the firmware where iNavFlight/inav#11837 appends OSD_MZTC_STATUS after
OSD_TERRAIN_AGL. The profile name elements move to 173-175 so the ids
stay unique once both land; osd-item-id-uniqueness would fail otherwise.
@wdunn001

Copy link
Copy Markdown

Where the F722 failure comes from

I reproduced CI job build (14) locally with the flags CI uses
(-DWARNINGS_AS_ERRORS=ON -DCI_JOB_INDEX=14 -DCI_JOB_COUNT=15). One target in
that chunk fails, ZEEZF7V3 (STM32F722, 480 KB usable flash). The other ten
build.

The same target fails on plain maintenance-10.x with none of this branch
applied:

tree ZEEZF7V3 flash result
maintenance-10.x at 58de0dba4 (this PR's merge base) 491691 B overflows by 171 B
maintenance-10.x at d1a87ed6c (current head) 491691 B overflows by 171 B
this PR at 739fb45 491723 B overflows by 203 B

Unrelated PRs fail the same job. feature/uart-baud-in-place (run
35647088610) fails build (14) and nothing else. PRs #11996 and #11998 are
open against the same regression from the coordinated turn work.

The 32 bytes that were ours

The difference above is 32 bytes. Those were avoidable. USE_MZTC is off on
512 KB targets. Five additions still reached those builds:

  • BOXMZTCCALIBRATE raised CHECKBOX_ITEM_COUNT to 65. That widened
    boxBitmask_t from two words to three at every site that reads a mode.
  • its boxes[] entry and the "THERMAL CALIBRATE" string
  • ADJUSTMENT_MZTC_ZOOM raised ADJUSTMENT_FUNCTION_COUNT
  • its row in defaultAdjustmentConfigs
  • OSD_MZTC_STATUS raised OSD_ITEM_COUNT. That widened the item_pos array
    inside the osdLayoutsConfig parameter group.

All five now sit behind USE_MZTC, in sensei-hacker#51 against this
PR's branch. A target that compiles the camera out
links 491691 B. That is byte for byte what upstream links without this branch.
Gating OSD_ITEM_COUNT back down also keeps the OSD parameter group the same
size as upstream on those boards. Their stored layouts stay untouched.

Every translation unit that reaches those three headers includes platform.h
first. I checked that with a temporary #error
keyed on a platform.h macro, built across an F4, an F7 and an H7 target.

Verification

  • maintenance-10.x at d1a87ed6c merged into this branch with no conflicts.
    Upstream has since moved to 752bae1da, 49 commits ahead of this branch.
  • 604 of 604 unit tests pass
  • 20 targets built clean with warnings as errors, spanning F4, F7 and H7, with
    the camera compiled in and compiled out: AOCODARCF722AIO, FOXEERF722V2,
    FOXEERH743, IFLIGHT_BLITZ_F7_AIO, KAKUTEF4, MATEKF405SE, MATEKF411SE,
    MATEKF722SE, MATEKH743, SPEEDYBEEF405V4, TBS_LUCID_H7_WING,
    TBS_LUCID_H7_WING_MINI, TMOTORF7V2, TMOTORVELOXF7V2, TUNERCF405,
    VANTAC_RF007, WARPF7, WINGFC, ZEEZF7, ZEEZF7V2
  • ZEEZF7V3 is the one remaining failure. It fails identically without this
    branch.

@sensei-hacker sensei-hacker changed the title MZTC thermal camera integration — merged + resolved against maintenance-10.x (supersedes PR #11005) MZTC thermal camera integration — merge conflicts resolved against maintenance-10.x (supersedes PR #11005) Sep 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants