Developer Reference

SPOT Tools JSON File Spec

Published format reference for developers integrating tooling, converters, or automated pipelines with SPOT Tools JSON files.

SPOT Tools JSON File Spec

This document defines the JSON format used by SPOT Tools to load/save radio and channel state.

Audience: developers integrating tooling, converters, or automated pipelines.

Format Overview

SPOT Tools stores:

  • Radio-wide settings (radio)
  • Channel list (channels)

Current exported schema:

  • schema_version: 9
  • radio_model: "uv-k5-telemetry"

Canonical Top-Level Shape

{
  "schema_version": 8,
  "radio_model": "uv-k5-telemetry",
  "radio": { "...": "see Radio section" },
  "channel_count": 200,
  "channels": [ { "...": "see Channel section" } ]
}

Notes:

  • On import, only channels is strictly required.
  • channel_count is informational on import (not authoritative).
  • schema_version and radio_model are currently not enforced by the parser.

Radio Object

radio has two sub-objects:

  • active_options: primary telemetry-relevant settings
  • legacy_values: retained compatibility/settings state

radio.active_options (canonical)

{
  "beep_control": true,
  "bpm_control": true,
  "battery_type": 0,
  "scan": {
    "active_list_mask": 1,
    "list_names": ["L1", "...", "L16"],
    "resume_mode": "stop",
    "dwell_time_seconds": 5
  },
  "telemetry": {
    "peak_time": 22
  },
  "receiver": {
    "squelch_level": 0,
    "rf_gain": 12,
    "tag_mode": "channel"
  },
  "display": {
    "power_on_display_mode": "full_screen",
    "backlight_time": 4
  },
  "boot": {
    "line1": "SPOT",
    "line2": "Telemetry RX"
  },
  "keys": {
    "flashlight_enabled": true,
    "arrow_orientation": "up_down"
  },
  "meter_calibration": {
    "s0_level": 130,
    "s9_level": 76
  },
  "developer": {
    "pulse_threshold_dbm": -95,
    "full_scale_dbm": -31,
    "battery_calibration": 2300
  }
}

Allowed values and constraints

  • beep_control: boolean
  • bpm_control: boolean
  • battery_type: integer
  • scan.active_list_mask: integer mask, effective visible range 0..65535 (& 0xffff on read/write today)
  • scan.list_names: array (up to first 16 used), each entry:
    • string
    • printable ASCII only (0x20..0x7E)
    • max 10 chars
    • fallback to L<n> if cleaned result is empty
  • scan.resume_mode: "stop" | "dwell"
  • scan.dwell_time_seconds: integer, sanitized to nearest of 1, 2, 3, 5, 10, 15, 20, 25, 30
  • telemetry.peak_time: integer, sanitized to nearest of 5, 10, 20, 22, 24, 29, 39 (encoded values for 0.5, 1, 2, 3, 5, 10, 20 seconds)
  • receiver.squelch_level: integer
  • receiver.rf_gain: integer, clamped to 0..24
  • receiver.tag_mode: "channel" | "tag"
  • display.power_on_display_mode: "full_screen" | "message" | "voltage" | "none"
  • display.backlight_time: integer
  • boot.line1, boot.line2: string, truncated to 15 chars
  • keys.flashlight_enabled: boolean
  • keys.arrow_orientation: "up_down" | "left_right"
  • meter_calibration.s0_level, meter_calibration.s9_level: integer
  • developer.pulse_threshold_dbm: integer, optional
  • developer.full_scale_dbm: integer, optional
  • developer.battery_calibration: integer, optional

radio.legacy_values (canonical)

{
  "tx_timeout_timer": 1,
  "mic_sensitivity": 4,
  "roger_mode": "off",
  "repeater_tail_tone_elimination": 0,
  "tx_vfo_index": 0,
  "auto_keypad_lock": false,
  "cross_band_rx_tx": "off",
  "dual_watch": "off",
  "vfo_open": true,
  "raw": {}
}

