# KTag Konfigurator Subapp A Jetpack Compose Android application for configuring KTag laser tag devices via BLE and coordinating game sessions. ## Overview The Konfigurator manages the full lifecycle of a KTag game session: configuring game parameters, discovering and assigning devices to teams, broadcasting game start/stop events, and running game timers. It communicates with KTag devices over BLE using the KTag 27-byte packet protocol. ## Architecture The app follows the **MVVM (Model-View-ViewModel)** pattern with a state machine driving screen transitions. ``` ┌─────────────────────────────────────────────────────────┐ │ KonfiguratorActivity │ │ (Compose Host + Permissions) │ └─────────────────────┬───────────────────────────────────┘ │ ┌─────────────────────▼───────────────────────────────────┐ │ StateMachineViewModel │ │ • State machine (6 screens) │ │ • BLE scanning & advertising │ │ • Multi-device configuration │ │ • Game config management │ └──────┬──────────────┬───────────────────────────────────┘ │ │ ┌──────▼──────┐ ┌─────▼──────────────────────────────────┐ │ BleManager │ │ MultiDeviceConfigurator │ │ (Singleton) │ │ (Sequential BLE config with mutex) │ └─────────────┘ └────────────────────────────────────────┘ ``` ## File Structure ``` src/main/java/club/clubk/ktag/apps/konfigurator/ ├── KonfiguratorActivity.kt # Compose activity with BLE permissions ├── StateMachineViewModel.kt # State machine, BLE orchestration ├── BleManager.kt # BLE scan/advertise singleton ├── MultiDeviceConfigurator.kt # Sequential multi-device configuration ├── KTagPacket.kt # Packet types, parsing, generators ├── Device.kt # Device data class ├── DeviceState.kt # Sealed class: Configurable/Ready/Playing/WrapUp ├── AppState.kt # Sealed class: 6 screen states ├── GameConfig.kt # Game parameters data class ├── ConfigurationProgress.kt # Configuration progress tracking ├── Player.kt # Player data class ├── SoundManager.kt # Audio feedback (SoundPool) ├── KonfiguratorSubApp.kt # Subapp registration ├── KonfiguratorInitializer.kt # androidx.startup initializer └── ui/theme/ ├── Theme.kt # Material3 dynamic theming ├── Color.kt # Color palette └── Type.kt # Typography ``` ## State Machine ``` ┌──────────────────────────────────────────────────────────────┐ │ │ ▼ │ ┌───────────────┐ │ │ GameSettings │ Configure duration, health, rounds │ └───────┬───────┘ │ │ → start BLE scanning │ ▼ │ ┌───────────────┐ │ │ TeamSettings │ Discover devices, assign teams, apply config │ └───────┬───────┘ │ │ [all devices ready] │ │ → stop scanning │ │ → advertise InstigatingGame packet │ ▼ │ ┌───────────────┐ │ │ PregameTimer │ Countdown before game │ └───────┬───────┘ │ │ [timer expires] │ │ → stop advertising │ ▼ │ ┌───────────────┐ │ │ Countdown │ Countdown lights sequence (5+1 s) │ └───────┬───────┘ │ │ [lights out] │ ▼ │ ┌───────────────┐ │ │ GameTimer │ Active game countdown │ └───────┬───────┘ │ │ [timer expires / End Game] │ │ → advertise EVENT_GAME_OVER │ ▼ │ ┌───────────────┐ │ │ WrapUp │ → advertise EVENT_WRAPUP_COMPLETE ──────────────────┘ └───────────────┘ ``` | State | Description | |----------------|----------------------------------------------------------------------------| | `GameSettings` | Configure game duration, health, and rounds | | `TeamSettings` | Scan for BLE devices, assign teams (Purple/Red/Blue), apply config | | `PregameTimer` | Pregame countdown; InstigatingGame packet keeps devices in sync | | `Countdown` | Countdown lights-out sequence (6 s) | | `GameTimer` | Active game countdown with pie-chart timer; early-end available | | `WrapUp` | Game complete; broadcasts EVENT_WRAPUP_COMPLETE, returns to GameSettings | ## Key Components ### StateMachineViewModel Manages game state and BLE operations: - **State Machine**: Transitions between 6 screen states, plays audio on each transition - **Device Discovery**: BLE scanning with KTag manufacturer data filter (0xFFFF) - **Team Assignment**: Cycle devices through teams (0=Purple, 1=Red, 2=Blue) - **Configuration**: Sends Parameters packets to assign team and health to each device - **Game Control**: Broadcasts Instigate Game, Event (ready/game over/wrapup) packets ### BleManager Singleton managing BLE operations: - **Advertising**: Broadcasts KTag packets as manufacturer-specific data - **Scanning**: Filters for KTag magic bytes with scan holdoff (5s between scans) - **Device Configuration**: Advertise config packet, scan for ACK, with 5s timeout - **Thread Safety**: Coroutine-based with managed scan start jobs ### MultiDeviceConfigurator Sequential device configuration: - **Mutex-Protected**: Ensures one BLE config operation at a time - **Progress Callbacks**: Reports per-device success/failure and overall progress - **Error Handling**: Catches exceptions per-device, continues with remaining devices ### KTagPacket Packet system with sealed class hierarchy: - 7 packet types: InstigateGame, Event, Tag, Console, Status, Parameters, Hello - `byteArrayToKTagPacket()`: Deserializes raw bytes to typed packets - `kTagPacketToByteArray()`: Serializes packets to BLE advertisement bytes - Packet generators with auto-incrementing event counters ## Game Configuration | Parameter | Default | Description | |---------------------------|---------|---------------------------------| | `gameDurationMin` | 10 | Game duration in minutes | | `timeUntilCountdownS` | 30 | Pregame countdown in seconds | | `numRounds` | 2 | Number of rounds | | `maxHealth` | 10 | Maximum player health | | `specialWeaponsOnReentry` | 1 | Special weapons on game reentry | ## How to Add a New Configurable Parameter to a Device Each configurable parameter follows the same "set then confirm" pattern: the app holds a *desired* value and a *broadcasted* (device-confirmed) value. The accent bar and checkmark on a device card turn green only when every parameter's desired value matches its broadcasted value. The steps below use `fooBar` / `broadcastedFooBar` as the example parameter name. ### 1. Add the protocol key — `core/.../Packet.java` Parameter keys are defined in the [KTag BLE Protocol Specification](https://ktag.clubk.club/Technology/BLE/). Check there first — the key you need may already exist as a `PARAMETER_KEY_*` constant in `Packet.java`. If it does not, add it: ```java public static final int PARAMETER_KEY_FOO_BAR = ; ``` Each Parameters BLE packet carries two key-value slots. If both slots in the last packet are already occupied, the new parameter needs its own packet (see step 6). ### 2. Add fields to the device model — `Device.kt` ```kotlin var fooBar: Int? = null, var broadcastedFooBar: Int? = null ``` `fooBar` is the value the operator wants to send. `broadcastedFooBar` is the value last acknowledged by the device. ### 3. Register the parameter for match-checking — `Device.kt` Add one line to `allSettingsMatch()`: ```kotlin fun allSettingsMatch(): Boolean = paramMatches(team, broadcastedTeam) && paramMatches(maxHealth, broadcastedMaxHealth) && paramMatches(specialWeaponsOnReentry, broadcastedSpecialWeaponsOnReentry) && paramMatches(fooBar, broadcastedFooBar) // ← add this ``` ### 4. Register the parameter for configure confirmation — `StateMachineViewModel.kt` In `configureDevices()`, two `confirmOnSuccess` lambdas are built in parallel with the packet list. Each lambda is applied to the device immediately when its specific packet is ACK'd. Add `broadcastedFooBar` to whichever lambda corresponds to the packet that carries `PARAMETER_KEY_FOO_BAR`: ```kotlin confirmOnSuccess += { d -> d.copy( broadcastedTeam = d.team ?: d.broadcastedTeam, broadcastedMaxHealth = d.maxHealth ?: d.broadcastedMaxHealth, broadcastedFooBar = d.fooBar ?: d.broadcastedFooBar // ← add this )} ``` This ensures a failed packet never blocks confirmation of packets that succeeded. ### 5. Preserve both fields when refreshing a device — `StateMachineViewModel.kt` `addOrRefreshDevice()` merges incoming scan data with the existing device record. Add two lines following the same pattern as the other parameters: ```kotlin newDevice.fooBar = oldDevice.fooBar ?: newDevice.fooBar newDevice.broadcastedFooBar = newDevice.broadcastedFooBar ?: oldDevice.broadcastedFooBar ``` ### 6. Initialize and send the parameter — `StateMachineViewModel.kt` **Initialize** `fooBar` when a Hello packet is received (typically from game config defaults, same as `maxHealth`): ```kotlin scannedDevice.fooBar = _currentGameConfig.value.fooBar ``` **Send** it during configure. In `configureDevices()`, the `flatMap` builds one `Pair` per BLE packet. Each Parameters packet has two key-value slots. Either fill an empty slot in an existing packet or add a new one: ```kotlin Pair(device.address, Packet.Parameters(targetAddr, Packet.PARAMETER_SUBTYPE_REQUEST_CHANGE, Packet.PARAMETER_KEY_FOO_BAR, device.fooBar ?: gameCfg.fooBar, Packet.PARAMETER_KEY_NONE, 0)) ``` If the device also broadcasts `fooBar` in its Status packets, parse it in the Status handler and set `scannedDevice.broadcastedFooBar` there (same as `broadcastedTeam`). This provides an ongoing truth-check from the device in addition to the immediate confirmation from the ACK packet. ### 7. Add an update function — `StateMachineViewModel.kt` ```kotlin fun updateDeviceFooBar(deviceAddress: String, value: Int) = updateDeviceField(deviceAddress) { it.copy(fooBar = value) } ``` `updateDeviceField` handles the `_devices` update and sets `_pendingApply = true` automatically. ### 8. Add a UI field to the device detail dialog — `KonfiguratorActivity.kt` In `DeviceDetailDialog`, add a state variable and an `IntegerTextField`: ```kotlin var fooBarText by remember { mutableStateOf(device.fooBar?.toString() ?: "") } var fooBarError by remember { mutableStateOf(false) } IntegerTextField( value = fooBarText, onValueChange = { fooBarText = it; fooBarError = false }, label = "Foo Bar", isError = fooBarError ) ``` Update the `onSave` lambda signature to include the new value, validate it in the Save button, and call `stateMachine.updateDeviceFooBar(device.address, fooBar)` alongside the other update calls. ## Dependencies - Jetpack Compose (Material3) - ViewModel + Compose integration - BLE (BluetoothLeScanner, BluetoothLeAdvertiser) - SoundPool (audio feedback)