diff --git a/README.md b/README.md index 325a551..3db5e10 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ including programming several different radios from the same codeplug. | **AnyTone AT-D578UV** | DMR + Analog mobile | 2 m / 1.25 m / 70 cm | Direct USB — channels, zones, scan lists, talkgroups, settings, call-sign DB | 4000 channels in 250 zones; 264 menu settings including the FM and DMR APRS setup; 500k-entry caller-ID database. AM channels in 108–136 MHz go to the air-band memory, which only the D578UV **PLUS** receives — that part is written to the published map but has not been checked on a PLUS. Same protocol as the D890UV over a different memory map | | **Yaesu FT5D** | C4FM (System Fusion) + Analog | VHF / UHF TX, wideband RX | microSD — patches the radio's own backup file | 900 channels in 24 banks; channels, banks, and menu settings | | **Yaesu FTM-300DR** | C4FM (System Fusion) + Analog | 2 m / 70 cm TX, 108–999 MHz RX | microSD — patches the radio's own backup file | 999 memories; memories and 147 menu settings, including the APRS and SmartBeaconing setup | +| **Yaesu FTM-510DR** | C4FM (System Fusion) + Analog | 2 m / 70 cm TX, 108–550 MHz RX | microSD — patches the radio's own backup file | 999 memories; memories and 284 settings — Setup Menu items, band skip, DTMF memories, and the APRS setup down to digi paths, message texts and status texts | | **Icom ID-52** | D-STAR + Analog | VHF / UHF TX, 108–174 / 225–479 MHz RX | microSD — patches the radio's own `.icf` file | 1000 memories in 100 groups; memories and menu settings restore in one operation | | **Icom ID-5100** | D-STAR + Analog | 2 m / 70 cm TX, 118–174 / 375–550 MHz RX | Data cable — read, write, settings | 1000 memories in 26 named banks, programmed by Icom's clone protocol over the cable; 292 menu settings including My Call Sign, TX messages and the D-PRS (APRS) position, object, item and weather setup, written in the same upload as the memories | | **Kenwood TH-D72** | APRS + Analog | 2 m / 70 cm TX, 118–174 / 320–524 MHz RX | Direct USB — read, write, settings | 1000 memories; 113 menu settings over the radio's own `MU` command | diff --git a/src-tauri/src/commands/program.rs b/src-tauri/src/commands/program.rs index f11886e..f83d46b 100644 --- a/src-tauri/src/commands/program.rs +++ b/src-tauri/src/commands/program.rs @@ -520,6 +520,7 @@ pub async fn find_memory_cards(format: String) -> Vec { "icom_id52_icf" => id52_cards(), "kenwood_thd75_sd" => thd75_cards(), "yaesu_ftm300_sd" => ftm300_cards(), + "yaesu_ftm510_sd" => ftm510_cards(), _ => Vec::new(), }) .await @@ -570,6 +571,28 @@ fn ftm300_cards() -> Vec { out } +/// The FTM-510 is the FTM-300's arrangement under its own folder and name: +/// Setup Menu 107 BACKUP → WRITE TO SD → ALL writes this path, READ FROM SD +/// → ALL loads it back. +fn ftm510_cards() -> Vec { + let mut out = Vec::new(); + for root in mounted_roots() { + let path = root.join("FTM510D").join("BACKUP").join("CLNFTM510D.dat"); + let Ok(image) = std::fs::read(&path) else { + continue; + }; + if crate::radios::yaesu_ftm510::sd_image::validate_backup(&image).is_err() { + continue; + } + out.push(MemoryCard { + has_original: path.with_extension("dat.orig").exists(), + volume: volume_name(&root), + path: path.to_string_lossy().into_owned(), + }); + } + out +} + /// The ID-52 keeps its settings files in one fixed folder but names them /// itself, so what gets offered is the **folder**, not a file: the write creates /// a new `SetYYYYMMDD_NN.icf` in it rather than overwriting anything the @@ -704,6 +727,31 @@ pub async fn read_ftm300_settings_from_backup(path: String) -> Result Result { + tauri::async_runtime::spawn_blocking(move || { + let image = std::fs::read(&path).map_err(|e| { + format!( + "Could not read {path}: {e}. Pick FTM510D/BACKUP/CLNFTM510D.dat from the \ + radio's microSD card." + ) + })?; + crate::radios::yaesu_ftm510::sd_image::validate_backup(&image)?; + let settings = crate::radios::yaesu_ftm510::settings::decode_settings(&image); + let count = settings.as_object().map(|o| o.len()).unwrap_or(0); + Ok(RadioSettingsRead { + settings, + count, + backup_path: path, + }) + }) + .await + .estr()? +} + /// The ID-52's version of [`read_ft5d_settings_from_backup`], reading the `.icf` /// the radio writes with `SET > SD Card > Save Setting`. /// diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 2f2debf..fa0d571 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -61,7 +61,7 @@ mod tests { // original set): currently the Baofeng UV-5R, TIDRADIO TD-H3, AnyTone // AT-D890UV and AT-D578UV, Yaesu FT5D, Icom ID-52, Kenwood TH-D75, Kenwood TH-D72 // the Binteradio BT-9000, Kenwood TM-D710, Icom ID-5100, TYT MD-380 and Yaesu - // FTM-300DR. (0015 + // FTM-300DR and FTM-510DR. (0015 // removed the Vero VR-N76 placeholder.) None of the last five has a // migration of its own — seeding INSERTs new (manufacturer, model) // rows, so a new model reaches existing databases on the next startup @@ -71,9 +71,9 @@ mod tests { .await .unwrap(); assert_eq!( - count.0, 13, + count.0, 14, "expected the UV-5R, TD-H3, AT-D890UV, AT-D578UV, FT5D, ID-52, TH-D75, TH-D72, BT-9000, \ - TM-D710, ID-5100, MD-380 and FTM-300DR seeded models" + TM-D710, ID-5100, MD-380, FTM-300DR and FTM-510DR seeded models" ); let models: Vec<(String,)> = @@ -85,7 +85,7 @@ mod tests { assert_eq!( names, vec![ - "AT-D578UV", "AT-D890UV", "BT-9000", "FT5D", "FTM-300DR", "ID-5100", "ID-52", + "AT-D578UV", "AT-D890UV", "BT-9000", "FT5D", "FTM-300DR", "FTM-510DR", "ID-5100", "ID-52", "MD-380", "TD-H3", "TH-D72", "TH-D75", "TM-D710", "UV-5R" ] ); @@ -96,7 +96,7 @@ mod tests { .fetch_one(&pool) .await .unwrap(); - assert_eq!(count2.0, 13, "seeding should be idempotent"); + assert_eq!(count2.0, 14, "seeding should be idempotent"); // A new database starts with NO talkgroups. The BrandMeister list used // to be compiled in and seeded here; it is downloaded on request now, diff --git a/src-tauri/src/ftm510_settings_schema.json b/src-tauri/src/ftm510_settings_schema.json new file mode 100644 index 0000000..777979c --- /dev/null +++ b/src-tauri/src/ftm510_settings_schema.json @@ -0,0 +1,5083 @@ +[ + { + "key": "section-config", + "label": "Config", + "type": "section", + "group": "General" + }, + { + "key": "auto-power-off", + "label": "APO", + "type": "select", + "options": [ + "OFF", + "0.5 hour", + "1.0 hour", + "1.5 hour", + "2.0 hour", + "3.0 hour", + "4.0 hour", + "5.0 hour", + "6.0 hour", + "7.0 hour", + "8.0 hour", + "9.0 hour", + "10.0 hour", + "11.0 hour", + "12.0 hour" + ] + }, + { + "key": "unit", + "label": "Unit", + "type": "select", + "options": [ + "METRIC", + "INCH" + ] + }, + { + "key": "date-format", + "label": "Date Format", + "type": "select", + "options": [ + "mmm/dd/yyyy", + "yyyy/mmm/dd", + "dd/mmm/yyyy", + "yyyy/dd/mmm" + ] + }, + { + "key": "time-format", + "label": "Time Format", + "type": "select", + "options": [ + "24 hour", + "12 hour" + ] + }, + { + "key": "time-zone", + "label": "Time Zone", + "type": "select", + "options": [ + "UTC-14:00", + "UTC-13:30", + "UTC-13:00", + "UTC-12:30", + "UTC-12:00", + "UTC-11:30", + "UTC-11:00", + "UTC-10:30", + "UTC-10:00", + "UTC-09:30", + "UTC-09:00", + "UTC-08:30", + "UTC-08:00", + "UTC-07:30", + "UTC-07:00", + "UTC-06:30", + "UTC-06:00", + "UTC-05:30", + "UTC-05:00", + "UTC-04:30", + "UTC-04:00", + "UTC-03:30", + "UTC-03:00", + "UTC-02:30", + "UTC-02:00", + "UTC-01:30", + "UTC-01:00", + "UTC-00:30", + "UTC±00:00", + "UTC+00:30", + "UTC+01:00", + "UTC+01:30", + "UTC+02:00", + "UTC+02:30", + "UTC+03:00", + "UTC+03:30", + "UTC+04:00", + "UTC+04:30", + "UTC+05:00", + "UTC+05:30", + "UTC+06:00", + "UTC+06:30", + "UTC+07:00", + "UTC+07:30", + "UTC+08:00", + "UTC+08:30", + "UTC+09:00", + "UTC+09:30", + "UTC+10:00", + "UTC+10:30", + "UTC+11:00", + "UTC+11:30", + "UTC+12:00", + "UTC+12:30", + "UTC+13:00", + "UTC+13:30", + "UTC+14:00" + ] + }, + { + "key": "callsign", + "label": "Callsign", + "type": "text", + "max_length": 10 + }, + { + "key": "section-beep", + "label": "Beep", + "type": "section", + "group": "Sounds" + }, + { + "key": "beep", + "label": "Beep", + "type": "boolean" + }, + { + "key": "beep-level", + "label": "Beep Level", + "type": "select", + "options": [ + "LOW", + "HIGH" + ] + }, + { + "key": "section-gps", + "label": "GPS", + "type": "section", + "group": "GPS" + }, + { + "key": "gps-datum", + "label": "GPS Datum", + "type": "select", + "options": [ + "WGS-84", + "TOKYO MEAN" + ] + }, + { + "key": "gps-device", + "label": "GPS Device", + "type": "select", + "options": [ + "INTERNAL", + "EXTERNAL" + ] + }, + { + "key": "gps-log", + "label": "GPS Log", + "type": "select", + "options": [ + "OFF", + "1sec", + "2sec", + "5sec", + "10sec", + "30sec", + "60sec" + ] + }, + { + "key": "section-display", + "label": "Display", + "type": "section", + "group": "Display" + }, + { + "key": "display-select", + "label": "Display Select", + "type": "select", + "options": [ + "BACKTRACK", + "ALTITUDE", + "TIMER/CLOCK", + "GPS INFORMATION" + ] + }, + { + "key": "location-info", + "label": "Location Info", + "type": "select", + "options": [ + "COMPASS", + "NUMERIC" + ] + }, + { + "key": "compass", + "label": "Compass", + "type": "select", + "options": [ + "NORTH UP", + "HEADING UP" + ] + }, + { + "key": "a-band-scope", + "label": "A Band Scope", + "type": "select", + "options": [ + "WIDE", + "NARROW" + ] + }, + { + "key": "b-band-scope", + "label": "B Band Scope", + "type": "select", + "options": [ + "WIDE", + "NARROW" + ] + }, + { + "key": "lcd-brightness", + "label": "LCD Brightness", + "type": "select", + "options": [ + "MIN", + "MID", + "MAX" + ] + }, + { + "key": "display-color", + "label": "Frequency Color", + "type": "select", + "options": [ + "WHITE", + "BLUE", + "RED" + ] + }, + { + "key": "section-tx", + "label": "TX", + "type": "section", + "group": "Transmit & Receive" + }, + { + "key": "time-out-timer", + "label": "TOT", + "type": "select", + "options": [ + "OFF", + "1 min", + "2 min", + "3 min", + "5 min", + "10 min", + "15 min", + "20 min", + "30 min" + ] + }, + { + "key": "mic-gain", + "label": "MIC Gain", + "type": "select", + "options": [ + "MIN", + "LOW", + "NORMAL", + "HIGH", + "MAX" + ] + }, + { + "key": "vox", + "label": "VOX", + "type": "select", + "options": [ + "OFF", + "LOW", + "HIGH" + ] + }, + { + "key": "vox-delay", + "label": "VOX Delay", + "type": "select", + "options": [ + "0.5s", + "1s", + "1.5s", + "2s", + "2.5s", + "3s" + ] + }, + { + "key": "vox-mic", + "label": "VOX MIC", + "type": "select", + "options": [ + "FRONT", + "REAR" + ] + }, + { + "key": "section-rx", + "label": "RX", + "type": "section", + "group": "Transmit & Receive" + }, + { + "key": "sub-band", + "label": "Sub Band", + "type": "boolean" + }, + { + "key": "sub-band-mute", + "label": "Sub Band Mute", + "type": "select", + "options": [ + "OFF", + "ON" + ] + }, + { + "key": "s-dx", + "label": "S-DX", + "type": "select", + "options": [ + "OFF", + "S-DX", + "ASP", + "AUTO" + ] + }, + { + "key": "section-repeater", + "label": "Repeater", + "type": "section", + "group": "Transmit & Receive" + }, + { + "key": "a-rpt-ars", + "label": "A RPT ARS", + "type": "select", + "options": [ + "OFF", + "AUTO" + ] + }, + { + "key": "b-rpt-ars", + "label": "B RPT ARS", + "type": "select", + "options": [ + "OFF", + "AUTO" + ] + }, + { + "key": "section-audio-equalizer", + "label": "Audio Equalizer", + "type": "section", + "group": "Sounds" + }, + { + "key": "front-tone", + "label": "Front Tone", + "type": "select", + "options": [ + "OFF", + "ON" + ] + }, + { + "key": "front-tone-eq", + "label": "Front Tone EQ", + "type": "select", + "options": [ + "FLAT", + "HI PITCH", + "LO PITCH", + "BPF" + ] + }, + { + "key": "rear-tone", + "label": "Rear Tone", + "type": "select", + "options": [ + "FLAT", + "HI PITCH", + "LO PITCH", + "BPF", + "1KHz LPF", + "700Hz LPF" + ] + }, + { + "key": "rear-out", + "label": "Rear Out", + "type": "select", + "options": [ + "0%", + "10%", + "20%", + "30%", + "40%", + "50%", + "60%", + "70%", + "80%", + "90%", + "100%" + ] + }, + { + "key": "aess-phase", + "label": "AESS Phase", + "type": "select", + "options": [ + "OFF", + "1.25ms", + "2.50ms", + "3.75ms", + "5.00ms", + "6.25ms", + "7.50ms", + "8.75ms", + "10.00ms", + "11.25ms", + "12.50ms", + "13.75ms", + "15.00ms", + "16.25ms", + "17.50ms", + "18.75ms", + "20.00ms" + ] + }, + { + "key": "section-audio", + "label": "Audio", + "type": "section", + "group": "Sounds" + }, + { + "key": "recording-band", + "label": "Recording Band", + "type": "select", + "options": [ + "MAIN", + "SUB", + "MAIN+SUB" + ] + }, + { + "key": "recording-mic", + "label": "Recording MIC", + "type": "boolean" + }, + { + "key": "front-speaker-mute", + "label": "Front Speaker Mute", + "type": "select", + "options": [ + "CONTINUE", + "AUTO MUTE" + ] + }, + { + "key": "section-fvs-2-voice-guide", + "label": "FVS-2 Voice Guide", + "type": "section", + "group": "Sounds" + }, + { + "key": "voice-memory-play-rec", + "label": "Play/Rec", + "type": "select", + "options": [ + "FREE 5 min", + "LAST 30 sec" + ] + }, + { + "key": "voice-announce", + "label": "Announce", + "type": "select", + "options": [ + "OFF", + "MANUAL", + "AUTO" + ] + }, + { + "key": "voice-language", + "label": "Language", + "type": "select", + "options": [ + "ENGLISH", + "JAPANESE" + ] + }, + { + "key": "voice-volume", + "label": "Volume", + "type": "select", + "options": [ + "LOW", + "MID", + "HIGH" + ] + }, + { + "key": "voice-rx-mute", + "label": "RX Mute", + "type": "boolean" + }, + { + "key": "section-memory", + "label": "Memory", + "type": "section", + "group": "Memory & VFO" + }, + { + "key": "memory-list-mode", + "label": "Memory List Mode", + "type": "boolean" + }, + { + "key": "pmg-timer", + "label": "PMG Timer", + "type": "select", + "options": [ + "0.5s", + "1s", + "2s" + ] + }, + { + "key": "pmg-hold", + "label": "PMG Hold", + "type": "select", + "options": [ + "2s", + "5s", + "10s", + "20s", + "30s" + ] + }, + { + "key": "section-band-skip", + "label": "Band Skip", + "type": "section", + "group": "Memory & VFO" + }, + { + "key": "band-skip-a-air", + "label": "A Band AIR", + "type": "select", + "options": [ + "OFF", + "ON" + ] + }, + { + "key": "band-skip-a-vhf", + "label": "A Band VHF", + "type": "select", + "options": [ + "OFF", + "ON" + ] + }, + { + "key": "band-skip-a-uhf", + "label": "A Band UHF", + "type": "select", + "options": [ + "OFF", + "ON" + ] + }, + { + "key": "band-skip-a-other", + "label": "A Band OTHER", + "type": "select", + "options": [ + "OFF", + "ON" + ] + }, + { + "key": "band-skip-b-air", + "label": "B Band AIR", + "type": "select", + "options": [ + "OFF", + "ON" + ] + }, + { + "key": "band-skip-b-vhf", + "label": "B Band VHF", + "type": "select", + "options": [ + "OFF", + "ON" + ] + }, + { + "key": "band-skip-b-uhf", + "label": "B Band UHF", + "type": "select", + "options": [ + "OFF", + "ON" + ] + }, + { + "key": "band-skip-b-other", + "label": "B Band OTHER", + "type": "select", + "options": [ + "OFF", + "ON" + ] + }, + { + "key": "section-scan", + "label": "Scan", + "type": "section", + "group": "Scan" + }, + { + "key": "a-scan-resume", + "label": "A Scan Resume", + "type": "select", + "options": [ + "BUSY", + "HOLD", + "1 sec", + "3 sec", + "5 sec" + ] + }, + { + "key": "b-scan-resume", + "label": "B Scan Resume", + "type": "select", + "options": [ + "BUSY", + "HOLD", + "1 sec", + "3 sec", + "5 sec" + ] + }, + { + "key": "a-dual-receive-interval", + "label": "A Dual Receive Interval", + "type": "select", + "options": [ + "0.5 sec", + "1.0 sec", + "2.0 sec", + "3.0 sec", + "5.0 sec", + "7.0 sec", + "10 sec" + ] + }, + { + "key": "b-dual-receive-interval", + "label": "B Dual Receive Interval", + "type": "select", + "options": [ + "0.5 sec", + "1.0 sec", + "2.0 sec", + "3.0 sec", + "5.0 sec", + "7.0 sec", + "10 sec" + ] + }, + { + "key": "a-priority-revert", + "label": "A Priority Revert", + "type": "boolean" + }, + { + "key": "b-priority-revert", + "label": "B Priority Revert", + "type": "boolean" + }, + { + "key": "dual-receive-mode", + "label": "Dual Receive Mode", + "type": "select", + "options": [ + "OFF", + "PRIORITY SCAN" + ] + }, + { + "key": "section-mic-program-key", + "label": "MIC Program Key", + "type": "section", + "group": "Keys" + }, + { + "key": "mic-key-p1", + "label": "P1", + "type": "select", + "options": [ + "OFF", + "2nd PTT", + "GM", + "REC/STOP", + "SCAN", + "HOME CH", + "RPT SHIFT", + "REVERSE", + "TX POWER", + "SQL OFF", + "T-CALL", + "VOICE", + "D_X", + "WX", + "STN LIST", + "MSG LIST", + "REPLY", + "MSG EDIT", + "DW" + ] + }, + { + "key": "mic-key-p2", + "label": "P2", + "type": "select", + "options": [ + "OFF", + "2nd PTT", + "GM", + "REC/STOP", + "SCAN", + "HOME CH", + "RPT SHIFT", + "REVERSE", + "TX POWER", + "SQL OFF", + "T-CALL", + "VOICE", + "D_X", + "WX", + "STN LIST", + "MSG LIST", + "REPLY", + "MSG EDIT", + "DW" + ] + }, + { + "key": "mic-key-p3", + "label": "P3", + "type": "select", + "options": [ + "OFF", + "2nd PTT", + "GM", + "REC/STOP", + "SCAN", + "HOME CH", + "RPT SHIFT", + "REVERSE", + "TX POWER", + "SQL OFF", + "T-CALL", + "VOICE", + "D_X", + "WX", + "STN LIST", + "MSG LIST", + "REPLY", + "MSG EDIT", + "DW" + ] + }, + { + "key": "mic-key-p4", + "label": "P4", + "type": "select", + "options": [ + "OFF", + "2nd PTT", + "GM", + "REC/STOP", + "SCAN", + "HOME CH", + "RPT SHIFT", + "REVERSE", + "TX POWER", + "SQL OFF", + "T-CALL", + "VOICE", + "D_X", + "WX", + "STN LIST", + "MSG LIST", + "REPLY", + "MSG EDIT", + "DW" + ] + }, + { + "key": "section-function-registration", + "label": "Function Registration", + "type": "section", + "group": "Keys" + }, + { + "key": "function-key-1", + "label": "Function Key 1", + "type": "select", + "options": [ + "001: FREQENCY INPUT", + "002: LCD BRIGHTNESS", + "003: FREQUENCY COLOR", + "004: BAND SCOPE", + "005: LOCATION INFO", + "006: COMPASS", + "007: DISPLAY MODE", + "008: TX POWER", + "009: AMS TX MODE", + "010: MIC GAIN", + "011: VOX", + "012: AUTO DIALER", + "013: TOT", + "014: DIGITAL VW", + "015: FM BANDWIDTH", + "016: RX MODE", + "017: SUB BUND", + "018: AUDIO EQUALIZER", + "019: HOME CH", + "020: MEMORY LIST", + "021: MEMORY LIST MODE", + "022: PMG CLEAR", + "023: BEEP", + "024: BAND SKIP", + "025: RPT ARS", + "026: RPT SHIFT", + "027: RPT SHIFT FREQ", + "028: RPT REVERSE", + "029: MIC PROGRAM KEY", + "030: DATE&TIME ADJUST", + "031: DATE&TIME FORMAT", + "032: TIME ZONE", + "033: STEP", + "034: CLOCK TYPE", + "035: UNIT", + "036: APO", + "037: GPS DATUM", + "038: GPS DEVICE", + "039: GPS LOG", + "040: RECODING", + "041: REC/STOP", + "042: FRONT SP MUTE", + "043: DTMF", + "044: DTMF MEMORY", + "045: SQL TYPE", + "046: TONE SQL FREQ", + "047: SQL EXPANSION", + "048: PAGER CODE", + "049: PR FREQUENCY", + "050: BELL RINGER", + "051: WX ALERT", + "052: SCAN", + "053: DUAL RCV MODE", + "054: DUAL RX INTERVAL", + "055: PRIORITY REVERT", + "056: SCAN RESUME", + "057: DIGITAL POPUP", + "058: LOCATION SERVICE", + "059: STANDBY BEEP", + "060: DP-ID LIST", + "061: RANGE RINGER", + "062: RADIO ID", + "063: LOG LIST", + "064: RPT/WIRES FREQ", + "065: SEARCH SETUP", + "066: EDIT CATEGORYTAG", + "067: DELETE ROOM/NODE", + "068: WIRES DG-ID", + "069: COM PORT", + "070: DATA BAND", + "071: DATA SPEED", + "072: DATA SQL", + "073: APRS DESTINATION", + "074: APRS FILTER", + "075: APRS MSG TEXT", + "076: APRS MODEM", + "077: APRS MUTE", + "078: APRS POPUP", + "079: APRS RINGER", + "080: APRS RINGER (CS)", + "081: APRS TX DELAY", + "082: APRS UNITS", + "083: BEACON INFO", + "084: BEACON STATUSTXT", + "085: BEACON TX SET", + "086: DIGI PATH", + "087: DIGI PATH 1", + "088: DIGI PATH 2", + "089: DIGI PATH 3", + "090: DIGI PATH 4", + "091: DIGI PATH FULL 1", + "092: DIGI PATH FULL 2", + "093: CALLSIGN (APRS)", + "094: MESSAGE GROUP", + "095: MESSAGE REPLY", + "096: MY POSITION SET", + "097: MY POSITION", + "098: MY SYMBOL", + "099: POSITION COMMENT", + "100: SmartBeaconing", + "101: SORT FILTER", + "102: VOICE ALERT", + "103: STATION LIST", + "104: MESSAGE LIST", + "105: BEACON TX SELECT", + "106: BEACON TX", + "107: BACKUP", + "108: MEMORY INFO", + "109: FORMAT", + "110: JPG IMPORT", + "111: Bluetooth", + "112: VOICE MEMORY", + "113: FVS REC", + "114: TRACK SELECT", + "115: PLAY", + "116: STOP", + "117: CLEAR", + "118: VOICE GUIDE", + "119: This → Other", + "120: Other → This", + "121: CALLSIGN", + "122: MEMORY CH RESET", + "123: APRS RESET", + "124: CONFIG SET", + "125: CONFIG RECALL", + "126: SOFTWARE VERSION" + ] + }, + { + "key": "function-key-2", + "label": "Function Key 2", + "type": "select", + "options": [ + "001: FREQENCY INPUT", + "002: LCD BRIGHTNESS", + "003: FREQUENCY COLOR", + "004: BAND SCOPE", + "005: LOCATION INFO", + "006: COMPASS", + "007: DISPLAY MODE", + "008: TX POWER", + "009: AMS TX MODE", + "010: MIC GAIN", + "011: VOX", + "012: AUTO DIALER", + "013: TOT", + "014: DIGITAL VW", + "015: FM BANDWIDTH", + "016: RX MODE", + "017: SUB BUND", + "018: AUDIO EQUALIZER", + "019: HOME CH", + "020: MEMORY LIST", + "021: MEMORY LIST MODE", + "022: PMG CLEAR", + "023: BEEP", + "024: BAND SKIP", + "025: RPT ARS", + "026: RPT SHIFT", + "027: RPT SHIFT FREQ", + "028: RPT REVERSE", + "029: MIC PROGRAM KEY", + "030: DATE&TIME ADJUST", + "031: DATE&TIME FORMAT", + "032: TIME ZONE", + "033: STEP", + "034: CLOCK TYPE", + "035: UNIT", + "036: APO", + "037: GPS DATUM", + "038: GPS DEVICE", + "039: GPS LOG", + "040: RECODING", + "041: REC/STOP", + "042: FRONT SP MUTE", + "043: DTMF", + "044: DTMF MEMORY", + "045: SQL TYPE", + "046: TONE SQL FREQ", + "047: SQL EXPANSION", + "048: PAGER CODE", + "049: PR FREQUENCY", + "050: BELL RINGER", + "051: WX ALERT", + "052: SCAN", + "053: DUAL RCV MODE", + "054: DUAL RX INTERVAL", + "055: PRIORITY REVERT", + "056: SCAN RESUME", + "057: DIGITAL POPUP", + "058: LOCATION SERVICE", + "059: STANDBY BEEP", + "060: DP-ID LIST", + "061: RANGE RINGER", + "062: RADIO ID", + "063: LOG LIST", + "064: RPT/WIRES FREQ", + "065: SEARCH SETUP", + "066: EDIT CATEGORYTAG", + "067: DELETE ROOM/NODE", + "068: WIRES DG-ID", + "069: COM PORT", + "070: DATA BAND", + "071: DATA SPEED", + "072: DATA SQL", + "073: APRS DESTINATION", + "074: APRS FILTER", + "075: APRS MSG TEXT", + "076: APRS MODEM", + "077: APRS MUTE", + "078: APRS POPUP", + "079: APRS RINGER", + "080: APRS RINGER (CS)", + "081: APRS TX DELAY", + "082: APRS UNITS", + "083: BEACON INFO", + "084: BEACON STATUSTXT", + "085: BEACON TX SET", + "086: DIGI PATH", + "087: DIGI PATH 1", + "088: DIGI PATH 2", + "089: DIGI PATH 3", + "090: DIGI PATH 4", + "091: DIGI PATH FULL 1", + "092: DIGI PATH FULL 2", + "093: CALLSIGN (APRS)", + "094: MESSAGE GROUP", + "095: MESSAGE REPLY", + "096: MY POSITION SET", + "097: MY POSITION", + "098: MY SYMBOL", + "099: POSITION COMMENT", + "100: SmartBeaconing", + "101: SORT FILTER", + "102: VOICE ALERT", + "103: STATION LIST", + "104: MESSAGE LIST", + "105: BEACON TX SELECT", + "106: BEACON TX", + "107: BACKUP", + "108: MEMORY INFO", + "109: FORMAT", + "110: JPG IMPORT", + "111: Bluetooth", + "112: VOICE MEMORY", + "113: FVS REC", + "114: TRACK SELECT", + "115: PLAY", + "116: STOP", + "117: CLEAR", + "118: VOICE GUIDE", + "119: This → Other", + "120: Other → This", + "121: CALLSIGN", + "122: MEMORY CH RESET", + "123: APRS RESET", + "124: CONFIG SET", + "125: CONFIG RECALL", + "126: SOFTWARE VERSION" + ] + }, + { + "key": "function-key-3", + "label": "Function Key 3", + "type": "select", + "options": [ + "001: FREQENCY INPUT", + "002: LCD BRIGHTNESS", + "003: FREQUENCY COLOR", + "004: BAND SCOPE", + "005: LOCATION INFO", + "006: COMPASS", + "007: DISPLAY MODE", + "008: TX POWER", + "009: AMS TX MODE", + "010: MIC GAIN", + "011: VOX", + "012: AUTO DIALER", + "013: TOT", + "014: DIGITAL VW", + "015: FM BANDWIDTH", + "016: RX MODE", + "017: SUB BUND", + "018: AUDIO EQUALIZER", + "019: HOME CH", + "020: MEMORY LIST", + "021: MEMORY LIST MODE", + "022: PMG CLEAR", + "023: BEEP", + "024: BAND SKIP", + "025: RPT ARS", + "026: RPT SHIFT", + "027: RPT SHIFT FREQ", + "028: RPT REVERSE", + "029: MIC PROGRAM KEY", + "030: DATE&TIME ADJUST", + "031: DATE&TIME FORMAT", + "032: TIME ZONE", + "033: STEP", + "034: CLOCK TYPE", + "035: UNIT", + "036: APO", + "037: GPS DATUM", + "038: GPS DEVICE", + "039: GPS LOG", + "040: RECODING", + "041: REC/STOP", + "042: FRONT SP MUTE", + "043: DTMF", + "044: DTMF MEMORY", + "045: SQL TYPE", + "046: TONE SQL FREQ", + "047: SQL EXPANSION", + "048: PAGER CODE", + "049: PR FREQUENCY", + "050: BELL RINGER", + "051: WX ALERT", + "052: SCAN", + "053: DUAL RCV MODE", + "054: DUAL RX INTERVAL", + "055: PRIORITY REVERT", + "056: SCAN RESUME", + "057: DIGITAL POPUP", + "058: LOCATION SERVICE", + "059: STANDBY BEEP", + "060: DP-ID LIST", + "061: RANGE RINGER", + "062: RADIO ID", + "063: LOG LIST", + "064: RPT/WIRES FREQ", + "065: SEARCH SETUP", + "066: EDIT CATEGORYTAG", + "067: DELETE ROOM/NODE", + "068: WIRES DG-ID", + "069: COM PORT", + "070: DATA BAND", + "071: DATA SPEED", + "072: DATA SQL", + "073: APRS DESTINATION", + "074: APRS FILTER", + "075: APRS MSG TEXT", + "076: APRS MODEM", + "077: APRS MUTE", + "078: APRS POPUP", + "079: APRS RINGER", + "080: APRS RINGER (CS)", + "081: APRS TX DELAY", + "082: APRS UNITS", + "083: BEACON INFO", + "084: BEACON STATUSTXT", + "085: BEACON TX SET", + "086: DIGI PATH", + "087: DIGI PATH 1", + "088: DIGI PATH 2", + "089: DIGI PATH 3", + "090: DIGI PATH 4", + "091: DIGI PATH FULL 1", + "092: DIGI PATH FULL 2", + "093: CALLSIGN (APRS)", + "094: MESSAGE GROUP", + "095: MESSAGE REPLY", + "096: MY POSITION SET", + "097: MY POSITION", + "098: MY SYMBOL", + "099: POSITION COMMENT", + "100: SmartBeaconing", + "101: SORT FILTER", + "102: VOICE ALERT", + "103: STATION LIST", + "104: MESSAGE LIST", + "105: BEACON TX SELECT", + "106: BEACON TX", + "107: BACKUP", + "108: MEMORY INFO", + "109: FORMAT", + "110: JPG IMPORT", + "111: Bluetooth", + "112: VOICE MEMORY", + "113: FVS REC", + "114: TRACK SELECT", + "115: PLAY", + "116: STOP", + "117: CLEAR", + "118: VOICE GUIDE", + "119: This → Other", + "120: Other → This", + "121: CALLSIGN", + "122: MEMORY CH RESET", + "123: APRS RESET", + "124: CONFIG SET", + "125: CONFIG RECALL", + "126: SOFTWARE VERSION" + ] + }, + { + "key": "function-key-4", + "label": "Function Key 4", + "type": "select", + "options": [ + "001: FREQENCY INPUT", + "002: LCD BRIGHTNESS", + "003: FREQUENCY COLOR", + "004: BAND SCOPE", + "005: LOCATION INFO", + "006: COMPASS", + "007: DISPLAY MODE", + "008: TX POWER", + "009: AMS TX MODE", + "010: MIC GAIN", + "011: VOX", + "012: AUTO DIALER", + "013: TOT", + "014: DIGITAL VW", + "015: FM BANDWIDTH", + "016: RX MODE", + "017: SUB BUND", + "018: AUDIO EQUALIZER", + "019: HOME CH", + "020: MEMORY LIST", + "021: MEMORY LIST MODE", + "022: PMG CLEAR", + "023: BEEP", + "024: BAND SKIP", + "025: RPT ARS", + "026: RPT SHIFT", + "027: RPT SHIFT FREQ", + "028: RPT REVERSE", + "029: MIC PROGRAM KEY", + "030: DATE&TIME ADJUST", + "031: DATE&TIME FORMAT", + "032: TIME ZONE", + "033: STEP", + "034: CLOCK TYPE", + "035: UNIT", + "036: APO", + "037: GPS DATUM", + "038: GPS DEVICE", + "039: GPS LOG", + "040: RECODING", + "041: REC/STOP", + "042: FRONT SP MUTE", + "043: DTMF", + "044: DTMF MEMORY", + "045: SQL TYPE", + "046: TONE SQL FREQ", + "047: SQL EXPANSION", + "048: PAGER CODE", + "049: PR FREQUENCY", + "050: BELL RINGER", + "051: WX ALERT", + "052: SCAN", + "053: DUAL RCV MODE", + "054: DUAL RX INTERVAL", + "055: PRIORITY REVERT", + "056: SCAN RESUME", + "057: DIGITAL POPUP", + "058: LOCATION SERVICE", + "059: STANDBY BEEP", + "060: DP-ID LIST", + "061: RANGE RINGER", + "062: RADIO ID", + "063: LOG LIST", + "064: RPT/WIRES FREQ", + "065: SEARCH SETUP", + "066: EDIT CATEGORYTAG", + "067: DELETE ROOM/NODE", + "068: WIRES DG-ID", + "069: COM PORT", + "070: DATA BAND", + "071: DATA SPEED", + "072: DATA SQL", + "073: APRS DESTINATION", + "074: APRS FILTER", + "075: APRS MSG TEXT", + "076: APRS MODEM", + "077: APRS MUTE", + "078: APRS POPUP", + "079: APRS RINGER", + "080: APRS RINGER (CS)", + "081: APRS TX DELAY", + "082: APRS UNITS", + "083: BEACON INFO", + "084: BEACON STATUSTXT", + "085: BEACON TX SET", + "086: DIGI PATH", + "087: DIGI PATH 1", + "088: DIGI PATH 2", + "089: DIGI PATH 3", + "090: DIGI PATH 4", + "091: DIGI PATH FULL 1", + "092: DIGI PATH FULL 2", + "093: CALLSIGN (APRS)", + "094: MESSAGE GROUP", + "095: MESSAGE REPLY", + "096: MY POSITION SET", + "097: MY POSITION", + "098: MY SYMBOL", + "099: POSITION COMMENT", + "100: SmartBeaconing", + "101: SORT FILTER", + "102: VOICE ALERT", + "103: STATION LIST", + "104: MESSAGE LIST", + "105: BEACON TX SELECT", + "106: BEACON TX", + "107: BACKUP", + "108: MEMORY INFO", + "109: FORMAT", + "110: JPG IMPORT", + "111: Bluetooth", + "112: VOICE MEMORY", + "113: FVS REC", + "114: TRACK SELECT", + "115: PLAY", + "116: STOP", + "117: CLEAR", + "118: VOICE GUIDE", + "119: This → Other", + "120: Other → This", + "121: CALLSIGN", + "122: MEMORY CH RESET", + "123: APRS RESET", + "124: CONFIG SET", + "125: CONFIG RECALL", + "126: SOFTWARE VERSION" + ] + }, + { + "key": "function-key-5", + "label": "Function Key 5", + "type": "select", + "options": [ + "001: FREQENCY INPUT", + "002: LCD BRIGHTNESS", + "003: FREQUENCY COLOR", + "004: BAND SCOPE", + "005: LOCATION INFO", + "006: COMPASS", + "007: DISPLAY MODE", + "008: TX POWER", + "009: AMS TX MODE", + "010: MIC GAIN", + "011: VOX", + "012: AUTO DIALER", + "013: TOT", + "014: DIGITAL VW", + "015: FM BANDWIDTH", + "016: RX MODE", + "017: SUB BUND", + "018: AUDIO EQUALIZER", + "019: HOME CH", + "020: MEMORY LIST", + "021: MEMORY LIST MODE", + "022: PMG CLEAR", + "023: BEEP", + "024: BAND SKIP", + "025: RPT ARS", + "026: RPT SHIFT", + "027: RPT SHIFT FREQ", + "028: RPT REVERSE", + "029: MIC PROGRAM KEY", + "030: DATE&TIME ADJUST", + "031: DATE&TIME FORMAT", + "032: TIME ZONE", + "033: STEP", + "034: CLOCK TYPE", + "035: UNIT", + "036: APO", + "037: GPS DATUM", + "038: GPS DEVICE", + "039: GPS LOG", + "040: RECODING", + "041: REC/STOP", + "042: FRONT SP MUTE", + "043: DTMF", + "044: DTMF MEMORY", + "045: SQL TYPE", + "046: TONE SQL FREQ", + "047: SQL EXPANSION", + "048: PAGER CODE", + "049: PR FREQUENCY", + "050: BELL RINGER", + "051: WX ALERT", + "052: SCAN", + "053: DUAL RCV MODE", + "054: DUAL RX INTERVAL", + "055: PRIORITY REVERT", + "056: SCAN RESUME", + "057: DIGITAL POPUP", + "058: LOCATION SERVICE", + "059: STANDBY BEEP", + "060: DP-ID LIST", + "061: RANGE RINGER", + "062: RADIO ID", + "063: LOG LIST", + "064: RPT/WIRES FREQ", + "065: SEARCH SETUP", + "066: EDIT CATEGORYTAG", + "067: DELETE ROOM/NODE", + "068: WIRES DG-ID", + "069: COM PORT", + "070: DATA BAND", + "071: DATA SPEED", + "072: DATA SQL", + "073: APRS DESTINATION", + "074: APRS FILTER", + "075: APRS MSG TEXT", + "076: APRS MODEM", + "077: APRS MUTE", + "078: APRS POPUP", + "079: APRS RINGER", + "080: APRS RINGER (CS)", + "081: APRS TX DELAY", + "082: APRS UNITS", + "083: BEACON INFO", + "084: BEACON STATUSTXT", + "085: BEACON TX SET", + "086: DIGI PATH", + "087: DIGI PATH 1", + "088: DIGI PATH 2", + "089: DIGI PATH 3", + "090: DIGI PATH 4", + "091: DIGI PATH FULL 1", + "092: DIGI PATH FULL 2", + "093: CALLSIGN (APRS)", + "094: MESSAGE GROUP", + "095: MESSAGE REPLY", + "096: MY POSITION SET", + "097: MY POSITION", + "098: MY SYMBOL", + "099: POSITION COMMENT", + "100: SmartBeaconing", + "101: SORT FILTER", + "102: VOICE ALERT", + "103: STATION LIST", + "104: MESSAGE LIST", + "105: BEACON TX SELECT", + "106: BEACON TX", + "107: BACKUP", + "108: MEMORY INFO", + "109: FORMAT", + "110: JPG IMPORT", + "111: Bluetooth", + "112: VOICE MEMORY", + "113: FVS REC", + "114: TRACK SELECT", + "115: PLAY", + "116: STOP", + "117: CLEAR", + "118: VOICE GUIDE", + "119: This → Other", + "120: Other → This", + "121: CALLSIGN", + "122: MEMORY CH RESET", + "123: APRS RESET", + "124: CONFIG SET", + "125: CONFIG RECALL", + "126: SOFTWARE VERSION" + ] + }, + { + "key": "function-key-6", + "label": "Function Key 6", + "type": "select", + "options": [ + "001: FREQENCY INPUT", + "002: LCD BRIGHTNESS", + "003: FREQUENCY COLOR", + "004: BAND SCOPE", + "005: LOCATION INFO", + "006: COMPASS", + "007: DISPLAY MODE", + "008: TX POWER", + "009: AMS TX MODE", + "010: MIC GAIN", + "011: VOX", + "012: AUTO DIALER", + "013: TOT", + "014: DIGITAL VW", + "015: FM BANDWIDTH", + "016: RX MODE", + "017: SUB BUND", + "018: AUDIO EQUALIZER", + "019: HOME CH", + "020: MEMORY LIST", + "021: MEMORY LIST MODE", + "022: PMG CLEAR", + "023: BEEP", + "024: BAND SKIP", + "025: RPT ARS", + "026: RPT SHIFT", + "027: RPT SHIFT FREQ", + "028: RPT REVERSE", + "029: MIC PROGRAM KEY", + "030: DATE&TIME ADJUST", + "031: DATE&TIME FORMAT", + "032: TIME ZONE", + "033: STEP", + "034: CLOCK TYPE", + "035: UNIT", + "036: APO", + "037: GPS DATUM", + "038: GPS DEVICE", + "039: GPS LOG", + "040: RECODING", + "041: REC/STOP", + "042: FRONT SP MUTE", + "043: DTMF", + "044: DTMF MEMORY", + "045: SQL TYPE", + "046: TONE SQL FREQ", + "047: SQL EXPANSION", + "048: PAGER CODE", + "049: PR FREQUENCY", + "050: BELL RINGER", + "051: WX ALERT", + "052: SCAN", + "053: DUAL RCV MODE", + "054: DUAL RX INTERVAL", + "055: PRIORITY REVERT", + "056: SCAN RESUME", + "057: DIGITAL POPUP", + "058: LOCATION SERVICE", + "059: STANDBY BEEP", + "060: DP-ID LIST", + "061: RANGE RINGER", + "062: RADIO ID", + "063: LOG LIST", + "064: RPT/WIRES FREQ", + "065: SEARCH SETUP", + "066: EDIT CATEGORYTAG", + "067: DELETE ROOM/NODE", + "068: WIRES DG-ID", + "069: COM PORT", + "070: DATA BAND", + "071: DATA SPEED", + "072: DATA SQL", + "073: APRS DESTINATION", + "074: APRS FILTER", + "075: APRS MSG TEXT", + "076: APRS MODEM", + "077: APRS MUTE", + "078: APRS POPUP", + "079: APRS RINGER", + "080: APRS RINGER (CS)", + "081: APRS TX DELAY", + "082: APRS UNITS", + "083: BEACON INFO", + "084: BEACON STATUSTXT", + "085: BEACON TX SET", + "086: DIGI PATH", + "087: DIGI PATH 1", + "088: DIGI PATH 2", + "089: DIGI PATH 3", + "090: DIGI PATH 4", + "091: DIGI PATH FULL 1", + "092: DIGI PATH FULL 2", + "093: CALLSIGN (APRS)", + "094: MESSAGE GROUP", + "095: MESSAGE REPLY", + "096: MY POSITION SET", + "097: MY POSITION", + "098: MY SYMBOL", + "099: POSITION COMMENT", + "100: SmartBeaconing", + "101: SORT FILTER", + "102: VOICE ALERT", + "103: STATION LIST", + "104: MESSAGE LIST", + "105: BEACON TX SELECT", + "106: BEACON TX", + "107: BACKUP", + "108: MEMORY INFO", + "109: FORMAT", + "110: JPG IMPORT", + "111: Bluetooth", + "112: VOICE MEMORY", + "113: FVS REC", + "114: TRACK SELECT", + "115: PLAY", + "116: STOP", + "117: CLEAR", + "118: VOICE GUIDE", + "119: This → Other", + "120: Other → This", + "121: CALLSIGN", + "122: MEMORY CH RESET", + "123: APRS RESET", + "124: CONFIG SET", + "125: CONFIG RECALL", + "126: SOFTWARE VERSION" + ] + }, + { + "key": "function-key-7", + "label": "Function Key 7", + "type": "select", + "options": [ + "001: FREQENCY INPUT", + "002: LCD BRIGHTNESS", + "003: FREQUENCY COLOR", + "004: BAND SCOPE", + "005: LOCATION INFO", + "006: COMPASS", + "007: DISPLAY MODE", + "008: TX POWER", + "009: AMS TX MODE", + "010: MIC GAIN", + "011: VOX", + "012: AUTO DIALER", + "013: TOT", + "014: DIGITAL VW", + "015: FM BANDWIDTH", + "016: RX MODE", + "017: SUB BUND", + "018: AUDIO EQUALIZER", + "019: HOME CH", + "020: MEMORY LIST", + "021: MEMORY LIST MODE", + "022: PMG CLEAR", + "023: BEEP", + "024: BAND SKIP", + "025: RPT ARS", + "026: RPT SHIFT", + "027: RPT SHIFT FREQ", + "028: RPT REVERSE", + "029: MIC PROGRAM KEY", + "030: DATE&TIME ADJUST", + "031: DATE&TIME FORMAT", + "032: TIME ZONE", + "033: STEP", + "034: CLOCK TYPE", + "035: UNIT", + "036: APO", + "037: GPS DATUM", + "038: GPS DEVICE", + "039: GPS LOG", + "040: RECODING", + "041: REC/STOP", + "042: FRONT SP MUTE", + "043: DTMF", + "044: DTMF MEMORY", + "045: SQL TYPE", + "046: TONE SQL FREQ", + "047: SQL EXPANSION", + "048: PAGER CODE", + "049: PR FREQUENCY", + "050: BELL RINGER", + "051: WX ALERT", + "052: SCAN", + "053: DUAL RCV MODE", + "054: DUAL RX INTERVAL", + "055: PRIORITY REVERT", + "056: SCAN RESUME", + "057: DIGITAL POPUP", + "058: LOCATION SERVICE", + "059: STANDBY BEEP", + "060: DP-ID LIST", + "061: RANGE RINGER", + "062: RADIO ID", + "063: LOG LIST", + "064: RPT/WIRES FREQ", + "065: SEARCH SETUP", + "066: EDIT CATEGORYTAG", + "067: DELETE ROOM/NODE", + "068: WIRES DG-ID", + "069: COM PORT", + "070: DATA BAND", + "071: DATA SPEED", + "072: DATA SQL", + "073: APRS DESTINATION", + "074: APRS FILTER", + "075: APRS MSG TEXT", + "076: APRS MODEM", + "077: APRS MUTE", + "078: APRS POPUP", + "079: APRS RINGER", + "080: APRS RINGER (CS)", + "081: APRS TX DELAY", + "082: APRS UNITS", + "083: BEACON INFO", + "084: BEACON STATUSTXT", + "085: BEACON TX SET", + "086: DIGI PATH", + "087: DIGI PATH 1", + "088: DIGI PATH 2", + "089: DIGI PATH 3", + "090: DIGI PATH 4", + "091: DIGI PATH FULL 1", + "092: DIGI PATH FULL 2", + "093: CALLSIGN (APRS)", + "094: MESSAGE GROUP", + "095: MESSAGE REPLY", + "096: MY POSITION SET", + "097: MY POSITION", + "098: MY SYMBOL", + "099: POSITION COMMENT", + "100: SmartBeaconing", + "101: SORT FILTER", + "102: VOICE ALERT", + "103: STATION LIST", + "104: MESSAGE LIST", + "105: BEACON TX SELECT", + "106: BEACON TX", + "107: BACKUP", + "108: MEMORY INFO", + "109: FORMAT", + "110: JPG IMPORT", + "111: Bluetooth", + "112: VOICE MEMORY", + "113: FVS REC", + "114: TRACK SELECT", + "115: PLAY", + "116: STOP", + "117: CLEAR", + "118: VOICE GUIDE", + "119: This → Other", + "120: Other → This", + "121: CALLSIGN", + "122: MEMORY CH RESET", + "123: APRS RESET", + "124: CONFIG SET", + "125: CONFIG RECALL", + "126: SOFTWARE VERSION" + ] + }, + { + "key": "function-key-8", + "label": "Function Key 8", + "type": "select", + "options": [ + "001: FREQENCY INPUT", + "002: LCD BRIGHTNESS", + "003: FREQUENCY COLOR", + "004: BAND SCOPE", + "005: LOCATION INFO", + "006: COMPASS", + "007: DISPLAY MODE", + "008: TX POWER", + "009: AMS TX MODE", + "010: MIC GAIN", + "011: VOX", + "012: AUTO DIALER", + "013: TOT", + "014: DIGITAL VW", + "015: FM BANDWIDTH", + "016: RX MODE", + "017: SUB BUND", + "018: AUDIO EQUALIZER", + "019: HOME CH", + "020: MEMORY LIST", + "021: MEMORY LIST MODE", + "022: PMG CLEAR", + "023: BEEP", + "024: BAND SKIP", + "025: RPT ARS", + "026: RPT SHIFT", + "027: RPT SHIFT FREQ", + "028: RPT REVERSE", + "029: MIC PROGRAM KEY", + "030: DATE&TIME ADJUST", + "031: DATE&TIME FORMAT", + "032: TIME ZONE", + "033: STEP", + "034: CLOCK TYPE", + "035: UNIT", + "036: APO", + "037: GPS DATUM", + "038: GPS DEVICE", + "039: GPS LOG", + "040: RECODING", + "041: REC/STOP", + "042: FRONT SP MUTE", + "043: DTMF", + "044: DTMF MEMORY", + "045: SQL TYPE", + "046: TONE SQL FREQ", + "047: SQL EXPANSION", + "048: PAGER CODE", + "049: PR FREQUENCY", + "050: BELL RINGER", + "051: WX ALERT", + "052: SCAN", + "053: DUAL RCV MODE", + "054: DUAL RX INTERVAL", + "055: PRIORITY REVERT", + "056: SCAN RESUME", + "057: DIGITAL POPUP", + "058: LOCATION SERVICE", + "059: STANDBY BEEP", + "060: DP-ID LIST", + "061: RANGE RINGER", + "062: RADIO ID", + "063: LOG LIST", + "064: RPT/WIRES FREQ", + "065: SEARCH SETUP", + "066: EDIT CATEGORYTAG", + "067: DELETE ROOM/NODE", + "068: WIRES DG-ID", + "069: COM PORT", + "070: DATA BAND", + "071: DATA SPEED", + "072: DATA SQL", + "073: APRS DESTINATION", + "074: APRS FILTER", + "075: APRS MSG TEXT", + "076: APRS MODEM", + "077: APRS MUTE", + "078: APRS POPUP", + "079: APRS RINGER", + "080: APRS RINGER (CS)", + "081: APRS TX DELAY", + "082: APRS UNITS", + "083: BEACON INFO", + "084: BEACON STATUSTXT", + "085: BEACON TX SET", + "086: DIGI PATH", + "087: DIGI PATH 1", + "088: DIGI PATH 2", + "089: DIGI PATH 3", + "090: DIGI PATH 4", + "091: DIGI PATH FULL 1", + "092: DIGI PATH FULL 2", + "093: CALLSIGN (APRS)", + "094: MESSAGE GROUP", + "095: MESSAGE REPLY", + "096: MY POSITION SET", + "097: MY POSITION", + "098: MY SYMBOL", + "099: POSITION COMMENT", + "100: SmartBeaconing", + "101: SORT FILTER", + "102: VOICE ALERT", + "103: STATION LIST", + "104: MESSAGE LIST", + "105: BEACON TX SELECT", + "106: BEACON TX", + "107: BACKUP", + "108: MEMORY INFO", + "109: FORMAT", + "110: JPG IMPORT", + "111: Bluetooth", + "112: VOICE MEMORY", + "113: FVS REC", + "114: TRACK SELECT", + "115: PLAY", + "116: STOP", + "117: CLEAR", + "118: VOICE GUIDE", + "119: This → Other", + "120: Other → This", + "121: CALLSIGN", + "122: MEMORY CH RESET", + "123: APRS RESET", + "124: CONFIG SET", + "125: CONFIG RECALL", + "126: SOFTWARE VERSION" + ] + }, + { + "key": "section-signaling", + "label": "Signaling", + "type": "section", + "group": "DTMF & Signaling" + }, + { + "key": "auto-dialer", + "label": "Auto Dialer", + "type": "boolean" + }, + { + "key": "a-bell-ringer", + "label": "A Bell Ringer", + "type": "select", + "options": [ + "OFF", + "1 time", + "3 times", + "5 times", + "8 times", + "CONTINUOUS" + ] + }, + { + "key": "b-bell-ringer", + "label": "B Bell Ringer", + "type": "select", + "options": [ + "OFF", + "1 time", + "3 times", + "5 times", + "8 times", + "CONTINUOUS" + ] + }, + { + "key": "sql-expansion", + "label": "SQL Expansion", + "type": "boolean" + }, + { + "key": "wx-alert", + "label": "WX Alert", + "type": "boolean" + }, + { + "key": "pager-rx-code-1", + "label": "Pager RX Code 1", + "type": "select", + "options": [ + "01", + "02", + "03", + "04", + "05", + "06", + "07", + "08", + "09", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30", + "31", + "32", + "33", + "34", + "35", + "36", + "37", + "38", + "39", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49", + "50" + ] + }, + { + "key": "pager-rx-code-2", + "label": "Pager RX Code 2", + "type": "select", + "options": [ + "01", + "02", + "03", + "04", + "05", + "06", + "07", + "08", + "09", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30", + "31", + "32", + "33", + "34", + "35", + "36", + "37", + "38", + "39", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49", + "50" + ] + }, + { + "key": "pager-tx-code-1", + "label": "Pager TX Code 1", + "type": "select", + "options": [ + "01", + "02", + "03", + "04", + "05", + "06", + "07", + "08", + "09", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30", + "31", + "32", + "33", + "34", + "35", + "36", + "37", + "38", + "39", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49", + "50" + ] + }, + { + "key": "pager-tx-code-2", + "label": "Pager TX Code 2", + "type": "select", + "options": [ + "01", + "02", + "03", + "04", + "05", + "06", + "07", + "08", + "09", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30", + "31", + "32", + "33", + "34", + "35", + "36", + "37", + "38", + "39", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49", + "50" + ] + }, + { + "key": "section-dtmf-memory", + "label": "DTMF Memory", + "type": "section", + "group": "DTMF & Signaling" + }, + { + "key": "dtmf-memory-1", + "label": "DTMF Memory 1", + "type": "text", + "max_length": 16 + }, + { + "key": "dtmf-memory-2", + "label": "DTMF Memory 2", + "type": "text", + "max_length": 16 + }, + { + "key": "dtmf-memory-3", + "label": "DTMF Memory 3", + "type": "text", + "max_length": 16 + }, + { + "key": "dtmf-memory-4", + "label": "DTMF Memory 4", + "type": "text", + "max_length": 16 + }, + { + "key": "dtmf-memory-5", + "label": "DTMF Memory 5", + "type": "text", + "max_length": 16 + }, + { + "key": "dtmf-memory-6", + "label": "DTMF Memory 6", + "type": "text", + "max_length": 16 + }, + { + "key": "dtmf-memory-7", + "label": "DTMF Memory 7", + "type": "text", + "max_length": 16 + }, + { + "key": "dtmf-memory-8", + "label": "DTMF Memory 8", + "type": "text", + "max_length": 16 + }, + { + "key": "dtmf-memory-9", + "label": "DTMF Memory 9", + "type": "text", + "max_length": 16 + }, + { + "key": "section-digital", + "label": "Digital", + "type": "section", + "group": "Digital" + }, + { + "key": "ams-tx-mode", + "label": "AMS TX Mode", + "type": "select", + "options": [ + "AUTO", + "TX FM FIXED", + "TX DN FIXED" + ] + }, + { + "key": "digital-popup", + "label": "Digital Popup", + "type": "select", + "options": [ + "OFF", + "2 sec", + "4 sec", + "6 sec", + "8 sec", + "10 sec", + "20 sec", + "30 sec", + "60 sec", + "CONTINUE" + ] + }, + { + "key": "location-service", + "label": "Location Service", + "type": "boolean" + }, + { + "key": "standby-beep", + "label": "Standby Beep", + "type": "boolean" + }, + { + "key": "digital-vw", + "label": "Digital VW", + "type": "boolean" + }, + { + "key": "gm-range-ringer", + "label": "GM Range Ringer", + "type": "boolean" + }, + { + "key": "section-wires-x", + "label": "WIRES-X", + "type": "section", + "group": "Digital" + }, + { + "key": "wires-x-rpt-wires-freq", + "label": "RPT/WIRES Freq", + "type": "select", + "options": [ + "MANUAL", + "PRESET" + ] + }, + { + "key": "wires-x-search-setup", + "label": "Search Setup", + "type": "select", + "options": [ + "HISTORY", + "ACTIVITY" + ] + }, + { + "key": "wires-x-dg-id", + "label": "WIRES DG-ID", + "type": "select", + "options": [ + "AUTO", + "01", + "02", + "03", + "04", + "05", + "06", + "07", + "08", + "09", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30", + "31", + "32", + "33", + "34", + "35", + "36", + "37", + "38", + "39", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49", + "50", + "51", + "52", + "53", + "54", + "55", + "56", + "57", + "58", + "59", + "60", + "61", + "62", + "63", + "64", + "65", + "66", + "67", + "68", + "69", + "70", + "71", + "72", + "73", + "74", + "75", + "76", + "77", + "78", + "79", + "80", + "81", + "82", + "83", + "84", + "85", + "86", + "87", + "88", + "89", + "90", + "91", + "92", + "93", + "94", + "95", + "96", + "97", + "98", + "99" + ] + }, + { + "key": "section-bluetooth", + "label": "Bluetooth", + "type": "section", + "group": "Bluetooth" + }, + { + "key": "bluetooth", + "label": "Bluetooth", + "type": "boolean" + }, + { + "key": "bluetooth-audio", + "label": "Bluetooth Audio", + "type": "select", + "options": [ + "AUTO", + "FIX" + ] + }, + { + "key": "section-data", + "label": "Data", + "type": "section", + "group": "Connections" + }, + { + "key": "com-port-speed", + "label": "COM Port Speed", + "type": "select", + "options": [ + "4800bps", + "9600bps", + "19200bps", + "38400bps", + "57600bps" + ] + }, + { + "key": "com-port-output", + "label": "COM Port Output", + "type": "select", + "options": [ + "OFF", + "GPS OUT", + "PACKET", + "WAY POINT" + ] + }, + { + "key": "wp-format", + "label": "WP Format", + "type": "select", + "options": [ + "NMEA9", + "NMEA8", + "NMEA7", + "NMEA6" + ] + }, + { + "key": "wp-filter", + "label": "WP Filter", + "type": "select", + "options": [ + "ALL", + "MOBILE", + "FREQUENCY", + "OBJECT/ITEM", + "DIGIPEATER", + "VoIP", + "WEATHER", + "YAESU", + "CALL RINGER", + "RNG RINGER" + ] + }, + { + "key": "aprs-band", + "label": "APRS Band", + "type": "select", + "options": [ + "MAIN", + "SUB", + "THIS BAND(A)", + "OTHER BAND(B)" + ] + }, + { + "key": "data-band", + "label": "DATA Band", + "type": "select", + "options": [ + "MAIN", + "SUB", + "THIS BAND(A)", + "OTHER BAND(B)" + ] + }, + { + "key": "aprs-speed", + "label": "APRS Speed", + "type": "select", + "options": [ + "1200 bps", + "9600 bps" + ] + }, + { + "key": "data-speed", + "label": "DATA Speed", + "type": "select", + "options": [ + "1200 bps", + "9600 bps" + ] + }, + { + "key": "aprs-squelch", + "label": "APRS Squelch", + "type": "select", + "options": [ + "RX BAND", + "TX/RX BAND" + ] + }, + { + "key": "data-squelch", + "label": "DATA Squelch", + "type": "select", + "options": [ + "RX BAND", + "TX/RX BAND" + ] + }, + { + "key": "data-squelch-tx", + "label": "Data Squelch TX", + "type": "boolean" + }, + { + "key": "section-aprs", + "label": "APRS", + "type": "section", + "group": "APRS" + }, + { + "key": "aprs-modem", + "label": "APRS Modem", + "type": "boolean" + }, + { + "key": "aprs-mute", + "label": "APRS Mute", + "type": "boolean" + }, + { + "key": "aprs-tx-delay", + "label": "APRS TX Delay", + "type": "select", + "options": [ + "100ms", + "150ms", + "200ms", + "250ms", + "300ms", + "400ms", + "500ms", + "750ms", + "1000ms" + ] + }, + { + "key": "aprs-callsign", + "label": "APRS Callsign", + "type": "text", + "max_length": 6 + }, + { + "key": "aprs-callsign-ssid", + "label": "APRS Callsign SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "position-comment", + "label": "Position Comment", + "type": "select", + "options": [ + "Off Duty", + "En Route", + "In Service", + "Returning", + "Committed", + "Special", + "Priority", + "Custom 0", + "Custom 1", + "Custom 2", + "Custom 3", + "Custom 4", + "Custom 5", + "Custom 6", + "EMERGENCY!" + ] + }, + { + "key": "my-position", + "label": "My Position", + "type": "select", + "options": [ + "GPS", + "MANUAL" + ] + }, + { + "key": "my-symbol", + "label": "My Symbol", + "type": "select", + "options": [ + "1", + "2", + "3", + "4" + ] + }, + { + "key": "my-symbol-1", + "label": "My Symbol 1", + "type": "select", + "options": [ + "/#", + "/&", + "/'", + "/-", + "/.", + "/0", + "/:", + "/;", + "/<", + "/=", + "/>", + "/C", + "/E", + "/I", + "/K", + "/O", + "/P", + "/R", + "/T", + "/U", + "/V", + "/W", + "/X", + "/Y", + "/[", + "/\\", + "/^", + "/_", + "/a", + "/b", + "/f", + "/g", + "/j", + "/k", + "/m", + "/r", + "/s", + "/u", + "/v", + "/y", + "\\#", + "\\&", + "\\-", + "\\.", + "\\0", + "E0", + "I0", + "W0", + "\\;", + "\\>", + "\\A", + "\\K", + "\\W", + "\\Y", + "KY", + "YY", + "\\^", + "\\_", + "\\m", + "\\n", + "\\s", + "\\u", + "\\v", + "\\x" + ] + }, + { + "key": "my-symbol-2", + "label": "My Symbol 2", + "type": "select", + "options": [ + "/#", + "/&", + "/'", + "/-", + "/.", + "/0", + "/:", + "/;", + "/<", + "/=", + "/>", + "/C", + "/E", + "/I", + "/K", + "/O", + "/P", + "/R", + "/T", + "/U", + "/V", + "/W", + "/X", + "/Y", + "/[", + "/\\", + "/^", + "/_", + "/a", + "/b", + "/f", + "/g", + "/j", + "/k", + "/m", + "/r", + "/s", + "/u", + "/v", + "/y", + "\\#", + "\\&", + "\\-", + "\\.", + "\\0", + "E0", + "I0", + "W0", + "\\;", + "\\>", + "\\A", + "\\K", + "\\W", + "\\Y", + "KY", + "YY", + "\\^", + "\\_", + "\\m", + "\\n", + "\\s", + "\\u", + "\\v", + "\\x" + ] + }, + { + "key": "my-symbol-3", + "label": "My Symbol 3", + "type": "select", + "options": [ + "/#", + "/&", + "/'", + "/-", + "/.", + "/0", + "/:", + "/;", + "/<", + "/=", + "/>", + "/C", + "/E", + "/I", + "/K", + "/O", + "/P", + "/R", + "/T", + "/U", + "/V", + "/W", + "/X", + "/Y", + "/[", + "/\\", + "/^", + "/_", + "/a", + "/b", + "/f", + "/g", + "/j", + "/k", + "/m", + "/r", + "/s", + "/u", + "/v", + "/y", + "\\#", + "\\&", + "\\-", + "\\.", + "\\0", + "E0", + "I0", + "W0", + "\\;", + "\\>", + "\\A", + "\\K", + "\\W", + "\\Y", + "KY", + "YY", + "\\^", + "\\_", + "\\m", + "\\n", + "\\s", + "\\u", + "\\v", + "\\x" + ] + }, + { + "key": "my-symbol-4-table", + "label": "My Symbol 4 Table", + "type": "select", + "options": [ + "/", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "\\" + ] + }, + { + "key": "section-aprs-beacon", + "label": "APRS Beacon", + "type": "section", + "group": "APRS" + }, + { + "key": "beacon-auto", + "label": "Beacon Auto", + "type": "boolean" + }, + { + "key": "beacon-interval", + "label": "Beacon Interval", + "type": "select", + "options": [ + "30 sec", + "1 min", + "2 min", + "3 min", + "5 min", + "10 min", + "15 min", + "20 min", + "30 min", + "60 min" + ] + }, + { + "key": "beacon-proportional", + "label": "Proportional", + "type": "boolean" + }, + { + "key": "beacon-decay", + "label": "Decay", + "type": "boolean" + }, + { + "key": "beacon-low-speed", + "label": "Low Speed", + "type": "select", + "options": [ + "1mph", + "2mph", + "3mph", + "4mph", + "5mph", + "6mph", + "7mph", + "8mph", + "9mph", + "10mph", + "11mph", + "12mph", + "13mph", + "14mph", + "15mph", + "16mph", + "17mph", + "18mph", + "19mph", + "20mph", + "21mph", + "22mph", + "23mph", + "24mph", + "25mph", + "26mph", + "27mph", + "28mph", + "29mph", + "30mph", + "31mph", + "32mph", + "33mph", + "34mph", + "35mph", + "36mph", + "37mph", + "38mph", + "39mph", + "40mph", + "41mph", + "42mph", + "43mph", + "44mph", + "45mph", + "46mph", + "47mph", + "48mph", + "49mph", + "50mph", + "51mph", + "52mph", + "53mph", + "54mph", + "55mph", + "56mph", + "57mph", + "58mph", + "59mph", + "60mph", + "61mph", + "62mph", + "63mph", + "64mph", + "65mph", + "66mph", + "67mph", + "68mph", + "69mph", + "70mph", + "71mph", + "72mph", + "73mph", + "74mph", + "75mph", + "76mph", + "77mph", + "78mph", + "79mph", + "80mph", + "81mph", + "82mph", + "83mph", + "84mph", + "85mph", + "86mph", + "87mph", + "88mph", + "89mph", + "90mph", + "91mph", + "92mph", + "93mph", + "94mph", + "95mph", + "96mph", + "97mph", + "98mph", + "99mph" + ] + }, + { + "key": "beacon-rate-limit", + "label": "Rate Limit", + "type": "select", + "options": [ + "5 sec", + "6 sec", + "7 sec", + "8 sec", + "9 sec", + "10 sec", + "11 sec", + "12 sec", + "13 sec", + "14 sec", + "15 sec", + "16 sec", + "17 sec", + "18 sec", + "19 sec", + "20 sec", + "21 sec", + "22 sec", + "23 sec", + "24 sec", + "25 sec", + "26 sec", + "27 sec", + "28 sec", + "29 sec", + "30 sec", + "31 sec", + "32 sec", + "33 sec", + "34 sec", + "35 sec", + "36 sec", + "37 sec", + "38 sec", + "39 sec", + "40 sec", + "41 sec", + "42 sec", + "43 sec", + "44 sec", + "45 sec", + "46 sec", + "47 sec", + "48 sec", + "49 sec", + "50 sec", + "51 sec", + "52 sec", + "53 sec", + "54 sec", + "55 sec", + "56 sec", + "57 sec", + "58 sec", + "59 sec", + "60 sec", + "61 sec", + "62 sec", + "63 sec", + "64 sec", + "65 sec", + "66 sec", + "67 sec", + "68 sec", + "69 sec", + "70 sec", + "71 sec", + "72 sec", + "73 sec", + "74 sec", + "75 sec", + "76 sec", + "77 sec", + "78 sec", + "79 sec", + "80 sec", + "81 sec", + "82 sec", + "83 sec", + "84 sec", + "85 sec", + "86 sec", + "87 sec", + "88 sec", + "89 sec", + "90 sec", + "91 sec", + "92 sec", + "93 sec", + "94 sec", + "95 sec", + "96 sec", + "97 sec", + "98 sec", + "99 sec", + "100 sec", + "101 sec", + "102 sec", + "103 sec", + "104 sec", + "105 sec", + "106 sec", + "107 sec", + "108 sec", + "109 sec", + "110 sec", + "111 sec", + "112 sec", + "113 sec", + "114 sec", + "115 sec", + "116 sec", + "117 sec", + "118 sec", + "119 sec", + "120 sec", + "121 sec", + "122 sec", + "123 sec", + "124 sec", + "125 sec", + "126 sec", + "127 sec", + "128 sec", + "129 sec", + "130 sec", + "131 sec", + "132 sec", + "133 sec", + "134 sec", + "135 sec", + "136 sec", + "137 sec", + "138 sec", + "139 sec", + "140 sec", + "141 sec", + "142 sec", + "143 sec", + "144 sec", + "145 sec", + "146 sec", + "147 sec", + "148 sec", + "149 sec", + "150 sec", + "151 sec", + "152 sec", + "153 sec", + "154 sec", + "155 sec", + "156 sec", + "157 sec", + "158 sec", + "159 sec", + "160 sec", + "161 sec", + "162 sec", + "163 sec", + "164 sec", + "165 sec", + "166 sec", + "167 sec", + "168 sec", + "169 sec", + "170 sec", + "171 sec", + "172 sec", + "173 sec", + "174 sec", + "175 sec", + "176 sec", + "177 sec", + "178 sec", + "179 sec", + "180 sec" + ] + }, + { + "key": "beacon-ambiguity", + "label": "Ambiguity", + "type": "select", + "options": [ + "OFF", + "1digit", + "2digits", + "3digits", + "4digits" + ] + }, + { + "key": "beacon-speed-course", + "label": "Speed/Course", + "type": "boolean" + }, + { + "key": "beacon-altitude", + "label": "Altitude", + "type": "boolean" + }, + { + "key": "beacon-status-text", + "label": "Beacon Status Text", + "type": "select", + "options": [ + "OFF", + "1", + "2", + "3", + "4", + "5" + ] + }, + { + "key": "beacon-status-tx-rate", + "label": "Status TX Rate", + "type": "select", + "options": [ + "1/1", + "1/2", + "1/3", + "1/4", + "1/5", + "1/6", + "1/7", + "1/8", + "1/2(FREQ)", + "1/3(FREQ)", + "1/4(FREQ)", + "1/5(FREQ)", + "1/6(FREQ)", + "1/7(FREQ)", + "1/8(FREQ)" + ] + }, + { + "key": "beacon-status-text-1", + "label": "Status Text 1", + "type": "text", + "max_length": 60 + }, + { + "key": "beacon-status-text-1-freq", + "label": "Status Text 1 Frequency", + "type": "select", + "options": [ + "NONE", + "FREQUENCY", + "FREQ & SQL & SHIFT" + ] + }, + { + "key": "beacon-status-text-2", + "label": "Status Text 2", + "type": "text", + "max_length": 60 + }, + { + "key": "beacon-status-text-2-freq", + "label": "Status Text 2 Frequency", + "type": "select", + "options": [ + "NONE", + "FREQUENCY", + "FREQ & SQL & SHIFT" + ] + }, + { + "key": "beacon-status-text-3", + "label": "Status Text 3", + "type": "text", + "max_length": 60 + }, + { + "key": "beacon-status-text-3-freq", + "label": "Status Text 3 Frequency", + "type": "select", + "options": [ + "NONE", + "FREQUENCY", + "FREQ & SQL & SHIFT" + ] + }, + { + "key": "beacon-status-text-4", + "label": "Status Text 4", + "type": "text", + "max_length": 60 + }, + { + "key": "beacon-status-text-4-freq", + "label": "Status Text 4 Frequency", + "type": "select", + "options": [ + "NONE", + "FREQUENCY", + "FREQ & SQL & SHIFT" + ] + }, + { + "key": "beacon-status-text-5", + "label": "Status Text 5", + "type": "text", + "max_length": 60 + }, + { + "key": "beacon-status-text-5-freq", + "label": "Status Text 5 Frequency", + "type": "select", + "options": [ + "NONE", + "FREQUENCY", + "FREQ & SQL & SHIFT" + ] + }, + { + "key": "section-digi-path", + "label": "Digi Path", + "type": "section", + "group": "APRS" + }, + { + "key": "digi-path", + "label": "Digi Path", + "type": "select", + "options": [ + "OFF", + "WIDE1-1", + "WIDE1-1, WIDE2-1", + "PATH1", + "PATH2", + "PATH3", + "PATH4", + "FULL1", + "FULL2" + ] + }, + { + "key": "digi-path1-1", + "label": "Digi PATH1-1", + "type": "text", + "max_length": 6 + }, + { + "key": "digi-path1-1-ssid", + "label": "Digi PATH1-1 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "digi-path1-2", + "label": "Digi PATH1-2", + "type": "text", + "max_length": 6 + }, + { + "key": "digi-path1-2-ssid", + "label": "Digi PATH1-2 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "digi-path2-1", + "label": "Digi PATH2-1", + "type": "text", + "max_length": 6 + }, + { + "key": "digi-path2-1-ssid", + "label": "Digi PATH2-1 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "digi-path2-2", + "label": "Digi PATH2-2", + "type": "text", + "max_length": 6 + }, + { + "key": "digi-path2-2-ssid", + "label": "Digi PATH2-2 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "digi-path3-1", + "label": "Digi PATH3-1", + "type": "text", + "max_length": 6 + }, + { + "key": "digi-path3-1-ssid", + "label": "Digi PATH3-1 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "digi-path3-2", + "label": "Digi PATH3-2", + "type": "text", + "max_length": 6 + }, + { + "key": "digi-path3-2-ssid", + "label": "Digi PATH3-2 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "digi-path4-1", + "label": "Digi PATH4-1", + "type": "text", + "max_length": 6 + }, + { + "key": "digi-path4-1-ssid", + "label": "Digi PATH4-1 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "digi-path4-2", + "label": "Digi PATH4-2", + "type": "text", + "max_length": 6 + }, + { + "key": "digi-path4-2-ssid", + "label": "Digi PATH4-2 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "digi-full1-1", + "label": "Digi FULL1-1", + "type": "text", + "max_length": 6 + }, + { + "key": "digi-full1-1-ssid", + "label": "Digi FULL1-1 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "digi-full1-2", + "label": "Digi FULL1-2", + "type": "text", + "max_length": 6 + }, + { + "key": "digi-full1-2-ssid", + "label": "Digi FULL1-2 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "digi-full1-3", + "label": "Digi FULL1-3", + "type": "text", + "max_length": 6 + }, + { + "key": "digi-full1-3-ssid", + "label": "Digi FULL1-3 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "digi-full1-4", + "label": "Digi FULL1-4", + "type": "text", + "max_length": 6 + }, + { + "key": "digi-full1-4-ssid", + "label": "Digi FULL1-4 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "digi-full1-5", + "label": "Digi FULL1-5", + "type": "text", + "max_length": 6 + }, + { + "key": "digi-full1-5-ssid", + "label": "Digi FULL1-5 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "digi-full1-6", + "label": "Digi FULL1-6", + "type": "text", + "max_length": 6 + }, + { + "key": "digi-full1-6-ssid", + "label": "Digi FULL1-6 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "digi-full1-7", + "label": "Digi FULL1-7", + "type": "text", + "max_length": 6 + }, + { + "key": "digi-full1-7-ssid", + "label": "Digi FULL1-7 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "digi-full1-8", + "label": "Digi FULL1-8", + "type": "text", + "max_length": 6 + }, + { + "key": "digi-full1-8-ssid", + "label": "Digi FULL1-8 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "digi-full2-1", + "label": "Digi FULL2-1", + "type": "text", + "max_length": 6 + }, + { + "key": "digi-full2-1-ssid", + "label": "Digi FULL2-1 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "digi-full2-2", + "label": "Digi FULL2-2", + "type": "text", + "max_length": 6 + }, + { + "key": "digi-full2-2-ssid", + "label": "Digi FULL2-2 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "digi-full2-3", + "label": "Digi FULL2-3", + "type": "text", + "max_length": 6 + }, + { + "key": "digi-full2-3-ssid", + "label": "Digi FULL2-3 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "digi-full2-4", + "label": "Digi FULL2-4", + "type": "text", + "max_length": 6 + }, + { + "key": "digi-full2-4-ssid", + "label": "Digi FULL2-4 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "digi-full2-5", + "label": "Digi FULL2-5", + "type": "text", + "max_length": 6 + }, + { + "key": "digi-full2-5-ssid", + "label": "Digi FULL2-5 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "digi-full2-6", + "label": "Digi FULL2-6", + "type": "text", + "max_length": 6 + }, + { + "key": "digi-full2-6-ssid", + "label": "Digi FULL2-6 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "digi-full2-7", + "label": "Digi FULL2-7", + "type": "text", + "max_length": 6 + }, + { + "key": "digi-full2-7-ssid", + "label": "Digi FULL2-7 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "digi-full2-8", + "label": "Digi FULL2-8", + "type": "text", + "max_length": 6 + }, + { + "key": "digi-full2-8-ssid", + "label": "Digi FULL2-8 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "section-aprs-messages", + "label": "APRS Messages", + "type": "section", + "group": "APRS" + }, + { + "key": "aprs-message-text", + "label": "Message Text", + "type": "select", + "options": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8" + ] + }, + { + "key": "aprs-message-text-1", + "label": "Message Text 1", + "type": "text", + "max_length": 16 + }, + { + "key": "aprs-message-text-2", + "label": "Message Text 2", + "type": "text", + "max_length": 16 + }, + { + "key": "aprs-message-text-3", + "label": "Message Text 3", + "type": "text", + "max_length": 16 + }, + { + "key": "aprs-message-text-4", + "label": "Message Text 4", + "type": "text", + "max_length": 16 + }, + { + "key": "aprs-message-text-5", + "label": "Message Text 5", + "type": "text", + "max_length": 16 + }, + { + "key": "aprs-message-text-6", + "label": "Message Text 6", + "type": "text", + "max_length": 16 + }, + { + "key": "aprs-message-text-7", + "label": "Message Text 7", + "type": "text", + "max_length": 16 + }, + { + "key": "aprs-message-text-8", + "label": "Message Text 8", + "type": "text", + "max_length": 16 + }, + { + "key": "aprs-message-group-1", + "label": "Message Group 1", + "type": "text", + "max_length": 9 + }, + { + "key": "aprs-message-group-2", + "label": "Message Group 2", + "type": "text", + "max_length": 9 + }, + { + "key": "aprs-message-group-3", + "label": "Message Group 3", + "type": "text", + "max_length": 9 + }, + { + "key": "aprs-message-group-4", + "label": "Message Group 4", + "type": "text", + "max_length": 9 + }, + { + "key": "aprs-message-group-5", + "label": "Message Group 5", + "type": "text", + "max_length": 9 + }, + { + "key": "aprs-message-group-6", + "label": "Message Group 6", + "type": "text", + "max_length": 9 + }, + { + "key": "aprs-bulletin-1", + "label": "Bulletin 1", + "type": "text", + "max_length": 9 + }, + { + "key": "aprs-bulletin-2", + "label": "Bulletin 2", + "type": "text", + "max_length": 9 + }, + { + "key": "aprs-bulletin-3", + "label": "Bulletin 3", + "type": "text", + "max_length": 9 + }, + { + "key": "message-reply", + "label": "Message Reply", + "type": "boolean" + }, + { + "key": "message-reply-text", + "label": "Reply Text", + "type": "text", + "max_length": 64 + }, + { + "key": "section-aprs-popup-filter", + "label": "APRS Popup & Filter", + "type": "section", + "group": "APRS" + }, + { + "key": "aprs-popup-beacon", + "label": "Beacon Popup", + "type": "select", + "options": [ + "OFF", + "3 sec", + "5 sec", + "10 sec", + "HOLD", + "CALL 3 sec", + "CALL 5 sec", + "CALL 10 sec", + "CALL HOLD" + ] + }, + { + "key": "aprs-popup-message", + "label": "Message Popup", + "type": "select", + "options": [ + "OFF", + "3 sec", + "5 sec", + "10 sec", + "HOLD", + "CALL 3 sec", + "CALL 5 sec", + "CALL 10 sec", + "CALL HOLD" + ] + }, + { + "key": "aprs-popup-my-packet", + "label": "My Packet Popup", + "type": "boolean" + }, + { + "key": "aprs-sort", + "label": "Sort", + "type": "select", + "options": [ + "TIME", + "CALLSIGN", + "DISTANCE" + ] + }, + { + "key": "aprs-filter", + "label": "Filter", + "type": "select", + "options": [ + "ALL", + "MOBILE", + "FREQUENCY", + "OBJECT/ITEM", + "DIGIPEATER", + "VoIP", + "WEATHER", + "YAESU", + "OTHER PACKET", + "CALL RINGER", + "RANGE RINGER", + "1200 bps", + "9600 bps" + ] + }, + { + "key": "voice-alert", + "label": "Voice Alert", + "type": "select", + "options": [ + "NORMAL", + "TONE SQL", + "DCS", + "RX-TSQL", + "RX-DCS" + ] + }, + { + "key": "voice-alert-tone", + "label": "Voice Alert Tone", + "type": "select", + "options": [ + "67.0Hz", + "69.3Hz", + "71.9Hz", + "74.4Hz", + "77.0Hz", + "79.7Hz", + "82.5Hz", + "85.4Hz", + "88.5Hz", + "91.5Hz", + "94.8Hz", + "97.4Hz", + "100.0Hz", + "103.5Hz", + "107.2Hz", + "110.9Hz", + "114.8Hz", + "118.8Hz", + "123.0Hz", + "127.3Hz", + "131.8Hz", + "136.5Hz", + "141.3Hz", + "146.2Hz", + "151.4Hz", + "156.7Hz", + "159.8Hz", + "162.2Hz", + "165.5Hz", + "167.9Hz", + "171.3Hz", + "173.8Hz", + "177.3Hz", + "179.9Hz", + "183.5Hz", + "186.2Hz", + "189.9Hz", + "192.8Hz", + "196.6Hz", + "199.5Hz", + "203.5Hz", + "206.5Hz", + "210.7Hz", + "218.1Hz", + "225.7Hz", + "229.1Hz", + "233.6Hz", + "241.8Hz", + "250.3Hz", + "254.1Hz" + ] + }, + { + "key": "voice-alert-dcs", + "label": "Voice Alert DCS", + "type": "select", + "options": [ + "023", + "025", + "026", + "031", + "032", + "036", + "043", + "047", + "051", + "053", + "054", + "065", + "071", + "072", + "073", + "074", + "114", + "115", + "116", + "122", + "125", + "131", + "132", + "134", + "143", + "145", + "152", + "155", + "156", + "162", + "165", + "172", + "174", + "205", + "212", + "223", + "225", + "226", + "243", + "244", + "245", + "246", + "251", + "252", + "255", + "261", + "263", + "265", + "266", + "271", + "274", + "306", + "311", + "315", + "325", + "331", + "332", + "343", + "346", + "351", + "356", + "364", + "365", + "371", + "411", + "412", + "413", + "423", + "431", + "432", + "445", + "446", + "452", + "454", + "455", + "462", + "464", + "465", + "466", + "503", + "506", + "516", + "523", + "526", + "532", + "546", + "565", + "606", + "612", + "624", + "627", + "631", + "632", + "654", + "662", + "664", + "703", + "712", + "723", + "731", + "732", + "734", + "743", + "754" + ] + }, + { + "key": "aprs-filter-mic-e", + "label": "Filter MIC-E", + "type": "boolean" + }, + { + "key": "aprs-filter-position", + "label": "Filter Position", + "type": "boolean" + }, + { + "key": "aprs-filter-weather", + "label": "Filter Weather", + "type": "boolean" + }, + { + "key": "aprs-filter-object", + "label": "Filter Object", + "type": "boolean" + }, + { + "key": "aprs-filter-item", + "label": "Filter Item", + "type": "boolean" + }, + { + "key": "aprs-filter-status", + "label": "Filter Status", + "type": "boolean" + }, + { + "key": "aprs-filter-other", + "label": "Filter Other", + "type": "boolean" + }, + { + "key": "aprs-filter-altnet", + "label": "Filter Altnet", + "type": "boolean" + }, + { + "key": "aprs-range-limit", + "label": "Range Limit", + "type": "select", + "options": [ + "OFF", + "1 km(mi)", + "10 km(mi)", + "100 km(mi)", + "1000 km(mi)", + "3000 km(mi)" + ] + }, + { + "key": "section-aprs-ringer", + "label": "APRS Ringer", + "type": "section", + "group": "APRS" + }, + { + "key": "aprs-ringer-tx-beacon", + "label": "TX Beacon", + "type": "boolean" + }, + { + "key": "aprs-ringer-tx-message", + "label": "TX Message", + "type": "boolean" + }, + { + "key": "aprs-ringer-rx-beacon", + "label": "RX Beacon", + "type": "boolean" + }, + { + "key": "aprs-ringer-rx-message", + "label": "RX Message", + "type": "boolean" + }, + { + "key": "aprs-ringer-my-packet", + "label": "My Packet", + "type": "boolean" + }, + { + "key": "aprs-ringer-call-ringer", + "label": "Call Ringer", + "type": "boolean" + }, + { + "key": "aprs-ringer-msg-voice", + "label": "MSG Voice", + "type": "boolean" + }, + { + "key": "aprs-ringer-range", + "label": "Range Ringer", + "type": "select", + "options": [ + "OFF", + "1", + "5", + "10", + "50", + "100" + ] + }, + { + "key": "aprs-ringer-call-1", + "label": "Ringer Call 1", + "type": "text", + "max_length": 6 + }, + { + "key": "aprs-ringer-call-1-ssid", + "label": "Ringer Call 1 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "aprs-ringer-call-2", + "label": "Ringer Call 2", + "type": "text", + "max_length": 6 + }, + { + "key": "aprs-ringer-call-2-ssid", + "label": "Ringer Call 2 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "aprs-ringer-call-3", + "label": "Ringer Call 3", + "type": "text", + "max_length": 6 + }, + { + "key": "aprs-ringer-call-3-ssid", + "label": "Ringer Call 3 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "aprs-ringer-call-4", + "label": "Ringer Call 4", + "type": "text", + "max_length": 6 + }, + { + "key": "aprs-ringer-call-4-ssid", + "label": "Ringer Call 4 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "aprs-ringer-call-5", + "label": "Ringer Call 5", + "type": "text", + "max_length": 6 + }, + { + "key": "aprs-ringer-call-5-ssid", + "label": "Ringer Call 5 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "aprs-ringer-call-6", + "label": "Ringer Call 6", + "type": "text", + "max_length": 6 + }, + { + "key": "aprs-ringer-call-6-ssid", + "label": "Ringer Call 6 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "aprs-ringer-call-7", + "label": "Ringer Call 7", + "type": "text", + "max_length": 6 + }, + { + "key": "aprs-ringer-call-7-ssid", + "label": "Ringer Call 7 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "aprs-ringer-call-8", + "label": "Ringer Call 8", + "type": "text", + "max_length": 6 + }, + { + "key": "aprs-ringer-call-8-ssid", + "label": "Ringer Call 8 SSID", + "type": "select", + "options": [ + "(none)", + "-1", + "-2", + "-3", + "-4", + "-5", + "-6", + "-7", + "-8", + "-9", + "-10", + "-11", + "-12", + "-13", + "-14", + "-15" + ] + }, + { + "key": "section-aprs-units", + "label": "APRS Units", + "type": "section", + "group": "APRS" + }, + { + "key": "aprs-unit-position", + "label": "Position", + "type": "select", + "options": [ + "dd°mm'ss\"", + "dd°mm.mm'" + ] + }, + { + "key": "aprs-unit-distance", + "label": "Distance", + "type": "select", + "options": [ + "km", + "mile" + ] + }, + { + "key": "aprs-unit-speed", + "label": "Speed", + "type": "select", + "options": [ + "km/h", + "mph", + "knot" + ] + }, + { + "key": "aprs-unit-altitude", + "label": "Altitude", + "type": "select", + "options": [ + "m", + "ft" + ] + }, + { + "key": "aprs-unit-baro", + "label": "Baro", + "type": "select", + "options": [ + "hPa", + "mb", + "mmHg", + "inHg" + ] + }, + { + "key": "aprs-unit-temp", + "label": "Temp", + "type": "select", + "options": [ + "°C", + "F" + ] + }, + { + "key": "aprs-unit-rain", + "label": "Rain", + "type": "select", + "options": [ + "mm", + "inch" + ] + }, + { + "key": "aprs-unit-wind", + "label": "Wind", + "type": "select", + "options": [ + "m/s", + "mph", + "knot" + ] + }, + { + "key": "section-smartbeaconing", + "label": "SmartBeaconing", + "type": "section", + "group": "APRS" + }, + { + "key": "smartbeaconing", + "label": "SmartBeaconing", + "type": "select", + "options": [ + "OFF", + "TYPE1", + "TYPE2", + "TYPE3" + ] + }, + { + "key": "smartbeaconing-type-1-low-speed", + "label": "Type 1 Low Speed", + "type": "integer", + "min": 2, + "max": 30 + }, + { + "key": "smartbeaconing-type-1-high-speed", + "label": "Type 1 High Speed", + "type": "integer", + "min": 3, + "max": 90 + }, + { + "key": "smartbeaconing-type-1-slow-rate", + "label": "Type 1 Slow Rate (min)", + "type": "integer", + "min": 1, + "max": 100 + }, + { + "key": "smartbeaconing-type-1-fast-rate", + "label": "Type 1 Fast Rate (sec)", + "type": "integer", + "min": 10, + "max": 180 + }, + { + "key": "smartbeaconing-type-1-turn-angle", + "label": "Type 1 Turn Angle (°)", + "type": "integer", + "min": 5, + "max": 90 + }, + { + "key": "smartbeaconing-type-1-turn-slope", + "label": "Type 1 Turn Slope", + "type": "integer", + "min": 1, + "max": 255 + }, + { + "key": "smartbeaconing-type-1-turn-time", + "label": "Type 1 Turn Time (sec)", + "type": "integer", + "min": 5, + "max": 180 + }, + { + "key": "smartbeaconing-type-2-low-speed", + "label": "Type 2 Low Speed", + "type": "integer", + "min": 2, + "max": 30 + }, + { + "key": "smartbeaconing-type-2-high-speed", + "label": "Type 2 High Speed", + "type": "integer", + "min": 3, + "max": 90 + }, + { + "key": "smartbeaconing-type-2-slow-rate", + "label": "Type 2 Slow Rate (min)", + "type": "integer", + "min": 1, + "max": 100 + }, + { + "key": "smartbeaconing-type-2-fast-rate", + "label": "Type 2 Fast Rate (sec)", + "type": "integer", + "min": 10, + "max": 180 + }, + { + "key": "smartbeaconing-type-2-turn-angle", + "label": "Type 2 Turn Angle (°)", + "type": "integer", + "min": 5, + "max": 90 + }, + { + "key": "smartbeaconing-type-2-turn-slope", + "label": "Type 2 Turn Slope", + "type": "integer", + "min": 1, + "max": 255 + }, + { + "key": "smartbeaconing-type-2-turn-time", + "label": "Type 2 Turn Time (sec)", + "type": "integer", + "min": 5, + "max": 180 + }, + { + "key": "smartbeaconing-type-3-low-speed", + "label": "Type 3 Low Speed", + "type": "integer", + "min": 2, + "max": 30 + }, + { + "key": "smartbeaconing-type-3-high-speed", + "label": "Type 3 High Speed", + "type": "integer", + "min": 3, + "max": 90 + }, + { + "key": "smartbeaconing-type-3-slow-rate", + "label": "Type 3 Slow Rate (min)", + "type": "integer", + "min": 1, + "max": 100 + }, + { + "key": "smartbeaconing-type-3-fast-rate", + "label": "Type 3 Fast Rate (sec)", + "type": "integer", + "min": 10, + "max": 180 + }, + { + "key": "smartbeaconing-type-3-turn-angle", + "label": "Type 3 Turn Angle (°)", + "type": "integer", + "min": 5, + "max": 90 + }, + { + "key": "smartbeaconing-type-3-turn-slope", + "label": "Type 3 Turn Slope", + "type": "integer", + "min": 1, + "max": 255 + }, + { + "key": "smartbeaconing-type-3-turn-time", + "label": "Type 3 Turn Time (sec)", + "type": "integer", + "min": 5, + "max": 180 + } +] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index bdef575..e6a67a0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -234,6 +234,7 @@ pub fn run() { commands::program::write_card_settings, commands::program::read_thd75_settings_from_card, commands::program::read_ftm300_settings_from_backup, + commands::program::read_ftm510_settings_from_backup, commands::codeplugs::get_codeplug_memory_group, commands::codeplugs::set_codeplug_memory_group, commands::program::find_memory_cards, diff --git a/src-tauri/src/radios/mod.rs b/src-tauri/src/radios/mod.rs index 531ef65..cb45a83 100644 --- a/src-tauri/src/radios/mod.rs +++ b/src-tauri/src/radios/mod.rs @@ -58,3 +58,4 @@ pub(crate) mod tyt_md380; mod wiring; pub(crate) mod yaesu_ft5d; pub(crate) mod yaesu_ftm300; +pub(crate) mod yaesu_ftm510; diff --git a/src-tauri/src/radios/registry.rs b/src-tauri/src/radios/registry.rs index 720b3d5..7664dcd 100644 --- a/src-tauri/src/radios/registry.rs +++ b/src-tauri/src/radios/registry.rs @@ -23,7 +23,7 @@ use crate::models::RadioModel; /// Every driver compiled into the app. Order is not significant — lookups are /// by `key()`, which is unique. (A static array rather than a slice literal: /// references to statics aren't const-promotable inside a returned temporary.) -static DRIVERS: [&dyn RadioDriver; 13] = [ +static DRIVERS: [&dyn RadioDriver; 14] = [ &super::baofeng_uv5r::DRIVER, &super::binteradio_bt9000::DRIVER, &super::tidradio_tdh3::DRIVER, @@ -37,6 +37,7 @@ static DRIVERS: [&dyn RadioDriver; 13] = [ &super::icom_id5100::DRIVER, &super::tyt_md380::DRIVER, &super::yaesu_ftm300::DRIVER, + &super::yaesu_ftm510::DRIVER, ]; pub(crate) fn all_drivers() -> &'static [&'static dyn RadioDriver] { @@ -91,6 +92,7 @@ mod tests { "kenwood_thd72", "kenwood_tmd710", "yaesu_ftm300", + "yaesu_ftm510", ] { let d = driver_for_key(key).unwrap_or_else(|| panic!("no driver for '{key}'")); assert_eq!(d.key(), key); @@ -125,6 +127,8 @@ mod tests { "yaesu_ft5d" | "icom_id52" | "kenwood_thd75" => (false, false), // FTM-300 (#48): the fourth card radio, same reason. "yaesu_ftm300" => (false, false), + // FTM-510 (#151): the FTM-300's sibling, programmed the same way. + "yaesu_ftm510" => (false, false), // TH-D72: reads and writes its 19 menu parameters over the // `MU` ASCII command — no clone session involved, which is why // it claims both halves where the card radios claim neither. diff --git a/src-tauri/src/radios/yaesu_ftm300/sd_image.rs b/src-tauri/src/radios/yaesu_ftm300/sd_image.rs index d509a17..0b9917b 100644 --- a/src-tauri/src/radios/yaesu_ftm300/sd_image.rs +++ b/src-tauri/src/radios/yaesu_ftm300/sd_image.rs @@ -364,7 +364,7 @@ fn name_len_code(len: usize) -> u16 { /// Printable ASCII, trailing spaces dropped (they would count toward the /// length code), at most 16 characters. Lower case is kept — the radio stores /// and displays it. -pub(super) fn tag(name: &str) -> String { +pub(crate) fn tag(name: &str) -> String { let s: String = name .chars() .filter(|c| c.is_ascii_graphic() || *c == ' ') @@ -374,7 +374,7 @@ pub(super) fn tag(name: &str) -> String { } /// A cleaned tag's two stored forms: the 16-byte name field and its length code. -pub(super) fn name_fields(name: &str) -> ([u8; NAME_LEN], u16) { +pub(crate) fn name_fields(name: &str) -> ([u8; NAME_LEN], u16) { let mut field = [0xFFu8; NAME_LEN]; field[..name.len()].copy_from_slice(name.as_bytes()); (field, name_len_code(name.len())) diff --git a/src-tauri/src/radios/yaesu_ftm510/dev_export.rs b/src-tauri/src/radios/yaesu_ftm510/dev_export.rs new file mode 100644 index 0000000..39a80b3 --- /dev/null +++ b/src-tauri/src/radios/yaesu_ftm510/dev_export.rs @@ -0,0 +1,87 @@ +//! THROWAWAY (#151, step 4): run the app's own export pipeline against the dev +//! database and write a `CLNFTM510D.dat` from a real codeplug. +//! +//! A harness, not a unit test: it needs Tim's dev SQLite DB and his radio's own +//! backup, neither of which is in the repo. The channels come from any codeplug +//! (no FTM-510 codeplug exists until the app has seeded the model), filtered +//! through the FTM-510's own seeded row exactly as `generate_codeplug` would. +//! +//! ```sh +//! CPM_DEV_DB="$HOME/Library/Application Support/com.ww8l.codeplugmagic.dev/codeplug_manager.sqlite3" \ +//! CPM_CODEPLUG=3 FTM510_DAT=scratchpad/yaesu_ftm510/captures/ftm510_01_new_radio_callsign.dat \ +//! cargo test --lib yaesu_ftm510::dev_export -- --ignored --nocapture +//! ``` + +use crate::commands::export::{exclusion_reason, expand_for_export, resolve_codeplug_groups, ExpandedChannel}; +use crate::radios::driver::ExportRequest; + +#[tokio::test] +#[ignore = "needs the dev database and a real CLNFTM510D.dat"] +async fn a_real_codeplug_exports_through_the_app_pipeline() { + let db = std::env::var("CPM_DEV_DB").expect("CPM_DEV_DB"); + let codeplug_id: i64 = std::env::var("CPM_CODEPLUG").expect("CPM_CODEPLUG").parse().unwrap(); + let dat = std::env::var("FTM510_DAT").expect("FTM510_DAT"); + let pool = sqlx::SqlitePool::connect(&format!("sqlite:file:{db}?mode=ro")).await.expect("open dev db"); + + let model = crate::seed::test_model("FTM-510DR"); + let groups = resolve_codeplug_groups(&pool, codeplug_id).await.expect("groups"); + let mut seen = std::collections::HashSet::new(); + let channels: Vec<_> = groups + .iter() + .flat_map(|g| g.channels.iter().cloned()) + .filter(|c| seen.insert(c.id)) + .collect(); + let expanded = expand_for_export(&pool, channels).await.expect("expand"); + + let mut excluded = Vec::new(); + let included: Vec<&ExpandedChannel> = expanded + .iter() + .filter(|ec| match exclusion_reason(&ec.channel, &model) { + Some(why) => { + excluded.push(format!(" {:>10.4} {}", ec.channel.rx_freq, why)); + false + } + None => true, + }) + .collect(); + println!("{} channels in, {} included, {} excluded", expanded.len(), included.len(), excluded.len()); + for line in &excluded { + println!("{line}"); + } + + let dir = std::env::temp_dir().join("ftm510_dev_export"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let target = dir.join("CLNFTM510D.dat"); + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(".."); + std::fs::copy(root.join(&dat), &target).expect("copy template"); + + let exporter = crate::radios::registry::exporter_for_format("yaesu_ftm510_sd").expect("exporter"); + let n = exporter + .export( + target.to_str().unwrap(), + &ExportRequest { channels: &included, groups: &groups, model: &model, profile_settings: None, memory_group: &[] }, + ) + .expect("export"); + let out = std::fs::read(&target).unwrap(); + let template = std::fs::read(root.join(&dat)).unwrap(); + + // Count what landed, the way the radio decides: byte 0 bit 7 of each of the 999 records. + let used = (0..999).filter(|i| out[0x800 + i * 16] & 0x80 != 0).count(); + println!("wrote {n}; {used} records marked used"); + for (slot, ec) in included.iter().enumerate().take(8) { + let r = &out[0x800 + slot * 16..0x800 + slot * 16 + 16]; + let name = String::from_utf8_lossy(out[0x10000 + slot * 16..0x10010 + slot * 16].split(|&b| b == 0xFF).next().unwrap()).into_owned(); + println!(" {:3} {:>10.4} {:02x?} {name:?}", slot + 1, ec.channel.rx_freq, r); + } + assert_eq!(used, included.len()); + assert_eq!(n, included.len()); + // Everything outside the three memory tables is the template's. + let outside = |i: usize| !(0x800..0x800 + 999 * 16).contains(&i) + && !(0x4CB0..0x4CB0 + 999 * 2).contains(&i) + && !(0x10000..0x10000 + 999 * 16).contains(&i); + let strays: Vec = (0..out.len()).filter(|&i| outside(i) && out[i] != template[i]).collect(); + assert!(strays.is_empty(), "bytes outside the memory tables moved: {strays:x?}"); + println!("nothing outside the memory tables moved; settings, PMS, Home, APRS untouched"); +} + diff --git a/src-tauri/src/radios/yaesu_ftm510/ftm510_settings_table.rs b/src-tauri/src/radios/yaesu_ftm510/ftm510_settings_table.rs new file mode 100644 index 0000000..4519ec7 --- /dev/null +++ b/src-tauri/src/radios/yaesu_ftm510/ftm510_settings_table.rs @@ -0,0 +1,291 @@ +// GENERATED by scratchpad/yaesu_ftm510/adms/gen_settings.py from the ADMS-18 measurement +// campaign (MEASURED.md). Do not hand-edit; regenerate. +// +// Offsets are FILE offsets into CLNFTM510D.dat, each measured on ADMS-18's own writes of +// this radio's file. Labels are ADMS-18's option text. +pub(crate) const FTM510_SETTINGS_FIELDS: &[SF] = &[ + SF { key: "auto-power-off", byte: 0x000a4, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "OFF"), (1, "0.5 hour"), (2, "1.0 hour"), (3, "1.5 hour"), (4, "2.0 hour"), (6, "3.0 hour"), (8, "4.0 hour"), (10, "5.0 hour"), (12, "6.0 hour"), (14, "7.0 hour"), (16, "8.0 hour"), (18, "9.0 hour"), (20, "10.0 hour"), (22, "11.0 hour"), (24, "12.0 hour")] } }, + SF { key: "unit", byte: 0x000a1, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 1, labels: &[(0, "METRIC"), (1, "INCH")] } }, + SF { key: "date-format", byte: 0x000fa, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 3, labels: &[(2, "mmm/dd/yyyy"), (0, "yyyy/mmm/dd"), (4, "dd/mmm/yyyy"), (1, "yyyy/dd/mmm")] } }, + SF { key: "time-format", byte: 0x000fa, mirror: 0x00000, kind: SK::Enum { shift: 4, width: 1, labels: &[(0, "24 hour"), (1, "12 hour")] } }, + SF { key: "time-zone", byte: 0x000a8, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(156, "UTC-14:00"), (155, "UTC-13:30"), (154, "UTC-13:00"), (153, "UTC-12:30"), (152, "UTC-12:00"), (151, "UTC-11:30"), (150, "UTC-11:00"), (149, "UTC-10:30"), (148, "UTC-10:00"), (147, "UTC-09:30"), (146, "UTC-09:00"), (145, "UTC-08:30"), (144, "UTC-08:00"), (143, "UTC-07:30"), (142, "UTC-07:00"), (141, "UTC-06:30"), (140, "UTC-06:00"), (139, "UTC-05:30"), (138, "UTC-05:00"), (137, "UTC-04:30"), (136, "UTC-04:00"), (135, "UTC-03:30"), (134, "UTC-03:00"), (133, "UTC-02:30"), (132, "UTC-02:00"), (131, "UTC-01:30"), (130, "UTC-01:00"), (129, "UTC-00:30"), (0, "UTC±00:00"), (1, "UTC+00:30"), (2, "UTC+01:00"), (3, "UTC+01:30"), (4, "UTC+02:00"), (5, "UTC+02:30"), (6, "UTC+03:00"), (7, "UTC+03:30"), (8, "UTC+04:00"), (9, "UTC+04:30"), (10, "UTC+05:00"), (11, "UTC+05:30"), (12, "UTC+06:00"), (13, "UTC+06:30"), (14, "UTC+07:00"), (15, "UTC+07:30"), (16, "UTC+08:00"), (17, "UTC+08:30"), (18, "UTC+09:00"), (19, "UTC+09:30"), (20, "UTC+10:00"), (21, "UTC+10:30"), (22, "UTC+11:00"), (23, "UTC+11:30"), (24, "UTC+12:00"), (25, "UTC+12:30"), (26, "UTC+13:00"), (27, "UTC+13:30"), (28, "UTC+14:00")] } }, + SF { key: "callsign", byte: 0x002c8, mirror: 0x00000, kind: SK::Text { len: 10, pad: 0xff, set: TS::CallPunct } }, + SF { key: "beep", byte: 0x000eb, mirror: 0x00000, kind: SK::Bool { shift: 3, active_low: false } }, + SF { key: "beep-level", byte: 0x000fb, mirror: 0x00000, kind: SK::Enum { shift: 4, width: 1, labels: &[(0, "LOW"), (1, "HIGH")] } }, + SF { key: "gps-datum", byte: 0x000af, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(21, "WGS-84"), (178, "TOKYO MEAN")] } }, + SF { key: "gps-device", byte: 0x000f8, mirror: 0x00000, kind: SK::Enum { shift: 7, width: 1, labels: &[(0, "INTERNAL"), (1, "EXTERNAL")] } }, + SF { key: "gps-log", byte: 0x000b5, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "OFF"), (1, "1sec"), (2, "2sec"), (3, "5sec"), (4, "10sec"), (5, "30sec"), (6, "60sec")] } }, + SF { key: "display-select", byte: 0x000ad, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "BACKTRACK"), (1, "ALTITUDE"), (2, "TIMER/CLOCK"), (3, "GPS INFORMATION")] } }, + SF { key: "location-info", byte: 0x000e8, mirror: 0x00000, kind: SK::Enum { shift: 2, width: 1, labels: &[(0, "COMPASS"), (1, "NUMERIC")] } }, + SF { key: "compass", byte: 0x002d6, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 1, labels: &[(0, "NORTH UP"), (1, "HEADING UP")] } }, + SF { key: "a-band-scope", byte: 0x000e5, mirror: 0x00000, kind: SK::Enum { shift: 5, width: 1, labels: &[(0, "WIDE"), (1, "NARROW")] } }, + SF { key: "b-band-scope", byte: 0x000f5, mirror: 0x00000, kind: SK::Enum { shift: 5, width: 1, labels: &[(0, "WIDE"), (1, "NARROW")] } }, + SF { key: "lcd-brightness", byte: 0x0028f, mirror: 0x00000, kind: SK::Enum { shift: 1, width: 7, labels: &[(1, "MIN"), (2, "MID"), (3, "MAX")] } }, + SF { key: "display-color", byte: 0x000bc, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "WHITE"), (1, "BLUE"), (2, "RED")] } }, + SF { key: "time-out-timer", byte: 0x000aa, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "OFF"), (6, "1 min"), (7, "2 min"), (8, "3 min"), (1, "5 min"), (2, "10 min"), (3, "15 min"), (4, "20 min"), (5, "30 min")] } }, + SF { key: "mic-gain", byte: 0x002dd, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "MIN"), (1, "LOW"), (2, "NORMAL"), (3, "HIGH"), (4, "MAX")] } }, + SF { key: "vox", byte: 0x000b6, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "OFF"), (1, "LOW"), (2, "HIGH")] } }, + SF { key: "vox-delay", byte: 0x000b7, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "0.5s"), (1, "1s"), (2, "1.5s"), (3, "2s"), (4, "2.5s"), (5, "3s")] } }, + SF { key: "vox-mic", byte: 0x000b3, mirror: 0x00000, kind: SK::Enum { shift: 7, width: 1, labels: &[(0, "FRONT"), (1, "REAR")] } }, + SF { key: "sub-band", byte: 0x000f9, mirror: 0x00000, kind: SK::Bool { shift: 3, active_low: true } }, + SF { key: "sub-band-mute", byte: 0x000e9, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 2, labels: &[(0, "OFF"), (3, "ON")] } }, + SF { key: "s-dx", byte: 0x000fb, mirror: 0x00000, kind: SK::Bits { bits: &[(0x0fb, 5), (0x0f8, 0), (0x0e8, 3)], labels: &[(0, "OFF"), (2, "S-DX"), (3, "ASP"), (5, "AUTO")] } }, + SF { key: "a-rpt-ars", byte: 0x000e4, mirror: 0x00000, kind: SK::Enum { shift: 2, width: 1, labels: &[(0, "OFF"), (1, "AUTO")] } }, + SF { key: "b-rpt-ars", byte: 0x000f4, mirror: 0x00000, kind: SK::Enum { shift: 2, width: 1, labels: &[(0, "OFF"), (1, "AUTO")] } }, + SF { key: "front-tone", byte: 0x002c1, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "OFF"), (1, "ON")] } }, + SF { key: "front-tone-eq", byte: 0x002c2, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "FLAT"), (1, "HI PITCH"), (2, "LO PITCH"), (3, "BPF")] } }, + SF { key: "rear-tone", byte: 0x002c3, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "FLAT"), (1, "HI PITCH"), (2, "LO PITCH"), (3, "BPF"), (4, "1KHz LPF"), (5, "700Hz LPF")] } }, + SF { key: "rear-out", byte: 0x000ee, mirror: 0x000fe, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "0%"), (1, "10%"), (2, "20%"), (3, "30%"), (4, "40%"), (5, "50%"), (6, "60%"), (7, "70%"), (8, "80%"), (9, "90%"), (10, "100%")] } }, + SF { key: "aess-phase", byte: 0x002b7, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "OFF"), (1, "1.25ms"), (2, "2.50ms"), (3, "3.75ms"), (4, "5.00ms"), (5, "6.25ms"), (6, "7.50ms"), (7, "8.75ms"), (8, "10.00ms"), (9, "11.25ms"), (10, "12.50ms"), (11, "13.75ms"), (12, "15.00ms"), (13, "16.25ms"), (14, "17.50ms"), (15, "18.75ms"), (16, "20.00ms")] } }, + SF { key: "recording-band", byte: 0x000b8, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(1, "MAIN"), (2, "SUB"), (3, "MAIN+SUB")] } }, + SF { key: "recording-mic", byte: 0x000ea, mirror: 0x00000, kind: SK::Bool { shift: 3, active_low: false } }, + SF { key: "front-speaker-mute", byte: 0x000e8, mirror: 0x00000, kind: SK::Enum { shift: 6, width: 1, labels: &[(0, "CONTINUE"), (1, "AUTO MUTE")] } }, + SF { key: "voice-memory-play-rec", byte: 0x000fb, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 1, labels: &[(0, "FREE 5 min"), (1, "LAST 30 sec")] } }, + SF { key: "voice-announce", byte: 0x000ae, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "OFF"), (1, "MANUAL"), (2, "AUTO")] } }, + SF { key: "voice-language", byte: 0x000eb, mirror: 0x00000, kind: SK::Enum { shift: 6, width: 1, labels: &[(0, "ENGLISH"), (1, "JAPANESE")] } }, + SF { key: "voice-volume", byte: 0x000a9, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "LOW"), (1, "MID"), (2, "HIGH")] } }, + SF { key: "voice-rx-mute", byte: 0x000eb, mirror: 0x00000, kind: SK::Bool { shift: 2, active_low: true } }, + SF { key: "memory-list-mode", byte: 0x000f8, mirror: 0x00000, kind: SK::Bool { shift: 6, active_low: false } }, + SF { key: "pmg-timer", byte: 0x002d9, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "0.5s"), (1, "1s"), (2, "2s")] } }, + SF { key: "pmg-hold", byte: 0x002c4, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "2s"), (1, "5s"), (2, "10s"), (3, "20s"), (4, "30s")] } }, + SF { key: "band-skip-a-air", byte: 0x000ec, mirror: 0x00000, kind: SK::Bits { bits: &[(0x0ec, 0)], labels: &[(0, "OFF"), (1, "ON")] } }, + SF { key: "band-skip-a-vhf", byte: 0x000ec, mirror: 0x00000, kind: SK::Bits { bits: &[(0x0ec, 1)], labels: &[(0, "OFF"), (1, "ON")] } }, + SF { key: "band-skip-a-uhf", byte: 0x000ec, mirror: 0x00000, kind: SK::Bits { bits: &[(0x0ec, 3)], labels: &[(0, "OFF"), (1, "ON")] } }, + SF { key: "band-skip-a-other", byte: 0x000ec, mirror: 0x00000, kind: SK::Bits { bits: &[(0x0ec, 2), (0x0ec, 4)], labels: &[(0, "OFF"), (3, "ON")] } }, + SF { key: "band-skip-b-air", byte: 0x000fc, mirror: 0x00000, kind: SK::Bits { bits: &[(0x0fc, 0)], labels: &[(0, "OFF"), (1, "ON")] } }, + SF { key: "band-skip-b-vhf", byte: 0x000fc, mirror: 0x00000, kind: SK::Bits { bits: &[(0x0fc, 1)], labels: &[(0, "OFF"), (1, "ON")] } }, + SF { key: "band-skip-b-uhf", byte: 0x000fc, mirror: 0x00000, kind: SK::Bits { bits: &[(0x0fc, 3)], labels: &[(0, "OFF"), (1, "ON")] } }, + SF { key: "band-skip-b-other", byte: 0x000fc, mirror: 0x00000, kind: SK::Bits { bits: &[(0x0fc, 2), (0x0fc, 4)], labels: &[(0, "OFF"), (3, "ON")] } }, + SF { key: "a-scan-resume", byte: 0x000c9, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "BUSY"), (1, "HOLD"), (2, "1 sec"), (3, "3 sec"), (4, "5 sec")] } }, + SF { key: "b-scan-resume", byte: 0x000d9, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "BUSY"), (1, "HOLD"), (2, "1 sec"), (3, "3 sec"), (4, "5 sec")] } }, + SF { key: "a-dual-receive-interval", byte: 0x000cb, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "0.5 sec"), (1, "1.0 sec"), (2, "2.0 sec"), (3, "3.0 sec"), (4, "5.0 sec"), (5, "7.0 sec"), (6, "10 sec")] } }, + SF { key: "b-dual-receive-interval", byte: 0x000db, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "0.5 sec"), (1, "1.0 sec"), (2, "2.0 sec"), (3, "3.0 sec"), (4, "5.0 sec"), (5, "7.0 sec"), (6, "10 sec")] } }, + SF { key: "a-priority-revert", byte: 0x000e2, mirror: 0x00000, kind: SK::Bool { shift: 0, active_low: false } }, + SF { key: "b-priority-revert", byte: 0x000f2, mirror: 0x00000, kind: SK::Bool { shift: 0, active_low: false } }, + SF { key: "dual-receive-mode", byte: 0x000e2, mirror: 0x00000, kind: SK::Enum { shift: 7, width: 1, labels: &[(0, "OFF"), (1, "PRIORITY SCAN")] } }, + SF { key: "mic-key-p1", byte: 0x002df, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(220, "OFF"), (221, "2nd PTT"), (222, "GM"), (223, "REC/STOP"), (224, "SCAN"), (225, "HOME CH"), (226, "RPT SHIFT"), (227, "REVERSE"), (228, "TX POWER"), (229, "SQL OFF"), (230, "T-CALL"), (231, "VOICE"), (232, "D_X"), (233, "WX"), (234, "STN LIST"), (235, "MSG LIST"), (236, "REPLY"), (237, "MSG EDIT"), (238, "DW")] } }, + SF { key: "mic-key-p2", byte: 0x002e0, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(220, "OFF"), (221, "2nd PTT"), (222, "GM"), (223, "REC/STOP"), (224, "SCAN"), (225, "HOME CH"), (226, "RPT SHIFT"), (227, "REVERSE"), (228, "TX POWER"), (229, "SQL OFF"), (230, "T-CALL"), (231, "VOICE"), (232, "D_X"), (233, "WX"), (234, "STN LIST"), (235, "MSG LIST"), (236, "REPLY"), (237, "MSG EDIT"), (238, "DW")] } }, + SF { key: "mic-key-p3", byte: 0x002e1, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(220, "OFF"), (221, "2nd PTT"), (222, "GM"), (223, "REC/STOP"), (224, "SCAN"), (225, "HOME CH"), (226, "RPT SHIFT"), (227, "REVERSE"), (228, "TX POWER"), (229, "SQL OFF"), (230, "T-CALL"), (231, "VOICE"), (232, "D_X"), (233, "WX"), (234, "STN LIST"), (235, "MSG LIST"), (236, "REPLY"), (237, "MSG EDIT"), (238, "DW")] } }, + SF { key: "mic-key-p4", byte: 0x002e2, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(220, "OFF"), (221, "2nd PTT"), (222, "GM"), (223, "REC/STOP"), (224, "SCAN"), (225, "HOME CH"), (226, "RPT SHIFT"), (227, "REVERSE"), (228, "TX POWER"), (229, "SQL OFF"), (230, "T-CALL"), (231, "VOICE"), (232, "D_X"), (233, "WX"), (234, "STN LIST"), (235, "MSG LIST"), (236, "REPLY"), (237, "MSG EDIT"), (238, "DW")] } }, + SF { key: "function-key-1", byte: 0x002a2, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "001: FREQENCY INPUT"), (1, "002: LCD BRIGHTNESS"), (2, "003: FREQUENCY COLOR"), (3, "004: BAND SCOPE"), (4, "005: LOCATION INFO"), (5, "006: COMPASS"), (6, "007: DISPLAY MODE"), (7, "008: TX POWER"), (8, "009: AMS TX MODE"), (9, "010: MIC GAIN"), (10, "011: VOX"), (11, "012: AUTO DIALER"), (12, "013: TOT"), (13, "014: DIGITAL VW"), (14, "015: FM BANDWIDTH"), (15, "016: RX MODE"), (16, "017: SUB BUND"), (17, "018: AUDIO EQUALIZER"), (18, "019: HOME CH"), (19, "020: MEMORY LIST"), (20, "021: MEMORY LIST MODE"), (21, "022: PMG CLEAR"), (22, "023: BEEP"), (23, "024: BAND SKIP"), (24, "025: RPT ARS"), (25, "026: RPT SHIFT"), (26, "027: RPT SHIFT FREQ"), (27, "028: RPT REVERSE"), (28, "029: MIC PROGRAM KEY"), (29, "030: DATE&TIME ADJUST"), (30, "031: DATE&TIME FORMAT"), (31, "032: TIME ZONE"), (32, "033: STEP"), (33, "034: CLOCK TYPE"), (34, "035: UNIT"), (35, "036: APO"), (36, "037: GPS DATUM"), (37, "038: GPS DEVICE"), (38, "039: GPS LOG"), (39, "040: RECODING"), (40, "041: REC/STOP"), (41, "042: FRONT SP MUTE"), (42, "043: DTMF"), (43, "044: DTMF MEMORY"), (44, "045: SQL TYPE"), (45, "046: TONE SQL FREQ"), (46, "047: SQL EXPANSION"), (47, "048: PAGER CODE"), (48, "049: PR FREQUENCY"), (49, "050: BELL RINGER"), (50, "051: WX ALERT"), (51, "052: SCAN"), (52, "053: DUAL RCV MODE"), (53, "054: DUAL RX INTERVAL"), (54, "055: PRIORITY REVERT"), (55, "056: SCAN RESUME"), (56, "057: DIGITAL POPUP"), (57, "058: LOCATION SERVICE"), (58, "059: STANDBY BEEP"), (59, "060: DP-ID LIST"), (60, "061: RANGE RINGER"), (61, "062: RADIO ID"), (62, "063: LOG LIST"), (63, "064: RPT/WIRES FREQ"), (64, "065: SEARCH SETUP"), (65, "066: EDIT CATEGORYTAG"), (66, "067: DELETE ROOM/NODE"), (67, "068: WIRES DG-ID"), (68, "069: COM PORT"), (69, "070: DATA BAND"), (70, "071: DATA SPEED"), (71, "072: DATA SQL"), (72, "073: APRS DESTINATION"), (73, "074: APRS FILTER"), (74, "075: APRS MSG TEXT"), (75, "076: APRS MODEM"), (76, "077: APRS MUTE"), (77, "078: APRS POPUP"), (78, "079: APRS RINGER"), (79, "080: APRS RINGER (CS)"), (80, "081: APRS TX DELAY"), (81, "082: APRS UNITS"), (82, "083: BEACON INFO"), (83, "084: BEACON STATUSTXT"), (84, "085: BEACON TX SET"), (85, "086: DIGI PATH"), (86, "087: DIGI PATH 1"), (87, "088: DIGI PATH 2"), (88, "089: DIGI PATH 3"), (89, "090: DIGI PATH 4"), (90, "091: DIGI PATH FULL 1"), (91, "092: DIGI PATH FULL 2"), (92, "093: CALLSIGN (APRS)"), (93, "094: MESSAGE GROUP"), (94, "095: MESSAGE REPLY"), (95, "096: MY POSITION SET"), (96, "097: MY POSITION"), (97, "098: MY SYMBOL"), (98, "099: POSITION COMMENT"), (99, "100: SmartBeaconing"), (100, "101: SORT FILTER"), (101, "102: VOICE ALERT"), (102, "103: STATION LIST"), (103, "104: MESSAGE LIST"), (104, "105: BEACON TX SELECT"), (105, "106: BEACON TX"), (106, "107: BACKUP"), (107, "108: MEMORY INFO"), (108, "109: FORMAT"), (109, "110: JPG IMPORT"), (110, "111: Bluetooth"), (111, "112: VOICE MEMORY"), (112, "113: FVS REC"), (113, "114: TRACK SELECT"), (114, "115: PLAY"), (115, "116: STOP"), (116, "117: CLEAR"), (117, "118: VOICE GUIDE"), (118, "119: This → Other"), (119, "120: Other → This"), (120, "121: CALLSIGN"), (121, "122: MEMORY CH RESET"), (122, "123: APRS RESET"), (123, "124: CONFIG SET"), (124, "125: CONFIG RECALL"), (125, "126: SOFTWARE VERSION")] } }, + SF { key: "function-key-2", byte: 0x002a3, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "001: FREQENCY INPUT"), (1, "002: LCD BRIGHTNESS"), (2, "003: FREQUENCY COLOR"), (3, "004: BAND SCOPE"), (4, "005: LOCATION INFO"), (5, "006: COMPASS"), (6, "007: DISPLAY MODE"), (7, "008: TX POWER"), (8, "009: AMS TX MODE"), (9, "010: MIC GAIN"), (10, "011: VOX"), (11, "012: AUTO DIALER"), (12, "013: TOT"), (13, "014: DIGITAL VW"), (14, "015: FM BANDWIDTH"), (15, "016: RX MODE"), (16, "017: SUB BUND"), (17, "018: AUDIO EQUALIZER"), (18, "019: HOME CH"), (19, "020: MEMORY LIST"), (20, "021: MEMORY LIST MODE"), (21, "022: PMG CLEAR"), (22, "023: BEEP"), (23, "024: BAND SKIP"), (24, "025: RPT ARS"), (25, "026: RPT SHIFT"), (26, "027: RPT SHIFT FREQ"), (27, "028: RPT REVERSE"), (28, "029: MIC PROGRAM KEY"), (29, "030: DATE&TIME ADJUST"), (30, "031: DATE&TIME FORMAT"), (31, "032: TIME ZONE"), (32, "033: STEP"), (33, "034: CLOCK TYPE"), (34, "035: UNIT"), (35, "036: APO"), (36, "037: GPS DATUM"), (37, "038: GPS DEVICE"), (38, "039: GPS LOG"), (39, "040: RECODING"), (40, "041: REC/STOP"), (41, "042: FRONT SP MUTE"), (42, "043: DTMF"), (43, "044: DTMF MEMORY"), (44, "045: SQL TYPE"), (45, "046: TONE SQL FREQ"), (46, "047: SQL EXPANSION"), (47, "048: PAGER CODE"), (48, "049: PR FREQUENCY"), (49, "050: BELL RINGER"), (50, "051: WX ALERT"), (51, "052: SCAN"), (52, "053: DUAL RCV MODE"), (53, "054: DUAL RX INTERVAL"), (54, "055: PRIORITY REVERT"), (55, "056: SCAN RESUME"), (56, "057: DIGITAL POPUP"), (57, "058: LOCATION SERVICE"), (58, "059: STANDBY BEEP"), (59, "060: DP-ID LIST"), (60, "061: RANGE RINGER"), (61, "062: RADIO ID"), (62, "063: LOG LIST"), (63, "064: RPT/WIRES FREQ"), (64, "065: SEARCH SETUP"), (65, "066: EDIT CATEGORYTAG"), (66, "067: DELETE ROOM/NODE"), (67, "068: WIRES DG-ID"), (68, "069: COM PORT"), (69, "070: DATA BAND"), (70, "071: DATA SPEED"), (71, "072: DATA SQL"), (72, "073: APRS DESTINATION"), (73, "074: APRS FILTER"), (74, "075: APRS MSG TEXT"), (75, "076: APRS MODEM"), (76, "077: APRS MUTE"), (77, "078: APRS POPUP"), (78, "079: APRS RINGER"), (79, "080: APRS RINGER (CS)"), (80, "081: APRS TX DELAY"), (81, "082: APRS UNITS"), (82, "083: BEACON INFO"), (83, "084: BEACON STATUSTXT"), (84, "085: BEACON TX SET"), (85, "086: DIGI PATH"), (86, "087: DIGI PATH 1"), (87, "088: DIGI PATH 2"), (88, "089: DIGI PATH 3"), (89, "090: DIGI PATH 4"), (90, "091: DIGI PATH FULL 1"), (91, "092: DIGI PATH FULL 2"), (92, "093: CALLSIGN (APRS)"), (93, "094: MESSAGE GROUP"), (94, "095: MESSAGE REPLY"), (95, "096: MY POSITION SET"), (96, "097: MY POSITION"), (97, "098: MY SYMBOL"), (98, "099: POSITION COMMENT"), (99, "100: SmartBeaconing"), (100, "101: SORT FILTER"), (101, "102: VOICE ALERT"), (102, "103: STATION LIST"), (103, "104: MESSAGE LIST"), (104, "105: BEACON TX SELECT"), (105, "106: BEACON TX"), (106, "107: BACKUP"), (107, "108: MEMORY INFO"), (108, "109: FORMAT"), (109, "110: JPG IMPORT"), (110, "111: Bluetooth"), (111, "112: VOICE MEMORY"), (112, "113: FVS REC"), (113, "114: TRACK SELECT"), (114, "115: PLAY"), (115, "116: STOP"), (116, "117: CLEAR"), (117, "118: VOICE GUIDE"), (118, "119: This → Other"), (119, "120: Other → This"), (120, "121: CALLSIGN"), (121, "122: MEMORY CH RESET"), (122, "123: APRS RESET"), (123, "124: CONFIG SET"), (124, "125: CONFIG RECALL"), (125, "126: SOFTWARE VERSION")] } }, + SF { key: "function-key-3", byte: 0x002a4, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "001: FREQENCY INPUT"), (1, "002: LCD BRIGHTNESS"), (2, "003: FREQUENCY COLOR"), (3, "004: BAND SCOPE"), (4, "005: LOCATION INFO"), (5, "006: COMPASS"), (6, "007: DISPLAY MODE"), (7, "008: TX POWER"), (8, "009: AMS TX MODE"), (9, "010: MIC GAIN"), (10, "011: VOX"), (11, "012: AUTO DIALER"), (12, "013: TOT"), (13, "014: DIGITAL VW"), (14, "015: FM BANDWIDTH"), (15, "016: RX MODE"), (16, "017: SUB BUND"), (17, "018: AUDIO EQUALIZER"), (18, "019: HOME CH"), (19, "020: MEMORY LIST"), (20, "021: MEMORY LIST MODE"), (21, "022: PMG CLEAR"), (22, "023: BEEP"), (23, "024: BAND SKIP"), (24, "025: RPT ARS"), (25, "026: RPT SHIFT"), (26, "027: RPT SHIFT FREQ"), (27, "028: RPT REVERSE"), (28, "029: MIC PROGRAM KEY"), (29, "030: DATE&TIME ADJUST"), (30, "031: DATE&TIME FORMAT"), (31, "032: TIME ZONE"), (32, "033: STEP"), (33, "034: CLOCK TYPE"), (34, "035: UNIT"), (35, "036: APO"), (36, "037: GPS DATUM"), (37, "038: GPS DEVICE"), (38, "039: GPS LOG"), (39, "040: RECODING"), (40, "041: REC/STOP"), (41, "042: FRONT SP MUTE"), (42, "043: DTMF"), (43, "044: DTMF MEMORY"), (44, "045: SQL TYPE"), (45, "046: TONE SQL FREQ"), (46, "047: SQL EXPANSION"), (47, "048: PAGER CODE"), (48, "049: PR FREQUENCY"), (49, "050: BELL RINGER"), (50, "051: WX ALERT"), (51, "052: SCAN"), (52, "053: DUAL RCV MODE"), (53, "054: DUAL RX INTERVAL"), (54, "055: PRIORITY REVERT"), (55, "056: SCAN RESUME"), (56, "057: DIGITAL POPUP"), (57, "058: LOCATION SERVICE"), (58, "059: STANDBY BEEP"), (59, "060: DP-ID LIST"), (60, "061: RANGE RINGER"), (61, "062: RADIO ID"), (62, "063: LOG LIST"), (63, "064: RPT/WIRES FREQ"), (64, "065: SEARCH SETUP"), (65, "066: EDIT CATEGORYTAG"), (66, "067: DELETE ROOM/NODE"), (67, "068: WIRES DG-ID"), (68, "069: COM PORT"), (69, "070: DATA BAND"), (70, "071: DATA SPEED"), (71, "072: DATA SQL"), (72, "073: APRS DESTINATION"), (73, "074: APRS FILTER"), (74, "075: APRS MSG TEXT"), (75, "076: APRS MODEM"), (76, "077: APRS MUTE"), (77, "078: APRS POPUP"), (78, "079: APRS RINGER"), (79, "080: APRS RINGER (CS)"), (80, "081: APRS TX DELAY"), (81, "082: APRS UNITS"), (82, "083: BEACON INFO"), (83, "084: BEACON STATUSTXT"), (84, "085: BEACON TX SET"), (85, "086: DIGI PATH"), (86, "087: DIGI PATH 1"), (87, "088: DIGI PATH 2"), (88, "089: DIGI PATH 3"), (89, "090: DIGI PATH 4"), (90, "091: DIGI PATH FULL 1"), (91, "092: DIGI PATH FULL 2"), (92, "093: CALLSIGN (APRS)"), (93, "094: MESSAGE GROUP"), (94, "095: MESSAGE REPLY"), (95, "096: MY POSITION SET"), (96, "097: MY POSITION"), (97, "098: MY SYMBOL"), (98, "099: POSITION COMMENT"), (99, "100: SmartBeaconing"), (100, "101: SORT FILTER"), (101, "102: VOICE ALERT"), (102, "103: STATION LIST"), (103, "104: MESSAGE LIST"), (104, "105: BEACON TX SELECT"), (105, "106: BEACON TX"), (106, "107: BACKUP"), (107, "108: MEMORY INFO"), (108, "109: FORMAT"), (109, "110: JPG IMPORT"), (110, "111: Bluetooth"), (111, "112: VOICE MEMORY"), (112, "113: FVS REC"), (113, "114: TRACK SELECT"), (114, "115: PLAY"), (115, "116: STOP"), (116, "117: CLEAR"), (117, "118: VOICE GUIDE"), (118, "119: This → Other"), (119, "120: Other → This"), (120, "121: CALLSIGN"), (121, "122: MEMORY CH RESET"), (122, "123: APRS RESET"), (123, "124: CONFIG SET"), (124, "125: CONFIG RECALL"), (125, "126: SOFTWARE VERSION")] } }, + SF { key: "function-key-4", byte: 0x002a5, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "001: FREQENCY INPUT"), (1, "002: LCD BRIGHTNESS"), (2, "003: FREQUENCY COLOR"), (3, "004: BAND SCOPE"), (4, "005: LOCATION INFO"), (5, "006: COMPASS"), (6, "007: DISPLAY MODE"), (7, "008: TX POWER"), (8, "009: AMS TX MODE"), (9, "010: MIC GAIN"), (10, "011: VOX"), (11, "012: AUTO DIALER"), (12, "013: TOT"), (13, "014: DIGITAL VW"), (14, "015: FM BANDWIDTH"), (15, "016: RX MODE"), (16, "017: SUB BUND"), (17, "018: AUDIO EQUALIZER"), (18, "019: HOME CH"), (19, "020: MEMORY LIST"), (20, "021: MEMORY LIST MODE"), (21, "022: PMG CLEAR"), (22, "023: BEEP"), (23, "024: BAND SKIP"), (24, "025: RPT ARS"), (25, "026: RPT SHIFT"), (26, "027: RPT SHIFT FREQ"), (27, "028: RPT REVERSE"), (28, "029: MIC PROGRAM KEY"), (29, "030: DATE&TIME ADJUST"), (30, "031: DATE&TIME FORMAT"), (31, "032: TIME ZONE"), (32, "033: STEP"), (33, "034: CLOCK TYPE"), (34, "035: UNIT"), (35, "036: APO"), (36, "037: GPS DATUM"), (37, "038: GPS DEVICE"), (38, "039: GPS LOG"), (39, "040: RECODING"), (40, "041: REC/STOP"), (41, "042: FRONT SP MUTE"), (42, "043: DTMF"), (43, "044: DTMF MEMORY"), (44, "045: SQL TYPE"), (45, "046: TONE SQL FREQ"), (46, "047: SQL EXPANSION"), (47, "048: PAGER CODE"), (48, "049: PR FREQUENCY"), (49, "050: BELL RINGER"), (50, "051: WX ALERT"), (51, "052: SCAN"), (52, "053: DUAL RCV MODE"), (53, "054: DUAL RX INTERVAL"), (54, "055: PRIORITY REVERT"), (55, "056: SCAN RESUME"), (56, "057: DIGITAL POPUP"), (57, "058: LOCATION SERVICE"), (58, "059: STANDBY BEEP"), (59, "060: DP-ID LIST"), (60, "061: RANGE RINGER"), (61, "062: RADIO ID"), (62, "063: LOG LIST"), (63, "064: RPT/WIRES FREQ"), (64, "065: SEARCH SETUP"), (65, "066: EDIT CATEGORYTAG"), (66, "067: DELETE ROOM/NODE"), (67, "068: WIRES DG-ID"), (68, "069: COM PORT"), (69, "070: DATA BAND"), (70, "071: DATA SPEED"), (71, "072: DATA SQL"), (72, "073: APRS DESTINATION"), (73, "074: APRS FILTER"), (74, "075: APRS MSG TEXT"), (75, "076: APRS MODEM"), (76, "077: APRS MUTE"), (77, "078: APRS POPUP"), (78, "079: APRS RINGER"), (79, "080: APRS RINGER (CS)"), (80, "081: APRS TX DELAY"), (81, "082: APRS UNITS"), (82, "083: BEACON INFO"), (83, "084: BEACON STATUSTXT"), (84, "085: BEACON TX SET"), (85, "086: DIGI PATH"), (86, "087: DIGI PATH 1"), (87, "088: DIGI PATH 2"), (88, "089: DIGI PATH 3"), (89, "090: DIGI PATH 4"), (90, "091: DIGI PATH FULL 1"), (91, "092: DIGI PATH FULL 2"), (92, "093: CALLSIGN (APRS)"), (93, "094: MESSAGE GROUP"), (94, "095: MESSAGE REPLY"), (95, "096: MY POSITION SET"), (96, "097: MY POSITION"), (97, "098: MY SYMBOL"), (98, "099: POSITION COMMENT"), (99, "100: SmartBeaconing"), (100, "101: SORT FILTER"), (101, "102: VOICE ALERT"), (102, "103: STATION LIST"), (103, "104: MESSAGE LIST"), (104, "105: BEACON TX SELECT"), (105, "106: BEACON TX"), (106, "107: BACKUP"), (107, "108: MEMORY INFO"), (108, "109: FORMAT"), (109, "110: JPG IMPORT"), (110, "111: Bluetooth"), (111, "112: VOICE MEMORY"), (112, "113: FVS REC"), (113, "114: TRACK SELECT"), (114, "115: PLAY"), (115, "116: STOP"), (116, "117: CLEAR"), (117, "118: VOICE GUIDE"), (118, "119: This → Other"), (119, "120: Other → This"), (120, "121: CALLSIGN"), (121, "122: MEMORY CH RESET"), (122, "123: APRS RESET"), (123, "124: CONFIG SET"), (124, "125: CONFIG RECALL"), (125, "126: SOFTWARE VERSION")] } }, + SF { key: "function-key-5", byte: 0x002a6, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "001: FREQENCY INPUT"), (1, "002: LCD BRIGHTNESS"), (2, "003: FREQUENCY COLOR"), (3, "004: BAND SCOPE"), (4, "005: LOCATION INFO"), (5, "006: COMPASS"), (6, "007: DISPLAY MODE"), (7, "008: TX POWER"), (8, "009: AMS TX MODE"), (9, "010: MIC GAIN"), (10, "011: VOX"), (11, "012: AUTO DIALER"), (12, "013: TOT"), (13, "014: DIGITAL VW"), (14, "015: FM BANDWIDTH"), (15, "016: RX MODE"), (16, "017: SUB BUND"), (17, "018: AUDIO EQUALIZER"), (18, "019: HOME CH"), (19, "020: MEMORY LIST"), (20, "021: MEMORY LIST MODE"), (21, "022: PMG CLEAR"), (22, "023: BEEP"), (23, "024: BAND SKIP"), (24, "025: RPT ARS"), (25, "026: RPT SHIFT"), (26, "027: RPT SHIFT FREQ"), (27, "028: RPT REVERSE"), (28, "029: MIC PROGRAM KEY"), (29, "030: DATE&TIME ADJUST"), (30, "031: DATE&TIME FORMAT"), (31, "032: TIME ZONE"), (32, "033: STEP"), (33, "034: CLOCK TYPE"), (34, "035: UNIT"), (35, "036: APO"), (36, "037: GPS DATUM"), (37, "038: GPS DEVICE"), (38, "039: GPS LOG"), (39, "040: RECODING"), (40, "041: REC/STOP"), (41, "042: FRONT SP MUTE"), (42, "043: DTMF"), (43, "044: DTMF MEMORY"), (44, "045: SQL TYPE"), (45, "046: TONE SQL FREQ"), (46, "047: SQL EXPANSION"), (47, "048: PAGER CODE"), (48, "049: PR FREQUENCY"), (49, "050: BELL RINGER"), (50, "051: WX ALERT"), (51, "052: SCAN"), (52, "053: DUAL RCV MODE"), (53, "054: DUAL RX INTERVAL"), (54, "055: PRIORITY REVERT"), (55, "056: SCAN RESUME"), (56, "057: DIGITAL POPUP"), (57, "058: LOCATION SERVICE"), (58, "059: STANDBY BEEP"), (59, "060: DP-ID LIST"), (60, "061: RANGE RINGER"), (61, "062: RADIO ID"), (62, "063: LOG LIST"), (63, "064: RPT/WIRES FREQ"), (64, "065: SEARCH SETUP"), (65, "066: EDIT CATEGORYTAG"), (66, "067: DELETE ROOM/NODE"), (67, "068: WIRES DG-ID"), (68, "069: COM PORT"), (69, "070: DATA BAND"), (70, "071: DATA SPEED"), (71, "072: DATA SQL"), (72, "073: APRS DESTINATION"), (73, "074: APRS FILTER"), (74, "075: APRS MSG TEXT"), (75, "076: APRS MODEM"), (76, "077: APRS MUTE"), (77, "078: APRS POPUP"), (78, "079: APRS RINGER"), (79, "080: APRS RINGER (CS)"), (80, "081: APRS TX DELAY"), (81, "082: APRS UNITS"), (82, "083: BEACON INFO"), (83, "084: BEACON STATUSTXT"), (84, "085: BEACON TX SET"), (85, "086: DIGI PATH"), (86, "087: DIGI PATH 1"), (87, "088: DIGI PATH 2"), (88, "089: DIGI PATH 3"), (89, "090: DIGI PATH 4"), (90, "091: DIGI PATH FULL 1"), (91, "092: DIGI PATH FULL 2"), (92, "093: CALLSIGN (APRS)"), (93, "094: MESSAGE GROUP"), (94, "095: MESSAGE REPLY"), (95, "096: MY POSITION SET"), (96, "097: MY POSITION"), (97, "098: MY SYMBOL"), (98, "099: POSITION COMMENT"), (99, "100: SmartBeaconing"), (100, "101: SORT FILTER"), (101, "102: VOICE ALERT"), (102, "103: STATION LIST"), (103, "104: MESSAGE LIST"), (104, "105: BEACON TX SELECT"), (105, "106: BEACON TX"), (106, "107: BACKUP"), (107, "108: MEMORY INFO"), (108, "109: FORMAT"), (109, "110: JPG IMPORT"), (110, "111: Bluetooth"), (111, "112: VOICE MEMORY"), (112, "113: FVS REC"), (113, "114: TRACK SELECT"), (114, "115: PLAY"), (115, "116: STOP"), (116, "117: CLEAR"), (117, "118: VOICE GUIDE"), (118, "119: This → Other"), (119, "120: Other → This"), (120, "121: CALLSIGN"), (121, "122: MEMORY CH RESET"), (122, "123: APRS RESET"), (123, "124: CONFIG SET"), (124, "125: CONFIG RECALL"), (125, "126: SOFTWARE VERSION")] } }, + SF { key: "function-key-6", byte: 0x002a7, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "001: FREQENCY INPUT"), (1, "002: LCD BRIGHTNESS"), (2, "003: FREQUENCY COLOR"), (3, "004: BAND SCOPE"), (4, "005: LOCATION INFO"), (5, "006: COMPASS"), (6, "007: DISPLAY MODE"), (7, "008: TX POWER"), (8, "009: AMS TX MODE"), (9, "010: MIC GAIN"), (10, "011: VOX"), (11, "012: AUTO DIALER"), (12, "013: TOT"), (13, "014: DIGITAL VW"), (14, "015: FM BANDWIDTH"), (15, "016: RX MODE"), (16, "017: SUB BUND"), (17, "018: AUDIO EQUALIZER"), (18, "019: HOME CH"), (19, "020: MEMORY LIST"), (20, "021: MEMORY LIST MODE"), (21, "022: PMG CLEAR"), (22, "023: BEEP"), (23, "024: BAND SKIP"), (24, "025: RPT ARS"), (25, "026: RPT SHIFT"), (26, "027: RPT SHIFT FREQ"), (27, "028: RPT REVERSE"), (28, "029: MIC PROGRAM KEY"), (29, "030: DATE&TIME ADJUST"), (30, "031: DATE&TIME FORMAT"), (31, "032: TIME ZONE"), (32, "033: STEP"), (33, "034: CLOCK TYPE"), (34, "035: UNIT"), (35, "036: APO"), (36, "037: GPS DATUM"), (37, "038: GPS DEVICE"), (38, "039: GPS LOG"), (39, "040: RECODING"), (40, "041: REC/STOP"), (41, "042: FRONT SP MUTE"), (42, "043: DTMF"), (43, "044: DTMF MEMORY"), (44, "045: SQL TYPE"), (45, "046: TONE SQL FREQ"), (46, "047: SQL EXPANSION"), (47, "048: PAGER CODE"), (48, "049: PR FREQUENCY"), (49, "050: BELL RINGER"), (50, "051: WX ALERT"), (51, "052: SCAN"), (52, "053: DUAL RCV MODE"), (53, "054: DUAL RX INTERVAL"), (54, "055: PRIORITY REVERT"), (55, "056: SCAN RESUME"), (56, "057: DIGITAL POPUP"), (57, "058: LOCATION SERVICE"), (58, "059: STANDBY BEEP"), (59, "060: DP-ID LIST"), (60, "061: RANGE RINGER"), (61, "062: RADIO ID"), (62, "063: LOG LIST"), (63, "064: RPT/WIRES FREQ"), (64, "065: SEARCH SETUP"), (65, "066: EDIT CATEGORYTAG"), (66, "067: DELETE ROOM/NODE"), (67, "068: WIRES DG-ID"), (68, "069: COM PORT"), (69, "070: DATA BAND"), (70, "071: DATA SPEED"), (71, "072: DATA SQL"), (72, "073: APRS DESTINATION"), (73, "074: APRS FILTER"), (74, "075: APRS MSG TEXT"), (75, "076: APRS MODEM"), (76, "077: APRS MUTE"), (77, "078: APRS POPUP"), (78, "079: APRS RINGER"), (79, "080: APRS RINGER (CS)"), (80, "081: APRS TX DELAY"), (81, "082: APRS UNITS"), (82, "083: BEACON INFO"), (83, "084: BEACON STATUSTXT"), (84, "085: BEACON TX SET"), (85, "086: DIGI PATH"), (86, "087: DIGI PATH 1"), (87, "088: DIGI PATH 2"), (88, "089: DIGI PATH 3"), (89, "090: DIGI PATH 4"), (90, "091: DIGI PATH FULL 1"), (91, "092: DIGI PATH FULL 2"), (92, "093: CALLSIGN (APRS)"), (93, "094: MESSAGE GROUP"), (94, "095: MESSAGE REPLY"), (95, "096: MY POSITION SET"), (96, "097: MY POSITION"), (97, "098: MY SYMBOL"), (98, "099: POSITION COMMENT"), (99, "100: SmartBeaconing"), (100, "101: SORT FILTER"), (101, "102: VOICE ALERT"), (102, "103: STATION LIST"), (103, "104: MESSAGE LIST"), (104, "105: BEACON TX SELECT"), (105, "106: BEACON TX"), (106, "107: BACKUP"), (107, "108: MEMORY INFO"), (108, "109: FORMAT"), (109, "110: JPG IMPORT"), (110, "111: Bluetooth"), (111, "112: VOICE MEMORY"), (112, "113: FVS REC"), (113, "114: TRACK SELECT"), (114, "115: PLAY"), (115, "116: STOP"), (116, "117: CLEAR"), (117, "118: VOICE GUIDE"), (118, "119: This → Other"), (119, "120: Other → This"), (120, "121: CALLSIGN"), (121, "122: MEMORY CH RESET"), (122, "123: APRS RESET"), (123, "124: CONFIG SET"), (124, "125: CONFIG RECALL"), (125, "126: SOFTWARE VERSION")] } }, + SF { key: "function-key-7", byte: 0x002a8, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "001: FREQENCY INPUT"), (1, "002: LCD BRIGHTNESS"), (2, "003: FREQUENCY COLOR"), (3, "004: BAND SCOPE"), (4, "005: LOCATION INFO"), (5, "006: COMPASS"), (6, "007: DISPLAY MODE"), (7, "008: TX POWER"), (8, "009: AMS TX MODE"), (9, "010: MIC GAIN"), (10, "011: VOX"), (11, "012: AUTO DIALER"), (12, "013: TOT"), (13, "014: DIGITAL VW"), (14, "015: FM BANDWIDTH"), (15, "016: RX MODE"), (16, "017: SUB BUND"), (17, "018: AUDIO EQUALIZER"), (18, "019: HOME CH"), (19, "020: MEMORY LIST"), (20, "021: MEMORY LIST MODE"), (21, "022: PMG CLEAR"), (22, "023: BEEP"), (23, "024: BAND SKIP"), (24, "025: RPT ARS"), (25, "026: RPT SHIFT"), (26, "027: RPT SHIFT FREQ"), (27, "028: RPT REVERSE"), (28, "029: MIC PROGRAM KEY"), (29, "030: DATE&TIME ADJUST"), (30, "031: DATE&TIME FORMAT"), (31, "032: TIME ZONE"), (32, "033: STEP"), (33, "034: CLOCK TYPE"), (34, "035: UNIT"), (35, "036: APO"), (36, "037: GPS DATUM"), (37, "038: GPS DEVICE"), (38, "039: GPS LOG"), (39, "040: RECODING"), (40, "041: REC/STOP"), (41, "042: FRONT SP MUTE"), (42, "043: DTMF"), (43, "044: DTMF MEMORY"), (44, "045: SQL TYPE"), (45, "046: TONE SQL FREQ"), (46, "047: SQL EXPANSION"), (47, "048: PAGER CODE"), (48, "049: PR FREQUENCY"), (49, "050: BELL RINGER"), (50, "051: WX ALERT"), (51, "052: SCAN"), (52, "053: DUAL RCV MODE"), (53, "054: DUAL RX INTERVAL"), (54, "055: PRIORITY REVERT"), (55, "056: SCAN RESUME"), (56, "057: DIGITAL POPUP"), (57, "058: LOCATION SERVICE"), (58, "059: STANDBY BEEP"), (59, "060: DP-ID LIST"), (60, "061: RANGE RINGER"), (61, "062: RADIO ID"), (62, "063: LOG LIST"), (63, "064: RPT/WIRES FREQ"), (64, "065: SEARCH SETUP"), (65, "066: EDIT CATEGORYTAG"), (66, "067: DELETE ROOM/NODE"), (67, "068: WIRES DG-ID"), (68, "069: COM PORT"), (69, "070: DATA BAND"), (70, "071: DATA SPEED"), (71, "072: DATA SQL"), (72, "073: APRS DESTINATION"), (73, "074: APRS FILTER"), (74, "075: APRS MSG TEXT"), (75, "076: APRS MODEM"), (76, "077: APRS MUTE"), (77, "078: APRS POPUP"), (78, "079: APRS RINGER"), (79, "080: APRS RINGER (CS)"), (80, "081: APRS TX DELAY"), (81, "082: APRS UNITS"), (82, "083: BEACON INFO"), (83, "084: BEACON STATUSTXT"), (84, "085: BEACON TX SET"), (85, "086: DIGI PATH"), (86, "087: DIGI PATH 1"), (87, "088: DIGI PATH 2"), (88, "089: DIGI PATH 3"), (89, "090: DIGI PATH 4"), (90, "091: DIGI PATH FULL 1"), (91, "092: DIGI PATH FULL 2"), (92, "093: CALLSIGN (APRS)"), (93, "094: MESSAGE GROUP"), (94, "095: MESSAGE REPLY"), (95, "096: MY POSITION SET"), (96, "097: MY POSITION"), (97, "098: MY SYMBOL"), (98, "099: POSITION COMMENT"), (99, "100: SmartBeaconing"), (100, "101: SORT FILTER"), (101, "102: VOICE ALERT"), (102, "103: STATION LIST"), (103, "104: MESSAGE LIST"), (104, "105: BEACON TX SELECT"), (105, "106: BEACON TX"), (106, "107: BACKUP"), (107, "108: MEMORY INFO"), (108, "109: FORMAT"), (109, "110: JPG IMPORT"), (110, "111: Bluetooth"), (111, "112: VOICE MEMORY"), (112, "113: FVS REC"), (113, "114: TRACK SELECT"), (114, "115: PLAY"), (115, "116: STOP"), (116, "117: CLEAR"), (117, "118: VOICE GUIDE"), (118, "119: This → Other"), (119, "120: Other → This"), (120, "121: CALLSIGN"), (121, "122: MEMORY CH RESET"), (122, "123: APRS RESET"), (123, "124: CONFIG SET"), (124, "125: CONFIG RECALL"), (125, "126: SOFTWARE VERSION")] } }, + SF { key: "function-key-8", byte: 0x002a9, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "001: FREQENCY INPUT"), (1, "002: LCD BRIGHTNESS"), (2, "003: FREQUENCY COLOR"), (3, "004: BAND SCOPE"), (4, "005: LOCATION INFO"), (5, "006: COMPASS"), (6, "007: DISPLAY MODE"), (7, "008: TX POWER"), (8, "009: AMS TX MODE"), (9, "010: MIC GAIN"), (10, "011: VOX"), (11, "012: AUTO DIALER"), (12, "013: TOT"), (13, "014: DIGITAL VW"), (14, "015: FM BANDWIDTH"), (15, "016: RX MODE"), (16, "017: SUB BUND"), (17, "018: AUDIO EQUALIZER"), (18, "019: HOME CH"), (19, "020: MEMORY LIST"), (20, "021: MEMORY LIST MODE"), (21, "022: PMG CLEAR"), (22, "023: BEEP"), (23, "024: BAND SKIP"), (24, "025: RPT ARS"), (25, "026: RPT SHIFT"), (26, "027: RPT SHIFT FREQ"), (27, "028: RPT REVERSE"), (28, "029: MIC PROGRAM KEY"), (29, "030: DATE&TIME ADJUST"), (30, "031: DATE&TIME FORMAT"), (31, "032: TIME ZONE"), (32, "033: STEP"), (33, "034: CLOCK TYPE"), (34, "035: UNIT"), (35, "036: APO"), (36, "037: GPS DATUM"), (37, "038: GPS DEVICE"), (38, "039: GPS LOG"), (39, "040: RECODING"), (40, "041: REC/STOP"), (41, "042: FRONT SP MUTE"), (42, "043: DTMF"), (43, "044: DTMF MEMORY"), (44, "045: SQL TYPE"), (45, "046: TONE SQL FREQ"), (46, "047: SQL EXPANSION"), (47, "048: PAGER CODE"), (48, "049: PR FREQUENCY"), (49, "050: BELL RINGER"), (50, "051: WX ALERT"), (51, "052: SCAN"), (52, "053: DUAL RCV MODE"), (53, "054: DUAL RX INTERVAL"), (54, "055: PRIORITY REVERT"), (55, "056: SCAN RESUME"), (56, "057: DIGITAL POPUP"), (57, "058: LOCATION SERVICE"), (58, "059: STANDBY BEEP"), (59, "060: DP-ID LIST"), (60, "061: RANGE RINGER"), (61, "062: RADIO ID"), (62, "063: LOG LIST"), (63, "064: RPT/WIRES FREQ"), (64, "065: SEARCH SETUP"), (65, "066: EDIT CATEGORYTAG"), (66, "067: DELETE ROOM/NODE"), (67, "068: WIRES DG-ID"), (68, "069: COM PORT"), (69, "070: DATA BAND"), (70, "071: DATA SPEED"), (71, "072: DATA SQL"), (72, "073: APRS DESTINATION"), (73, "074: APRS FILTER"), (74, "075: APRS MSG TEXT"), (75, "076: APRS MODEM"), (76, "077: APRS MUTE"), (77, "078: APRS POPUP"), (78, "079: APRS RINGER"), (79, "080: APRS RINGER (CS)"), (80, "081: APRS TX DELAY"), (81, "082: APRS UNITS"), (82, "083: BEACON INFO"), (83, "084: BEACON STATUSTXT"), (84, "085: BEACON TX SET"), (85, "086: DIGI PATH"), (86, "087: DIGI PATH 1"), (87, "088: DIGI PATH 2"), (88, "089: DIGI PATH 3"), (89, "090: DIGI PATH 4"), (90, "091: DIGI PATH FULL 1"), (91, "092: DIGI PATH FULL 2"), (92, "093: CALLSIGN (APRS)"), (93, "094: MESSAGE GROUP"), (94, "095: MESSAGE REPLY"), (95, "096: MY POSITION SET"), (96, "097: MY POSITION"), (97, "098: MY SYMBOL"), (98, "099: POSITION COMMENT"), (99, "100: SmartBeaconing"), (100, "101: SORT FILTER"), (101, "102: VOICE ALERT"), (102, "103: STATION LIST"), (103, "104: MESSAGE LIST"), (104, "105: BEACON TX SELECT"), (105, "106: BEACON TX"), (106, "107: BACKUP"), (107, "108: MEMORY INFO"), (108, "109: FORMAT"), (109, "110: JPG IMPORT"), (110, "111: Bluetooth"), (111, "112: VOICE MEMORY"), (112, "113: FVS REC"), (113, "114: TRACK SELECT"), (114, "115: PLAY"), (115, "116: STOP"), (116, "117: CLEAR"), (117, "118: VOICE GUIDE"), (118, "119: This → Other"), (119, "120: Other → This"), (120, "121: CALLSIGN"), (121, "122: MEMORY CH RESET"), (122, "123: APRS RESET"), (123, "124: CONFIG SET"), (124, "125: CONFIG RECALL"), (125, "126: SOFTWARE VERSION")] } }, + SF { key: "auto-dialer", byte: 0x000f9, mirror: 0x00000, kind: SK::Bool { shift: 2, active_low: false } }, + SF { key: "a-bell-ringer", byte: 0x0028e, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "OFF"), (1, "1 time"), (2, "3 times"), (3, "5 times"), (4, "8 times"), (5, "CONTINUOUS")] } }, + SF { key: "b-bell-ringer", byte: 0x0029e, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "OFF"), (1, "1 time"), (2, "3 times"), (3, "5 times"), (4, "8 times"), (5, "CONTINUOUS")] } }, + SF { key: "sql-expansion", byte: 0x000e3, mirror: 0x000f3, kind: SK::Bool { shift: 0, active_low: false } }, + SF { key: "wx-alert", byte: 0x000e2, mirror: 0x00000, kind: SK::Bool { shift: 6, active_low: false } }, + SF { key: "pager-rx-code-1", byte: 0x002d2, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "01"), (1, "02"), (2, "03"), (3, "04"), (4, "05"), (5, "06"), (6, "07"), (7, "08"), (8, "09"), (9, "10"), (10, "11"), (11, "12"), (12, "13"), (13, "14"), (14, "15"), (15, "16"), (16, "17"), (17, "18"), (18, "19"), (19, "20"), (20, "21"), (21, "22"), (22, "23"), (23, "24"), (24, "25"), (25, "26"), (26, "27"), (27, "28"), (28, "29"), (29, "30"), (30, "31"), (31, "32"), (32, "33"), (33, "34"), (34, "35"), (35, "36"), (36, "37"), (37, "38"), (38, "39"), (39, "40"), (40, "41"), (41, "42"), (42, "43"), (43, "44"), (44, "45"), (45, "46"), (46, "47"), (47, "48"), (48, "49"), (49, "50")] } }, + SF { key: "pager-rx-code-2", byte: 0x002d3, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "01"), (1, "02"), (2, "03"), (3, "04"), (4, "05"), (5, "06"), (6, "07"), (7, "08"), (8, "09"), (9, "10"), (10, "11"), (11, "12"), (12, "13"), (13, "14"), (14, "15"), (15, "16"), (16, "17"), (17, "18"), (18, "19"), (19, "20"), (20, "21"), (21, "22"), (22, "23"), (23, "24"), (24, "25"), (25, "26"), (26, "27"), (27, "28"), (28, "29"), (29, "30"), (30, "31"), (31, "32"), (32, "33"), (33, "34"), (34, "35"), (35, "36"), (36, "37"), (37, "38"), (38, "39"), (39, "40"), (40, "41"), (41, "42"), (42, "43"), (43, "44"), (44, "45"), (45, "46"), (46, "47"), (47, "48"), (48, "49"), (49, "50")] } }, + SF { key: "pager-tx-code-1", byte: 0x002d4, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "01"), (1, "02"), (2, "03"), (3, "04"), (4, "05"), (5, "06"), (6, "07"), (7, "08"), (8, "09"), (9, "10"), (10, "11"), (11, "12"), (12, "13"), (13, "14"), (14, "15"), (15, "16"), (16, "17"), (17, "18"), (18, "19"), (19, "20"), (20, "21"), (21, "22"), (22, "23"), (23, "24"), (24, "25"), (25, "26"), (26, "27"), (27, "28"), (28, "29"), (29, "30"), (30, "31"), (31, "32"), (32, "33"), (33, "34"), (34, "35"), (35, "36"), (36, "37"), (37, "38"), (38, "39"), (39, "40"), (40, "41"), (41, "42"), (42, "43"), (43, "44"), (44, "45"), (45, "46"), (46, "47"), (47, "48"), (48, "49"), (49, "50")] } }, + SF { key: "pager-tx-code-2", byte: 0x002d5, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "01"), (1, "02"), (2, "03"), (3, "04"), (4, "05"), (5, "06"), (6, "07"), (7, "08"), (8, "09"), (9, "10"), (10, "11"), (11, "12"), (12, "13"), (13, "14"), (14, "15"), (15, "16"), (16, "17"), (17, "18"), (18, "19"), (19, "20"), (20, "21"), (21, "22"), (22, "23"), (23, "24"), (24, "25"), (25, "26"), (26, "27"), (27, "28"), (28, "29"), (29, "30"), (30, "31"), (31, "32"), (32, "33"), (33, "34"), (34, "35"), (35, "36"), (36, "37"), (37, "38"), (38, "39"), (39, "40"), (40, "41"), (41, "42"), (42, "43"), (43, "44"), (44, "45"), (45, "46"), (46, "47"), (47, "48"), (48, "49"), (49, "50")] } }, + SF { key: "dtmf-memory-1", byte: 0x00300, mirror: 0x00000, kind: SK::Dtmf { len: 16 } }, + SF { key: "dtmf-memory-2", byte: 0x00310, mirror: 0x00000, kind: SK::Dtmf { len: 16 } }, + SF { key: "dtmf-memory-3", byte: 0x00320, mirror: 0x00000, kind: SK::Dtmf { len: 16 } }, + SF { key: "dtmf-memory-4", byte: 0x00330, mirror: 0x00000, kind: SK::Dtmf { len: 16 } }, + SF { key: "dtmf-memory-5", byte: 0x00340, mirror: 0x00000, kind: SK::Dtmf { len: 16 } }, + SF { key: "dtmf-memory-6", byte: 0x00350, mirror: 0x00000, kind: SK::Dtmf { len: 16 } }, + SF { key: "dtmf-memory-7", byte: 0x00360, mirror: 0x00000, kind: SK::Dtmf { len: 16 } }, + SF { key: "dtmf-memory-8", byte: 0x00370, mirror: 0x00000, kind: SK::Dtmf { len: 16 } }, + SF { key: "dtmf-memory-9", byte: 0x00380, mirror: 0x00000, kind: SK::Dtmf { len: 16 } }, + SF { key: "ams-tx-mode", byte: 0x000ef, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "AUTO"), (1, "TX FM FIXED"), (2, "TX DN FIXED")] } }, + SF { key: "digital-popup", byte: 0x002db, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "OFF"), (1, "2 sec"), (2, "4 sec"), (3, "6 sec"), (4, "8 sec"), (5, "10 sec"), (6, "20 sec"), (7, "30 sec"), (8, "60 sec"), (255, "CONTINUE")] } }, + SF { key: "location-service", byte: 0x000eb, mirror: 0x00000, kind: SK::Bool { shift: 7, active_low: false } }, + SF { key: "standby-beep", byte: 0x000e8, mirror: 0x00000, kind: SK::Bool { shift: 4, active_low: true } }, + SF { key: "digital-vw", byte: 0x000e8, mirror: 0x00000, kind: SK::Bool { shift: 1, active_low: false } }, + SF { key: "gm-range-ringer", byte: 0x000f8, mirror: 0x00000, kind: SK::Bool { shift: 5, active_low: false } }, + SF { key: "wires-x-rpt-wires-freq", byte: 0x000f8, mirror: 0x00000, kind: SK::Enum { shift: 4, width: 1, labels: &[(0, "MANUAL"), (1, "PRESET")] } }, + SF { key: "wires-x-search-setup", byte: 0x000f8, mirror: 0x00000, kind: SK::Enum { shift: 3, width: 1, labels: &[(0, "HISTORY"), (1, "ACTIVITY")] } }, + SF { key: "wires-x-dg-id", byte: 0x0029f, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "AUTO"), (1, "01"), (2, "02"), (3, "03"), (4, "04"), (5, "05"), (6, "06"), (7, "07"), (8, "08"), (9, "09"), (10, "10"), (11, "11"), (12, "12"), (13, "13"), (14, "14"), (15, "15"), (16, "16"), (17, "17"), (18, "18"), (19, "19"), (20, "20"), (21, "21"), (22, "22"), (23, "23"), (24, "24"), (25, "25"), (26, "26"), (27, "27"), (28, "28"), (29, "29"), (30, "30"), (31, "31"), (32, "32"), (33, "33"), (34, "34"), (35, "35"), (36, "36"), (37, "37"), (38, "38"), (39, "39"), (40, "40"), (41, "41"), (42, "42"), (43, "43"), (44, "44"), (45, "45"), (46, "46"), (47, "47"), (48, "48"), (49, "49"), (50, "50"), (51, "51"), (52, "52"), (53, "53"), (54, "54"), (55, "55"), (56, "56"), (57, "57"), (58, "58"), (59, "59"), (60, "60"), (61, "61"), (62, "62"), (63, "63"), (64, "64"), (65, "65"), (66, "66"), (67, "67"), (68, "68"), (69, "69"), (70, "70"), (71, "71"), (72, "72"), (73, "73"), (74, "74"), (75, "75"), (76, "76"), (77, "77"), (78, "78"), (79, "79"), (80, "80"), (81, "81"), (82, "82"), (83, "83"), (84, "84"), (85, "85"), (86, "86"), (87, "87"), (88, "88"), (89, "89"), (90, "90"), (91, "91"), (92, "92"), (93, "93"), (94, "94"), (95, "95"), (96, "96"), (97, "97"), (98, "98"), (99, "99")] } }, + SF { key: "bluetooth", byte: 0x000f9, mirror: 0x00000, kind: SK::Bool { shift: 1, active_low: false } }, + SF { key: "bluetooth-audio", byte: 0x000f9, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 1, labels: &[(0, "AUTO"), (1, "FIX")] } }, + SF { key: "com-port-speed", byte: 0x00549, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "4800bps"), (1, "9600bps"), (2, "19200bps"), (3, "38400bps"), (4, "57600bps")] } }, + SF { key: "com-port-output", byte: 0x00548, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "OFF"), (1, "GPS OUT"), (2, "PACKET"), (3, "WAY POINT")] } }, + SF { key: "wp-format", byte: 0x00562, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "NMEA9"), (1, "NMEA8"), (2, "NMEA7"), (3, "NMEA6")] } }, + SF { key: "wp-filter", byte: 0x00563, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "ALL"), (1, "MOBILE"), (2, "FREQUENCY"), (3, "OBJECT/ITEM"), (4, "DIGIPEATER"), (5, "VoIP"), (6, "WEATHER"), (7, "YAESU"), (9, "CALL RINGER"), (10, "RNG RINGER")] } }, + SF { key: "aprs-band", byte: 0x00543, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "MAIN"), (1, "SUB"), (2, "THIS BAND(A)"), (3, "OTHER BAND(B)")] } }, + SF { key: "data-band", byte: 0x00544, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "MAIN"), (1, "SUB"), (2, "THIS BAND(A)"), (3, "OTHER BAND(B)")] } }, + SF { key: "aprs-speed", byte: 0x00534, mirror: 0x00000, kind: SK::Enum { shift: 6, width: 1, labels: &[(0, "1200 bps"), (1, "9600 bps")] } }, + SF { key: "data-speed", byte: 0x00534, mirror: 0x00000, kind: SK::Enum { shift: 5, width: 1, labels: &[(0, "1200 bps"), (1, "9600 bps")] } }, + SF { key: "aprs-squelch", byte: 0x00535, mirror: 0x00000, kind: SK::Enum { shift: 7, width: 1, labels: &[(0, "RX BAND"), (1, "TX/RX BAND")] } }, + SF { key: "data-squelch", byte: 0x00535, mirror: 0x00000, kind: SK::Enum { shift: 6, width: 1, labels: &[(0, "RX BAND"), (1, "TX/RX BAND")] } }, + SF { key: "data-squelch-tx", byte: 0x00535, mirror: 0x00000, kind: SK::Bool { shift: 5, active_low: false } }, + SF { key: "aprs-modem", byte: 0x00534, mirror: 0x00000, kind: SK::Bool { shift: 0, active_low: false } }, + SF { key: "aprs-mute", byte: 0x00534, mirror: 0x00000, kind: SK::Bool { shift: 1, active_low: false } }, + SF { key: "aprs-tx-delay", byte: 0x00547, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "100ms"), (1, "150ms"), (2, "200ms"), (3, "250ms"), (4, "300ms"), (5, "400ms"), (6, "500ms"), (7, "750ms"), (8, "1000ms")] } }, + SF { key: "aprs-callsign", byte: 0x00508, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "aprs-callsign-ssid", byte: 0x0050e, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "position-comment", byte: 0x00532, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "Off Duty"), (1, "En Route"), (2, "In Service"), (3, "Returning"), (4, "Committed"), (5, "Special"), (6, "Priority"), (7, "Custom 0"), (8, "Custom 1"), (9, "Custom 2"), (10, "Custom 3"), (11, "Custom 4"), (12, "Custom 5"), (13, "Custom 6"), (14, "EMERGENCY!")] } }, + SF { key: "my-position", byte: 0x00541, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "GPS"), (1, "MANUAL")] } }, + SF { key: "my-symbol", byte: 0x0052c, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "1"), (1, "2"), (2, "3"), (3, "4")] } }, + SF { key: "my-symbol-1", byte: 0x00522, mirror: 0x00000, kind: SK::Enum16 { labels: &[(12067, "/#"), (12070, "/&"), (12071, "/'"), (12077, "/-"), (12078, "/."), (12080, "/0"), (12090, "/:"), (12091, "/;"), (12092, "/<"), (12093, "/="), (12094, "/>"), (12099, "/C"), (12101, "/E"), (12105, "/I"), (12107, "/K"), (12111, "/O"), (12112, "/P"), (12114, "/R"), (12116, "/T"), (12117, "/U"), (12118, "/V"), (12119, "/W"), (12120, "/X"), (12121, "/Y"), (12123, "/["), (12124, "/\\"), (12126, "/^"), (12127, "/_"), (12129, "/a"), (12130, "/b"), (12134, "/f"), (12135, "/g"), (12138, "/j"), (12139, "/k"), (12141, "/m"), (12146, "/r"), (12147, "/s"), (12149, "/u"), (12150, "/v"), (12153, "/y"), (23587, "\\#"), (23590, "\\&"), (23597, "\\-"), (23598, "\\."), (23600, "\\0"), (17712, "E0"), (18736, "I0"), (22320, "W0"), (23611, "\\;"), (23614, "\\>"), (23617, "\\A"), (23627, "\\K"), (23639, "\\W"), (23641, "\\Y"), (19289, "KY"), (22873, "YY"), (23646, "\\^"), (23647, "\\_"), (23661, "\\m"), (23662, "\\n"), (23667, "\\s"), (23669, "\\u"), (23670, "\\v"), (23672, "\\x")] } }, + SF { key: "my-symbol-2", byte: 0x00524, mirror: 0x00000, kind: SK::Enum16 { labels: &[(12067, "/#"), (12070, "/&"), (12071, "/'"), (12077, "/-"), (12078, "/."), (12080, "/0"), (12090, "/:"), (12091, "/;"), (12092, "/<"), (12093, "/="), (12094, "/>"), (12099, "/C"), (12101, "/E"), (12105, "/I"), (12107, "/K"), (12111, "/O"), (12112, "/P"), (12114, "/R"), (12116, "/T"), (12117, "/U"), (12118, "/V"), (12119, "/W"), (12120, "/X"), (12121, "/Y"), (12123, "/["), (12124, "/\\"), (12126, "/^"), (12127, "/_"), (12129, "/a"), (12130, "/b"), (12134, "/f"), (12135, "/g"), (12138, "/j"), (12139, "/k"), (12141, "/m"), (12146, "/r"), (12147, "/s"), (12149, "/u"), (12150, "/v"), (12153, "/y"), (23587, "\\#"), (23590, "\\&"), (23597, "\\-"), (23598, "\\."), (23600, "\\0"), (17712, "E0"), (18736, "I0"), (22320, "W0"), (23611, "\\;"), (23614, "\\>"), (23617, "\\A"), (23627, "\\K"), (23639, "\\W"), (23641, "\\Y"), (19289, "KY"), (22873, "YY"), (23646, "\\^"), (23647, "\\_"), (23661, "\\m"), (23662, "\\n"), (23667, "\\s"), (23669, "\\u"), (23670, "\\v"), (23672, "\\x")] } }, + SF { key: "my-symbol-3", byte: 0x00526, mirror: 0x00000, kind: SK::Enum16 { labels: &[(12067, "/#"), (12070, "/&"), (12071, "/'"), (12077, "/-"), (12078, "/."), (12080, "/0"), (12090, "/:"), (12091, "/;"), (12092, "/<"), (12093, "/="), (12094, "/>"), (12099, "/C"), (12101, "/E"), (12105, "/I"), (12107, "/K"), (12111, "/O"), (12112, "/P"), (12114, "/R"), (12116, "/T"), (12117, "/U"), (12118, "/V"), (12119, "/W"), (12120, "/X"), (12121, "/Y"), (12123, "/["), (12124, "/\\"), (12126, "/^"), (12127, "/_"), (12129, "/a"), (12130, "/b"), (12134, "/f"), (12135, "/g"), (12138, "/j"), (12139, "/k"), (12141, "/m"), (12146, "/r"), (12147, "/s"), (12149, "/u"), (12150, "/v"), (12153, "/y"), (23587, "\\#"), (23590, "\\&"), (23597, "\\-"), (23598, "\\."), (23600, "\\0"), (17712, "E0"), (18736, "I0"), (22320, "W0"), (23611, "\\;"), (23614, "\\>"), (23617, "\\A"), (23627, "\\K"), (23639, "\\W"), (23641, "\\Y"), (19289, "KY"), (22873, "YY"), (23646, "\\^"), (23647, "\\_"), (23661, "\\m"), (23662, "\\n"), (23667, "\\s"), (23669, "\\u"), (23670, "\\v"), (23672, "\\x")] } }, + SF { key: "my-symbol-4-table", byte: 0x00528, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(47, "/"), (48, "0"), (49, "1"), (50, "2"), (51, "3"), (52, "4"), (53, "5"), (54, "6"), (55, "7"), (56, "8"), (57, "9"), (65, "A"), (66, "B"), (67, "C"), (68, "D"), (69, "E"), (70, "F"), (71, "G"), (72, "H"), (73, "I"), (74, "J"), (75, "K"), (76, "L"), (77, "M"), (78, "N"), (79, "O"), (80, "P"), (81, "Q"), (82, "R"), (83, "S"), (84, "T"), (85, "U"), (86, "V"), (87, "W"), (88, "X"), (89, "Y"), (90, "Z"), (92, "\\")] } }, + SF { key: "beacon-auto", byte: 0x00533, mirror: 0x00000, kind: SK::Bool { shift: 6, active_low: false } }, + SF { key: "beacon-interval", byte: 0x0052e, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "30 sec"), (1, "1 min"), (2, "2 min"), (3, "3 min"), (4, "5 min"), (5, "10 min"), (6, "15 min"), (7, "20 min"), (8, "30 min"), (9, "60 min")] } }, + SF { key: "beacon-proportional", byte: 0x00533, mirror: 0x00000, kind: SK::Bool { shift: 7, active_low: false } }, + SF { key: "beacon-decay", byte: 0x00533, mirror: 0x00000, kind: SK::Bool { shift: 4, active_low: false } }, + SF { key: "beacon-low-speed", byte: 0x00530, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(1, "1mph"), (2, "2mph"), (3, "3mph"), (4, "4mph"), (5, "5mph"), (6, "6mph"), (7, "7mph"), (8, "8mph"), (9, "9mph"), (10, "10mph"), (11, "11mph"), (12, "12mph"), (13, "13mph"), (14, "14mph"), (15, "15mph"), (16, "16mph"), (17, "17mph"), (18, "18mph"), (19, "19mph"), (20, "20mph"), (21, "21mph"), (22, "22mph"), (23, "23mph"), (24, "24mph"), (25, "25mph"), (26, "26mph"), (27, "27mph"), (28, "28mph"), (29, "29mph"), (30, "30mph"), (31, "31mph"), (32, "32mph"), (33, "33mph"), (34, "34mph"), (35, "35mph"), (36, "36mph"), (37, "37mph"), (38, "38mph"), (39, "39mph"), (40, "40mph"), (41, "41mph"), (42, "42mph"), (43, "43mph"), (44, "44mph"), (45, "45mph"), (46, "46mph"), (47, "47mph"), (48, "48mph"), (49, "49mph"), (50, "50mph"), (51, "51mph"), (52, "52mph"), (53, "53mph"), (54, "54mph"), (55, "55mph"), (56, "56mph"), (57, "57mph"), (58, "58mph"), (59, "59mph"), (60, "60mph"), (61, "61mph"), (62, "62mph"), (63, "63mph"), (64, "64mph"), (65, "65mph"), (66, "66mph"), (67, "67mph"), (68, "68mph"), (69, "69mph"), (70, "70mph"), (71, "71mph"), (72, "72mph"), (73, "73mph"), (74, "74mph"), (75, "75mph"), (76, "76mph"), (77, "77mph"), (78, "78mph"), (79, "79mph"), (80, "80mph"), (81, "81mph"), (82, "82mph"), (83, "83mph"), (84, "84mph"), (85, "85mph"), (86, "86mph"), (87, "87mph"), (88, "88mph"), (89, "89mph"), (90, "90mph"), (91, "91mph"), (92, "92mph"), (93, "93mph"), (94, "94mph"), (95, "95mph"), (96, "96mph"), (97, "97mph"), (98, "98mph"), (99, "99mph")] } }, + SF { key: "beacon-rate-limit", byte: 0x0052f, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(5, "5 sec"), (6, "6 sec"), (7, "7 sec"), (8, "8 sec"), (9, "9 sec"), (10, "10 sec"), (11, "11 sec"), (12, "12 sec"), (13, "13 sec"), (14, "14 sec"), (15, "15 sec"), (16, "16 sec"), (17, "17 sec"), (18, "18 sec"), (19, "19 sec"), (20, "20 sec"), (21, "21 sec"), (22, "22 sec"), (23, "23 sec"), (24, "24 sec"), (25, "25 sec"), (26, "26 sec"), (27, "27 sec"), (28, "28 sec"), (29, "29 sec"), (30, "30 sec"), (31, "31 sec"), (32, "32 sec"), (33, "33 sec"), (34, "34 sec"), (35, "35 sec"), (36, "36 sec"), (37, "37 sec"), (38, "38 sec"), (39, "39 sec"), (40, "40 sec"), (41, "41 sec"), (42, "42 sec"), (43, "43 sec"), (44, "44 sec"), (45, "45 sec"), (46, "46 sec"), (47, "47 sec"), (48, "48 sec"), (49, "49 sec"), (50, "50 sec"), (51, "51 sec"), (52, "52 sec"), (53, "53 sec"), (54, "54 sec"), (55, "55 sec"), (56, "56 sec"), (57, "57 sec"), (58, "58 sec"), (59, "59 sec"), (60, "60 sec"), (61, "61 sec"), (62, "62 sec"), (63, "63 sec"), (64, "64 sec"), (65, "65 sec"), (66, "66 sec"), (67, "67 sec"), (68, "68 sec"), (69, "69 sec"), (70, "70 sec"), (71, "71 sec"), (72, "72 sec"), (73, "73 sec"), (74, "74 sec"), (75, "75 sec"), (76, "76 sec"), (77, "77 sec"), (78, "78 sec"), (79, "79 sec"), (80, "80 sec"), (81, "81 sec"), (82, "82 sec"), (83, "83 sec"), (84, "84 sec"), (85, "85 sec"), (86, "86 sec"), (87, "87 sec"), (88, "88 sec"), (89, "89 sec"), (90, "90 sec"), (91, "91 sec"), (92, "92 sec"), (93, "93 sec"), (94, "94 sec"), (95, "95 sec"), (96, "96 sec"), (97, "97 sec"), (98, "98 sec"), (99, "99 sec"), (100, "100 sec"), (101, "101 sec"), (102, "102 sec"), (103, "103 sec"), (104, "104 sec"), (105, "105 sec"), (106, "106 sec"), (107, "107 sec"), (108, "108 sec"), (109, "109 sec"), (110, "110 sec"), (111, "111 sec"), (112, "112 sec"), (113, "113 sec"), (114, "114 sec"), (115, "115 sec"), (116, "116 sec"), (117, "117 sec"), (118, "118 sec"), (119, "119 sec"), (120, "120 sec"), (121, "121 sec"), (122, "122 sec"), (123, "123 sec"), (124, "124 sec"), (125, "125 sec"), (126, "126 sec"), (127, "127 sec"), (128, "128 sec"), (129, "129 sec"), (130, "130 sec"), (131, "131 sec"), (132, "132 sec"), (133, "133 sec"), (134, "134 sec"), (135, "135 sec"), (136, "136 sec"), (137, "137 sec"), (138, "138 sec"), (139, "139 sec"), (140, "140 sec"), (141, "141 sec"), (142, "142 sec"), (143, "143 sec"), (144, "144 sec"), (145, "145 sec"), (146, "146 sec"), (147, "147 sec"), (148, "148 sec"), (149, "149 sec"), (150, "150 sec"), (151, "151 sec"), (152, "152 sec"), (153, "153 sec"), (154, "154 sec"), (155, "155 sec"), (156, "156 sec"), (157, "157 sec"), (158, "158 sec"), (159, "159 sec"), (160, "160 sec"), (161, "161 sec"), (162, "162 sec"), (163, "163 sec"), (164, "164 sec"), (165, "165 sec"), (166, "166 sec"), (167, "167 sec"), (168, "168 sec"), (169, "169 sec"), (170, "170 sec"), (171, "171 sec"), (172, "172 sec"), (173, "173 sec"), (174, "174 sec"), (175, "175 sec"), (176, "176 sec"), (177, "177 sec"), (178, "178 sec"), (179, "179 sec"), (180, "180 sec")] } }, + SF { key: "beacon-ambiguity", byte: 0x00531, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "OFF"), (1, "1digit"), (2, "2digits"), (3, "3digits"), (4, "4digits")] } }, + SF { key: "beacon-speed-course", byte: 0x00533, mirror: 0x00000, kind: SK::Bool { shift: 3, active_low: false } }, + SF { key: "beacon-altitude", byte: 0x00533, mirror: 0x00000, kind: SK::Bool { shift: 2, active_low: false } }, + SF { key: "beacon-status-text", byte: 0x00536, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "OFF"), (1, "1"), (2, "2"), (3, "3"), (4, "4"), (5, "5")] } }, + SF { key: "beacon-status-tx-rate", byte: 0x00537, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "1/1"), (1, "1/2"), (2, "1/3"), (3, "1/4"), (4, "1/5"), (5, "1/6"), (6, "1/7"), (7, "1/8"), (8, "1/2(FREQ)"), (9, "1/3(FREQ)"), (10, "1/4(FREQ)"), (11, "1/5(FREQ)"), (12, "1/6(FREQ)"), (13, "1/7(FREQ)"), (14, "1/8(FREQ)")] } }, + SF { key: "beacon-status-text-1", byte: 0x07e00, mirror: 0x00000, kind: SK::Text { len: 60, pad: 0xca, set: TS::Plain } }, + SF { key: "beacon-status-text-1-freq", byte: 0x07e3c, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "NONE"), (1, "FREQUENCY"), (2, "FREQ & SQL & SHIFT")] } }, + SF { key: "beacon-status-text-2", byte: 0x07e40, mirror: 0x00000, kind: SK::Text { len: 60, pad: 0xca, set: TS::Plain } }, + SF { key: "beacon-status-text-2-freq", byte: 0x07e7c, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "NONE"), (1, "FREQUENCY"), (2, "FREQ & SQL & SHIFT")] } }, + SF { key: "beacon-status-text-3", byte: 0x07e80, mirror: 0x00000, kind: SK::Text { len: 60, pad: 0xca, set: TS::Plain } }, + SF { key: "beacon-status-text-3-freq", byte: 0x07ebc, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "NONE"), (1, "FREQUENCY"), (2, "FREQ & SQL & SHIFT")] } }, + SF { key: "beacon-status-text-4", byte: 0x07ec0, mirror: 0x00000, kind: SK::Text { len: 60, pad: 0xca, set: TS::Plain } }, + SF { key: "beacon-status-text-4-freq", byte: 0x07efc, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "NONE"), (1, "FREQUENCY"), (2, "FREQ & SQL & SHIFT")] } }, + SF { key: "beacon-status-text-5", byte: 0x07f00, mirror: 0x00000, kind: SK::Text { len: 60, pad: 0xca, set: TS::Plain } }, + SF { key: "beacon-status-text-5-freq", byte: 0x07f3c, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "NONE"), (1, "FREQUENCY"), (2, "FREQ & SQL & SHIFT")] } }, + SF { key: "digi-path", byte: 0x0052d, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "OFF"), (1, "WIDE1-1"), (2, "WIDE1-1, WIDE2-1"), (3, "PATH1"), (4, "PATH2"), (5, "PATH3"), (6, "PATH4"), (7, "FULL1"), (8, "FULL2")] } }, + SF { key: "digi-path1-1", byte: 0x00680, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "digi-path1-1-ssid", byte: 0x00686, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "digi-path1-2", byte: 0x00688, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "digi-path1-2-ssid", byte: 0x0068e, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "digi-path2-1", byte: 0x00690, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "digi-path2-1-ssid", byte: 0x00696, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "digi-path2-2", byte: 0x00698, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "digi-path2-2-ssid", byte: 0x0069e, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "digi-path3-1", byte: 0x006a0, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "digi-path3-1-ssid", byte: 0x006a6, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "digi-path3-2", byte: 0x006a8, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "digi-path3-2-ssid", byte: 0x006ae, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "digi-path4-1", byte: 0x006b0, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "digi-path4-1-ssid", byte: 0x006b6, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "digi-path4-2", byte: 0x006b8, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "digi-path4-2-ssid", byte: 0x006be, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "digi-full1-1", byte: 0x006c0, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "digi-full1-1-ssid", byte: 0x006c6, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "digi-full1-2", byte: 0x006c8, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "digi-full1-2-ssid", byte: 0x006ce, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "digi-full1-3", byte: 0x006d0, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "digi-full1-3-ssid", byte: 0x006d6, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "digi-full1-4", byte: 0x006d8, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "digi-full1-4-ssid", byte: 0x006de, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "digi-full1-5", byte: 0x006e0, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "digi-full1-5-ssid", byte: 0x006e6, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "digi-full1-6", byte: 0x006e8, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "digi-full1-6-ssid", byte: 0x006ee, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "digi-full1-7", byte: 0x006f0, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "digi-full1-7-ssid", byte: 0x006f6, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "digi-full1-8", byte: 0x006f8, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "digi-full1-8-ssid", byte: 0x006fe, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "digi-full2-1", byte: 0x00700, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "digi-full2-1-ssid", byte: 0x00706, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "digi-full2-2", byte: 0x00708, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "digi-full2-2-ssid", byte: 0x0070e, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "digi-full2-3", byte: 0x00710, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "digi-full2-3-ssid", byte: 0x00716, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "digi-full2-4", byte: 0x00718, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "digi-full2-4-ssid", byte: 0x0071e, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "digi-full2-5", byte: 0x00720, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "digi-full2-5-ssid", byte: 0x00726, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "digi-full2-6", byte: 0x00728, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "digi-full2-6-ssid", byte: 0x0072e, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "digi-full2-7", byte: 0x00730, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "digi-full2-7-ssid", byte: 0x00736, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "digi-full2-8", byte: 0x00738, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "digi-full2-8-ssid", byte: 0x0073e, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "aprs-message-text", byte: 0x00560, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "1"), (1, "2"), (2, "3"), (3, "4"), (4, "5"), (5, "6"), (6, "7"), (7, "8")] } }, + SF { key: "aprs-message-text-1", byte: 0x00780, mirror: 0x00000, kind: SK::Text { len: 16, pad: 0xca, set: TS::Plain } }, + SF { key: "aprs-message-text-2", byte: 0x00790, mirror: 0x00000, kind: SK::Text { len: 16, pad: 0xca, set: TS::Plain } }, + SF { key: "aprs-message-text-3", byte: 0x007a0, mirror: 0x00000, kind: SK::Text { len: 16, pad: 0xca, set: TS::Plain } }, + SF { key: "aprs-message-text-4", byte: 0x007b0, mirror: 0x00000, kind: SK::Text { len: 16, pad: 0xca, set: TS::Plain } }, + SF { key: "aprs-message-text-5", byte: 0x007c0, mirror: 0x00000, kind: SK::Text { len: 16, pad: 0xca, set: TS::Plain } }, + SF { key: "aprs-message-text-6", byte: 0x007d0, mirror: 0x00000, kind: SK::Text { len: 16, pad: 0xca, set: TS::Plain } }, + SF { key: "aprs-message-text-7", byte: 0x007e0, mirror: 0x00000, kind: SK::Text { len: 16, pad: 0xca, set: TS::Plain } }, + SF { key: "aprs-message-text-8", byte: 0x007f0, mirror: 0x00000, kind: SK::Text { len: 16, pad: 0xca, set: TS::Plain } }, + SF { key: "aprs-message-group-1", byte: 0x00600, mirror: 0x00000, kind: SK::Text { len: 9, pad: 0xca, set: TS::Group } }, + SF { key: "aprs-message-group-2", byte: 0x0060a, mirror: 0x00000, kind: SK::Text { len: 9, pad: 0xca, set: TS::Group } }, + SF { key: "aprs-message-group-3", byte: 0x00614, mirror: 0x00000, kind: SK::Text { len: 9, pad: 0xca, set: TS::Group } }, + SF { key: "aprs-message-group-4", byte: 0x0061e, mirror: 0x00000, kind: SK::Text { len: 9, pad: 0xca, set: TS::Group } }, + SF { key: "aprs-message-group-5", byte: 0x00628, mirror: 0x00000, kind: SK::Text { len: 9, pad: 0xca, set: TS::Group } }, + SF { key: "aprs-message-group-6", byte: 0x00632, mirror: 0x00000, kind: SK::Text { len: 9, pad: 0xca, set: TS::Group } }, + SF { key: "aprs-bulletin-1", byte: 0x0063c, mirror: 0x00000, kind: SK::Text { len: 9, pad: 0xca, set: TS::Group } }, + SF { key: "aprs-bulletin-2", byte: 0x00646, mirror: 0x00000, kind: SK::Text { len: 9, pad: 0xca, set: TS::Group } }, + SF { key: "aprs-bulletin-3", byte: 0x00650, mirror: 0x00000, kind: SK::Text { len: 9, pad: 0xca, set: TS::Group } }, + SF { key: "message-reply", byte: 0x00533, mirror: 0x00000, kind: SK::Bool { shift: 0, active_low: false } }, + SF { key: "message-reply-text", byte: 0x00580, mirror: 0x00000, kind: SK::Text { len: 64, pad: 0xca, set: TS::Plain } }, + SF { key: "aprs-popup-beacon", byte: 0x00545, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "OFF"), (3, "3 sec"), (5, "5 sec"), (10, "10 sec"), (128, "HOLD"), (67, "CALL 3 sec"), (69, "CALL 5 sec"), (74, "CALL 10 sec"), (192, "CALL HOLD")] } }, + SF { key: "aprs-popup-message", byte: 0x00546, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "OFF"), (3, "3 sec"), (5, "5 sec"), (10, "10 sec"), (128, "HOLD"), (67, "CALL 3 sec"), (69, "CALL 5 sec"), (74, "CALL 10 sec"), (192, "CALL HOLD")] } }, + SF { key: "aprs-popup-my-packet", byte: 0x00561, mirror: 0x00000, kind: SK::Bool { shift: 6, active_low: false } }, + SF { key: "aprs-sort", byte: 0x00570, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "TIME"), (1, "CALLSIGN"), (2, "DISTANCE")] } }, + SF { key: "aprs-filter", byte: 0x00571, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "ALL"), (1, "MOBILE"), (2, "FREQUENCY"), (3, "OBJECT/ITEM"), (4, "DIGIPEATER"), (5, "VoIP"), (6, "WEATHER"), (7, "YAESU"), (8, "OTHER PACKET"), (9, "CALL RINGER"), (10, "RANGE RINGER"), (11, "1200 bps"), (12, "9600 bps")] } }, + SF { key: "voice-alert", byte: 0x0056d, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "NORMAL"), (1, "TONE SQL"), (2, "DCS"), (3, "RX-TSQL"), (4, "RX-DCS")] } }, + SF { key: "voice-alert-tone", byte: 0x0056e, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "67.0Hz"), (1, "69.3Hz"), (2, "71.9Hz"), (3, "74.4Hz"), (4, "77.0Hz"), (5, "79.7Hz"), (6, "82.5Hz"), (7, "85.4Hz"), (8, "88.5Hz"), (9, "91.5Hz"), (10, "94.8Hz"), (11, "97.4Hz"), (12, "100.0Hz"), (13, "103.5Hz"), (14, "107.2Hz"), (15, "110.9Hz"), (16, "114.8Hz"), (17, "118.8Hz"), (18, "123.0Hz"), (19, "127.3Hz"), (20, "131.8Hz"), (21, "136.5Hz"), (22, "141.3Hz"), (23, "146.2Hz"), (24, "151.4Hz"), (25, "156.7Hz"), (26, "159.8Hz"), (27, "162.2Hz"), (28, "165.5Hz"), (29, "167.9Hz"), (30, "171.3Hz"), (31, "173.8Hz"), (32, "177.3Hz"), (33, "179.9Hz"), (34, "183.5Hz"), (35, "186.2Hz"), (36, "189.9Hz"), (37, "192.8Hz"), (38, "196.6Hz"), (39, "199.5Hz"), (40, "203.5Hz"), (41, "206.5Hz"), (42, "210.7Hz"), (43, "218.1Hz"), (44, "225.7Hz"), (45, "229.1Hz"), (46, "233.6Hz"), (47, "241.8Hz"), (48, "250.3Hz"), (49, "254.1Hz")] } }, + SF { key: "voice-alert-dcs", byte: 0x0056f, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "023"), (1, "025"), (2, "026"), (3, "031"), (4, "032"), (5, "036"), (6, "043"), (7, "047"), (8, "051"), (9, "053"), (10, "054"), (11, "065"), (12, "071"), (13, "072"), (14, "073"), (15, "074"), (16, "114"), (17, "115"), (18, "116"), (19, "122"), (20, "125"), (21, "131"), (22, "132"), (23, "134"), (24, "143"), (25, "145"), (26, "152"), (27, "155"), (28, "156"), (29, "162"), (30, "165"), (31, "172"), (32, "174"), (33, "205"), (34, "212"), (35, "223"), (36, "225"), (37, "226"), (38, "243"), (39, "244"), (40, "245"), (41, "246"), (42, "251"), (43, "252"), (44, "255"), (45, "261"), (46, "263"), (47, "265"), (48, "266"), (49, "271"), (50, "274"), (51, "306"), (52, "311"), (53, "315"), (54, "325"), (55, "331"), (56, "332"), (57, "343"), (58, "346"), (59, "351"), (60, "356"), (61, "364"), (62, "365"), (63, "371"), (64, "411"), (65, "412"), (66, "413"), (67, "423"), (68, "431"), (69, "432"), (70, "445"), (71, "446"), (72, "452"), (73, "454"), (74, "455"), (75, "462"), (76, "464"), (77, "465"), (78, "466"), (79, "503"), (80, "506"), (81, "516"), (82, "523"), (83, "526"), (84, "532"), (85, "546"), (86, "565"), (87, "606"), (88, "612"), (89, "624"), (90, "627"), (91, "631"), (92, "632"), (93, "654"), (94, "662"), (95, "664"), (96, "703"), (97, "712"), (98, "723"), (99, "731"), (100, "732"), (101, "734"), (102, "743"), (103, "754")] } }, + SF { key: "aprs-filter-mic-e", byte: 0x00538, mirror: 0x00000, kind: SK::Bool { shift: 0, active_low: false } }, + SF { key: "aprs-filter-position", byte: 0x00538, mirror: 0x00000, kind: SK::Bool { shift: 1, active_low: false } }, + SF { key: "aprs-filter-weather", byte: 0x00538, mirror: 0x00000, kind: SK::Bool { shift: 2, active_low: false } }, + SF { key: "aprs-filter-object", byte: 0x00538, mirror: 0x00000, kind: SK::Bool { shift: 3, active_low: false } }, + SF { key: "aprs-filter-item", byte: 0x00538, mirror: 0x00000, kind: SK::Bool { shift: 4, active_low: false } }, + SF { key: "aprs-filter-status", byte: 0x00538, mirror: 0x00000, kind: SK::Bool { shift: 5, active_low: false } }, + SF { key: "aprs-filter-other", byte: 0x00538, mirror: 0x00000, kind: SK::Bool { shift: 6, active_low: false } }, + SF { key: "aprs-filter-altnet", byte: 0x00561, mirror: 0x00000, kind: SK::Bool { shift: 5, active_low: false } }, + SF { key: "aprs-range-limit", byte: 0x0052a, mirror: 0x00000, kind: SK::Enum16 { labels: &[(0, "OFF"), (1, "1 km(mi)"), (10, "10 km(mi)"), (100, "100 km(mi)"), (1000, "1000 km(mi)"), (3000, "3000 km(mi)")] } }, + SF { key: "aprs-ringer-tx-beacon", byte: 0x00539, mirror: 0x00000, kind: SK::Bool { shift: 0, active_low: false } }, + SF { key: "aprs-ringer-tx-message", byte: 0x0053a, mirror: 0x00000, kind: SK::Bool { shift: 0, active_low: false } }, + SF { key: "aprs-ringer-rx-beacon", byte: 0x0053b, mirror: 0x00000, kind: SK::Bool { shift: 0, active_low: false } }, + SF { key: "aprs-ringer-rx-message", byte: 0x0053c, mirror: 0x00000, kind: SK::Bool { shift: 0, active_low: false } }, + SF { key: "aprs-ringer-my-packet", byte: 0x0053d, mirror: 0x00000, kind: SK::Bool { shift: 0, active_low: false } }, + SF { key: "aprs-ringer-call-ringer", byte: 0x0053e, mirror: 0x00000, kind: SK::Bool { shift: 0, active_low: false } }, + SF { key: "aprs-ringer-msg-voice", byte: 0x00540, mirror: 0x00000, kind: SK::Bool { shift: 0, active_low: false } }, + SF { key: "aprs-ringer-range", byte: 0x0053f, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "OFF"), (1, "1"), (5, "5"), (10, "10"), (50, "50"), (100, "100")] } }, + SF { key: "aprs-ringer-call-1", byte: 0x00740, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "aprs-ringer-call-1-ssid", byte: 0x00746, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "aprs-ringer-call-2", byte: 0x00748, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "aprs-ringer-call-2-ssid", byte: 0x0074e, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "aprs-ringer-call-3", byte: 0x00750, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "aprs-ringer-call-3-ssid", byte: 0x00756, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "aprs-ringer-call-4", byte: 0x00758, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "aprs-ringer-call-4-ssid", byte: 0x0075e, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "aprs-ringer-call-5", byte: 0x00760, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "aprs-ringer-call-5-ssid", byte: 0x00766, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "aprs-ringer-call-6", byte: 0x00768, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "aprs-ringer-call-6-ssid", byte: 0x0076e, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "aprs-ringer-call-7", byte: 0x00770, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "aprs-ringer-call-7-ssid", byte: 0x00776, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "aprs-ringer-call-8", byte: 0x00778, mirror: 0x00000, kind: SK::Text { len: 6, pad: 0xca, set: TS::Call } }, + SF { key: "aprs-ringer-call-8-ssid", byte: 0x0077e, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(202, "(none)"), (1, "-1"), (2, "-2"), (3, "-3"), (4, "-4"), (5, "-5"), (6, "-6"), (7, "-7"), (8, "-8"), (9, "-9"), (10, "-10"), (11, "-11"), (12, "-12"), (13, "-13"), (14, "-14"), (15, "-15")] } }, + SF { key: "aprs-unit-position", byte: 0x00500, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 1, labels: &[(0, "dd°mm'ss\""), (1, "dd°mm.mm'")] } }, + SF { key: "aprs-unit-distance", byte: 0x00501, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 1, labels: &[(0, "km"), (1, "mile")] } }, + SF { key: "aprs-unit-speed", byte: 0x00502, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "km/h"), (1, "mph"), (2, "knot")] } }, + SF { key: "aprs-unit-altitude", byte: 0x00503, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 1, labels: &[(0, "m"), (1, "ft")] } }, + SF { key: "aprs-unit-baro", byte: 0x00504, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "hPa"), (1, "mb"), (2, "mmHg"), (3, "inHg")] } }, + SF { key: "aprs-unit-temp", byte: 0x00505, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 1, labels: &[(0, "°C"), (1, "F")] } }, + SF { key: "aprs-unit-rain", byte: 0x00506, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 1, labels: &[(0, "mm"), (1, "inch")] } }, + SF { key: "aprs-unit-wind", byte: 0x00507, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "m/s"), (1, "mph"), (2, "knot")] } }, + SF { key: "smartbeaconing", byte: 0x0054a, mirror: 0x00000, kind: SK::Enum { shift: 0, width: 8, labels: &[(0, "OFF"), (1, "TYPE1"), (2, "TYPE2"), (3, "TYPE3")] } }, + SF { key: "smartbeaconing-type-1-low-speed", byte: 0x0054b, mirror: 0x00000, kind: SK::Uint }, + SF { key: "smartbeaconing-type-1-high-speed", byte: 0x0054c, mirror: 0x00000, kind: SK::Uint }, + SF { key: "smartbeaconing-type-1-slow-rate", byte: 0x0054d, mirror: 0x00000, kind: SK::Uint }, + SF { key: "smartbeaconing-type-1-fast-rate", byte: 0x0054e, mirror: 0x00000, kind: SK::Uint }, + SF { key: "smartbeaconing-type-1-turn-angle", byte: 0x0054f, mirror: 0x00000, kind: SK::Uint }, + SF { key: "smartbeaconing-type-1-turn-slope", byte: 0x00550, mirror: 0x00000, kind: SK::Uint }, + SF { key: "smartbeaconing-type-1-turn-time", byte: 0x00551, mirror: 0x00000, kind: SK::Uint }, + SF { key: "smartbeaconing-type-2-low-speed", byte: 0x00552, mirror: 0x00000, kind: SK::Uint }, + SF { key: "smartbeaconing-type-2-high-speed", byte: 0x00553, mirror: 0x00000, kind: SK::Uint }, + SF { key: "smartbeaconing-type-2-slow-rate", byte: 0x00554, mirror: 0x00000, kind: SK::Uint }, + SF { key: "smartbeaconing-type-2-fast-rate", byte: 0x00555, mirror: 0x00000, kind: SK::Uint }, + SF { key: "smartbeaconing-type-2-turn-angle", byte: 0x00556, mirror: 0x00000, kind: SK::Uint }, + SF { key: "smartbeaconing-type-2-turn-slope", byte: 0x00557, mirror: 0x00000, kind: SK::Uint }, + SF { key: "smartbeaconing-type-2-turn-time", byte: 0x00558, mirror: 0x00000, kind: SK::Uint }, + SF { key: "smartbeaconing-type-3-low-speed", byte: 0x00559, mirror: 0x00000, kind: SK::Uint }, + SF { key: "smartbeaconing-type-3-high-speed", byte: 0x0055a, mirror: 0x00000, kind: SK::Uint }, + SF { key: "smartbeaconing-type-3-slow-rate", byte: 0x0055b, mirror: 0x00000, kind: SK::Uint }, + SF { key: "smartbeaconing-type-3-fast-rate", byte: 0x0055c, mirror: 0x00000, kind: SK::Uint }, + SF { key: "smartbeaconing-type-3-turn-angle", byte: 0x0055d, mirror: 0x00000, kind: SK::Uint }, + SF { key: "smartbeaconing-type-3-turn-slope", byte: 0x0055e, mirror: 0x00000, kind: SK::Uint }, + SF { key: "smartbeaconing-type-3-turn-time", byte: 0x0055f, mirror: 0x00000, kind: SK::Uint }, +]; diff --git a/src-tauri/src/radios/yaesu_ftm510/gate.rs b/src-tauri/src/radios/yaesu_ftm510/gate.rs new file mode 100644 index 0000000..eeb1a66 --- /dev/null +++ b/src-tauri/src/radios/yaesu_ftm510/gate.rs @@ -0,0 +1,238 @@ +//! The step-3 gate: decode every record a real FTM-510 wrote into a channel, +//! re-encode it, and compare with the radio's bytes. `#[ignore]`d and env-gated +//! because the inputs are a personal radio's backup in the gitignored scratchpad: +//! +//! ```text +//! FTM510_DAT=../scratchpad/yaesu_ftm510/captures/ftm510_01_new_radio_callsign.dat \ +//! FTM500_DAT=../scratchpad/yaesu_ftm510/sources/captures-thirdparty/CLNFTM500D.dat \ +//! cargo test --lib yaesu_ftm510::gate -- --ignored --nocapture +//! ``` +//! +//! A factory-fresh radio holds two memories, so the 20 records it wrote in the +//! memory format — memories 1-2, the two live VFO records, and 4 groups × 4 +//! bands of VFO / Home records — are the corpus. Two things have no column in +//! the channel database and are masked and COUNTED, never silently ignored: +//! byte 0's used bit on VFO / Home records (they are never "used"), and byte 5's +//! step nibble (a VFO keeps a configured step; the encoder derives one from the +//! frequency). Everything else must match byte for byte. +//! +//! `FTM500_DAT`, if set, runs the same comparison over an FTM-500 file someone +//! edited with ADMS — a sibling, reported, never asserted. + +use super::sd_image::{decode_freq, patch_backup, validate_backup, IMAGE_LEN, MEM_SLOTS}; +use crate::commands::export::ExpandedChannel; +use crate::models::Channel; +use crate::radios::yaesu_ftm300::sd_image::encode_record; + +const TONES: [f64; 50] = [ + 67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5, 85.4, 88.5, 91.5, 94.8, 97.4, 100.0, 103.5, 107.2, 110.9, + 114.8, 118.8, 123.0, 127.3, 131.8, 136.5, 141.3, 146.2, 151.4, 156.7, 159.8, 162.2, 165.5, 167.9, + 171.3, 173.8, 177.3, 179.9, 183.5, 186.2, 189.9, 192.8, 196.6, 199.5, 203.5, 206.5, 210.7, 218.1, + 225.7, 229.1, 233.6, 241.8, 250.3, 254.1, +]; + +/// A record read back into the channel that would produce it. Returns None for +/// a record using something the channel database cannot say (cross tone modes, +/// AMS, a user CTCSS other than the radio's default). +fn record_to_channel(r: &[u8]) -> Option { + let rx = decode_freq(&r[2..5]); + let shift = u16::from_be_bytes([r[12], r[13]]) as i64 * 50_000; + let tx = match r[1] & 0x07 { + 0 => rx, + 2 => rx - shift, + 3 => rx + shift, + 4 => decode_freq(&r[6..9]), + _ => return None, + }; + if r[1] & 0x40 != 0 || r[11] & 0x1F != 0x0F { + return None; // AMS, or a user CTCSS this encoder never writes + } + let mode = match (r[1] & 0x30, r[11] & 0x60) { + (0x10, 0) => "AM", + (0x20, 0) => "NFM", + (0, 0x40) => "YSF", + (0, 0x60) => "YSF-VW", + (0, 0) => "FM", + _ => return None, + }; + let tone = TONES[(r[9] & 0x3F) as usize]; + let tone_mode = match r[5] >> 4 { + 0 => "off", + 1 => "Tone", + 2 => "TSQL", + 4 => "DTCS", + _ => return None, + }; + let power = ["High", "Med", "Low"][(r[9] >> 6) as usize % 3]; + Some(Channel { + id: 1, + rx_freq: rx as f64 / 1e6, + tx_freq: Some(tx as f64 / 1e6), + mode: Some(mode.into()), + tone_mode: Some(tone_mode.into()), + ctcss_uplink: Some(tone), + ctcss_downlink: Some(tone), + dcs_code: Some(String::new()), + power: Some(power.into()), + dcs_polarity: "NN".into(), + cross_mode: "Tone->Tone".into(), + source: "gate".into(), + ..Default::default() + }) +} + +fn wrap(c: Channel) -> ExpandedChannel { + ExpandedChannel { channel: c, tg_label: None, timeslot: None, tg_number: None, tg_call_type: None, tg_inline: false } +} + +/// (offset, matched, masked-step, masked-used, mismatched) +fn compare(label: &str, img: &[u8], at: usize, used_expected: bool) -> (bool, bool, bool) { + let r = &img[at..at + 16]; + let Some(c) = record_to_channel(r) else { + println!(" {label} @{at:#06x}: not expressible as a channel: {:02x?}", r); + return (false, false, false); + }; + let mut ours = encode_record(&wrap(c)); + let mut step_masked = false; + let mut used_masked = false; + if !used_expected && ours[0] & 0x80 != 0 && r[0] & 0x80 == 0 { + ours[0] &= 0x7F; + used_masked = true; + } + if ours[5] & 0x0F != r[5] & 0x0F { + ours[5] = (ours[5] & 0xF0) | (r[5] & 0x0F); + step_masked = true; + } + let ok = ours[..] == r[..]; + if !ok { + println!(" {label} @{at:#06x}: MISMATCH\n radio {:02x?}\n ours {:02x?}", r, ours); + } + (ok, step_masked, used_masked) +} + +#[test] +#[ignore] +fn reencodes_a_real_ftm510s_records() { + let Ok(path) = std::env::var("FTM510_DAT") else { + println!("FTM510_DAT not set — skipped"); + return; + }; + let img = std::fs::read(&path).unwrap(); + validate_backup(&img).unwrap(); + + let mut sites: Vec<(String, usize, bool)> = Vec::new(); + for s in 0..MEM_SLOTS { + if img[0x800 + s * 16] & 0x80 != 0 { + sites.push((format!("memory {}", s + 1), 0x800 + s * 16, true)); + } + } + sites.push(("live VFO A".into(), 0x080, false)); + sites.push(("live VFO B".into(), 0x090, false)); + for (name, base) in [("VFO A", 0x100), ("VFO B", 0x150), ("HOME A", 0x1A0), ("HOME B", 0x1F0)] { + // Band 4 (480+, default 850.000 MHz) is outside this radio; ADMS-18 hides it. + for band in 0..4 { + sites.push((format!("{name} band {band}"), base + band * 16, false)); + } + } + let (mut ok, mut steps, mut used) = (0, 0, 0); + for (label, at, u) in &sites { + let (m, s, us) = compare(label, &img, *at, *u); + ok += m as usize; + steps += s as usize; + used += us as usize; + } + println!("{ok}/{} records re-encode ({steps} step nibbles masked, {used} used bits masked)", sites.len()); + assert_eq!(ok, sites.len(), "a record the radio wrote did not re-encode"); + assert!(sites.len() >= 20, "expected at least the 20 factory records, found {}", sites.len()); + + // Whole file: patching the radio's own memories back into it changes nothing + // but the factory empty slots holding step 0 — and the names the export's + // shared de-duplication gives channels that collide. A factory radio's two + // memories are identical and nameless, so the second one is named; that is + // the app's rule for every radio, reported here rather than masked. + let mems: Vec = (0..MEM_SLOTS) + .filter(|s| img[0x800 + s * 16] & 0x80 != 0) + .map(|s| { + let mut c = record_to_channel(&img[0x800 + s * 16..0x810 + s * 16]).unwrap(); + c.name_short = None; + wrap(c) + }) + .collect(); + let refs: Vec<&ExpandedChannel> = mems.iter().collect(); + let model = crate::seed::test_model("FTM-510DR"); + let names = crate::commands::export::expanded_names(refs.iter().copied(), &model); + let renamed: Vec = (0..names.len()).filter(|&s| !names[s].is_empty()).collect(); + for &s in &renamed { + println!("memory {} named {:?} by the export's de-duplication", s + 1, names[s]); + } + let in_renamed_name = |i: usize| { + renamed.iter().any(|&s| (0x4CB0 + s * 2..0x4CB2 + s * 2).contains(&i) || (0x10000 + s * 16..0x10010 + s * 16).contains(&i)) + }; + let out = patch_backup(&img, &refs, &model).unwrap(); + let diff: Vec = (0..IMAGE_LEN).filter(|&i| out[i] != img[i] && !in_renamed_name(i)).collect(); + let all_step = diff.iter().all(|&i| i >= 0x800 && (i - 0x800) % 16 == 5 && img[i] == 0 && out[i] == 4); + println!("whole-file patch: {} other bytes differ, all of them factory step-0 empties: {all_step}", diff.len()); + assert!(all_step && diff.len() <= 14, "{:x?}", &diff[..diff.len().min(20)]); + + if let Ok(p) = std::env::var("FTM500_DAT") { + let s = std::fs::read(p).unwrap(); + let (mut n, mut good) = (0, 0); + for slot in 0..MEM_SLOTS { + let at = 0x800 + slot * 16; + if s[at] & 0x80 == 0 { + continue; + } + n += 1; + good += compare(&format!("FTM-500 memory {}", slot + 1), &s, at, true).0 as usize; + } + println!("FTM-500 sibling (reported, not asserted): {good}/{n} re-encode"); + } +} + +/// The settings table's self-check: decode every ADMS-18 signature-pass file +/// with the generated table and compare with what ADMS-18 DISPLAYED for that +/// file (each control's achieved option, re-read after setting). 14 independent +/// samples per control. +/// +/// ```text +/// FTM510_ADMS=../scratchpad/yaesu_ftm510/adms \ +/// cargo test --lib yaesu_ftm510::gate::settings -- --ignored --nocapture +/// ``` +#[test] +#[ignore] +fn settings_decode_what_adms18_displayed() { + use serde_json::Value; + let Ok(dir) = std::env::var("FTM510_ADMS") else { + println!("FTM510_ADMS not set — skipped"); + return; + }; + let dir = std::path::PathBuf::from(dir); + let x: Value = serde_json::from_str(&std::fs::read_to_string(dir.join("xref.json")).unwrap()).unwrap(); + let (mut ok, mut bad, mut n) = (0, 0, 0); + for k in 0..14 { + let img = std::fs::read(dir.join(format!("bits/pass{k}.dat"))).unwrap(); + let ach: Value = serde_json::from_str(&std::fs::read_to_string(dir.join(format!("bits/pass{k}.achieved.json"))).unwrap()).unwrap(); + let ours = super::settings::decode_settings(&img); + n += 1; + for (src, key) in x["xref"].as_object().unwrap() { + let Some(idx) = ach["achieved"][src].as_u64() else { continue }; + let shown = x["items"][src][idx as usize].as_str().unwrap(); + let want = match ours[key.as_str().unwrap()] { + Value::Bool(_) => Value::Bool(shown == "ON"), + _ => Value::String(shown.to_string()), + }; + // Options deliberately left out of the table decode as `#raw`. + if ours[key.as_str().unwrap()] == want { + ok += 1; + } else if key.as_str().unwrap().starts_with("function-key") && (idx == 0 || idx == 127) { + continue; + } else { + bad += 1; + println!(" pass {k} {src} -> {key}: ADMS showed {shown:?}, table decodes {}", ours[key.as_str().unwrap()]); + } + } + } + println!("{n} files: {ok} agree, {bad} disagree"); + assert_eq!(bad, 0); + assert!(ok > 1500, "too few comparisons ({ok}) — is the cross-reference empty?"); +} diff --git a/src-tauri/src/radios/yaesu_ftm510/ladder.rs b/src-tauri/src/radios/yaesu_ftm510/ladder.rs new file mode 100644 index 0000000..db14380 --- /dev/null +++ b/src-tauri/src/radios/yaesu_ftm510/ladder.rs @@ -0,0 +1,125 @@ +//! THROWAWAY (#151, step 7): build the hardware-ladder files from the radio's +//! own backup. Each file asserts its property before Tim loads it, so a failure +//! at the radio is the radio's answer, not a bad file. +//! +//! ```sh +//! FTM510_DAT=../scratchpad/yaesu_ftm510/captures/ftm510_01_new_radio_callsign.dat \ +//! FTM510_OUT=../scratchpad/yaesu_ftm510/ladder CPM_DEV_DB= CPM_CODEPLUG=6 \ +//! cargo test --lib yaesu_ftm510::ladder -- --ignored --nocapture +//! ``` +//! +//! | file | step | what changes against the radio's own file | +//! |---|---|---| +//! | `CLNFTM510D01.dat` | 1 identity | nothing — proves the container and the numbered name | +//! | `CLNFTM510D02.dat` | 2 one name | memory 1's name + its name-length entry: **the checksum test** | +//! | `CLNFTM510D03.dat` | 3 full codeplug | codeplug 6 through the app's export pipeline | +//! | `CLNFTM510D04.dat` | 4 band probe | 11 memories at every coverage edge | +//! | `CLNFTM510D05.dat` | 5 settings | four settings, nothing else | + +use super::sd_image::{patch_backup, IMAGE_LEN}; +use super::settings::{apply_settings, decode_settings}; +use crate::commands::export::{exclusion_reason, expand_for_export, resolve_codeplug_groups, ExpandedChannel}; +use crate::models::Channel; +use crate::radios::yaesu_ftm300::sd_image::{name_fields, tag}; + +fn chan(id: i64, name: &str, rx: f64, mode: &str) -> ExpandedChannel { + ExpandedChannel { + channel: Channel { + id, + name_short: Some(name.into()), + rx_freq: rx, + tx_freq: Some(rx), + mode: Some(mode.into()), + dcs_polarity: "NN".into(), + cross_mode: "Tone->Tone".into(), + source: "ladder".into(), + ..Default::default() + }, + tg_label: None, + timeslot: None, + tg_number: None, + tg_call_type: None, + tg_inline: false, + } +} + +fn diff(a: &[u8], b: &[u8]) -> Vec { + (0..a.len()).filter(|&i| a[i] != b[i]).collect() +} + +#[tokio::test] +#[ignore = "builds hardware-ladder files from a personal radio's backup"] +async fn build_the_ladder_files() { + let radio = std::fs::read(std::env::var("FTM510_DAT").expect("FTM510_DAT")).unwrap(); + let out = std::path::PathBuf::from(std::env::var("FTM510_OUT").expect("FTM510_OUT")); + std::fs::create_dir_all(&out).unwrap(); + assert_eq!(radio.len(), IMAGE_LEN); + let model = crate::seed::test_model("FTM-510DR"); + let write = |n: u32, img: &[u8]| std::fs::write(out.join(format!("CLNFTM510D{n:02}.dat")), img).unwrap(); + + // 1 — identity. + write(1, &radio); + + // 2 — one name: memory 1 (factory 144.000, no name) becomes "LADDER 2". The + // name field and its length entry are the only bytes that may move. + let mut two = radio.clone(); + let (field, code) = name_fields(&tag("LADDER 2")); + two[0x10000..0x10010].copy_from_slice(&field); + two[0x4CB0..0x4CB2].copy_from_slice(&code.to_be_bytes()); + let d = diff(&radio, &two); + println!("step 2: {} bytes differ {:x?}; length code {code:#06x}", d.len(), d); + assert!(d.iter().all(|&i| (0x10000..0x10010).contains(&i) || (0x4CB0..0x4CB2).contains(&i))); + write(2, &two); + + // 3 — the full codeplug through the app's own pipeline. + if let (Ok(db), Ok(cp)) = (std::env::var("CPM_DEV_DB"), std::env::var("CPM_CODEPLUG")) { + let pool = sqlx::SqlitePool::connect(&format!("sqlite:file:{db}?mode=ro")).await.unwrap(); + let groups = resolve_codeplug_groups(&pool, cp.parse().unwrap()).await.unwrap(); + let mut seen = std::collections::HashSet::new(); + let chans: Vec<_> = groups.iter().flat_map(|g| g.channels.iter().cloned()).filter(|c| seen.insert(c.id)).collect(); + let expanded = expand_for_export(&pool, chans).await.unwrap(); + let included: Vec<&ExpandedChannel> = expanded.iter().filter(|ec| exclusion_reason(&ec.channel, &model).is_none()).collect(); + let three = patch_backup(&radio, &included, &model).unwrap(); + let used = (0..999).filter(|i| three[0x800 + i * 16] & 0x80 != 0).count(); + println!("step 3: {} channels in, {} written, {used} marked used", expanded.len(), included.len()); + for (slot, ec) in included.iter().enumerate() { + let name = String::from_utf8_lossy(three[0x10000 + slot * 16..0x10010 + slot * 16].split(|&b| b == 0xFF).next().unwrap()).into_owned(); + println!(" {:3} {:>10.4} {:<5} {name}", slot + 1, ec.channel.rx_freq, ec.channel.mode.as_deref().unwrap_or("")); + } + assert_eq!(used, included.len()); + assert_eq!(&three[..0x800], &radio[..0x800], "settings must be the radio's"); + write(3, &three); + } else { + println!("step 3 skipped: CPM_DEV_DB / CPM_CODEPLUG not set"); + } + + // 4 — band probe: every edge of 108-550 MHz and both sides of each internal + // boundary (137 / 174 / 400 / 480), 222.1 receive-only. Count what lands. + let probe = [ + (108.0, "AM"), (136.99375, "AM"), (137.0, "FM"), (173.99375, "FM"), (174.0, "FM"), (222.1, "FM"), + (399.99375, "FM"), (400.0, "FM"), (479.99375, "FM"), (480.0, "FM"), (549.9875, "FM"), + ]; + let pc: Vec = probe.iter().enumerate().map(|(i, (f, m))| chan(i as i64 + 1, &format!("P{f}"), *f, m)).collect(); + let refs: Vec<&ExpandedChannel> = pc.iter().collect(); + let four = patch_backup(&radio, &refs, &model).unwrap(); + for (slot, (f, _)) in probe.iter().enumerate() { + let r = &four[0x800 + slot * 16..0x810 + slot * 16]; + assert_eq!(super::sd_image::decode_freq(&r[2..5]), (f * 1e6).round() as i64); + println!(" step 4 memory {:2}: {f:>10} MHz band {} {:02x?}", slot + 1, r[0] & 0x0F, &r[..6]); + } + write(4, &four); + + // 5 — settings, each visible on the radio's own screen, nothing else moved. + let before = decode_settings(&radio); + let want = serde_json::json!({"lcd-brightness": "MID", "beep-level": "HIGH", "sub-band-mute": "ON", "s-dx": "AUTO"}); + let mut five = radio.clone(); + assert_eq!(apply_settings(&mut five, want.as_object().unwrap()), 4); + let after = decode_settings(&five); + for (k, v) in want.as_object().unwrap() { + println!(" step 5 {k}: {} -> {}", before[k], after[k]); + assert_eq!(&after[k], v); + } + let d = diff(&radio, &five); + println!("step 5: bytes moved {:x?}", d.iter().map(|&i| (i, radio[i], five[i])).collect::>()); + write(5, &five); +} diff --git a/src-tauri/src/radios/yaesu_ftm510/mod.rs b/src-tauri/src/radios/yaesu_ftm510/mod.rs new file mode 100644 index 0000000..b93d711 --- /dev/null +++ b/src-tauri/src/radios/yaesu_ftm510/mod.rs @@ -0,0 +1,225 @@ +//! Yaesu FTM-510DR driver (`driver_key = "yaesu_ftm510"`, #151) — microSD +//! programming, memories and Setup Menu settings both. +//! +//! The operator backs the radio up with Setup Menu **107 BACKUP → WRITE TO SD → +//! ALL**, which writes `FTM510D/BACKUP/CLNFTM510D.dat`; [`sd_image`] patches the +//! memories into that file and **READ FROM SD → ALL** loads it back. +//! +//! ## A sibling's format, re-measured +//! +//! No published map exists for the FTM-510 (CHIRP has none; fulgidus/channel-forge +//! has a rough FTM-500 note). The file turned out to be the FTM-300's layout — +//! same 98,304 bytes, 16-byte records at `0x0800`, a name-length table at +//! `0x4CB0`, names at `0x10000` — so the record encoder is the FTM-300's, +//! reused rather than copied. That is an inference by analogy until proven, and +//! it was proven against the 510's own bytes, not the FTM-300's: +//! +//! - the 510's file magic is its own (`AH089$`; FTM-300 `AH071$`, FTM-500 `AH081$`); +//! - Tim's radio-written file: both factory memories and all 20 VFO / Home records +//! decode and re-encode under this encoder (`gate.rs`); +//! - Yaesu's ADMS-18 writing a memory (146.520, `TEST`) put every byte where the +//! FTM-300 encoder puts it — except the name-length entry (below). +//! +//! What differs from the FTM-300: receive coverage stops at 550 MHz (the FTM-300 +//! goes to 999), and the 8.33 kHz airband code — measured on the FTM-300's screen — +//! has not been seen on a 510, so every off-grid airband point is refused here. +//! +//! ## Hardware-proven (2026-11-03, Tim's radio, `ladder.rs`) +//! +//! All five ladder steps passed, each loaded with 107 BACKUP → READ FROM SD → ALL +//! from a file placed beside the radio's own as `CLNFTM510D01`-`05` — the radio +//! lists and loads numbered files it did not write itself. +//! +//! 1. Identity copy: loaded, call sign and memories intact. +//! 2. One renamed memory: loaded and showed `LADDER 2` — **no checksum is +//! checked on load**, and the radio reads names with the FTM-300's +//! `(1 << (16 - len)) - 1` length rule (ADMS-18 writes `0x0000`; both load). +//! 3. Full codeplug, 61 memories from the app's pipeline: names, shifts, DN, +//! narrow FM and the count all as written. +//! 4. Band probe: all 11 edges of 108-550 MHz landed, both sides of each +//! internal boundary. +//! 5. Settings: LCD MID, beep HIGH, **Sub Band Mute ON = 3** (ADMS-18 was right; +//! CHIRP's FTM-300 reading is inverted) and **S-DX AUTO** across its three +//! scattered bits (the screen showed AUTO-A, SPU-1 fitted). +//! +//! ## Still open +//! +//! - **Memory-group flag.** The FTM-300's M-GRP is byte 0 bit 4; the 510 has +//! PMG instead, and an FTM-500 file carries an unexplained byte 0 bit 3. Neither +//! is set here. +//! +//! Working notes: `scratchpad/yaesu_ftm510/` (gitignored) — `PLAN.md`, +//! `FINDINGS.md`, `adms/MEASURED.md`. + +pub(crate) mod sd_image; +pub(crate) mod settings; + +use crate::radios::driver::{ + CardSettingsWriter, CodeplugExporter, ExportRequest, RadioDriver, RadioIdentity, +}; + +pub(crate) struct YaesuFtm510; + +pub(crate) static DRIVER: YaesuFtm510 = YaesuFtm510; + +impl RadioDriver for YaesuFtm510 { + fn key(&self) -> &'static str { + "yaesu_ftm510" + } + + fn display_name(&self) -> &'static str { + "Yaesu FTM-510DR" + } + + /// Unused: nothing here opens a port. ADMS-18 also reaches the radio over + /// an SCU-20 cable, but the card needs no cable and no driver. + fn baud(&self) -> u32 { + 38400 + } + + /// There is no handshake, because there is no cable path. + fn identify(&self, _port: &str) -> Result { + Err("The FTM-510 is programmed from its microSD card, not over a cable. On the radio, \ + Setup Menu 107 BACKUP → WRITE TO SD → ALL, put the card in this computer, and use \ + the card actions instead." + .into()) + } + + fn as_codeplug_exporter(&self) -> Option<&dyn CodeplugExporter> { + Some(self) + } +} + +impl CodeplugExporter for YaesuFtm510 { + fn export_format(&self) -> &'static str { + "yaesu_ftm510_sd" + } + + /// The radio's microSD `CLNFTM510D.dat`. + fn target_extensions(&self) -> &'static [&'static str] { + &["dat"] + } + + /// `path` is the operator's own `FTM510D/BACKUP/CLNFTM510D.dat`: read as the + /// template and written back in place, because READ FROM SD lists files by + /// the names the radio gave them. The untouched original is kept beside it + /// as `CLNFTM510D.dat.orig`, written once and never overwritten. + fn export(&self, path: &str, req: &ExportRequest) -> Result { + patch_in_place(path, |template| { + let mut image = sd_image::patch_backup(template, req.channels, req.model)?; + // Non-channel settings from the codeplug's radio profile. A codeplug with + // no profile leaves every one exactly as the operator's backup had it. + if let Some(json) = req.profile_settings { + let parsed: serde_json::Value = serde_json::from_str(json) + .map_err(|e| format!("This profile's settings are not valid JSON: {e}"))?; + if let Some(map) = parsed.as_object() { + settings::apply_settings(&mut image, map); + } + } + Ok((image, req.channels.len())) + }) + } + + fn as_card_settings_writer(&self) -> Option<&dyn CardSettingsWriter> { + Some(self) + } +} + +impl CardSettingsWriter for YaesuFtm510 { + /// The same file and rules as the export — in place, `.orig` kept once — + /// with only the settings applied. Every memory is the template's. + fn export_settings( + &self, + path: &str, + settings: &serde_json::Map, + ) -> Result { + patch_in_place(path, |template| { + sd_image::validate_backup(template)?; + let mut image = template.to_vec(); + let n = settings::apply_settings(&mut image, settings); + Ok((image, n)) + }) + } +} + +fn patch_in_place( + path: &str, + patch: impl FnOnce(&[u8]) -> Result<(Vec, usize), String>, +) -> Result { + crate::radios::card_file::patch_in_place( + path, + "Pick FTM510D/BACKUP/CLNFTM510D.dat on the radio's microSD card — back the radio \ + up first (Setup Menu 107 BACKUP → WRITE TO SD → ALL) if it is not there.", + patch, + ) +} + +#[cfg(test)] +mod dev_export; +#[cfg(test)] +mod gate; +#[cfg(test)] +mod ladder; + +#[cfg(test)] +mod tests { + use super::*; + use crate::radios::driver::DriverCapabilities; + + /// Step 6's gate: one export carries the memories AND the profile's + /// settings into the same file, and keeps the untouched original. + #[test] + fn an_export_carries_memories_and_settings_together() { + use crate::commands::export::ExpandedChannel; + use crate::models::Channel; + let dir = std::env::temp_dir().join(format!("ftm510-export-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("CLNFTM510D.dat"); + let mut template = vec![0x5Au8; sd_image::IMAGE_LEN]; + template[..6].copy_from_slice(b"AH089$"); + std::fs::write(&path, &template).unwrap(); + + let ec = ExpandedChannel { + channel: Channel { + id: 7, + name_short: Some("NCARC".into()), + rx_freq: 447.275, + tx_freq: Some(442.275), + dcs_polarity: "NN".into(), + cross_mode: "Tone->Tone".into(), + source: "test".into(), + ..Default::default() + }, + tg_label: None, + timeslot: None, + tg_number: None, + tg_call_type: None, + tg_inline: false, + }; + let refs = [&ec]; + let model = crate::seed::test_model("FTM-510DR"); + let req = ExportRequest { + channels: &refs, + groups: &[], + model: &model, + profile_settings: Some(r#"{"lcd-brightness":"MID","aprs-modem":true,"aprs-callsign":"WW8L"}"#), + memory_group: &[], + }; + assert_eq!(DRIVER.export(path.to_str().unwrap(), &req).unwrap(), 1); + let out = std::fs::read(&path).unwrap(); + assert_eq!(&out[0x800..0x805], &[0x83, 0x02, 0x84, 0x47, 0x27]); + let s = settings::decode_settings(&out); + assert_eq!(s["lcd-brightness"], "MID"); + assert_eq!(s["aprs-modem"], true); + assert_eq!(s["aprs-callsign"], "WW8L"); + assert_eq!(std::fs::read(path.with_extension("dat.orig")).unwrap(), template); + std::fs::remove_dir_all(&dir).ok(); + } + + #[test] + fn advertises_export_only() { + let caps = DriverCapabilities::of(&DRIVER); + assert!(!caps.download_image && !caps.program_image && !caps.read_settings); + assert!(DRIVER.as_codeplug_exporter().is_some()); + } +} diff --git a/src-tauri/src/radios/yaesu_ftm510/sd_image.rs b/src-tauri/src/radios/yaesu_ftm510/sd_image.rs new file mode 100644 index 0000000..dcb3789 --- /dev/null +++ b/src-tauri/src/radios/yaesu_ftm510/sd_image.rs @@ -0,0 +1,306 @@ +//! FTM-510DR microSD codeplug writer: patch memories into the radio's own +//! `FTM510D/BACKUP/CLNFTM510D.dat`. +//! +//! Same layout as the FTM-300's (`yaesu_ftm300::sd_image`), whose record and +//! name encoders this reuses; see the module doc in `mod.rs` for what was checked +//! on the 510 itself. Only the 999 regular memories are rewritten: +//! +//! | region | address | what | +//! |---|---|---| +//! | `memory[999]` | `0x0800` | 16-byte records | +//! | `name_len[999]` | `0x4CB0` | u16 BE `(1 << (16 - len)) - 1`, `0xFFFF` = no name | +//! | `name[999]` | `0x10000` | 16 ASCII bytes, `0xFF`-padded | +//! +//! Each table runs on for the 100 PMS entries, and the VFO / Home records sit at +//! `0x0080`-`0x023F` in the same record format; all of those are left exactly as +//! the operator had them, as is every setting. + +use crate::commands::export::{expanded_names, tx_frequency, ExpandedChannel}; +use crate::models::RadioModel; +use crate::radios::yaesu_ftm300::sd_image::{encode_record, name_fields, tag, NAME_LEN}; + +pub(crate) const IMAGE_LEN: usize = 0x18000; +const MAGIC: &[u8] = b"AH089$"; + +const MEM_BASE: usize = 0x0800; +const MEM_STRIDE: usize = 16; +pub(crate) const MEM_SLOTS: usize = 999; +const NAME_LEN_BASE: usize = 0x4CB0; +const NAME_BASE: usize = 0x10000; + +/// What the radio holds in an empty slot: 1,083 of the 1,097 empty slots in +/// Tim's new radio are exactly this (144.000 MHz, 12.5 kHz step, 100.0 Hz, +/// 600 kHz) — the FTM-300's empty record, byte for byte. The other 14 carry +/// step 0 instead; factory leftovers, not written back. +const EMPTY_RECORD: [u8; MEM_STRIDE] = [ + 0x01, 0x00, 0x01, 0x44, 0x00, 0x04, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x0F, 0x00, 0x0C, 0x00, 0x00, +]; + +/// The radio's receive range (Operating Manual specifications: 108–550 MHz, +/// continuous in five ranges). A frequency outside it would become a slot the +/// radio shows empty while the export reported success. +const RX_MIN_HZ: i64 = 108_000_000; +const RX_MAX_HZ: i64 = 550_000_000; + +/// Refuse a file that is not an FTM-510 ALL backup, naming what it is. +pub(crate) fn validate_backup(image: &[u8]) -> Result<(), String> { + if image.len() != IMAGE_LEN { + return Err(format!( + "This is not an FTM-510 backup: it is {} bytes, and the radio's \ + FTM510D/BACKUP/CLNFTM510D.dat is always {IMAGE_LEN}. On the radio, \ + Setup Menu 107 BACKUP → WRITE TO SD → ALL, then pick that file.", + image.len() + )); + } + if &image[..MAGIC.len()] != MAGIC { + let other = match &image[..MAGIC.len()] { + b"AH071$" => " (that is an FTM-300 backup)", + b"AH081$" => " (that is an FTM-500 backup)", + _ => "", + }; + return Err(format!( + "This backup is from a different radio: it starts {:?}{other}, and an FTM-510 \ + backup starts \"AH089$\". The FTM-510 writer only patches FTM-510 backups.", + String::from_utf8_lossy(&image[..MAGIC.len()]) + )); + } + Ok(()) +} + +/// Whether the 510 is known to display `hz` as written: inside its receive +/// range and on the 625 Hz grid every 5 / 6.25 / 12.5 kHz channel sits on. +/// The FTM-300's measured +8.33 kHz airband code is NOT accepted here — it has +/// never been seen on a 510's screen. +fn writable(hz: i64) -> bool { + (RX_MIN_HZ..=RX_MAX_HZ).contains(&hz) && hz.rem_euclid(10_000) % 625 == 0 +} + +fn write_slot(img: &mut [u8], slot: usize, record: &[u8; MEM_STRIDE], name: &str) { + let at = MEM_BASE + slot * MEM_STRIDE; + img[at..at + MEM_STRIDE].copy_from_slice(record); + let (field, code) = name_fields(name); + img[NAME_BASE + slot * NAME_LEN..NAME_BASE + (slot + 1) * NAME_LEN].copy_from_slice(&field); + img[NAME_LEN_BASE + slot * 2..NAME_LEN_BASE + slot * 2 + 2].copy_from_slice(&code.to_be_bytes()); +} + +/// Patch `template` with `channels` in memory-slot order (slot 1 first). +/// +/// Every one of the 999 regular slots is rewritten — a filled one with its +/// channel, the rest with the radio's own empty record — so nothing from the +/// operator's old memory list survives into the new one. PMS, VFO, Home and +/// every setting are the template's. No banks, so channel lists flatten. +/// +/// Overflow and frequencies the radio would not show as written are errors, +/// never a silent truncation or rounding. +pub(crate) fn patch_backup( + template: &[u8], + channels: &[&ExpandedChannel], + model: &RadioModel, +) -> Result, String> { + validate_backup(template)?; + if channels.len() > MEM_SLOTS { + return Err(format!( + "This codeplug has {} channels; the FTM-510 holds {MEM_SLOTS}. \ + Remove {} before exporting.", + channels.len(), + channels.len() - MEM_SLOTS + )); + } + let bad: Vec = channels + .iter() + .filter(|ec| !writable(hz(ec.channel.rx_freq)) || !writable(hz(tx_frequency(&ec.channel)))) + .map(|ec| format!("{:.5} MHz", ec.channel.rx_freq)) + .collect(); + if !bad.is_empty() { + return Err(format!( + "The FTM-510 cannot hold these as written — outside its 108–550 MHz range, or an \ + 8.33 kHz airband point whose code has not been measured on this radio: {}. \ + Remove them from this codeplug.", + bad.join(", ") + )); + } + let mut img = template.to_vec(); + let names = expanded_names(channels.iter().copied(), model); + for (slot, (ec, name)) in channels.iter().zip(&names).enumerate() { + write_slot(&mut img, slot, &encode_record(ec), &tag(name)); + } + for slot in channels.len()..MEM_SLOTS { + write_slot(&mut img, slot, &EMPTY_RECORD, ""); + } + debug_assert_eq!(&img[..MEM_BASE], &template[..MEM_BASE]); + Ok(img) +} + +fn hz(mhz: f64) -> i64 { + (mhz * 1_000_000.0).round() as i64 +} + +#[cfg(test)] +pub(super) fn decode_freq(b: &[u8]) -> i64 { + let bcd = |x: u8| ((x >> 4) * 10 + (x & 0x0F)) as i64; + let tens = (b[0] & 0x0F) as i64 * 10_000 + bcd(b[1]) * 100 + bcd(b[2]); + let mut hz = tens * 10_000; + for (bit, add) in [(0x80u8, 5000i64), (0x40, 2500), (0x20, 1250), (0x10, 625)] { + if b[0] & bit != 0 { + hz += add; + } + } + hz +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::Channel; + + fn chan(rx: f64, tx: Option) -> Channel { + Channel { + id: 1, + name_short: Some("TEST".into()), + rx_freq: rx, + tx_freq: tx, + dcs_polarity: "NN".into(), + cross_mode: "Tone->Tone".into(), + source: "test".into(), + ..Default::default() + } + } + + fn ec(c: Channel) -> ExpandedChannel { + ExpandedChannel { + channel: c, + tg_label: None, + timeslot: None, + tg_number: None, + tg_call_type: None, + tg_inline: false, + } + } + + fn template() -> Vec { + let mut t = vec![0x5Au8; IMAGE_LEN]; + t[..6].copy_from_slice(MAGIC); + t + } + + fn model() -> RadioModel { + crate::seed::test_model("FTM-510DR") + } + + /// Records copied out of Tim's factory-fresh FTM-510 (`captures/ftm510_01`): + /// memories 1 and 2 as the factory left them (144.000, no name). Both must + /// come out of the encoder byte for byte. + #[test] + fn the_factory_memories_reencode_byte_for_byte() { + let radio = [0x81, 0x00, 0x01, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x0F, 0x00, 0x0C, 0x00, 0x00]; + let mut c = chan(144.0, None); + c.name_short = None; + assert_eq!(encode_record(&ec(c)), radio); + } + + /// ADMS-18 (Yaesu's own programmer) writing memory 3 = 146.520 named `TEST`: + /// the record it wrote, with the AMS flag it sets on every new memory + /// (byte 1 `0x40`) masked, because this encoder programs FM, not AMS. + #[test] + fn matches_what_adms18_wrote_for_a_new_memory() { + let adms = [0x81, 0x40, 0x01, 0x46, 0x52, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x0F, 0x00, 0x0C, 0x00, 0x00]; + let mut ours = encode_record(&ec(chan(146.52, None))); + ours[1] |= 0x40; + assert_eq!(ours, adms); + let (field, _) = name_fields(&tag("TEST")); + assert_eq!(&field[..5], b"TEST\xFF"); + } + + /// The VFO / Home records the radio wrote, in the memory format: one per + /// band, each decoding to the defaults ADMS-18 displays for them. + #[test] + fn the_radios_band_records_decode() { + let rows: &[(&[u8; 16], i64, u16)] = &[ + (&[0x00, 0x10, 0x01, 0x08, 0x00, 0x07, 0, 0, 0, 0x0C, 0, 0x0F, 0x00, 0x00, 0, 0], 108_000_000, 0), + (&[0x01, 0x00, 0x01, 0x44, 0x00, 0x00, 0, 0, 0, 0x0C, 0, 0x0F, 0x00, 0x0C, 0, 0], 144_000_000, 12), + (&[0x02, 0x00, 0x02, 0x22, 0x00, 0x04, 0, 0, 0, 0x0C, 0, 0x0F, 0x00, 0x20, 0, 0], 222_000_000, 32), + (&[0x03, 0x00, 0x04, 0x30, 0x00, 0x07, 0, 0, 0, 0x0C, 0, 0x0F, 0x00, 0x64, 0, 0], 430_000_000, 100), + ]; + for (r, want_hz, want_shift) in rows { + assert_eq!(decode_freq(&r[2..5]), *want_hz); + assert_eq!(u16::from_be_bytes([r[12], r[13]]), *want_shift, "{want_hz}"); + } + // AIR is AM: the same byte-1 flag the encoder writes for an AM channel. + let mut c = chan(108.0, None); + c.mode = Some("AM".into()); + assert_eq!(encode_record(&ec(c))[1], rows[0].0[1]); + } + + #[test] + fn rejects_other_radios_files() { + assert!(validate_backup(&template()).is_ok()); + assert!(validate_backup(&[0u8; 100]).unwrap_err().contains("98304")); + let mut t = template(); + t[..6].copy_from_slice(b"AH071$"); + assert!(validate_backup(&t).unwrap_err().contains("FTM-300")); + t[..6].copy_from_slice(b"AH081$"); + assert!(validate_backup(&t).unwrap_err().contains("FTM-500")); + } + + /// Above 550 MHz is outside this radio (the FTM-300 went to 999), and the + /// 8.33 kHz airband code is unmeasured here — both refused by name. + #[test] + fn refuses_what_the_radio_would_not_show() { + for (rx, tx) in [(851.0125, None), (107.9, None), (127.80833, None), (146.52, Some(600.0))] { + let mut c = chan(rx, tx); + if rx < 137.0 { + c.mode = Some("AM".into()); + } + let e = ec(c); + let err = patch_backup(&template(), &[&e], &model()).unwrap_err(); + assert!(err.contains(&format!("{rx:.5}")), "{rx}: {err}"); + } + let e = ec(chan(549.9875, None)); + assert!(patch_backup(&template(), &[&e], &model()).is_ok()); + } + + #[test] + fn overflow_is_an_error() { + let e = ec(chan(146.52, None)); + let many: Vec<&ExpandedChannel> = std::iter::repeat_n(&e, MEM_SLOTS + 1).collect(); + assert!(patch_backup(&template(), &many, &model()).unwrap_err().contains("999")); + } + + /// Only the three memory tables change: every byte outside them, the + /// settings, VFO / Home and PMS entries included, is the template's. + #[test] + fn patches_only_the_regular_memories() { + let t = template(); + let mut c = chan(447.275, Some(442.275)); + c.tone_mode = Some("TSQL".into()); + c.ctcss_uplink = Some(100.0); + c.name_short = Some("NCARC".into()); + let e = ec(c); + let out = patch_backup(&t, &[&e], &model()).unwrap(); + assert_eq!(&out[0x800..0x810], &[0x83, 0x02, 0x84, 0x47, 0x27, 0x20, 0, 0, 0, 0x0C, 0, 0x0F, 0, 0x64, 0, 0]); + assert_eq!(&out[NAME_BASE..NAME_BASE + 6], b"NCARC\xFF"); + assert_eq!(&out[NAME_LEN_BASE..NAME_LEN_BASE + 2], &[0x07, 0xFF]); + assert_eq!(&out[0x810..0x820], &EMPTY_RECORD); + let in_tables = |i: usize| { + (MEM_BASE..MEM_BASE + MEM_SLOTS * MEM_STRIDE).contains(&i) + || (NAME_LEN_BASE..NAME_LEN_BASE + MEM_SLOTS * 2).contains(&i) + || (NAME_BASE..NAME_BASE + MEM_SLOTS * NAME_LEN).contains(&i) + }; + assert!((0..IMAGE_LEN).filter(|&i| !in_tables(i)).all(|i| out[i] == t[i])); + // PMS entries (slots 1000-1099) are past the 999 and untouched. + assert_eq!(out[MEM_BASE + MEM_SLOTS * MEM_STRIDE], 0x5A); + } + + /// Encode → decode over the fields this encoder writes, at every band + /// edge the radio claims and both sides of each internal boundary. + #[test] + fn frequencies_round_trip_across_the_coverage() { + for mhz in [108.0, 136.99375, 137.0, 146.52, 173.99375, 174.0, 222.1, 399.99375, 400.0, 446.10625, 479.99375, 480.0, 549.9875] { + let r = encode_record(&ec(chan(mhz, None))); + assert_eq!(decode_freq(&r[2..5]), hz(mhz), "{mhz}"); + let band = r[0] & 0x0F; + let want = [137.0, 174.0, 400.0, 480.0].iter().filter(|&&b| mhz >= b).count() as u8; + assert_eq!(band, want, "{mhz}"); + } + } +} diff --git a/src-tauri/src/radios/yaesu_ftm510/settings.rs b/src-tauri/src/radios/yaesu_ftm510/settings.rs new file mode 100644 index 0000000..b8e31fa --- /dev/null +++ b/src-tauri/src/radios/yaesu_ftm510/settings.rs @@ -0,0 +1,409 @@ +//! FTM-510 Setup Menu settings: decode from, and apply to, `CLNFTM510D.dat`. +//! +//! The field table and the profile form's schema are generated together from +//! one measurement (`scratchpad/yaesu_ftm510/adms/gen_settings.py`, fed by the +//! ADMS-18 campaign: 14 bit-signature passes, one-field isolation saves, and +//! token / radio-button / grid passes), so the decoder and the form cannot +//! drift. Every address was measured on files Yaesu's own ADMS-18 wrote for +//! this radio; none is copied from the FTM-300, though most turned out to match. +//! +//! The FTM-300's machinery (`yaesu_ftm300::settings`) covered most of this +//! radio, but not all of it: S-DX lives in three bits of three bytes, and a few +//! band-A/B settings are written to both bands' bytes at once. Hence two kinds +//! the FTM-300 never needed — `Bits` and a `mirror` byte — and this module of +//! its own rather than a change to a shipped radio's code. +//! +//! Settings share bytes all over `0x80-0xFF`, so every write is +//! read-modify-write: the other bits in the byte are someone else's setting. + +use serde_json::{Map, Value}; + +pub(crate) struct SF { + pub key: &'static str, + /// File offset into `CLNFTM510D.dat`. + pub byte: u32, + /// A second byte the same bits are written to (0 = none): ADMS-18 writes + /// Rear Out and SQL Expansion to both bands' bytes. + pub mirror: u32, + pub kind: SK, +} + +pub(crate) enum SK { + /// One bit. `active_low` stores ON as 0 — measured, never assumed. + Bool { shift: u8, active_low: bool }, + /// `width` bits starting at `shift`, mapped to labels by stored value. + Enum { + shift: u8, + width: u8, + labels: &'static [(u32, &'static str)], + }, + /// Big-endian u16 mapped to labels by stored value. + Enum16 { labels: &'static [(u32, &'static str)] }, + /// A value assembled from single bits scattered over the file: bit i of + /// the value is `bits[i]` = (offset, bit). `byte` is the first of them. + Bits { + bits: &'static [(u32, u8)], + labels: &'static [(u32, &'static str)], + }, + /// Fixed-length ASCII, padded with `pad`, restricted to `set`. + Text { len: u32, pad: u8, set: TS }, + /// One byte holding the number itself (the SmartBeaconing parameters). + /// The schema carries the manual's range; `settings_bounds` enforces it. + Uint, + /// A DTMF memory: one key per byte (0-9, A-D = 0x0A-0x0D, `*` 0x0E, `#` + /// 0x0F), padded with 0xFF — measured with ADMS-18 on all sixteen keys. + Dtmf { len: u32 }, +} + +/// What a text field may hold. Checked, never filtered: stripping a character +/// out would write a different call sign or message than the one asked for. +#[derive(Clone, Copy)] +pub(crate) enum TS { + /// Call sign: A-Z and 0-9 (stored upper case). + Call, + /// The radio's own call sign also takes `-` and `/`. + CallPunct, + /// An APRS message group or bulletin name: call-sign characters plus `*` + /// and `?` (the radio's defaults are `ALL******` and `BLN?`). + Group, + /// Free text — APRS messages and status texts: printable ASCII. + Plain, +} + +include!("ftm510_settings_table.rs"); + +fn mask(width: u8) -> u32 { + (1u32 << width) - 1 +} + +fn label_for(labels: &[(u32, &str)], raw: u32) -> Value { + match labels.iter().find(|(v, _)| *v == raw) { + Some((_, l)) => Value::String((*l).to_string()), + // An unknown stored value comes back as `#N` rather than a wrong label, + // so a write of an untouched profile cannot normalise it away. + None => Value::String(format!("#{raw}")), + } +} + +/// Every byte a field may change. +fn touches(f: &SF) -> Vec { + let at = f.byte as usize; + let mut out: Vec = match &f.kind { + SK::Enum16 { .. } => vec![at, at + 1], + SK::Text { len, .. } | SK::Dtmf { len } => (at..at + *len as usize).collect(), + SK::Bits { bits, .. } => bits.iter().map(|(b, _)| *b as usize).collect(), + _ => vec![at], + }; + if f.mirror != 0 { + out.push(f.mirror as usize); + } + out +} + +fn decode_field(img: &[u8], f: &SF) -> Value { + let at = f.byte as usize; + match &f.kind { + SK::Bool { shift, active_low } => Value::Bool(((img[at] >> shift) & 1 == 1) != *active_low), + SK::Enum { shift, width, labels } => label_for(labels, (img[at] as u32 >> shift) & mask(*width)), + SK::Enum16 { labels } => label_for(labels, u16::from_be_bytes([img[at], img[at + 1]]) as u32), + SK::Bits { bits, labels } => { + let raw = bits + .iter() + .enumerate() + .fold(0u32, |v, (i, (b, bit))| v | ((((img[*b as usize] >> bit) & 1) as u32) << i)); + label_for(labels, raw) + } + SK::Uint => Value::from(img[at]), + SK::Text { len, pad, .. } => { + let raw = &img[at..at + *len as usize]; + let cut = raw.iter().position(|&b| b == *pad || b == 0).unwrap_or(raw.len()); + Value::String(String::from_utf8_lossy(&raw[..cut]).trim_end().to_string()) + } + SK::Dtmf { len } => Value::String( + img[at..at + *len as usize] + .iter() + .take_while(|&&b| b < 16) + .map(|&b| DTMF_KEYS[b as usize] as char) + .collect(), + ), + } +} + +const DTMF_KEYS: &[u8; 16] = b"0123456789ABCD*#"; + +/// Decode every known setting, shaped like the profile form. +pub(crate) fn decode_settings(img: &[u8]) -> Value { + let mut out = Map::new(); + for f in FTM510_SETTINGS_FIELDS { + if touches(f).iter().any(|&i| i >= img.len()) { + continue; + } + out.insert(f.key.to_string(), decode_field(img, f)); + } + Value::Object(out) +} + +fn raw_for(labels: &[(u32, &str)], v: &Value) -> Option { + let s = v.as_str()?; + labels + .iter() + .find(|(_, l)| *l == s) + .map(|(raw, _)| *raw) + // An unknown raw value handed back by `label_for` round-trips as itself. + .or_else(|| s.strip_prefix('#')?.parse().ok()) +} + +fn text_fits(s: &str, len: u32, set: TS) -> bool { + let ok = |c: char| match set { + TS::Call => c.is_ascii_alphanumeric(), + TS::CallPunct => c.is_ascii_alphanumeric() || c == '-' || c == '/', + TS::Group => c.is_ascii_alphanumeric() || c == '*' || c == '?', + TS::Plain => (' '..='~').contains(&c), + }; + s.chars().count() <= len as usize && s.chars().all(ok) +} + +fn put_bits(img: &mut [u8], at: usize, shift: u8, width: u8, raw: u32) { + let m = (mask(width) << shift) as u8; + img[at] = (img[at] & !m) | ((raw << shift) as u8 & m); +} + +/// Write a profile's settings into the image, returning how many fields were +/// applied. Unknown keys and wrongly-shaped values are skipped, never guessed; +/// a field already holding the value is left untouched (no bytes rewritten). +pub(crate) fn apply_settings(img: &mut [u8], settings: &Map) -> usize { + let mut written = 0; + for f in FTM510_SETTINGS_FIELDS { + let Some(v) = settings.get(f.key) else { + continue; + }; + if touches(f).iter().any(|&i| i >= img.len()) { + continue; + } + if decode_field(img, f) == *v { + written += 1; + continue; + } + let at = f.byte as usize; + let mirror = f.mirror as usize; + let ok = match &f.kind { + SK::Bool { shift, active_low } => match v.as_bool() { + Some(b) => { + let bit = (b != *active_low) as u32; + put_bits(img, at, *shift, 1, bit); + if mirror != 0 { + put_bits(img, mirror, *shift, 1, bit); + } + true + } + None => false, + }, + SK::Enum { shift, width, labels } => match raw_for(labels, v) { + Some(raw) if raw <= mask(*width) => { + put_bits(img, at, *shift, *width, raw); + if mirror != 0 { + put_bits(img, mirror, *shift, *width, raw); + } + true + } + _ => false, + }, + SK::Enum16 { labels } => match raw_for(labels, v) { + Some(raw) if raw <= 0xFFFF => { + img[at..at + 2].copy_from_slice(&(raw as u16).to_be_bytes()); + true + } + _ => false, + }, + SK::Bits { bits, labels } => match raw_for(labels, v) { + Some(raw) if raw < 1 << bits.len() => { + for (i, (b, bit)) in bits.iter().enumerate() { + put_bits(img, *b as usize, *bit, 1, (raw >> i) & 1); + } + true + } + _ => false, + }, + SK::Uint => match v.as_u64() { + Some(n) if n <= 0xFF => { + img[at] = n as u8; + true + } + _ => false, + }, + SK::Dtmf { len } => match v.as_str().map(|s| s.to_ascii_uppercase()) { + Some(s) if s.len() <= *len as usize && s.bytes().all(|c| DTMF_KEYS.contains(&c)) => { + let field = &mut img[at..at + *len as usize]; + field.fill(0xFF); + for (d, c) in field.iter_mut().zip(s.bytes()) { + *d = DTMF_KEYS.iter().position(|&k| k == c).unwrap() as u8; + } + true + } + _ => false, + }, + // A value the field cannot hold is skipped, not cut down to fit. + SK::Text { len, pad, set } => match v.as_str() { + Some(s) if text_fits(s, *len, *set) => { + let s = match set { + TS::Plain => s.to_string(), + _ => s.to_ascii_uppercase(), + }; + let field = &mut img[at..at + *len as usize]; + field.fill(*pad); + field[..s.len()].copy_from_slice(s.as_bytes()); + true + } + _ => false, + }, + }; + written += usize::from(ok); + } + written +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::radios::yaesu_ftm510::sd_image::IMAGE_LEN; + + fn schema() -> Vec { + serde_json::from_str(include_str!("../../ftm510_settings_schema.json")).unwrap() + } + + /// The generator emits both halves; this is the guard that they still agree. + #[test] + fn table_and_schema_agree() { + let fields: Vec = schema().into_iter().filter(|f| f["type"] != "section").collect(); + assert_eq!(fields.len(), FTM510_SETTINGS_FIELDS.len()); + for (s, f) in fields.iter().zip(FTM510_SETTINGS_FIELDS) { + assert_eq!(s["key"], f.key); + match &f.kind { + SK::Bool { .. } => assert_eq!(s["type"], "boolean", "{}", f.key), + SK::Enum { labels, .. } | SK::Enum16 { labels } | SK::Bits { labels, .. } => { + let opts: Vec<&str> = s["options"].as_array().unwrap().iter().map(|o| o.as_str().unwrap()).collect(); + let want: Vec<&str> = labels.iter().map(|(_, l)| *l).collect(); + assert_eq!(opts, want, "{}", f.key); + } + SK::Text { len, .. } => { + assert_eq!(s["type"], "text", "{}", f.key); + assert_eq!(s["max_length"], *len, "{}", f.key); + } + SK::Uint => assert_eq!(s["type"], "integer", "{}", f.key), + SK::Dtmf { len } => { + assert_eq!(s["type"], "text", "{}", f.key); + assert_eq!(s["max_length"], *len, "{}", f.key); + } + } + } + let mut keys: Vec<&str> = FTM510_SETTINGS_FIELDS.iter().map(|f| f.key).collect(); + keys.sort_unstable(); + let n = keys.len(); + keys.dedup(); + assert_eq!(keys.len(), n, "duplicate settings key"); + } + + /// Every value of every field written and read back, and nothing outside + /// the field's own bytes moved — the shared-byte fields must not trample + /// each other. + #[test] + fn every_value_round_trips_alone() { + let base: Vec = (0..IMAGE_LEN).map(|i| (i * 37 % 251) as u8).collect(); + for f in FTM510_SETTINGS_FIELDS { + let values: Vec = match &f.kind { + SK::Bool { .. } => vec![Value::Bool(true), Value::Bool(false)], + SK::Enum { labels, .. } | SK::Enum16 { labels } | SK::Bits { labels, .. } => { + labels.iter().map(|(_, l)| Value::String((*l).into())).collect() + } + SK::Text { set: TS::Plain, .. } => vec![Value::String("Mobile 73".into())], + SK::Text { .. } => vec![Value::String("WW8L".into())], + SK::Uint => vec![Value::from(1), Value::from(255)], + SK::Dtmf { .. } => vec![Value::String("0123456789ABCD*#".into()), Value::String("73".into())], + }; + for v in values { + let mut img = base.clone(); + let mut m = Map::new(); + m.insert(f.key.into(), v.clone()); + assert_eq!(apply_settings(&mut img, &m), 1, "{} {v}", f.key); + assert_eq!(decode_settings(&img)[f.key], v, "{}", f.key); + let own = touches(f); + let moved: Vec = (0..img.len()).filter(|&i| img[i] != base[i]).collect(); + assert!(moved.iter().all(|i| own.contains(i)), "{} moved {moved:x?}", f.key); + if f.mirror != 0 { + assert_eq!(decode_field(&img, &SF { key: "", byte: f.mirror, mirror: 0, kind: clone_kind(&f.kind) }), v, "{} mirror", f.key); + } + } + } + } + + fn clone_kind(k: &SK) -> SK { + match k { + SK::Bool { shift, active_low } => SK::Bool { shift: *shift, active_low: *active_low }, + SK::Enum { shift, width, labels } => SK::Enum { shift: *shift, width: *width, labels }, + _ => unreachable!("only Bool and Enum fields are mirrored"), + } + } + + /// Unchanged values touch no bytes: decoding an image and writing the + /// result straight back must be a no-op. + #[test] + fn writing_back_what_was_read_changes_nothing() { + let img: Vec = (0..IMAGE_LEN).map(|i| (i * 91 % 253) as u8).collect(); + let decoded = decode_settings(&img); + let mut out = img.clone(); + apply_settings(&mut out, decoded.as_object().unwrap()); + assert_eq!(out, img); + } + + /// A value the field cannot hold is refused whole, never trimmed. + #[test] + fn a_text_value_that_does_not_fit_is_not_written() { + let base = vec![0u8; IMAGE_LEN]; + for (key, bad) in [("aprs-callsign", "WW8L-9"), ("aprs-callsign", "WW8LXYZ"), ("callsign", "WW8L 1"), ("aprs-message-group-1", "ALL-CALL")] { + let mut img = base.clone(); + let mut m = Map::new(); + m.insert(key.into(), Value::String(bad.into())); + assert_eq!(apply_settings(&mut img, &m), 0, "{key} {bad}"); + assert_eq!(img, base, "{key} {bad}"); + } + let mut img = base.clone(); + let mut m = Map::new(); + m.insert("callsign".into(), Value::String("ww8l/m".into())); + assert_eq!(apply_settings(&mut img, &m), 1); + assert_eq!(decode_settings(&img)["callsign"], "WW8L/M"); + } + + /// The factory-fresh radio's own file (bytes copied out of capture 01) + /// decodes to the manual's printed defaults for the fields whose default is + /// distinctive — the cross-check that refuted three FTM-300 addresses. + #[test] + fn the_new_radios_defaults_decode_to_the_manuals() { + let mut img = vec![0u8; IMAGE_LEN]; + for (at, b) in [ + (0xA4, 0x00), (0xAA, 0x01), (0xAD, 0x00), (0xAF, 0x15), (0xB5, 0x00), (0xB6, 0x00), + (0xB7, 0x00), (0xEB, 0x88), (0xFA, 0x22), (0xFB, 0x20), (0x28F, 0x06), (0x2D6, 0x01), + (0x2DD, 0x02), (0x2DF, 0xDD), (0x2E0, 0xE1), (0x2E1, 0xE8), (0x2E2, 0xE9), (0x2C8, b'W'), + (0x2C9, b'W'), (0x2CA, b'8'), (0x2CB, b'L'), + ] { + img[at] = b; + } + img[0x2CC..0x2D2].fill(0xFF); + let s = decode_settings(&img); + for (k, want) in [ + ("auto-power-off", "OFF"), ("time-out-timer", "5 min"), ("display-select", "BACKTRACK"), + ("gps-datum", "WGS-84"), ("gps-log", "OFF"), ("vox", "OFF"), ("vox-delay", "0.5s"), + ("date-format", "mmm/dd/yyyy"), ("time-format", "24 hour"), ("lcd-brightness", "MAX"), + ("compass", "HEADING UP"), ("mic-gain", "NORMAL"), ("mic-key-p1", "2nd PTT"), + ("mic-key-p2", "HOME CH"), ("mic-key-p3", "D_X"), ("mic-key-p4", "WX"), ("callsign", "WW8L"), + ] { + assert_eq!(s[k], want, "{k}"); + } + assert_eq!(s["beep"], true); + assert_eq!(s["beep-level"], "LOW"); + } + + #[test] + fn an_aprs_radio_gets_aprs_settings() { + assert!(schema().iter().filter(|f| f["group"] == "APRS").count() >= 1); + } +} diff --git a/src-tauri/src/seed.rs b/src-tauri/src/seed.rs index 4540fd2..d1c2bac 100644 --- a/src-tauri/src/seed.rs +++ b/src-tauri/src/seed.rs @@ -1306,6 +1306,61 @@ fn models() -> Vec { // microSD backup, not over a cable. non_channel_settings_schema: include_str!("ftm300_settings_schema.json"), }, + // -------------------------------------------------------- + // Yaesu FTM-510DR (#151) — the FTM-300's successor family (FTM-500/510): + // C4FM + analog FM dual-band mobile, 999 memories (plus 50 PMS + // pairs and the VFO / Home records the writer leaves alone), + // 16-character tags, 55 W on 2 m / 50 W on 70 cm. + // Programmed from its own microSD card: `yaesu_ftm510_sd` patches + // FTM510D/BACKUP/CLNFTM510D.dat (magic AH089$, the FTM-300's layout) + // in place, and Setup Menu 107 BACKUP → READ FROM SD → ALL loads it + // back; see radios/yaesu_ftm510/. + // NOTES: (a) no banks or scan lists, so channel lists flatten; the + // 510 has PMG, not the FTM-300's M-GRP, so no memory group. (b) + // tx_bands is the DR's US 144–148 / 430–450; the DE is 144–146 / + // 430–440. (c) rx_bands is the Operating Manual's continuous + // 108–550 MHz — hardware-proven: the ladder's band probe landed all + // 11 edges and boundaries (2026-11-03). + // -------------------------------------------------------- + ModelSeed { + manufacturer: "Yaesu", + model: "FTM-510DR", + driver_key: Some("yaesu_ftm510"), + programming_ui: Some("generic"), + display_name: "Yaesu FTM-510DR", + analog_capable: true, + dmr_capable: false, + dstar_capable: false, + ysf_capable: true, + nxdn_capable: false, + p25_capable: false, + m17_capable: false, + aprs_capable: true, + covers_hf: false, + covers_vhf: true, + covers_uhf: true, + covers_220: false, + covers_900: false, + freq_min: 144.0, + freq_max: 450.0, + tx_bands: Some("[[144.0,148.0],[430.0,450.0]]"), + rx_bands: Some("[[108.0,550.0]]"), + memory_channels: 999, + zones_supported: false, + max_zones: None, + channels_per_zone: None, + scan_lists_supported: false, + max_scan_lists: None, + banks_supported: false, + max_name_length: 16, + export_format: "yaesu_ftm510_sd", + connection_type: "microSD", + // GENERATED by scratchpad/yaesu_ftm510/adms/gen_settings.py — the same + // spec emits radios/yaesu_ftm510/ftm510_settings_table.rs, so the form + // and the decoder cannot drift. Read out of and written back into the + // microSD backup, not over a cable. + non_channel_settings_schema: include_str!("ftm510_settings_schema.json"), + }, ] } diff --git a/src/components/profiles/ProfileEditor.tsx b/src/components/profiles/ProfileEditor.tsx index 7c6f908..41d160b 100644 --- a/src/components/profiles/ProfileEditor.tsx +++ b/src/components/profiles/ProfileEditor.tsx @@ -236,6 +236,7 @@ const CARD_SETTINGS_READERS: Record< icom_id52_icf: api.readId52SettingsFromCard, kenwood_thd75_sd: api.readThd75SettingsFromCard, yaesu_ftm300_sd: api.readFtm300SettingsFromBackup, + yaesu_ftm510_sd: api.readFtm510SettingsFromBackup, }; /** diff --git a/src/lib/api.ts b/src/lib/api.ts index 9a09f1e..bc90025 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -400,6 +400,9 @@ export const api = { // The FTM-300's CLNFTM300D.dat; same shape as the FT5D's. See radios/yaesu_ftm300/settings.rs. readFtm300SettingsFromBackup: (path: string) => invoke("read_ftm300_settings_from_backup", { path }), + // The FTM-510's CLNFTM510D.dat; same shape again. See radios/yaesu_ftm510/settings.rs. + readFtm510SettingsFromBackup: (path: string) => + invoke("read_ftm510_settings_from_backup", { path }), // A card radio's settings alone, patched into its own file on the card — // every memory left as the file has it. The radio loads it from its menu. writeCardSettings: (profileId: number, path: string) => diff --git a/src/lib/radioProgramming.ts b/src/lib/radioProgramming.ts index b900b24..43889ee 100644 --- a/src/lib/radioProgramming.ts +++ b/src/lib/radioProgramming.ts @@ -217,6 +217,24 @@ const MEDIA_WRITES: Record = { carries: "your call sign, your APRS call sign and SSID, a fixed My Position if you have set one, and the APRS station list and messages the radio has heard — including other stations' positions", }, + // The FTM-300's sibling: same arrangement, its own folder and file. Menu + // path is the FTM-510 Advance Manual's (107 BACKUP). + yaesu_ftm510_sd: { + action: "Write to SD card…", + pickTitle: "Select FTM510D/BACKUP/CLNFTM510D.dat on the radio’s microSD card", + filterName: "FTM-510 backup", + extensions: ["dat"], + before: + "On the radio, save everything to the card: press and hold the FUNC knob → 107 BACKUP → WRITE TO SD → ALL → OK. Then put the card in this computer and pick FTM510D/BACKUP/CLNFTM510D.dat below.", + after: + "Put the card back in the radio and load it: press and hold the FUNC knob → 107 BACKUP → READ FROM SD → ALL → pick CLNFTM510D → OK. The radio restarts when it has loaded.", + found: + "It already holds a backup, so there is nothing to pick — just check it is current (the radio writes it with the FUNC knob → 107 BACKUP → WRITE TO SD → ALL).", + preserves: + "The FTM-510 has no banks, so channel lists become one memory list in order. Everything else already on the radio — Home, VFO and PMS channels, APRS, GPS, WIRES-X and every Setup Menu item your profile does not set — is left untouched. The first write saves your untouched file beside it as CLNFTM510D.dat.orig; later writes never overwrite that pristine copy.", + carries: + "your call sign, your APRS call sign and SSID, a fixed My Position if you have set one, and the APRS station list and messages the radio has heard — including other stations' positions", + }, }; /// How a model is programmed from removable media, or null if it is a cable