Allowed enum values:

  • roger_mode: "off" | "roger" | "mdc"
  • cross_band_rx_tx: "off" | "chan_a" | "chan_b"
  • dual_watch: "off" | "chan_a" | "chan_b"

legacy_values.raw is preserved as a JSON object and round-tripped.

Channel Object

Canonical per-channel shape:

{
  "index": 0,
  "active": true,
  "name": "664 R L",
  "rx_frequency": 16840830,
  "step_setting": 9,
  "attributes": {
    "band": 2,
    "scanlists": [1, 2, 3]
  },
  "legacy": {
    "bandwidth": "narrow"
  }
}

Required fields:

  • index (integer)
  • active (boolean)
  • name (string)
  • rx_frequency (number)
  • step_setting (number)
  • attributes (object) with:
    • band (integer)

Optional:

  • legacy.bandwidth ("wide" | "narrow")

Scan List Encoding (Channel Attributes)

Canonical export uses:

  • attributes.scanlists: array of integers 1..16

Import supports three forms (priority order):

  1. attributes.scanlists (preferred)
  2. attributes.scanlist_mask (0..65535 for the currently visible 16-list model)
  3. legacy booleans:
    • attributes.scanlist1
    • attributes.scanlist2

Step Setting Encoding

Canonical export uses:

  • step_setting (top-level per channel)

Import accepts:

  • step_setting (preferred)
  • legacy fallback: legacy.step_setting

Compatibility and Fallback Rules

Radio compatibility

  • beep_control may be provided in either:
    • radio.active_options.beep_control
    • radio.legacy_values.beep_control (fallback)
  • bpm_control same fallback behavior as beep_control.
  • battery_type may be provided in:
    • radio.active_options.battery_type
    • radio.legacy_values.battery_type (fallback)

Legacy scan default mapping

If radio.active_options.scan.active_list_mask is absent, parser accepts legacy:

  • radio.active_options.scan.default_list with values:
    • "list1".."list5" -> maps to single-bit active_list_mask
    • "all" -> falls back to default mode/mask

If legacy default_list is used and not "all", target_mode is inferred as "lists".

Legacy scan.target_mode handling

  • Import accepts radio.active_options.scan.target_mode values "lists" and "all" for backward compatibility.
  • Canonical export does not require this field; mode is derived from active_list_mask by current firmware-facing logic.

Normalization (Sanitization) Rules

After parsing/import, SPOT Tools sanitizes channels (sanitizeChannels):

  • name: trim, printable ASCII only, max 10 chars
  • scanlistMask: forced to the currently visible 16-bit range (& 0xffff)
  • stepSetting: if unsupported, replaced with default telemetry step (STEP_INDEX_0_5_KHZ)
  • bandwidth: forced to "narrow"
  • active channels are validated against telemetry constraints:
    • band must be 0..6
    • frequency range must be:
      • min 14_400_000
      • max exclusive 17_400_000
  • invalid active channels are coerced/inactivated per sanitizer logic

Important:

  • Save and write paths also run sanitization, so file values may be corrected on output.

Export Behavior (Canonicalization)

When SPOT Tools saves JSON:

  • emits schema_version: 9
  • emits canonical radio.active_options + radio.legacy_values
  • emits channels with:
    • index, active, name, rx_frequency, step_setting
    • attributes.band
    • attributes.scanlists (not scanlist_mask, scanlist1, scanlist2)
    • legacy.bandwidth
  • emits radio.active_options.receiver.tag_mode
  • emits radio.active_options.developer only when one or more optional developer-backed values are actually present

Strictness and Unknown Fields

  • Known fields are type-checked strictly.
  • Missing optional fields fall back to defaults.
  • Unknown fields are generally ignored by parser.
  • legacy_values.raw is the designated container for opaque vendor/compat data.

Minimal Valid Input Example

{
  "channels": [
    {
      "index": 0,
      "active": true,
      "name": "CH001",
      "rx_frequency": 16840000,
      "attributes": {
        "band": 2,
        "scanlists": [1]
      }
    }
  ]
}

This is accepted; omitted radio fields are filled from defaults.