Private, local-first electronic situational awareness for Android 9+ (Kotlin, Jetpack Compose).
Sotreus shows what the phone can observe over Bluetooth LE and Wi-Fi, learns what is normal for a place, remembers what you have encountered before, and explains why something may be worth a look — without claiming that a radio is a threat. Core awareness works with no account, no wallet and no network. Observations, places, labels and notes stay on the phone.
- Now — live radios as Proximity Bands (rings by 30 s average signal strength; position around a ring is never a direction; pinch to zoom until crowded dots separate) or a sortable list with freshness, stale state and Wi-Fi throttling surfaced.
- Entities — device-family classification, "most likely…" explanations, your labels (Mine, Expected, Tagged, Watch, Ignore), names, notes, addresses, raw evidence and a proximity check.
- Attention — explainable events with human-readable reasons and inputs. Journey movement evidence uses fresh, location-matched observations of the same broadcast address at separated locations, independently of whether the product is recognised. Not a threat score.
- Places — user-defined places with optional coordinates (current location or map pick), a radius (50–300 m) and a baseline learned from visits. While Sotreus is open, the place is picked from the phone's location: inside a saved place's radius you are at that place, and you leave it only once clearly outside, so small moves don't change it. On launch the radios wait for a new fix (up to 10 s) so nothing is tagged to where the app was last used. A place picked by hand holds while the phone stays near where you picked it.
- Sessions — Journey and Sit sessions with a live timeline, summaries and sit comparison. Journey movement can use phone location with the screen off while the session is active. Route saving is separate: without it, coordinates used for movement stay in memory; only evidence summaries persist. Both choices are explained in Privacy. The live session shows when accurate location is unavailable.
- Privacy — retention, coordinate masking, privacy-reduced exports, per-item deletion. Picking places by location stores no path. Opt-in plus-code tags (off by default) keep a ~14 m plus code of the phone's location on each observation while Sotreus is open.
- Vendor lookup — local IEEE and Bluetooth definitions, plus opt-in manual lookup through MACLookup. Only a public 24-bit vendor prefix is sent, together with the network connection (the provider sees the phone's IP address). No full radio address, location, name or note is sent. Private and unknown address types are excluded. Results show their source and date; positive results are cached for 30 days, missing vendors for one day. The local classification stays separate.
- Friends — QR pairing and opt-in Nearby presence using rotating anonymous BLE tokens.
- Context — optional sources, each labelled with where it came from and each switchable off:
- Satellites (PREDICTED) — orbital elements from CelesTrak (Earth observation, weather and space stations), downloaded once a day; what is passing overhead now and the next 24 h of passes are computed on the phone with SGP4. Predictions are checked in unit tests against Skyfield.
- Aircraft (NETWORK) — positions reported by OpenSky Network within a chosen radius. Coarse area asks for the 3° × 3° box around the 1° grid cell you are in and filters on the phone; Exact area asks for a box around the radius. Off by default. Polled once a minute while the Context tab is open and every 5 minutes during a session.
- Remote ID (SENSED, beta) — ASTM F3411 drone broadcasts decoded from the phone's own
Bluetooth (service data
FFFA) and Wi-Fi beacon (vendor IEFA:0B:BC) scans: UAS ID, maker, position, altitude, speed, course and operator position. Nothing is sent anywhere. - A 3D scene heads the tab, drawn on the phone with no map tiles: a dot-matrix Earth with the day/night terminator, every catalog satellite moving in real time (or as a ×60 / ×600 time-lapse) and lines of sight to those above your horizon; switching to Aircraft or Remote ID flies the camera down to a tilted plate around you with range rings, coastline, altitude stems, courses, trails, and drone operator positions. Drag to turn, pinch to zoom, tap an object for its details.
- Session summaries list context that overlapped the session in time — never as a cause.
- Solana Mobile devices — optional wallet sign-in (Sign In With Solana via Mobile Wallet Adapter) and salted-Merkle proof stamping on mainnet. Only a 32-byte commitment goes on-chain.
Requires JDK 17+ and the Android SDK (sdk.dir in local.properties or ANDROID_HOME).
Wallet authorization, balance, blockhash and confirmation use Solana mainnet only. Copy .env.example to .env
(or set the same name in local.properties) and put the full NOWNodes mainnet URL in
NOWNODES_SOLANA_RPC_URL, including a key in the path. Alternatively, set NOWNODES_SOLANA_RPC_URL=https://sol.nownodes.io
and NOWNODES_SOLANA_API_KEY for the documented api-key header. An empty configuration uses
https://api.mainnet-beta.solana.com. That URL is compiled into both APKs, so a build can reveal
a key embedded in it or in the header configuration. .env is gitignored. Set the same
Actions secrets for CI builds. Old devnet wallets must reconnect on mainnet; historical
receipts stay local and read-only. Mainnet transactions require wallet approval and pay
a real SOL network fee.
./gradlew test lint assembleGenericDebug assembleSolanaMobileDebug
Instrumented UI tests (emulator or device):
./gradlew connectedDebugAndroidTest
Both distributions use the application ID com.sotreus.app (the same ID is used on iOS). The
distribution flavors are build profiles — store and signing key — not feature sets:
| Flavor | Ships to | Signing |
|---|---|---|
generic |
Google Play (AAB) and a sideload APK | signing/generic.properties |
solanaMobile |
Solana dApp Store (APK) | signing/solanaMobile.properties |
Each signing/<flavor>.properties holds storeFile, storePassword, keyAlias and
keyPassword. The folder is git-ignored. Debug builds keep the debug key. A release build
fails until that flavor's properties file, or the SOTREUS_* env vars used in CI, is present.
The two release APKs cannot be installed side by side.
Create two different keys from the repo root. JDK 17+ writes a PKCS12 keystore; the .jks name
is only a filename. keytool prompts for the passwords and the certificate name. Back both files
up. Losing the Solana key blocks dApp Store updates. The generic key is the Play upload key; let
Google generate the Play app-signing key.
mkdir -p signing
keytool -genkeypair \
-v \
-keystore signing/sotreus-generic.jks \
-alias sotreus-generic \
-keyalg RSA \
-keysize 4096 \
-validity 25000
keytool -genkeypair \
-v \
-keystore signing/sotreus-solana-mobile.jks \
-alias sotreus-solana-mobile \
-keyalg RSA \
-keysize 4096 \
-validity 25000signing/generic.properties (PKCS12 keeps one password, so storePassword and keyPassword are the same value):
storeFile=signing/sotreus-generic.jks
storePassword=REPLACE
keyAlias=sotreus-generic
keyPassword=REPLACEsigning/solanaMobile.properties uses storeFile=signing/sotreus-solana-mobile.jks and
keyAlias=sotreus-solana-mobile. Then:
./gradlew :app:bundleGenericRelease :app:assembleGenericRelease :app:assembleSolanaMobileReleaseBump versionCode and versionName in app/build.gradle.kts before each store upload, and add
the release to CHANGELOG.md.
Pushing a v* tag runs .github/workflows/release.yml and publishes
Sotreus-<version>-generic.apk, Sotreus-<version>-solana-mobile.apk, and
Sotreus-<version>-generic.aab. A manual run publishes a prerelease tagged ci-<short sha>.
Add Actions secrets GENERIC_KEYSTORE_BASE64, GENERIC_STORE_PASSWORD, GENERIC_KEY_ALIAS,
GENERIC_KEY_PASSWORD, SOLANA_MOBILE_KEYSTORE_BASE64, SOLANA_MOBILE_STORE_PASSWORD,
SOLANA_MOBILE_KEY_ALIAS, and SOLANA_MOBILE_KEY_PASSWORD. Encode a keystore with
base64 -i signing/sotreus-generic.jks | tr -d '\n'.
git tag -a v1.1.1 -m "Sotreus 1.1.1" && git push origin v1.1.1Which auth and settings screens appear is decided at runtime by the device: Solana Mobile hardware (Saga, Seeker) gets the wallet and Proofs screens; every other device gets the local profile. Debug builds can force the Solana screens from Settings › Diagnostics.
| Module | Holds |
|---|---|
:app |
Application, activity, navigation shell, distribution bindings |
:core:model |
Android-free domain types |
:core:navigation |
Type-safe routes shared by all features |
:core:designsystem |
SotreusTheme, design tokens, bundled fonts, icons |
:core:ui |
Shared components (Bands canvas, rows, chips, panels, dialogs) |
:core:database |
Room schema, DAOs and migrations (schemas in core/database/schemas) |
:core:data |
Observation pipeline, repositories, settings, export, session service |
:core:crypto |
Deterministic CBOR, salted Merkle batches, presence tokens, Base58 |
:core:testing |
FakeSotreusData sample world for previews |
:intelligence:fieldwatch |
Ported Fieldwatch domain logic (MIT) |
:intelligence:core |
Classification, baselines, attention engine, proximity, compare |
:sensing:android |
BLE / Wi-Fi radios (ported from Fieldwatch), permissions, location, simulator |
:social:presence |
Nearby presence advertising and token resolution |
:integration:solana |
Mobile Wallet Adapter gateway, mainnet RPC, Memo transaction |
:context:core |
Context sources: OpenSky client, CelesTrak catalog, SGP4 pass prediction, Remote ID tracker |
:feature:* |
onboarding, now, entity, attention, history, place, journey, settings, friends, proofs, context |
Convention plugins live in build-logic/convention; versions in gradle/libs.versions.toml.
Movement currently requires at least three locations separated by 300 m plus GPS uncertainty, 600 m overall separation, eight minutes and twelve independently sampled observations. Gaps over two minutes reset continuity. Coordinates must have reported accuracy of 50 m or better and be within 20 seconds of the radio measurement. These conservative starting thresholds need field validation; they establish shared observations, not a device owner or intent. Address rotation starts a new entity. Evidence collected before a process restart is not stitched to a new run.
The release lookup snapshot is recorded in sensing/android/src/main/assets/lookups/radiodb.sources.json
with source URLs, counts and SHA-256 hashes. Refresh it explicitly before a release:
python3 tools/lookups/build_radio_db.py --download --source-dir /tmp/sotreus-radio-sources --date YYYY-MM-DD
python3 -m unittest discover -s tools/lookups -p 'test_*.py'
./gradlew :intelligence:fieldwatch:testUse the actual snapshot date for YYYY-MM-DD. Keep the source directory to reproduce the output
without --download; an unchanged input snapshot and date produce identical binaries. The tool
uses the official IEEE MA-L, MA-M, MA-S, IAB and CID CSVs, plus Nordic Semiconductor's Bluetooth
JSON definitions. It preserves the bundled official SIG appearance definitions where Nordic's
subset has no entry. Source failures and unexpected coverage reductions stop the update.
No database refresh happens silently on a user's phone.
Version 1.1.1 adds Settings → Field tools for separately opted-in seven-day tracker stop history, calibrated GNSS health, live phone paths and online street maps. Live paths do not enable route storage. Stop history is a separate coordinate store; unknown/rotating BLE addresses are excluded, and a stable broadcast identifier still does not establish a physical identity.
Session exports offer JSON, GeoJSON and ATAK Mission Packages. The default coarsens coordinates and times and salts identifiers per export. Radio hear points describe this phone's position; Remote ID advertised aircraft/operator positions are labelled separately and remain unverified.
Settings → Language packs offers 26 partial community translations and a Sotreus English template for local imports. Missing or incompatible entries stay English. Signatures supports reviewing/importing packs and restoring bundled catalog 98 without replacing vendor data. Online map consent is separate from manual vendor lookup consent. OpenFreeMap map areas and the requester's IP are visible to the map service when enabled; see its mobile integration guide.
See radio validation for the field test matrix and current limits.
Sotreus builds on the work below. Licences for bundled third-party material are listed in
NOTICE and licenses/.
- Fieldwatch — https://github.com/OffGridPete/Fieldwatch (MIT, © 2026 Off Grid Pete LLC). The field-tested foundation for Sotreus' radio layer. Ported with notices kept in each file: the BLE and Wi-Fi scanners and their scheduling around Android throttling, advertisement and Wi-Fi IE parsers, the device-family signature catalog and matching engine, plain-language device explanations, the relative-loudness proximity logic (Hunt), the live-sighting merge logic, the vendor/company lookup tables and Fieldwatch's own unit tests.
- Mobile Wallet Adapter — https://github.com/solana-mobile/mobile-wallet-adapter (specification) — wallet connection, Sign In With Solana and transaction signing.
- Solana Mobile documentation — https://github.com/solana-mobile/solana-mobile-docs
- Solana Kotlin Compose scaffold — https://github.com/solana-mobile/solana-kotlin-compose-scaffold (reference for MWA usage in Compose).
- dApp Store publishing — https://github.com/solana-mobile/dapp-publishing
- Sign In With Solana — https://github.com/phantom/sign-in-with-solana
-
OpenSky Network — https://opensky-network.org (REST API, GitHub) — aircraft state vectors, anonymous access. Matthias Schäfer et al., "Bringing up OpenSky", IPSN 2014.
-
CelesTrak — https://celestrak.org/NORAD/elements/ — orbital elements (GP data) by group.
-
predict4java — https://github.com/g4dpz/predict4java (MIT) — SGP4/SDP4 propagation and pass prediction on the phone.
-
Skyfield — https://github.com/skyfielders/python-skyfield — independent reference values for the prediction tests.
-
FAA Remote ID — https://www.faa.gov/uas/getting_started/remote_id (14 CFR Part 89), ASTM F3411 broadcast format; OpenDroneID — https://github.com/opendroneid/opendroneid-core-c — the reference message layout followed by the Fieldwatch decoder Sotreus uses.
-
Natural Earth — https://github.com/nvkelso/natural-earth-vector (public domain) — land and 1:50m coastlines for the Context globe, packed by
tools/geodata/build_globe_assets.py.
- MapLibre Native — https://github.com/maplibre/maplibre-native — the map view for places.
- OpenFreeMap — https://github.com/hyperknot/openfreemap — free vector tiles (no API key), built on OpenMapTiles. Map data © OpenStreetMap contributors. Tiles load only when you ask to see a map.
- Geist and Geist Mono — https://github.com/vercel/geist-font (SIL OFL 1.1)
- Instrument Serif — https://github.com/Instrument/instrument-serif (SIL OFL 1.1)
- Android Jetpack (Compose, Room, Navigation, WorkManager, DataStore) — https://github.com/androidx/androidx
- Dagger / Hilt — https://github.com/google/dagger
- kotlinx.serialization — https://github.com/Kotlin/kotlinx.serialization
- Bouncy Castle (X25519, Ed25519) — https://github.com/bcgit/bc-java
- OkHttp — https://github.com/square/okhttp
- ZXing — https://github.com/zxing/zxing and ZXing Android Embedded — https://github.com/journeyapps/zxing-android-embedded (friend QR codes)
- Now in Android — https://github.com/android/nowinandroid — the convention-plugin and modular feature layout follow its patterns.