Skip to content
NetSepioPublic

Repository files navigation

Sotreus

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.

What's in V1

  • 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 IE FA: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.

Build

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

Distributions

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.

Release signing

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 25000

signing/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=REPLACE

signing/solanaMobile.properties uses storeFile=signing/sotreus-solana-mobile.jks and keyAlias=sotreus-solana-mobile. Then:

./gradlew :app:bundleGenericRelease :app:assembleGenericRelease :app:assembleSolanaMobileRelease

Bump 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.1

Which 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.

Modules

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.

Radio evidence and lookup maintenance

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:test

Use 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.

References and inspiration

Sotreus builds on the work below. Licences for bundled third-party material are listed in NOTICE and licenses/.

Radio awareness

  • 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.

Solana Mobile

Context sources

Maps

Fonts

Libraries

Project structure

Releases

Packages

Contributors

Languages