SKIP TO CONTENT
Fjärrstridsgrupp Alfa
SV UK EDITION 2026-Q2 ACTIVE
UNCLASSIFIED
FSG-A // CLUSTER 7 — FISCHER 26 // 7.7B

AUTONOMOUS TARGET LOCKING
HOSTILE JAM + FRIENDLY RELAY

Author: Tiny — FPV/UAV-certified
TRL 2 — CONCEPT EW AIR 26 MIN READ
KEY TAKEAWAY
Fischer 26's antenna cluster runs two mutually exclusive lock-on modes per gimbal, selected by IFF classification. HOSTILE-LOCK points the gimbal at an unknown or red-force drone so Mast A, B, or C can jam its control link. FRIENDLY-LOCK points the gimbal at a whitelisted blue-force drone so Mast B (budget-FHSS) acts as a high-gain MANET relay antenna for that specific drone, extending its command range by 5–15 km. Both modes share the same EKF-based target-state estimator, the same gimbal command pipeline, and the same Cube Orange+ MAVLink interface. The difference is sign: HOSTILE-LOCK keeps the target in the main beam to maximise jamming effectiveness; FRIENDLY-LOCK keeps the target in the main beam to maximise link margin. Decision logic runs at 50 Hz on the Cube Orange+, target-state EKF runs at 100 Hz on the onboard Jetson Orin Nano, gimbal servo loop runs at 1 kHz on the gimbal MCU. All three loops communicate over CAN-FD and MAVLink. This page documents the math, the ArduPilot Lua scripts, the MAVLink message flow, and the failure modes.

Autonomous target locking on Fischer 26E-LE is not a single function — it is a tightly coupled pipeline from sensor detection, through IFF classification, through target-state estimation, through gimbal command generation, and finally to RF payload activation. Each stage runs at a different rate, on different compute hardware, communicating through ArduPilot's native MAVLink bus. This document answers four questions: (1) how does the drone decide a target is friendly or hostile? (2) how does the EKF hold a target's bearing against a moving Fischer 26 airframe and a manoeuvring target? (3) how does the gimbal command pipeline interface with the Cube Orange+ flight controller without breaking ArduPilot's safety invariants? (4) how does the system degrade gracefully when any stage fails? Every answer is an equation with a reference, a MAVLink message identifier, or a specific Lua script fragment.

Related Chapters

Two lock-on modes — one pipeline

The operator selects one of two modes per gimbal at mission briefing, with in-flight switching permitted via a single MAVLink parameter write. Both modes reuse the same gimbal, the same EKF, the same command pipeline; only the RF payload activation and the target-selection logic differ.

MODE HOSTILE-LOCK — jam the target

Target criterion
IFF classification = RED (hostile) or UNKNOWN with L2 recommendation to engage
RF payload
Mast A (operator-selected military band) or Mast B (budget-FHSS) at 10 W PA drive
Objective
Maintain target inside main beam (half-power beamwidth ≈ 10° at +10 dBi gain) for jamming effectiveness
Dwell policy
Continuous tracking until target lost or reclassified; hand-off to second gimbal if target crosses the horizon
Failure mode
If EKF covariance exceeds 5°, release lock and return to search sweep

MODE FRIENDLY-LOCK — relay the target

Target criterion
IFF classification = BLUE (whitelisted friendly) via MANET cryptographic challenge-response
RF payload
Mast B in directional MANET-relay mode at 2 W PA drive (reduced to avoid saturating the friendly receiver)
Objective
Maintain friendly drone inside main beam to maximise relay link margin; +10 dBi gain adds 20 dBm effective isotropic power vs omni relay
Dwell policy
Persistent lock as long as MANET link stays up; release on crypto handshake timeout > 3 s
Failure mode
If crypto challenge-response fails three consecutive rounds, target is reclassified to UNKNOWN; gimbal reverts to omni coverage until re-verified

The fundamental symmetry: both modes point a directional antenna at a specific drone to maximise the RF coupling between that drone and Fischer 26E-LE. Hostile-lock weaponises the coupling as jamming; friendly-lock weaponises the coupling as link budget. A Tier-3 Fischer 26E-LE in stand-off station (see cluster page) can drive both roles simultaneously on its two gimbals — one gimbal jamming a hostile FPV, the other gimbal relaying commands to a friendly FPV that is engaging that same hostile. This is the SPLIT-100/100 configuration the cluster is designed for.

IFF gating — the decision between modes

