A collection of KTag applications and tools in a single Android app.
  • Kotlin 91.5%
  • Java 8.5%
Find a file
Joe Kearney 3097adbfea Cleanup.
2026-07-23 21:25:09 -05:00
app Cleanup. 2026-07-23 21:25:09 -05:00
core Fixes to the KotH subapp. 2026-07-19 09:06:56 -05:00
gradle Stop BLE when it is no longer needed. 2026-07-21 19:06:14 -05:00
mqtt-broker Lower minSdk to 22 for Fire OS 5 tablets: desugar Java 8 APIs for Moquette, gate Sentry to API 24+ (MediaPipe), pin Paho fork to 4.3 2026-07-18 13:36:25 -05:00
shared-services Cleanup. 2026-07-23 21:25:09 -05:00
subapp-bletool Stop BLE when it is no longer needed. 2026-07-21 19:06:14 -05:00
subapp-collector Changes to support 3-team games. 2026-07-23 21:17:14 -05:00
subapp-fountain Lower minSdk to 22 for Fire OS 5 tablets: desugar Java 8 APIs for Moquette, gate Sentry to API 24+ (MediaPipe), pin Paho fork to 4.3 2026-07-18 13:36:25 -05:00
subapp-konfigurator Stop BLE when it is no longer needed. 2026-07-21 19:06:14 -05:00
subapp-koth Changes to support 3-team games. 2026-07-23 21:17:14 -05:00
subapp-medic Lower minSdk to 22 for Fire OS 5 tablets: desugar Java 8 APIs for Moquette, gate Sentry to API 24+ (MediaPipe), pin Paho fork to 4.3 2026-07-18 13:36:25 -05:00
subapp-mine Lower minSdk to 22 for Fire OS 5 tablets: desugar Java 8 APIs for Moquette, gate Sentry to API 24+ (MediaPipe), pin Paho fork to 4.3 2026-07-18 13:36:25 -05:00
subapp-sample Lower minSdk to 22 for Fire OS 5 tablets: desugar Java 8 APIs for Moquette, gate Sentry to API 24+ (MediaPipe), pin Paho fork to 4.3 2026-07-18 13:36:25 -05:00
subapp-sentry Lower minSdk to 22 for Fire OS 5 tablets: desugar Java 8 APIs for Moquette, gate Sentry to API 24+ (MediaPipe), pin Paho fork to 4.3 2026-07-18 13:36:25 -05:00
subapp-terminal Lower minSdk to 22 for Fire OS 5 tablets: desugar Java 8 APIs for Moquette, gate Sentry to API 24+ (MediaPipe), pin Paho fork to 4.3 2026-07-18 13:36:25 -05:00
.gitignore Initial public release. 2026-03-01 17:03:10 -06:00
build.gradle.kts Initial public release. 2026-03-01 17:03:10 -06:00
gradle.properties Initial public release. 2026-03-01 17:03:10 -06:00
gradlew Changes to enable the scoreboard. 2026-07-08 20:13:51 -05:00
gradlew.bat Changes to enable the scoreboard. 2026-07-08 20:13:51 -05:00
LICENSE Initial public release. 2026-03-01 17:03:10 -06:00
README.md Lower minSdk to 22 for Fire OS 5 tablets: desugar Java 8 APIs for Moquette, gate Sentry to API 24+ (MediaPipe), pin Paho fork to 4.3 2026-07-18 13:36:25 -05:00
settings.gradle.kts First working K-Point transfer. 2026-05-27 21:58:57 -05:00

KTag Apps

A modular Android app that serves as a launcher for a collection of related subapps. The main screen displays a grid of icons, each launching an independent subapp. Each subapp is its own Gradle module, making it easy to develop and add new ones without touching existing code.

