#!/usr/bin/python3
import os
import sys
import json
import copy
import argparse
import subprocess
import shlex

from voxl_configure_common import *

VTX_SERVICE_NAME="voxl-vtx"
CAMERA_SERVICE_NAME="voxl-camera-server"

VTX_CONFIG_FILE = "/etc/modalai/voxl-vtx.conf"
CAMERA_CONFIG_FILE = "/etc/modalai/voxl-camera-server.conf"

H264_MAX_PAYLOAD = 1342
H265_MAX_PAYLOAD = 1341
DEFAULT_PAYLOAD  = 1000
LONG_RANGE_PAYLOAD = 750

# Define standard profiles
DEFAULT_PROFILE = {
    "name": "Default",
    "mcs": 0,
    "mtu": DEFAULT_PAYLOAD,
    "power": 1000,
    "source": "hires_default_misp_encoded",
    "udp_camera_bitrate_bps": 500000,
    "invert_toggle":   -1,
    "joystick_toggle": 13,
    "ai_detection_enable": True,
    "ai_detection_source": "object_tracker_meta",
    "ai_detection_qty": 3,
    "ai_detection_threshold": 0.5
}

TRACKING_FRONT_DEFAULT_CONFIG = {
    "name": "Tracking Front",
    "mcs": 0,
    "mtu": DEFAULT_PAYLOAD,
    "power": 1000,
    "source": "tracking_front_misp_encoded",
    "udp_camera_bitrate_bps": 500000,
    "invert_toggle":   -1,
    "joystick_toggle": 10,
    "ai_detection_enable": False,
    "ai_detection_source": "object_tracker_meta",
    "ai_detection_qty": 3,
    "ai_detection_threshold": 0.75
}

LONG_RANGE_PROFILE = {
    "name": "Long Range",
    "mcs": 0,
    "mtu": LONG_RANGE_PAYLOAD,
    "power": 1000,
    "source": "hires_long_range_misp_encoded",
    "udp_camera_bitrate_bps": 500000,
    "invert_toggle":   -1,
    "joystick_toggle": -1,
    "ai_detection_enable": True,
    "ai_detection_source": "object_tracker_meta",
    "ai_detection_qty": 3,
    "ai_detection_threshold": 0.5
}

LOW_LATENCY_PROFILE = {
    "name": "Low Latency",
    "mcs": 3,
    "mtu": H265_MAX_PAYLOAD,
    "power": 750,
    "source": "hires_low_latency_misp_encoded",
    "udp_camera_bitrate_bps": 500000,
    "invert_toggle":   -1,
    "joystick_toggle": -1,
    "ai_detection_enable": True,
    "ai_detection_source": "object_tracker_meta",
    "ai_detection_qty": 3,
    "ai_detection_threshold": 0.5
}

# Boson config
BOSON_DEFAULT_CONFIG:dict = {
    "name":     "Boson",
    "mcs":      0,
    "mtu":      H265_MAX_PAYLOAD,
    "power":    1000,
    "source":   "boson_misp_encoded",
    "udp_camera_bitrate_bps": 500000,
    "invert_toggle":   12,
    "joystick_toggle":   11,
    "ai_detection_enable": False,
    "ai_detection_source": "object_tracker_meta",
    "ai_detection_qty":   3,
    "ai_detection_threshold": 0.75
}

# Boson UVC config
BOSON_UVC_DEFAULT_CONFIG:dict = {
    "name":     "Boson",
    "mcs":      0,
    "mtu":      H265_MAX_PAYLOAD,
    "power":    1000,
    "source":   "uvc_encoded",
    "udp_camera_bitrate_bps": 500000,
    "invert_toggle":   12,
    "joystick_toggle":   -1,
    "ai_detection_enable": False,
    "ai_detection_source": "object_tracker_meta",
    "ai_detection_qty":   3,
    "ai_detection_threshold": 0.75
}

# Lepton config
LEPTON_DEFAULT_CONFIG:dict = {
    "name":     "Lepton",
    "mcs":      0,
    "mtu":      H265_MAX_PAYLOAD,
    "power":    1000,
    "source":   "lepton0_encoded",
    "udp_camera_bitrate_bps": 500000,
    "invert_toggle":   -1,
    "joystick_toggle":   -1,
    "ai_detection_enable": False,
    "ai_detection_source": "object_tracker_meta",
    "ai_detection_qty":   3,
    "ai_detection_threshold": 0.75
}

# Downward Tracking config
DOWNWARD_TRACKING_DEFAULT_CONFIG:dict = {
    "name":     "Tracking Down",
    "mcs":      0,
    "mtu":      DEFAULT_PAYLOAD,
    "power":    1000,
    "source":   "tracking_down_misp_encoded",
    "udp_camera_bitrate_bps": 500000,
    "invert_toggle":   -1,
    "joystick_toggle":   8,
    "ai_detection_enable": False,
    "ai_detection_source": "object_tracker_meta",
    "ai_detection_qty":   3,
    "ai_detection_threshold": 0.75
}

