...where the 'K' stands for quality. https://ktag.clubk.club/Technology/SystemK/
  • C 91.6%
  • C++ 7.4%
  • Python 0.6%
  • CMake 0.4%
Find a file
Joe Kearney 190b2b9a51
All checks were successful
Tests / build-and-test (push) Successful in 34s
Fix for the Konfigurator breaking BLE advertising (and related game features) (#17)
As was reported [here](https://forum.ktag.clubk.club/t/the-ktag-app-breaks-at-sundown-may-30th-2026/74), the use of the [Konfigurator](https://git.ktag.clubk.club/Software/Android-KTag-Apps/src/branch/main/subapp-konfigurator) (and, to my knowledge, only the use of the Konfigurator) would cause SystemK-based devices to perform erratically or not at all.  In particular, BLE-based features like the KPoint game mode did not work at all.  This fix address some of the root causes of that misbehavior.

The key fix was the substitution of cooperative timers in the place of the FreeRTOS timers used in the SystemK State Machine: we were overflowing timer queues and causing all kinds of problems.  That can't happen anymore.  This is described in more detail in the README.

## Related PRs:

* Software/KTag-HIL-Tester#2
* Software/2024A-SW#20
* Software/2020TPC-SW#10

Co-authored-by: Joe Kearney <joe@clubk.club>
Reviewed-on: #17
2026-10-16 01:27:02 +00:00
.forgejo/workflows SystemK v2.01: The K-Points game (#16) 2026-08-29 14:19:55 +00:00
Audio SystemK v2.01: The K-Points game (#16) 2026-08-29 14:19:55 +00:00
BLE Fix for the Konfigurator breaking BLE advertising (and related game features) (#17) 2026-10-16 01:27:02 +00:00
CC SystemK v2.01: The K-Points game (#16) 2026-08-29 14:19:55 +00:00
Events Fix for the Konfigurator breaking BLE advertising (and related game features) (#17) 2026-10-16 01:27:02 +00:00
Game Fix for the Konfigurator breaking BLE advertising (and related game features) (#17) 2026-10-16 01:27:02 +00:00
IR SystemK v2.00: Interface Improvements (#15) 2026-03-26 02:08:40 +00:00
Logging SystemK v2.01: The K-Points game (#16) 2026-08-29 14:19:55 +00:00
Menu SystemK v2.00: Interface Improvements (#15) 2026-03-26 02:08:40 +00:00
NeoPixels SystemK v2.01: The K-Points game (#16) 2026-08-29 14:19:55 +00:00
Protocols Fix for the Konfigurator breaking BLE advertising (and related game features) (#17) 2026-10-16 01:27:02 +00:00
Settings Fix for the Konfigurator breaking BLE advertising (and related game features) (#17) 2026-10-16 01:27:02 +00:00
States Fix for the Konfigurator breaking BLE advertising (and related game features) (#17) 2026-10-16 01:27:02 +00:00
Tests Fix for the Konfigurator breaking BLE advertising (and related game features) (#17) 2026-10-16 01:27:02 +00:00
Timers Fix for the Konfigurator breaking BLE advertising (and related game features) (#17) 2026-10-16 01:27:02 +00:00
.gitignore SystemK v2.01: The K-Points game (#16) 2026-08-29 14:19:55 +00:00
CMakeLists.txt Fix for the Konfigurator breaking BLE advertising (and related game features) (#17) 2026-10-16 01:27:02 +00:00
Colors.h Annum Novum Faustum Felicem MMXXVI (#11) 2026-01-10 23:18:53 +00:00
Console_HW_Interface.h SystemK v2.00: Interface Improvements (#15) 2026-03-26 02:08:40 +00:00
Developer Certificate of Origin.txt Initial public release of SystemK. 2025-01-25 13:45:14 -06:00
Kconfig Added support for configurable logging on the ESP32 platform. (#10) 2026-01-10 22:25:50 +00:00
KIsForQuality.png Reworked BLE according to v0.12 of the KTag Beacon Specification (#5) 2025-09-01 17:44:10 +00:00
LICENSE Annum Novum Faustum Felicem MMXXVI (#11) 2026-01-10 23:18:53 +00:00
README.md Fix for the Konfigurator breaking BLE advertising (and related game features) (#17) 2026-10-16 01:27:02 +00:00
Results.h SystemK v2.01: The K-Points game (#16) 2026-08-29 14:19:55 +00:00
SystemK.c Fix for the Konfigurator breaking BLE advertising (and related game features) (#17) 2026-10-16 01:27:02 +00:00
SystemK.h Fix for the Konfigurator breaking BLE advertising (and related game features) (#17) 2026-10-16 01:27:02 +00:00

SystemK

...where the 'K' stands for Quality.

K is for Quality

SystemK is the shared C library at the core of KTag devices. It implements game logic, state management, BLE/IR communication, audio, and LED animation, and it runs on FreeRTOS across multiple hardware platforms (including ESP32 and PSoC 6). Each platform provides a thin set of hardware-specific driver implementations; everything above that layer is shared.


Architecture Overview

SystemK is organized around a central event queue and a UML-style state machine. Hardware drivers post events to the queue; the state machine consumes them and orchestrates the subsystems in response.

┌────────────────────────────────────────────────────────────────┐
│                        Host Application                        │
│           (provides HW drivers + FreeRTOS task setup)          │
└───────────────────────────────┬────────────────────────────────┘
                                │ Initialize_SystemK()
┌───────────────────────────────▼────────────────────────────────┐
│                                                                │
│                            SystemK                             │
│                                                                │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                    State Machine Task                   │   │
│  │  (Entry / Do / Exit per state; reads KEvent queue)      │   │
│  └──────┬──────────┬─────────┬──────────┬──────────────────┘   │
│         │          │         │          │                      │
│  ┌──────▼──┐ ┌─────▼──┐ ┌────▼───┐ ┌────▼────┐                 │
│  │   BLE   │ │  Game  │ │ Audio  │ │NeoPixels│  ...            │
│  └──────┬──┘ └────────┘ └────────┘ └────┬────┘                 │
│         │     KEvent Queue              │ Queue                │
│  ┌──────▼───────────────────────────────▼─────────────────┐    │
│  │                   Events (KEvent_T)                    │    │
│  └──────────────────────────────────────────────────────┬─┘    │
└─────────────────────────────────────────────────────────┼──────┘
                                                          │
┌─────────────────────────────────────────────────────────▼──────┐
│              Hardware Abstraction Layer (*_HW_Interface.h)     │
│   BLE_HW  │  IR_HW  │  Audio_HW  │  NeoPixels_HW  │  Console   │
└────────────────────────────────────────────────────────────────┘

Initialization

A host application calls Initialize_SystemK(), which:

  1. Creates the static KEvent queue (32 entries).
  2. Loads Settings from persistent storage.
  3. Initializes the BLE stack (platform-specific).
  4. Spawns the State Machine task (static allocation, tskIDLE_PRIORITY + 1).

The state machine begins in STATE_INITIALIZING and automatically transitions to STATE_CONFIGURING once startup completes. The host application is responsible for creating the NeoPixels task and for wiring up IR, BLE, and audio callbacks before calling Initialize_SystemK().


State Machine

The state machine implements UML 2.5.1 hierarchical states. Each state provides three callbacks:

Callback When called
Entry Once, on entering the state
Do Every loop iteration while the state is active
Exit Once, before leaving the state

States

STATE_INITIALIZING
STATE_REPROGRAMMING
STATE_CONFIGURING
STATE_READY

STATE_STARTING_GAME__INSTIGATING    ─┐
STATE_STARTING_GAME__RESPONDING      ├─ Starting Game
STATE_STARTING_GAME__COUNTING_DOWN  ─┘

STATE_PLAYING__INTERACTING          ─┐
STATE_PLAYING__TAGGED_OUT            ├─ Playing
STATE_PLAYING__CONSENTING           ─┘

STATE_WRAPPING_UP

State transitions are driven by events from the KEvent queue. A state calls Transition_For_Event(context, next_state, event) to request a transition; the state machine loop detects the change and runs Exit → Entry → Do in the correct order.

Each pass of the loop also calls CooperativeTimer_ProcessRegistered() before dispatching Do, so every SystemK timer callback runs on the state machine task. See Timers.

BLE Background Advertising

The state machine owns a single periodic BLEBackgroundAdvertisingTimer that runs continuously (except when explicitly suspended). Its callback calls RefreshBLEBackgroundAdvertising(), which re-reads the current state and publishes BLE_UpdateHelloPacket() in STATE_CONFIGURING or BLE_UpdateStatusPacket() otherwise, at a state-appropriate interval.

Because it is a cooperative timer, that callback runs on the state machine task and may publish directly. Only the state machine task ever publishes an advertising payload, which is what keeps a payload staged for one state from reaching the air after the machine has moved to another.

States that manage their own BLE advertising (e.g., STATE_STARTING_GAME__INSTIGATING) call SuspendBLEBackgroundAdvertising() on entry and ResumeBLEBackgroundAdvertising() on exit.


Event System

The KEvent_T queue is the communication channel between hardware callbacks and the state machine. No subsystem calls directly into the state machine.

Timers are the exception, and deliberately so: a cooperative timer callback is already running on the state machine task, so it calls what it needs directly instead of posting an event and waiting for the queue to be drained.

typedef struct {
    KEvent_ID_T  ID;
    void        *Data;
} KEvent_T;

Key event categories:

Category Examples
Hardware input KEVENT_TRIGGER_SWITCH_PRESSED/RELEASED, KEVENT_ACCESSORY_SWITCH_PRESSED
IR reception KEVENT_TAG_RECEIVED (Data → DecodedPacket_T *)
BLE KEVENT_BLE_PACKET_RECEIVED (Data → BLE_Packet_T *)
Game KEVENT_TAGGED_OUT, KEVENT_GAME_OVER, KEVENT_OUT_OF_SHOTS
Transfer KEVENT_TRANSFER_CONSENT_NEEDED, KEVENT_TRANSFER_COMPLETED, KEVENT_TRANSFER_INVENTORY_RECEIVED, KEVENT_TRANSFER_TIMEOUT
Audio KEVENT_AUDIO_COMPLETED
Menu KEVENT_MENU_ENTER, KEVENT_MENU_UP, KEVENT_MENU_SELECT

Post from task context with Post_KEvent(); from an ISR with Post_KEvent_From_ISR().


Timers

SystemK does not use FreeRTOS software timers. Its timers are cooperative: plain structs, registered once, and processed by whichever task owns them — which in SystemK is always the state machine task.

static CooperativeTimer My_Timer;

CooperativeTimer_Init(&My_Timer, My_Callback, NULL);
(void)CooperativeTimer_Register(&My_Timer);        // idempotent
CooperativeTimer_Start(&My_Timer, pdMS_TO_TICKS(3000));          // one-shot
CooperativeTimer_StartPeriodic(&My_Timer, pdMS_TO_TICKS(500));   // periodic
CooperativeTimer_Stop(&My_Timer);

Two properties are the reason for the design:

  • A callback may do anything the owning task may do. It can block, take a mutex, or publish an advertising payload, because it is the state machine. On the FreeRTOS timer daemon, one blocking callback stalls every timer in the system.
  • Starting and stopping cannot fail. There is no command queue between the caller and the timer, so there is no dropped-command path to detect or report.

In exchange, timers only advance while the owning task loops, so resolution is that task's period. Don't build a timer here that needs to be finer than the loop servicing it.

Two behaviors worth knowing:

  • A periodic timer whose owner was blocked longer than one period skips the missed deadlines rather than firing repeatedly to catch up.
  • The schedule is updated before the callback runs, so a callback that re-arms or stops its own timer has the last word.

Host drivers outside SystemK may still use FreeRTOS timers (both ESP32 BLE drivers do, for BLEUnquietTimer); those callbacks run on the timer daemon and must not block.


Subsystems

Subsystem Purpose Key interface
Events Static event queue; inter-subsystem communication Post_KEvent(), Receive_KEvent()
BLE Advertising, scanning, peer-to-peer game sync BLE_Update*Packet(), BLE_ScanAndAdvertise()
Transfer Two-phase-commit item/K-Point transfer between devices BLE_Transfer_SendRequest(), BLE_Transfer_HandlePacket()
IR / Protocols Tag encoding and decoding across 10 protocols PROTOCOLS_EncodePacket(), PROTOCOLS_MaybeDecodePacket()
Audio Sound effects, speech synthesis Perform_Audio_Action(AudioActionID_T)
NeoPixels LED animation (1- and 4-channel devices) Post NeoPixelsAction_T to xQueueNeoPixels
Game Player health, shots, timing, friendly-fire rules GAME_Build_My_Tag_Packet(), GAME_Reduce_Health()
Settings Persistent device configuration (team, player, volume, …) SETTINGS_get_*(), SETTINGS_set_*(), SETTINGS_Save()
Menu On-device configuration UI (tree of MenuItem_T nodes) MenuItem_T callback tree; driven by KEVENT_MENU_* events
Timers Cooperative timers processed by the state machine task CooperativeTimer_Start(), CooperativeTimer_Register()
Logging Debug output abstraction KLOG_ERROR(), KLOG_WARN(), KLOG_INFO()

IR Protocols

SystemK includes codecs for ten laser tag protocols:

Dubuque · Dynasty · Laser X · Miles Tag II · NEC · Nerf Laser Ops Pro · Nerf Laser Strike · Nerf Phoenix LTX · Squad Hero · Test

Each protocol implements encoding (tag → IR pulse train), decoding (pulse train → tag), and a team/player color mapping.


Hardware Abstraction

Each hardware-dependent subsystem declares a *_HW_Interface.h header with no implementation. The host platform provides the implementation.

BLE_HW_Interface.h

SystemKResult_T BLE_Init(void);
SystemKResult_T BLE_GetMyAddress(uint8_t BD_ADDR[BD_ADDR_SIZE]);
SystemKResult_T BLE_ScanAndAdvertise(void);
SystemKResult_T BLE_SetAdvertisingData(BLE_AdvertisingData_T *data);
SystemKResult_T BLE_StopAdvertising(void);
SystemKResult_T BLE_StopScanning(void);
SystemKResult_T BLE_Quiet(uint32_t duration_ms);  // 0 = indefinite
SystemKResult_T BLE_Unquiet(void);

IR_HW_Interface.h

PreparedTag_T  *Prepare_Tag(const TagPacket_T *packet);
SystemKResult_T Send_Tag(PreparedTag_T *tag);

Audio_HW_Interface.h

SystemKResult_T Perform_Audio_Action(AudioAction_T *action);

Audio actions are enum-driven (40 types). Set action->Play_To_Completion = true to block until the sound finishes; completion also posts KEVENT_AUDIO_COMPLETED.

NeoPixel_HW_Interface.h

SystemKResult_T HW_NeoPixels_Init(void);
SystemKResult_T HW_NeoPixels_Set_Color(NeoPixelsChannel_T channel,
                                       uint8_t position, color_t color);
SystemKResult_T HW_NeoPixels_Publish(void);

Supports four channels (BARREL, RECEIVER, DISPLAY, EFFECTS); single-channel devices use BARREL only.

Console_HW_Interface.h

SystemKResult_T HW_Execute_Console_Command(const char *command);

Data Flow: Shot Fired

At game start (STATE_STARTING_GAME__COUNTING_DOWN), the tag is built and encoded once:

GAME_Build_My_Tag_Packet() → TagPacket_T
Prepare_Tag(TagPacket_T)   → PreparedTag_T  (stored as GAME_Prepared_Tag)

This pre-encoded PreparedTag_T is reused for every shot during the game.

On each trigger press (KEVENT_TRIGGER_SWITCH_PRESSED):

  1. Check shot holdoff — if fired too recently, post KEVENT_MISFIRE.
  2. If clear: Send_Tag(GAME_Prepared_Tag) via the IR hardware driver.
    • Failure → KEVENT_MISFIRE.
    • Success → decrement Shots_Remaining.
    • No shots remaining → KEVENT_OUT_OF_SHOTS.
  3. Start the long-press timer to detect full-auto fire or trigger-hold reload.

After IR transmission the hardware driver posts KEVENT_TAG_SENT:

  1. Record shot time and calculate next holdoff (configured minimum, plus a random component for non-Dubuque protocols to resolve simultaneous-fire duels).
  2. Trigger AUDIO_PLAY_SHOT_FIRED and NEOPIXELS_PLAY_SHOT_FIRED.
  3. Increment Shots_Fired stat.

On trigger release (KEVENT_TRIGGER_SWITCH_RELEASED): stop the long-press timer. No shot is fired on release.

Data Flow: Tag Received

  1. IR decoder ISR decodes a pulse train → KEVENT_TAG_RECEIVED posted (Data = DecodedPacket_T *).
  2. Playing__Interacting_Do calls HandleTagReceived().
  3. GAME_Team_Can_Tag_Me() checks whether the sender's team can tag this player. Friendly fire → AUDIO_PLAY_FRIENDLY_FIRE; ignore.
  4. For valid tags, check the purple-team invincibility window: if this device just fired a purple tag, the incoming tag may be its own echo and is discarded (protocol-dependent logic).
  5. If the tag is valid and not an echo: GAME_Reduce_Health(damage) → AUDIO_PLAY_TAG_RECEIVED + NEOPIXELS_TAG_RECEIVED + GAME_Increment_Tags_Received().
  6. If health reaches zero, KEVENT_TAGGED_OUT is posted and the state transitions to STATE_PLAYING__TAGGED_OUT.

Data Flow: Item Transfer

The Transfer subsystem (BLE_Transfer) implements a two-phase-commit (TPC) protocol over BLE_PACKET_TYPE_TRANSFER packets to move inventory items (e.g., K-Points) between devices. Only one transaction is active at a time.

  1. The initiating device calls BLE_Transfer_SendRequest(), sending a REQUEST_VOLUNTARY, REQUEST_INVOLUNTARY_FRIENDLY, or REQUEST_INVOLUNTARY_UNFRIENDLY packet and entering TRANSFER_SM_REQUESTER_WAITING.
  2. The receiving device's BLE_Transfer_HandlePacket() processes the request:
    • Voluntary requests post KEVENT_TRANSFER_CONSENT_NEEDED, and Playing__Interacting_Do transitions to STATE_PLAYING__CONSENTING to await the player's decision via BLE_Transfer_RespondToConsentRequest().
    • Involuntary requests proceed automatically: the device replies with OFFER_ATOMIC and enters TRANSFER_SM_SENDER_OFFERING.
  3. The requester acknowledges with ACKNOWLEDGE_ATOMIC_OFFER (TRANSFER_SM_RECEIVER_ACKED); the sender deducts inventory, replies with COMMIT_ATOMIC_OFFER (TRANSFER_SM_SENDER_COMMITTING), and the receiver credits inventory and replies with ACKNOWLEDGE_ATOMIC_COMMIT (TRANSFER_SM_RECEIVER_COMPLETED).
  4. Each step is retried on KEVENT_TRANSFER_TIMEOUT via BLE_Transfer_HandleTimeout(); exhausting retries aborts the transaction (SYSTEMK_RESULT_ABORTED) and triggers AUDIO_PLAY_TRANSFER_FAILED. The TPC protocol intentionally prefers accidental item loss over double-counting, so BLE_Transfer_Cancel() does not restore deducted inventory.
  5. On success, KEVENT_TRANSFER_COMPLETED is posted with a TransferRole_T (TRANSFER_ROLE_SENDER or TRANSFER_ROLE_RECEIVER), triggering AUDIO_PLAY_TRANSFER_SENT/AUDIO_PLAY_TRANSFER_RECEIVED and a NEOPIXELS_FIZZLE effect.

Tests

SystemK has a host-based test suite under Tests/. Tests run on Linux (no embedded hardware required) using Unity (MIT license, vendored at Tests/Unity/).

Running the tests

cd Tests
cmake -B build -S .
cmake --build build
./build/SystemK_Tests

Or via CTest: cd Tests/build && ctest --output-on-failure

Architecture

The suite currently runs 349 tests.

The test build compiles all SystemK state machine and game-logic sources for the host with -DHOST_TEST_PLATFORM=1. A small number of additions to production source, all guarded by #ifdef HOST_TEST_PLATFORM, support driving the state machine from test code:

  • State_Machine_Init_For_Test() — initializes and registers the BLE background advertising timer
  • State_Machine_Step() — runs one iteration of the main state machine loop, including timer processing
  • State_Machine_Reset_For_Test() — resets context to its initial state
  • CooperativeTimer_ResetRegistryForTest() — empties the timer registry
  • Per-module *_GetTimerForTest() accessors, so a test can inspect or drive a specific timer (State_Machine_, Configuring_, Playing_, Interacting_, BLE_Transfer_)

Because cooperative timers take the current tick as a parameter, time is an input in tests: set it with TEST_SetTickCount() / TEST_AdvanceTicksBy() and drive a timer with TEST_FireCooperativeTimer(). No real delays, and no dependence on a timer daemon.

FreeRTOS is replaced by lightweight stubs (Tests/Support/FreeRTOS/) that implement queues as circular buffers and FreeRTOS timers as plain structs. The hardware interfaces (Audio, BLE_HW, IR, NeoPixels_HW, Console, Settings) and Menu are replaced by call-tracking mocks in Tests/Support/Mocks/.

Tests/Support/ is shared. The 2024A-SW and KTag-HIL-Tester host suites include these stubs and helpers too, and their BLE drivers still use FreeRTOS timers. Nothing here is dead until it has zero callers in all three projects — checking only SystemK's own Tests/Tests/ will mislead you.


Build System

SystemK uses CMake. For ESP-IDF targets it registers as an idf_component. Platform-specific code is selected at compile time via CONFIG_* macros (set in Kconfig or passed by the host application's build):

Macro Purpose
CONFIG_KTAG_N_NEOPIXEL_CHANNELS 1 or 4 NeoPixel channels
CONFIG_KTAG_MAX_NEOPIXELS_PER_CHANNEL LED count per channel
CONFIG_KTAG_ANIMATION_STEP_TIME_IN_ms Animation frame interval (default is 10 ms)
CONFIG_SYSTEMK_LOG_LEVEL Logging verbosity

Error Handling

All SystemK functions return SystemKResult_T. The value SYSTEMK_RESULT_SUCCESS (0) indicates success; all other values indicate a specific failure. See Results.h for the full list.

License: AGPL-3.0-or-later

This software is part of the KTag project, a DIY laser tag game with customizable features and wide interoperability.

🛡 https://ktag.clubk.club 🃞

Copyright © 2018-2026 Joseph P. Kearney and the KTag developers.

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.

There should be a copy of the GNU Affero General Public License in the LICENSE file in the root of this repository. If not, see http://www.gnu.org/licenses/.

Open-Source Software

This software in turn makes use of the following open-source software libraries and components:

Name Version License (SPDX) URL
CC 1.4.3 MIT https://github.com/JacksonAllan/CC