Project Structure

  • app — Main launcher module. Displays the subapp grid and handles navigation.
  • core — Shared interfaces (SubApp, SubAppRegistry) that all modules depend on.
  • shared-services — Common settings infrastructure (SettingsSubApp, BaseSettingsActivity, SummarizedEditTextPreference) for subapps with user preferences. Also provides SharedMqttClient, a singleton MQTT client shared by all subapps, DeviceInfoMqttSync for cross-device info synchronization, LocationPublisher for GPS position reporting, and GameStateController, a single shared, MQTT-synced game-phase state machine used by subapp-konfigurator and subapp-koth.
  • subapp-sample — Example subapp demonstrating the full pattern.
  • subapp-bletool — Tool for debugging KTag BLE issues. Identical functionality to the old Android BLE Tool.
  • subapp-koth — App for hosting King of the Hill games (with MQTT).
  • subapp-medic — App for simulating a medic (with proximity-based healing and MQTT).
  • subapp-terminal — USB serial terminal for communicating with KTag devices using usb-serial-for-android.
  • subapp-mine — App for automatically tagging nearby devices via BLE.
  • subapp-fountain — App that continuously broadcasts K-Point offers to nearby KTag devices via BLE (OFFER_UNLIMITED).
  • subapp-collector — App that solicits and collects K-Points from nearby KTag devices via the atomic BLE transfer protocol.
  • subapp-konfigurator — App for configuring KTag laser tag devices via BLE and coordinating game sessions.
  • subapp-sentry — Autonomous sentry gun app that uses the device camera and on-device ML (MediaPipe / EfficientDet-Lite0) to detect people and fire the laser tag gun via USB serial when a person intersects the crosshair.
  • mqtt-broker — Embedded MQTT broker module using Moquette. Runs as a foreground service with optional SSL, authentication, and mDNS discovery. Also hosts the unified "MQTT Settings" page (client connection settings, broker settings, and mDNS broker discovery). Configured via the overflow menu in the main launcher.

Building

Open the project in Android Studio and sync Gradle, or run:

./gradlew assembleDebug

Minimum Android Version

The app runs on Android 5.1+ (minSdk 22, targetSdk 34). The floor was deliberately lowered from 24 to 22 so that older tablets — specifically Fire OS 5 Kindle Fires — can be used as battlefield devices (scoreboard, MQTT broker host, game coordination). Supporting API 22 required a few arrangements worth knowing before touching dependencies:

  • Core library desugaring is enabled in the app module. Moquette and Netty use Java 8 library APIs (java.util.stream, Optional, CompletableFuture) that only exist on-device from API 24; desugaring backports them.
  • The Sentry subapp requires API 24 because MediaPipe's native libraries genuinely need it. Rather than raising the whole app's floor, Sentry is gated at runtime: SentryInitializer skips registration below API 24 (so its icon never appears in the launcher), SentryActivity finishes immediately if launched another way (e.g. via its USB_DEVICE_ATTACHED intent filter), and MediaPipe's declared minSdk is overridden with tools:overrideLibrary in the app manifest. Use this same pattern for any future subapp that needs a newer API than the app's floor.
  • The Paho Android service fork is pinned to 4.3 (see gradle/libs.versions.toml). AndroidX raised its minimum SDK to 23 in its 2025 releases and has since been removing pre-23 compatibility code, so libraries from that wave (Room 2.7+, core-ktx 1.16+, WorkManager 2.10.2+) are a genuine crash risk on API 22 — not just a manifest formality. Fork 4.4+ pulls them in transitively; 4.3 is the newest release that doesn't.
  • Dependency upgrades are constrained by the same androidx floor: the current Compose BOM / androidx versions are roughly the newest that still support API 22. Upgrading past them means raising minSdk back to 23+ and dropping old-tablet support.

Hardware caveats on old tablets, independent of the API level: BLE advertising (how the app transmits to taggers) is chipset-dependent and unsupported on some pre-2016 devices — BluetoothAdapter.bluetoothLeAdvertiser returns null, and the app degrades to listen-only. BLE scanning still works, as does everything over MQTT/Wi-Fi. (A Fire OS 5 Kindle Fire has been tested and confirmed to support BLE advertising — both directions work.)

KTag Colors

The KTag color palette should be used consistently across all subapps:

Color Hex Usage
Green #4BA838 Success states, positive indicators
Blue #4D6CFA Blue team, links, interactive elements
Red #F34213 Red team, warnings, destructive actions
Yellow #FFC857 Highlights, accents
Purple #9B59B6 All teams, combined team indicators
Dark Gray #323031 Backgrounds, text, icons

These colors are defined in each module's Color.kt file (e.g., KTagGreen, KTagBlue, KTagRed, KTagYellow, KTagPurple, KTagDarkGray).