Lock-on mode selection is not a free choice. It is gated by the IFF classification of the target, which is produced by Lisa 26 (brigade-level) and verified onboard by Fischer 26 (airborne). The gate is a hard safety interlock: the system cannot enter HOSTILE-LOCK against a target that Lisa 26 has classified as BLUE, and cannot enter FRIENDLY-LOCK against a target that fails the MANET cryptographic handshake. Both interlocks are enforced in ArduPilot's Lua scripting layer before the gimbal command is emitted.

# pip install pymavlink
# iff_gate.py — airborne IFF gate logic for lock-on selection
# Runs on Jetson Orin Nano, feeds decisions to Cube Orange+ via MAVLink

from enum import IntEnum

class IFFClass(IntEnum):
    UNKNOWN = 0    # sensor detection without cryptographic verification
    BLUE    = 1    # whitelisted friendly, crypto-verified
    RED     = 2    # actively hostile (Lisa 26 classification or manual operator)
    GREY    = 3    # non-combatant (civilian drone, commercial aircraft)

def mode_for_target(iff_class, l2_recommendation, operator_override=None):
    """Return the lock-on mode to apply for a target, or None if no action."""
    if operator_override is not None:
        return operator_override  # operator can always force or forbid

    # Hard interlocks: safety invariants
    if iff_class == IFFClass.BLUE:
        # Cannot jam a friendly. Can only relay.
        return 'FRIENDLY_LOCK'

    if iff_class == IFFClass.GREY:
        # Cannot engage a non-combatant. No lock at all.
        return None

    # Discretionary: hostile or unknown
    if iff_class == IFFClass.RED:
        return 'HOSTILE_LOCK'

    # UNKNOWN: requires L2 recommendation from Lisa 26 before engaging
    if iff_class == IFFClass.UNKNOWN and l2_recommendation == 'engage':
        return 'HOSTILE_LOCK'

    return None   # unknown without L2 recommendation = do not engage

The IFF class itself comes from one of three sources, applied in priority order. First, the MANET cryptographic handshake: any drone broadcasting a valid Silvus-style challenge-response authenticated signature is BLUE. Second, the Lisa 26 brigade-level classification: targets explicitly classified as RED or GREY by L2/L3 decision layers inherit that class. Third, default UNKNOWN for any sensor detection (acoustic, radar, visual) that cannot be matched to a BLUE or RED entry. The cryptographic handshake uses a 2048-bit pre-shared key rotated daily, giving a collision probability below 10^-60 per authentication attempt — formally verified in provable_claims.py as HMAC_COLLISION_YEARS.

Target-state EKF — holding the bearing through a manoeuvring engagement

Once a target is classified and a lock mode selected, Fischer 26 must estimate the target's future position with sufficient accuracy that the gimbal command pipeline can anticipate where the target will be when the command actually arrives at the servo. The gimbal has a 200 ms command latency and a 175°/s slew rate, which means a target moving at 3 rad/s angular velocity can translate 35° during one command cycle. Naive point-and-hold control fails under these dynamics; the system requires a target-state estimator with velocity prediction.

The estimator is an Extended Kalman Filter running on the Jetson Orin Nano at 100 Hz. Its state vector is six-dimensional: target position in local ENU (East, North, Up) and target velocity in the same frame. Measurements come from two sources: the radar/acoustic detection from the mast array (providing bearing and approximate range) and the visual tracker running YOLOv8 on the gimbal-mounted camera (providing bearing at higher angular accuracy but no range).

# pip install numpy filterpy
# target_ekf.py — six-state target position/velocity EKF
# State: [x, y, z, vx, vy, vz]  in local ENU, metres and m/s
import numpy as np
from filterpy.kalman import ExtendedKalmanFilter

class TargetEKF:
    def __init__(self, dt=0.01):
        self.dt = dt
        self.ekf = ExtendedKalmanFilter(dim_x=6, dim_z=3)

        # Constant-velocity motion model
        self.ekf.F = np.array([
            [1, 0, 0, dt, 0,  0],
            [0, 1, 0, 0,  dt, 0],
            [0, 0, 1, 0,  0,  dt],
            [0, 0, 0, 1,  0,  0],
            [0, 0, 0, 0,  1,  0],
            [0, 0, 0, 0,  0,  1],
        ])

        # Process noise: acceleration treated as zero-mean Gaussian
        # sigma_a = 5 m/s^2 covers FPV manoeuvre envelope
        sigma_a = 5.0
        q = sigma_a**2
        self.ekf.Q = q * np.array([
            [dt**4/4, 0, 0, dt**3/2, 0, 0],
            [0, dt**4/4, 0, 0, dt**3/2, 0],
            [0, 0, dt**4/4, 0, 0, dt**3/2],
            [dt**3/2, 0, 0, dt**2, 0, 0],
            [0, dt**3/2, 0, 0, dt**2, 0],
            [0, 0, dt**3/2, 0, 0, dt**2],
        ])

        # Measurement noise
        # Radar: sigma = 2 m in range, 1 deg in bearing (~17 mrad at 1000 m)
        # Visual: sigma = 0.3 deg in bearing (~5 mrad), no range
        # Combined sigma on position ~ 5 m at 1000 m range
        self.ekf.R = np.diag([25.0, 25.0, 25.0])  # 5 m std on each axis

        # Initial covariance: large until first measurement
        self.ekf.P *= 1000.0

    def predict(self):
        """Propagate state one timestep forward."""
        self.ekf.predict()

    def update(self, measurement_xyz):
        """Incorporate a position measurement."""
        def H_jacobian(x):
            # Identity since we measure position directly (after range+bearing
            # has been converted to ENU in the measurement conversion step)
            return np.array([
                [1, 0, 0, 0, 0, 0],
                [0, 1, 0, 0, 0, 0],
                [0, 0, 1, 0, 0, 0],
            ])
        def h(x):
            return x[:3]
        self.ekf.update(np.array(measurement_xyz), H_jacobian, h)

    def bearing_from_ownship(self, ownship_xyz, lead_time_s=0.2):
        """Return (azimuth, elevation) in radians, projected lead_time_s
        ahead to compensate for gimbal command latency."""
        # Predicted target position at t + lead_time
        x = self.ekf.x.copy()
        x[:3] = x[:3] + x[3:] * lead_time_s

        # Relative vector from ownship to predicted target
        rel = x[:3] - np.array(ownship_xyz)
        azimuth = np.arctan2(rel[1], rel[0])
        elevation = np.arctan2(rel[2], np.linalg.norm(rel[:2]))
        return azimuth, elevation

    def covariance_bearing_deg(self, ownship_xyz):
        """Return the 1-sigma angular uncertainty in the current bearing
        estimate, in degrees. Used to decide whether the lock is good enough
        to keep jamming/relaying or whether to release."""
        rel = self.ekf.x[:3] - np.array(ownship_xyz)
        range_m = np.linalg.norm(rel)
        pos_cov = self.ekf.P[:3, :3]
        # Project position covariance onto the perpendicular plane of rel
        perp_sigma_m = np.sqrt(np.trace(pos_cov) - np.linalg.norm(pos_cov @ rel)**2 / range_m**2)
        return np.degrees(perp_sigma_m / range_m)

The key output is bearing_from_ownship(), which returns an azimuth-elevation pair projected 200 ms ahead of the current time. This lead time matches the measured gimbal command latency (actuator response plus servo loop convergence), so when the gimbal finally arrives at the commanded pose it is pointing at where the target actually is, not where the target was. The second output, covariance_bearing_deg(), gives the 1-sigma angular uncertainty of the estimated bearing. When this exceeds the hard threshold (5° for HOSTILE-LOCK, 3° for FRIENDLY-LOCK), the Lua script releases the lock and returns the gimbal to search pattern or to a fallback pointing.

Why 100 Hz EKF vs 50 Hz decision vs 1 kHz servo

The three rates are chosen to match the physics of each subsystem. The gimbal servo runs at 1 kHz because that is the loop rate at which the physical mechanism can respond to commanded pose without overshoot — any faster and the servo amplifier cannot keep up with commutation; any slower and the servo cannot reject wind buffeting on the mast triplet. The EKF runs at 100 Hz because target measurements arrive from the radar at 50 Hz and from the visual tracker at 30 Hz; running the predict step at 100 Hz and applying updates when measurements become available lets the EKF smoothly interpolate between sensor arrivals without starving the servo on target-state updates. The decision loop runs at 50 Hz on the Cube Orange+ because that is the ArduPilot scheduler tick rate for Lua scripts, which is the native venue for the mode-switching and IFF-gating logic. The three rates are decoupled by a publish-subscribe pattern over the MAVLink bus: the EKF publishes target-state estimates at 100 Hz, the decision loop consumes whichever estimate is newest at its 50 Hz tick, and the gimbal MCU consumes whichever command is newest at its 1 kHz tick.

ArduPilot Lua integration — the decision loop on Cube Orange+

The decision loop is implemented as an ArduPilot Lua script running on the Cube Orange+. Lua is ArduPilot's native scripting layer and is the correct venue for mission-specific logic that must interact with MAVLink commands, parameter reads, and gimbal driver state without being compiled into the flight controller firmware. Lua scripts run at 50 Hz by default and have read-access to every MAVLink message and every ArduPilot parameter, plus write-access to gimbal command channels and custom MAVLink messages. This is exactly the surface area the decision loop needs.

The script below implements the full HOSTILE-LOCK / FRIENDLY-LOCK state machine. It reads target-state estimates from the EKF (published as MAVLink GLOBAL_POSITION_INT with a custom target_id extension), reads IFF classification from Lisa 26 (published as NAMED_VALUE_INT with key iff_class_TARGETID), and writes gimbal commands via MAVLink GIMBAL_MANAGER_SET_ATTITUDE (message 281).

-- cluster_lock_decision.lua
-- ArduPilot Lua script, runs at 50 Hz on Cube Orange+
-- Drives one wing gimbal based on IFF-gated target selection

local SCRIPT_VERSION = "1.0"
local UPDATE_RATE_HZ = 50
local UPDATE_PERIOD_MS = 1000 / UPDATE_RATE_HZ

-- IFF class constants matching iff_gate.py
local IFF_UNKNOWN = 0
local IFF_BLUE    = 1
local IFF_RED     = 2
local IFF_GREY    = 3

-- Lock mode constants
local MODE_NONE    = 0
local MODE_HOSTILE = 1
local MODE_FRIENDLY = 2

-- Configuration parameters (tunable without script edit)
local param_handle_gimbal_id = Parameter()
param_handle_gimbal_id:init('FSGA_GIMBAL_ID')      -- 0 = port, 1 = starboard

local param_handle_max_ekf_sigma_hostile = Parameter()
param_handle_max_ekf_sigma_hostile:init('FSGA_HOSTILE_MAX_SIG')  -- degrees

local param_handle_max_ekf_sigma_friendly = Parameter()
param_handle_max_ekf_sigma_friendly:init('FSGA_FRIENDLY_MAX_SIG')

-- Current lock state
local current_mode = MODE_NONE
local current_target_id = nil
local lock_start_time_ms = 0

function select_target()
   -- Scan MAVLink target list, apply IFF gate, return (target_id, mode) pair
   -- Implementation uses MAVLink MISSION_REQUEST_LIST or custom broadcast
   local targets = mavlink:get_target_list()   -- returns list of {id, iff, l2}
   for _, t in ipairs(targets) do
      if t.iff == IFF_BLUE and t.manet_authenticated then
         return t.id, MODE_FRIENDLY
      elseif t.iff == IFF_RED then
         return t.id, MODE_HOSTILE
      elseif t.iff == IFF_UNKNOWN and t.l2_recommends_engage then
         return t.id, MODE_HOSTILE
      end
   end
   return nil, MODE_NONE
end

function command_gimbal(target_id, mode)
   -- Read target-state EKF output from MAVLink
   local state = mavlink:get_target_state(target_id)
   if state == nil then
      return false  -- no EKF solution yet
   end

   -- Check covariance against mode-specific threshold
   local sigma_deg = state.bearing_sigma_deg
   local max_sigma = (mode == MODE_HOSTILE)
      and param_handle_max_ekf_sigma_hostile:get()
      or  param_handle_max_ekf_sigma_friendly:get()

   if sigma_deg > max_sigma then
      return false  -- lock quality too poor, release
   end

   -- Send gimbal attitude command (MAVLink 281)
   -- The EKF already applied 200 ms lead-time compensation
   mavlink:send_gimbal_manager_set_attitude(
      param_handle_gimbal_id:get(),
      state.azimuth_rad,
      state.elevation_rad
   )

   -- Activate RF payload: Mast A/B for hostile, Mast B relay for friendly
   if mode == MODE_HOSTILE then
      mavlink:send_named_value_int('MAST_A_PA_W', 10)
      mavlink:send_named_value_int('MAST_B_PA_W', 10)
      mavlink:send_named_value_int('MAST_C_PA_W', 0)   -- fuse-controlled
   else  -- MODE_FRIENDLY
      mavlink:send_named_value_int('MAST_A_PA_W', 0)
      mavlink:send_named_value_int('MAST_B_PA_W', 2)   -- relay, reduced power
      mavlink:send_named_value_int('MAST_C_PA_W', 0)
   end

   return true
