Turn a spare Android phone into a network-discoverable SiLA 2 laboratory instrument.
Why · Features · Quick start · Connect a client · Cloud · Architecture · Building · Troubleshooting
That drawer full of retired Android phones is actually a stack of battery-backed sensor nodes — each with an IMU, light and pressure sensors, a camera, a speaker, a torch and a Wi-Fi radio. Silaphone runs a full SiLA 2 v1.1 server directly on the phone and exposes that hardware as standard SiLA Features over gRPC — announced on the lab network via mDNS, or dialled out to a SiLA 2 cloud gateway for access from anywhere. Instantly usable from any SiLA 2-compliant client, scheduler or orchestration platform. No custom drivers, no vendor lock-in, no extra hardware.
- Standards-first — speaks plain SiLA 2 v1.1: feature definitions in FDL, gRPC transport, zeroconf discovery per SiLA 2 Part B. If your stack talks SiLA, it talks to Silaphone with zero integration work.
- Hardware you already own — ambient monitoring, incubator light checks, "did the centrifuge actually spin" vibration sensing, timelapse snapshots of a plate reader: a €0 phone covers a surprising amount of lab-monitoring ground.
- Self-sufficient node — runs as a foreground service, survives screen-off, can auto-start on boot, and keeps its configuration (name, port, enabled features) across restarts. Plug it into a charger and forget it.
- Works beyond the LAN — alongside local mDNS discovery, Silaphone can register with a SiLA 2 cloud gateway in server-initiated mode, so a phone behind NAT or a firewall shows up as an online instrument with no inbound ports. See Cloud connectivity.
- Honest feature discovery — only sensors that physically exist on the device are registered as SiLA Features, and each one can be toggled in the dashboard before the server starts.
Sensor features stream their values as observable properties; hardware features expose commands.
| Feature | Exposes | SiLA type | Unit |
|---|---|---|---|
LightSensor |
AmbientLight (observable property) |
Real | lx |
Accelerometer |
Acceleration X/Y/Z (observable property) |
Structure of Real | m/s² |
Gyroscope |
RotationRate X/Y/Z (observable property) |
Structure of Real | rad/s |
Magnetometer |
MagneticField X/Y/Z (observable property) |
Structure of Real | µT |
Barometer |
Pressure (observable property) |
Real | hPa |
ProximitySensor |
Distance (observable property) |
Real | cm |
Temperature |
AmbientTemperature (observable property) |
Real | °C |
Battery |
BatteryLevel (observable property) |
Integer | % |
Location |
Coordinates lat/lon/alt (observable property) |
Structure of Real | °, m |
Camera |
GrabSnapshot(Zoom) → JPEG image |
Command → Binary | — |
Flashlight |
SetFlashlightState(Enabled) + FlashlightActive |
Command + observable property | — |
AlarmSpeaker |
PlayAlarm(DurationMs) |
Command | — |
The standard SiLAService core feature (org.silastandard/core/SiLAService/v1) is
always present: server UUID, name, type, version, vendor URL, implemented features,
GetFeatureDefinition and SetServerName.
- Install the app on the phone (build from source below) and join the lab Wi-Fi network.
- Optionally adjust the server name, port (default
50052) and the set of enabled features in the dashboard. - Tap Start Server. The dashboard shows the IP address, port and server UUID a SiLA client needs, plus a live log and the number of connected clients.
- Leave the phone on a charger. The server runs as a foreground service; enable auto-start on boot and the battery-optimization exemption for unattended, reboot-proof operation.
The phone announces itself via mDNS/DNS-SD as _sila._tcp (service name = server
UUID, with server_name, description and version TXT records), so discovery-aware
clients find it without any manual configuration.
The fastest demo: install the sister app Silaphone Client on a second phone — it discovers the server automatically, browses every feature and runs commands from a touch UI.
From Python, using the sila2 library:
from sila2.client import SilaClient
# Silaphone currently serves plaintext gRPC → connect in insecure mode
client = SilaClient("192.168.1.42", 50052, insecure=True)
print(client.SiLAService.ServerName.get())
# Stream the ambient light sensor
for value in client.LightSensor.AmbientLight.subscribe():
print(f"{value} lx")Any other SiLA 2-compliant tooling (Java/C#/JS implementations, LIMS connectors, workflow orchestrators) works the same way — point it at the IP and port from the dashboard, or let it discover the server via zeroconf.
SiLA 2 v1.1 defines a server-initiated connection mode for instruments that live behind NAT or a firewall: instead of waiting to be discovered on the LAN, the server dials out to a cloud gateway, which then multiplexes all client traffic back over that single outbound connection. Silaphone speaks this mode natively — point it at a UniteLabs-style gateway and the phone appears as an online instrument from anywhere, with no inbound ports, port-forwarding or VPN.
Configure it in the dashboard's server settings under Cloud gateway. The fields mirror the connector environment variables you may already use:
| Setting | Connector env var | Example |
|---|---|---|
| Hostname | CLOUD_SERVER_ENDPOINT__HOSTNAME |
21f5e10e-…-91ca37f4a619.dev.unitelabs.io |
| Port | CLOUD_SERVER_ENDPOINT__PORT |
443 |
| Use TLS | CLOUD_SERVER_ENDPOINT__TLS |
on |
Enable the toggle, enter the gateway hostname, and start the server — it registers itself with the gateway automatically. Cloud mode runs alongside the LAN server (SiLA 2 v1.1 allows both connection modes at once), reconnects with exponential backoff if the link drops, and reports its status (Connecting / Connected / retrying) on the dashboard with activity in the shared console log.
Authentication — most gateways authorize a server by its registered UUID, so plain
TLS with no client certificate is enough to connect. For deployments that require
mutual TLS, paste a client certificate chain, its PKCS#8 private key
(-----BEGIN PRIVATE KEY-----; convert a PKCS#1 key with
openssl pkcs8 -topk8 -nocrypt) and, optionally, a custom CA certificate into the same
settings. The private key never leaves the phone — it is excluded from cloud backups.
flowchart LR
subgraph Phone["Android phone — Silaphone"]
HW["Sensors · Camera<br/>Torch · Speaker"] --> FEAT["SiLA Feature<br/>implementations"]
FEAT --> REG["Shared call<br/>registry"]
REG --> GRPC["gRPC server<br/>(Netty) :50052"]
REG --> CLOUD["Cloud client<br/>(OkHttp · TLS/mTLS)"]
SVC["Foreground service"] -. owns .-> GRPC
GRPC -. announces .-> MDNS["mDNS / DNS-SD<br/>_sila._tcp"]
end
MDNS -.-> NET(("Lab network"))
GRPC <--> NET
NET <--> CLIENTS["SiLA 2 clients<br/>Silaphone Client · sila2 Python · orchestrators"]
CLOUD == "dials out :443" ==> GW(("Cloud gateway"))
GW <--> REMOTE["Remote SiLA 2 clients<br/>LIMS · orchestrators"]
A few implementation notes for the curious:
- Service-owned server — the foreground service owns the gRPC server; the Compose dashboard is purely a control surface. Closing the UI does not stop the instrument.
- Lean protobuf layer — SiLA messages are marshalled with a hand-rolled, unit-tested protobuf wire-format writer/reader instead of protoc code generation.
- Tuned Netty transport —
grpc-netty-shadedwith NIO event loops configured for Android, 30 s keep-alive, and graceful fallback to gRPC-managed loops. - Two ways in, one set of handlers — a shared call registry lets the LAN gRPC
server and the outbound cloud client invoke the very same feature handlers, so cloud
mode adds no duplicate logic. The cloud link is a
grpc-okhttpchannel that dials the gateway'sConnectSiLAServerbidirectional stream and reconnects with backoff. - Live subscriber bookkeeping — per-feature client subscriptions are tracked, and new subscribers immediately receive the last cached sensor value.
- Persistent configuration — server name, port, enabled features and the auto-start flag are stored with Jetpack DataStore.
- JDK 17+ — the Gradle wrapper downloads everything else
- (or simply Android Studio, which bundles both)
./gradlew :app:assembleDebug # APK → app/build/outputs/apk/debug/
./gradlew :app:testDebugUnitTest # unit tests (Robolectric + Roborazzi screenshots)Or open the project in Android Studio and press Run.
Release APKs are signed from environment variables — no keys live in the repo:
export KEYSTORE_PATH=/path/to/keystore.jks
export STORE_PASSWORD=...
export KEY_PASSWORD=...
./gradlew :app:assembleRelease| Permission | Why |
|---|---|
INTERNET, ACCESS_WIFI_STATE |
gRPC server and IP address display |
CHANGE_WIFI_MULTICAST_STATE |
mDNS service announcement |
CAMERA |
Camera and Flashlight features |
ACCESS_FINE_LOCATION / ACCESS_COARSE_LOCATION |
Location feature (GPS) |
FOREGROUND_SERVICE*, WAKE_LOCK, POST_NOTIFICATIONS |
keep the server alive with a visible notification |
RECEIVE_BOOT_COMPLETED |
optional auto-start after reboot |
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS |
optional exemption for unattended operation |
Camera, GPS and autofocus are declared as optional hardware — the app installs and runs on devices without them; the corresponding features simply aren't registered.
- Transport security: the LAN listener currently serves plaintext gRPC (h2c) — connect with your client's insecure/unencrypted mode; TLS for the LAN listener is on the roadmap. The cloud connection runs over TLS, with optional mutual-TLS client certificates, as the gateway requires.
- Discovery: implemented per SiLA 2 Part B (service type
_sila._tcp, service name = server UUID). - Cloud connectivity: server-initiated mode per SiLA 2 Part A, registering with a
gateway over the
CloudClientEndpointbidirectional stream.
- Server isn't discovered — many lab/guest Wi-Fi networks enable AP/client isolation or filter multicast, which breaks mDNS. The server still works: connect by the IP and port shown on the dashboard.
- Server stops after a while — exempt the app from battery optimization (offered in settings) and keep the phone on a charger. Some vendors (Xiaomi, Huawei, …) have additional aggressive task killers that need per-app exceptions.
- A sensor feature is missing — Silaphone only registers features for hardware that actually exists on the device; check the feature toggles on the dashboard.
- The cloud connection won't establish — confirm the gateway hostname and port
(usually
443) and that Use TLS matches what the gateway expects. Watch the dashboard status and console log: repeated retries usually mean a wrong hostname, a TLS mismatch, or that the server's UUID isn't registered on the gateway yet.
| Repository | Role |
|---|---|
| Silaphone (this repo) | SiLA 2 server — the phone is the instrument |
| Silaphone Client | SiLA 2 client — discover, inspect and drive any SiLA 2 server from a phone |
Together they make a self-contained, two-phone SiLA 2 demo lab — handy for teaching the standard, testing integrations, or smoke-testing your network setup before real instruments arrive.
Issues and pull requests are very welcome — especially real-world reports from lab
networks, additional sensor features, and TLS for the LAN listener. Please run
./gradlew :app:testDebugUnitTest before submitting.
- The SiLA Consortium for the open SiLA 2 standard.
- Everyone keeping old phones out of landfills, one lab sensor at a time. 🥽

