Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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`.
Expand All @@ -82,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.
Expand Down Expand Up @@ -195,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`,
Expand Down
6 changes: 6 additions & 0 deletions lib/l10n.dart
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ const _en = <String, String>{
'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',
Expand Down Expand Up @@ -419,6 +422,9 @@ const _id = <String, String>{
'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',
Expand Down
34 changes: 34 additions & 0 deletions lib/protocol/messages.dart
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ class RobotState {
this.load,
this.speed,
this.trip,
this.net,
});

factory RobotState.fromJson(Map<String, dynamic> json) {
Expand Down Expand Up @@ -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']),
);
}

Expand Down Expand Up @@ -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';

Expand Down Expand Up @@ -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});
Expand Down
2 changes: 1 addition & 1 deletion lib/screens/connect_screen.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'dart:math' as math;

import 'package:flutter/material.dart';

Check notice on line 3 in lib/screens/connect_screen.dart

View workflow job for this annotation

GitHub Actions / analyze-test

DEPRECATED_MEMBER_USE

'package:flutter/material.dart' is deprecated and shouldn't be used. Use package:material_ui/material_ui.dart instead. This feature was deprecated after v3.47.0-0.0.pre.

import '../net/teleop_client.dart';
import '../protocol/messages.dart';
Expand All @@ -10,7 +10,7 @@
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 {
Expand Down
22 changes: 20 additions & 2 deletions lib/widgets/glance.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'dart:math' as math;

import 'package:flutter/material.dart';

Check notice on line 3 in lib/widgets/glance.dart

View workflow job for this annotation

GitHub Actions / analyze-test

DEPRECATED_MEMBER_USE

'package:flutter/material.dart' is deprecated and shouldn't be used. Use package:material_ui/material_ui.dart instead. This feature was deprecated after v3.47.0-0.0.pre.

import '../l10n.dart';
import '../protocol/messages.dart';
Expand All @@ -8,8 +8,8 @@
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,
Expand All @@ -31,11 +31,13 @@
final inspection = state.inspection;
final people = state.people;
final load = state.load;
final net = state.net;
final rows = <Widget>[
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(
Expand Down Expand Up @@ -136,6 +138,22 @@
];
}

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});
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
15 changes: 15 additions & 0 deletions test/glance_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<Text>(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': []},
Expand Down
14 changes: 14 additions & 0 deletions test/protocol_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
Loading