# 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)
