Add FT-991A Remote Control App for macOS

Complete Phase 1 implementation of the Yaesu FT-991A remote control
application with CAT protocol support over USB serial (CP210x).

Features implemented:
- SerialPortManager with auto-detection of CP210x ports
- Full CAT protocol parser and command builder
- RadioState model with all transceiver parameters
- Modern SwiftUI interface with frequency/mode/level controls
- Skeuomorphic front panel view (switchable)
- Debug panel with CAT command console
- QSO log panel with CSV export/import
- Audio routing panel with BlackHole integration
- Settings with connection, UI, keyboard configuration
- Menu bar extra for background operation
- German/English localization
- Logging system for debugging

Supports: Frequency control, VFO A/B, all modes (LSB/USB/CW/FM/AM/
DATA/RTTY/C4FM), level controls, NB/NR/DNF/ATU/Split functions,
S-meter/Power/SWR metering, PTT control via Shift key.

Target: macOS 15.0+ (Sequoia/Tahoe)
This commit is contained in:
Claude
2025-12-18 10:59:15 +00:00
parent 20904e2a96
commit 1e153f2f85
30 changed files with 6665 additions and 0 deletions
@@ -0,0 +1,306 @@
//
// RadioViewModel.swift
// FT991A-Remote
//
// Main ViewModel for radio control
//
import Foundation
import Combine
import SwiftUI
// MARK: - Radio ViewModel
@MainActor
class RadioViewModel: ObservableObject {
// MARK: - Published Properties
// Connection
@Published var isConnected = false
@Published var connectionState: ConnectionState = .disconnected
@Published var availablePorts: [SerialPort] = []
@Published var selectedPort: String = ""
@Published var baudRate: Int = 38400
// Radio State (mirrored for convenience)
@Published var vfoAFrequency: Int = 14_250_000
@Published var vfoBFrequency: Int = 14_255_000
@Published var activeVFO: VFO = .a
@Published var mode: OperatingMode = .usb
@Published var frequencyStep: FrequencyStep = .khz1
// Levels
@Published var afGain: Int = 128
@Published var rfGain: Int = 255
@Published var squelch: Int = 0
@Published var micGain: Int = 50
@Published var power: Int = 100
// Functions
@Published var noiseBlanker = false
@Published var noiseReduction = false
@Published var dnf = false
@Published var contour = false
@Published var atu = false
@Published var split = false
@Published var ipo = false
// Metering
@Published var sMeter: Int = 0
@Published var powerMeter: Int = 0
@Published var swrMeter: Int = 0
@Published var isTransmitting = false
// Statistics
@Published var bytesSent: UInt64 = 0
@Published var bytesReceived: UInt64 = 0
// Debug
@Published var commandHistory: [CommandLogEntry] = []
// MARK: - Services
private let serialManager = SerialPortManager()
private let catProtocol: CATProtocol
private var cancellables = Set<AnyCancellable>()
// MARK: - Computed Properties
var activeFrequency: Int {
activeVFO == .a ? vfoAFrequency : vfoBFrequency
}
var frequencyDisplay: String {
formatFrequency(activeFrequency)
}
var sMeterDisplay: String {
let normalized = Double(sMeter) / 255.0
if normalized <= 0.6 {
let sUnit = Int(normalized / 0.6 * 9.0)
return "S\(sUnit)"
} else {
let db = Int((normalized - 0.6) / 0.4 * 60.0)
return "S9+\(db)"
}
}
var currentBand: Band? {
Band.from(frequency: activeFrequency)
}
// MARK: - Initialization
init() {
catProtocol = CATProtocol(serialManager: serialManager)
setupBindings()
refreshPorts()
}
private func setupBindings() {
// Serial Manager bindings
serialManager.$connectionState
.receive(on: DispatchQueue.main)
.sink { [weak self] state in
self?.connectionState = state
self?.isConnected = state.isConnected
}
.store(in: &cancellables)
serialManager.$availablePorts
.receive(on: DispatchQueue.main)
.assign(to: &$availablePorts)
serialManager.$selectedPortPath
.receive(on: DispatchQueue.main)
.assign(to: &$selectedPort)
serialManager.$bytesSent
.receive(on: DispatchQueue.main)
.assign(to: &$bytesSent)
serialManager.$bytesReceived
.receive(on: DispatchQueue.main)
.assign(to: &$bytesReceived)
// CAT Protocol bindings
catProtocol.$radioState
.receive(on: DispatchQueue.main)
.sink { [weak self] state in
self?.updateFromRadioState(state)
}
.store(in: &cancellables)
catProtocol.$commandHistory
.receive(on: DispatchQueue.main)
.assign(to: &$commandHistory)
}
private func updateFromRadioState(_ state: RadioState) {
vfoAFrequency = state.vfoAFrequency
vfoBFrequency = state.vfoBFrequency
activeVFO = state.activeVFO
mode = state.mode
afGain = state.afGain
rfGain = state.rfGain
squelch = state.squelch
micGain = state.micGain
power = state.power
noiseBlanker = state.noiseBlanker
noiseReduction = state.noiseReduction
dnf = state.dnf
contour = state.contour
atu = state.atu
split = state.split
ipo = state.ipo
sMeter = state.sMeter
powerMeter = state.powerMeter
swrMeter = state.swrMeter
isTransmitting = state.isTransmitting
}
// MARK: - Connection
func refreshPorts() {
serialManager.refreshPorts()
}
func connect() {
serialManager.selectedPortPath = selectedPort
serialManager.baudRate = baudRate
serialManager.connect()
}
func disconnect() {
serialManager.disconnect()
}
func toggleConnection() {
if isConnected {
disconnect()
} else {
connect()
}
}
func selectPort(_ path: String) {
selectedPort = path
serialManager.selectedPortPath = path
}
// MARK: - Frequency Control
func setFrequency(_ frequency: Int) {
catProtocol.setFrequency(frequency, vfo: activeVFO)
}
func incrementFrequency() {
catProtocol.changeFrequency(by: frequencyStep.rawValue)
}
func decrementFrequency() {
catProtocol.changeFrequency(by: -frequencyStep.rawValue)
}
func selectBand(_ band: Band) {
catProtocol.selectBand(band)
}
// MARK: - VFO Control
func selectVFO(_ vfo: VFO) {
catProtocol.selectVFO(vfo)
}
func swapVFO() {
catProtocol.swapVFO()
}
func equalizeVFO() {
catProtocol.equalizeVFO()
}
// MARK: - Mode Control
func setMode(_ mode: OperatingMode) {
catProtocol.setMode(mode)
}
// MARK: - Level Control
func setAFGain(_ value: Int) {
catProtocol.setAFGain(value)
}
func setRFGain(_ value: Int) {
catProtocol.setRFGain(value)
}
func setSquelch(_ value: Int) {
catProtocol.setSquelch(value)
}
func setMICGain(_ value: Int) {
catProtocol.setMICGain(value)
}
func setPower(_ value: Int) {
catProtocol.setPower(value)
}
// MARK: - Function Control
func toggleNB() {
catProtocol.toggleNB()
}
func toggleNR() {
catProtocol.toggleNR()
}
func toggleDNF() {
catProtocol.toggleDNF()
}
func toggleSplit() {
catProtocol.toggleSplit()
}
func startATUTune() {
catProtocol.startATUTune()
}
// MARK: - PTT Control
func startTransmit(dataMode: Bool = false) {
catProtocol.startTransmit(dataMode: dataMode)
}
func stopTransmit() {
catProtocol.stopTransmit()
}
func toggleTransmit(dataMode: Bool = false) {
catProtocol.toggleTransmit(dataMode: dataMode)
}
// MARK: - Debug
func sendRawCommand(_ command: String) {
catProtocol.sendRaw(command)
}
func clearCommandHistory() {
catProtocol.clearCommandHistory()
}
// MARK: - Helpers
func formatFrequency(_ freq: Int) -> String {
let mhz = freq / 1_000_000
let khz = (freq % 1_000_000) / 1_000
let hz = freq % 1_000
return String(format: "%d.%03d.%03d", mhz, khz, hz)
}
}