neiam
/apollos_crib
It's Apollo's Crib
public · Issues · Pulls 1 · Labels · Forks · Compare · Actions failure · Badges · Packages
Log in to mark this repository.
Put a vision on an Android home screen
04f2bcb · Gabriel Morell · 2026-09-02 18:05
Message
{commit_body(@commit)}
Files changed
modified
README.md
+12
−0
| 22 | 22 | Foci are anchors for querying in physical space |
| 23 | 23 | Plani can be configured to provide the equivalent to an unanchored Foci updated via Ankyra |
| 24 | 24 | |
| 25 | +## Clients | |
| 26 | + | |
| 27 | +Ankyra clients subscribe to a rabbit user's MQTT topic and draw whatever the | |
| 28 | +Pythiae publishes to it. | |
| 29 | + | |
| 30 | +- [`clients/lilygo4.7`](clients/lilygo4.7) — a LilyGo 4.7" e-paper panel, MicroPython | |
| 31 | +- [`clients/android`](clients/android) — a [Smartspacer](https://github.com/KieronQuinn/Smartspacer) | |
| 32 | + plugin putting a vision on the Android home and lock screen | |
| 33 | + | |
| 34 | +[`apollos-types`](https://github.com/neiam/apollos-types) is the source of truth | |
| 35 | +for the published wire format. | |
| 36 | + | |
| 25 | 37 | ## Public Demo |
| 26 | 38 | |
| 27 | 39 | The public demo instance https://ac.gmp.io/ is here, and the landing data displays queries for various entities around |
added
clients/android/.gitignore
+10
−0
| 1 | +*.iml | |
| 2 | +.gradle/ | |
| 3 | +local.properties | |
| 4 | +.idea/ | |
| 5 | +.DS_Store | |
| 6 | +build/ | |
| 7 | +captures/ | |
| 8 | +.externalNativeBuild | |
| 9 | +.cxx | |
| 10 | +*.apk |
added
clients/android/README.md
+93
−0
| 1 | +# Apollo's Crib for Android | |
| 2 | + | |
| 3 | +A [Smartspacer](https://github.com/KieronQuinn/Smartspacer) plugin that puts a | |
| 4 | +vision's queries on the Android home and lock screen. | |
| 5 | + | |
| 6 | +It is an Ankyra client, the same as `clients/lilygo4.7`: it subscribes to a | |
| 7 | +rabbit user's MQTT topic, keeps the last board it heard on disk, and draws it | |
| 8 | +as Smartspacer Targets. Nothing is polled and no HTTP endpoint is involved -- | |
| 9 | +the Pythiae publishes on change and the phone redraws when it does. | |
| 10 | + | |
| 11 | +## What it shows | |
| 12 | + | |
| 13 | +| Target | Source | What it draws | | |
| 14 | +| --- | --- | --- | | |
| 15 | +| Transit | `gtfs` | The next departures from a stop, soonest route first, with a separate target for a severe or stop-specific alert | | |
| 16 | +| Bikeshare | `gbfs` | Bikes and docks at a station, or loose bikes for an area query | | |
| 17 | +| Weather | `weather` | Current conditions at a focus | | |
| 18 | + | |
| 19 | +Each is added once per query: add the Transit target twice and you get two | |
| 20 | +stops, each choosing its own query when Smartspacer opens the setup screen. | |
| 21 | + | |
| 22 | +## Setting it up | |
| 23 | + | |
| 24 | +1. In Apollo's Crib, go to `cfg/ankyra` and note the rabbit user's **username**, | |
| 25 | + **password** and **topic**. Leave auto-registration on, or add the client id | |
| 26 | + the app shows you to that Ankyra's client ids -- the broker will refuse the | |
| 27 | + connection otherwise, and Ankyra counts its consumers by that id. | |
| 28 | +2. Point a Pythiae at the vision you want and give it that Ankyra. | |
| 29 | +3. Install this app, fill in the broker host, port (1883 for the RabbitMQ MQTT | |
| 30 | + plugin) and those credentials, and connect. The screen shows the queries as | |
| 31 | + they arrive. | |
| 32 | +4. In Smartspacer: Targets, add, Apollo's Crib. Pick the query the target shows. | |
| 33 | + | |
| 34 | +The connection is a foreground service with an ongoing notification. That is | |
| 35 | +what an always-open socket costs on Android; the alternative is polling, which | |
| 36 | +this deliberately does not do. | |
| 37 | + | |
| 38 | +## Building | |
| 39 | + | |
| 40 | +```sh | |
| 41 | +echo "sdk.dir=$ANDROID_HOME" > local.properties | |
| 42 | +./gradlew :app:assembleDebug :app:testDebugUnitTest | |
| 43 | +``` | |
| 44 | + | |
| 45 | +The debug APK is large (~36 MB) because HiveMQ's MQTT client brings Netty and | |
| 46 | +RxJava. Release builds have optimization switched off in `app/build.gradle.kts` | |
| 47 | +for now -- turning it on wants the SDK keep rule from the Smartspacer wiki. | |
| 48 | + | |
| 49 | +## The wire format | |
| 50 | + | |
| 51 | +The payload is one JSON object per tick, keyed `"<type>-<queryId>"`: | |
| 52 | + | |
| 53 | +```json | |
| 54 | +{ | |
| 55 | + "gtfs-12": { | |
| 56 | + "data": [ | |
| 57 | + {"route": "14", "route_name": "14", "dest": "Heath St", "dir": "Inbound", | |
| 58 | + "mode": "Bus", "times": ["08:00:00"], "color": "#FFC72C"} | |
| 59 | + ], | |
| 60 | + "query": {"name": "Forest Hills", "meta": {}} | |
| 61 | + } | |
| 62 | +} | |
| 63 | +``` | |
| 64 | + | |
| 65 | +A publisher that could not resolve the query sends the bare list instead of the | |
| 66 | +`{data, query}` wrapper; both are read. | |
| 67 | + | |
| 68 | +**[`apollos-types`](https://github.com/neiam/apollos-types) is the source of | |
| 69 | +truth for these shapes.** The Kotlin types in `types/` mirror that crate -- | |
| 70 | +when a field is added there, add it here. Unknown fields are ignored rather | |
| 71 | +than fatal, so a newer publisher does not break an older phone. | |
| 72 | + | |
| 73 | +## Adding a source | |
| 74 | + | |
| 75 | +The seam is deliberate. Weather is in here as the worked example of how small | |
| 76 | +it should be: | |
| 77 | + | |
| 78 | +1. A type in `types/`, mirroring the crate's struct. | |
| 79 | +2. An entry in `SourceType`. | |
| 80 | +3. A `SourceRenderer` in `targets/`, which is the only place that decides what | |
| 81 | + the thing looks like. | |
| 82 | +4. A four-line `ApollosTargetProvider` subclass, added to `Targets`. | |
| 83 | +5. A `<provider>` in the manifest with its own authority. | |
| 84 | + | |
| 85 | +Everything else -- the subscription, the store, staleness, dismissal, binding a | |
| 86 | +target to a query -- is already handled and does not need touching. | |
| 87 | + | |
| 88 | +## Look | |
| 89 | + | |
| 90 | +The palette and typography are lifted from the DMS Android app: the same OKLCh | |
| 91 | +themes ("Her", "After Dark", "Forest" ...) that the Scribus public routes render | |
| 92 | +under, and B612 for text. The Smartspace targets themselves are drawn by the | |
| 93 | +launcher and are not ours to type. |
added
clients/android/app/build.gradle.kts
+64
−0
| 1 | +plugins { | |
| 2 | + alias(libs.plugins.android.application) | |
| 3 | + alias(libs.plugins.kotlin.compose) | |
| 4 | + alias(libs.plugins.kotlin.serialization) | |
| 5 | +} | |
| 6 | + | |
| 7 | +android { | |
| 8 | + namespace = "io.neiam.apolloscrib" | |
| 9 | + compileSdk { | |
| 10 | + version = release(36) { | |
| 11 | + minorApiLevel = 1 | |
| 12 | + } | |
| 13 | + } | |
| 14 | + | |
| 15 | + defaultConfig { | |
| 16 | + applicationId = "io.neiam.apolloscrib" | |
| 17 | + // Smartspacer itself requires 12, and the foreground service type | |
| 18 | + // declarations below are 14-era. 31 keeps the manifest honest. | |
| 19 | + minSdk = 31 | |
| 20 | + targetSdk = 36 | |
| 21 | + versionCode = 1 | |
| 22 | + versionName = "0.1.0" | |
| 23 | + } | |
| 24 | + | |
| 25 | + buildTypes { | |
| 26 | + release { | |
| 27 | + optimization { | |
| 28 | + enable = false | |
| 29 | + } | |
| 30 | + } | |
| 31 | + } | |
| 32 | + compileOptions { | |
| 33 | + sourceCompatibility = JavaVersion.VERSION_17 | |
| 34 | + targetCompatibility = JavaVersion.VERSION_17 | |
| 35 | + } | |
| 36 | + buildFeatures { | |
| 37 | + compose = true | |
| 38 | + } | |
| 39 | + packaging { | |
| 40 | + resources { | |
| 41 | + excludes += "META-INF/INDEX.LIST" | |
| 42 | + excludes += "META-INF/io.netty.versions.properties" | |
| 43 | + } | |
| 44 | + } | |
| 45 | +} | |
| 46 | + | |
| 47 | +dependencies { | |
| 48 | + implementation(libs.smartspacer.sdk.plugin) | |
| 49 | + implementation(libs.hivemq.mqtt.client) | |
| 50 | + implementation(libs.kotlinx.serialization.json) | |
| 51 | + | |
| 52 | + implementation(platform(libs.androidx.compose.bom)) | |
| 53 | + implementation(libs.androidx.activity.compose) | |
| 54 | + implementation(libs.androidx.compose.material3) | |
| 55 | + implementation(libs.androidx.compose.ui) | |
| 56 | + implementation(libs.androidx.compose.ui.tooling.preview) | |
| 57 | + implementation(libs.androidx.core.ktx) | |
| 58 | + implementation(libs.androidx.lifecycle.runtime.ktx) | |
| 59 | + implementation(libs.androidx.lifecycle.runtime.compose) | |
| 60 | + | |
| 61 | + debugImplementation(libs.androidx.compose.ui.tooling) | |
| 62 | + testImplementation(libs.junit) | |
| 63 | + testImplementation(libs.kotlinx.serialization.json) | |
| 64 | +} |
added
clients/android/app/src/main/AndroidManifest.xml
+86
−0
| 1 | +<?xml version="1.0" encoding="utf-8"?> | |
| 2 | +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> | |
| 3 | + | |
| 4 | + <uses-permission android:name="android.permission.INTERNET" /> | |
| 5 | + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> | |
| 6 | + <!-- The Ankyra subscription is a socket held open while nothing else in | |
| 7 | + this app is running, which on modern Android is a foreground service. --> | |
| 8 | + <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> | |
| 9 | + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> | |
| 10 | + <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> | |
| 11 | + <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> | |
| 12 | + | |
| 13 | + <application | |
| 14 | + android:allowBackup="true" | |
| 15 | + android:icon="@mipmap/ic_launcher" | |
| 16 | + android:label="@string/app_name" | |
| 17 | + android:supportsRtl="true" | |
| 18 | + android:theme="@style/Theme.ApollosCrib"> | |
| 19 | + | |
| 20 | + <activity | |
| 21 | + android:name=".ui.MainActivity" | |
| 22 | + android:exported="true" | |
| 23 | + android:label="@string/app_name"> | |
| 24 | + <intent-filter> | |
| 25 | + <action android:name="android.intent.action.MAIN" /> | |
| 26 | + <category android:name="android.intent.category.LAUNCHER" /> | |
| 27 | + </intent-filter> | |
| 28 | + </activity> | |
| 29 | + | |
| 30 | + <!-- Opened by Smartspacer as both the setup and the config activity for | |
| 31 | + every one of the Targets below; which query it binds comes in as an | |
| 32 | + extra. Exported because Smartspacer is the one launching it. --> | |
| 33 | + <activity | |
| 34 | + android:name=".ui.ConfigurationActivity" | |
| 35 | + android:exported="true" | |
| 36 | + android:label="@string/configure" /> | |
| 37 | + | |
| 38 | + <service | |
| 39 | + android:name=".mqtt.AnkyraService" | |
| 40 | + android:exported="false" | |
| 41 | + android:foregroundServiceType="dataSync" /> | |
| 42 | + | |
| 43 | + <receiver | |
| 44 | + android:name=".mqtt.BootReceiver" | |
| 45 | + android:exported="true"> | |
| 46 | + <intent-filter> | |
| 47 | + <action android:name="android.intent.action.BOOT_COMPLETED" /> | |
| 48 | + </intent-filter> | |
| 49 | + </receiver> | |
| 50 | + | |
| 51 | + <!-- One provider per source type. Smartspacer keys everything off the | |
| 52 | + authority, so separate authorities are what let a user add transit | |
| 53 | + and bikeshare independently. All three must be exported, with this | |
| 54 | + action and this permission, or Smartspacer will not see them. --> | |
| 55 | + <provider | |
| 56 | + android:name=".targets.GtfsTargetProvider" | |
| 57 | + android:authorities="${applicationId}.target.gtfs" | |
| 58 | + android:permission="com.kieronquinn.app.smartspacer.permission.ACCESS_SMARTSPACER_TARGETS" | |
| 59 | + android:exported="true"> | |
| 60 | + <intent-filter> | |
| 61 | + <action android:name="com.kieronquinn.app.smartspacer.TARGET" /> | |
| 62 | + </intent-filter> | |
| 63 | + </provider> | |
| 64 | + | |
| 65 | + <provider | |
| 66 | + android:name=".targets.GbfsTargetProvider" | |
| 67 | + android:authorities="${applicationId}.target.gbfs" | |
| 68 | + android:permission="com.kieronquinn.app.smartspacer.permission.ACCESS_SMARTSPACER_TARGETS" | |
| 69 | + android:exported="true"> | |
| 70 | + <intent-filter> | |
| 71 | + <action android:name="com.kieronquinn.app.smartspacer.TARGET" /> | |
| 72 | + </intent-filter> | |
| 73 | + </provider> | |
| 74 | + | |
| 75 | + <provider | |
| 76 | + android:name=".targets.WeatherTargetProvider" | |
| 77 | + android:authorities="${applicationId}.target.weather" | |
| 78 | + android:permission="com.kieronquinn.app.smartspacer.permission.ACCESS_SMARTSPACER_TARGETS" | |
| 79 | + android:exported="true"> | |
| 80 | + <intent-filter> | |
| 81 | + <action android:name="com.kieronquinn.app.smartspacer.TARGET" /> | |
| 82 | + </intent-filter> | |
| 83 | + </provider> | |
| 84 | + | |
| 85 | + </application> | |
| 86 | +</manifest> |
added
clients/android/app/src/main/java/io/neiam/apolloscrib/data/Settings.kt
+106
−0
Click to load diff…
added
clients/android/app/src/main/java/io/neiam/apolloscrib/data/VisionStore.kt
+57
−0
Click to load diff…
added
clients/android/app/src/main/java/io/neiam/apolloscrib/mqtt/AnkyraClient.kt
+108
−0
Click to load diff…
added
clients/android/app/src/main/java/io/neiam/apolloscrib/mqtt/AnkyraService.kt
+157
−0
Click to load diff…
added
clients/android/app/src/main/java/io/neiam/apolloscrib/mqtt/BootReceiver.kt
+17
−0
Click to load diff…
added
clients/android/app/src/main/java/io/neiam/apolloscrib/targets/ApollosTargetProvider.kt
+107
−0
Click to load diff…
added
clients/android/app/src/main/java/io/neiam/apolloscrib/targets/GbfsRenderer.kt
+61
−0
Click to load diff…
added
clients/android/app/src/main/java/io/neiam/apolloscrib/targets/GtfsRenderer.kt
+97
−0
Click to load diff…
added
clients/android/app/src/main/java/io/neiam/apolloscrib/targets/Providers.kt
+21
−0
Click to load diff…
added
clients/android/app/src/main/java/io/neiam/apolloscrib/targets/Renderers.kt
+142
−0
Click to load diff…
added
clients/android/app/src/main/java/io/neiam/apolloscrib/targets/WeatherRenderer.kt
+40
−0
Click to load diff…
added
clients/android/app/src/main/java/io/neiam/apolloscrib/types/Gbfs.kt
+41
−0
Click to load diff…
added
clients/android/app/src/main/java/io/neiam/apolloscrib/types/Gtfs.kt
+53
−0
Click to load diff…
added
clients/android/app/src/main/java/io/neiam/apolloscrib/types/Weather.kt
+35
−0
Click to load diff…
added
clients/android/app/src/main/java/io/neiam/apolloscrib/types/Wire.kt
+103
−0
Click to load diff…
added
clients/android/app/src/main/java/io/neiam/apolloscrib/ui/ConfigurationActivity.kt
+129
−0
Click to load diff…
added
clients/android/app/src/main/java/io/neiam/apolloscrib/ui/MainActivity.kt
+252
−0
Click to load diff…
added
clients/android/app/src/main/java/io/neiam/apolloscrib/ui/theme/AppTheme.kt
+152
−0
Click to load diff…
added
clients/android/app/src/main/java/io/neiam/apolloscrib/ui/theme/CribTheme.kt
+34
−0
Click to load diff…
added
clients/android/app/src/main/java/io/neiam/apolloscrib/ui/theme/Typography.kt
+42
−0
Click to load diff…
added
clients/android/app/src/main/res/drawable/ic_alert.xml
+7
−0
Click to load diff…
added
clients/android/app/src/main/res/drawable/ic_bike.xml
+7
−0
Click to load diff…
added
clients/android/app/src/main/res/drawable/ic_crib.xml
+7
−0
Click to load diff…
added
clients/android/app/src/main/res/drawable/ic_launcher_foreground.xml
+9
−0
Click to load diff…
added
clients/android/app/src/main/res/drawable/ic_transit.xml
+7
−0
Click to load diff…
added
clients/android/app/src/main/res/drawable/ic_weather.xml
+7
−0
Click to load diff…
modified
?
+0
−0
Click to load diff…
modified
?
+0
−0
Click to load diff…
added
clients/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+5
−0
Click to load diff…
added
clients/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+5
−0
Click to load diff…
added
clients/android/app/src/main/res/values/ic_launcher_background.xml
+4
−0
Click to load diff…
added
clients/android/app/src/main/res/values/strings.xml
+5
−0
Click to load diff…
added
clients/android/app/src/main/res/values/themes.xml
+6
−0
Click to load diff…
added
clients/android/app/src/test/java/io/neiam/apolloscrib/types/WireTest.kt
+101
−0
Click to load diff…
added
clients/android/build.gradle.kts
+5
−0
Click to load diff…
added
clients/android/gradle.properties
+5
−0
Click to load diff…
added
clients/android/gradle/libs.versions.toml
+36
−0
Click to load diff…
modified
?
+0
−0
Click to load diff…
added
clients/android/gradle/wrapper/gradle-wrapper.properties
+8
−0
Click to load diff…
added
clients/android/gradlew
+251
−0
Click to load diff…
added
clients/android/gradlew.bat
+94
−0
Click to load diff…
added
clients/android/settings.gradle.kts
+26
−0
Click to load diff…
Parents: 86d2342