Adding a New SubApp

1. Create the module directory

subapp-yourname/
└── src/main/
    ├── AndroidManifest.xml
    ├── java/club/clubk/ktag/apps/yourname/
    │   ├── YourSubApp.kt
    │   ├── YourActivity.kt
    │   └── YourInitializer.kt
    └── res/drawable/
        └── ic_yourname.xml

2. Add build.gradle.kts

plugins {
    alias(libs.plugins.android.library)
    alias(libs.plugins.kotlin.compose)
}

android {
    namespace = "club.clubk.ktag.apps.yourname"
    compileSdk = 34

    defaultConfig {
        minSdk = 22
    }

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }

    buildFeatures {
        compose = true
    }
}

dependencies {
    implementation(project(":core"))
    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.activity.compose)
    implementation(platform(libs.androidx.compose.bom))
    implementation(libs.androidx.compose.ui)
    implementation(libs.androidx.compose.material3)
    implementation(libs.androidx.startup)
}

3. Implement the SubApp interface

// YourSubApp.kt
package club.clubk.ktag.apps.yourname

import android.content.Context
import android.content.Intent
import club.clubk.ktag.apps.core.SubApp

class YourSubApp : SubApp {
    override val id = "yourname"
    override val name = "Your App"
    override val icon = R.drawable.ic_yourname

    override fun createIntent(context: Context): Intent {
        return Intent(context, YourActivity::class.java)
    }
}

4. Create the entry Activity

// YourActivity.kt
package club.clubk.ktag.apps.yourname

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent

class YourActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            // Your UI here
        }
    }
}

5. Create the Initializer

This registers your subapp with the launcher automatically on app startup:

// YourInitializer.kt
package club.clubk.ktag.apps.yourname

import android.content.Context
import androidx.startup.Initializer
import club.clubk.ktag.apps.core.SubAppRegistry

class YourInitializer : Initializer<Unit> {
    override fun create(context: Context) {
        SubAppRegistry.register(YourSubApp())
    }

    override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
}

6. Declare the Activity and Initializer in AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application>
        <activity
            android:name=".YourActivity"
            android:exported="false"
            android:label="Your App" />

        <provider
            android:name="androidx.startup.InitializationProvider"
            android:authorities="${applicationId}.androidx-startup"
            android:exported="false">
            <meta-data
                android:name="club.clubk.ktag.apps.yourname.YourInitializer"
                android:value="androidx.startup" />
        </provider>
    </application>
</manifest>

7. Add a drawable icon

Place a vector drawable at src/main/res/drawable/ic_yourname.xml. This is what appears in the launcher grid.

8. Register the module with the project

In settings.gradle.kts, add:

include(":subapp-yourname")

In app/build.gradle.kts, add the dependency:

implementation(project(":subapp-yourname"))

Sync Gradle and run the app — your new subapp will appear in the launcher grid.

Adding Settings to a SubApp

If your subapp needs user-configurable settings, use the shared-services module.

1. Add the dependency

In your subapp's build.gradle.kts:

dependencies {
    implementation(project(":core"))
    implementation(project(":shared-services"))
    // ... other dependencies
}

2. Implement SettingsSubApp instead of SubApp

// YourSubApp.kt
package club.clubk.ktag.apps.yourname

import android.content.Context
import android.content.Intent
import club.clubk.ktag.apps.sharedservices.SettingsSubApp

class YourSubApp : SettingsSubApp {
    override val id = "yourname"
    override val name = "Your App"
    override val icon = R.drawable.ic_yourname
    override val settingsPreferencesResId = R.xml.settings_pref
    override val usesMqtt = true  // set to false if not using MQTT

    override fun createIntent(context: Context): Intent {
        return Intent(context, YourActivity::class.java)
    }
}

3. Create a settings activity

// YourSettingsActivity.kt
package club.clubk.ktag.apps.yourname

import android.content.Context
import android.content.Intent
import club.clubk.ktag.apps.sharedservices.BaseSettingsActivity

class YourSettingsActivity : BaseSettingsActivity() {
    companion object {
        @JvmStatic
        fun createIntent(context: Context): Intent {
            return BaseSettingsActivity.createIntent(context, R.xml.settings_pref, YourSettingsActivity::class.java)
        }
    }
}

