Skip to content

Repository files navigation

SSTV Alarm

An Android alarm clock that refuses to be swiped away.

When it goes off it does not play a ringtone. It fetches the day's forecast and the trending headlines, draws them into a picture, encodes that picture into a Slow-Scan Television signal — the analogue mode radio amateurs have used to send images over HF since the 1950s — and transmits it out of the speaker on a loop.

To turn it off you have to decode the transmission and type the information you read off it.

A frame decoding line by line The main menu screen Multi-device mode

Another device is decoding the image

Features

  • Daily alarm with exact scheduling, surviving reboot and Doze
  • Optional heads-up notification ahead of the alarm, with a one-tap "dismiss today" action, and a configurable lead time
  • Rings again on an interval if it is ignored, until the code is entered
  • Emergency code as an escape hatch, in case you genuinely cannot wait for a decode
  • Weather location from a hand-picked city or from the device
  • Headlines from any RSS or Atom feed you point it at (Google RSS by default)
  • Celsius or Fahrenheit, defaulting to your region
  • 24 SSTV formats to transmit in, from an 8-second Robot frame to a PD one taking nearly 5 minutes
  • Alarm volume on the home screen, so you can set it without leaving the app
  • Test transmissions, with the option to mute one or accept the emergency code for that run only
  • Material 3 throughout, including Material You dynamic colour on the alarm screen

How you get out of it

The picture carries the day's high and low. You type them into the numpad, and each value is padded to a fixed width so the answer is always plain digits:

Reading You type Why
30 °C and 24 °C 3024 two digits each
7 °C and 13 °C 0713 single digits get a leading zero
−1 °C and −6 °C 0106 there is no minus key, so the sign is dropped
117 °F and 97 °F 117097 Fahrenheit past 100 needs three digits each

Which reading comes first is randomised per alarm, and only the transmitted picture says which — so the image genuinely has to be decoded rather than guessed at. The numpad digits are shuffled each time too, which kills the muscle memory that makes normal alarms so easy to dismiss asleep.

If the forecast cannot be fetched there is nothing to read off the weather, so the picture falls back to carrying a random code instead.

Two ways to decode it

Single device. Hold a button and the picture paints itself on screen, line by line, in step with the audio going out of the speaker. Release it and the lines you missed stay as static until the transmission loops back round.