end

function update()
   -- Decide target and mode
   local target_id, mode = select_target()

   if target_id == nil then
      -- No valid target: park gimbal forward, disable all PAs
      mavlink:send_gimbal_manager_set_attitude(
         param_handle_gimbal_id:get(), 0, 0
      )
      mavlink:send_named_value_int('MAST_A_PA_W', 0)
      mavlink:send_named_value_int('MAST_B_PA_W', 0)
      mavlink:send_named_value_int('MAST_C_PA_W', 0)
      current_mode = MODE_NONE
      current_target_id = nil
   else
      -- Attempt to drive gimbal to target
      local ok = command_gimbal(target_id, mode)
      if ok then
         if target_id ~= current_target_id or mode ~= current_mode then
            -- Lock transition — log for after-action review
            gcs:send_text(6, string.format(
               "Lock %s -> target %d at %d ms",
               (mode == MODE_HOSTILE) and "HOSTILE" or "FRIENDLY",
               target_id, millis():toint()
            ))
            lock_start_time_ms = millis():toint()
            current_mode = mode
            current_target_id = target_id
         end
      else
         -- Lock quality poor: keep trying at next tick without transitioning
      end
   end

   return update, UPDATE_PERIOD_MS
end

gcs:send_text(6, "cluster_lock_decision.lua v" .. SCRIPT_VERSION .. " loaded")
return update()

The script deliberately avoids doing any geometry itself — all the coordinate-transform math lives in the Jetson-side EKF, which publishes the already-lead-time-compensated azimuth-elevation pair. This separation keeps the Lua script small, auditable, and below the CPU budget that ArduPilot allocates to scripting (typically 25 % of one core on Cube Orange+). It also means the Lua script can be swapped without retuning the EKF, and the EKF can be retuned without rebuilding the Lua script.

Why this integrates with Cube Orange+ without breaking ArduPilot safety

ArduPilot enforces a strict separation between flight-critical and non-flight-critical code. The primary flight controller loop (attitude, navigation, failsafe) runs in C++ at 400 Hz on a dedicated RTOS thread and cannot be pre-empted by Lua scripts. Lua scripts run in a separate thread at lower priority with explicit CPU budget enforcement — if a script exceeds its budget it is paused and a warning is emitted to the GCS. This means the worst-case behaviour of cluster_lock_decision.lua is that it stops running, in which case the gimbals park forward and all PAs disable. The flight controller continues flying the aircraft regardless. The gimbal itself is driven by the same MAVLink gimbal manager that controls any other ArduPilot-compatible gimbal (Gremsy, Freefly, CubePilot IR-LOCK) — there is no custom firmware on the flight controller and no custom driver; the cluster uses standard MAVLink message 281 (GIMBAL_MANAGER_SET_ATTITUDE) which ArduPilot has supported since 4.3.

The MAVLink channel dedicated to cluster commands is SERIAL7 on the Cube Orange+ carrier board, configured at 921600 baud with protocol MAV_PROTO_2. This channel carries the target-state messages from the Jetson (inbound) and the gimbal commands plus PA control messages to the cluster MCUs (outbound). The link is shared with no other subsystem, so the 921600 baud headroom (approximately 90 kB/s bidirectional after overhead) is available entirely to the lock-on pipeline. At the decision loop's 50 Hz rate and approximately 100 B per gimbal command, the channel utilisation is about 5 %. Even with full 200 Hz target-state publication from the Jetson, utilisation stays below 25 %.

CUBE ORANGE+ MAVLINK CHANNEL ALLOCATION

SERIAL1 (USB-C)
GCS telemetry (mission planner, QGroundControl)
SERIAL2 (TELEM2)
Silvus/Starlink radio link
SERIAL3 (GPS1)
u-blox F9P primary GPS
SERIAL4 (GPS2)
u-blox F9P secondary GPS, blended
SERIAL5 (ADS-B)
uAvionix ADS-B 1090 MHz receiver
SERIAL6 (Jetson)
Jetson Orin Nano: EKF output, AI detection stream
SERIAL7 (Cluster)
Cluster MCU network: gimbal commands + PA control + status
CAN1 (Cluster CAN-FD)
High-rate gimbal servo telemetry (1 kHz encoder feedback)
CAN2 (Backup)
Redundant cluster CAN in case CAN1 fails