4. Create the preferences XML

Create src/main/res/xml/settings_pref.xml:

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <PreferenceCategory android:title="Your Settings">
        <club.clubk.ktag.apps.sharedservices.SummarizedEditTextPreference
            app:customHint="Hint text when empty"
            android:inputType="text"
            android:key="your_setting_key"
            android:summary="%s"
            android:title="Setting Title" />
    </PreferenceCategory>

</PreferenceScreen>

5. Declare the settings activity in AndroidManifest.xml

<activity
    android:name=".YourSettingsActivity"
    android:exported="false"
    android:label="Settings"
    android:parentActivityName=".YourActivity"
    android:theme="@style/Theme.AppCompat.Light.DarkActionBar">
    <meta-data
        android:name="android.support.PARENT_ACTIVITY"
        android:value=".YourActivity"/>
</activity>

6. Launch settings from your activity

// In Java:
Intent intent = YourSettingsActivity.createIntent(this);
startActivity(intent);
// In Kotlin:
startActivity(YourSettingsActivity.createIntent(this))

Using MQTT

MQTT connection settings (server URI, username, password, battlefield) are configured centrally in the "MQTT Settings" page accessible from the main launcher's overflow menu. Subapps do not need their own MQTT settings UI.

A single SharedMqttClient singleton (in shared-services) manages the MQTT connection for the entire app. It connects at app startup and reconnects automatically when settings change. Subapps publish and subscribe through it:

import club.clubk.ktag.apps.sharedservices.SharedMqttClient
import club.clubk.ktag.apps.sharedservices.MqttMessageListener

// Publish a message
val battlefield = SharedMqttClient.battlefield
SharedMqttClient.publish("KTag/$battlefield/YourApp/Hello", "Hello!".toByteArray(), qos = 1, retained = false)

// Subscribe to a topic
val listener = object : MqttMessageListener {
    override fun onMessageReceived(topic: String, payload: ByteArray) {
        val text = String(payload)
        // handle message
    }
}
SharedMqttClient.subscribe("KTag/$battlefield/YourApp/Listen", listener)

// Unsubscribe when done (e.g. in cleanup/onCleared)
SharedMqttClient.unsubscribe("KTag/$battlefield/YourApp/Listen", listener)

GPS Location Publishing

Once connected to an MQTT broker, the app automatically publishes the device's GPS position every 30 seconds (subject to OS scheduling) to:

KTag/{Battlefield}/Devices/{Device ID}/Location

The payload is a JSON object:

{"lat":51.5074,"lon":-0.1278,"alt":12.3,"accuracy":5.0}

alt and accuracy are omitted if the device does not provide them. The message is published with QoS 1 and retained=true, so a subscriber joining mid-game will immediately receive the last known position. Location updates stop automatically when the MQTT connection is lost.

