Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ __pycache__/
.venv/
venv/


# Local JETI Studio test source (tools/publish/make_apps_json.py --local)
Apps.local.json
3,311 changes: 3,311 additions & 0 deletions Apps.json

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ built spec-first with GitHub Spec Kit (`/speckit-specify`, `/speckit-plan`,
`/speckit-tasks`, `/speckit-implement`). Each feature gets its own
`specs/NNN-name/`. A feature may create a new app or change an existing one.

## Open to-dos for Aaron

At the start of a session, remind Aaron of any items here before starting
other work. Remove an item once he says it's done.

- Install and set up RTK to help with console commands.

## Apps in this repo

| App (menu name) | Script | Spec | Based on |
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,25 @@ Speed Gauge speaks in its own voice (Piper "Amy"), in
(see [CREDITS.md](CREDITS.md)); [`tools/voice/`](tools/voice/README.md)
regenerates them. Without them the app works in the transmitter's voice.

## Install with JETI Studio

1. In JETI Studio open **File → Configuration**, and add this line to the
list of Lua app sources:

```
https://raw.githubusercontent.com/Flazzarious/jeti-lua-apps/main/Apps.json
```

2. Connect the transmitter by USB. The released apps appear in JETI
Studio's Lua app list; select one and install it.

Each app installs exactly its released version (its files come from the
release tag). Descriptions: [Speed Gauge](docs/apps/speed-gauge.md).

For maintainers: after tagging a release, run
`python tools/publish/make_apps_json.py`, commit `Apps.json` and merge it to
`main`.

Apps here are for telemetry, timers, announcements and displays only. Nothing
in this repo may control surfaces, throttle or any flight function. See the
constitution: [`.specify/memory/constitution.md`](.specify/memory/constitution.md).
Expand Down
Binary file added docs/apps/img/speed-gauge-double.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/apps/img/speed-gauge-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/apps/img/speed-gauge-single.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/apps/img/speed-gauge.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 57 additions & 0 deletions docs/apps/speed-gauge.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Speed Gauge</title>
</head>
<body style="padding:10px;float:left;max-width:500px;">
<h1>Speed Gauge</h1>
<p>Spoken airspeed callouts, stall and overspeed warnings, and a
speedometer gauge for the DS-24 II / DC-24 II.</p>
<img src="img/speed-gauge.png" alt="Speed Gauge full screen" width="480">
<ul>
<li><b>Speed callouts</b> in a clear voice: more often when the
speed changes, every few seconds below landing speed, or a fast
number-only mode on a switch.</li>
<li>Callouts wait until the model first passes <i>Callouts start
above</i> (default 30 mph), and are never spoken below 5 mph.</li>
<li><b>Max speed callout</b> shortly after each new peak.</li>
<li><b>Warnings:</b> stall (at most twice per slowdown), overspeed
and "airspeed alive", with stick vibration.</li>
<li><b>Gauge:</b> a speed bar (single size), a small dial (double
size) or a full-screen dial with stall, overspeed, air density,
elevation, temperature and raw sensor speed.</li>
<li><b>Air density correction</b> (optional): true airspeed from
field elevation and the temperature: standard, entered by hand,
or live from a sensor such as the MSpeed's.</li>
<li>Works with a pitot airspeed sensor (e.g. JETI MSpeed) or GPS
speed.</li>
</ul>
<p>Double and single size on the main screen:</p>
<img src="img/speed-gauge-double.png" alt="Speed Gauge double size" width="480">
<img src="img/speed-gauge-single.png" alt="Speed Gauge single size" width="480">
<p><b>Installing takes a while:</b> Speed Gauge brings its own voice,
about 550 files (24 MB), mostly short audio clips, and copying them
to the transmitter is slow. Let JETI Studio finish before
disconnecting.</p>
<h3>Setting up</h3>
<ol>
<li>Add <b>Speed Gauge</b> in <i>Applications - User Applications</i>.</li>
<li>In its settings choose the speed sensor and units, assign a
callouts switch, and set your model's landing, stall and
overspeed speeds. Set <i>Gauge max limit</i> to your sensor's top
speed (MSpeed: 350 km/h / 215 mph; MSpeed 450 EX: 450 km/h /
280 mph).</li>
<li>Put <b>Speed Gauge</b> or <b>Speed Gauge (full screen)</b> on
a page in <i>Timers/Sensors - Displayed telemetry</i>.</li>
</ol>
<p>Version: <b>0.3.0</b></p>
<p>Compatibility: <b>DC/DS-24II</b>, firmware 6.x</p>
<p>Based on <b>DFM Speed Announcer</b> by DFM (Dave McQueeney), MIT
license. Voice: Piper text-to-speech with the "Amy" voice by Mycroft /
Rhasspy; the voice files are licensed CC BY-SA 4.0. Speed Gauge: MIT
license, Copyright (c) 2026 Aaron George.</p>
<p>Source and full credits:
<a href="https://github.com/Flazzarious/jeti-lua-apps">github.com/Flazzarious/jeti-lua-apps</a></p>
</body>
</html>
69 changes: 69 additions & 0 deletions docs/apps/speed-gauge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Speed Gauge