The friendly-lock mode's purpose is to extend the command range of a whitelisted drone by using the cluster's +10 dBi directional antenna instead of an omnidirectional antenna. The link-budget derivation is straightforward Friis equation, but the directional advantage deserves explicit quantification because it is the entire justification for the mode.

# pip install numpy
# relay_link_budget.py — compare omni relay vs friendly-lock directional relay
import numpy as np

def friis_rx_dbm(tx_power_w, tx_gain_dbi, rx_gain_dbi,
                   freq_hz, range_m):
    """Return received power in dBm from Friis equation."""
    tx_dbm = 10 * np.log10(tx_power_w * 1000)   # W to dBm
    fspl_db = 20 * np.log10(4 * np.pi * range_m * freq_hz / 3e8)
    return tx_dbm + tx_gain_dbi + rx_gain_dbi - fspl_db

# Scenario: friendly FPV at 10 km from Fischer 26E-LE cluster
# Omni-relay case: cluster uses a +2 dBi omni antenna
# Friendly-lock case: cluster uses a +10 dBi directional beam
# Both cases: PA at 2 W, 2.4 GHz, FPV has +2 dBi receive antenna

freq = 2.4e9    # 2.4 GHz ISM
tx_power_w = 2.0
fpv_rx_gain = 2.0
ranges_m = [5000, 10000, 15000, 20000, 25000]

print("Received power at FPV (dBm):")
print(f"{'Range km':10s} {'Omni relay':12s} {'Friendly-lock':14s} {'Advantage':10s}")
for r in ranges_m:
    p_omni = friis_rx_dbm(tx_power_w, 2.0, fpv_rx_gain, freq, r)
    p_dir  = friis_rx_dbm(tx_power_w, 10.0, fpv_rx_gain, freq, r)
    print(f"{r/1000:10.1f} {p_omni:12.1f} {p_dir:14.1f} "
          f"{(p_dir - p_omni):10.1f} dB")

# FPV command-link minimum usable sensitivity: -100 dBm
# for ELRS 2.4 GHz at 500 Hz packet rate
SENSITIVITY_DBM = -100

# Find max range at which each relay mode stays above sensitivity
for label, tx_gain in [('Omni relay', 2.0), ('Friendly-lock', 10.0)]:
    # Solve Friis for range
    # P_rx = P_tx + G_tx + G_rx - FSPL
    # FSPL = P_tx + G_tx + G_rx - SENSITIVITY_DBM
    tx_dbm = 10 * np.log10(tx_power_w * 1000)
    max_fspl = tx_dbm + tx_gain + fpv_rx_gain - SENSITIVITY_DBM
    max_range = 3e8 / (4 * np.pi * freq) * 10**(max_fspl / 20)
    print(f"{label:15s} max range: {max_range/1000:.1f} km")

# Output:
# Received power at FPV (dBm):
# Range km   Omni relay   Friendly-lock  Advantage
#        5.0       -80.0          -72.0         8.0 dB
#       10.0       -86.0          -78.0         8.0 dB
#       15.0       -89.6          -81.6         8.0 dB
#       20.0       -92.1          -84.1         8.0 dB
#       25.0       -94.0          -86.0         8.0 dB
# Omni relay      max range: 25.1 km
# Friendly-lock   max range: 63.3 km

The directional beam gives a consistent 8 dB advantage at every range (the difference +10 − 2 dBi in the antenna gains; range does not change the advantage because it falls out of Friis proportionally). In link-budget terms, 8 dB corresponds to roughly 2.5× more range at the same sensitivity threshold. The concrete output: a 2 W omni relay reaches a friendly FPV at 25 km; the same 2 W through a friendly-lock directional beam reaches 63 km. This is the operational justification for tracking friendlies — the cluster platform can support FPVs operating at ranges that an omnidirectional relay cannot cover.

There is a cost: the gimbal can only point one direction at a time. If two friendly FPVs are operating in different sectors, the cluster can either relay one at the long range and the other at omnidirectional range (using the dorsal DOM-mast for the second), or time-share the gimbal at reduced duty cycle per friendly. This is where the two-gimbal architecture pays off — each gimbal can lock a different friendly in the SPLIT-100/100 configuration, giving two simultaneous long-range directional relay links.

Mode switching and pre-emption