config_header = """/**
 * voxl-vtx Configuration File
 *
 * ============================================================================
 * WIRELESS TRANSMISSION SETTINGS
 * ============================================================================
 * frequency: Center frequency in MHz (VRX must match)
 * bandwidth: Channel bandwidth in MHz (20 or 40, VRX must match)
 * card: WiFi interface name (e.g., "wlan0")
 * enable_pcap: Enable RF PCAP injection
 * enable_pit: Enable PIT mode (low transmit power for bench testing)
 *
 * ============================================================================
 * FORWARD ERROR CORRECTION
 * ============================================================================
 * enable_fec: Enable Forward Error Correction (VRX must match, True/False)
 * fec_percent: FEC overhead percentage [-1, 1000]
 *              -1: Auto calculated FEC %, minimum of 75%
 *               0: No FEC applied but headers included in payload
 *               >0: Specific percentage of FEC to use (Ex: 20, 75, 100)
 * fec_block_size: Number of fragments per FEC block (larger will be more computationally expensive)
 *
 * ============================================================================
 * ADVANCED WIRELESS SETTINGS
 * ============================================================================
 * enable_stbc: Enable Space-Time Block Coding (number of spatial streams)
 * regulatory_mode: Regulatory compliance mode ("off", "fcc")
 *
 * ============================================================================
 * VIDEO PROFILE CONFIGURATION
 * ============================================================================
 * active_profile_idx: Index of currently active video profile (0-based)
 * rc_toggle: RC channel to use for profile switching
 * osd_rc_toggle: RC channel for OSD enable/disable control (-1=disabled, 1-18=channel, LOW=osd enabled, HIGH=osd disabled)
 * osd_joystick_toggle: Joystick button bit for OSD enable/disable (-1=disabled, 0-32=bit, LOW=osd enabled, HIGH=osd disabled)
 * led_joystick_button_off: Joystick button bit that indicates LEDs off when pressed alone (-1=disabled, 0-31=bit, default 3)
 * led_joystick_button_ir: Joystick button bit that indicates IR LEDs on when pressed alone (-1=disabled, 0-31=bit, default 4)
 *                         Neither button pressed = overt LEDs on, both pressed = no change
 * zoom_joystick_buttons: Array of joystick button indices (0-31) for zoom presets. Each button sets an
 *   evenly-spaced zoom level between 1.0x and zoom_max. Example: [14,15,16,17,18,19] with zoom_max=4.0
 *   maps btn14=1.0x, btn15=1.5x, btn16=2.25x, btn17=2.75x, btn18=3.5x, btn19=4.0x.
 *   Buttons 0-15 use MANUAL_CONTROL.buttons; buttons 16-31 use buttons2.
 *   Set to [] to disable (default). Compatible with zoom_ch - both can be active simultaneously.
 * profiles: Array of video stream profiles (see profile section below)
 *
 * Profile Configuration:
 *   name: Profile name (Default, Boson, etc)
 *   source: Pipe name for video input (ex: "hires_default_misp_encoded")
 *   mcs: Modulation and Coding Scheme (0-7, higher=faster but less robust)
 *   mtu: Image payload size in bytes
 *   power: Transmit power in mW (0-1000)
 *   invert_toggle: RC channel to use for camera color invert (Boson only, white/black hot)
 *   joystick_toggle: Button bit to use for joystick control
 *   ai_detection_enable: Enable AI object detection for this profile
 *   ai_detection_source: Source pipe for object detection data for this profile 
 *   ai_detection_qty: Maximum number of detected objects to pass to VRX
 *   ai_detection_threshold: Detection confidence threshold (0.0-1.0)
 *
 * ============================================================================
 * TELEMETRY & DATA RATES
 * ============================================================================
 * info_rate_hz: Rate in hertz at which VTX info packets are sent to VRX
 * mavlink_telem_rate_hz: Rate in hertz at which MAVLink telemetry is forwarded to VRX
 * team_race_en: Enable team race behavior 
 *            "off": Disabled
 *            "on":  Enabled 
 *            "auto": Attempt to auto detect from ELRS receiver settings
 * team_race_tx_timeout_ms: Amount of time in milliseconds to TX for after ELRS failsafe
 * rc_deadman_en: Enable RC dead man's switch. Disables all TX when the flight
 *                controller stops reporting a healthy RC link (MAVLink SYS_STATUS
 *                RC_RECEIVER health bit, or external FC rc_failsafe) for longer
 *                than rc_deadman_tx_timeout_ms. TX resumes as soon as RC recovers.
 * rc_deadman_tx_timeout_ms: Amount of time in milliseconds to keep TXing after RC loss before disabling TX
 *
 * ============================================================================
 * UDP MODE (for LTE/Doodle/Silvus/etc)
 * ============================================================================
 * udp: Use UDP for TX/RX instead of packet injection
 * udp_interface_ip: Local IP address for UDP interface
 * udp_interface_tx_port: Local transmit port for UDP (-1 to disable)
 * udp_interface_rx_port: Local receive port for UDP
 * udp_receiver_configs: Array of remote UDP receivers to send to
 *   ip: IP address of remote endpoint
 *   port: Port of remote endpoint
 *
 * ============================================================================
 * RF/IP VIDEO FALLBACK (requires both RF and IP/UDP transports configured)
 * ============================================================================
 * enable_ip_fallback: Automatically move video TX to IP when the RF link degrades,
 *                         and back to RF when it recovers.
 * snr_low_threshold: Switch video RF->IP when RF SNR (dB) sustains below this value
 * snr_high_threshold: Switch video IP->RF when RF SNR (dB) sustains above this value (hysteresis)
 * link_switch_dwell_ms: How long the SNR condition must hold before switching
 * ip_liveness_timeout_ms: IP is "available" only if a packet arrived over UDP within this window
 * pcap_liveness_timeout_ms: RF is "alive" only if a packet arrived over pcap within this window.
 *                           If RF goes silent (no pcap packets) and IP is available, video TX
 *                           switches to IP immediately regardless of last reported SNR.
 *
 * ============================================================================
 * HARDWARE & SYSTEM SETTINGS
 * ============================================================================
 * i2c_port: I2C bus for temperature/power monitoring
 *           -1: Disabled
 *           0+: Enabled, Autodetect VTX power and temperature sensor
 * perf_mode: Set CPU to performance governor when armed for lower latency (True/False)
 *
 * ============================================================================
 * DEBUG & TESTING
 * ============================================================================
 * disable_transmissions: Disable wireless transmissions (for testing/debug)
 * debug_log: Debug logging to disk behavior
 *            "off": No logging enabled
 *            "on":  Start logging on application startup
 *            "arm": Start logging when drone is armed
 *
 */
 """

default_config = {
    # WIRELESS TRANSMISSION SETTINGS
    "frequency": 5805,
    "bandwidth": 20,
    "card": "wlan0",
    "enable_pcap": True,
    "enable_pit": True,

    # FORWARD ERROR CORRECTION
    "enable_fec": True,
    "fec_percent": -1,
    "fec_block_size": 30,

    # ADVANCED WIRELESS SETTINGS
    "enable_stbc": 1,
    "regulatory_mode": "off",

    # Zoom settings
    "zoom_ch": 12,
    "zoom_max": 4.0,
    "zoom_ch_deadband": 10,
    "zoom_joystick_buttons": [14, 15, 16, 17, 18, 19],

    # VIDEO PROFILE CONFIGURATION
    "active_profile_idx": 0,
    "rc_toggle": -1,
    "rc_switch_profiles": [],
    "osd_rc_toggle": -1,
    "osd_joystick_toggle": -1,
    "led_joystick_button_off": 3,
    "led_joystick_button_ir": 4,
    "profiles": [],

    # TELEMETRY & DATA RATES
    "info_rate_hz": 5,
    "mavlink_telem_rate_hz": 30,
    "team_race_en": "off",
    "team_race_tx_timeout_ms": 0,

    # UDP MODE
    "udp": False,
    "udp_interface_ip": "",
    "udp_interface_tx_port": -1,
    "udp_interface_rx_port": 50001,
    "enable_ip_fallback": False,
    "snr_low_threshold": 15,
    "snr_high_threshold": 20,
    "link_switch_dwell_ms": 1000,
    "ip_liveness_timeout_ms": 2000,
    "pcap_liveness_timeout_ms": 2000,
    "rc_deadman_en": False,
    "rc_deadman_tx_timeout_ms": 60000,
    "udp_receiver_configs": [],

    # HARDWARE & SYSTEM SETTINGS
    "i2c_port": 0,  # Always enable this
    "perf_mode": False,

    # DEBUG & TESTING
    "disable_transmissions": False,
    "debug_log": "off"
}

