import Foundation import NeXTsoundCore import USBShim // A bench probe for one SoundBox: reads and writes its HID feature reports through libusb, as the app does, and // prints what came back. It never resets, flashes or changes the host's audio defaults. let vendorID: UInt16 = 0xCAFE let productID: UInt16 = 0x4014 let hidInterface: UInt16 = 2 let reportValue: UInt16 = 0x0301 final class Board { private var context: UnsafeMutableRawPointer? private var handle: UnsafeMutableRawPointer? init() throws { let result = nxs_libusb_open(vendorID, productID, &context, &handle) guard result == 0 else { throw ProbeError.transport("open \(result)") } } deinit { nxs_libusb_close(context, handle) } /// Sends raw report bytes and returns the raw reply bytes. func transfer(_ report: Data) throws -> Data { var reply = [UInt8](repeating: 0, count: WireCodec.reportSize) let count = report.withUnsafeBytes { request in reply.withUnsafeMutableBufferPointer { buffer in nxs_libusb_exchange_feature_report( context, handle, hidInterface, reportValue, request.bindMemory(to: UInt8.self).baseAddress, buffer.baseAddress, buffer.count, 2000 ) } } guard count == WireCodec.reportSize else { throw ProbeError.transport("exchange \(count)") } return Data(reply) } /// Sends a request and polls with the identical bytes until its own reply arrives, as the app does for deferred /// replies. func exchange(_ request: WireMessage) throws -> WireMessage { let report = try WireCodec.encode(request) let deadline = Date().addingTimeInterval(10) while true { let reply = try WireCodec.decode(transfer(report)) if reply.matchesRequest(request) { return reply } guard Date() < deadline else { throw ProbeError.transport("no reply to request \(request.requestID)") } usleep(50000) } } func request(_ operation: WireOperation, minor: UInt8, generation: UInt32 = 0, payload: Data = Data()) throws -> WireMessage { let message = WireMessage( operation: operation, requestID: .random(in: 1 ... .max), generation: generation, payload: payload, protocolMinor: minor ) return try exchange(message) } } enum ProbeError: Error { case transport(String) case failed(String) } func hex(_ data: Data) -> String { data.map { String(format: "%02x", $0) }.joined() } func result(_ reply: WireMessage) -> String { reply.resultCode == 0 ? "ok" : "result \(reply.resultCode)" } /// Prints capabilities at both protocol minors, status, the active snapshot and the diagnostics as key=value lines. func dump(_ board: Board) throws { for minor: UInt8 in [0, 1] { let reply = try board.request(.capabilities, minor: minor) let capabilities = try WireCodec.decodeCapabilities(reply.payload) print( "capabilities.minor\(minor) firmware=\(capabilities.firmwareVersion) serial=\(capabilities.serial)", "name=\"\(capabilities.deviceName)\" flags=\(capabilities.featureFlags) metadata=\(capabilities.supportsMetadata)", "generation=\(reply.generation)" ) } let status = try board.request(.status, minor: 0) let runtime = try WireCodec.decodeRuntimeStatus(status.payload) print( "status generation=\(status.generation) persisted=\(runtime.persistedGeneration) storage=\(runtime.storageIsAvailable)", "maxUs=\(runtime.maxProcessingUs) underruns=\(runtime.underrunCount) overruns=\(runtime.overrunCount)" ) let state = try board.request(.readState, minor: 0) let snapshot = try WireCodec.decodeSnapshot(state.payload) print("snapshot generation=\(state.generation) muted=\(snapshot.settings.isMuted) attenuation=\(snapshot.settings.attenuationDb)") print("snapshot.hex=\(hex(state.payload))") } /// Reads both metadata parts and prints the decoded fields with the generation of each reply. func readMetadata(_ board: Board) throws { var block = Data() var generations: [UInt32] = [] for part in 0 ..< WireCodec.metadataPartCount { let reply = try board.request(.readMetadata, minor: 1, payload: WireCodec.metadataReadPayload(part: part)) guard reply.resultCode == 0 else { print("readMetadata part \(part): \(result(reply))") return } block += reply.payload generations.append(reply.generation) } let metadata = try WireCodec.decodeOwnerMetadata(block) let date = metadata.date.map { "\($0.year)-\($0.month)-\($0.day)" } ?? "unset" print( "metadata generations=\(generations) date=\(date) serial=\(metadata.serialNumber.map(String.init) ?? "unset")", "owner=\"\(metadata.owner)\" note=\"\(metadata.note.replacingOccurrences(of: "\n", with: "\\n"))\"" ) } /// Writes metadata in its two parts at the board's current generation and prints each reply. func writeMetadata(_ board: Board, _ metadata: OwnerMetadata) throws { let generation = try board.request(.capabilities, minor: 0).generation for (part, payload) in try WireCodec.metadataWritePayloads(metadata).enumerated() { let reply = try board.request(.writeMetadata, minor: 1, generation: generation, payload: payload) print("write part \(part) at \(generation): \(result(reply)) generation=\(reply.generation)") } } let arguments = Array(CommandLine.arguments.dropFirst()) let board = try Board() switch arguments.first { case "dump": try dump(board) case "read-metadata": try readMetadata(board) case "write-test-metadata": try writeMetadata(board, OwnerMetadata( date: CalendarDay(year: 2026, month: 10, day: 24), serialNumber: 0xFF_FFFF, owner: "Bench test, Österreich", note: "Written by the bench probe.\nSecond line." )) case "clear-metadata": try writeMetadata(board, OwnerMetadata()) default: print("usage: BoardProbe dump | read-metadata | write-test-metadata | clear-metadata") }