Android-KTag-Apps/subapp-konfigurator/README.md
2026-03-01 17:03:10 -06:00

14 KiB

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. Check there first — the key you need may already exist as a PARAMETER_KEY_* constant in Packet.java. If it does not, add it:

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

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():

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:

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:

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

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:

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

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:

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)