import Foundation enum PlaybackMode: Int, CaseIterable, Identifiable, Hashable { case hoerspiel = 1, album, party, single, audiobook, admin = 255 case hoerspielRange = 7, albumRange, partyRange, audiobookSingle, repeatLast, quiz, memory, bluetooth, teapot, audiobookRange var id: Int { rawValue } var title: String { switch self { case .hoerspiel: "Hörspiel"; case .album: "Album"; case .party: "Party"; case .single: "Einzelner Track" case .audiobook: "Hörbuch"; case .admin: "Admin-Karte"; case .hoerspielRange: "Hörspiel von-bis" case .albumRange: "Album von-bis"; case .partyRange: "Party von-bis"; case .audiobookSingle: "Hörbuch einzeln" case .repeatLast: "Letzte Karte wiederholen"; case .quiz: "Quiz-Spiel"; case .memory: "Memory-Spiel" case .bluetooth: "Bluetooth umschalten"; case .teapot: "Teekesselchen-Spiel"; case .audiobookRange: "Hörbuch von-bis" } } var needsFolder: Bool { ![.repeatLast, .bluetooth].contains(self) } var needsSpecial: Bool { [.single, .audiobookSingle, .hoerspielRange, .albumRange, .partyRange, .quiz, .audiobookRange].contains(self) } var needsSpecial2: Bool { [.hoerspielRange, .albumRange, .partyRange, .quiz, .audiobookRange].contains(self) } } enum TonuinoModifier: Int, CaseIterable, Identifiable, Hashable { case sleepTimer = 1 case freezeDance case fireWaterAir case toddlerMode case kindergartenMode case repeatSingle case bluetoothToggle case jukebox case pauseAfterTrack case standbyTimerToggle case endlessToggle var id: Int { rawValue } var title: String { switch self { case .sleepTimer: "Einschlaf-Timer" case .freezeDance: "Stopptanz" case .fireWaterAir: "Feuer, Wasser, Luft" case .toddlerMode: "Kleinkindmodus" case .kindergartenMode: "Kindergartenmodus" case .repeatSingle: "Track wiederholen" case .bluetoothToggle: "Bluetooth umschalten" case .jukebox: "Jukebox" case .pauseAfterTrack: "Nach jedem Track pausieren" case .standbyTimerToggle: "Standby-Timer umschalten" case .endlessToggle: "Endlosmodus umschalten" } } var firmwareNote: String? { switch self { case .bluetoothToggle: "Benötigt eine TonUINO-Firmware mit BT_MODULE." case .jukebox: "Benötigt eine TonUINO-Firmware mit MODIFICATION_CARD_JUKEBOX." case .pauseAfterTrack: "Benötigt eine TonUINO-Firmware mit MODIFICATION_CARD_PAUSE_AFTER_TRACK." case .standbyTimerToggle: "Benötigt TonUINO-TNG 3.3.2 oder neuer." case .endlessToggle: "Benötigt TonUINO-TNG 3.3.3 oder neuer." default: nil } } } struct TonuinoCard: Equatable { var folder: UInt8 var mode: PlaybackMode var special: UInt8 = 0 var special2: UInt8 = 0 var modifier: TonuinoModifier? = nil var displayTitle: String { modifier?.title ?? mode.title } func payload() -> Data { Data([0x13, 0x37, 0xB3, 0x47, 0x02, folder, UInt8(modifier?.rawValue ?? mode.rawValue), special, special2] + Array(repeating: 0, count: 7)) } static func decode(_ bytes: Data) -> TonuinoCard? { guard bytes.count >= 9, Array(bytes.prefix(4)) == [0x13, 0x37, 0xB3, 0x47], bytes[4] > 0, bytes[4] <= 2, let mode = PlaybackMode(rawValue: Int(bytes[6])) else { return nil } let folder = bytes[5] let modifier = folder == 0 ? TonuinoModifier(rawValue: Int(bytes[6])) : nil return TonuinoCard(folder: folder, mode: mode, special: bytes[7], special2: bytes[8], modifier: modifier) } static func makeModifier(_ modifier: TonuinoModifier, special: UInt8 = 0) -> TonuinoCard { let fallbackMode = PlaybackMode(rawValue: modifier.rawValue) ?? .admin return TonuinoCard(folder: 0, mode: fallbackMode, special: special, modifier: modifier) } } enum MifareClassicDump { static func make1K(for card: TonuinoCard) -> Data { var dump = Data(repeating: 0, count: 1024) let uid = (0..<4).map { _ in UInt8.random(in: 0...255) } let bcc = uid.reduce(UInt8(0), ^) let manufacturerBlock = uid + [bcc, 0x08, 0x04, 0x00] + Array(repeating: UInt8(0), count: 8) dump.replaceSubrange(0..<16, with: manufacturerBlock) // TonUINO stores its 16-byte card payload in MIFARE Classic block 4. dump.replaceSubrange(64..<80, with: card.payload()) // Use the standard transport configuration and default FF keys in each sector trailer. let sectorTrailer: [UInt8] = [ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF ] for block in stride(from: 3, through: 63, by: 4) { let start = block * 16 dump.replaceSubrange(start..<(start + 16), with: sectorTrailer) } return dump } }