Fujimod
브라우저에서 카메라 펌웨어를 시험하고 Fujimod SD 부팅 파일을 만든다
저장소 정보는 아직 수집하지 않았습니다.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
RailSpotting Android app: a phone with an RTL-SDR dongle as a mobile receiver
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗An Android app that turns a phone into a RailSpotting mobile receiver. The owner signs in with Google, plugs an RTL-SDR dongle in with a USB OTG adapter, and starts listening. The phone then reports what the dongle hears, and where the phone is, to the RailSpotting headend under API contract v1.7.
The one dongle works in one of three modes, which the owner switches on the Activity tab:
Using it: the app reports to RailSpotting (railspotting.com), a service that is not open source. You need a RailSpotting account (sign in with Google) for the phone to be a receiver. This repository is the app and its Rust core; the service's API is described by the contract documents it publishes.
com.railspotting.androidhttps://api.railspotting.com/api/v1The contract and its server side live in the headend repository (docs/contract-v1.7.md,
docs/android-handoff.md, web/api/receiver-openapi.json).
The app is two parts:
| Part | Language | Does |
|---|---|---|
app/ | Kotlin | Google sign-in (Credential Manager), the Keystore, location (fused provider), the foreground service, USB permission for the dongle, live listen audio, the screens (Jetpack Compose) |
core/ | Rust | The receiver: heartbeats, the event and report outbox, GPS fix rules, request signing, and the dongle's processing |
native/ | C | The dongle driver: libusb and a patched librtlsdr, built with the NDK (see native/README.md) |
core/ builds on the fixed receiver's own crates, from the receiver/ submodule
(railspotting/Railspotting-Receiver):
proto: request signing and the API client;store: the SQLite outbox;dsp and engine: the SDR processing (SdrSource, SdrHandle), event detection, the EOT
decoder and the voice audio tap.Why this split. The radio side is most of what a receiver does, and it is subtle:
The fixed receivers run this code in production. Sharing it means a phone's events and EOT decodes are built exactly as a fixed receiver's and pass the same tests, and a fix reaches both. A Kotlin rewrite would mean two copies to keep in step.
The Android-only parts stay in Kotlin, because they need the platform's own APIs:
UsbManager.AudioTrack.The cost of the split is the build: it needs the NDK and cargo-ndk, and core/ and native/ must
build for each ABI. The Gradle build runs both steps itself.
The JNI surface is small (app/.../core/Native.kt, core/src/jni_api.rs). It passes JSON
text, so no models are duplicated.
The dongle. Android lets an app list no USB devices. It gets a file descriptor for a dongle
the owner allowed. Kotlin asks for permission, opens the dongle and passes the descriptor to the
core. The receiver's engine::rtl then opens it through rtlsdr_open_sys_dev, a small patch to
librtlsdr (native/README.md).
Kept few, all from Google or JetBrains:
core-ktx, activity-compose, lifecycle-runtime-compose, Compose UI and Material 3
(through the Compose BOM);androidx.credentials and credentials-play-services-auth, plus Google's googleid library,
for sign-in;play-services-location for the fused location provider;com.google.android.play:integrity), asked once when a phone links: the headend
checks Google's verdict to decide how far to trust a new phone (proposed contract v1.14). It
never blocks linking; builds from source link too, and the headend marks them as such;androidx.webkit, to open the live map on the phone's position (a script that runs before the
page's own).There is no JSON or HTTP library on the Kotlin side: Android's org.json is enough, and the core
makes every headend call. The Rust side adds only jni to what the receiver already uses. The
dongle driver, libusb and librtlsdr, is built from pinned source in native/.
| Path | What |
|---|---|
app/src/main/java/com/railspotting/android/ | the app: signin/, service/ListenService.kt, usb/, location/, store/ (Keystore, settings), ui/ |
app/src/test/ | Kotlin unit tests (JVM) |
core/src/ | the Rust core: agent.rs (the receiver loop), radio.rs (the dongle's modes and live listen), location.rs (fix rules), link.rs (hello, link, clock), jni_api.rs |
core/tests/ | the core end to end against a fake mobile headend and a simulated dongle (the receiver's radio simulator); the headend's signing vectors |
native/ | the dongle driver: libusb and librtlsdr (submodules), the librtlsdr patch, build.sh |
receiver/ | git submodule: the RailSpotting receiver (Rust workspace) |
You need:
29.0.14206865;cargo-ndk.On a Mac with Homebrew:
brew install openjdk@17
brew install --cask android-commandlinetools
sdkmanager --sdk_root="$HOME/Library/Android/sdk" --licenses
sdkmanager --sdk_root="$HOME/Library/Android/sdk" "platform-tools" "platforms;android-36" "ndk;29.0.14206865"
rustup target add aarch64-linux-android x86_64-linux-android
cargo install cargo-ndk --locked
Clone with the submodule, and point Gradle at the SDK:
git clone --recurse-submodules [email protected]:railspotting/Railspotting-Android.git
cd Railspotting-Android
echo "sdk.dir=$HOME/Library/Android/sdk" > local.properties
Build and install a debug build on a phone connected with USB debugging:
./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk
Before every build, two tasks build the native code for each ABI:
cargoNdk builds core/ into app/build/rustJniLibs;sdrDriver builds libusb and the patched librtlsdr into app/build/sdrJniLibs.Both are quick when nothing has changed.
Release builds are signed with the upload key named in keystore.properties at the repository
root. That file is git-ignored; never commit it or the key. Make the key once:
keytool -genkeypair -v -keystore railspotting-upload.jks -alias upload -keyalg RSA -keysize 4096 -validity 10000
Then write keystore.properties:
storeFile=railspotting-upload.jks
storePassword=...
keyAlias=upload
keyPassword=...
The same four values can come from the environment instead: RS_KEYSTORE, RS_KEYSTORE_PASSWORD,
RS_KEY_ALIAS, RS_KEY_PASSWORD. Then:
./gradlew assembleRelease # app/build/outputs/apk/release/
./gradlew bundleRelease # app/build/outputs/bundle/release/, for Google Play
Keep the key and its passwords in a password manager, with a backup. Phones that installed a release only accept updates signed with the same key: lose it and every phone has to uninstall and reinstall. If the app ever goes on Google Play, use Play App Signing there.
The app updates itself from this repository's GitHub releases, without a store
(app/src/main/java/com/railspotting/android/update/). Each release carries the signed APK and
latest.json (version code and name, the APK's address, its SHA-256, notes). Release builds
look at releases/latest/download/latest.json once a day and from the menu's Check now. A newer
version is downloaded, checked (SHA-256, package name, version, and the same signing key as the
installed app) and installed through Android's installer, never while listening. Android asks the
owner to confirm the first update; later ones install by themselves (Android 12 and later).
Debug builds never update themselves.
To publish a release (the version step and notes):
tools/release.sh patch "What changed"
Releases take the smallest step: a fix or a small feature is a patch release (0.3.0 to 0.3.1).
minor and major are for when the owner decides a release is that big; an exact version
(1.2.3) also works. The version code goes up by one each time (app/version.properties). The repository must be
public for phones to download releases.
Google sign-in on a phone needs an Android OAuth client in the same Google Cloud project
as the website's web OAuth client. The app asks Credential Manager for a token for the headend's
web client ID, which it gets from GET /mobile/hello. Google issues that token only to an app
whose package name and signing certificate are registered in the project as an Android client.
Nothing in the app's code changes.
com.railspotting.android.Debug (each developer machine's debug key, for development builds):
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android | grep SHA1
The debug keystore is made by the first debug build.
Release upload key, for builds you sign yourself:
keytool -list -v -keystore railspotting-upload.jks -alias upload | grep SHA1
Play App Signing, if the app is on Google Play: copy the SHA-1 of the app signing key from Play Console (Test and release > App integrity > App signing).
Until the Android client exists, sign-in fails with a Credential Manager error ("developer console is not set up correctly" or similar). That error comes from Google, not from the headend.
The core, on the development machine (no phone needed):
cd core && cargo test
These cover:
internal/sig);403 revoked;X-RS-Server-Time;429 Retry-After);mobile block;The fake headend (core/tests/common) follows the headend's mobile_test.go.
The app's unit tests (JVM):
./gradlew testDebugUnitTest
These cover:
There is no sandbox headend. Before linking against production, ask the owner for a test Google account. A new mobile receiver is provisional and private, so testing doesn't touch the public map.
Signing in shows the terms and privacy links from /mobile/hello first. It makes the
receiver's Ed25519 key once per install and links with POST /mobile/link.
403 revoked).Listening runs in a foreground service with a notification and a Stop button. Nothing runs outside it: no radio, no location and no headend calls.
mobile.heartbeat_interval_s, uploads events, and runs the
dongle.Listening means the dongle is connected and streaming: the contract's "a scanner is connected and scanning", with the dongle in the scanner's place. GPS is on only then:
location_min_move_m moved, and at least every
location_interval_s, both from /config;Location permission: precise location, with an explanation before the prompt. No background location permission is needed: the service is started from the screen with the location type, so it keeps location access with the screen off.
Offline:
429 and Retry-After are honoured.Sharing is a switch, on by default, sent in every heartbeat. With it off, nothing the phone hears reaches the public map.
The dongle runs the receiver's own SDR code in one of three roles:
eot): the plan's EOT channel, with packets decoded on the phone.vhf): the plan's voice channels that fit the dongle's 2.4 MHz.itc): 220.3 MHz at 1.024 MS/s. It starts on the national channel (102), where base
stations send beacons, and adds the local channel of each base it hears. The receiver's ITC
decoder runs on the phone. Signal states, changes and base stations show on the Activity tab;
two days of messages are kept in the app's storage (). Nothing ITC goes to the headend:
the mobile contract (v1.7) has no place for it, and the heartbeat lists the dongle with no
role, as a spare. The receiver's local ITC feed is off on the phone, since any app could reach
a port on 127.0.0.1.The app's own source is licensed under either of the Apache License 2.0 (LICENSE-APACHE) or the
MIT license (LICENSE-MIT), at your option, like the receiver it builds on.
The APK also ships librtlsdr (GNU GPL 2.0 or later), with a small patch, and libusb (GNU
LGPL 2.1 or later) as their own shared libraries; the receiver only loads librtlsdr from the
system. Because the APK combines GPL code with the app, the APK as a whole is distributed under
the GNU GPL version 3, which these licences all permit (NOTICE).
app/src/main/assets/licenses). The list is generated: run python3 tools/open_source.py after
changing dependencies (a test checks it is complete).Never commit:
keystore.properties;google-services.json; the app doesn't use one;Tests use made-up values ([email protected], rs-0a1b2c3d4e5f, the contract's published test
key). Commits use the GitHub noreply address.
itc/The Activity tab switches modes, and the dongle reopens in the new role at once. There is no speech transcription on the phone. Burst recordings are off, to save storage.
Activity lists what the phone heard since listening started, newest first, with EOT packets decoded. Calls on the air are pulled to the top.
Train alerts come in a high-importance "Trains" notification while listening; both are on by default, and the Activity tab turns them off.
/api/public/regions, /api/public/state)
for the regions within the radius, and alerts once on each new EOT alert whose crossing (or
receiver) is within it. The first look at a region notes what is there without alerting. The
phone's position is compared on the phone and never sent: it is the newest fix, or the
provider's last known position.The channel list is what /config gives this receiver.
/location answers used ≥ 1), the app fetches /config
at once, rather than at the next heartbeat.docs/contract-v1.9.md in
the headend repository).
Every /location answer carries config_version; when it differs, the app fetches /config
at once. A second refresh within 30 s is limited by the headend, and the app says so.Android Auto (Car App Library, points-of-interest category):
Auto allows only its own templates in the car, so the web live map is not there. Test builds
accept any car host; release builds only Google's Android Auto (res/values/car_hosts.xml). To
try it without a car, use Google's Desktop Head Unit:
adb forward tcp:5277 tcp:5277, then run
$ANDROID_HOME/extras/google/auto/desktop-head-unit (SDK package extras;google;auto).Live listen works in road-channel mode, while the Activity tab is open. It plays the call on the air. A call plays to its end, then the newest call still on the air takes over; between calls the speaker is quiet.
Sign out stops listening, forgets the key and device ID, and clears the receiver's folder. The receiver stays on the account.