The MQTT Settings page includes a Device ID field (defaults to the device's model name) used as the MQTT client identifier. When Autodiscovery is enabled, the client automatically finds a KTag broker on the local network via mDNS (_mqtt._tcp. with purpose=KTag MQTT Broker) and connects using the credentials KTag / {Battlefield}. The broker automatically ensures this credential exists on startup.

Shared Game State

There is exactly one game per battlefield. GameStateController (in shared-services) is a single, process-wide state machine — not owned by any one subapp — that both subapp-konfigurator (which drives the pre-game setup phases) and subapp-koth (which reacts to the in-game phases) read and write through the same API, so they can't disagree about what phase the game is in or when a timed phase ends.

Its 7 states mirror the physical devices' own SystemK top-level states (Packet.SYSTEMK_STATE_* in core/.../ble/Packet.java), so the vocabulary is consistent between the app layer and the wire protocol:

SETUP → CONFIGURING → READY → STARTING_GAME_INSTIGATING → STARTING_GAME_COUNTING_DOWN → PLAYING_INTERACTING → WRAPPING_UP → (back to SETUP)

SETUP/CONFIGURING have no SystemK equivalent (device provisioning happens before any physical device exists to have a state) and are only ever driven by subapp-konfigurator.

Any subapp calls GameStateController.requestCommand(command) to request a transition. This applies immediately, in-process — it never depends on an MQTT round-trip — so it works correctly on a single phone with no broker configured at all. Publishing to MQTT afterward is a courtesy broadcast for other phones:

KTag/{Battlefield}/Game/State     # retained, QoS 1 — the current, authoritative snapshot
KTag/{Battlefield}/Game/Command   # not retained, QoS 1 — a request to transition

Multiple phones' independent controllers converge the same way DeviceInfoRepository resolves conflicting device names: deterministically, via a monotonic sequence number and self-healing republish, rather than through a single elected "hub" phone. Time-boxed phases (the pregame countdown, the lights-out countdown, and the active game timer) are timed by GameStateController itself, reading GamePreferenceKeys.GAME_DURATION/TIME_UNTIL_COUNTDOWN — subapps display the remaining time by reading GameStateController.snapshot.value.phaseEndsAtEpochMs, they don't run their own independent timers.

The active game's length (gameDurationMs) is resolved from preferences once, when StartGame is applied (entering STARTING_GAME_INSTIGATING), and carried forward unchanged in the snapshot from then on. This matters because game duration is just one of several settings editable from either subapp-konfigurator's or subapp-koth's own settings screen — without carrying the resolved value forward, whichever phone's own timer happens to apply the later STARTING_GAME_COUNTING_DOWN → PLAYING_INTERACTING transition could silently re-read (and re-decide the whole battlefield's remaining game length from) a value someone edited after the game already started.

Separately from the live game FSM, all 9 GamePreferenceKeys settings (game duration, countdown delay, num rounds, max health, special weapons on reentry, shot capacity, reload on reentry, min time between shots, min RSSI to initiate transfer) are kept in sync across every device in the battlefield by GameConfigSync, published retained to Game/Config. Neither settings screen needs to know this exists: Konfigurator's own Compose form and KotH's Preference-XML screen both keep writing straight to SharedPreferences exactly as before, and GameConfigSync watches for those writes via a SharedPreferences.OnSharedPreferenceChangeListener — the one choke point common to both, since a standard Android Preference widget has no other call site to hook. A settled batch of changes is republished using the same monotonic-sequence-plus-wall-clock-tiebreak scheme as Game/State, so simultaneous edits on two devices still converge deterministically (most recent write wins). This is what the previous paragraph's caveat is protecting against: a device's current settings now always reflect whoever last edited them, on any device — it's specifically the value already latched into an in-progress game's snapshot that stays frozen once StartGame applies, not the underlying preference itself.

Since the Game/State topic is retained, it can persist indefinitely (e.g. across days). subapp-konfigurator prompts the operator on launch — if the shared state isn't already SETUP — to either join whatever's in progress or reset the battlefield via GameCommand.ForceReset, a command deliberately legal from any state except SETUP itself (the one exception to every other command being legal from exactly one source state). Resetting also broadcasts EVENT_FORCE_STATE (event_data = SYSTEMK_STATE_CONFIGURING) over BLE, so physical devices left stuck in a stale state (e.g. WRAPPING_UP from an earlier session) are pulled back in sync too — the same convention already used by the KTag-HIL-Tester firmware test suite for resetting a device to a known state.

Game/State looping back to SETUP resets it to a blank snapshot, so nothing about a completed game survives there for long. GameStateController.recordResult() additionally publishes a separate, durable record — Game/LastResult — that is only ever overwritten by the next game's own result, never by the SETUP reset. It carries the game's real start/end wall-clock time (gameStartedAtEpochMs, similarly established once and carried forward, alongside gameEndedAtEpochMs captured at recordResult() time), the participant roster (captured once by subapp-konfigurator via setParticipants() when the game starts), and an optional per-hill breakdown (populated by subapp-koth). See the MQTT Topics Reference below for the full payload shape.

MQTT Topics Reference

All topics are rooted at KTag/<battlefield>/..., where <battlefield> comes from the MQTT Settings page. These are the topics owned by shared-services and used across multiple subapps; see each subapp's own README for topics specific to it.

Topic Publisher Retained QoS Payload
Devices/<deviceId>/Info DeviceInfoMqttSync Yes 1 JSON device name/type/version, keyed by BLE MAC
Devices/<deviceId>/Location LocationPublisher (app-wide, started once from MainActivity) Yes 1 {"lat":...,"lon":...,"alt":...,"accuracy":...}
Game/State GameStateController (used by subapp-konfigurator + subapp-koth) Yes 1 JSON GameStateSnapshot — current phase, deadline, game duration, result
Game/Command GameStateController No 1 {"command": "..."} — a requested transition
Game/LastResult GameStateController (via recordResult()) Yes 1 JSON GameLastResult — the most recently completed game's start/end time, winner, times, per-hill breakdown, and participants (see below). Unlike Game/State, never wiped by the next game's SETUP transition — only overwritten by that next game's own result
Game/Config GameConfigSync Yes 1 JSON envelope wrapping all 9 GamePreferenceKeys settings plus sync metadata (see below) — kept in sync across every device in the battlefield

Game/Config payload shape

{
  "config": {
    "gameDurationMin": 10,
    "timeUntilCountdownS": 30,
    "numRounds": 2,
    "maxHealth": 30,
    "specialWeaponsOnReentry": 1,
    "shotCapacity": 30,
    "reloadOnReentry": true,
    "minTimeBetweenShotsMs": 250,
    "minRssiToInitiateTransfer": -50
  },
  "sequence": 7,
  "updatedAtEpochMs": 1751840000000
}

config is the resolved value of all 9 GamePreferenceKeys at the time of publish. sequence is a monotonic counter, bumped by whichever device's edit triggered the publish — the primary ordering key for conflict resolution, same role as GameStateSnapshot.sequence. updatedAtEpochMs is only consulted as a tie-break on the rare occasion two devices publish the same sequence (near-simultaneous edits); whichever has the later timestamp wins. A device that receives a Game/Config message with a lower sequence than its own republishes its own value instead of adopting, the same self-healing-republish behavior Game/State already uses to catch up a straggler.

Game/LastResult payload shape

Game/State is reset to a blank snapshot the instant a game loops back to SETUP, so it can never answer "what just happened." Game/LastResult exists for exactly that — a durable, self-contained record of the last completed game, useful for a public scoreboard or future reporting:

{
  "gameStartedAtEpochMs": 1751840000000,
  "gameEndedAtEpochMs": 1751840600000,
  "winner": "RED",
  "redTimeMs": 512000,
  "blueTimeMs": 388000,
  "participants": [
    { "name": "Red 1", "team": "RED" },
    { "name": "Blue 1", "team": "BLUE" },
    { "name": "Purple 1", "team": "PURPLE" }
  ],
  "breakdown": [
    { "name": "Hill A", "redTimeMs": 300000, "blueTimeMs": 200000 },
    { "name": "Hill B", "redTimeMs": 212000, "blueTimeMs": 188000 }
  ]
}

participants is captured once by subapp-konfigurator when the game starts (Device.name/team, not BLE address — a name is what's meaningful to a spectator or report). team is RED, BLUE, or PURPLE — Purple is a real, hostile team (hostile to Red, Blue, and other Purple devices — see subapp-mine's all-teams targeting, not a neutral/non-combatant designation) that just doesn't currently accumulate hill-possession time, so it's still listed as a participant but never appears in breakdown/redTimeMs/blueTimeMs. breakdown is populated by subapp-koth (one entry per hill that reported a score) and is empty for game modes that don't have sub-scores.

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 © 2025-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
Kotlin 2.2.20 Apache-2.0 https://kotlinlang.org/
JetBrains Mono 2.304 OFL-1.1 https://github.com/JetBrains/JetBrainsMono
Moquette MQTT Broker 0.17 Apache-2.0 https://github.com/moquette-io/moquette
Eclipse Paho MQTT 1.2.5 EPL-2.0 https://github.com/eclipse/paho.mqtt.java
paho.mqtt.android 4.3 Apache-2.0 https://github.com/hannesa2/paho.mqtt.android
usb-serial-for-android 3.10.0 MIT https://github.com/mik3y/usb-serial-for-android/
CameraX 1.4.0 Apache-2.0 https://developer.android.com/jetpack/androidx/releases/camera
MediaPipe Tasks 0.10.32 Apache-2.0 https://developers.google.com/mediapipe
EfficientDet-Lite0 1 Apache-2.0 https://tfhub.dev/tensorflow/lite-model/efficientdet/lite0/detection/metadata/1