Files
Ai/FT991A-Remote/FT991A-Remote/Utilities/Logger.swift
T
Claude 1e153f2f85 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)
2025-12-18 10:59:15 +00:00

224 lines
5.9 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// Logger.swift
// FT991A-Remote
//
// Debug logging system
//
import Foundation
import os.log
// MARK: - Log Level
enum LogLevel: String, Comparable {
case debug = "DEBUG"
case info = "INFO"
case warning = "WARN"
case error = "ERROR"
var osLogType: OSLogType {
switch self {
case .debug: return .debug
case .info: return .info
case .warning: return .default
case .error: return .error
}
}
var symbol: String {
switch self {
case .debug: return "🔍"
case .info: return "️"
case .warning: return "⚠️"
case .error: return "❌"
}
}
static func < (lhs: LogLevel, rhs: LogLevel) -> Bool {
let order: [LogLevel] = [.debug, .info, .warning, .error]
guard let lhsIndex = order.firstIndex(of: lhs),
let rhsIndex = order.firstIndex(of: rhs) else { return false }
return lhsIndex < rhsIndex
}
}
// MARK: - Log Entry
struct LogEntry: Identifiable {
let id = UUID()
let timestamp: Date
let level: LogLevel
let message: String
let file: String
let function: String
let line: Int
var timeString: String {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss.SSS"
return formatter.string(from: timestamp)
}
var shortFile: String {
URL(fileURLWithPath: file).lastPathComponent
}
var formattedMessage: String {
"[\(timeString)] [\(level.rawValue)] \(message)"
}
var detailedMessage: String {
"[\(timeString)] [\(level.rawValue)] [\(shortFile):\(line)] \(message)"
}
}
// MARK: - Logger
class Logger: ObservableObject {
// MARK: - Singleton
static let shared = Logger()
// MARK: - Published Properties
@Published var entries: [LogEntry] = []
@Published var minimumLevel: LogLevel = .debug
@Published var isLoggingEnabled = true
// MARK: - Private Properties
private let osLog = OSLog(subsystem: "com.ft991a.remote", category: "General")
private let queue = DispatchQueue(label: "logger.queue", qos: .utility)
private let maxEntries = 1000
// File logging
private var logFileURL: URL?
private var logFileHandle: FileHandle?
// MARK: - Initialization
private init() {
setupFileLogging()
}
deinit {
logFileHandle?.closeFile()
}
// MARK: - Logging
func log(
_ message: String,
level: LogLevel = .info,
file: String = #file,
function: String = #function,
line: Int = #line
) {
guard isLoggingEnabled, level >= minimumLevel else { return }
let entry = LogEntry(
timestamp: Date(),
level: level,
message: message,
file: file,
function: function,
line: line
)
// Console output
queue.async {
os_log("%{public}@", log: self.osLog, type: level.osLogType, entry.formattedMessage)
#if DEBUG
print(entry.detailedMessage)
#endif
}
// In-memory storage
DispatchQueue.main.async {
self.entries.append(entry)
if self.entries.count > self.maxEntries {
self.entries.removeFirst(100)
}
}
// File logging
writeToFile(entry)
}
// MARK: - Convenience Methods
func debug(_ message: String, file: String = #file, function: String = #function, line: Int = #line) {
log(message, level: .debug, file: file, function: function, line: line)
}
func info(_ message: String, file: String = #file, function: String = #function, line: Int = #line) {
log(message, level: .info, file: file, function: function, line: line)
}
func warning(_ message: String, file: String = #file, function: String = #function, line: Int = #line) {
log(message, level: .warning, file: file, function: function, line: line)
}
func error(_ message: String, file: String = #file, function: String = #function, line: Int = #line) {
log(message, level: .error, file: file, function: function, line: line)
}
// MARK: - File Logging
private func setupFileLogging() {
let fileManager = FileManager.default
guard let logsDir = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else { return }
let appLogsDir = logsDir.appendingPathComponent("FT991A-Remote/Logs", isDirectory: true)
do {
try fileManager.createDirectory(at: appLogsDir, withIntermediateDirectories: true)
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
let fileName = "ft991a_\(formatter.string(from: Date())).log"
logFileURL = appLogsDir.appendingPathComponent(fileName)
if !fileManager.fileExists(atPath: logFileURL!.path) {
fileManager.createFile(atPath: logFileURL!.path, contents: nil)
}
logFileHandle = try FileHandle(forWritingTo: logFileURL!)
logFileHandle?.seekToEndOfFile()
let header = "\n=== FT-991A Remote Log Started at \(Date()) ===\n"
if let data = header.data(using: .utf8) {
logFileHandle?.write(data)
}
} catch {
print("Failed to setup file logging: \(error)")
}
}
private func writeToFile(_ entry: LogEntry) {
guard let handle = logFileHandle else { return }
queue.async {
if let data = (entry.detailedMessage + "\n").data(using: .utf8) {
handle.write(data)
}
}
}
// MARK: - Management
func clear() {
entries.removeAll()
}
func exportLogs() -> String {
entries.map { $0.detailedMessage }.joined(separator: "\n")
}
var filteredEntries: [LogEntry] {
entries.filter { $0.level >= minimumLevel }
}
}