A full-featured Android native Java mirror of the OpenSynaptic/Gsyn Flutter dashboard.
Receives, decodes, stores, visualizes, and sends Gsyn binary protocol packets — all on-device.
- Features
- Architecture Overview
- Protocol Layer
- Data Layer
- Transport Layer
- Rules Engine
- UI Layer
- Google Maps Setup
- Getting Started
- Build & Test
- Release & CI/CD
- Configuration Reference
- Differences from Flutter Source
- Unit Tests
- Contributing
| Document | Audience | Contents |
|---|---|---|
| docs/GETTING_STARTED.md | All developers | Setup, project structure, first run |
| docs/ARCHITECTURE.md | Mid–Senior | Layered design, threading, theming |
| docs/DATA_FLOW.md | All | UDP → decode → DB → UI complete trace |
| docs/PROTOCOL.md | Protocol devs | Binary packet format, CRC, Base62 |
| docs/DASHBOARD_CARDS.md | UI contributors | Card system, drag reorder, custom sensors |
| docs/UI_PATTERNS.md | Junior devs / students | ViewBinding, theming, i18n, RecyclerView |
| docs/CONTRIBUTING.md | Contributors | Branch convention, checklist, pitfalls |
| Feature | Detail |
|---|---|
| 📡 UDP Transport | Bidirectional UDP socket — listen on configurable port, send packets to any IP:port |
| 🔗 MQTT Transport | Eclipse Paho v3 client — subscribe/publish with TLS support |
| 🔒 Binary Protocol | Full Gsyn packet codec: CRC-8/CRC-16 validation, Base62 encoding, FULL/DIFF/HEART frames |
| 🗺️ Google Maps | Live device markers with online/offline colour coding, satellite/hybrid/normal layers |
| 📊 Real-time Charts | Native Canvas MiniTrendChartView — temperature & humidity trends |
| 🚨 Alerts | Three-level alert system (Info / Warning / Critical) with acknowledgement |
| ⚙️ Rules Engine | Threshold-based automation: create alerts, send commands, or log events |
| 🎨 Theming | Multiple accent colours + background presets, dark/light mode support |
| 🗄️ SQLite Persistence | Full local database: devices, sensor data, alerts, rules, operation logs |
| 📤 CSV Export | One-tap sensor history export |
┌─────────────────────────────────────────────────────────────┐
│ UI Layer │
│ MainActivity · DashboardFragment · DevicesFragment │
│ AlertsFragment · SendFragment · SettingsFragment │
│ MapMirrorFragment · HistoryMirrorFragment │
│ RulesMirrorFragment · HealthMirrorFragment │
└────────────────────────┬────────────────────────────────────┘
│ observes / calls
┌────────────────────────▼────────────────────────────────────┐
│ AppController (Singleton) │
│ Coordinates: Repository ↔ TransportManager ↔ RulesEngine │
└───────┬─────────────────┬──────────────────┬────────────────┘
│ │ │
┌───────▼──────┐ ┌───────▼──────┐ ┌───────▼──────┐
│ AppRepository│ │TransportMgr │ │ RulesEngine │
│ (SQLite CRUD)│ │(UDP + MQTT) │ │ (Thresholds) │
└───────┬──────┘ └───────┬──────┘ └──────────────┘
│ │
┌───────▼──────────────────▼──────┐
│ Protocol Layer │
│ PacketDecoder · PacketBuilder │
│ BodyParser · DiffEngine │
│ OsCmd · OsCrc · Base62Codec │
│ ProtocolConstants · Geohash │
└──────────────────────────────────┘
app/src/main/java/com/opensynaptic/gsynjava/core/protocol/
Every Gsyn wire packet follows this fixed header layout:
Byte 0 : CMD (command byte, see OsCmd)
Byte 1 : AID (source Application ID, uint8)
Byte 2 : TID (target Application ID, uint8)
Byte 3 : SEQ (sequence number, uint8)
Byte 4 : LEN (body length, uint8)
Bytes 5..N : BODY (variable, command-dependent)
Byte N+1 : CRC8 (CRC-8/SMBUS of bytes 0..N)
| Constant | Hex | Category | Description |
|---|---|---|---|
PING |
0x01 |
Control | Heartbeat request |
PONG |
0x02 |
Control | Heartbeat reply |
ID_REQUEST |
0x03 |
Control | Node requests an AID assignment |
ID_ASSIGN |
0x05 |
Control | Master assigns an AID to a node |
TIME_REQUEST |
0x07 |
Control | Node requests UNIX timestamp |
TIME_RESPONSE |
0x08 |
Control | Master sends 4-byte UNIX time |
HANDSHAKE_ACK |
0x09 |
Control | Handshake accepted |
HANDSHAKE_NACK |
0x0A |
Control | Handshake rejected |
SECURE_DICT_READY |
0x10 |
Secure | Encryption dictionary is ready |
DATA_FULL |
0x20 |
Data | Full sensor frame (all sensors) |
DATA_DIFF |
0x21 |
Data | Differential update frame |
DATA_HEART |
0x22 |
Data | Heartbeat data (reuses template) |
DATA_FULL_SENSOR |
0x23 |
Data | Single-sensor FULL frame |
Helper predicates: OsCmd.isDataCmd(cmd) · OsCmd.isSecureCmd(cmd) · OsCmd.normalizeDataCmd(cmd)
| Algorithm | Poly | Init | Use |
|---|---|---|---|
| CRC-8/SMBUS | 0x07 |
0x00 |
Packet integrity (every packet) |
| CRC-16/CCITT-FALSE | 0x1021 |
0xFFFF |
Secure payload validation |
Compact encoding for sensor values and timestamps embedded in packet bodies.
String encoded = Base62Codec.encode(value); // uint32 → ≤6 chars
long decoded = Base62Codec.decode(encoded);
String sv = Base62Codec.encodeSensorValue(25.6f, "°C"); // float → scaled Base62
String ts = Base62Codec.encodeTimestamp(System.currentTimeMillis()); // 8-char Base64urlParses DATA_FULL / DATA_FULL_SENSOR body bytes into List<SensorReading>.
Body format (DATA_FULL, one line per sensor):
[sensorId:2B][unit:1B][state:1B][value:Base62]\n
PacketDecoder.Result r = PacketDecoder.decode(rawBytes);
if (r.valid) {
byte cmd = r.meta.cmd;
List<SensorReading> readings = r.sensorReadings;
}PacketBuilder.buildPing(aid, tid, seq);
PacketBuilder.buildPong(aid, tid, seq);
PacketBuilder.buildIdRequest(aid, tid, seq);
PacketBuilder.buildIdAssign(aid, tid, seq, assignedId);
PacketBuilder.buildTimeRequest(aid, tid, seq);
PacketBuilder.buildDataFullSensor(aid, tid, seq, sensorId, unit, state, value);
PacketBuilder.buildRawHex("01 00 00 00 01 01 00");Template-based compression for DIFF and HEART frames.
FULL → stored as per-AID template
DIFF → only changed fields; engine reconstructs full reading list
HEART → no delta; engine re-emits the last FULL template
Canonical sensor IDs, unit codes, state codes, and defaultUnitFor(sensorId) lookup.
double[] latLng = GeohashDecoder.decode("wtw3ew5"); // → [31.23, 121.47]
app/src/main/java/com/opensynaptic/gsynjava/data/
| Model | Key Fields |
|---|---|
Device |
aid, name, status, lat, lng, lastSeenMs, transportType |
SensorData |
deviceAid, sensorId, value, unit, state, timestampMs |
AlertItem |
id, deviceAid, level (0/1/2), message, createdMs, acknowledged |
Rule |
id, sensorId, condition, threshold, action, enabled, cooldownMs |
OperationLog |
id, action, details, timestampMs |
SensorReading |
sensorId, unit, state, value — protocol-level (pre-DB) |
DeviceMessage |
meta + readings — fully decoded packet |
TransportStats |
udpConnected, mqttConnected, messagesPerSecond, totalMessages |
CREATE TABLE devices (aid INTEGER PRIMARY KEY, name TEXT, status TEXT,
lat REAL, lng REAL, last_seen_ms INTEGER, transport_type TEXT);
CREATE TABLE sensor_data (id INTEGER PRIMARY KEY AUTOINCREMENT,
device_aid INTEGER, sensor_id TEXT, value REAL,
unit TEXT, state INTEGER, timestamp_ms INTEGER);
CREATE TABLE alerts (id INTEGER PRIMARY KEY AUTOINCREMENT,
device_aid INTEGER, sensor_id TEXT, level INTEGER,
message TEXT, created_ms INTEGER, acknowledged INTEGER DEFAULT 0);
CREATE TABLE rules (id INTEGER PRIMARY KEY AUTOINCREMENT,
sensor_id TEXT, condition TEXT, threshold REAL,
action TEXT, enabled INTEGER DEFAULT 1, cooldown_ms INTEGER DEFAULT 60000);
CREATE TABLE operation_logs (id INTEGER PRIMARY KEY AUTOINCREMENT,
action TEXT, details TEXT, timestamp_ms INTEGER);// Devices
repo.upsertDevice(device);
repo.getAllDevices();
repo.getTotalDeviceCount();
repo.getOnlineDeviceCount();
// Sensor data (auto-trims data older than 7 days)
repo.insertSensorDataBatch(aid, readings);
repo.querySensorData(fromMs, toMs, limit);
repo.exportSensorDataCsv(fromMs, toMs);
// Alerts
repo.insertAlert(alert);
repo.acknowledgeAlert(alertId);
repo.getUnacknowledgedAlertCount();
repo.getAlerts(sensorId /*null=all*/, limit);
// Rules
repo.insertRule(rule); repo.updateRule(rule); repo.deleteRule(id);
repo.getEnabledRules();
// Audit log
repo.logOperation(action, details);
repo.getOperationLogs(limit);
app/src/main/java/com/opensynaptic/gsynjava/transport/TransportManager.java
Binds to 0.0.0.0:<port> on a dedicated background thread. Sends via DatagramSocket.
tm.startUdp(9876);
tm.sendUdp(bytes, "192.168.1.100", 9876);
tm.stopUdp();tm.startMqtt("tcp://broker:1883", "user", "pass");
tm.stopMqtt();Subscribes to gsyn/#. Publishes outgoing to gsyn/out/<aid>.
Raw bytes (UDP / MQTT)
→ PacketDecoder.decode() CRC validation
→ DiffEngine.process() DIFF/HEART reconstruction
→ AppRepository.upsertDevice()
→ AppRepository.insertSensorDataBatch()
→ RulesEngine.evaluate() threshold automation
→ MessageListener.onMessage() UI refresh callbacks
tm.addMessageListener(msg -> { /* DeviceMessage */ });
tm.addStatsListener(stats -> { /* TransportStats every 1 s */ });
tm.removeMessageListener(listener);
tm.removeStatsListener(listener);
app/src/main/java/com/opensynaptic/gsynjava/rules/RulesEngine.java
Evaluated automatically after every incoming data batch. Rule fields:
| Field | Values | Meaning |
|---|---|---|
sensorId |
"TEMP", "HUM", … |
Sensor to watch |
condition |
">" ">=" "<" "<=" "==" |
Comparison operator |
threshold |
double |
Trigger value |
action |
"create_alert" / "send_command" / "log_only" |
Effect |
cooldownMs |
long (default 60 000) |
Min ms between repeated triggers |
MainActivity (DrawerLayout)
├── BottomNavigationView
│ ├── Dashboard — KPI + trends + gauges
│ ├── Devices — list + search + detail sheet
│ ├── Alerts — filter by severity, ACK
│ ├── Send — command builder (3 tabs)
│ └── Settings — UDP / MQTT config
└── Side Drawer (in-app fragments)
├── Map — Google Maps device markers
├── History — 24h sensor table + CSV export
├── Rules — rule CRUD + toggle
└── Health — transport status + DB stats
chart.setTitle("Temperature");
chart.setChartColor(0xFFFF7043);
chart.setSeries(List.of(22f, 23.5f, 25f, 24f));Gradient fill, grid lines, peak/min highlight dots — all pure Canvas, no 3rd-party chart lib.
// In Activity.onCreate() BEFORE super.onCreate()
getTheme().applyStyle(AppThemeConfig.getAccentOverlayRes(accentPreset), true);
getTheme().applyStyle(AppThemeConfig.getBgOverlayRes(bgPreset), true);
// After setContentView
AppThemeConfig.applyBgToWindow(getWindow(), this);- Enable Maps SDK for Android
- Create or select an API Key
Add an Android app restriction with:
- Package name:
com.opensynaptic.gsynjava - SHA-1 (get your debug key):
keytool -list -v -keystore ~/.android/debug.keystore \ -alias androiddebugkey -storepass android -keypass android
local.properties (never commit):
MAPS_API_KEY=YOUR_KEY_HEREThe debug build intentionally omits the
.debugpackage suffix so the same key works for both debug and release builds.
| Tool | Minimum |
|---|---|
| Android Studio | Hedgehog 2023.1+ |
| JDK | 17 |
| Android SDK | API 24 |
| Target SDK | API 34 |
| Google Play Services | Required on device/emulator |
git clone https://github.com/ChrisLee0721/Gsyn-Java.git
cd Gsyn-JavaOpen in Android Studio → File → Open → select this folder.
Create local.properties:
sdk.dir=/path/to/Android/Sdk
MAPS_API_KEY=YOUR_KEY# Unit tests
.\gradlew.bat :app:testDebugUnitTest
# Debug APK
.\gradlew.bat :app:assembleDebug
# Install to device
.\gradlew.bat :app:installDebug✅ base62_roundtrip
✅ timestamp_roundtrip
✅ packet_build_and_decode
✅ body_parse_sensor
✅ diff_engine_full_heart_roundtrip
✅ diff_engine_clear
✅ protocol_constants_default_unit
✅ os_cmd_is_secure
git tag v1.2.0
git push origin v1.2.0 # triggers GitHub Actions → signed APK → GitHub ReleaseRequired GitHub Secrets:
| Secret | Description |
|---|---|
KEYSTORE_BASE64 |
Release keystore (Base64) |
KEYSTORE_PASSWORD |
Keystore password |
KEY_ALIAS |
Key alias |
KEY_PASSWORD |
Key password |
MAPS_API_KEY |
Google Maps API key |
See RELEASE.md for full instructions.
| Key | Default | Description |
|---|---|---|
udp_port |
9876 |
UDP listen port |
udp_enabled |
false |
Auto-start UDP on launch |
mqtt_broker |
"" |
Broker URL (tcp://host:1883) |
mqtt_user |
"" |
MQTT username |
mqtt_pass |
"" |
MQTT password |
mqtt_enabled |
false |
Auto-connect MQTT on launch |
theme_preset |
DEFAULT |
Accent colour |
bg_preset |
DEFAULT |
Background preset |
| Key | Default | Card |
|---|---|---|
dash_kpi_row1 |
true |
Total devices / Online rate |
dash_kpi_row2 |
true |
Active alerts / Throughput |
dash_kpi_row3 |
true |
Rules / Total messages |
dash_gauges |
true |
Water level / Humidity gauges |
dash_charts |
true |
Temperature / Humidity trends |
dash_activity |
true |
Recent alerts & operations |
dash_readings |
true |
Latest raw sensor readings |
| Aspect | Flutter Source | Java Mirror |
|---|---|---|
| DI / State | Riverpod Provider | Singleton (AppController) |
| Charts | fl_chart |
Native Canvas MiniTrendChartView |
| Maps | flutter_map + OSM |
Google Maps SDK for Android |
| MQTT | mqtt5_client |
Eclipse Paho MQTT v3 |
| Navigation | GoRouter | BottomNav + DrawerLayout |
| Multi-language | LocaleProvider |
strings.xml (Chinese default) |
- Fork → feature branch:
git checkout -b feat/your-feature - Follow Conventional Commits
- Open a Pull Request against
main
© OpenSynaptic — MIT License