# Parse args
def parse_args() -> dict:
    """
    Parse and validate args
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument('-h', '--help',        help='Show this help message and exit', required=False, action='store_true')
    parser.add_argument('-l', '--list',        help='Show the current configuration',  required=False, action='store_true')

    # Wifibroadcast args
    parser.add_argument('-F', '--frequency',   help='Set frequency', required=False, type=int)
    parser.add_argument('-a', '--active_profile_idx',  help='Set Active Profile', required=False, type=int)

    # Profile specific args
    parser.add_argument('-m', '--mcs',         help='Set MCS index', required=False, type=int)
    parser.add_argument('-s', '--source',      help='Set pipe to get video from', required=False, type=str)
    parser.add_argument('-p', '--power',       help='Set transmitter power (idx)', required=False, type=int)
    parser.add_argument('-P', '--power-mw',    help='Set transmitter power in mW', required=False, type=int)

    # Switch between voxl-vtx and voxl-softap and factory settings for voxl-vtx
    parser.add_argument('-t', '--toggle',               help='Toggle between voxl-vtx and voxl-softap',   required=False, action='store_true')
    parser.add_argument('-e', '--enable',               help='Load modified kernel module for RTL8812AU', required=False, action='store_true')
    parser.add_argument('-d', '--disable',              help='Load default kernel module for RTL8812AU',  required=False, action='store_true')
    parser.add_argument('-f', '--factory_enable',       help='Load modified kernel module for RTL8812AU and restore default configuration file', required=False, action='store_true')
    parser.add_argument('-L', '--factory_enable_lte',   help='Setup for LTE operation', required=False, action='store_true')
    parser.add_argument('-I', '--factory_enable_fiber', help='Setup for fiber operation', required=False, action='store_true')
    parser.add_argument('-D', '--factory_enable_doodle',    help='Setup for Doodle operation', required=False, action='store_true')
    parser.add_argument('--factory_enable_dtc',       help='Setup for DTC operation', required=False, action='store_true')
    parser.add_argument('--lepton_enable',            help='Setup for Lepton', required=False, action='store_true')

    # SKU specific setups
    parser.add_argument('--d0008_v4',             help='Setup for D0008_v4', required=False, action='store_true')
    parser.add_argument('--d0008_v5',             help='Setup for D0008_v5', required=False, action='store_true')
    parser.add_argument('--d0008_v5_X2',          help='Setup for D0008_v5_X2', required=False, action='store_true')
    parser.add_argument('--d0013',                help='Setup for D0013',    required=False, action='store_true')
    parser.add_argument('--d0020_strike',         help='Setup for D0020 strike', required=False, action='store_true')

    # UDP helpers
    parser.add_argument('--enable_ip_fallback',      help='Enable IP fallback', required=False, action='store_true')
    parser.add_argument('--enable_rc_deadman',       help="Enable RC dead man's switch", required=False, action='store_true')
    parser.add_argument('-A', '--add_udp_receiver',      help='Add UDP receiver', required=False, type=str)
    parser.add_argument('-R', '--remove_udp_receiver',      help='Remove UDP receiver', required=False, type=str)
    parser.add_argument('-C', '--clear_udp_receivers',      help='Clear all UDP receivers', required=False, type=str)

    # Wizard
    parser.add_argument('-w', '--wizard',     help='Start interactive wizard', required=False, action='store_true')

    return vars(parser.parse_args())

# Print help message
def print_usage() -> None:
    """
    Print usage/help message
    """

    print("Description: Configure voxl-vtx settings.")
    print("")
    print("Usage: voxl-configure-vtx -h -l -F -m -s -p -P -t -e -d -f -w")
    print("")
    print("Optional Arguments (for VTX settings only, besides the wizard):")
    print(" -h, --help                  Show this help message and exit")
    print(" -l, --list                  Show the current configuration")

    # Wifibroadcast args
    print(" -F, --frequency             Set frequency")
    print(" -m, --mcs                   Set MCS index for the current active profile")
    print(" -s, --source                Set pipe to get video from for the current active profile")
    print(" -p, --power                 Set power idx (not mW) for the current active profile (does not save to configuration file)")
    print(" -P, --power-mw              Set power in milliwatts (mW) for the current active profile")


    # Toggle wifibroadcast/softap args
    print(" -t, --toggle                    Toggle between Wifibroadcast and SoftAP mode")
    print(" -e, --enable                    Load modified kernel module for RTL8812AU and enable voxl-vtx service")
    print(" -d, --disable                   Load default kernel module for RTL8812AU and remove modified kernel module after stopping and disabling voxl-vtx service")
    print(" -f, --factory_enable            Load modified kernel module for RTL8812AU, set efuse, enable voxl-vtx service, and restore default configuration file")
    print(" -L, --factory_enable_lte        Setup for LTE operation")
    print(" -D, --factory_enable_doodle     Setup for Doodle radio")
    print(" --factory_enable_dtc            Setup for DTC radio")
    print(" --lepton_enable                 Setup for Lepton")

    # SKU specific setups
    print("     --d0008_v4              Setup for d0008_v4")
    print("     --d0008_v5              Setup for d0008_v5")
    print("     --d0008_v5_X2           Setup for d0008_v5_X2")
    print("     --d0013                 Setup for d0013")
    print("     --d0020_strike          Setup for d0020 strike")

    # Wizard
    print(" -w, --wizard                Start interactive wizard")
    print("")

# Change between voxl-vtx and softap, they both use the same network interface so only one can run at a time
def toggle_vtx_softap() -> int:
    """
    Change between voxl-vtx and softap
    """
    if service_running("voxl-softap"):
        print("Currently in softap mode. Transitioning to vtx mode: disabling voxl-softap and enabling voxl-vtx\n")
        if not (disable_and_stop_service("voxl-softap", announce=True) and enable_and_start_service("voxl-vtx", announce=True)):
            print(f"{BOLD}{RED}Failed to transition to VTX mode{CLEAR}")
            return -1
    elif service_running("voxl-vtx"):
        print("Currently in VTX mode. Transitioning to softap mode: disabling voxl-vtx and enabling voxl-softap\n")
        if not (disable_and_stop_service("voxl-vtx", announce=True) and enable_and_start_service("voxl-softap", announce=True)):
            print(f"{BOLD}{RED}Failed to transition to softap mode{CLEAR}")
            return -1
    else:
        print("Neither voxl-vtx or voxl-softap currently running!")
    return 0

# The VTX config file: path, header, and defaults bound once
CONFIG = ConfigFile(VTX_CONFIG_FILE, config_header, default_config)

def camera_rc_switch_slots(profiles: list) -> list:
    """
    Default RC switch wheel slots for camera profiles (Default, Boson,
    Tracking, etc): Long Range and Low Latency stay OSD-menu-only. Capped at
    5 so the equal RC bins stay wide enough to select reliably.
    """
    osd_only = (LONG_RANGE_PROFILE["source"], LOW_LATENCY_PROFILE["source"])
    slots = [i for i, p in enumerate(profiles) if p.get("source") not in osd_only][:5]
    return slots if len(slots) >= 2 else []  # <2 slots is pointless; empty disables switching

def write_factory_default_config() -> None:
    """
    Write the factory default config file.
    """
    save_json_with_header(VTX_CONFIG_FILE, default_config, config_header)

class SkuPreset:
    """
    Per-SKU hardware settings applied to an existing config (i2c bus for the
    power/temperature monitor, RC channel for profile switching).
    rc_toggle None means the SKU does not touch rc_toggle; clear_rc_switch
    disables RC profile switching entirely.
    """
    def __init__(self, i2c_port: int, rc_toggle=None, clear_rc_switch: bool = False):
        self.i2c_port = i2c_port
        self.rc_toggle = rc_toggle
        self.clear_rc_switch = clear_rc_switch

SKU_PRESETS = {
    "d0008_v4":     SkuPreset(i2c_port=1, rc_toggle=5),
    "d0008_v5":     SkuPreset(i2c_port=1, rc_toggle=11),
    "d0008_v5_X2":  SkuPreset(i2c_port=1, rc_toggle=11),
    # Stinger V2 supports I2C power and temperature monitoring on i2c-0: https://docs.modalai.com/M0195/#modalai-vtx-i2c-interface
    "d0013":        SkuPreset(i2c_port=0, rc_toggle=11),
    "d0020_strike": SkuPreset(i2c_port=1, clear_rc_switch=True),
}

class RadioPreset:
    """
    A radio/link factory setup: config overrides applied on top of a freshly
    written default config (insertion order only matters for the printed
    'key -> value' lines). force_d0008_v5 mirrors the fiber setup, which
    always builds its default config as a d0008_v5.
    """
    def __init__(self, overrides: dict, force_d0008_v5: bool = False):
        self.overrides = overrides
        self.force_d0008_v5 = force_d0008_v5

# Radio factory presets, keyed by their CLI flag name. 
# Adding a new radio setup is a new entry here.
RADIO_PRESETS = {
    "factory_enable_lte": RadioPreset(
        overrides={
            "enable_pcap": False,
            "udp": True,
        }),
    "factory_enable_doodle": RadioPreset(
        overrides={
            "enable_pcap": False,
            "udp": True,
            "udp_interface_ip": "10.223.0.100",
            "udp_receiver_configs": [{"ip": "10.223.0.101", "port": 50000}],
        }),
    "factory_enable_dtc": RadioPreset(
        overrides={
            "enable_pcap": False,
            "udp": True,
            "udp_interface_ip": "192.168.0.100",
            "udp_receiver_configs": [{"ip": "192.168.0.101", "port": 50000}],
        }),
    "factory_enable_fiber": RadioPreset(
        overrides={
            "enable_pcap": False,
            "udp": True,
            "udp_interface_ip": "10.0.0.10",
            "udp_receiver_configs": [{"ip": "10.0.0.2", "port": 50000}],
        },
        force_d0008_v5=True),
}

# Order radio presets are applied in when multiple flags are given
RADIO_PRESET_DISPATCH = (
    "factory_enable_lte",
    "factory_enable_doodle",
    "factory_enable_dtc",
    "factory_enable_fiber",
)

def build_profiles(read_cameras: bool, uvc_boson: bool, force_boson: bool, lepton_enable: bool) -> list:
    """
    Assemble the default profile list: Default first, then camera-detected
    profiles (Boson/Tracking) and Lepton if requested, then the universal
    Long Range and Low Latency fallbacks. Profiles are deep-copied so the
    module-level profile constants are never mutated.
    """
    profiles = [copy.deepcopy(DEFAULT_PROFILE)]

    # Check for additional camera-based profiles
    if read_cameras:
        try:
            camera_config_json = load_json_with_comments(CAMERA_CONFIG_FILE)
            camera_names = {camera["name"].lower() for camera in camera_config_json["cameras"]}
        except Exception as e:
            print(f"{WARN}[WARNING] Could not parse camera config: {e}{RESET}")
            camera_names = set()

        has_boson = any("boson" in name for name in camera_names) or force_boson

        # Boson profiles
        if has_boson:
            if uvc_boson:
                profiles.append(copy.deepcopy(BOSON_UVC_DEFAULT_CONFIG))
            else:
                profiles.append(copy.deepcopy(BOSON_DEFAULT_CONFIG))
        # Tracking camera profiles
        if "tracking_front" in camera_names:
            profiles.append(copy.deepcopy(TRACKING_FRONT_DEFAULT_CONFIG))
        if "tracking_down" in camera_names:
            profiles.append(copy.deepcopy(DOWNWARD_TRACKING_DEFAULT_CONFIG))

        # Add lepton profile if requested
        if lepton_enable:
            profiles.append(copy.deepcopy(LEPTON_DEFAULT_CONFIG))

    # Add universal fallback profiles
    profiles.extend([copy.deepcopy(LONG_RANGE_PROFILE), copy.deepcopy(LOW_LATENCY_PROFILE)])
    return profiles

def write_default_config(args: dict, include_cameras: bool = False, force_d0008_v5: bool = False) -> None:
    """
    Write the default config file with ordered profiles based on available
    cameras, with SKU-specific profile policy derived from the given args.
    """
    print("VTX Configuration:")

    d0008_v5     = force_d0008_v5 or args.get("d0008_v5", False)
    d0008_v5_X2  = args.get("d0008_v5_X2", False)
    d0013        = args.get("d0013", False)
    d0020_strike = args.get("d0020_strike", False)
    lepton_enable = args.get("lepton_enable", False)

    # Start with default config structure (make a copy)
    config = default_config.copy()
    config["profiles"] = build_profiles(
        read_cameras=(d0008_v5 or d0008_v5_X2 or d0013 or d0020_strike or lepton_enable or include_cameras),
        uvc_boson=d0008_v5_X2,
        force_boson=d0008_v5_X2,
        lepton_enable=lepton_enable)

    # RC switch slots: drone-specific setups get camera profiles only 
    # D0020 strike disables RC profile switching.
    if d0020_strike:
        config["rc_switch_profiles"] = []
    elif d0008_v5 or d0008_v5_X2 or d0013:
        config["rc_switch_profiles"] = camera_rc_switch_slots(config["profiles"])
    else:
        config["rc_switch_profiles"] = [i for i, p in enumerate(config["profiles"])
                                        if p["source"] != LONG_RANGE_PROFILE["source"]][:5]

    # If d0008_v5, d0008_v5_X2, D0020, or D0020 strike then set power for all profiles to max power
    if d0008_v5 or d0008_v5_X2 or d0020_strike:
        for profile in config["profiles"]:
            profile["power"] = -1

    # Write config to file
    save_json_with_header(VTX_CONFIG_FILE, config, config_header)

    # Print for visual confirmation
    for profile in config["profiles"]:
        print(f"\tProfile: {profile['name']} - {profile['source']}")
    print(f"\tRC switch slots: {config['rc_switch_profiles']}")

# Fields shown by --list: (config key, display label), in print order
DUMP_FIELDS = (
    ("mcs",                   "MCS"),
    ("power",                 "Power"),
    ("card",                  "Card"),
    ("bandwidth",             "Bandwidth"),
    ("frequency",             "Frequency"),
    ("enable_fec",            "Enable_FEC"),
    ("fec_percent",           "FEC_percent"),
    ("fec_block_size",        "FEC_block_size"),
    ("enable_stbc",           "Enable_stbc"),
    ("perf_mode",             "Perf_mode"),
    ("enable_pit",            "Enable_pit"),
    ("i2c_port",              "I2C Port"),
    ("rc_toggle",             "Toggle"),
    ("source",                "Source"),
    ("secondary_source",      "Secondary_source"),
    ("disable_transmissions", "Disable_transmissions"),
)

# Load modified 88XXau .ko
def enable_vtx():
    """
    Swap default 88XXau kernel module with modified 88XXau kernel module for wfb
    """
    enable_service(VTX_SERVICE_NAME, announce=True)
    subprocess.run(['bash', '/usr/bin/enable_vtx.sh'])

# Load modified 88XXau .ko
def factory_enable_vtx(args, restart=False) -> int:
    """
    Swap default 88XXau kernel module with modified 88XXau kernel module for wfb
    """
    # Restore default config file
    write_default_config(args)
    CONFIG.update_param("udp", False)
    enable_service(VTX_SERVICE_NAME, announce=True)


    # Load VTX driver
    rc = subprocess.run(['bash', '/usr/bin/enable_vtx.sh'])
    if(rc.returncode):
        print(f"{RED}{BOLD}[ERROR] Enable vtx script failed.{RESET}")

    ret = rc.returncode

    # Set the efuse before swapping kernel modules
    print("Setting M0185 efuse")
    rc = subprocess.run(['bash', '/usr/bin/set_efuse.sh'], stdout=subprocess.DEVNULL)
    if(rc.returncode):
        print(f"{RED}{BOLD}[ERROR] Set efuse script failed. RC: {rc.returncode}{RESET}")

    ret |= rc.returncode

    # if restart: restart_service(VTX_SERVICE_NAME)
    return ret

def apply_radio_preset(preset: RadioPreset, args: dict):
    """
    Enable voxl-vtx service and update config file for the given radio preset
    (LTE, Doodle, DTC, fiber): restore the default config with camera
    profiles, then apply the preset's overrides in one write.
    """
    # Restore default config file
    write_default_config(args, include_cameras=True, force_d0008_v5=preset.force_d0008_v5)
    CONFIG.load()
    CONFIG.apply(preset.overrides)
    CONFIG.save()
    enable_service(VTX_SERVICE_NAME, announce=True)

# Load modified 88XXau .ko
def disable_vtx():
    """
    Swap modified 88XXau_vtx kernel module with default 88XXau kernel module
    """
    disable_and_stop_service(VTX_SERVICE_NAME, announce=True)
    subprocess.run(['bash', '/usr/bin/disable_vtx.sh'])

# Verify power is within valid range
def verify_power(power: int) -> int:
    """
    Verify that the power setting a user is trying to apply is valid and truncate it to within a acceptable/safe range is invalid value is given

    @power: The power index that the user wants to set
    @return: The validated or truncated power index
    """
    if power > 53: print(f"{WARN}{BOLD}[WARNING] Setting TPI above 53 may damage your hardware.{RESET}")
    if power > 63:
        print(f"{WARN}{BOLD}[WARNING] TPI above max requested: {power}. Max TPI is 63, setting to max.{RESET}")
        power = 63
    return power

# Verify power is within valid range (0-1000mw)
def verify_power_mw(power: int) -> int:
    """
    Verify that the power setting a user is trying to apply is valid and truncate it to within a acceptable/safe range is invalid value is given

    @power: The power index that the user wants to set
    @return: The validated or truncated power index
    """
    if power > 1000:
        print(f"{WARN}{BOLD}[WARNING] Max power output is 1000mW.{RESET}")
        power = 1000
    elif power < 0:
        print(f"{WARN}{BOLD}[WARNING] Minimum power output is 0mW.{RESET}")
        power = 0
    return power

# Verify mcs is within valid range
def verify_mcs(mcs: int) -> int:
    """
    Verify that the mcs index a user is trying to apply is valid and truncate it to within a acceptable/safe range is invalid value is given

    @mcs: The mcs index that the user wants to set
    @return: The validated or truncated mcs index
    """
    if mcs > 7:
        print(f"{WARN}{BOLD}[WARNING] MCS index provided ({mcs}) is invalid, must be 0-7.{RESET}")
        return -1

    if mcs < 0:
        print(f"{WARN}{BOLD}[WARNING] MCS index provided ({mcs}) is invalid, must be 0-7.{RESET}")
        return -1

    return mcs

# Set override value for frequency
def set_override_frequency(frequency:int) -> None:
    """
    Set the override frequency in the rtl8812au kernel driver. This value will be set
    instead of whatever frequency is passed into the command "iw dev wlan0 set freq 5180 HT20"

    @frequency: Frequency to set the VTX to. Will default to 5865MHz if requested freq is not in dict
    @Return: None
    """
    channel_file:str = "/sys/module/88XXau_vtx/parameters/channel_override"
    if not os.path.exists(channel_file):
        print(f"{WARN}[WARNING] Channel override file is missing, please run --enable of --factory_enable to load the correct kernel module.{RESET}")
        return
    with open(channel_file, 'w') as override_file:
        if (override_file.writable()): override_file.write(f"{freq_to_ch.get(frequency, 173)}")
        else: print(f"Can't write to file: {override_file}")

# Add a new VTX Profile via the wizard
def add_vtx_profile(cfg: dict) -> None:
    """
    Add a new profile: clone the built-in Default template or an existing
    profile (so mtu, bitrate, and ai_detection settings come from a sane
    base), prompt for the key fields, append, and save. Empty input keeps
    the cloned value. The new profile is not added to rc_switch_profiles
    automatically.
    """
    profiles = cfg.get('profiles', [])

    # Pick a base to clone
    print(f"{LIT_GRN}Clone new profile from:{RESET}")
    print("[1] Back")
    print("[2] Default template")
    for idx, p in enumerate(profiles):
        print(f"[{idx + 3}] {p.get('name','<unnamed>')}")

    try:
        choice = int(input())
        print(f"{CLEAR}", end="")
    except KeyboardInterrupt:
        print()
        exit(0)
    except ValueError:
        print(f"{WARN}Invalid answer provided, please select a valid option.{RESET}")
        return

    if choice == 1:
        return
    if choice == 2:
        base = copy.deepcopy(DEFAULT_PROFILE)
    elif 0 <= choice - 3 < len(profiles):
        base = copy.deepcopy(profiles[choice - 3])
    else:
        print(f"{WARN}Invalid answer provided, please select a valid option.{RESET}")
        return

    # Prompt the key fields; remaining fields keep the cloned values and can
    # be tuned afterwards from the profile edit menu
    try:
        name = input(f"Enter profile name (enter to keep '{base['name']}'): ").strip()
        if name: base["name"] = name
        source = input(f"Enter source pipe (enter to keep '{base['source']}'): ").strip()
        if source: base["source"] = source
        raw = input(f"Enter mcs 0-7 (enter to keep {base['mcs']}): ").strip()
        if raw:
            val = int(raw)
            if verify_mcs(val) == -1:
                return
            base["mcs"] = val
        raw = input(f"Enter power in mW (enter to keep {base['power']}): ").strip()
        if raw:
            base["power"] = verify_power_mw(int(raw))
    except KeyboardInterrupt:
        print()
        return
    except ValueError:
        print(f"{WARN}Invalid answer provided, please select a valid option.{RESET}")
        return

    cfg.setdefault('profiles', []).append(base)
    save_json_with_header(VTX_CONFIG_FILE, cfg, config_header)
    print(f"\tAdded profile: {base['name']} - {base['source']}")

# Delete a VTX Profile via the wizard
def delete_vtx_profile(cfg: dict, prof_idx: int) -> bool:
    """
    Delete a profile after confirmation, remapping active_profile_idx and
    rc_switch_profiles so they keep referring to the same profiles.

    @cfg: loaded config dict (saved on deletion)
    @prof_idx: index of the profile to delete
    @return: True if the profile was deleted and the config saved
    """
    profiles = cfg.get('profiles', [])
    if len(profiles) <= 1:
        print(f"{WARN}Cannot delete the last remaining profile.{RESET}")
        return False

    prof = profiles[prof_idx]
    try:
        ans = input(f"Delete profile '{prof.get('name','<unnamed>')}' ({prof.get('source','?')})? (y/n): ").strip().lower()
    except KeyboardInterrupt:
        print()
        return False
    if ans not in ("y", "yes"):
        return False

    profiles.pop(prof_idx)
    print(f"\tDeleted profile: {prof.get('name','<unnamed>')} - {prof.get('source','?')}")

    # Remap index-based references so they keep pointing at the same profiles
    active = cfg.get("active_profile_idx", 0)
    if active == prof_idx:
        cfg["active_profile_idx"] = 0
        print(f"\tactive_profile_idx: {active} -> 0 (deleted profile was active)")
    elif active > prof_idx:
        cfg["active_profile_idx"] = active - 1
        print(f"\tactive_profile_idx: {active} -> {active - 1}")

    slots = cfg.get("rc_switch_profiles")
    if slots:
        new_slots = [s - 1 if s > prof_idx else s for s in slots if s != prof_idx]
        if new_slots != slots:
            cfg["rc_switch_profiles"] = new_slots
            print(f"\trc_switch_profiles: {slots} -> {new_slots}")

    save_json_with_header(VTX_CONFIG_FILE, cfg, config_header)
    return True

# Update a VTX Profile
def update_vtx_profile() -> None:
    """
    Wizard for selecting, editing, adding, and deleting VTX profiles.
    """
    while True:
        # Load current config
        try:
            cfg = load_json_with_comments(VTX_CONFIG_FILE)
        except FileNotFoundError:
            print("[ERROR] Cannot find VTX config file.")
            return

        profiles = cfg.get('profiles', [])
        num = len(profiles)

        # Build menu
        print(f"{LIT_GRN}Select a profile to edit:{RESET}")
        print("[1] Quit")
        print("[2] Back")
        print("[3] Restart Service")
        print("[4] Add Profile")
        for idx, p in enumerate(profiles):
            print(f"[{idx + 5}] {p.get('name','<unnamed>')}")

        choice = input()
        try:
            field = int(choice)
            print(f"{CLEAR}", end="")
        except KeyboardInterrupt:
            print()
            exit(0)
        except ValueError:
            print(f"{WARN} Invalid selection.{RESET}")
            continue

        # Handle special options
        if field == 1:
            exit(0)
        if field == 2:
            return              # Back to top‐level wizard
        if field == 3:
            restart_service(VTX_SERVICE_NAME)
            continue
        if field == 4:
            add_vtx_profile(cfg)
            continue

        # Handle profiles now
        prof_idx = field - 5
        if prof_idx < 0 or prof_idx >= num:
            print(f"{WARN}Invalid answer provided, please select a valid option.{RESET}")
            continue

        # Now edit a single profile
        while True:
            prof = cfg['profiles'][prof_idx]
            print(f"{LIT_GRN}Editing profile [{prof_idx}] {prof['name']}{RESET}")
            print("[1] Quit")
            print("[2] Back")
            print("[3] Restart Service")
            print("[4] Delete this profile")
            fields = list(prof.keys())
            for i, fld in enumerate(fields, start=5):
                print(f"[{i}] {fld} (current: {prof[fld]})")

            sub = input()
            try:
                profile_selected = int(sub)
                print(f"{CLEAR}", end="")
            except KeyboardInterrupt:
                print()
                exit(0)
            except ValueError:
                print(f"{WARN}Invalid answer provided, please select a valid option.{RESET}")
                continue

            if profile_selected == 1:
                exit(0)
            if profile_selected == 2:
                break    # back to profiles list
            if profile_selected == 3:
                restart_service(VTX_SERVICE_NAME)
                continue
            if profile_selected == 4:
                if delete_vtx_profile(cfg, prof_idx):
                    break    # profile gone; back to profiles list
                continue

            fld_idx = profile_selected - 5
            if fld_idx < 0 or fld_idx >= len(fields):
                print(f"{WARN} Selection out of range.{RESET}")
                continue

            field = fields[fld_idx]
            old = prof[field]

            # Prompt for the right type; mcs and power keep their validation,
            # everything else prompts based on the current value's type
            try:
                if field == "mcs":
                    val = int(input(f"Enter new mcs (0-7, current {old}): "))
                    if verify_mcs(val) == -1:
                        continue
                    new = val
                elif field == "power":
                    val = int(input(f"Enter new power in mW (current: {old}): "))
                    new = verify_power_mw(val)
                else:
                    new = prompt_field_value(field, old)
                    if new is None:   # type not editable, message already printed
                        continue
            except (KeyboardInterrupt, ValueError):
                    print(f"{WARN} Invalid input: {sub}:{field}:{old}.{RESET}")
                    continue

            # Save changes
            print(f"\t{field}: {old} -> {new}")
            cfg['profiles'][prof_idx][field] = new
            save_json_with_header(VTX_CONFIG_FILE, cfg, config_header)

# ---------------------------------------------------------------------------
# Per-flag handlers for update_vtx_config. Each handler gets the loaded
# config dict plus the active profile index / profile count, performs its
# validation and voxl-send-command side effects, and returns either None
# (done, skip the generic config write) or a (field, value) pair to fall
# through to the generic write (top-level key or active-profile key).
# ---------------------------------------------------------------------------

def _handle_mcs(cfg, value, active_profile_idx, num_profiles):
    if verify_mcs(value) == -1:
        return None
    _ = subprocess.run(['voxl-send-command', 'vtx_latency', 'set_mcs', shlex.quote(str(value))], stdout=subprocess.DEVNULL)
    return ("mcs", value)

def _handle_power(cfg, value, active_profile_idx, num_profiles):
    value = verify_power(value)
    _ = subprocess.run(['voxl-send-command', 'vtx_latency', 'set_power', shlex.quote(str(value))], stdout=subprocess.DEVNULL)
    return None    # Don't write value to config file

def _handle_power_mw(cfg, value, active_profile_idx, num_profiles):
    value = verify_power_mw(value)
    _ = subprocess.run(['voxl-send-command', 'vtx_latency', 'set_mw_power', shlex.quote(str(value))], stdout=subprocess.DEVNULL)
    return ("power", value)

def _handle_frequency(cfg, value, active_profile_idx, num_profiles):
    if freq_to_ch.get(value, -1) == -1:
        print(f"Invalid frequency provided: {value}.")
        return None
    set_override_frequency(value)
    _ = subprocess.run(['voxl-send-command', 'vtx_latency', 'set_frequency', shlex.quote(str(value))], stdout=subprocess.DEVNULL)
    return ("frequency", value)

def _handle_active_profile_idx(cfg, value, active_profile_idx, num_profiles):
    if value+1 > num_profiles:
        print(f"Provided active profile idx ({value}, zero indexed) is not valid. Number of provided profiles: {num_profiles}")
        return None
    _ = subprocess.run(['voxl-send-command', 'vtx_latency', 'set_profile_idx', shlex.quote(str(value))], stdout=subprocess.DEVNULL)
    return ("active_profile_idx", value)

def _make_sku_handler(sku: SkuPreset):
    def handler(cfg, value, active_profile_idx, num_profiles):
        print(f"\ti2c_port: {cfg['i2c_port']} -> {sku.i2c_port}")
        cfg["i2c_port"] = sku.i2c_port
        if sku.rc_toggle is not None:
            print(f"\trc_toggle: {cfg['rc_toggle']} -> {sku.rc_toggle}")
            cfg["rc_toggle"] = sku.rc_toggle
        if sku.clear_rc_switch:
            cfg["rc_switch_profiles"] = []
        else:
            cfg["rc_switch_profiles"] = camera_rc_switch_slots(cfg.get("profiles", []))
        return None
    return handler

def _handle_enable_ip_fallback(cfg, value, active_profile_idx, num_profiles):
    cfg["enable_ip_fallback"] = True
    cfg["udp"] = True
    return ("enable_ip_fallback", True)

def _handle_enable_rc_deadman(cfg, value, active_profile_idx, num_profiles):
    cfg["rc_deadman_en"] = True
    return None

def _handle_add_udp_receiver(cfg, value, active_profile_idx, num_profiles):
    # When the service is running it persists the change itself, but we
    # must still mirror it into the config dict so the save at the end of
    # update_vtx_config doesn't clobber it with the stale copy loaded at
    # startup
    ip = value.split(':')[0]
    port = int(value.split(':')[1])
    if(service_running(VTX_SERVICE_NAME)): _ = subprocess.run(['voxl-send-command', 'vtx_latency', 'add_udp_receiver', shlex.quote(value)], stdout=subprocess.DEVNULL)
    cfg.setdefault("udp_receiver_configs", []) # Only sets [] if key (udp_receiver_configs) is missing
    if not any(r.get("ip") == ip and r.get("port") == port for r in cfg["udp_receiver_configs"]):
        cfg["udp_receiver_configs"].append({"ip": ip, "port": port})
    print(f"\tAdded UDP receiver: {value}")
    return None

def _handle_remove_udp_receiver(cfg, value, active_profile_idx, num_profiles):
    ip = value.split(':')[0]
    port = int(value.split(':')[1])
    if(service_running(VTX_SERVICE_NAME)): _ = subprocess.run(['voxl-send-command', 'vtx_latency', 'remove_udp_receiver', shlex.quote(value)], stdout=subprocess.DEVNULL)
    cfg["udp_receiver_configs"] = [r for r in cfg.get("udp_receiver_configs", []) if r.get("ip") != ip or r.get("port") != port]
    print(f"\tRemoved UDP receiver: {value}")
    return None

def _handle_clear_udp_receivers(cfg, value, active_profile_idx, num_profiles):
    if(service_running(VTX_SERVICE_NAME)): _ = subprocess.run(['voxl-send-command', 'vtx_latency', 'clear_udp_receivers', '1'], stdout=subprocess.DEVNULL)
    cfg["udp_receiver_configs"] = []
    print(f"\tCleared all UDP receivers")
    return None

ARG_HANDLERS = {
    "mcs":                _handle_mcs,
    "power":              _handle_power,
    "power_mw":           _handle_power_mw,
    "frequency":          _handle_frequency,
    "active_profile_idx": _handle_active_profile_idx,
    "enable_ip_fallback": _handle_enable_ip_fallback,
    "enable_rc_deadman":  _handle_enable_rc_deadman,
    "add_udp_receiver":   _handle_add_udp_receiver,
    "remove_udp_receiver": _handle_remove_udp_receiver,
    "clear_udp_receivers": _handle_clear_udp_receivers,
    **{flag: _make_sku_handler(sku) for flag, sku in SKU_PRESETS.items()},
}

# Handle updating wifibroadcast configuration file
def update_vtx_config(args: dict) -> int:
    """
    Update parameters in VTX configuration file given a set of args from command line
    @args: Dictionary of args passed in at the command line containing fields to be updated in the wifibroadcast configuration file
    """
    rc : int = 0
    try:
        if any(args.get(flag) for flag in SKU_PRESETS):
            rc = factory_enable_vtx(args)

        vtx_config_json = load_json_with_comments(VTX_CONFIG_FILE)

        # For MCS, Power, and Source we need to get the active profile idx first, validate user input
        active_profile_idx = vtx_config_json.get("active_profile_idx", 0)
        num_profiles = len(vtx_config_json.get('profiles', []))

        # Handle all the args passed in
        for field, value in args.items():
            if value is None or value is False: continue
            if field == "lepton_enable": continue

            handler = ARG_HANDLERS.get(field)
            if handler:
                result = handler(vtx_config_json, value, active_profile_idx, num_profiles)
                if result is None: continue
                field, value = result

            # MCS, Power, and Source are based on vtx profile so handle that here
            if field == "power" or field == "mcs" or field == "source":
                print(f"\t{field}: {vtx_config_json['profiles'][active_profile_idx][field]} -> {value}")
                vtx_config_json["profiles"][active_profile_idx][field] = value
            else:
                print(f"\t{field}: {vtx_config_json[field]} -> {value}")
                vtx_config_json[field] = value

        # Save changes to file
        save_json_with_header(VTX_CONFIG_FILE, vtx_config_json, config_header)
    except FileNotFoundError as e:
        print(f"[ERROR] Unable to locate VTX configuration file.")
        print(str(e))
        rc = -1
    except Exception as e:
        print(f"[ERROR] An unexpected error occurred while uploading VTX configuration. {str(e)}")
        if vtx_config_json is not None:
            save_json_with_header(VTX_CONFIG_FILE, vtx_config_json, config_header)
        rc = -1

    # Restart service with new settings if one of the presets
    # if args.get("d0008_v4") or args.get("d0008_v5") or args.get("d0013"): restart_service(VTX_SERVICE_NAME)
    return rc

# Item schemas for JSON-object list fields edited via the wizard, so adding
# to an empty list prompts per key with the right types
LIST_FIELD_TEMPLATES = {
    "udp_receiver_configs": UDP_RECEIVER_TEMPLATE,
}

# Handle updating wifibroadcast configuration file via wizard
def wizard_update_vtx_config() -> bool:
    """
    Update parameters in VTX configuration file via wizard
    """
    options = []
    cases = {}
    invalid = False

    while True:
        try:
            read_file(VTX_CONFIG_FILE)
            vtx_config_json = load_json_with_comments(VTX_CONFIG_FILE)
            options = list(vtx_config_json.keys())
            options.insert(0,"Quit")
            options.insert(1,"Back")
            options.insert(2,"Restart Service")

            print(f"{LIT_GRN}Select a field to update:{RESET}")
            for index, option in enumerate(options):
                print(f"[{index + 1}] {option}")
                cases[index + 1] = option

            try:
                field = cases.get(int(input()))
                print(f"{CLEAR}", end="")
            except KeyboardInterrupt:
                print()
                exit(0)
            except Exception:
                print(f"{WARN}Invalid answer provided, please select a valid option.{RESET}")
                continue

            if field == "Back": break
            if field == "Quit": exit(0)
            if field == "Restart Service":
                restart_service(VTX_SERVICE_NAME)
                continue
            if field == "profiles":
                update_vtx_profile()
                continue
            try:
                if isinstance(vtx_config_json[field], list):
                    value = edit_list_field(field, vtx_config_json[field], LIST_FIELD_TEMPLATES.get(field))
                else:
                    value = prompt_field_value(field, vtx_config_json[field])
                    if value is None:   # type not editable, message already printed
                        continue
            except KeyboardInterrupt:
                print()
                invalid = True
                continue
            except Exception:
                print(f"{WARN}Invalid answer provided, please select a valid option.{RESET}")
                invalid = True
                continue

            if field == "frequency":
                if freq_to_ch.get(value, -1) == -1:
                    print(f"Invalid frequency provided: {value}.")
                    continue
                set_override_frequency(value)
                _ = subprocess.run(['voxl-send-command', 'vtx_latency', 'set_channel', shlex.quote(str(freq_to_ch.get(value)))], stdout=subprocess.DEVNULL)

            print(f"{field}: {vtx_config_json[field]} -> {value}")
            vtx_config_json[field] = value
            save_json_with_header(VTX_CONFIG_FILE, vtx_config_json, config_header)
        except FileNotFoundError as e:
            print(f"{RED}{BOLD}[ERROR] Unable to locate configuration file, please run voxl-vtx at least once so that it generates a configuration file.{RESET}")
            print(str(e))
            return False
        except Exception as e:
            if vtx_config_json is not None:
                save_json_with_header(VTX_CONFIG_FILE, vtx_config_json, config_header)
            if not invalid:
                print(f"{RED}{BOLD}[ERROR] An unexpected error occurred while uploading configuration. {str(e)}{RESET}")
                return False
            invalid = False
    return True

# Wizard for handling interactive mode
def wizard() -> int:
    """
    Handle interactive mode, guide user through option menus and handle input.
    Wizard options:
    [1] Quit wizard
    [2] Restart VTX     -> Restart voxl-vtx service after making changes
    [3] VTX settings    -> Show current VTX settings
    """
    return run_wizard("Restart VTX", VTX_SERVICE_NAME, "VTX Settings", wizard_update_vtx_config)

# Truthy flags that trigger a config update pass (mcs and active_profile_idx
# are handled separately below since 0 is a valid value for both)
CONFIG_UPDATE_TRUTHY_FLAGS = (
    "frequency", "source", "power", "power_mw",
    "d0008_v4", "d0008_v5", "d0008_v5_X2", "d0013", "d0020_strike",
    "enable_ip_fallback", "enable_rc_deadman",
    "add_udp_receiver", "remove_udp_receiver", "clear_udp_receivers",
)

def should_update_vtx_config(args: dict) -> bool:
    return (any(args[flag] for flag in CONFIG_UPDATE_TRUTHY_FLAGS)
            or (args["mcs"] != None and args["mcs"] >= 0)
            or (args["active_profile_idx"] != None and args["active_profile_idx"] >= 0))

if __name__ == "__main__":
    rc: int = 0
    args = parse_args()

    # Print help message if no args provided
    if all(value is None or value is False for value in args.values()) or args["help"]:
        print_usage()
        exit(0)

    if args["disable"]:
        disable_vtx()
        exit(0)

    if not os.path.exists(VTX_CONFIG_FILE):
        print(f"{WARN}[WARNING] Unable to locate VTX configuration file! Generating default configuration file now.{RESET}")
        write_factory_default_config()

    if os.path.exists(VTX_CONFIG_FILE) and os.path.getsize(VTX_CONFIG_FILE)<5 and not args["factory_enable"]:
        print(f"{WARN}[WARNING] VTX configuration file may be empty (file size: {os.path.getsize(VTX_CONFIG_FILE)})! Generating default configuration file now.{RESET}")
        write_factory_default_config()

    # Dump current configuration
    if args["list"]: dump_config(VTX_CONFIG_FILE, DUMP_FIELDS)

    # Check for use wizard (no args provided)
    if args["wizard"]: exit(wizard())

    # Handle swapping between wifi and softap (they all use the same network interface and will fight each other for control of it if more than one is running)
    if args["toggle"]: toggle_vtx_softap()

    # Handle swapping kernel modules and radio factory presets
    if args["factory_enable"]: rc = factory_enable_vtx(args, True)
    for preset_name in RADIO_PRESET_DISPATCH:
        if args[preset_name]: rc = apply_radio_preset(RADIO_PRESETS[preset_name], args)
    if args["enable"]:  rc = enable_vtx()

    # Handle Wifibroadcast configuration
    if should_update_vtx_config(args): rc = update_vtx_config(args)
    exit(rc)
