diff --git a/lib/main.dart b/lib/main.dart index 161efe245..3c2b5272c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,11 +1,11 @@ -/// The entrypoint of the app. -/// -/// These `library` declarations are not needed, the default name for a Dart library is simply the +/// The entrypoint of the app. +/// +/// These `library` declarations are not needed, the default name for a Dart library is simply the /// name of the file. However, DartDoc comments placed above a library declaration will show up on /// the libraries page in the generated documentation. -/// +/// /// This library's main purpose is to execute the app defined in the app library and is designed to -/// be as simple as possible. +/// be as simple as possible. library main; import "dart:async"; @@ -15,22 +15,36 @@ import "package:flutter/material.dart"; import "package:rover_dashboard/app.dart"; import "package:rover_dashboard/data.dart"; import "package:rover_dashboard/models.dart"; -import "package:rover_dashboard/services.dart"; /// Network errors that can be fixed by a simple reset. const networkErrors = {1234, 1231}; +/// Converts an error and optional stack trace into a [BurtLog] and calls [LogsModel.handleLog]. +void logError(Object error, StackTrace? stackTrace) => models.logs.handleLog( + BurtLog( + level: BurtLogLevel.critical, + title: "Dashboard Error. Click for details", + body: "$error\n$stackTrace", + device: Device.DASHBOARD, + ), +); + void main() async { - runZonedGuarded( - () => runApp(RoverControlDashboard()), - (error, stack) async { - if (error is SocketException && networkErrors.contains(error.osError!.errorCode)) { - models.home.setMessage(severity: Severity.critical, text: "Network error, restart by clicking the network icon"); - } else { - models.home.setMessage(severity: Severity.critical, text: "Dashboard error. See the logs"); - await services.files.logError(error, stack); - Error.throwWithStackTrace(error, stack); - } - } - ); + // Logs sync errors to the logs page + FlutterError.onError = (FlutterErrorDetails details) { + logError(details.exception, details.stack); + FlutterError.presentError(details); // do the regular error behavior + }; + // Logs async errors to the logs page + runZonedGuarded( + () => runApp(RoverControlDashboard()), + (error, stackTrace) async { + if (error is SocketException && networkErrors.contains(error.osError!.errorCode)) { + models.home.setMessage(severity: Severity.critical, text: "Network error, restart by clicking the network icon"); + } else { + logError(error, stackTrace); + Error.throwWithStackTrace(error, stackTrace); // do the regular error behavior + } + } + ); } diff --git a/lib/src/models/data/home.dart b/lib/src/models/data/home.dart index 6cdfa2faf..d6047f2c2 100644 --- a/lib/src/models/data/home.dart +++ b/lib/src/models/data/home.dart @@ -35,7 +35,7 @@ class HomeModel extends Model { if (_hasError && severity != Severity.critical) return; // Don't replace critical messages _messageTimer?.cancel(); // the new message might be cleared if the old one were about to message = TaskbarMessage(severity: severity, text: text); - if (logMessage) models.logs.handleLog(message!.burtLog); + if (logMessage) models.logs.handleLog(message!.burtLog, display: false); notifyListeners(); _hasError = permanent; _messageTimer = Timer(const Duration(seconds: 3), clear); diff --git a/lib/src/models/data/logs.dart b/lib/src/models/data/logs.dart index 3d83be953..bc6fc66fa 100644 --- a/lib/src/models/data/logs.dart +++ b/lib/src/models/data/logs.dart @@ -70,7 +70,7 @@ class LogsModel extends Model { }; /// Sends a log message to be shown in the footer. - void handleLog(BurtLog log) { + void handleLog(BurtLog log, {bool display = true}) { // Save to disk and memory saveToFileBuffer.add(log); logsForDevice(log.device)?.addWithLimit(log); @@ -79,12 +79,12 @@ class LogsModel extends Model { notifyListeners(); // Show important messages to the footer. - if (log.device != Device.DASHBOARD) { // Prevents showing dashboard messages that have already been shown + if (display) { // Prevents showing dashboard messages that have already been shown switch (log.level) { case BurtLogLevel.critical: models.home.setMessage(severity: Severity.critical, text: log.title, permanent: true, logMessage: false); case BurtLogLevel.warning: models.home.setMessage(severity: Severity.warning, text: log.title, logMessage: false); case BurtLogLevel.error: models.home.setMessage(severity: Severity.error, text: log.title, logMessage: false); - case BurtLogLevel.info: // Info messages from other devices are not important enough to show here + case BurtLogLevel.info: models.home.setMessage(severity: Severity.info, text: "${log.device.humanName}: ${log.title}", logMessage: false); case BurtLogLevel.debug: case BurtLogLevel.trace: case BurtLogLevel.BURT_LOG_LEVEL_UNDEFINED: diff --git a/lib/src/models/rover/controls/arm.dart b/lib/src/models/rover/controls/arm.dart index d1c70c325..9998ec5d0 100644 --- a/lib/src/models/rover/controls/arm.dart +++ b/lib/src/models/rover/controls/arm.dart @@ -8,7 +8,7 @@ class ArmControls extends RoverControls { ArmSettings get settings => models.settings.arm; /// The coordinates of the gripper. - /// + /// /// The arm uses IK to move all the joints to stay at these coordinates. Coordinates ik = Coordinates(x: 0.5, y: 0.5, z: 0.5); @@ -38,15 +38,15 @@ class ArmControls extends RoverControls { @override List parseInputs(GamepadState state) => [ // Manual control - if (state.normalRightX.abs() > state.normalRightJoystickY.abs() && state.normalRightX != 0) + if (state.normalRightX.abs() > state.normalRightY.abs() && state.normalRightX != 0) ArmCommand(swivel: MotorCommand(moveRadians: state.normalRightX * settings.swivel)), - if (state.normalRightJoystickY.abs() > state.normalRightX.abs() && state.normalRightJoystickY != 0) - ArmCommand(shoulder: MotorCommand(moveRadians: state.normalRightJoystickY * settings.shoulder)), + if (state.normalRightY.abs() > state.normalRightX.abs() && state.normalRightY != 0) + ArmCommand(shoulder: MotorCommand(moveRadians: state.normalRightY * settings.shoulder)), if (state.normalLeftY != 0) ArmCommand(elbow: MotorCommand(moveRadians: state.normalLeftY * settings.elbow)), - // The bumpers should be pseudo-IK: Move the shoulder and elbow in sync. - if (state.normalShoulders != 0) ArmCommand( - shoulder: MotorCommand(moveRadians: state.normalShoulders * settings.shoulder * -1), - elbow: MotorCommand(moveRadians: state.normalShoulders * settings.elbow), + // The bumpers should be pseudo-IK: Move the shoulder and elbow in sync. + if (state.normalShoulder != 0) ArmCommand( + shoulder: MotorCommand(moveRadians: state.normalShoulder * settings.shoulder * -1), + elbow: MotorCommand(moveRadians: state.normalShoulder * settings.elbow), ), // Gripper diff --git a/lib/src/pages/controllers/controller.dart b/lib/src/pages/controllers/controller.dart index a9ac51c35..521b3ab33 100644 --- a/lib/src/pages/controllers/controller.dart +++ b/lib/src/pages/controllers/controller.dart @@ -46,6 +46,8 @@ class ControllerWidget extends ReusableReactiveWidget { required double y, required Offset offsetOnImage, required Size widgetSize, + // TODO(Levi-Lesches): Support thumbstick presses, https://github.com/Levi-Lesches/sdl_gamepad/issues/1 + required bool? isPressed, }) { final scaleFactor = _getBackgroundFitWidth(widgetSize) / imageSize.width; @@ -62,7 +64,7 @@ class ControllerWidget extends ReusableReactiveWidget { width: joystickRadius, height: joystickRadius, decoration: BoxDecoration( - color: gamepadColor, + color: (isPressed ?? false) ? gamepadColor : null, borderRadius: BorderRadius.circular(10000), ), ), @@ -161,12 +163,14 @@ class ControllerWidget extends ReusableReactiveWidget { y: -1 * (state?.normalLeftY ?? 0), offsetOnImage: leftStick, widgetSize: widgetSize, + isPressed: true, ), _controllerJoystick( x: state?.normalRightX ?? 0, y: -1 * (state?.normalRightY ?? 0), offsetOnImage: rightStick, widgetSize: widgetSize, + isPressed: true, ), ], ), diff --git a/lib/src/services/files.dart b/lib/src/services/files.dart index 355ec85e9..be1fb8984 100644 --- a/lib/src/services/files.dart +++ b/lib/src/services/files.dart @@ -140,15 +140,9 @@ class FilesService extends Service { WrappedMessage.fromBuffer(base64.decode(line)), ]; - /// Outputs error to log file - Future logError(Object error, StackTrace stack) async{ - if (!_isInit) return; - final file = loggingDir / "errors.log"; - await file.writeAsString("${DateTime.now().timeStamp} $error $stack\n", mode: FileMode.writeOnlyAppend); - } - /// Outputs a log to its device's respective log file. Future logMessage(BurtLog log) async { + if (!_isInit) return; final file = loggingDir / "${log.device.humanName}.log"; await file.writeAsString("${log.format()}\n", mode: FileMode.writeOnlyAppend); }