Plan P5: a value bubble on the pitch slider while dragging, press-and-drag on the video as a speed joystick (re-sent at 10 Hz, zero on release), and a gimbal settings sheet: mode, 30° extension, yaw follow, recenter / face down / yaw recenter, roll fine-tune, auto calibration and advanced speed/smoothness rows gated by the gimbal's capability ranges. GimbalBridge reports mode, trim, calibration and stop/overload state in the gimbal telemetry group, and getter-only settings in a new gimbalSettings slow group. The reported-plus-pending overlay moves out of CameraState into a SettingsOverlay mixin shared by camera and gimbal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
90 lines
2.8 KiB
Dart
90 lines
2.8 KiB
Dart
import 'package:flutter/foundation.dart';
|
||
|
||
import 'parse.dart';
|
||
import 'settings_overlay.dart';
|
||
|
||
/// Gimbal attitude, mode and status, fed from the `gimbal` group of the
|
||
/// telemetry tick; the getter-only settings (pitch extension, yaw follow,
|
||
/// controller tuning) from the `gimbalSettings` group, overlaid with the
|
||
/// gimbal sheet's pending writes ([SettingsOverlay]).
|
||
class GimbalState extends ChangeNotifier with SettingsOverlay {
|
||
double? pitch; // −90..+30-ish
|
||
double? roll;
|
||
double? yaw;
|
||
|
||
/// `YAW_FOLLOW` / `FPV` / `FREE`.
|
||
String? mode;
|
||
|
||
/// Yaw relative to the aircraft's nose, for gimbals that pan.
|
||
double? yawRelative;
|
||
|
||
double? pitchFineTune;
|
||
double? rollFineTune;
|
||
double? yawFineTune;
|
||
|
||
bool calibrating = false;
|
||
int? calibrationProgress; // 0..100
|
||
bool? calibrationSuccessful;
|
||
|
||
/// How the last calibration seen from start to end went; null until one ends.
|
||
bool? lastCalibrationOk;
|
||
|
||
bool pitchAtStop = false;
|
||
bool yawAtStop = false;
|
||
bool motorOverloaded = false;
|
||
|
||
DateTime? updatedAt;
|
||
|
||
// The `gimbalSettings` group, as the sheet shows it (pending writes included).
|
||
bool? get pitchExtension => asBool(settings['pitchExtension']);
|
||
bool? get yawFollow => asBool(settings['yawFollow']);
|
||
|
||
void apply(Map<String, dynamic> g) {
|
||
pitch = finite(g['pitch']);
|
||
roll = finite(g['roll']);
|
||
yaw = finite(g['yaw']);
|
||
mode = asString(g['mode']);
|
||
yawRelative = finite(g['yawRelative']);
|
||
pitchFineTune = finite(g['pitchFineTune']);
|
||
rollFineTune = finite(g['rollFineTune']);
|
||
yawFineTune = finite(g['yawFineTune']);
|
||
final bool wasCalibrating = calibrating;
|
||
calibrating = asBool(g['calibrating']) ?? false;
|
||
calibrationProgress = asInt(g['calibrationProgress']);
|
||
calibrationSuccessful = asBool(g['calibrationSuccessful']);
|
||
if (wasCalibrating && !calibrating) lastCalibrationOk = calibrationSuccessful;
|
||
pitchAtStop = asBool(g['pitchAtStop']) ?? false;
|
||
yawAtStop = asBool(g['yawAtStop']) ?? false;
|
||
motorOverloaded = asBool(g['motorOverloaded']) ?? false;
|
||
updatedAt = DateTime.now();
|
||
notifyListeners();
|
||
}
|
||
|
||
/// From the telemetry tick; an absent group means the gimbal reported none.
|
||
void applySettings(Map<String, dynamic>? s) {
|
||
replaceReported(s ?? const <String, dynamic>{});
|
||
notifyListeners();
|
||
}
|
||
|
||
/// Shows [mode] ahead of the gimbal's next state callback.
|
||
void setModeOptimistic(String mode) {
|
||
if (this.mode == mode) return;
|
||
this.mode = mode;
|
||
notifyListeners();
|
||
}
|
||
|
||
void clear() {
|
||
pitch = roll = yaw = yawRelative = null;
|
||
pitchFineTune = rollFineTune = yawFineTune = null;
|
||
mode = null;
|
||
calibrating = false;
|
||
calibrationProgress = null;
|
||
calibrationSuccessful = null;
|
||
lastCalibrationOk = null;
|
||
pitchAtStop = yawAtStop = motorOverloaded = false;
|
||
clearSettings();
|
||
updatedAt = null;
|
||
notifyListeners();
|
||
}
|
||
}
|