Music's second part: the marked playlists' tracks are downloaded as
Jellyfin keeps them, on unmetered networks only, while the app is open
or resumes, and deleted once no marked playlist holds them. Each
track's tempo is worked out once on the phone: a BPM tag where the file
has one, else a minute decoded with MediaCodec and its spectral flux
autocorrelated (Tempo.kt, tested on the JVM). The music screen shows
what is kept and deletes it after asking.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A route's page shares it as GPX 1.1 (Android's share sheet, else saved
where the person chooses): its way a track with elevations, its
waypoints and turns as named points. A GPX file comes in from the Routes
tab or, on Android, opened with Train or shared to it; it is planned
along the paths through its points or kept as drawn, then named and
saved as a new route.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The Record sheet offers the routes with a way planned; the recorder's
service follows the one chosen offline (RouteGuide.kt): turns told 50 m
before them (100 m on a bike) after a tone, off the route past 40 m for
10 s a cue with the way back, told again on rejoining, the end told.
The recording file keeps the route and the guidance; the recording
screen shows the next turn or the way back, and swipes to a map of the
route, the track and the position, north or the heading up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Routes are synced `route` rows (the model `SavedRoute`, as Flutter has a
`Route`), sealed as plans are; the migration `routes` only creates the
table, its space index and its cascade to the sync engine's spaces. A
route's payload holds its name, activity, profile, waypoints and the way
planned, points and elevations delta-coded and gzipped.
Home gets a fourth tab, Routes, and the planner: waypoints tapped on the
map, held and dragged, the last undone, a new route's first where the
phone is (GPS through the recorder's channel), the way planned by the
instance's BRouter after each change, an earlier answer dropped; its
distance, climb, descent, elevation and surfaces under the map.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The plan chosen on the Record sheet goes to the recording service spelled
out: each step's end, its target (zones as the heart rates they span for
the profile's highest rate) and its spoken line in the app's language.
PlanRunner.kt moves through the steps on the moving time and distance,
counts a timed step's last seconds down and cues off target from a 30 s
window; PlanCues.kt gives a tone, a vibration and the voice, ducking
music. The file's plan and step lines bring it back, and the recording
screen shows the step, what is left of it, its target, the next step and
Lap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A recording with a Loop set up reads its heart rate through Polar's SDK
while no strap sends: the strap first, the Loop where there is none or
while it is lost. The SDK is one for the process, held by the open
app's bridge and by the recording service, so that the band stays
connected with the app gone; the app reopening finds it connected. The
recording file gains `loop` state lines; the rate is kept alone, as `h`
lines, and the Record sheet names the source.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Train's server relays Polar's firmware service for the people signed in
(`firmware`: offered, check, download), a fetched file kept in memory a
day per hardware and version, off with LOOP_FIRMWARE_RELAY=off. The app
asks on the day's first connection and on the band's screen, from Polar
or the instance as the person chose, never Polar in the instance's
place; an update runs only after asking, reads and keeps the band
first, and sets it up again with its figures and clock after.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Each read of the band lists the sessions it holds, reads those from the
read's first day on (`LoopDays.kt`, one line per exercise: its start
with the band's offset, length, Polar's sport ID, figures and heart
rate at its interval), and deletes those up to the day kept, as the
days. The app keeps each as a workout of origin Polar Loop with its
heart rate (`takeLoopSessions`), Polar's running and cycling as
Train's and every other sport as the new `Activity.other`, which a
workout's edit sheet now changes; a session overlapping a workout Train
recorded gives it its heart rate where it has none instead. The
sessions taken are remembered in `loop.json`, so one deleted in Train
isn't brought back. Health Connect writes the Loop's workouts as
Train's own.
The SDK gives sessions as its protobuf messages: protobuf-javalite
3.20.0, the SDK's own, is named for compiling only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The server gains Train's synced `day`, `day_series` (a day's heart
rate, deleted with its day) and `night`, each a sealed payload alone;
the migration `days` only adds their tables, indexes and foreign keys
(to their sync space, and a chunk's to its day, deferred), and the
person's deletion takes them with their spaces.
On Android, `LoopDays.kt` reads a band's days from a date to today,
framed by the SDK's sync start and stop, into lines of JSON, then
deletes on the band what the app kept up to the date it names, never
today; `LoopBridge` answers `readDays` with them, one band call at a
time, and `background` schedules `LoopWorker`, a WorkManager job every
four hours that appends the same lines to the person's
`loop_days.txt` while the app doesn't hold the band.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
On Android, the Loop through Polar's BLE SDK 8.3.0 (from JitPack, asked
for Polar's group alone, its files pinned by SHA-256 in Gradle's
verification metadata): LoopBridge.kt over app.halkyon.train/loop, the
first-time use's figures mapped and checked in LoopSetup.kt, tested on
the JVM. PolarLoop per person remembers the band in loop.json, connects
while the app is open, sets its clock at each connection, keeps what it
tells (battery, charging, firmware), offers a changed profile and sends
it only when asked (D35: first-time use writes a new user identifier
each time), and lets the band go by a factory reset after asking. The
setup sheet finds Loops nearest first and asks only for what the
profile lacks; Polar's defaults (60, 220 less the age, a VO2max from
Polar's fitness tables) go to the band alone.
Polar's SDK is not free software (D36, correcting D32): the app's AGPL
gains a section 7 permission for linking it, LICENSE.md names it, and
its licence and bundled notices join the licence list on Android.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
connect-client 1.1.0 behind app.halkyon.train/health: its status, the
permissions by the app's three names (write, read, history), Train's
records written by their client IDs and deleted by them, other apps'
sessions read through the changes token or whole, their samples, and a
route with the person's consent; the rationale's links from Health
Connect. Android 8 at least now, as the client needs it. The records'
mapping is tested on the JVM, the channel's encoding in Dart; the fake
platform is for the parts to come.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Rowing indoors is offered again, with the rower chosen on the Record
sheet: a sheet looks for Fitness Machine Service rowers, and the one
chosen is remembered in the person's folder. The recorder service
connects to it as to the strap (one NotifyLink for both), parses
FTMS's Rower Data in Kotlin (tested on the JVM on frames written from
the specification and D8's), counts its totals from its first frame,
pauses 5 s after the last stroke, and gets the connectedDevice type. A
row's screen shows its metres, split per 500 m, stroke rate and watts;
its page its average split, stroke rate and power, the highest power,
its strokes, splits per 500 m and charts of the split, stroke rate and
power. Not yet tried on the WP 5000.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The Record sheet gains a Heart rate row: a sheet looks for straps
advertising the Heart Rate Service, the nearest first, once nearby
devices are allowed, and the one chosen is remembered in the person's
folder; it records with every workout unless switched off for one.
The recorder service connects to it with Android's own Bluetooth API,
no package (D26), parses Heart Rate Measurement in Kotlin (tested on
the JVM), writes each rate and the strap's state to the recording
file, and asks for a lost strap again for as long as it records. The
recording screen shows the heart rate, and "Strap lost" while it is
gone; a workout's page shows the heart's average and highest rate,
each split's average and its chart.
iOS's recording waits for a Mac (D25), after the rower.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
RecorderService, a foreground service of the types location and health
started from the open app, takes fixes from LocationManager's GPS and
steps from the step detector, pauses by itself (AutoPause, tested on the
JVM) and appends each sample to the person's recording file, flushed
every five seconds. MainActivity asks for location, notifications and,
for auto-pause on foot, physical activity, never background location
(D19), and bridges the service to the app.
The app follows the file's lines with RecordingBloc and shows the moving
time, the distance, the pace or speed and GPS's state; the notification
shows the time and the distance with Pause or Resume. Stop is held down;
a stopped recording is saved as a workout and its series, or discarded
after asking, and one cut off or left unsaved is offered as the app
opens. Home has the Record button where the device records.
Three choices of the implementation's are recorded under D21 for review:
rowing waits for 5f, auto-pause on foot without steps goes by GPS speed
(below 2 km/h, on above 3 km/h), and Stop's hint shows above the button.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
tool/app_icons.py at the root replaces Learn's and Todo's scripts: each
app's Lucide glyph (lucide-static 1.50.0) drawn by librsvg, fading from
cobalt through the colours ADR 0031 names, then every platform's
launcher icon through flutter_launcher_icons, Windows' .ico with every
size, and Learn's snap icon.
Learn's icon becomes the graduation cap, its window's on Linux too, with
Lucide's notice in its licence list; the kingfisher stays the emblem on
the rail and the launch screen. Todo's becomes list-todo, Train's bike.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
apps/train with halkyon_train_server, _client and _flutter, as Todo's:
halkyon_base configured by trainBase(), its own register alone and a
member of the instance's where the operator names one, the app on
halkyon_app's gates with Train's frame, account sheet and About, texts
in British English and German that speak of workouts, app.halkyon.train
on Android, iOS, Linux, macOS and Windows.
Where it differs from Todo: development ports of its own (8180 to 8190),
so it runs beside Learn's server; one initial migration with the schema
Todo's migrations end at (the same tables, compared line by line), not
Todo's seventeen; and Todo's icon as a placeholder until the icon tool
of ADR 0031 draws Train's bike. The workspace, the licences, the READMEs
and AGENTS.md name it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>