Spoken airspeed callouts, stall and overspeed warnings, and a speedometer
gauge for the JETI **DS-24 II / DC-24 II**.

![Speed Gauge full screen](https://raw.githubusercontent.com/Flazzarious/jeti-lua-apps/main/docs/apps/img/speed-gauge.png)

## What it does

- **Speed callouts** in a clear voice ("eighty five miles per hour"). They
come more often when the speed is changing and every few seconds below
landing speed. A continuous switch gives fast number-only callouts.
- **Callouts wait for flight:** nothing is spoken until the model first
passes "Callouts start above" (default 30 mph), and never below 5 mph.
- **Max speed callout:** "max 201 miles per hour" shortly after each new
peak.
- **Warnings:** stall (at most twice per slowdown), overspeed and "airspeed
alive", with stick vibration.
- **Gauge** for the main screen: a speed bar (single size), a small dial
(double size) or a full-screen dial with stall, overspeed, air density,
elevation, temperature and raw sensor speed.

![Speed Gauge double size, on the main screen](https://raw.githubusercontent.com/Flazzarious/jeti-lua-apps/main/docs/apps/img/speed-gauge-double.png)
![Speed Gauge single size, on the main screen](https://raw.githubusercontent.com/Flazzarious/jeti-lua-apps/main/docs/apps/img/speed-gauge-single.png)
- **Air density correction** (optional) shows true airspeed from your field
elevation and the temperature: standard, entered by hand, or read live
from a sensor such as the MSpeed's.
- Works with a pitot airspeed sensor (e.g. JETI MSpeed) or a GPS speed.

## Requirements

- JETI **DS-24 II or DC-24 II**, firmware 6.0 or newer. On other DC/DS-24
transmitters the callouts and warnings work, and the gauge window shows
a notice instead.
- A telemetry sensor that reports speed.

## Installing

Installing takes a while: Speed Gauge brings its own voice, about 550
files (24 MB), mostly short audio clips, and copying them to the
transmitter is slow. Let JETI Studio finish before disconnecting.

## Setting up

1. Add **Speed Gauge** in *Applications → User Applications*.
2. Open its settings (*Applications → Speed Gauge*):
- choose your **speed sensor** and the **units**;
- assign a **callouts on/off switch** (and, if you like, a continuous
callouts switch);
- set your model's **landing**, **stall** and **overspeed** speeds;
- set **Gauge max limit** to your sensor's top speed to use its whole
range (MSpeed: 350 km/h / 215 mph; MSpeed 450 EX: 450 km/h / 280 mph).
3. Put **Speed Gauge** (single or double size) or **Speed Gauge (full
screen)** on a page in *Timers/Sensors → Displayed telemetry*.

Every setting has a short explanation on the settings screen. Settings are
saved per model; the max speed resets at each start-up.

## Credits and licenses

- Based on **DFM Speed Announcer** v2.1 by **DFM (Dave McQueeney)**, MIT
license. Its warning recordings are included unmodified.
- Voice: **Piper** text-to-speech with the voice model **"Amy"** by
Mycroft / Rhasspy. The voice files (in `/Apps/AG-SpdGa/voice/`) are
licensed **CC BY-SA 4.0**.
- Speed Gauge itself: MIT license, Copyright (c) 2026 Aaron George.

Source, full credits and license texts:
[github.com/Flazzarious/jeti-lua-apps](https://github.com/Flazzarious/jeti-lua-apps)
151 changes: 151 additions & 0 deletions tools/publish/make_apps_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
"""make_apps_json.py - build Apps.json, the JETI Studio app source file.

Copyright (c) 2026 Aaron George
SPDX-License-Identifier: MIT

JETI Studio installs Lua apps from JSON source files that users add under
File -> Configuration. This writes one in JETI's own format, the format of
JETI's catalog (http://support.jetimodel.cz/files/update-dcds/apps.json)
and of JETI Studio's AppGenerator:

{"applications": [{"id": 0, "version": ..., "author": ...,
"hw": [transmitter type IDs], "releaseDate": RFC 2822 date,
"name": {"en": ...}, "description": {"en": URL of an HTML page},
"previewIcon": URL, "files": [{"url", "destination", "hash", "size"}]}]}

`destination` is relative to the SD card (e.g. "Apps/AG-SpdGa.lua"),
`hash` is the file's SHA-1 and `size` its length in bytes. Each app's files
are read from its release tag, and their download links point at that tag,
so users always install exactly the released version. The description page
and icon come from `main`.

Run from the repo root after tagging a release, then commit Apps.json and
merge it to `main`:

python tools/publish/make_apps_json.py

To try it in JETI Studio before publishing, `--local` also writes
Apps.local.json (gitignored), whose description and icon are the files in
this working folder; add its file:/// URL under File -> Configuration.

Standard library only.
"""

import hashlib
import json
import subprocess
import sys
from email.utils import format_datetime
from datetime import datetime, timezone
from pathlib import PurePosixPath
from urllib.parse import quote
from pathlib import Path

REPO = Path(__file__).resolve().parents[2]
RAW = "https://raw.githubusercontent.com/Flazzarious/jeti-lua-apps"

# Transmitter type IDs ("hw") in JETI's catalog: every "(DC/DS-24II)" app
# there uses exactly 3866 and 3867 (the DS-24 II and DC-24 II).
HW_DCDS24_II = [3866, 3867]

# One entry per published app. "tag" is its release tag; "paths" are the
# repo paths (files or folders) that make up the app on the transmitter.
APPS = [
{
"name": "Speed Gauge (DC/DS-24II)",
"script": "AG-SpdGa",
"tag": "AG-SpdGa-v0.3.0",
"author": "Aaron George (based on DFM Speed Announcer by Dave McQueeney)",
"hw": HW_DCDS24_II,
"description": "docs/apps/speed-gauge.html",
"previewIcon": "docs/apps/img/speed-gauge-icon.png",
"paths": [
"src/Apps/AG-SpdGa.lua",
"src/Apps/AG-SpdGa",
"src/Apps/lib/ag_dens.lua",
"src/Apps/lib/ag_gauge.lua",
],
},
]


def git(*args, text=True):
return subprocess.run(["git", *args], cwd=REPO, check=True,
capture_output=True, text=text).stdout


def url(ref, path):
return f"{RAW}/{quote(ref)}/{quote(path)}"


def app_version(tag, script):
"""APP_VERSION from the app's source at its tag, e.g. "0.3.0"."""
for line in git("show", f"{tag}:src/Apps/{script}.lua").splitlines():
if line.startswith("local APP_VERSION"):
return line.split('"')[1]
raise SystemExit(f"APP_VERSION not found in {script}.lua at {tag}")


def release_date(tag):
"""The tag's date as RFC 2822, e.g. "Sun, 04 Oct 2026 12:00:00 +0000"."""
stamp = int(git("log", "-1", "--format=%ct", tag).strip())
return format_datetime(datetime.fromtimestamp(stamp, timezone.utc))


def local_url(path):
"""file:/// URL of a file in this working folder (for --local)."""
return (REPO / path).resolve().as_uri()


def entry(app, local=False):
tag = app["tag"]
paths = git("ls-tree", "-r", "--name-only", tag, "--", *app["paths"]).split()
if not paths:
raise SystemExit(f"no files for {app['name']} at {tag}")
files = []
for p in sorted(paths):
data = git("show", f"{tag}:{p}", text=False)
files.append({
"url": url(tag, p),
# src/Apps mirrors Apps on the transmitter's SD card.
"destination": str(PurePosixPath(p).relative_to("src")),
"hash": hashlib.sha1(data).hexdigest(),
"size": len(data),
})
return {
"id": 0,
"version": app_version(tag, app["script"]),
"author": app["author"],
"hw": app["hw"],
"releaseDate": release_date(tag),
"name": {"en": app["name"]},
"description": {"en": local_url(app["description"]) if local
else url("main", app["description"])},
"previewIcon": (local_url(app["previewIcon"]) if local
else url("main", app["previewIcon"])),
"files": files,
}


def write(path, out):
# open() with newline= (Path.write_text has none before Python 3.10).
with open(path, "w", encoding="utf-8", newline="\n") as f:
f.write(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
print(f"wrote {path.relative_to(REPO)}")


def main():
out = {"applications": [entry(app) for app in APPS]}
write(REPO / "Apps.json", out)
if "--local" in sys.argv:
local = REPO / "Apps.local.json"
write(local, {"applications": [entry(app, local=True) for app in APPS]})
print(f"JETI Studio test source: {local.resolve().as_uri()}")
for a in out["applications"]:
total = sum(f["size"] for f in a["files"])
print(f"{a['name']['en']} {a['version']}: {len(a['files'])} files, "
f"{total / 1e6:.1f} MB, {a['releaseDate']}")


if __name__ == "__main__":
main()
Loading
Loading