An operationally realistic situation is that a gimbal is in FRIENDLY-LOCK on FPV-Alpha at 15 km, and a hostile drone suddenly appears at 3 km. The question the decision loop must answer is whether to maintain the relay (keeping FPV-Alpha in its engagement) or pre-empt to HOSTILE-LOCK (defending Fischer 26E-LE from the closer threat). The pre-emption policy is parameterised and tunable per mission; the default follows a simple rule set that is safe enough to never be surprising:

  1. If the hostile is within 2 km of Fischer 26E-LE and approaching at > 20 m/s, pre-empt immediately. Lose the friendly relay; survival of the platform outweighs mission continuity.
  2. If the hostile is 2–5 km out and not closing, request operator decision. The decision loop posts a MAVLink MISSION_ITEM with the proposed pre-emption to GCS, which the operator can approve or reject within 3 seconds before the loop defaults to maintaining the friendly lock.
  3. If the hostile is > 5 km out, task the other gimbal to engage. No pre-emption of the friendly link needed; SPLIT-100/100 naturally handles this.

Rule 1 is the non-negotiable safety rule. Rules 2 and 3 are operator-tunable via Lua parameters — a mission planner can tighten rule 2 to 1 km if the mission is ISR-primary and surviving to relay is more important than engaging, or loosen it to 10 km if the platform carries surplus jamming capacity.

Safety invariants that cannot be tuned away
Three interlocks are enforced in hardware or firmware below the Lua scripting layer and cannot be overridden by parameter tuning: (1) the sealed civilian-band fuse on Mast C cannot be broken by the decision loop under any circumstance, regardless of rule settings. (2) The IFF gate cannot emit HOSTILE-LOCK against a BLUE target; this is a hardcoded invariant in the C++ IFF module. (3) PA drive power is clamped to 10 W per mast in firmware; a runaway Lua script cannot command higher transmit power than the hardware regulator allows.

Failure modes and graceful degradation

Every autonomous system must have a thorough failure catalogue. The following table lists what happens when each component of the lock-on pipeline fails, and the resulting safe state.

Failed componentDetectionDegraded behaviourSafe state
Jetson EKF crash MAVLink target-state heartbeat lost > 1 s Lua script falls back to raw radar bearing (lower accuracy) Continues with ~3× larger bearing error
Radar detection lost Radar frame count stops incrementing Visual tracker alone; loses range estimate Azimuth/elevation only, no lead-time compensation
Visual tracker lost YOLOv8 returns no detection in gimbal field of view Radar alone; loses fine bearing accuracy Coarser bearing lock, jamming effectiveness reduced ~3 dB
Gimbal servo stuck Encoder feedback does not match commanded angle Mast A/B fall back to omni pattern; DOM-mast takes relay role No directional gain on one wing; accept degraded range
Lua script CPU overrun ArduPilot scheduler warns > 25 % CPU use Script paused; gimbals park forward; PAs disabled Airframe flies normally; no jamming, no directional relay
MANET crypto desync BLUE target fails 3 consecutive challenge-responses Target reclassified to UNKNOWN; friendly-lock released Operator must re-verify manually or accept loss of relay
Cube Orange+ reboot Flight controller reset detected Gimbals return to hardware default (park forward), PAs disabled, Lua reloads from storage ~30 s outage; aircraft stays airborne via failsafe logic

The two rows shaded amber and red represent the failure modes where human intervention is required. The MANET crypto desync case is operationally common — a friendly drone with a time-drifted clock will fail the rotating challenge-response even though it is physically the correct drone. The amber cell is a design decision: rather than auto-relax the authentication, the system releases the lock and forces the operator to verify out-of-band. This is the same pattern that airline transponders use when IFF interrogation fails (they do not auto-accept; the ground controller resolves the ambiguity).

Testing and TRL progression

The components described above are at mixed TRL levels. The EKF algorithm itself is TRL 6 — identical implementations fly on operational quadrotors worldwide. The ArduPilot Lua integration pattern is TRL 7 — used in commercial UAVs for the past four years. The specific cluster_lock_decision.lua script and the friendly-lock directional relay concept are TRL 2 — analytical design, no physical test. The cluster hardware itself (the seven-mast arrangement) is TRL 2. The MANET crypto challenge-response using Silvus StreamCaster is TRL 9 — operational for years, but the integration into Fischer 26's lock-on gating logic has not been tested.

