Initial public release.

This commit is contained in:
Joe Kearney 2026-03-01 17:03:10 -06:00
parent ed31acd60f
commit 58d87b11b7
249 changed files with 15831 additions and 4 deletions

View file

@ -0,0 +1,264 @@
# 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 = <key_id_from_spec>;
```
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<String, Packet.Parameters>` 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)