import Foundation import AppKit import IOBluetooth // MiniToo wire format documented by alvinunreal/divoom-minitoo-osx. // This helper keeps one Bluetooth Classic connection and watches an atomic packet file. final class Receiver: NSObject, IOBluetoothRFCOMMChannelDelegate { private var received = Data() private let lock = NSLock() func summary() -> String { lock.lock(); defer { lock.unlock() }; return received.prefix(80).map { String(format: "%02x", $0) }.joined() } func reset() { lock.lock(); defer { lock.unlock() }; received.removeAll() } func contains(_ bytes: Data) -> Bool { lock.lock(); defer { lock.unlock() }; return received.range(of: bytes) != nil } func rfcommChannelData(_ channel: IOBluetoothRFCOMMChannel!, data pointer: UnsafeMutableRawPointer!, length: Int) { lock.lock(); defer { lock.unlock() } received.append(Data(bytes: pointer, count: length)) if received.count > 65536 { received = Data(received.suffix(32768)) } } } func pump(_ seconds: Double) { let deadline = Date().addingTimeInterval(seconds) while Date() < deadline { RunLoop.current.run(until: deadline) // A worker run loop can return immediately when it has no event sources. let remaining = deadline.timeIntervalSinceNow if remaining > 0 { Thread.sleep(forTimeInterval: min(remaining, 0.01)) } } } func fail(_ message: String) -> Never { fputs(message + "\n", stderr); exit(1) } let application = NSApplication.shared application.setActivationPolicy(.accessory) application.finishLaunching() let args = CommandLine.arguments if args.count == 2 && args[1] == "--list" { for case let device as IOBluetoothDevice in IOBluetoothDevice.pairedDevices() ?? [] { if (device.name ?? "").lowercased().contains("divoom") { print("\(device.name ?? "Divoom") | \(device.addressString ?? "unknown") | connected=\(device.isConnected())") } } exit(0) } guard args.count == 3 else { fail("Usage: display DEVICE_ADDRESS PACKET_FILE, or display --list") } DispatchQueue.global(qos: .utility).async { guard let device = IOBluetoothDevice(addressString: args[1]) else { fail("Invalid Bluetooth address") } let path = URL(fileURLWithPath: args[2]) freopen(path.deletingLastPathComponent().appendingPathComponent("transport.log").path, "a", stdout) let receiver = Receiver() var channel: IOBluetoothRFCOMMChannel? var sent: Data? var nextAttempt = Date.distantPast while true { pump(2) if Date() < nextAttempt { continue } guard let data = try? Data(contentsOf: path), data != sent else { continue } // Validate the entire packet file before writing anything to the device. var packets: [Data] = [] var offset = 0 var valid = !data.isEmpty && data.count < 1_000_000 while valid && offset < data.count { if offset + 2 > data.count { valid = false; break } let count = Int(data[offset]) | Int(data[offset+1]) << 8 offset += 2 if count < 7 || count > 512 || offset + count > data.count { valid = false; break } let packet = Data(data[offset..