(This never opens the microphone. It reads the audio player's position, maps it onto the SSTV timeline, and paints the corresponding scan lines with the sync jitter and speckle a real decode has. What you see matches what a receiver pointed at the phone would produce, without the latency and failure modes of an actual audio loopback.)

Multi device. No on-screen decoding at all. Point a real SSTV decoder — Robot36, MMSSTV, QSSTV, or a radio — at the speaker and read the passcode off the received picture. Every format the app offers has a published timing specification, so anything that can decode SSTV can decode this.

The SSTV encoder

The encoder is now a standalone library

https://github.com/SEKY443/sstv-encoder

Dependency-free Kotlin, runs on both the JVM and Android, installable in one line via JitPack. If you want SSTV encoding for your own project, get it there rather than copying files out of this app — the API reference, the full mode tables and the implementation notes all live in that repo.

There is no maintained SSTV encoder library for Android. The well-known projects are decoders, and the C++ ones are GPL — which is why it was written from scratch. It used to live inside this app; it is now maintained separately and pulled in as the app's only non-AndroidX runtime dependency.

Output is 16-bit mono PCM at 22 050 Hz, played through AudioTrack with USAGE_ALARM so it follows the system alarm volume rather than media volume.

The two details that make or break a decode — a phase-continuous oscillator, and sample counts derived from cumulative elapsed time rather than per-segment rounding — are explained in the library repo.

What stays app-specific is the picture drawn for the medium: heavy strokes, high contrast, no fine detail, and a colour calibration bar across the top so a receiving operator can check the channels arrived in the right order.

Choosing a format

The library can generate 35 modes. The app offers 24 of them.

The missing eleven are the ones whose timings the library had to reconstruct because no published specification could be found — the Wraase SC1 and AVT families, and the legacy monochrome modes. They are fine to experiment with, but this alarm can only be dismissed by decoding what it sends, so offering a format that stock decoder software cannot read would let someone lock themselves out of their own alarm. SstvModeCatalog filters them out and a test fails if any ever slips through.

What is left runs from an 8-second Robot 8 B/W frame to PD 290 at 289 seconds — the mode names are nominal, the picker shows what each one actually takes — grouped by family with its size alongside.

Fitting the picture to the frame

The offered formats are not one shape. They run from 320 × 120 to 800 × 616, and the layout is drawn once in a fixed 320 × 256 design space and scaled onto whichever the mode uses. Scaling the canvas rather than every coordinate keeps one layout to reason about.

Stretching to fit works for the 4:3 and 5:4 frames, which land within a few percent of the design. It does not work for the short ones: Martin 3/4 at 320 × 128 and the Robot black-and-white modes at 320 × 120 lose about half their height, and the small labels squash until they are unreadable. The picker says so on those formats, and Scale up to fill the frame offers the alternative — scale both axes by the larger factor so the text keeps its drawn size, and let the layout run off the frame.

That crop is the interesting part. Centring it loses the temperature readings; anchoring at the top loses the band that names which reading to type first, which is randomised per alarm and therefore unguessable. Either loss makes the alarm impossible to answer honestly. The anchor sits between the two, keeping the weather block and the prompt band on screen and giving up the calibration bar, the title and the headlines — decoration rather than answers. SstvFrameCropTest checks that invariant for every format on offer.

Verifying it actually works

The audio is the entire product in multi-device mode: if the tones are wrong, no external decoder can read the passcode and the alarm becomes impossible to dismiss. So the encoder is tested by measuring the waveform it produces — counting zero crossings to recover the leader tone, the VIS header, the black and white frequencies and the green-blue-red sweep order — rather than by trusting the constants. The library repo covers that side in full.

51 unit tests here, covering the passcode rules, the settings and the scheduling arithmetic, plus the two invariants that keep the alarm answerable: that no unreadable format reaches the picker, and that scaling never crops away the readings or the typing order. The encoder's own tests — waveform measurement and the mode timing table — live with the library.

./gradlew test

Building

git clone <your-fork>
cd SSTVAlarm
./gradlew assembleDebug

Requires JDK 17+. Everything else — AGP 9.2.1, Gradle 9.4.1, Kotlin 2.2.10 — comes from the wrapper and the version catalog. minSdk 24, targetSdk 36.

No API keys to configure. Both data sources are keyless:

Source Used for
Open-Meteo daily forecast and city search
Google News RSS trending headlines, replaceable with any RSS or Atom feed

F-Droid

The app has no proprietary dependencies — no Google Play Services, no Firebase, no analytics, no ads — so it qualifies for F-Droid as-is, with one exception: the SSTV encoder is normally resolved from JitPack, and F-Droid's build environment has no network access beyond the repositories it explicitly allows, which does not include JitPack.

To keep this reproducible for F-Droid without giving up JitPack for everyday contributors, the encoder is also committed as a git submodule at libs/sstv-encoder, pinned to the release named by sstvEncoder in gradle/libs.versions.toml. settings.gradle.kts builds from it automatically whenever it is populated, and falls back to JitPack otherwise — so a plain git clone without --recurse-submodules still builds, it just resolves the encoder from JitPack instead of building it from source:

git clone --recurse-submodules <your-fork>
cd SSTVAlarm
./gradlew assembleDebug

F-Droid's own build metadata sets submodules: yes, which runs git submodule update --init --recursive before building, so its build always uses the in-repo source rather than JitPack.

Languages Support

Language Locale File
English en values/strings.xml
正體中文(台灣) zh-TW values-zh-rTW/strings.xml
繁體中文(香港) zh-HK values-zh-rHK/strings.xml
简体中文 zh-CN values-zh-rCN/strings.xml
日本語 ja values-ja/strings.xml

Translations welcome

If you speak a language that is not on that list, a pull request would be very welcome. You do not need to know Kotlin or even build the app — a translation is one XML file.

  1. Copy values/strings.xml to values-<language>/strings.xml. Android uses values-de, values-ko, values-pt-rBR, values-es, and so on.
  2. Translate the text between the tags. Never change the name="..." attributes — those are how the code finds each string.
  3. Add a matching <locale> entry to res/xml/locales_config.xml, otherwise your language only reaches users whose whole device is set to it.
  4. Run the checker and make sure it passes:
python3 tools/check_translations.py

Three things worth knowing before you start:

  • Placeholders must survive. %1$s and %2$d are filled in at runtime. Where several appear in one string they are numbered, so you may reorder them freely to suit your grammar. Getting one wrong crashes the app for that language only, which is exactly what tools/check_translations.py is there to catch.
  • The strings marked SSTV FRAME are drawn into the transmitted picture, which is between 320 × 120 and 800 × 616 pixels and then turned into audio. Keep them short and prefer upper case. The Chinese and Japanese translations deliberately leave those in English, because CJK glyphs at that size do not survive the round trip legibly — do the same if your script has the same problem.
  • English is the source of truth. values/strings.xml is where a string changes first, and a translation follows its wording. A translation that says more than the English is drift, not detail — it goes stale the moment the English is reworded.
  • Partial translations are fine. Anything you leave out falls back to English, so a half-finished language is still an improvement. Send it and fill in the rest later.

Temperature units follow the device region automatically. (Fahrenheit in the US 🦅 and a handful of other regions, Celsius everywhere else)

Project layout

core/    settings, persistence, temperature and passcode rules
data/    forecast, headlines, geocoding, location resolution
sstv/    frame renderer, bitmap bridge, audio playback
         (the encoder itself is the SEKY443/sstv-encoder dependency)
alarm/   scheduling, receivers, foreground service, alarm activity
ui/      Compose screens, numpad, simulated decoder

Notes and limitations

  • Location while the alarm is ringing. Android grants location "while in use", and an alarm fires with the app in the background — the platform silently returns null for every provider. The app works around this by capturing the position while it is in the foreground and reusing it, rather than asking for background location access. Setting a city by hand avoids the question entirely.
  • The alarm screen is dark by default regardless of system theme, on the grounds that a full-brightness white screen at seven in the morning is hostile. There is a setting to make it follow light and dark mode; the Material You palette applies either way.
  • The alarm volume will not go to zero. Android holds an app without the notification-policy permission above the stream's real minimum — a Pixel reports Min: 1 w/o perm:3 and clamps to 3 of 7 — because an app should not be able to silence an alarm. There is no public API for that second number, so the slider discovers it: a write that comes back higher than it asked for is the floor, and the track shrinks to match.
  • A muted test still paints the picture. Muting sets the AudioTrack volume to zero rather than skipping playback, so the playback head still advances and the single-device decode stays in step with a transmission nobody can hear.

About

An Android app which can wake you up with real time generated SSTV noise.

Topics

Resources

Stars

8 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages