117 lines
4.9 KiB
Swift
117 lines
4.9 KiB
Swift
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
|
|
}
|
|
}
|