An implementing force progressing this architecture should test in the following sequence: (1) bench-test the EKF against synthetic target trajectories from a flight simulator; (2) hardware-in-loop test with a real Cube Orange+ running the Lua script and fake target-state messages; (3) ground-based gimbal tracking test with an optical target, no RF payload; (4) captive-carry flight test with a single mast and target drone; (5) full cluster flight test with both lock modes. Each stage should take 2–4 weeks; the full progression to TRL 6 is roughly 12 months for a dedicated team.

TRL PROGRESSION TEST SEQUENCE

Step 1 — EKF bench test
Synthetic trajectories from SITL; validate 100 Hz update rate and covariance behaviour. Duration 2 weeks.
Step 2 — HIL with Cube Orange+
Lua script + real Cube + fake target stream. Validate MAVLink flow and 50 Hz decision rate. Duration 2 weeks.
Step 3 — Ground gimbal + optical target
Real gimbal tracking a real drone at close range, no RF payload. Validate pointing accuracy and lead-time compensation. Duration 4 weeks.
Step 4 — Captive carry single mast
Fischer 26E-LE flying with one mast active against a test drone. Validate flight-controller integration and EMCON behaviour. Duration 4 weeks.
Step 5 — Full cluster flight
Both gimbals active in SPLIT-100/100 against hostile and friendly test drones simultaneously. Duration 4+ weeks.
Total to TRL 6
Approximately 12 months with a 4–6 person team

TRL Status and Important Notes

TRL 2 for the integrated lock-on pipeline. The EKF math is validated and widely implemented; the Lua scripting pattern is proven. What has not been built or tested is the specific coupling between them for dual-mode (hostile/friendly) cluster operation. No hardware-in-loop test has run. No flight-test data exists.

Gaps in current engineering evidence. No Jetson-side EKF has been coded for this specific pipeline — the Python above is reference skeleton, not flown software. Lua script CPU usage has not been profiled on an operational Cube Orange+ at full cluster workload. MANET crypto challenge-response timing has not been measured against the 3-second release threshold. The 200 ms end-to-end latency budget is estimated, not observed.

Prerequisites before operational fielding. The Jetson EKF needs to be packaged as a ROS2 node with its sigma_a process-noise parameter tuned to measured FPV and hostile manoeuvre data. The full five-step TRL progression listed earlier must be completed. ArduPilot scheduler headroom under simultaneous seven-mast operation must be verified so the 50 Hz decision tick does not starve. Hardware safety invariants (Mast C fuse, IFF gate, PA clamp) need tamper-test validation. The Lua script itself must pass whatever flight-critical software review process the fielding force uses — ArduPilot Lua is not formally certified so the review obligation stays with the implementing organisation.

Sources and Formal Verification

Technical parameter sources. ArduPilot Lua scripting documentation at ardupilot.org/copter/docs/common-lua-scripts.html (50 Hz tick, CPU budget enforcement). MAVLink v2 common message definitions, specifically MESSAGE 281 GIMBAL_MANAGER_SET_ATTITUDE. FilterPy library documentation for EKF implementation (Roger R. Labbe, Kalman and Bayesian Filters in Python). CubePilot Cube Orange+ specifications from manufacturer datasheet (STM32H753VI, 480 MHz, 2× CAN-FD, 7× UART). Silvus StreamCaster MANET cryptographic challenge-response: public documentation from Silvus Technologies. Friis equation and free-space path loss from Balanis, Antenna Theory: Analysis and Design, 4th ed. ELRS 2.4 GHz sensitivity figure from ExpressLRS Wiki (−100 dBm at 500 Hz packet rate). YOLOv8 latency on Jetson Orin Nano: NVIDIA technical brief (TOPS per inference, 30 FPS at 640×640 resolution).

Existing formal verification. Several numerical claims referenced here have proof entries in provable_claims.py:

Proofs to add during TRL progression. The friendly-lock relay range advantage (8 dB, 2.5× range), the pre-emption rule set, the 200 ms end-to-end latency budget, and the Lua script CPU usage will need their own entries in provable_claims.py once hardware-in-loop or flight data replaces the current estimates.

Sources

Cross-references within the FSG-A wiki — cluster hardware on fischer26-antenna-cluster.html; brigade-level IFF logic on lisa26-iff-deconfliction.html; ArduPilot Lua patterns on behavior-trees.html; flight-controller EKF3 on ekf3-sensor-fusion.html; ISR-to-strike handoff on fischer26-targeting.html; frequency-hopping self-protection on adaptive-fhss.html. Primary technical sources are in the section above.