From fde3f7cc7d80ad5b537bd207ab90cf4f5d250cba Mon Sep 17 00:00:00 2001 From: Agus Darmawan Date: Sun, 15 Nov 2026 16:33:02 +0700 Subject: [PATCH 1/4] feat(protocol): the robot's internet check in the state --- lib/protocol/messages.dart | 34 ++++++++++++++++++++++++++++++++++ test/protocol_test.dart | 14 ++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/lib/protocol/messages.dart b/lib/protocol/messages.dart index 7f14a51..3b158ec 100644 --- a/lib/protocol/messages.dart +++ b/lib/protocol/messages.dart @@ -193,6 +193,7 @@ class RobotState { this.load, this.speed, this.trip, + this.net, }); factory RobotState.fromJson(Map json) { @@ -235,6 +236,7 @@ class RobotState { load: SystemLoad.fromJson(health), speed: PatrolSpeed.fromJson(json['speed']), trip: TripStatus.fromJson(json['trip']), + net: NetStatus.fromJson(json['net']), ); } @@ -280,6 +282,9 @@ class RobotState { /// without the trip meter. final TripStatus? trip; + /// The robot's own internet (a ping through its modem); null when it does not check. + final NetStatus? net; + bool get remote => mode == 'remote'; bool get localized => localization.isNotEmpty && localization != 'none'; @@ -591,6 +596,35 @@ class PeopleStatus { final String error; } +/// The robot's internet check (`state.net`): a ping of [target] through its modem. +class NetStatus { + const NetStatus({required this.online, this.ms, this.loss = 0, this.target = ''}); + + /// Null unless the check reports whether the robot is online. + static NetStatus? fromJson(Object? json) { + if (json is! Map || json['online'] is! bool) return null; + return NetStatus( + online: json['online'] as bool, + ms: _num(json['ms']), + loss: _num(json['loss']) ?? 0, + target: _str(json['target']), + ); + } + + /// One of the last two pings was answered. + final bool online; + + /// Round trip of the last answer in ms; null offline. + final double? ms; + + /// Share of the last pings that got no answer, in percent. + final double loss; + final String target; + + /// Online, but slow or losing a quarter of the pings. + bool get poor => online && (loss >= 25 || (ms ?? 0) > 300); +} + /// The robot computer's load (`state.health`): percentages, memory in GB, hottest zone in C. class SystemLoad { const SystemLoad({this.cpu, this.gpu, this.ram, this.ramUsed, this.ramTotal, this.temp}); diff --git a/test/protocol_test.dart b/test/protocol_test.dart index b218686..9fb1fea 100644 --- a/test/protocol_test.dart +++ b/test/protocol_test.dart @@ -203,6 +203,20 @@ void main() { expect(PeopleStatus.fromJson({'state': 'loading'})!.count, isNull); }); + test('the internet check in the state', () { + final net = RobotState.fromJson({ + 'net': {'online': true, 'ms': 38.4, 'loss': 8, 'target': '8.8.8.8', 'age': 2.0}, + }).net!; + expect(net.online, isTrue); + expect(net.ms, 38.4); + expect(net.loss, 8); + expect(net.poor, isFalse); + expect(RobotState.fromJson({'net': null}).net, isNull); + expect(RobotState.fromJson({}).net, isNull); + expect(RobotState.fromJson({'net': {'ms': 3}}).net, isNull); + expect(NetStatus.fromJson({'online': true, 'ms': 450})!.poor, isTrue); + }); + test('the trip meter in the state', () { final trip = RobotState.fromJson({ 'trip': {'m': 1234.5, 's': 3725, 'v': 0.41, 'avg': 0.38, 'max': 0.92, 'since': 1794550000}, From 108b7f6c67622ac27e18e9769d8be19f6363201c Mon Sep 17 00:00:00 2001 From: Agus Darmawan Date: Sun, 15 Nov 2026 16:33:02 +0700 Subject: [PATCH 2/4] feat: the robot's internet on the glance panel, red when offline --- lib/l10n.dart | 6 ++++++ lib/widgets/glance.dart | 22 ++++++++++++++++++++-- test/glance_test.dart | 15 +++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/lib/l10n.dart b/lib/l10n.dart index 4269af8..f501501 100644 --- a/lib/l10n.dart +++ b/lib/l10n.dart @@ -192,6 +192,9 @@ const _en = { 'people_waiting': 'People: waiting', 'people_loading': 'People: starting…', 'people_off': 'People: off', + 'net_online': 'Internet {ms} ms', + 'net_lossy': 'Internet {ms} ms, {loss}% lost', + 'net_offline': 'Internet offline', 'people_error': 'People: error', 'section_people': 'People detection', 'people_switch': 'Count people', @@ -419,6 +422,9 @@ const _id = { 'people_waiting': 'Orang: menunggu', 'people_loading': 'Orang: memulai…', 'people_off': 'Orang: mati', + 'net_online': 'Internet {ms} ms', + 'net_lossy': 'Internet {ms} ms, {loss}% hilang', + 'net_offline': 'Internet putus', 'people_error': 'Orang: error', 'section_people': 'Deteksi orang', 'people_switch': 'Hitung orang', diff --git a/lib/widgets/glance.dart b/lib/widgets/glance.dart index 7b204b9..3fc51f5 100644 --- a/lib/widgets/glance.dart +++ b/lib/widgets/glance.dart @@ -8,8 +8,8 @@ import '../theme.dart'; import 'hud.dart'; /// What the robot sees and how hard it works, at a glance under the small view: the last -/// inspection (clear or a finding), the people in view and the computer's load. Tap it for the -/// inspection page with the details. +/// inspection (clear or a finding), the people in view, the computer's load and the robot's +/// internet. Tap it for the inspection page with the details. class GlancePanel extends StatelessWidget { const GlancePanel({ super.key, @@ -31,11 +31,13 @@ class GlancePanel extends StatelessWidget { final inspection = state.inspection; final people = state.people; final load = state.load; + final net = state.net; final rows = [ if (inspection != null) _inspectionRow(inspection), if (inspection != null && inspection.queue > 0) _queueRow(inspection), if (people != null) _peopleRow(people), if (showLoad && load != null) ..._loadRows(load), + if (net != null) _netRow(net), ]; if (rows.isEmpty) return const SizedBox.shrink(); return GestureDetector( @@ -136,6 +138,22 @@ class GlancePanel extends StatelessWidget { ]; } + Widget _netRow(NetStatus net) { + if (!net.online) { + return _row(Icons.public_off_rounded, Palette.stop, t('net_offline'), strong: true); + } + final ms = (net.ms ?? 0).round(); + if (net.loss >= 25) { + final text = t('net_lossy', {'ms': ms, 'loss': net.loss.round()}); + return _row(Icons.public_rounded, Palette.caution, text); + } + return _row( + Icons.public_rounded, + net.poor ? Palette.caution : Palette.muted, + t('net_online', {'ms': ms}), + ); + } + String _withAge(String what, int? age) { if (age == null) return what; final ago = age < 60 ? t('ago_s', {'n': age}) : t('ago_min', {'n': age ~/ 60}); diff --git a/test/glance_test.dart b/test/glance_test.dart index c226dc4..e77484f 100644 --- a/test/glance_test.dart +++ b/test/glance_test.dart @@ -82,6 +82,21 @@ void main() { expect(text.style?.color, Palette.stop); }); + testWidgets("the robot's internet: online, losing pings, offline", (tester) async { + await show(tester, { + 'net': {'online': true, 'ms': 38.4, 'loss': 0, 'target': '8.8.8.8', 'age': 1.2}, + }); + expect(find.text('Internet 38 ms'), findsOneWidget); + await show(tester, { + 'net': {'online': true, 'ms': 61.0, 'loss': 33, 'target': '8.8.8.8'}, + }); + expect(find.text('Internet 61 ms, 33% hilang'), findsOneWidget); + const offline = {'online': false, 'ms': null, 'loss': 100, 'target': '8.8.8.8'}; + await show(tester, {'net': offline}, showLoad: false); + final text = tester.widget(find.text('Internet putus')); + expect(text.style?.color, Palette.stop); + }); + testWidgets('without inspection, people or load figures nothing shows', (tester) async { await show(tester, { 'health': {'ok': true, 'issues': []}, From 8ac2a54ef77c1b168395b852322558eff7cea26d Mon Sep 17 00:00:00 2001 From: Agus Darmawan Date: Sun, 15 Nov 2026 16:33:02 +0700 Subject: [PATCH 3/4] docs: the internet line in the readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 7511594..1a57582 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,8 @@ and takes the video over WebRTC. There is no ROS on the handheld. - Battery, link quality, protective field, health and the localization source (identity, tag or pose). - The robot's CPU, GPU and RAM use and its temperature, at a glance, for development. +- The robot's own internet (a ping through its modem, every 5 s): **Internet 38 ms**, yellow + when pings get lost or slow, **Internet offline** in red. Needs Pashupati with `state.net`. - Trip meter under the glance panel: distance and moving time, speed now, average and top speed since the last reset. The robot counts (also with no handheld connected) and keeps it across restarts; hold the panel to reset it to zero. Needs Pashupati with `odometry/trip`. From bd122c5782231802b1aa396f7b782df75c70c1d9 Mon Sep 17 00:00:00 2001 From: Agus Darmawan Date: Sun, 15 Nov 2026 16:33:02 +0700 Subject: [PATCH 4/4] chore(release): 2.8.0 with the robot's internet --- README.md | 10 +++++----- lib/screens/connect_screen.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1a57582..7081466 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ them, watch through their camera, teach routes and run their AI inspection, from Pocket 4 or any Android device in landscape.** [![CI](https://github.com/DarmawanRobotics/PashupatiController/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/DarmawanRobotics/PashupatiController/actions/workflows/ci.yml) -![Version](https://img.shields.io/badge/version-2.7.0-4FD1C5) +![Version](https://img.shields.io/badge/version-2.8.0-4FD1C5) ![Flutter](https://img.shields.io/badge/Flutter-3.35%2B-02569B?logo=flutter&logoColor=white) ![Android](https://img.shields.io/badge/Android-11%2B-3DDC84?logo=android&logoColor=white) ![Robot](https://img.shields.io/badge/robot-ROS%202%20Humble-22314E?logo=ros&logoColor=white) @@ -84,7 +84,7 @@ and takes the video over WebRTC. There is no ROS on the handheld. browser or file manager once), or from a computer: ```bash -adb install -r pashupati-controller-2.7.0.apk # -r: update in place, settings kept +adb install -r pashupati-controller-2.8.0.apk # -r: update in place, settings kept ``` Releases are signed with one release key, so a newer release installs over an older one. @@ -197,11 +197,11 @@ Bump `version` in `pubspec.yaml` (the name and the `+` build number), merge it t that commit with the same version: ```bash -git tag v2.7.0 && git push origin v2.7.0 +git tag v2.8.0 && git push origin v2.8.0 ``` -The `release` workflow tests, builds and publishes `pashupati-controller-2.7.0.apk` (and its -SHA-256) as the GitHub Release `v2.7.0`. A tag that differs from `pubspec.yaml` fails the run. +The `release` workflow tests, builds and publishes `pashupati-controller-2.8.0.apk` (and its +SHA-256) as the GitHub Release `v2.8.0`. A tag that differs from `pubspec.yaml` fails the run. The release key lives in four repository secrets (Settings → Secrets and variables → Actions): `ANDROID_KEYSTORE_BASE64` (the keystore, `base64 -w0 release.jks`), `ANDROID_KEYSTORE_PASSWORD`, diff --git a/lib/screens/connect_screen.dart b/lib/screens/connect_screen.dart index a478522..dd78df8 100644 --- a/lib/screens/connect_screen.dart +++ b/lib/screens/connect_screen.dart @@ -10,7 +10,7 @@ import '../widgets/hud.dart'; import 'drive_screen.dart'; import 'settings_screen.dart'; -const appVersion = '2.7.0'; // the version in pubspec.yaml (test/version_test.dart) +const appVersion = '2.8.0'; // the version in pubspec.yaml (test/version_test.dart) /// Robot picker: the main robot from the settings plus every robot that answers on the LAN. class ConnectScreen extends StatefulWidget { diff --git a/pubspec.yaml b/pubspec.yaml index 4634db8..c054b8a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_teleop description: Handheld teleop for Pashupati robots (Retroid Pocket and other Android devices). publish_to: none -version: 2.7.0+30 +version: 2.8.0+31 environment: sdk: ^3.9.0