Skip to content

Commit 82dcc54

Browse files
authored
Handle logs and new logs page (#130)
- Logs from rover devices are now shown in the logs page - important logs are shown in the footer - critical logs stay in the footer until manually cleared - clicking the footer goes to the logs page - logs page has autoscroll, device and level filter, help page - can even reset any device from the logs page - introduced ReactiveWidget - 📎 - Bumped version
1 parent e00396b commit 82dcc54

30 files changed

+783
-190
lines changed

assets/clippy.webp

30.9 KB
Loading

lib/app.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,16 @@ class RoverControlDashboard extends StatelessWidget {
3333
home: SplashPage(),
3434
debugShowCheckedModeBanner: false,
3535
theme: ThemeData(
36+
useMaterial3: false,
3637
colorScheme: const ColorScheme.light(
3738
primary: binghamtonGreen,
3839
secondary: binghamtonGreen,
3940
),
41+
appBarTheme: const AppBarTheme(
42+
backgroundColor: binghamtonGreen,
43+
// titleTextStyle: TextStyle(color: Colors.white),
44+
foregroundColor: Colors.white,
45+
),
4046
),
4147
darkTheme: ThemeData.from(
4248
colorScheme: const ColorScheme.dark(

lib/data.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ export "src/data/science.dart";
2828
export "src/data/settings.dart";
2929
export "src/data/socket.dart";
3030
export "src/data/taskbar_message.dart";
31+
export "src/data/utils.dart";

lib/main.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ void main() async {
2727
if (error is SocketException && networkErrors.contains(error.osError!.errorCode)) {
2828
models.home.setMessage(severity: Severity.critical, text: "Network error, restart by clicking the network icon");
2929
} else {
30-
models.home.setMessage(severity: Severity.critical, text: "Error occurred in the dashboard. See the logs");
30+
models.home.setMessage(severity: Severity.critical, text: "Dashboard error. See the logs");
3131
await services.files.logError(error, stack);
3232
Error.throwWithStackTrace(error, stack);
3333
}

lib/models.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
library models;
1111

1212
import "src/models/model.dart";
13+
import "src/models/data/logs.dart";
1314
import "src/models/data/home.dart";
1415
import "src/models/data/messages.dart";
1516
import "src/models/data/serial.dart";
@@ -23,6 +24,7 @@ export "src/models/model.dart";
2324

2425
// Data models
2526
export "src/models/data/home.dart";
27+
export "src/models/data/logs.dart";
2628
export "src/models/data/messages.dart";
2729
export "src/models/data/serial.dart";
2830
export "src/models/data/settings.dart";
@@ -38,6 +40,7 @@ export "src/models/rover/rover.dart";
3840

3941
// View models
4042
export "src/models/view/autonomy.dart";
43+
export "src/models/view/logs.dart";
4144
export "src/models/view/mars.dart";
4245
export "src/models/view/science.dart";
4346
export "src/models/view/timer.dart";
@@ -89,12 +92,16 @@ class Models extends Model {
8992

9093
/// The messages model.
9194
final messages = MessagesModel();
95+
96+
/// The logs model.
97+
final logs = LogsModel();
9298

9399
@override
94100
Future<void> init() async {
95101
// initialize all models here
96102
await settings.init();
97103
await home.init();
104+
await logs.init();
98105
await video.init();
99106
await rover.init();
100107
await serial.init();

lib/pages.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ library pages;
99
export "src/pages/autonomy.dart";
1010
export "src/pages/home.dart";
1111
export "src/pages/science.dart";
12+
export "src/pages/logs.dart";
1213
export "src/pages/settings.dart";
1314
export "src/pages/splash.dart";
1415

@@ -31,6 +32,9 @@ class Routes {
3132
/// The name of the MARS page.
3233
static const String mars = "MARS";
3334

35+
/// The name of the logs page.
36+
static const String logs = "Logs";
37+
3438
/// The name of the blank page.
3539
static const String blank = "blank";
3640
}

lib/src/data/protobuf.dart

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import "dart:math";
22

3+
import "package:protobuf/protobuf.dart";
34
import "package:rover_dashboard/data.dart";
45

56
export "package:protobuf/protobuf.dart" show GeneratedMessageGenericExtensions;
@@ -40,6 +41,16 @@ String getDataName(Device device) => switch (device) {
4041
_ => "Unknown",
4142
};
4243

44+
/// Utilities for a list of Protobuf enums.
45+
extension UndefinedFilter<T extends ProtobufEnum> on List<T> {
46+
/// Filters out `_UNDEFINED` values from the list.
47+
List<T> get filtered => [
48+
for (final value in this)
49+
if (value.value != 0)
50+
value,
51+
];
52+
}
53+
4354
/// Utilities for [Timestamp]s.
4455
extension TimestampUtils on Timestamp {
4556
/// The [Timestamp] version of [DateTime.now].
@@ -65,6 +76,7 @@ extension RoverStatusHumanName on RoverStatus {
6576
case RoverStatus.MANUAL: return "Manual";
6677
case RoverStatus.AUTONOMOUS: return "Autonomous";
6778
case RoverStatus.POWER_OFF: return "Off";
79+
case RoverStatus.RESTART: return "Restart";
6880
}
6981
// Do not use default or else you'll lose exhaustiveness checking.
7082
throw ArgumentError("Unrecognized rover status: $this");
@@ -131,7 +143,7 @@ extension DeviceUtils on Device {
131143
/// Gets a user-friendly name for a [Device].
132144
String get humanName {
133145
switch(this) {
134-
case Device.DEVICE_UNDEFINED: return "";
146+
case Device.DEVICE_UNDEFINED: return "Unknown device";
135147
case Device.DASHBOARD: return "Dashboard";
136148
case Device.SUBSYSTEMS: return "Subsystems";
137149
case Device.VIDEO: return "Video";
@@ -252,3 +264,43 @@ extension MotorDirectionUtils on MotorDirection {
252264
throw ArgumentError("Unrecognized MotorDirection: $this");
253265
}
254266
}
267+
268+
/// More human-friendly fields for [BurtLogLevel]s.
269+
extension LogLevelUtils on BurtLogLevel {
270+
/// The human-readable name of this level.
271+
String get humanName => switch(this) {
272+
BurtLogLevel.critical => "Critical",
273+
BurtLogLevel.error => "Error",
274+
BurtLogLevel.warning => "Warning",
275+
BurtLogLevel.info => "Info",
276+
BurtLogLevel.debug => "Debug",
277+
BurtLogLevel.trace => "Trace",
278+
_ => "Unknown",
279+
};
280+
281+
/// The label to represent this log.
282+
String get label => switch(this) {
283+
BurtLogLevel.critical => "[C]",
284+
BurtLogLevel.error => "[E]",
285+
BurtLogLevel.warning => "[W]",
286+
BurtLogLevel.info => "[I]",
287+
BurtLogLevel.debug => "[D]",
288+
BurtLogLevel.trace => "[T]",
289+
_ => "?",
290+
};
291+
}
292+
293+
/// Fomats [BurtLog] messages in plain-text. For the UI, use widgets.
294+
extension LogFormat on BurtLog {
295+
/// Fomats [BurtLog] messages in plain-text. For the UI, use widgets.
296+
String format() {
297+
final result = StringBuffer()
298+
..write(level.label)
299+
..write(" ")
300+
..write(title);
301+
if (body.isNotEmpty) {
302+
result..write("\n ")..write(body);
303+
}
304+
return result.toString();
305+
}
306+
}

lib/src/data/settings.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,19 +202,24 @@ class AutonomySettings {
202202
class EasterEggsSettings {
203203
/// Whether to do a SEGA-like intro during boot.
204204
final bool segaIntro;
205+
/// Whether clippy should appear by log messages.
206+
final bool enableClippy;
205207

206208
/// A const constructor.
207209
const EasterEggsSettings({
208210
required this.segaIntro,
211+
required this.enableClippy,
209212
});
210213

211214
/// Parses easter eggs settings from JSON.
212215
EasterEggsSettings.fromJson(Json? json) :
213-
segaIntro = json?["segaIntro"] ?? true;
216+
segaIntro = json?["segaIntro"] ?? true,
217+
enableClippy = json?["enableClippy"] ?? true;
214218

215219
/// Serializes these settings to JSON.
216220
Json toJson() => {
217221
"segaIntro": segaIntro,
222+
"enableClippy": enableClippy,
218223
};
219224
}
220225

lib/src/data/utils.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// Helpful extensions on maps.
2+
extension MapRecords<K, V> on Map<K, V> {
3+
/// A list of key-value records in this map. Allows easier iteration than [entries].
4+
Iterable<(K, V)> get records sync* {
5+
for (final entry in entries) {
6+
yield (entry.key, entry.value);
7+
}
8+
}
9+
}
10+
11+
/// Helpful extensions on [DateTime]s.
12+
extension DateTimeTimestamp on DateTime{
13+
/// Formats this [DateTime] as a simple timestamp.
14+
String get timeStamp => "$year-$month-$day-$hour-$minute";
15+
}

lib/src/models/data/home.dart

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ class HomeModel extends Model {
1010
/// The message currently displaying on the taskbar.
1111
TaskbarMessage? message;
1212

13+
/// Whether the current message is an error.
14+
bool _hasError = false;
15+
1316
/// The timer responsible for clearing the [message].
1417
Timer? _messageTimer;
1518

@@ -21,16 +24,35 @@ class HomeModel extends Model {
2124

2225
@override
2326
Future<void> init() async {
27+
models.settings.addListener(notifyListeners);
2428
final info = await PackageInfo.fromPlatform();
2529
version = "${info.version}+${info.buildNumber}";
2630
if (services.error != null) setMessage(severity: Severity.critical, text: services.error!);
2731
}
2832

2933
/// Sets a new message that will disappear in 3 seconds.
30-
void setMessage({required Severity severity, required String text}) {
34+
void setMessage({required Severity severity, required String text, bool permanent = false}) {
35+
if (_hasError) return; // Don't replace error messages
3136
_messageTimer?.cancel(); // the new message might be cleared if the old one were about to
3237
message = TaskbarMessage(severity: severity, text: text);
3338
notifyListeners();
34-
_messageTimer = Timer(const Duration(seconds: 3), () { message = null; notifyListeners(); });
39+
if (permanent) _hasError = true;
40+
_messageTimer = Timer(const Duration(seconds: 3), clear);
3541
}
42+
43+
/// Clears the current message. Errors won't be cleared unless [clearErrors] is set.
44+
void clear({bool clearErrors = false}) {
45+
if (_hasError && !clearErrors) return;
46+
_hasError = false;
47+
message = null;
48+
notifyListeners();
49+
}
50+
51+
@override
52+
void dispose() {
53+
_messageTimer?.cancel();
54+
mission.cancel();
55+
models.settings.removeListener(notifyListeners);
56+
super.dispose();
57+
}
3658
}

0 commit comments

Comments
 (0)