Repository: asv-soft/asv-drones Branch: main Commit: 9f78310d7315 Files: 322 Total size: 999.4 KB Directory structure: gitextract_fy5d8uuq/ ├── .config/ │ └── dotnet-tools.json ├── .csharpierignore ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ └── bug_report.md │ └── workflows/ │ ├── api-release-dev.yml │ ├── api-release.yml │ └── drones-release-windows.yml ├── .gitignore ├── .husky/ │ ├── pre-commit │ └── task-runner.json ├── LICENSE ├── README.md ├── api_build.bat ├── api_publish_github.bat ├── publish.bat ├── src/ │ ├── .aiassistant/ │ │ └── rules/ │ │ └── comments.md │ ├── .editorconfig │ ├── .gitignore │ ├── .run/ │ │ └── Publish win-x64.run.xml │ ├── Asv.Drones/ │ │ ├── App.axaml │ │ ├── App.axaml.cs │ │ ├── Asv.Drones.csproj │ │ ├── Asv.Drones.csproj.DotSettings │ │ ├── AsvDronesMixin.cs │ │ ├── Core/ │ │ │ ├── Commands/ │ │ │ │ ├── Behaviour/ │ │ │ │ │ ├── Remove/ │ │ │ │ │ │ └── RemoveItemCommand.cs │ │ │ │ │ └── Rename/ │ │ │ │ │ └── CommitRenameCommand.cs │ │ │ │ ├── FileBrowser/ │ │ │ │ │ ├── FileBrowserViewModel/ │ │ │ │ │ │ └── FindFileCommand.cs │ │ │ │ │ ├── Items/ │ │ │ │ │ │ ├── CalculateCrc32Command.cs │ │ │ │ │ │ └── CreateDirectoryCommand.cs │ │ │ │ │ ├── OpenFileBrowserCommand.cs │ │ │ │ │ └── Transfer/ │ │ │ │ │ ├── BurstDownloadItemCommand.cs │ │ │ │ │ ├── DownloadItemCommand.cs │ │ │ │ │ ├── ITransferFtpEntries.cs │ │ │ │ │ ├── TransferCommandBase.cs │ │ │ │ │ └── UploadItemCommand.cs │ │ │ │ ├── Flight/ │ │ │ │ │ ├── OpenFlight.cs │ │ │ │ │ ├── OpenFlightMode.cs │ │ │ │ │ └── Widgets/ │ │ │ │ │ └── UavWidget/ │ │ │ │ │ ├── AutoModeCommand.cs │ │ │ │ │ ├── GuidedModeCommand.cs │ │ │ │ │ ├── LandCommand.cs │ │ │ │ │ ├── MissionProgress/ │ │ │ │ │ │ └── UpdateMissionCommand.cs │ │ │ │ │ ├── RTLCommand.cs │ │ │ │ │ ├── StartMissionCommand.cs │ │ │ │ │ └── TakeOffCommand.cs │ │ │ │ ├── MavParams/ │ │ │ │ │ ├── MavParamsPageViewModel/ │ │ │ │ │ │ ├── RemoveAllPinsCommand.cs │ │ │ │ │ │ ├── StopUpdateParamsCommand.cs │ │ │ │ │ │ └── UpdateParamsCommand.cs │ │ │ │ │ └── OpenMavParamsCommand.cs │ │ │ │ ├── Mavlink/ │ │ │ │ │ ├── MavlinkCommands.cs │ │ │ │ │ ├── MavlinkCommandsMixin.cs │ │ │ │ │ ├── MavlinkParamReadCommand.cs │ │ │ │ │ ├── MavlinkParamsWriteCommand.cs │ │ │ │ │ └── NullMavlinkCommands.cs │ │ │ │ ├── PacketViewer/ │ │ │ │ │ ├── ClearAllPacketsCommand.cs │ │ │ │ │ ├── ExportPacketsToCsvCommand.cs │ │ │ │ │ └── OpenPacketViewer.cs │ │ │ │ └── Setup/ │ │ │ │ ├── FrameType/ │ │ │ │ │ └── ChangeFrameTypeCommand.cs │ │ │ │ └── OpenSetupCommand.cs │ │ │ ├── Controls/ │ │ │ │ └── DeviceTelemetry/ │ │ │ │ ├── AngleUavIndicator/ │ │ │ │ │ ├── Items/ │ │ │ │ │ │ ├── Pitch/ │ │ │ │ │ │ │ ├── PitchItem.cs │ │ │ │ │ │ │ └── PitchItem.properties.cs │ │ │ │ │ │ └── Roll/ │ │ │ │ │ │ ├── RollItem.cs │ │ │ │ │ │ └── RollItem.properties.cs │ │ │ │ │ ├── UavAngleIndicator.cs │ │ │ │ │ ├── UavAngleIndicator.properties.cs │ │ │ │ │ └── UavAngleIndicatorStyles.axaml │ │ │ │ ├── CompassUavIndicator/ │ │ │ │ │ ├── CompassScaleItem.cs │ │ │ │ │ ├── CompassUavIndicator.cs │ │ │ │ │ ├── CompassUavIndicator.properties.cs │ │ │ │ │ └── CompassUavIndicatorStyles.axaml │ │ │ │ ├── DeviceTelemetryDesignPreview.cs │ │ │ │ ├── OldAttitudeIndicator/ │ │ │ │ │ ├── AttitudeIndicator.cs │ │ │ │ │ ├── AttitudeIndicator.properties.cs │ │ │ │ │ ├── AttitudeIndicatorStyles.axaml │ │ │ │ │ └── Items/ │ │ │ │ │ ├── Heading/ │ │ │ │ │ │ └── HeadingScaleItem.cs │ │ │ │ │ ├── Pitch/ │ │ │ │ │ │ ├── PitchItem.cs │ │ │ │ │ │ └── PitchItem.properties.cs │ │ │ │ │ ├── Roll/ │ │ │ │ │ │ ├── RollItem.cs │ │ │ │ │ │ └── RollItem.properties.cs │ │ │ │ │ └── Scale/ │ │ │ │ │ ├── ScaleItem.cs │ │ │ │ │ └── ScaleItem.properties.cs │ │ │ │ ├── RouteUavIndicator/ │ │ │ │ │ ├── RouteUavIndicator.cs │ │ │ │ │ └── RouteUavIndicatorStyles.axaml │ │ │ │ └── Rtt/ │ │ │ │ ├── AltitudeUavIndicator/ │ │ │ │ │ ├── AltitudeUavIndicator.axaml │ │ │ │ │ ├── AltitudeUavIndicator.axaml.cs │ │ │ │ │ └── AltitudeUavIndicatorViewModel.cs │ │ │ │ ├── AngleUavRttIndicator/ │ │ │ │ │ ├── AngleUavRttIndicator.axaml │ │ │ │ │ ├── AngleUavRttIndicator.axaml.cs │ │ │ │ │ └── AngleUavRttIndicatorViewModel.cs │ │ │ │ ├── BatteryUavIndicator/ │ │ │ │ │ ├── BatteryUavIndicator.axaml │ │ │ │ │ ├── BatteryUavIndicator.axaml.cs │ │ │ │ │ └── BatteryUavIndicatorViewModel.cs │ │ │ │ ├── HeadingUavIndicator/ │ │ │ │ │ ├── HeadingUavIndicator.axaml │ │ │ │ │ ├── HeadingUavIndicator.axaml.cs │ │ │ │ │ └── HeadingUavIndicatorViewModel.cs │ │ │ │ ├── HomeAzimuthUavIndicator/ │ │ │ │ │ ├── HomeAzimuthUavIndicator.axaml │ │ │ │ │ ├── HomeAzimuthUavIndicator.axaml.cs │ │ │ │ │ └── HomeAzimuthUavIndicatorViewModel.cs │ │ │ │ └── VelocityUavIndicator/ │ │ │ │ ├── VelocityUavIndicator.axaml │ │ │ │ ├── VelocityUavIndicator.axaml.cs │ │ │ │ └── VelocityUavIndicatorViewModel.cs │ │ │ ├── Converters/ │ │ │ │ └── Crc32StatusToColorConverter.cs │ │ │ └── Services/ │ │ │ ├── ClientDeviceWidgetFactory/ │ │ │ │ └── ClientDeviceWidgetFactory.cs │ │ │ ├── Devices/ │ │ │ │ └── Gnss/ │ │ │ │ └── GnssDeviceManagerExtentsion.cs │ │ │ └── Files/ │ │ │ ├── BusyFlag.cs │ │ │ ├── Local/ │ │ │ │ └── LocalFilesService.cs │ │ │ ├── PathHelper.cs │ │ │ ├── ProgressWithLock.cs │ │ │ └── Remote/ │ │ │ ├── FtpClientService.cs │ │ │ └── RemoteEntriesSync.cs │ │ ├── RS.Designer.cs │ │ ├── RS.resx │ │ ├── RS.ru.resx │ │ └── Shell/ │ │ └── Pages/ │ │ ├── Device/ │ │ │ ├── FileBrowser/ │ │ │ │ ├── Contracts/ │ │ │ │ │ ├── FileBrowserBackend.cs │ │ │ │ │ ├── IBrowserItemsOperations.cs │ │ │ │ │ ├── LocalBrowserItemsOperations.cs │ │ │ │ │ └── RemoteBrowserItemsOperations.cs │ │ │ │ ├── Dialogs/ │ │ │ │ │ ├── BurstDownloadDialogView.axaml │ │ │ │ │ ├── BurstDownloadDialogView.axaml.cs │ │ │ │ │ └── BurstDownloadDialogViewModel.cs │ │ │ │ ├── FileBrowserView.axaml │ │ │ │ ├── FileBrowserView.axaml.cs │ │ │ │ ├── FileBrowserViewModel.cs │ │ │ │ ├── HomePageFileBrowserDeviceItemAction.cs │ │ │ │ ├── IO/ │ │ │ │ │ ├── FileSize.cs │ │ │ │ │ ├── FtpBrowserNamingPolicy.cs │ │ │ │ │ ├── FtpBrowserPath.cs │ │ │ │ │ └── Types/ │ │ │ │ │ ├── Crc32Status.cs │ │ │ │ │ └── FtpBrowserSourceType.cs │ │ │ │ └── Tree/ │ │ │ │ ├── BrowserNode.cs │ │ │ │ ├── BrowserTree.cs │ │ │ │ └── Items/ │ │ │ │ ├── BrowserItemComparer.cs │ │ │ │ ├── BrowserItemViewModel.cs │ │ │ │ ├── DirectoryItemViewModel.cs │ │ │ │ ├── FileItemViewModel.cs │ │ │ │ └── IBrowserItemViewModel.cs │ │ │ ├── MavParams/ │ │ │ │ ├── Dialog/ │ │ │ │ │ ├── TryCloseWithApprovalDialogView.axaml │ │ │ │ │ ├── TryCloseWithApprovalDialogView.axaml.cs │ │ │ │ │ └── TryCloseWithApprovalDialogViewModel.cs │ │ │ │ ├── HomePageParamsDeviceItemAction.cs │ │ │ │ ├── MavParamsPageView.axaml │ │ │ │ ├── MavParamsPageView.axaml.cs │ │ │ │ ├── MavParamsPageViewModel.cs │ │ │ │ └── ParamItem/ │ │ │ │ ├── ParamItemView.axaml │ │ │ │ ├── ParamItemView.axaml.cs │ │ │ │ ├── ParamItemViewModel.cs │ │ │ │ └── RoutedEvents/ │ │ │ │ └── ParamItemChangedEvent.cs │ │ │ └── Setup/ │ │ │ ├── DefaultSetupExtension.cs │ │ │ ├── HomePageSetupDeviceItemAction.cs │ │ │ ├── SetupMixin.cs │ │ │ ├── SetupPageView.axaml │ │ │ ├── SetupPageView.axaml.cs │ │ │ ├── SetupPageViewModel.cs │ │ │ └── Subpage/ │ │ │ ├── FrameType/ │ │ │ │ ├── DroneFrameItem/ │ │ │ │ │ ├── DroneFrameItemView.axaml │ │ │ │ │ ├── DroneFrameItemView.axaml.cs │ │ │ │ │ ├── DroneFrameItemViewModel.cs │ │ │ │ │ ├── NullDroneFrame.cs │ │ │ │ │ └── RoutedEvents/ │ │ │ │ │ └── CurrentDroneFrameChangeEvent.cs │ │ │ │ ├── FrameTypeSetupPageExtension.cs │ │ │ │ ├── SetupFrameTypeView.axaml │ │ │ │ ├── SetupFrameTypeView.axaml.cs │ │ │ │ └── SetupFrameTypeViewModel.cs │ │ │ ├── Motors/ │ │ │ │ ├── MotorItem/ │ │ │ │ │ ├── MotorItemView.axaml │ │ │ │ │ ├── MotorItemView.axaml.cs │ │ │ │ │ └── MotorItemViewModel.cs │ │ │ │ ├── MotorsSetupPageExtension.cs │ │ │ │ ├── SetupMotorsView.axaml │ │ │ │ ├── SetupMotorsView.axaml.cs │ │ │ │ └── SetupMotorsViewModel.cs │ │ │ └── SetupSubpage.cs │ │ ├── Flight/ │ │ │ ├── Anchors/ │ │ │ │ ├── FlightUavAnchorsExtension.cs │ │ │ │ ├── MissionAnchor.cs │ │ │ │ └── UavAnchor.cs │ │ │ ├── FlightPageView.axaml │ │ │ ├── FlightPageView.axaml.cs │ │ │ ├── FlightPageViewModel.cs │ │ │ ├── HomePageFlightExtension.cs │ │ │ └── Widgets/ │ │ │ ├── FlightWidgetsExtension.cs │ │ │ └── UavWidget/ │ │ │ ├── Dialogs/ │ │ │ │ ├── SetAltitudeDialogView.axaml │ │ │ │ ├── SetAltitudeDialogView.axaml.cs │ │ │ │ └── SetAltitudeDialogViewModel.cs │ │ │ ├── MissionProgress/ │ │ │ │ ├── MissionProgressView.axaml │ │ │ │ ├── MissionProgressView.axaml.cs │ │ │ │ └── MissionProgressViewModel.cs │ │ │ ├── UavWidgetView.axaml │ │ │ ├── UavWidgetView.axaml.cs │ │ │ └── UavWidgetViewModel.cs │ │ ├── FlightMode/ │ │ │ ├── Anchors/ │ │ │ │ └── FlightModeAnchorsExtension.cs │ │ │ ├── FlightModePageView.axaml │ │ │ ├── FlightModePageView.axaml.cs │ │ │ ├── FlightModePageViewModel.cs │ │ │ ├── HomePageFlightModeExtension.cs │ │ │ └── Widgets/ │ │ │ ├── Device/ │ │ │ │ ├── FlightModeClientDeviceWidgetExtension.cs │ │ │ │ └── Mavlink/ │ │ │ │ └── Drone/ │ │ │ │ ├── DroneFlightWidgetViewModel.cs │ │ │ │ ├── DroneWidgetCreationHandler.cs │ │ │ │ ├── Plane/ │ │ │ │ │ ├── PlaneWidgetCreationHandler.cs │ │ │ │ │ ├── PlaneWidgetViewModel.cs │ │ │ │ │ └── Sections/ │ │ │ │ │ ├── PlaneSectionExtension.cs │ │ │ │ │ ├── PlaneSectionView.axaml │ │ │ │ │ ├── PlaneSectionView.axaml.cs │ │ │ │ │ └── PlaneSectionViewModel.cs │ │ │ │ └── Sections/ │ │ │ │ ├── AttitudeIndicator/ │ │ │ │ │ ├── AttitudeIndicatorSectionView.axaml │ │ │ │ │ ├── AttitudeIndicatorSectionView.axaml.cs │ │ │ │ │ ├── AttitudeIndicatorSectionViewModel.cs │ │ │ │ │ └── DroneFlightWidgetExtensionAttitudeIndicatorSection.cs │ │ │ │ ├── FlightControl/ │ │ │ │ │ ├── DroneFlightWidgetFlightControlSectionExtension.cs │ │ │ │ │ ├── FlightControlSectionView.axaml │ │ │ │ │ ├── FlightControlSectionView.axaml.cs │ │ │ │ │ └── FlightControlSectionViewModel.cs │ │ │ │ └── Telemetry/ │ │ │ │ ├── DroneFlightWidgetTelemetrySectionExtension.cs │ │ │ │ ├── TelemetrySectionView.axaml │ │ │ │ ├── TelemetrySectionView.axaml.cs │ │ │ │ └── TelemetrySectionViewModel.cs │ │ │ └── Test/ │ │ │ ├── PluginFlightItemView.axaml │ │ │ ├── PluginFlightItemView.axaml.cs │ │ │ ├── PluginFlightItemViewModel.cs │ │ │ └── PluginFlightItemWidgetExtension.cs │ │ └── PacketViewer/ │ │ ├── Dialogs/ │ │ │ ├── SavePacketMessagesDialogView.axaml │ │ │ ├── SavePacketMessagesDialogView.axaml.cs │ │ │ └── SavePacketMessagesDialogViewModel.cs │ │ ├── Filters/ │ │ │ ├── Comparers/ │ │ │ │ ├── PacketFilterComparerBase.cs │ │ │ │ ├── SourcePacketFilterComparer.cs │ │ │ │ └── TypePacketFilterComparer.cs │ │ │ ├── PacketFilterViewModelBase.cs │ │ │ ├── SourcePacketFilterViewModel.cs │ │ │ └── TypePacketFilterViewModel.cs │ │ ├── HomePacketViewerExtension.cs │ │ ├── PacketConverter/ │ │ │ └── DefaultMavlinkPacketConverter.cs │ │ ├── PacketMessage/ │ │ │ ├── PacketMessageView.axaml │ │ │ ├── PacketMessageView.axaml.cs │ │ │ └── PacketMessageViewModel.cs │ │ ├── PacketViewerView.axaml │ │ ├── PacketViewerView.axaml.cs │ │ └── PacketViewerViewModel.cs │ ├── Asv.Drones.Android/ │ │ ├── Asv.Drones.Android.csproj │ │ ├── MainActivity.cs │ │ ├── Properties/ │ │ │ └── AndroidManifest.xml │ │ └── Resources/ │ │ ├── AboutResources.txt │ │ ├── drawable/ │ │ │ └── splash_screen.xml │ │ ├── drawable-night-v31/ │ │ │ └── avalonia_anim.xml │ │ ├── drawable-v31/ │ │ │ └── avalonia_anim.xml │ │ ├── values/ │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ ├── values-night/ │ │ │ └── colors.xml │ │ └── values-v31/ │ │ └── styles.xml │ ├── Asv.Drones.Api/ │ │ ├── Asv.Drones.Api.csproj │ │ ├── Asv.Drones.Api.csproj.DotSettings │ │ ├── Core/ │ │ │ ├── Commands/ │ │ │ │ ├── Behaviour/ │ │ │ │ │ ├── Remove/ │ │ │ │ │ │ ├── IRemoveItemCommand.cs │ │ │ │ │ │ └── ISupportRemove.cs │ │ │ │ │ └── Rename/ │ │ │ │ │ ├── ICommitRenameCommand.cs │ │ │ │ │ └── ISupportRename.cs │ │ │ │ ├── Commands.cs │ │ │ │ ├── Flight/ │ │ │ │ │ └── IFlightModeCommands.cs │ │ │ │ ├── Mavlink/ │ │ │ │ │ └── IMavlinkCommands.cs │ │ │ │ └── MavlinkMicroserviceCommand.cs │ │ │ ├── Controls/ │ │ │ │ ├── FlightWidget/ │ │ │ │ │ ├── FlightWidgetView.axaml │ │ │ │ │ ├── FlightWidgetView.axaml.cs │ │ │ │ │ ├── FlightWidgetViewModel.cs │ │ │ │ │ ├── FlightWidgetsComparer.cs │ │ │ │ │ ├── IFlightWidget.cs │ │ │ │ │ └── Section/ │ │ │ │ │ ├── FlightWidgetSectionsComparer.cs │ │ │ │ │ └── IFlightWidgetSection.cs │ │ │ │ └── MavParam/ │ │ │ │ ├── Button/ │ │ │ │ │ ├── MavParamButtonView.axaml │ │ │ │ │ ├── MavParamButtonView.axaml.cs │ │ │ │ │ └── MavParamButtonViewModel.cs │ │ │ │ ├── ComboBox/ │ │ │ │ │ ├── MavParamComboBoxView.axaml │ │ │ │ │ ├── MavParamComboBoxView.axaml.cs │ │ │ │ │ └── MavParamComboboxViewModel.cs │ │ │ │ ├── Geopoint/ │ │ │ │ │ ├── MavParamAltitudeTextBoxView.cs │ │ │ │ │ ├── MavParamAltitudeTextBoxViewModel.cs │ │ │ │ │ ├── MavParamLatLonTextBoxView.cs │ │ │ │ │ └── MavParamLatLonTextBoxViewModel.cs │ │ │ │ ├── MavParamFactory.cs │ │ │ │ ├── MavParamInfo.cs │ │ │ │ ├── MavParamViewModel.cs │ │ │ │ ├── String/ │ │ │ │ │ ├── MavParamAsciiCharView.cs │ │ │ │ │ └── MavParamAsciiCharViewModel.cs │ │ │ │ └── TextBox/ │ │ │ │ ├── MavParamTextBoxView.axaml │ │ │ │ ├── MavParamTextBoxView.axaml.cs │ │ │ │ └── MavParamTextBoxViewModel.cs │ │ │ └── Services/ │ │ │ ├── ClientDeviceWidgetFactory/ │ │ │ │ ├── IClientDeviceWidgetCreationHandler.cs │ │ │ │ └── IClientDeviceWidgetFactory.cs │ │ │ ├── Converters/ │ │ │ │ └── IPacketConverter.cs │ │ │ └── Devices/ │ │ │ └── Mavlink/ │ │ │ └── IMavlinkHost.cs │ │ ├── RS.Designer.cs │ │ ├── RS.resx │ │ ├── RS.ru.resx │ │ ├── Shell/ │ │ │ └── Pages/ │ │ │ ├── FileBrowser/ │ │ │ │ └── IFileBrowserViewModel.cs │ │ │ ├── Flight/ │ │ │ │ ├── FlightMode.cs │ │ │ │ ├── IFlightMode.cs │ │ │ │ └── Widgets/ │ │ │ │ └── UavWidget/ │ │ │ │ └── IUavFlightWidget.cs │ │ │ ├── FlightMode/ │ │ │ │ ├── IFlightModePage.cs │ │ │ │ └── Widgets/ │ │ │ │ └── Device/ │ │ │ │ ├── DeviceFlightWidgetViewModelBase.cs │ │ │ │ ├── IDeviceFlightWidget.cs │ │ │ │ └── Mavlink/ │ │ │ │ ├── Drone/ │ │ │ │ │ ├── DroneFlightWidgetViewModelBase.cs │ │ │ │ │ └── IDroneFlightWidget.cs │ │ │ │ ├── IMavlinkDeviceFlightWidget.cs │ │ │ │ └── MavlinkDeviceFlightWidgetViewModelBase.cs │ │ │ ├── MavParams/ │ │ │ │ └── IMavParamsPageViewModel.cs │ │ │ └── Setup/ │ │ │ ├── ISetupPage.cs │ │ │ └── Subpage/ │ │ │ └── ISetupSubpage.cs │ │ └── Tools/ │ │ └── Mavlink/ │ │ ├── DeviceIconMixin.cs │ │ └── MavlinkHost.cs │ ├── Asv.Drones.Desktop/ │ │ ├── Asv.Drones.Desktop.csproj │ │ ├── Program.cs │ │ ├── app.manifest │ │ ├── appsettings.Development.json │ │ ├── appsettings.Production.json │ │ └── appsettings.json │ ├── Asv.Drones.iOS/ │ │ ├── AppDelegate.cs │ │ ├── Asv.Drones.iOS.csproj │ │ ├── Entitlements.plist │ │ ├── Info.plist │ │ ├── Main.cs │ │ └── Resources/ │ │ └── LaunchScreen.xib │ ├── Asv.Drones.slnx │ ├── CodeStyle.ruleset │ ├── Directory.Build.props │ └── global.json ├── win-64-install.nsi ├── win-arm-install.iss ├── win-arm64-install.iss ├── win-x64-install.iss └── win-x86-install.iss ================================================ FILE CONTENTS ================================================ ================================================ FILE: .config/dotnet-tools.json ================================================ { "version": 1, "isRoot": true, "tools": { "husky": { "version": "0.8.0", "commands": [ "husky" ], "rollForward": false }, "run-script": { "version": "0.6.0", "commands": [ "r" ], "rollForward": false }, "csharpier": { "version": "1.2.1", "commands": [ "csharpier" ], "rollForward": false } } } ================================================ FILE: .csharpierignore ================================================ * !**/*.cs !**/*.axaml ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- 1. Description: A brief description of the error or issue you have encountered. 2. Steps to Reproduce: [Step 1] [Step 2] [Step 3] ... 3. Expected Result: Describe what you expected to see after performing the steps mentioned above. 4. Actual Result: Describe what actually happened after performing the steps mentioned above. 5. Screenshots: If applicable, add screenshots to help explain your problem. 6. Environment: Desktop: OS: [e.g., Windows 10] Version: [e.g., 99.0] 7. Additional Context: Include relevant console logs.[Your console logs here]❌Errors [], ⚠️Info [], 📜Log [] Please ensure you have provided all necessary steps and information to reproduce the bug. ================================================ FILE: .github/workflows/api-release-dev.yml ================================================ name: Release Api Github only on: push: tags: - "api-v[0-9]+.[0-9]+.[0-9]+-dev.[0-9]+" - "api-v[0-9]+.[0-9]+.[0-9]+-dev" env: GITHUB_PACKAGES_URL: 'https://nuget.pkg.github.com/asv-soft/index.json' PROJECT_NAME: 'Asv.Drones' PROPS_VERSION_VAR_NAME: 'ApiVersion' jobs: build: runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/api-v') steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: '10.x.x' - name: Add NuGet source run: dotnet nuget add source ${{ env.GITHUB_PACKAGES_URL }} \--username '${{secrets.USER_NAME}}' \--password '${{secrets.GIHUB_NUGET_AUTH_TOKEN}}' \--store-password-in-clear-text - name: Install dependencies run: | dotnet restore ./src/${{env.PROJECT_NAME}}/${{env.PROJECT_NAME}}.csproj dotnet restore ./src/${{env.PROJECT_NAME}}.Api/${{env.PROJECT_NAME}}.Api.csproj - name: Build run: | dotnet build ./src/${{env.PROJECT_NAME}}/${{env.PROJECT_NAME}}.csproj --configuration Release --no-restore dotnet build ./src/${{env.PROJECT_NAME}}.Api/${{env.PROJECT_NAME}}.Api.csproj --configuration Release --no-restore - name: Set version variable env: TAG: ${{ github.ref_name }} run: echo "VERSION=${TAG#api-v}" >> $GITHUB_ENV - name: Read version from Directory.Build.props id: read-version run: | version=$(grep -oP '<${{env.PROPS_VERSION_VAR_NAME}}>\K[^<]+' ./src/Directory.Build.props) echo "PropsVersion=${version}" >> $GITHUB_ENV - name: Compare tag with NuGet package version run: | if [ "${{ env.PropsVersion }}" != "${{ env.VERSION }}" ]; then echo "Error: Tag does not match NuGet package version" exit 1 fi - name: Pack package run: | dotnet pack ./src/${{env.PROJECT_NAME}}/${{env.PROJECT_NAME}}.csproj -c Release /p:ProductVersion=${VERSION} --no-build -o . dotnet pack ./src/${{env.PROJECT_NAME}}.Api/${{env.PROJECT_NAME}}.Api.csproj -c Release /p:ProductVersion=${VERSION} --no-build -o . - name: List output files run: ls -la - name: Push package to GitHub run: | dotnet nuget push ${{env.PROJECT_NAME}}.${{ env.VERSION }}.nupkg --api-key ${{ secrets.GIHUB_NUGET_AUTH_TOKEN }} --skip-duplicate --source ${{ env.GITHUB_PACKAGES_URL }} dotnet nuget push ${{env.PROJECT_NAME}}.Api.${{ env.VERSION }}.nupkg --api-key ${{ secrets.GIHUB_NUGET_AUTH_TOKEN }} --skip-duplicate --source ${{ env.GITHUB_PACKAGES_URL }} ================================================ FILE: .github/workflows/api-release.yml ================================================ name: Release Api NuGet on: push: tags: - "api-v[0-9]+.[0-9]+.[0-9]+" - "api-v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+" - "api-v[0-9]+.[0-9]+.[0-9]+-rc" env: NUGET_SOURCE_URL: 'https://api.nuget.org/v3/index.json' GITHUB_PACKAGES_URL: 'https://nuget.pkg.github.com/asv-soft/index.json' PROJECT_NAME: 'Asv.Drones' PROPS_VERSION_VAR_NAME: 'ApiVersion' jobs: build: runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/api-v') steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: '10.x.x' - name: Add NuGet source run: dotnet nuget add source ${{ env.GITHUB_PACKAGES_URL }} \--username '${{secrets.USER_NAME}}' \--password '${{secrets.GIHUB_NUGET_AUTH_TOKEN}}' \--store-password-in-clear-text - name: Install dependencies run: | dotnet restore ./src/${{env.PROJECT_NAME}}/${{env.PROJECT_NAME}}.csproj dotnet restore ./src/${{env.PROJECT_NAME}}.Api/${{env.PROJECT_NAME}}.Api.csproj - name: Build run: | dotnet build ./src/${{env.PROJECT_NAME}}/${{env.PROJECT_NAME}}.csproj --configuration Release --no-restore dotnet build ./src/${{env.PROJECT_NAME}}.Api/${{env.PROJECT_NAME}}.Api.csproj --configuration Release --no-restore - name: Set version variable env: TAG: ${{ github.ref_name }} run: echo "VERSION=${TAG#api-v}" >> $GITHUB_ENV - name: Read version from Directory.Build.props id: read-version run: | version=$(grep -oP '<${{env.PROPS_VERSION_VAR_NAME}}>\K[^<]+' ./src/Directory.Build.props) echo "PropsVersion=${version}" >> $GITHUB_ENV - name: Compare tag with NuGet package version run: | if [ "${{ env.PropsVersion }}" != "${{ env.VERSION }}" ]; then echo "Error: Tag does not match NuGet package version" exit 1 fi - name: Pack package run: | dotnet pack ./src/${{env.PROJECT_NAME}}/${{env.PROJECT_NAME}}.csproj -c Release /p:ProductVersion=${VERSION} --no-build -o . dotnet pack ./src/${{env.PROJECT_NAME}}.Api/${{env.PROJECT_NAME}}.Api.csproj -c Release /p:ProductVersion=${VERSION} --no-build -o . - name: List output files run: ls -la - name: Push package to GitHub run: | dotnet nuget push ${{env.PROJECT_NAME}}.${{ env.VERSION }}.nupkg --api-key ${{ secrets.GIHUB_NUGET_AUTH_TOKEN }} --skip-duplicate --source ${{ env.GITHUB_PACKAGES_URL }} dotnet nuget push ${{env.PROJECT_NAME}}.Api.${{ env.VERSION }}.nupkg --api-key ${{ secrets.GIHUB_NUGET_AUTH_TOKEN }} --skip-duplicate --source ${{ env.GITHUB_PACKAGES_URL }} - name: Push package to Nuget run: | dotnet nuget push ${{env.PROJECT_NAME}}.${{ env.VERSION }}.nupkg --api-key ${{ secrets.NUGET_AUTH_TOKEN }} --skip-duplicate --source ${{ env.NUGET_SOURCE_URL }} dotnet nuget push ${{env.PROJECT_NAME}}.Api.${{ env.VERSION }}.nupkg --api-key ${{ secrets.NUGET_AUTH_TOKEN }} --skip-duplicate --source ${{ env.NUGET_SOURCE_URL }} ================================================ FILE: .github/workflows/drones-release-windows.yml ================================================ name: Build and Release Drones For Windows on: push: tags: - 'v[0-9]+.[0-9]+.[0-9]+' - 'v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+' - 'v[0-9]+.[0-9]+.[0-9]+-rc' env: NUGET_SOURCE_URL: 'https://api.nuget.org/v3/index.json' GITHUB_PACKAGES_URL: 'https://nuget.pkg.github.com/asv-soft/index.json' PROJECT_NAME: 'Asv.Drones' PROPS_VERSION_VAR_NAME: 'ApiVersion' jobs: build-and-release: runs-on: windows-latest steps: - name: Checkout code uses: actions/checkout@v3 - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: '10.x.x' - name: Add NuGet source run: dotnet nuget add source ${{ env.GITHUB_PACKAGES_URL }} --username '${{secrets.USER_NAME}}' --password '${{secrets.GIHUB_NUGET_AUTH_TOKEN}}' --store-password-in-clear-text - name: Install dependencies run: | dotnet restore ./src/${{ env.PROJECT_NAME }}/${{ env.PROJECT_NAME }}.csproj dotnet restore ./src/${{ env.PROJECT_NAME }}.Desktop/${{ env.PROJECT_NAME }}.Desktop.csproj - name: Build run: | dotnet build ./src/${{ env.PROJECT_NAME }}/${{ env.PROJECT_NAME }}.csproj --no-restore dotnet build ./src/${{ env.PROJECT_NAME }}.Desktop/${{ env.PROJECT_NAME }}.Desktop.csproj --no-restore - name: Set version variable env: TAG: ${{ github.ref_name }} run: echo "VERSION=${TAG#v}" >> $GITHUB_ENV # here you must define path to your .csproj - name: Publish project for installer run: dotnet publish ./src/${{ env.PROJECT_NAME }}.Desktop/${{ env.PROJECT_NAME }}.Desktop.csproj -c Release -o ./publish/app - name: Sign app files uses: dlemstra/code-sign-action@v1 with: certificate: '${{ secrets.WINDOWS_SIGNING_CERTIFICATE }}' password: '${{ secrets.WINDOWS_SIGNING_PASSWORD }}' folder: './publish/app' recursive: true description: 'Sign The App' - name: Install NSIS run: | choco install nsis #here you must define path to your .nsi file (it is used for installer setup and creation) - name: Create EXE installer uses: joncloud/makensis-action@v4 with: script-file: win-64-install.nsi - name: Sign the installer uses: dlemstra/code-sign-action@v1 with: certificate: '${{ secrets.WINDOWS_SIGNING_CERTIFICATE }}' password: '${{ secrets.WINDOWS_SIGNING_PASSWORD }}' files: | AsvDronesInstaller.exe description: 'Sign The Installer' - name: List output files run: Get-ChildItem -Path ./publish/app -Force - name: Create Release id: create_release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GIHUB_NUGET_AUTH_TOKEN }} RELEASE_BODY: ${{ steps.create-release-notes.outputs.release-notes }} with: tag_name: ${{ github.ref }} release_name: Release ${{ github.ref }} draft: false prerelease: ${{ contains(github.ref, 'rc') }} - name: Upload Release Asset uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GIHUB_NUGET_AUTH_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: ./AsvDronesInstaller.exe asset_name: asv-drones-${{ github.ref_name }}-setup-windows-64.exe asset_content_type: application/vnd.microsoft.portable-executable ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.rsuser *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Mono auto generated files mono_crash.* # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ [Ll]ogs/ # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # Visual Studio 2017 auto generated files Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUnit *.VisualState.xml TestResult.xml nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ # .NET Core project.lock.json project.fragment.lock.json artifacts/ # StyleCop StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c *_h.h *.ilk *.meta *.obj *.iobj *.pch *.pdb *.ipdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *_wpftmp.csproj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # Visual Studio Trace Files *.e2e # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # AxoCover is a Code Coverage Tool .axoCover/* !.axoCover/settings.json # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # NuGet Symbol Packages *.snupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx *.appxbundle *.appxupload # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !?*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm ServiceFabricBackup/ *.rptproj.bak # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings *.rptproj.rsuser *- [Bb]ackup.rdl *- [Bb]ackup ([0-9]).rdl *- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # CodeRush personal settings .cr/personal # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Tabs Studio *.tss # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs # OpenCover UI analysis results OpenCover/ # Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log *.binlog # NVidia Nsight GPU debugger configuration file *.nvuser # MFractors (Xamarin productivity tool) working folder .mfractor/ # Local History for Visual Studio .localhistory/ # BeatPulse healthcheck temp database healthchecksdb # Backup folder for Package Reference Convert tool in Visual Studio 2017 MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ ================================================ FILE: .husky/pre-commit ================================================ #!/bin/sh . "$(dirname "$0")/_/husky.sh" ## husky task runner examples ------------------- ## Note : for local installation use 'dotnet' prefix. e.g. 'dotnet husky' ## run all tasks #husky run ### run all tasks with group: 'group-name' #husky run --group group-name ## run task with name: 'task-name' #husky run --name task-name ## pass hook arguments to task #husky run --args "$1" "$2" ## or put your custom commands ------------------- #echo 'Husky.Net is awesome!' dotnet husky run --group check dotnet husky run --group check ================================================ FILE: .husky/task-runner.json ================================================ { "$schema": "https://alirezanet.github.io/Husky.Net/schema.json", "tasks": [ { "name": "Run csharpier", "command": "dotnet", "args": [ "csharpier", "format", "." ], "group": "prettier" }, { "name": "Run csharpier check", "command": "dotnet", "args": [ "csharpier", "check", "." ], "group": "check" } ] } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 Asv Soft LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ![linkedin](https://github.com/user-attachments/assets/4fa5221e-7ae5-4b6b-98a8-1c1e39b49afb) [//]: [![Release](https://github.com/asv-soft/asv-drones/actions/workflows/ReleaseDeployAction.yml/badge.svg)](https://github.com/asv-soft/asv-drones/actions/workflows/ReleaseDeployAction.yml) ## 1. Introduction Asv.Drones: Empowering Innovation in Unmanned Aerial Systems Welcome to Asv.Drones, an advanced and modular open-source application designed to revolutionize the field of Unmanned Aerial Systems (UAS). Committed to fostering innovation and collaboration, Asv.Drones is not just a drone application; it's a community-driven platform that opens the doors to limitless possibilities. Key Features: 1. **Modularity:** Asv.Drones embraces a modular architecture, allowing users to tailor the application to their specific needs. Each module serves a distinct purpose, contributing to the overall functionality and versatility of the platform. 2. **Open Source Philosophy:** Transparency and collaboration lie at the heart of Asv.Drones. The entire application, along with its constituent modules, is open source. This means that not only can users benefit from the software, but they can also actively contribute to its enhancement and evolution. 3. **Modules Overview:** - **[Asv.Drones.Gbs](https://github.com/asv-soft/asv-drones-gbs) (Ground Base Station Service):** This module provides a robust ground base station service, ensuring seamless communication between the drone and the operator on the ground. Open source nature encourages customization for specific ground station requirements. - **Obsolete [Asv.Drones.Sdr](https://github.com/asv-soft/asv-drones-sdr) (SDR Payload Example Project):** Explore the possibilities of Software-Defined Radio (SDR) payloads with this open-source example project. Asv.Drones.Sdr serves as a foundation for integrating cutting-edge SDR technologies into unmanned aerial systems. - **[Asv.Gnss](https://github.com/asv-soft/asv-gnss) (GNSS Library):** The Asv.Gnss module is a comprehensive GNSS library that parses RTCMv2, RTCMv3 and NMEA protocols. It goes a step further by providing control over receivers through SBF, ComNav and UBX protocols, all tailored for .NET environments. - **[Asv.Mavlink](https://github.com/asv-soft/asv-mavlink) (MAVLink Library for .NET 9.0):** For seamless communication and control, Asv.MAVLink is a dedicated library compatible with .NET 9.0. It ensures that your drone's communication adheres to the MAVLink protocol standards. - **[Asv.Common](https://github.com/asv-soft/asv-common):** Asv.Common serves as the backbone, offering common types and extensions for all Asv-based libraries. It streamlines development, ensuring consistency and efficiency across different modules. - **[Asv.Avalonia](https://github.com/asv-soft/asv-avalonia):** Asv.Avalonia is a custom framework built on top of Avalonia. It defines the fundamental rules for cross-platform applications and allows for their rapid assembly. Out of the box, the framework provides an event system for ViewModels, a powerful undo/redo mechanism for various user actions, a theme with diverse styles, a cross-platform dialog system and more. Asv.Avalonia features various additional modules that extend its functionality. Here's a schematic representation of the whole project:

structure

4. **Community Collaboration:** Asv.Drones thrives on community collaboration. Developers, enthusiasts and innovators are encouraged to contribute, share insights and collectively shape the future of unmanned aerial systems. Embark on a journey of exploration, experimentation and innovation with Asv.Drones. Whether you're a developer, researcher or drone enthusiast, this open-source platform invites you to redefine the possibilities of unmanned aerial systems.

win win packet-viewer connections settings

## 2. Different sets of components Asv.Drones can work with different combinations of its components ### Example Of Usage With GBS **Ground Base Station Integration:** Asv.Drones offers seamless integration with ground base stations through our proprietary implementation called [Asv.Drones.Gbs](https://github.com/asv-soft/asv-drones-gbs). Built to operate via the MAVLink protocol, Asv.Drones.Gbs allows users to remotely manage and monitor drone operations from a centralized platform. Moreover, any other ground base station software compatible with MAVLink can seamlessly interface with our application, ensuring flexibility and interoperability across different systems (development of additional UI controls may be required). With Asv.Drones.Gbs, users can plan missions, monitor telemetry data and adjust flight parameters with ease. To connect to a gbs, create a new connection (usually tcp) in the connection settings.
gbs-packets
## 3. Getting Started ### Setting Up the Development Environment To ensure a smooth development experience, follow the steps below to set up your development environment: ### 3.1 Prerequisites: - **Operating System:** This project is compatible with Windows, macOS and Linux. Ensure that your development machine runs one of these supported operating systems. - **IDE (Integrated Development Environment):** We recommend using [Visual Studio](https://visualstudio.microsoft.com/) or [JetBrains Rider](https://www.jetbrains.com/rider/) as your IDE for C# development. - Make sure to install the necessary extensions and plugins for a better development experience. ### 3.2 .NET Installation: - This project is built using [.NET 9.0](https://dotnet.microsoft.com/download/dotnet/9.0), the latest version of the .NET platform. We recommend installing .NET 9.0 by following the instructions provided on the official [.NET website](https://dotnet.microsoft.com/download/dotnet/9.0). ```bash # Check your current .NET version dotnet --version ``` ### 3.3 Version Control: - If you haven't already, install a version control system such as [Git](https://git-scm.com/) to track changes and collaborate with other developers. ### 3.4 Clone the Repository: - Clone the project repository to your local machine using the following command: ```bash git clone https://github.com/asv-soft/asv-drones.git ``` ### 3.5 Restore Dependencies: - Navigate to the platform project directory and restore the required dependencies. There are three possible platform directories to build and debug our app: __Asv.Drones.Desktop__, __Asv.Drones.Android__, __Asv.Drones.iOS__. Currently, we support only the desktop platform. For example, we will use __Asv.Drones.Desktop__ platform, so you have to execute the following command: ```bash cd asv-drones/src/Asv.Drones.Desktop dotnet workload restore dotnet workload repair ``` ### 3.6 Build and Run: - After restoring, you have to build the project to ensure that everything is set up correctly, and if it's not - try to restore workloads again: ```bash dotnet build ``` - Run the project: ```bash dotnet run ``` Congratulations! Your development environment is now set up, and you are ready to start contributing to the project. If you encounter any issues during the setup process, refer to the project's documentation or reach out to the development team for assistance. ### Building for Android Coming soon... ## 4. Code Structure The organization of the codebase plays a crucial role in maintaining a clean, scalable and easily understandable project. This section outlines the structure of our codebase, highlighting key directories and their purposes. ### 4.1 Solution Organization Our solution is organized the following way: - **`src/`:** This directory contains the source code of the application. The code is further organized into projects, each residing in its own subdirectory. The goal is to promote modularity and maintainability. ``` src/ ├── Asv.Drones.Android/ ├── Asv.Drones.Desktop/ ├── Asv.Drones.iOS/ ├── Asv.Drones/ │ ├── Commands/ │ ├── Controls/ │ ├── Shell/ │ └── ... └── Asv.Drones.Api/ ├── Commands/ ├── Controls/ ├── Shell/ └── ... ### 4.2 Naming Conventions Consistent naming conventions are essential for code readability. Throughout the codebase, we follow the guidelines outlined [in our documentation](https://docs.asv.me/use-cases/for-developers) These conventions contribute to a unified and coherent codebase. By adhering to this organized structure and naming conventions, we aim to create a codebase that is easy to navigate, scalable and conducive to collaboration among developers. ## 5. Coding Style Maintaining a consistent coding style across the project enhances readability, reduces errors and facilitates collaboration. The following guidelines outline our preferred coding style for C#: **Note:** We have auto formatters in our project to make your life easier. Read more about them in the husky section. ### 5.1 C# Coding Style #### 5.1.1 Formatting - **Indentation:** Use tabs for indentation. Each level of indentation should consist of one tab. - **Brace Placement:** Place opening braces on the same line as the statement they belong to, and closing braces on a new line. ```c# // Good if (condition) { // Code here } // Bad if (condition) { // Code here } ``` #### 5.1.2 Naming Conventions - **Pascal Case:** Use Pascal case for class names, method names and property names. ```c# public class MyClass { public void MyMethod() { // Code here } public int MyProperty { get; set; } } ``` #### 5.1.3 Language Features - **Expression-bodied Members:** Utilize expression-bodied members for concise one-liners. ```c# // Good public int CalculateSquare(int x) => x * x; // Bad public int CalculateSquare(int x) { return x * x; } ``` - **Null Conditional Operator:** Use the null conditional operator (`?.`) for safe property or method access. ```c# // Good int? length = text?.Length; // Bad int length = (text != null) ? text.Length : 0; // or int length = text!.Length; ``` #### 5.1.4 Special Cases Usually you place public members after private members, but we have some exceptions: - **Page constants:** We place page ids and viewmodel ids at the top of the class. ```c# public const string PageId = "files.browser"; public const MaterialIconKind PageIcon = MaterialIconKind.FolderEye; ``` - **Export information:** We place export information at the end of the class. ```c# ``` #### 5.1.5 Husky We use husky to make sure that our code is formatted correctly before committing. You may use it for your own code. 1. Go to the src folder 2. run: ```bash dotnet tool restore ``` 3. run (use husky-unix instead of husky if you are on linux or macOS): ```bash dotnet r husky ``` 4. run the following command to format the code: ```bash dotnet husky run ``` ### 5.2 Documentation #### 5.2.1 Comments - **XML Documentation:** Include XML comments for classes, methods, and properties to provide comprehensive documentation. ```c# /// /// Represents a sample class. /// public class SampleClass { /// /// Calculates the sum of two numbers. /// /// The first number. /// The second number. /// The sum of the two numbers. public int Add(int a, int b) { // Code here } } ``` #### 5.2.2 Code Comments - Use comments sparingly and focus on explaining complex or non-intuitive code sections. By adhering to these coding style guidelines, we aim to create code that is straightforward to read, understand, and maintain. ## 6. Version Control Version control is a fundamental aspect of our development process, providing a systematic way to track changes, collaborate with team members and manage the evolution of our codebase. We use Git as our version control system. ### 6.1 Branching Strategy #### 6.1.1 Feature Branches For each new feature or bug fix, create a dedicated feature branch. The branch name should be descriptive of the feature or issue it addresses. ```bash # Example: Creating a new feature branch git checkout -b feature/my-new-feature ``` #### 6.1.2 Hotfix Branches In case of critical issues in the production environment, create a hotfix branch. This allows for a quick resolution without affecting the main development branch. ```bash # Example: Creating a hotfix branch git checkout -b hotfix/1.0.1 ``` ### 6.2 Commit Messages Write clear and concise commit messages that convey the purpose of the change. Follow these guidelines: - Start with a verb in the imperative mood (e.g., "Add," "Fix," "Update"). - Keep messages short but descriptive. - Use present tense. - Use feat, fix or chore prefixes to indicate the type of change. Example: ```bash # Good git commit -m "fix: add user authentication feature" # Bad git commit -m "Updated stuff" ``` ### 6.3 Pull Requests Before merging changes into the main branch, create a pull request (PR). This allows for code review and ensures that changes adhere to coding standards. - Assign reviewers to the PR. - Include a clear description of the changes. - Ensure that automated tests pass before merging. ### 6.4 Merging Strategy Adopt a merging strategy based on the nature of the changes: - **Feature Branches:** Merge feature branches into the main branch after code review and approval. - **Release Branches:** Merge release branches into the main branch and tag the commit for the release. ```bash # Example: Merging a feature branch git checkout main git merge --no-ff feature/my-new-feature ``` ### 6.5 Repository Hosting Our Git repository is hosted on [GitHub](https://github.com/asv-soft/asv-drones). Ensure that you have the necessary permissions and follow the best practices for repository management. By following these version control practices, we aim to maintain a well-organized and collaborative development process. ## 7. Build and Deployment The build and deployment processes are crucial parts of our development workflow. This section outlines the steps for building the project and deploying it using GitHub Actions. ### 7.1 Build Process To compile the project, use the following command: ```bash dotnet build ``` This command compiles the code and produces executable binaries. ### 7.2 Deployment using GitHub Releases Our application is deployed using [GitHub Actions](https://docs.github.com/en/actions). The latest release can be found [here](https://github.com/asv-soft/asv-drones/releases). ## 8. Contributing We welcome contributions from the community to help enhance and improve our project. Before contributing, please take a moment to review this guide. ### 8.1 Code Reviews All code changes undergo a review process to ensure quality and consistency. Here are the steps to follow: 1. **Fork the Repository:** Start by forking the repository to your own GitHub account. 2. **Create a Feature Branch:** Create a new branch for your feature or bug fix. ```bash git checkout -b feature/my-feature ``` 3. **Commit Changes:** Make your changes, commit them with clear and concise messages, and push the branch to your forked repository. ```bash git commit -m "feat: add new feature" git push origin feature/my-feature ``` 4. **Squash your commit:** Squash your commits into a single commit before submitting a pull request. ```bash git rebase -i main # squash your commits into a single commit by leaving the first line as "pick" and changing the rest to "squash" git push --force ``` 5**Open a Pull Request (PR):** Submit a pull request to the main repository, detailing the changes made and any relevant information. Ensure your PR adheres to the established coding standards. 6**Code Review:** Participate in the code review process by responding to feedback and making necessary adjustments. Addressing comments promptly helps streamline the review process. 7**Merge:** Once the code review is complete and the changes are approved, your pull request will be merged into the main branch. ### 8.2 Submitting Changes Before submitting changes, ensure the following: - **Coding Standards:** Adhere to the coding standards and guidelines outlined in this document. - **Tests:** If applicable, include tests for your changes and ensure that existing tests pass. - **Documentation:** Update relevant documentation, including code comments and external documentation, to reflect your changes. ### 8.3 Communication For larger changes or feature additions, it's beneficial to discuss the proposed changes beforehand. Engage with the community through: - **Opening an Issue:** Discuss your proposed changes by opening an issue. This provides an opportunity for community input before investing significant time in development. - **Joining Discussions:** Participate in existing discussions related to the project. Your insights and feedback are valuable. ### 8.4 Contributor License Agreement (CLA) By contributing to this project, you agree that your contributions will be licensed under the project's license. If a Contributor License Agreement (CLA) is required, it will be provided in the repository. We appreciate your contributions, and together we can make this project even better! ## 9. Code Documentation Clear and comprehensive code documentation is essential for ensuring that developers can easily understand, use and contribute to the project. Follow these guidelines for documenting your code: ### 9.1 Inline Comments Use inline comments to explain specific sections of your code, especially for complex logic or non-intuitive implementations. Follow these principles: - **Clarity:** Write comments that enhance code comprehension. If a piece of code is not self-explanatory, provide comments explaining the reasoning or intention. - **Conciseness:** Keep comments concise and to the point. Avoid unnecessary comments that do not add value. - **Update Comments:** Regularly review and update comments to reflect any changes in the code. Outdated comments can be misleading. Example: ```c# // Calculate the sum of two numbers int CalculateSum(int a, int b) { return a + b; } ``` ### 9.2 XML Documentation For classes, methods, properties and other significant code elements, use XML documentation comments to provide comprehensive information. Follow these guidelines: - **Summary:** Provide a summary that succinctly describes the purpose of the class or member. - **Parameters:** Document each parameter, specifying its purpose and any constraints. - **Returns:** If applicable, document the return value and its significance. - **Examples:** Include examples that demonstrate how to use the class or member. Example: ```c# /// /// Represents a utility class for mathematical operations. /// public class MathUtility { /// /// Calculates the sum of two numbers. /// /// The first number. /// The second number. /// The sum of the two numbers. public int CalculateSum(int a, int b) { return a + b; } } ``` ### 9.3 Consistency Ensure consistency in your documentation style across the codebase. Consistent documentation makes it easier for developers to navigate and understand the project. By following these documentation guidelines, we aim to create a codebase that is not only functional but also accessible and easily maintainable for all contributors. ## 10. Security Ensuring the security of our software is paramount to maintaining the integrity and confidentiality of user data. Developers should adhere to best practices and follow guidelines outlined in this section. ### 10.1 Code Security Practices #### 10.1.1 Input Validation Always validate and sanitize user input to prevent injection attacks and ensure the integrity of your application. ```c# // Example for C# public ActionResult ProcessUserInput(string userInput) { if (string.IsNullOrWhiteSpace(userInput)) { // Handle invalid input } // Process input } ``` #### 10.1.2 Authentication and Authorization Implement secure authentication and authorization mechanisms to control access to sensitive functionalities and data. Leverage industry-standard protocols like OAuth when applicable. #### 10.1.3 Secure Communication Ensure that communication between components, APIs and external services is encrypted using secure protocols (e.g., HTTPS). ### 10.2 Dependency Security #### 10.2.1 Dependency Scanning Regularly scan and update dependencies to identify and address security vulnerabilities. Leverage tools and services that provide automated dependency analysis. #### 10.2.2 Minimal Dependencies Keep dependencies to a minimum and only include libraries and packages that are actively maintained and have a good security track record. ### 10.3 Data Protection #### 10.3.1 Encryption Sensitive data, both at rest and in transit, should be encrypted. Use strong encryption algorithms and ensure proper key management. #### 10.3.2 Data Backups Implement regular data backup procedures to prevent data loss in the event of security incidents or system failures. ### 10.4 Secure Coding Standards Adhere to secure coding standards to mitigate common vulnerabilities. Follow principles such as the [OWASP Top Ten](https://owasp.org/www-project-top-ten/) to address security concerns in your codebase. ### 10.5 Reporting Security Issues If you discover a security vulnerability or have concerns about the security of the project, please report it immediately to our team at [our telegram channel](https://t.me/asvsoft). Do not disclose security-related issues publicly until they have been addressed. ### 9.6 Security Training Encourage ongoing security training for all team members to stay informed about the latest security threats and best practices. Knowledgeable developers are key to maintaining a secure codebase. By incorporating security practices into our development process, we aim to create a robust and secure software environment for our users. ## 11. License This project is licensed under the terms of the MIT License. A copy of the MIT License is provided in the [LICENSE](https://github.com/asv-soft/asv-drones?tab=MIT-1-ov-file#) file. ### MIT License ``` MIT License Copyright (c) 2023 Asv Soft LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` ### Using the MIT License The MIT License is a permissive open-source license that allows for the free use, modification and distribution of the software. It is important to review and understand the terms of the license before using, contributing to or distributing this software. By contributing to this project, you agree that your contributions will be licensed under the MIT License. For more details about the MIT License, please visit [opensource.org/licenses/MIT](https://opensource.org/licenses/MIT). ## 12. Contact If you have questions, suggestions, or need assistance with the project, we encourage you to reach out through the following channels: ### 12.1 Telegram Channel Visit our Telegram channel: [ASVSoft on Telegram](https://t.me/asvsoft) Feel free to join our Telegram community to engage in discussions, seek help, or share your insights. ### 12.2 GitHub Issues For bug reports, feature requests or any project-related discussions, please use our GitHub Issues: [Project Issues on GitHub](https://github.com/asv-soft/asv-drones/issues) Our GitHub repository is the central hub for project-related discussions and issue tracking. Please check existing issues before creating new ones to avoid duplication. ### 12.3 Security Concerns If you discover a security vulnerability or have concerns about the security of the project, please report it immediately to our telegram channel: [ASVSoft on Telegram](https://t.me/asvsoft). Do not disclose security-related issues publicly until they have been addressed. ### 12.4 General Inquiries For general inquiries or if you prefer email communication, you can reach us at [me@asv.me](mailto:me@asv.me). We value your feedback and contributions, and we look forward to hearing from you! ================================================ FILE: api_build.bat ================================================ @echo off setlocal enabledelayedexpansion rem ====== projects ====== set project=Asv.Drones.Gui.Api set "file=.\src\Directory.Build.props" :: ApiVersion for /f "tokens=2 delims=> " %%a in ('findstr /i /c:"" "%file%"') do ( set "line=%%a" for /f "tokens=1 delims=<" %%b in ("!line!") do ( set "ApiVersion=%%b" ) ) :: if defined ApiVersion ( echo ApiVersion: %ApiVersion% dotnet restore ./src/%project%/%project%.csproj dotnet build /p:SolutionDir=../;ProductVersion=%ApiVersion% ./src/%project%/%project%.csproj -c Release dotnet pack ./src/%project%/%project%.csproj -c Release ) else ( echo ApiVersion not found ) endlocal pause ================================================ FILE: api_publish_github.bat ================================================ @echo off setlocal enabledelayedexpansion rem ====== projects ====== set project=Asv.Drones.Gui.Api set "file=.\src\Directory.Build.props" :: ApiVersion for /f "tokens=2 delims=> " %%a in ('findstr /i /c:"" "%file%"') do ( set "line=%%a" for /f "tokens=1 delims=<" %%b in ("!line!") do ( set "ApiVersion=%%b" ) ) :: if defined ApiVersion ( echo ApiVersion: %ApiVersion% cd src\%project%\bin\Release\ rem dotnet nuget push %project%.%ApiVersion%.nupkg --skip-duplicate --source https://api.nuget.org/v3/index.json dotnet nuget push %project%.%ApiVersion%.nupkg --skip-duplicate --source https://nuget.pkg.github.com/asv-soft/index.json ) else ( echo ApiVersion not found ) endlocal pause ================================================ FILE: publish.bat ================================================ cd publish for /d %%i in (".\*") do ( rmdir /s /q "%%i" ) cd ../src/Asv.Drones.Gui.Desktop dotnet publish -c Release -r win-arm --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o ~/../../../publish/win-arm/app dotnet publish -c Release -r win-arm64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o ~/../../../publish/win-arm64/app dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o ~/../../../publish/win-x64/app dotnet publish -c Release -r win-x86 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o ~/../../../publish/win-x86/app dotnet publish -c Release -r linux-arm --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o ~/../../../publish/linux-arm/app dotnet publish -c Release -r linux-arm64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o ~/../../../publish/linux-arm64/app dotnet publish -c Release -r linux-musl-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o ~/../../../publish/linux-musl-x64/app dotnet publish -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o ~/../../../publish/linux-x64/app dotnet publish -c Release -r osx-arm64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o ~/../../../publish/osx-arm64/app dotnet publish -c Release -r osx-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o ~/../../../publish/osx-x64/app cd ../../publish del /S *.pdb cd win-arm/app move Asv.Drones.Gui.Desktop.exe asv-drones-win-arm.exe cd ../../win-arm64/app move Asv.Drones.Gui.Desktop.exe asv-drones-win-arm64.exe cd ../../win-x64/app move Asv.Drones.Gui.Desktop.exe asv-drones-win-x64.exe cd ../../win-x86/app move Asv.Drones.Gui.Desktop.exe asv-drones-win-x86.exe cd ../../linux-arm/app move Asv.Drones.Gui.Desktop asv-drones-linux-arm cd ../../linux-arm64/app move Asv.Drones.Gui.Desktop asv-drones-linux-arm64 cd ../../linux-musl-x64/app move Asv.Drones.Gui.Desktop asv-drones-linux-musl-x64 cd ../../linux-x64/app move Asv.Drones.Gui.Desktop asv-drones-linux-x64 cd ../../osx-arm64/app move Asv.Drones.Gui.Desktop asv-drones-osx-arm64 cd ../../osx-x64/app move Asv.Drones.Gui.Desktop asv-drones-osx-x64 cd ../../.. setlocal enabledelayedexpansion set "xmlFile=src\Directory.Build.props" for /f "tokens=2 delims=<> " %%a in ('findstr /i "" "%xmlFile%"') do ( set "productVersion=%%a" ) set "issFile=win-arm-install.iss" set "tempFile=%temp%\temp.iss" for /f "tokens=*" %%a in ('type "%issFile%"') do ( set "line=%%a" echo !line! | findstr /C:"#define MyAppVersion" > nul if !errorlevel! == 0 ( echo #define MyAppVersion "%productVersion%" >> "%tempFile%" ) else ( echo !line! >> "%tempFile%" ) ) move /y "%tempFile%" "%issFile%" > nul set "issFile=win-arm64-install.iss" set "tempFile=%temp%\temp.iss" for /f "tokens=*" %%a in ('type "%issFile%"') do ( set "line=%%a" echo !line! | findstr /C:"#define MyAppVersion" > nul if !errorlevel! == 0 ( echo #define MyAppVersion "%productVersion%" >> "%tempFile%" ) else ( echo !line! >> "%tempFile%" ) ) move /y "%tempFile%" "%issFile%" > nul set "issFile=win-x64-install.iss" set "tempFile=%temp%\temp.iss" for /f "tokens=*" %%a in ('type "%issFile%"') do ( set "line=%%a" echo !line! | findstr /C:"#define MyAppVersion" > nul if !errorlevel! == 0 ( echo #define MyAppVersion "%productVersion%" >> "%tempFile%" ) else ( echo !line! >> "%tempFile%" ) ) move /y "%tempFile%" "%issFile%" > nul set "issFile=win-x86-install.iss" set "tempFile=%temp%\temp.iss" for /f "tokens=*" %%a in ('type "%issFile%"') do ( set "line=%%a" echo !line! | findstr /C:"#define MyAppVersion" > nul if !errorlevel! == 0 ( echo #define MyAppVersion "%productVersion%" >> "%tempFile%" ) else ( echo !line! >> "%tempFile%" ) ) move /y "%tempFile%" "%issFile%" > nul endlocal cd publish iscc ../win-arm-install.iss iscc ../win-arm64-install.iss iscc ../win-x86-install.iss iscc ../win-x64-install.iss wsl sed -i 's/\r//' linux_packages.sh wsl sed -i 's/\r//' osx_packages.sh wsl ../linux_packages.sh wsl ../osx_packages.sh ================================================ FILE: src/.aiassistant/rules/comments.md ================================================ --- apply: always --- ## Comment and Documentation Language - Write all code comments in English. - Write all XML documentation, Markdown documentation, README content, and other developer-facing documentation in English. - Do not use Russian or mixed-language comments or documentation. - Keep terminology consistent across code, comments, and documentation. - Use clear English names for types, members, variables, files, modules, and public APIs. ## Comment Quality - Prefer self-explanatory code over excessive comments. - Add comments only when they explain intent, constraints, assumptions, tradeoffs, or non-obvious behavior. - Do not add comments that only restate what the code already makes obvious. - Keep comments concise, accurate, and aligned with the current implementation. - Update or remove comments when the code changes so documentation never becomes misleading. ## Architecture and Design - Keep the architecture clean, modular, and easy to maintain. - Follow SOLID principles in design and implementation. - Give each class, service, and module a single, well-defined responsibility. - Prefer composition over inheritance unless inheritance is clearly justified. - Minimize coupling and keep related behavior cohesive. - Separate domain logic from UI, infrastructure, persistence, and framework-specific concerns. - Depend on abstractions at system boundaries when this improves testability, extensibility, or clarity. - Keep public APIs explicit, stable, and easy to understand. - Eliminate duplicated logic through extraction or refactoring instead of copying behavior. - Avoid god objects, hidden side effects, and unclear ownership of responsibilities. --- apply: always --- Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed. **Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment. ## 1. Think Before Coding **Don't assume. Don't hide confusion. Surface tradeoffs.** Before implementing: - State your assumptions explicitly. If uncertain, ask. - If multiple interpretations exist, present them - don't pick silently. - If a simpler approach exists, say so. Push back when warranted. - If something is unclear, stop. Name what's confusing. Ask. ## 2. Simplicity First **Minimum code that solves the problem. Nothing speculative.** - No features beyond what was asked. - No abstractions for single-use code. - No "flexibility" or "configurability" that wasn't requested. - No error handling for impossible scenarios. - If you write 200 lines and it could be 50, rewrite it. Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. ## 3. Surgical Changes **Touch only what you must. Clean up only your own mess.** When editing existing code: - Don't "improve" adjacent code, comments, or formatting. - Don't refactor things that aren't broken. - Match existing style, even if you'd do it differently. - If you notice unrelated dead code, mention it - don't delete it. When your changes create orphans: - Remove imports/variables/functions that YOUR changes made unused. - Don't remove pre-existing dead code unless asked. The test: Every changed line should trace directly to the user's request. ## 4. Goal-Driven Execution **Define success criteria. Loop until verified.** Transform tasks into verifiable goals: - "Add validation" → "Write tests for invalid inputs, then make them pass" - "Fix the bug" → "Write a test that reproduces it, then make it pass" - "Refactor X" → "Ensure tests pass before and after" For multi-step tasks, state a brief plan: ``` 1. [Step] → verify: [check] 2. [Step] → verify: [check] 3. [Step] → verify: [check] ``` Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. --- **These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes. ================================================ FILE: src/.editorconfig ================================================ [*.cs] dotnet_diagnostic.RCS1037.severity = none dotnet_diagnostic.RCS1251.severity = none [*.axaml] csharpier_formatter = xml indent_size = 4 ================================================ FILE: src/.gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.rsuser *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Mono auto generated files mono_crash.* # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ [Ww][Ii][Nn]32/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ [Ll]ogs/ # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # Visual Studio 2017 auto generated files Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUnit *.VisualState.xml TestResult.xml nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ # .NET Core project.lock.json project.fragment.lock.json artifacts/ # Tye .tye/ # ASP.NET Scaffolding ScaffoldingReadMe.txt # StyleCop StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c *_h.h *.ilk *.meta *.obj *.iobj *.pch *.pdb *.ipdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *_wpftmp.csproj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # Visual Studio Trace Files *.e2e # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # AxoCover is a Code Coverage Tool .axoCover/* !.axoCover/settings.json # Coverlet is a free, cross platform Code Coverage Tool coverage*.json coverage*.xml coverage*.info # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # NuGet Symbol Packages *.snupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx *.appxbundle *.appxupload # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !?*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm ServiceFabricBackup/ *.rptproj.bak # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings *.rptproj.rsuser *- [Bb]ackup.rdl *- [Bb]ackup ([0-9]).rdl *- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # CodeRush personal settings .cr/personal # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Tabs Studio *.tss # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs # OpenCover UI analysis results OpenCover/ # Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log *.binlog # NVidia Nsight GPU debugger configuration file *.nvuser # MFractors (Xamarin productivity tool) working folder .mfractor/ # Local History for Visual Studio .localhistory/ # BeatPulse healthcheck temp database healthchecksdb # Backup folder for Package Reference Convert tool in Visual Studio 2017 MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ # Fody - auto-generated XML schema FodyWeavers.xsd ## ## Visual studio for Mac ## # globs Makefile.in *.userprefs *.usertasks config.make config.status aclocal.m4 install-sh autom4te.cache/ *.tar.gz tarballs/ test-results/ # Mac bundle stuff *.dmg *.app # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore # General .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore # Windows thumbnail cache files Thumbs.db ehthumbs.db ehthumbs_vista.db # Dump file *.stackdump # Folder config file [Dd]esktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msix *.msm *.msp # Windows shortcuts *.lnk # JetBrains Rider .idea/ *.sln.iml ## ## Visual Studio Code ## .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .qodo ================================================ FILE: src/.run/Publish win-x64.run.xml ================================================  ================================================ FILE: src/Asv.Drones/App.axaml ================================================ ================================================ FILE: src/Asv.Drones/App.axaml.cs ================================================ using Asv.Avalonia; using Avalonia.Markup.Xaml; namespace Asv.Drones; public partial class App : AsvApplication { public override void Initialize() { AvaloniaXamlLoader.Load(this); } } ================================================ FILE: src/Asv.Drones/Asv.Drones.csproj ================================================  $(TargetFramework) $(ProductVersion) $(ProductVersion) https://github.com/asv-soft https://github.com/asv-soft https://github.com/asv-soft enable preview true true ../CodeStyle.ruleset CS0169, CS0618, CS1502, CS1503, CS8524, CS8600, CS8601, CS8602, CS8603, CS8604, CS8625, CS8629, CS8762, CA1510, CA1851 true all runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive PublicResXFileCodeGenerator RS.Designer.cs True True RS.resx BurstDownloadDialogView.axaml Code ================================================ FILE: src/Asv.Drones/Asv.Drones.csproj.DotSettings ================================================  True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True False True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True ================================================ FILE: src/Asv.Drones/AsvDronesMixin.cs ================================================ using System; using Asv.Avalonia; using Asv.Avalonia.IO; using Asv.Drones.Api; using Asv.Drones.Plane; using Asv.Mavlink; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace Asv.Drones; public static class AsvDronesMixin { extension(IHostApplicationBuilder builder) { public IHostApplicationBuilder UseDronesApp(Action? configure = null) { configure ??= b => b.UseDefault(); configure(new Builder(builder)); return builder; } } public class Builder(IHostApplicationBuilder builder) { public IHostApplicationBuilder Parent => builder; public Builder UseDefault() { builder.Services.AddSingleton(); builder.Services.AddSingleton(); return UseMavlinkHost() .UseMavParams() .UseOptionalPacketViewer() .UseFlightMode() .UseExtendableFlightMode() .UseCommands() .UseFileBrowser() .UseSetupPage(); } public Builder UseControls() { builder.ViewLocator.RegisterViewFor(); builder.ViewLocator.RegisterViewFor< VelocityUavIndicatorViewModel, VelocityUavIndicator >(); builder.ViewLocator.RegisterViewFor< BatteryUavIndicatorViewModel, BatteryUavIndicator >(); return this; } public Builder UseCommands() { builder.Services.AddSingleton(); builder .RegisterMavlinkCommands() .Commands.Register() .Register() .Register() .Register() .Register() .Register() .Register() .Register() .Register() .Register() .Register() .Register() .Register() .Register() .Register() .Register() .Register() .Register() .Register() .Register() .Register() .Register() .Register() .Register() .Register() .Register() .Register() .Register() .Register(); return this; } public Builder UseMavlinkHost() { builder.Services.AddSingleton(); builder.Services.AddHostedService(svc => svc.GetRequiredService()); builder.Services.AddSingleton(svc => svc.GetRequiredService() ); builder.Services.AddSingleton(svc => svc.GetRequiredService() ); return this; } public Builder UseMavParams() { builder.ViewLocator.RegisterViewFor(); builder.ViewLocator.RegisterViewFor(); builder.ViewLocator.RegisterViewFor(); builder.ViewLocator.RegisterViewFor< MavParamAltitudeTextBoxViewModel, MavParamAltitudeTextBoxView >(); builder.ViewLocator.RegisterViewFor< MavParamLatLonTextBoxViewModel, MavParamLatLonTextBoxView >(); builder.ViewLocator.RegisterViewFor< MavParamAsciiCharViewModel, MavParamAsciiCharView >(); builder.ViewLocator.RegisterViewFor(); builder.Shell.Pages.Register( MavParamsPageViewModel.PageId ); builder.Shell.Pages.Home.UseItemExtension(); builder.ViewLocator.RegisterViewFor(); builder.ViewLocator.RegisterViewFor< TryCloseWithApprovalDialogViewModel, TryCloseWithApprovalDialogView >(); return this; } public Builder UseFileBrowser() { builder.Shell.Pages.Register( FileBrowserViewModel.PageId ); builder.Shell.Pages.Home.UseItemExtension(); builder.ViewLocator.RegisterViewFor< BurstDownloadDialogViewModel, BurstDownloadDialogView >(); return this; } public Builder UseFlightMode() { builder.Shell.Pages.Register( FlightPageViewModel.PageId ); builder.Shell.Pages.Home.UseExtension(); builder.Extensions.Register(); builder.Extensions.Register(); builder.ViewLocator.RegisterViewFor(); builder.ViewLocator.RegisterViewFor(); builder.ViewLocator.RegisterViewFor< SetAltitudeDialogViewModel, SetAltitudeDialogView >(); return this; } public Builder UseExtendableFlightMode() { // FlightMode builder.Shell.Pages.Register( FlightModePageViewModel.PageId ); builder.Shell.Pages.Home.UseExtension(); // Anchors builder.Extensions.Register(); // Factory for client device widgets builder.Services.AddSingleton(); // Create widgets for client devices builder.Extensions.Register(); // Widget for all drones builder.Services.AddSingleton< IClientDeviceWidgetCreationHandler, DroneWidgetCreationHandler >(); builder.Services.AddTransient(); builder.ViewLocator.RegisterViewFor(); // Sections for the drone Widget builder.Services.AddKeyedTransient( TelemetrySectionViewModel.SectionId ); builder.Extensions.Register< IDroneFlightWidget, DroneFlightWidgetTelemetrySectionExtension >(); builder.ViewLocator.RegisterViewFor(); builder.Services.AddKeyedTransient( AttitudeIndicatorSectionViewModel.SectionId ); builder.Extensions.Register< IDroneFlightWidget, DroneFlightWidgetExtensionAttitudeIndicatorSection >(); builder.ViewLocator.RegisterViewFor< AttitudeIndicatorSectionViewModel, AttitudeIndicatorSectionView >(); builder.Services.AddKeyedTransient( FlightControlSectionViewModel.SectionId ); builder.Extensions.Register< IDroneFlightWidget, DroneFlightWidgetFlightControlSectionExtension >(); builder.ViewLocator.RegisterViewFor< FlightControlSectionViewModel, FlightControlSectionView >(); // Test plugin widget builder.ViewLocator.RegisterViewFor(); builder.Extensions.Register(); // Test plane widget builder.Services.AddSingleton< IClientDeviceWidgetCreationHandler, PlaneWidgetCreationHandler >(); builder.Services.AddTransient(); builder.ViewLocator.RegisterViewFor(); // Test plane section builder.Services.AddKeyedTransient( PlaneSectionViewModel.SectionId ); builder.Extensions.Register(); builder.ViewLocator.RegisterViewFor(); return this; } public Builder UseOptionalPacketViewer() { builder.Shell.Pages.Register( PacketViewerViewModel.PageId ); builder.Shell.Pages.Home.UseExtension(); builder.ViewLocator.RegisterViewFor(); builder.Services.AddSingleton(); builder.ViewLocator.RegisterViewFor< SavePacketMessagesDialogViewModel, SavePacketMessagesDialogView >(); return this; } } } ================================================ FILE: src/Asv.Drones/Core/Commands/Behaviour/Remove/RemoveItemCommand.cs ================================================ using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Drones.Api; using Material.Icons; namespace Asv.Drones; public class RemoveItemCommand : ContextCommand, IRemoveItemCommand { public const string Id = IRemoveItemCommand.CommandId; private static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.RemoveItemCommand_CommandInfo_Name, Description = RS.RemoveItemCommand_CommandInfo_Description, Icon = MaterialIconKind.Delete, DefaultHotKey = "Shift + Delete", }; public override ICommandInfo Info => StaticInfo; protected override async ValueTask InternalExecute( ISupportRemove context, CommandArg newValue, CancellationToken cancel ) { // TODO: make removing items command undoable await context.RemoveAsync(cancel); return null; } } ================================================ FILE: src/Asv.Drones/Core/Commands/Behaviour/Rename/CommitRenameCommand.cs ================================================ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Drones.Api; using Material.Icons; namespace Asv.Drones; /// /// Executes with: /// - arg["old"] as Old Value. /// - arg["new"] as New Value. /// public class CommitRenameCommand : ContextCommand, ICommitRenameCommand { public const string Id = ICommitRenameCommand.CommandId; public const string OldValue = "old"; public const string NewValue = "new"; private static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.RenameItemCommand_CommandInfo_Name, Description = RS.RenameItemCommand_CommandInfo_Description, Icon = MaterialIconKind.Check, DefaultHotKey = "Enter", }; public override ICommandInfo Info => StaticInfo; public override async ValueTask InternalExecute( ISupportRename context, DictArg arg, CancellationToken cancel ) { arg.TryGetValue(OldValue, out var oldValue); arg.TryGetValue(NewValue, out var newValue); if (oldValue is not StringArg || newValue is not StringArg) { return null; } var oldString = oldValue.AsString(); var newString = newValue.AsString(); if (string.IsNullOrWhiteSpace(oldString) || string.IsNullOrWhiteSpace(newString)) { return null; } try { await context.RenameAsync(oldString, newString, cancel); } catch { return null; } return CommandArg.CreateDictionary( new Dictionary { { NewValue, CommandArg.CreateString(oldString) }, { OldValue, CommandArg.CreateString(newString) }, } ); } } ================================================ FILE: src/Asv.Drones/Core/Commands/FileBrowser/FileBrowserViewModel/FindFileCommand.cs ================================================ using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Material.Icons; namespace Asv.Drones; public class FindFileCommand : ContextCommand { public const string Id = $"{BaseId}.find_file"; private static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.FindFileOnLocalCommand_CommandInfo_Name, Description = RS.FindFileOnLocalCommand_CommandInfo_Description, Icon = MaterialIconKind.Magnify, DefaultHotKey = null, }; public override ICommandInfo Info => StaticInfo; protected override ValueTask InternalExecute( FileBrowserViewModel context, CommandArg arg, CancellationToken cancel ) { context.FindFileOnLocal(); return ValueTask.FromResult(null); } } ================================================ FILE: src/Asv.Drones/Core/Commands/FileBrowser/Items/CalculateCrc32Command.cs ================================================ using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Material.Icons; namespace Asv.Drones; public class CalculateCrc32Command : ContextCommand { public const string Id = $"{BaseId}.crc32"; private static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.CalculateCrc32Command_CommandInfo_Name, Description = RS.CalculateCrc32Command_CommandInfo_Description, Icon = MaterialIconKind.KeyOutline, DefaultHotKey = "Ctrl + Q", }; public override ICommandInfo Info => StaticInfo; protected override async ValueTask InternalExecute( IBrowserItemViewModel context, CommandArg newValue, CancellationToken cancel ) { await context.CalculateCrc32Async(cancel); return null; } } ================================================ FILE: src/Asv.Drones/Core/Commands/FileBrowser/Items/CreateDirectoryCommand.cs ================================================ using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Material.Icons; namespace Asv.Drones; public class CreateDirectoryCommand : ContextCommand { public const string Id = $"{BaseId}.create_directory"; private static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.CreateDirectoryCommand_CommandInfo_Name, Description = RS.CreateDirectoryCommand_CommandInfo_Description, Icon = MaterialIconKind.FolderAdd, DefaultHotKey = "Ctrl + N", }; public override ICommandInfo Info => StaticInfo; protected override async ValueTask InternalExecute( IBrowserItemViewModel context, CommandArg newValue, CancellationToken cancel ) { await context.CreateDirectoryAsync(cancel); return null; } } ================================================ FILE: src/Asv.Drones/Core/Commands/FileBrowser/OpenFileBrowserCommand.cs ================================================ using Asv.Avalonia; namespace Asv.Drones; public class OpenFileBrowserCommand(INavigationService nav) : OpenPageCommandBase(FileBrowserViewModel.PageId, nav) { #region Static public const string Id = $"{BaseId}.open.{FileBrowserViewModel.PageId}"; public static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.OpenFileBrowserCommand_CommandInfo_Name, Description = RS.OpenFileBrowserCommand_CommandInfo_Description, Icon = FileBrowserViewModel.PageIcon, DefaultHotKey = null, }; #endregion public override ICommandInfo Info => StaticInfo; } ================================================ FILE: src/Asv.Drones/Core/Commands/FileBrowser/Transfer/BurstDownloadItemCommand.cs ================================================ using System; using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Mavlink; using Material.Icons; namespace Asv.Drones; /// /// Executes with: /// - arg["src"] as string sourcePath. /// - arg["dst"] as string destinationPath. /// - arg["prt"] as int partSize. /// - arg["typ"] as string entryType. /// public class BurstDownloadItemCommand : TransferCommandBase { public const string Id = $"{BaseIdTransferCmd}.burst_download"; private static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.BurstDownloadItemCommand_CommandInfo_Name, Description = RS.BurstDownloadItemCommand_CommandInfo_Description, Icon = MaterialIconKind.TransferLeft, DefaultHotKey = null, }; public override ICommandInfo Info => StaticInfo; public override async ValueTask InternalExecute( ITransferFtpEntries context, DictArg newValue, CancellationToken cancel ) { if (!TryReadRequiredString(newValue, SourcePath, out var src)) { return null; } if (!TryReadRequiredString(newValue, DestinationPath, out var dst)) { return null; } if (!TryReadRequiredByte(newValue, PartSize, out var part)) { return null; } if (!TryReadRequiredEntryType(newValue, EntryType, out var type)) { return null; } await context.BurstDownloadItem(src, dst, part, type, cancel); return null; } } ================================================ FILE: src/Asv.Drones/Core/Commands/FileBrowser/Transfer/DownloadItemCommand.cs ================================================ using System; using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Mavlink; using Material.Icons; namespace Asv.Drones; /// /// Executes with: /// - arg["src"] as string sourcePath. /// - arg["dst"] as string destinationPath. /// - arg["prt"] as int partSize. /// - arg["typ"] as string entryType. /// public class DownloadItemCommand : TransferCommandBase { public const string Id = $"{BaseIdTransferCmd}.download"; private static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.DownloadItemCommand_CommandInfo_Name, Description = RS.DownloadItemCommand_CommandInfo_Description, Icon = MaterialIconKind.TransferLeft, DefaultHotKey = null, }; public override ICommandInfo Info => StaticInfo; public override async ValueTask InternalExecute( ITransferFtpEntries context, DictArg newValue, CancellationToken cancel ) { if (!TryReadRequiredString(newValue, SourcePath, out var src)) { return null; } if (!TryReadRequiredString(newValue, DestinationPath, out var dst)) { return null; } if (!TryReadRequiredByte(newValue, PartSize, out var part)) { return null; } if (!TryReadRequiredEntryType(newValue, EntryType, out var type)) { return null; } await context.DownloadItem(src, dst, part, type, cancel); return null; } } ================================================ FILE: src/Asv.Drones/Core/Commands/FileBrowser/Transfer/ITransferFtpEntries.cs ================================================ using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Mavlink; namespace Asv.Drones; public interface ITransferFtpEntries : IRoutable { ValueTask UploadItem( string source, string destination, FtpEntryType type, CancellationToken ct ); ValueTask DownloadItem( string source, string destination, byte partSize, FtpEntryType type, CancellationToken ct ); ValueTask BurstDownloadItem( string source, string destination, byte partSize, FtpEntryType type, CancellationToken ct ); } ================================================ FILE: src/Asv.Drones/Core/Commands/FileBrowser/Transfer/TransferCommandBase.cs ================================================ using System; using Asv.Avalonia; using Asv.Mavlink; namespace Asv.Drones; public abstract class TransferCommandBase : ContextCommand { protected const string BaseIdTransferCmd = $"{BaseId}.transfer"; public const string SourcePath = "src"; public const string DestinationPath = "dst"; public const string PartSize = "prt"; public const string EntryType = "typ"; protected static bool TryReadRequiredString(DictArg args, string key, out string value) { value = string.Empty; if (!args.TryGetValue(key, out var v)) { return false; } value = v.AsString(); return value.Length > 0; } protected static bool TryReadRequiredByte(DictArg args, string key, out byte value) { value = 0; if (!args.TryGetValue(key, out var v)) { return false; } var i = v.AsInt(); if (i is < byte.MinValue or > byte.MaxValue) { return false; } value = (byte)i; return true; } protected static bool TryReadRequiredEntryType( DictArg args, string key, out FtpEntryType entryType ) { entryType = default; return args.TryGetValue(key, out var v) && Enum.TryParse(v.AsString(), ignoreCase: true, out entryType); } } ================================================ FILE: src/Asv.Drones/Core/Commands/FileBrowser/Transfer/UploadItemCommand.cs ================================================ using System; using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Mavlink; using Material.Icons; namespace Asv.Drones; /// /// Executes with: /// - arg["src"] as string sourcePath. /// - arg["dst"] as string destinationPath. /// - arg["typ"] as string entryType. /// public class UploadItemCommand : TransferCommandBase { public const string Id = $"{BaseIdTransferCmd}.upload"; private static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.UploadItemCommand_CommandInfo_Name, Description = RS.UploadItemCommand_CommandInfo_Description, Icon = MaterialIconKind.TransferRight, DefaultHotKey = null, }; public override ICommandInfo Info => StaticInfo; public override async ValueTask InternalExecute( ITransferFtpEntries context, DictArg newValue, CancellationToken cancel ) { if (!TryReadRequiredString(newValue, SourcePath, out var src)) { return null; } if (!TryReadRequiredString(newValue, DestinationPath, out var dst)) { return null; } if (!TryReadRequiredEntryType(newValue, EntryType, out var type)) { return null; } await context.UploadItem(src, dst, type, cancel); return null; } } ================================================ FILE: src/Asv.Drones/Core/Commands/Flight/OpenFlight.cs ================================================ using Asv.Avalonia; namespace Asv.Drones; public class OpenFlightCommand(INavigationService nav) : OpenPageCommandBase(FlightModePageViewModel.PageId, nav) { #region Static public const string Id = $"{BaseId}.open.{FlightModePageViewModel.PageId}"; public static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = "Open Flight Mode (BETA)", Description = "Command opens Flight Mode (BETA)", Icon = FlightModePageViewModel.PageIcon, DefaultHotKey = null, // TODO: add after BETA }; #endregion public override ICommandInfo Info => StaticInfo; } ================================================ FILE: src/Asv.Drones/Core/Commands/Flight/OpenFlightMode.cs ================================================ using Asv.Avalonia; using Asv.Drones.Api; namespace Asv.Drones; public class OpenFlightModeCommand(INavigationService nav) : OpenPageCommandBase(FlightMode.PageId, nav) { #region Static public const string Id = $"{BaseId}.open.{FlightMode.PageId}"; public static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.OpenFlightModeCommand_CommandInfo_Name, Description = RS.OpenFlightModeCommand_CommandInfo_Description, Icon = FlightMode.PageIcon, DefaultHotKey = "Ctrl+F2", }; #endregion public override ICommandInfo Info => StaticInfo; } ================================================ FILE: src/Asv.Drones/Core/Commands/Flight/Widgets/UavWidget/AutoModeCommand.cs ================================================ using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Asv.IO; using Asv.Mavlink; using Material.Icons; namespace Asv.Drones; public class AutoModeCommand : ContextCommand // TODO: make basic class for commands that change the uav mode { #region Static public const string Id = $"{BaseId}.change.mode.auto"; internal static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.UavAction_AutoMode_Name, Description = RS.UavAction_AutoMode_Description, Icon = MaterialIconKind.Automatic, DefaultHotKey = null, }; #endregion public override ICommandInfo Info => StaticInfo; protected override async ValueTask InternalExecute( UavWidgetViewModel context, CommandArg newValue, CancellationToken cancel ) { var control = context.Device.GetMicroservice(); if (control == null) { return null; } await control.SetAutoMode(cancel); return null; } } ================================================ FILE: src/Asv.Drones/Core/Commands/Flight/Widgets/UavWidget/GuidedModeCommand.cs ================================================ using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Asv.IO; using Asv.Mavlink; using Material.Icons; namespace Asv.Drones; public class GuidedModeCommand : ContextCommand { #region Static public const string Id = $"{BaseId}.change.mode.guided"; internal static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.UavAction_GuidedMode, Description = RS.UavAction_GuidedMode_Description, Icon = MaterialIconKind.Controller, DefaultHotKey = null, }; #endregion public override ICommandInfo Info => StaticInfo; protected override async ValueTask InternalExecute( UavWidgetViewModel context, CommandArg newValue, CancellationToken cancel ) { var control = context.Device.GetMicroservice(); if (control == null) { return null; } await control.SetGuidedMode(cancel); return null; } } ================================================ FILE: src/Asv.Drones/Core/Commands/Flight/Widgets/UavWidget/LandCommand.cs ================================================ using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Asv.IO; using Asv.Mavlink; using Material.Icons; namespace Asv.Drones; public class LandCommand : ContextCommand { #region Static public const string Id = $"{BaseId}.uav.land"; internal static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.UavAction_Land, Description = RS.UavAction_Land_Description, Icon = MaterialIconKind.AeroplaneLanding, DefaultHotKey = null, }; #endregion public override ICommandInfo Info => StaticInfo; protected override async ValueTask InternalExecute( UavWidgetViewModel context, CommandArg newValue, CancellationToken cancel ) { var control = context.Device.GetMicroservice(); if (control == null) { return null; } await control.EnsureGuidedMode(cancel: cancel); await control.DoLand(cancel); return null; } } ================================================ FILE: src/Asv.Drones/Core/Commands/Flight/Widgets/UavWidget/MissionProgress/UpdateMissionCommand.cs ================================================ using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Material.Icons; namespace Asv.Drones; public class UpdateMissionCommand : ContextCommand { #region StaticInfo public const string Id = $"{BaseId}.mission-items.update"; internal static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.UavAction_Land, Description = RS.UavAction_Land_Description, Icon = MaterialIconKind.Reload, DefaultHotKey = null, }; #endregion public override ICommandInfo Info => StaticInfo; protected override async ValueTask InternalExecute( MissionProgressViewModel context, CommandArg newValue, CancellationToken cancel ) { await context.InitiateMissionPoints(cancel); return null; } } ================================================ FILE: src/Asv.Drones/Core/Commands/Flight/Widgets/UavWidget/RTLCommand.cs ================================================ using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Asv.IO; using Asv.Mavlink; using Material.Icons; namespace Asv.Drones; public class RTLCommand : ContextCommand { #region Static public const string Id = $"{BaseId}.uav.rtl"; internal static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.UavAction_Rtl_Name, Description = RS.UavAction_Rtl_Description, Icon = MaterialIconKind.Home, DefaultHotKey = null, }; #endregion public override ICommandInfo Info => StaticInfo; protected override async ValueTask InternalExecute( UavWidgetViewModel context, CommandArg newValue, CancellationToken cancel ) { var control = context.Device.GetMicroservice(); if (control is null) { return null; } await control.EnsureGuidedMode(cancel: cancel); await control.DoRtl(cancel); return null; } } ================================================ FILE: src/Asv.Drones/Core/Commands/Flight/Widgets/UavWidget/StartMissionCommand.cs ================================================ using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Asv.IO; using Asv.Mavlink; using Material.Icons; using R3; namespace Asv.Drones; public class StartMissionCommand : ContextCommand { #region Static public const string Id = $"{BaseId}.uav.start"; internal static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.UavAction_StartMission, Description = RS.UavAction_StartMission_Description, Icon = MaterialIconKind.MapMarkerPath, DefaultHotKey = null, }; #endregion public override ICommandInfo Info => StaticInfo; protected override async ValueTask InternalExecute( UavWidgetViewModel context, CommandArg newValue, CancellationToken cancel ) { var control = context.Device.GetMicroservice(); var mission = context.Device.GetMicroservice(); if (control is null || mission is null) { return null; } context.MissionProgress.UpdateMission.Execute(Unit.Default); await mission.SetCurrent(0, cancel); await control.SetAutoMode(cancel); return null; } } ================================================ FILE: src/Asv.Drones/Core/Commands/Flight/Widgets/UavWidget/TakeOffCommand.cs ================================================ using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Asv.IO; using Asv.Mavlink; using Material.Icons; namespace Asv.Drones; public class TakeOffCommand : ContextCommand { #region Static public const string Id = $"{BaseId}.uav.takeOff"; internal static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.UavAction_TakeOff, Description = RS.UavAction_TakeOff_Description, Icon = MaterialIconKind.AeroplaneTakeoff, DefaultHotKey = null, }; #endregion public override ICommandInfo Info => StaticInfo; public override async ValueTask InternalExecute( UavWidgetViewModel context, DoubleArg arg, CancellationToken cancel ) { var device = context.Device; var controlClient = device.GetMicroservice(); if (controlClient == null) { return null; } await controlClient.SetGuidedMode(cancel); await controlClient.TakeOff(arg.Value, cancel); return null; } } ================================================ FILE: src/Asv.Drones/Core/Commands/MavParams/MavParamsPageViewModel/RemoveAllPinsCommand.cs ================================================ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Material.Icons; namespace Asv.Drones; public class RemoveAllPinsCommand : ContextCommand { public const string Id = $"{BaseId}.params.remove-all-pins"; internal static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.UnpinAllParamsCommand_CommandInfo_Name, Description = RS.UnpinAllParamsCommand_CommandInfo_Description, Icon = MaterialIconKind.PinOff, DefaultHotKey = null, // TODO: make a key bind when new key listener system appears }; public override ICommandInfo Info => StaticInfo; public override ValueTask InternalExecute( MavParamsPageViewModel context, DictArg arg, CancellationToken cancel ) { if (context.AllParams is null) { return ValueTask.FromResult(null); } if (arg.Count == 0) { var oldValue = new DictArg(); foreach (var item in context.AllParams.Where(item => item.IsPinned.ViewValue.Value)) { oldValue.Add( new KeyValuePair(item.Id.ToString(), new BoolArg(true)) ); item.IsPinned.ModelValue.Value = false; } var notSelected = context .ViewedParams.Where(it => it.Id != context.SelectedItem.Value?.Id) .ToArray(); foreach (var item in notSelected) { context.ViewedParams.Remove(item); } return ValueTask.FromResult(oldValue); } foreach (var item in context.AllParams.Where(item => arg.ContainsKey(item.Id.ToString()))) { item.IsPinned.ModelValue.Value = !item.IsPinned.ModelValue.Value; } return ValueTask.FromResult(arg); } } ================================================ FILE: src/Asv.Drones/Core/Commands/MavParams/MavParamsPageViewModel/StopUpdateParamsCommand.cs ================================================ using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Material.Icons; namespace Asv.Drones; public class StopUpdateParamsCommand : ContextCommand { public const string Id = $"{BaseId}.params.stop-update"; internal static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.StopUpdateParamsCommand_CommandInfo_Name, Description = RS.StopUpdateParamsCommand_CommandInfo_Description, Icon = MaterialIconKind.CancelCircle, DefaultHotKey = null, // TODO: make a key bind when new key listener system appears }; public override ICommandInfo Info => StaticInfo; protected override ValueTask InternalExecute( MavParamsPageViewModel context, CommandArg newValue, CancellationToken cancel ) { context.StopUpdateParamsImpl(); return default; } } ================================================ FILE: src/Asv.Drones/Core/Commands/MavParams/MavParamsPageViewModel/UpdateParamsCommand.cs ================================================ using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Material.Icons; namespace Asv.Drones; public class UpdateParamsCommand : ContextCommand { public const string Id = $"{BaseId}.params.update"; internal static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.UpdateParamsCommand_CommandInfo_Name, Description = RS.UpdateParamsCommand_CommandInfo_Description, Icon = MaterialIconKind.Refresh, DefaultHotKey = null, // TODO: make a key bind when new key listener system appears }; public override ICommandInfo Info => StaticInfo; protected override async ValueTask InternalExecute( MavParamsPageViewModel context, CommandArg newValue, CancellationToken cancel ) { await context.UpdateParamsImpl(cancel); return null; } } ================================================ FILE: src/Asv.Drones/Core/Commands/MavParams/OpenMavParamsCommand.cs ================================================ using Asv.Avalonia; namespace Asv.Drones; public class OpenMavParamsCommand(INavigationService nav) : OpenPageCommandBase(MavParamsPageViewModel.PageId, nav) { #region Static public const string Id = $"{BaseId}.open.{MavParamsPageViewModel.PageId}"; public static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.OpenMavParamsCommand_CommandInfo_Name, Description = RS.OpenMavParamsCommand_CommandInfo_Description, Icon = MavParamsPageViewModel.PageIcon, DefaultHotKey = null, }; #endregion public override ICommandInfo Info => StaticInfo; } ================================================ FILE: src/Asv.Drones/Core/Commands/Mavlink/MavlinkCommands.cs ================================================ using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Drones.Api; using Asv.Mavlink; namespace Asv.Drones; public class MavlinkCommands : IMavlinkCommands { public ICommandInfo WriteParamInfo => MavlinkParamsWriteCommand.StaticInfo; public ValueTask WriteParam( IRoutable context, string name, MavParamValue value, CancellationToken cancel = default ) => MavlinkParamsWriteCommand.Execute(context, name, value, cancel); public ICommandInfo ReadParamInfo => MavlinkParamReadCommand.StaticInfo; public ValueTask ReadParam( IRoutable context, string name, CancellationToken cancel = default ) => MavlinkParamReadCommand.Execute(context, name, cancel); } ================================================ FILE: src/Asv.Drones/Core/Commands/Mavlink/MavlinkCommandsMixin.cs ================================================ using Asv.Drones.Api; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace Asv.Drones; public static class MavlinkCommandsMixin { public static IHostApplicationBuilder RegisterMavlinkCommands( this IHostApplicationBuilder builder ) { builder.Services.AddSingleton(); return builder; } } ================================================ FILE: src/Asv.Drones/Core/Commands/Mavlink/MavlinkParamReadCommand.cs ================================================ using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Drones.Api; using Asv.Mavlink; using Material.Icons; namespace Asv.Drones; public class MavlinkParamReadCommand : MavlinkMicroserviceCommand { public const string Id = $"{BaseId}.mavlink.param.read"; public static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.ReadParamCommand_CommandInfo_Name, Description = RS.ReadParamCommand_CommandInfo_Description, Icon = MaterialIconKind.Set, DefaultHotKey = null, }; public static ValueTask Execute( IRoutable context, string name, CancellationToken cancel = default ) { return context.ExecuteCommand(Id, CommandArg.CreateString(name), cancel: cancel); } public override ICommandInfo Info => StaticInfo; protected override async ValueTask InternalExecute( IParamsClientEx microservice, StringArg arg, CancellationToken cancel ) { await microservice.ReadOnce(arg.Value, cancel); return null; // this is command without undo, so we return null } } ================================================ FILE: src/Asv.Drones/Core/Commands/Mavlink/MavlinkParamsWriteCommand.cs ================================================ using System; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Drones.Api; using Asv.Mavlink; using Material.Icons; namespace Asv.Drones; public class MavlinkParamsWriteCommand : MavlinkMicroserviceCommand { public const string Id = $"{BaseId}.mavlink.param.write"; public static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.WritePatamCommand_CommandInfo_Name, Description = RS.WriteParamCommand_CommandInfo_Description, Icon = MaterialIconKind.Set, DefaultHotKey = null, }; public static ValueTask Execute( IRoutable context, string name, MavParamValue value, CancellationToken cancel = default ) { return context.ExecuteCommand( Id, CommandArg.ChangeAction(name, CommandArg.CreateString(value.PrintValue())), cancel ); } public override ICommandInfo Info => StaticInfo; protected override async ValueTask InternalExecute( IParamsClientEx microservice, ActionArg arg, CancellationToken cancel ) { if (string.IsNullOrWhiteSpace(arg.SubjectId)) { throw new ArgumentException( $@"{nameof(arg.SubjectId)} cannot be null or empty.", nameof(arg.SubjectId) ); } MavParamValue prevValue; if (!microservice.Items.TryGetValue(arg.SubjectId, out var param)) { prevValue = await microservice.ReadOnce(arg.SubjectId, cancel); } else { prevValue = param.Value.Value; } var stringValue = arg.Value?.AsString(); if (string.IsNullOrWhiteSpace(stringValue)) { throw new ArgumentException( $@"{nameof(arg.Value)} must be of type {CommandArg.Id.String}.", nameof(arg.Value) ); } var result = MavParamValue.TryParseValue(stringValue, prevValue.Type, out var value); if (!result.IsSuccess) { Debug.Assert(result.ValidationException != null, "result.ValidationException != null"); throw new ArgumentException( $"Cannot parse value '{stringValue}' to type {prevValue.Type}: {result.ValidationException.Message}", nameof(arg.Value), result.ValidationException ); } Debug.Assert(value != null, nameof(value) + " != null"); await microservice.WriteOnce(arg.SubjectId, value.Value, cancel); return CommandArg.ChangeAction( arg.SubjectId, CommandArg.CreateString(prevValue.PrintValue()) ); } } ================================================ FILE: src/Asv.Drones/Core/Commands/Mavlink/NullMavlinkCommands.cs ================================================ using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Drones.Api; using Asv.Mavlink; namespace Asv.Drones; public sealed class NullMavlinkCommands : IMavlinkCommands { public static NullMavlinkCommands Instance { get; } = new(); private NullMavlinkCommands() { } public ICommandInfo WriteParamInfo => MavlinkParamsWriteCommand.StaticInfo; public ValueTask WriteParam( IRoutable context, string name, MavParamValue value, CancellationToken cancel = default ) { return ValueTask.CompletedTask; } public ICommandInfo ReadParamInfo => MavlinkParamReadCommand.StaticInfo; public ValueTask ReadParam(IRoutable context, string name, CancellationToken cancel = default) { return ValueTask.CompletedTask; } } ================================================ FILE: src/Asv.Drones/Core/Commands/PacketViewer/ClearAllPacketsCommand.cs ================================================ using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Material.Icons; namespace Asv.Drones; public sealed class ClearAllPacketsCommand : ContextCommand { public const string Id = $"{BaseId}.packet-viewer.clear-all"; internal static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.ClearAllPacketsCommand_CommandInfo_Name, Description = RS.ClearAllPacketsCommand_CommandInfo_Description, Icon = MaterialIconKind.Bin, DefaultHotKey = null, // TODO: make a key bind later }; public override ICommandInfo Info => StaticInfo; protected override ValueTask InternalExecute( PacketViewerViewModel context, CommandArg newValue, CancellationToken cancel ) { context.ClearAllImpl(); return ValueTask.FromResult(null); } } ================================================ FILE: src/Asv.Drones/Core/Commands/PacketViewer/ExportPacketsToCsvCommand.cs ================================================ using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Material.Icons; namespace Asv.Drones; public class ExportPacketsToCsvCommand : ContextCommand { public const string Id = $"{BaseId}.packet-viewer.export-to-csv"; internal static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.ExportPacketsToCsvCommand_CommandInfo_Name, Description = RS.ExportPacketsToCsvCommand_CommandInfo_Description, Icon = MaterialIconKind.ContentSave, DefaultHotKey = null, // TODO: make a key bind later }; public override ICommandInfo Info => StaticInfo; protected override async ValueTask InternalExecute( PacketViewerViewModel context, CommandArg newValue, CancellationToken cancel ) { await context.ExportToCsvImpl(cancel); return null; } } ================================================ FILE: src/Asv.Drones/Core/Commands/PacketViewer/OpenPacketViewer.cs ================================================ using Asv.Avalonia; namespace Asv.Drones; public class OpenPacketViewerCommand(INavigationService nav) : OpenPageCommandBase(PacketViewerViewModel.PageId, nav) { #region Static public const string Id = $"{BaseId}.open.{PacketViewerViewModel.PageId}"; public static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.OpenPacketViewerCommand_CommandInfo_Name, Description = RS.OpenPacketViewerCommand_CommandInfo_Description, Icon = PacketViewerViewModel.PageIcon, DefaultHotKey = null, }; #endregion public override ICommandInfo Info => StaticInfo; } ================================================ FILE: src/Asv.Drones/Core/Commands/Setup/FrameType/ChangeFrameTypeCommand.cs ================================================ using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Material.Icons; namespace Asv.Drones; public class ChangeFrameTypeCommand : ContextCommand { public const string Id = $"{BaseId}.setup.frame-type.change"; internal static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.ChangeFrameTypeCommand_CommandInfo_Name, Description = RS.ChangeFrameTypeCommand_CommandInfo_Description, Icon = MaterialIconKind.KeyChange, DefaultHotKey = null, }; public override ICommandInfo Info => StaticInfo; public override async ValueTask InternalExecute( SetupFrameTypeViewModel context, StringArg newValue, CancellationToken cancel ) { var currentFrameId = context.CurrentFrame?.Value?.Id; if (currentFrameId is null) { return null; } await context.ChangeFrameType(newValue.Value, cancel); return new StringArg(currentFrameId); } } ================================================ FILE: src/Asv.Drones/Core/Commands/Setup/OpenSetupCommand.cs ================================================ using Asv.Avalonia; namespace Asv.Drones; public class OpenSetupCommand(INavigationService nav) : OpenPageCommandBase(SetupPageViewModel.PageId, nav) { #region Static public const string Id = $"{BaseId}.open.{SetupPageViewModel.PageId}"; public static readonly ICommandInfo StaticInfo = new CommandInfo { Id = Id, Name = RS.OpenSetupCommand_CommandInfo_Name, Description = RS.OpenSetupCommand_CommandInfo_Description, Icon = SetupPageViewModel.PageIcon, DefaultHotKey = null, }; #endregion public override ICommandInfo Info => StaticInfo; } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/AngleUavIndicator/Items/Pitch/PitchItem.cs ================================================ using System; using Avalonia; namespace Asv.Drones; public partial class PitchItem : AvaloniaObject { private readonly int _pitch; public PitchItem( int pitch, double scale, bool titleIsVisible = true, double controlHeight = 284 ) { _pitch = pitch; Value = ((controlHeight / 2) - pitch) * scale; if (titleIsVisible) { Title = pitch.ToString(); StartLine = new Point(0 * scale, 0 * scale); StopLine = new Point(20 * scale, 0 * scale); } else { Title = string.Empty; StartLine = new Point(4 * scale, 0 * scale); StopLine = new Point(16 * scale, 0 * scale); } IsVisible = Math.Abs(pitch) <= 20; } public void UpdateVisibility(double pitch) { IsVisible = pitch >= _pitch - 20 && pitch <= _pitch + 20; } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/AngleUavIndicator/Items/Pitch/PitchItem.properties.cs ================================================ using Avalonia; namespace Asv.Drones; public partial class PitchItem { public static readonly DirectProperty TitleProperty = AvaloniaProperty.RegisterDirect( nameof(Title), _ => _.Title, (_, value) => _.Title = value ); public string Title { get; set => SetAndRaise(TitleProperty, ref field, value); } public static readonly DirectProperty ValueProperty = AvaloniaProperty.RegisterDirect( nameof(Value), _ => _.Value, (_, value) => _.Value = value ); public double Value { get; set => SetAndRaise(ValueProperty, ref field, value); } public static readonly DirectProperty IsVisibleProperty = AvaloniaProperty.RegisterDirect( nameof(IsVisible), _ => _.IsVisible, (_, value) => _.IsVisible = value ); public bool IsVisible { get; set => SetAndRaise(IsVisibleProperty, ref field, value); } public static readonly DirectProperty StartLineProperty = AvaloniaProperty.RegisterDirect( nameof(StartLine), _ => _.StartLine, (_, value) => _.StartLine = value ); public Point StartLine { get; set => SetAndRaise(StartLineProperty, ref field, value); } public static readonly DirectProperty StopLineProperty = AvaloniaProperty.RegisterDirect( nameof(StopLine), _ => _.StopLine, (_, value) => _.StopLine = value ); public Point StopLine { get; set => SetAndRaise(StopLineProperty, ref field, value); } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/AngleUavIndicator/Items/Roll/RollItem.cs ================================================ using System; using Avalonia; namespace Asv.Drones; public partial class RollItem : AvaloniaObject { public RollItem(int angle) { Value = angle; Title = Math.Abs(angle) > 180 ? (360 - Math.Abs(angle)).ToString() : Math.Abs(angle).ToString(); } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/AngleUavIndicator/Items/Roll/RollItem.properties.cs ================================================ using Avalonia; namespace Asv.Drones; public partial class RollItem { public static readonly DirectProperty TitleProperty = AvaloniaProperty.RegisterDirect( nameof(Title), _ => _.Title, (_, value) => _.Title = value ); public string Title { get; set => SetAndRaise(TitleProperty, ref field, value); } public static readonly DirectProperty ValueProperty = AvaloniaProperty.RegisterDirect( nameof(Value), _ => _.Value, (_, value) => _.Value = value ); public double Value { get; set => SetAndRaise(ValueProperty, ref field, value); } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/AngleUavIndicator/UavAngleIndicator.cs ================================================ using System; using Avalonia; using Avalonia.Collections; using Avalonia.Controls.Primitives; namespace Asv.Drones; public partial class UavAngleIndicator : TemplatedControl { public UavAngleIndicator() { Scale = Math.Min(InternalWidth, InternalHeight) / 100; RollItems = new AvaloniaList( new OldAttitudeIndicator.RollItem(0), new OldAttitudeIndicator.RollItem(10), new OldAttitudeIndicator.RollItem(20), new OldAttitudeIndicator.RollItem(30), new OldAttitudeIndicator.RollItem(45), new OldAttitudeIndicator.RollItem(60), new OldAttitudeIndicator.RollItem(300), new OldAttitudeIndicator.RollItem(315), new OldAttitudeIndicator.RollItem(330), new OldAttitudeIndicator.RollItem(340), new OldAttitudeIndicator.RollItem(350) ); PitchItems = new AvaloniaList( new OldAttitudeIndicator.PitchItem(135, Scale, false), new OldAttitudeIndicator.PitchItem(130, Scale), new OldAttitudeIndicator.PitchItem(125, Scale, false), new OldAttitudeIndicator.PitchItem(120, Scale), new OldAttitudeIndicator.PitchItem(115, Scale, false), new OldAttitudeIndicator.PitchItem(110, Scale), new OldAttitudeIndicator.PitchItem(105, Scale, false), new OldAttitudeIndicator.PitchItem(100, Scale), new OldAttitudeIndicator.PitchItem(95, Scale, false), new OldAttitudeIndicator.PitchItem(90, Scale), new OldAttitudeIndicator.PitchItem(85, Scale, false), new OldAttitudeIndicator.PitchItem(80, Scale), new OldAttitudeIndicator.PitchItem(75, Scale, false), new OldAttitudeIndicator.PitchItem(70, Scale), new OldAttitudeIndicator.PitchItem(65, Scale, false), new OldAttitudeIndicator.PitchItem(60, Scale), new OldAttitudeIndicator.PitchItem(55, Scale, false), new OldAttitudeIndicator.PitchItem(50, Scale), new OldAttitudeIndicator.PitchItem(45, Scale, false), new OldAttitudeIndicator.PitchItem(40, Scale), new OldAttitudeIndicator.PitchItem(35, Scale, false), new OldAttitudeIndicator.PitchItem(30, Scale), new OldAttitudeIndicator.PitchItem(25, Scale, false), new OldAttitudeIndicator.PitchItem(20, Scale), new OldAttitudeIndicator.PitchItem(15, Scale, false), new OldAttitudeIndicator.PitchItem(10, Scale), new OldAttitudeIndicator.PitchItem(5, Scale, false), new OldAttitudeIndicator.PitchItem(0, Scale), new OldAttitudeIndicator.PitchItem(-5, Scale, false), new OldAttitudeIndicator.PitchItem(-10, Scale), new OldAttitudeIndicator.PitchItem(-15, Scale, false), new OldAttitudeIndicator.PitchItem(-20, Scale), new OldAttitudeIndicator.PitchItem(-25, Scale, false), new OldAttitudeIndicator.PitchItem(-30, Scale), new OldAttitudeIndicator.PitchItem(-35, Scale, false), new OldAttitudeIndicator.PitchItem(-40, Scale), new OldAttitudeIndicator.PitchItem(-45, Scale, false), new OldAttitudeIndicator.PitchItem(-50, Scale), new OldAttitudeIndicator.PitchItem(-55, Scale, false), new OldAttitudeIndicator.PitchItem(-60, Scale), new OldAttitudeIndicator.PitchItem(-65, Scale, false), new OldAttitudeIndicator.PitchItem(-70, Scale), new OldAttitudeIndicator.PitchItem(-75, Scale, false), new OldAttitudeIndicator.PitchItem(-80, Scale), new OldAttitudeIndicator.PitchItem(-85, Scale, false), new OldAttitudeIndicator.PitchItem(-90, Scale), new OldAttitudeIndicator.PitchItem(-95, Scale, false), new OldAttitudeIndicator.PitchItem(-100, Scale), new OldAttitudeIndicator.PitchItem(-105, Scale, false), new OldAttitudeIndicator.PitchItem(-110, Scale), new OldAttitudeIndicator.PitchItem(-115, Scale, false), new OldAttitudeIndicator.PitchItem(-120, Scale), new OldAttitudeIndicator.PitchItem(-125, Scale, false), new OldAttitudeIndicator.PitchItem(-130, Scale), new OldAttitudeIndicator.PitchItem(-135, Scale, false) ); } public double Scale { get; } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == RollAngleProperty) { UpdateRollAngle(change.Sender); } else if (change.Property == PitchAngleProperty) { UpdateAngle(change.Sender); } } private static void UpdateAngle(AvaloniaObject source) { if (source is not UavAngleIndicator indicator) { return; } var pitch = indicator.PitchAngle; UpdateRollAngle(source); foreach (var item in indicator.PitchItems) { item.UpdateVisibility(pitch); } } private static void UpdateRollAngle(AvaloniaObject source) { if (source is not UavAngleIndicator indicator) { return; } var roll = indicator.RollAngle; var pitch = indicator.PitchAngle; indicator.PitchTranslateX = -pitch * indicator.Scale * Math.Cos((roll - 90.0) * Math.PI / 180.0); indicator.PitchTranslateY = pitch * indicator.Scale * Math.Sin((90 - roll) * Math.PI / 180.0); } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/AngleUavIndicator/UavAngleIndicator.properties.cs ================================================ using System.Collections.Generic; using Avalonia; namespace Asv.Drones; public partial class UavAngleIndicator { public static readonly StyledProperty RollAngleProperty = AvaloniaProperty.Register< UavAngleIndicator, double >(nameof(RollAngle)); public double RollAngle { get => GetValue(RollAngleProperty); set => SetValue(RollAngleProperty, value); } public static readonly StyledProperty PitchAngleProperty = AvaloniaProperty.Register< UavAngleIndicator, double >(nameof(PitchAngle)); public double PitchAngle { get => GetValue(PitchAngleProperty); set => SetValue(PitchAngleProperty, value); } #region Internal direct property public static readonly DirectProperty InternalWidthProperty = AvaloniaProperty.RegisterDirect( nameof(InternalWidth), _ => _.InternalWidth, (_, value) => _.InternalWidth = value ); public double InternalWidth { get; set => SetAndRaise(InternalWidthProperty, ref field, value); } = 1000; public static readonly DirectProperty InternalHeightProperty = AvaloniaProperty.RegisterDirect( nameof(InternalHeight), _ => _.InternalHeight, (_, value) => _.InternalHeight = value ); public double InternalHeight { get; set => SetAndRaise(InternalHeightProperty, ref field, value); } = 1000; public static readonly DirectProperty PitchTranslateXProperty = AvaloniaProperty.RegisterDirect( nameof(PitchTranslateX), _ => _.PitchTranslateX, (_, value) => _.PitchTranslateX = value ); private double PitchTranslateX { get; set => SetAndRaise(PitchTranslateXProperty, ref field, value); } public static readonly DirectProperty PitchTranslateYProperty = AvaloniaProperty.RegisterDirect( nameof(PitchTranslateY), _ => _.PitchTranslateY, (_, value) => _.PitchTranslateY = value ); public double PitchTranslateY { get; set => SetAndRaise(PitchTranslateYProperty, ref field, value); } public static readonly DirectProperty< UavAngleIndicator, IEnumerable > RollItemsProperty = AvaloniaProperty.RegisterDirect< UavAngleIndicator, IEnumerable >(nameof(RollItems), _ => _.RollItems, (_, value) => _.RollItems = value); public IEnumerable RollItems { get; set => SetAndRaise(RollItemsProperty, ref field, value); } public static readonly DirectProperty< UavAngleIndicator, IEnumerable > PitchItemsProperty = AvaloniaProperty.RegisterDirect< UavAngleIndicator, IEnumerable >(nameof(PitchItems), _ => _.PitchItems, (_, value) => _.PitchItems = value); public IEnumerable PitchItems { get; set => SetAndRaise(PitchItemsProperty, ref field, value); } #endregion } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/AngleUavIndicator/UavAngleIndicatorStyles.axaml ================================================ ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/CompassUavIndicator/CompassScaleItem.cs ================================================ using System; using Avalonia; namespace Asv.Drones; public class CompassScaleItem : AvaloniaObject { private const double Center = 150.0; private const double OuterRadius = 128.0; private const double LabelRadius = 88.0; private const double LabelWidth = 44.0; private const double LabelHeight = 24.0; public CompassScaleItem(double angle, string? title, bool isMajor) { Angle = angle; Title = title; HasTitle = !string.IsNullOrWhiteSpace(title); TickLength = isMajor ? 20 : 10; TickWidth = isMajor ? 3 : 2; Update(0); } public double Angle { get; } public string? Title { get; } public bool HasTitle { get; } public double TickLength { get; } public double TickWidth { get; } public static readonly DirectProperty TickStartProperty = AvaloniaProperty.RegisterDirect( nameof(TickStart), item => item.TickStart, (item, value) => item.TickStart = value ); public Point TickStart { get; private set => SetAndRaise(TickStartProperty, ref field, value); } public static readonly DirectProperty TickEndProperty = AvaloniaProperty.RegisterDirect( nameof(TickEnd), item => item.TickEnd, (item, value) => item.TickEnd = value ); public Point TickEnd { get; private set => SetAndRaise(TickEndProperty, ref field, value); } public static readonly DirectProperty LabelLeftProperty = AvaloniaProperty.RegisterDirect( nameof(LabelLeft), item => item.LabelLeft, (item, value) => item.LabelLeft = value ); public double LabelLeft { get; private set => SetAndRaise(LabelLeftProperty, ref field, value); } public static readonly DirectProperty LabelTopProperty = AvaloniaProperty.RegisterDirect( nameof(LabelTop), item => item.LabelTop, (item, value) => item.LabelTop = value ); public double LabelTop { get; private set => SetAndRaise(LabelTopProperty, ref field, value); } public void Update(double heading) { var visualAngle = NormalizeSignedAngle(Angle - heading); var radians = visualAngle * Math.PI / 180.0; var sin = Math.Sin(radians); var cos = Math.Cos(radians); TickStart = new Point( Center + ((OuterRadius - TickLength) * sin), Center - ((OuterRadius - TickLength) * cos) ); TickEnd = new Point(Center + (OuterRadius * sin), Center - (OuterRadius * cos)); LabelLeft = Center + (LabelRadius * sin) - (LabelWidth / 2.0); LabelTop = Center - (LabelRadius * cos) - (LabelHeight / 2.0); } private static double NormalizeSignedAngle(double value) { var angle = value % 360.0; if (angle <= -180.0) { angle += 360.0; } else if (angle > 180.0) { angle -= 360.0; } return angle; } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/CompassUavIndicator/CompassUavIndicator.cs ================================================ using System.Linq; using Avalonia; using Avalonia.Collections; using Avalonia.Controls.Primitives; namespace Asv.Drones; public partial class CompassUavIndicator : TemplatedControl { public CompassUavIndicator() { CompassItems = new AvaloniaList( Enumerable .Range(0, 24) .Select(index => { var angle = index * 15.0; var title = angle switch { 0 => RS.HeadingScaleItem_Direction_N, 90 => RS.HeadingScaleItem_Direction_E, 180 => RS.HeadingScaleItem_Direction_S, 270 => RS.HeadingScaleItem_Direction_W, _ when angle % 30 == 0 => angle.ToString("F0"), _ => null, }; return new CompassScaleItem(angle, title, angle % 30 == 0); }) ); UpdateCompass(); } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == HeadingProperty || change.Property == HomeAzimuthProperty) { UpdateCompass(); } } private void UpdateCompass() { var heading = NormalizeAngle(Heading); HeadingText = $"{heading:F0}°"; foreach (var item in CompassItems) { item.Update(heading); } HomeMarkerRotation = double.IsNaN(HomeAzimuth) ? 0.0 : NormalizeSignedAngle(HomeAzimuth - heading); } private static double NormalizeAngle(double value) { if (double.IsNaN(value) || double.IsInfinity(value)) { return 0.0; } var angle = value % 360.0; return angle < 0.0 ? angle + 360.0 : angle; } private static double NormalizeSignedAngle(double value) { var angle = value % 360.0; if (angle <= -180.0) { angle += 360.0; } else if (angle > 180.0) { angle -= 360.0; } return angle; } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/CompassUavIndicator/CompassUavIndicator.properties.cs ================================================ using System.Collections.Generic; using Avalonia; namespace Asv.Drones; public partial class CompassUavIndicator { public static readonly StyledProperty HeadingProperty = AvaloniaProperty.Register< CompassUavIndicator, double >(nameof(Heading)); public double Heading { get => GetValue(HeadingProperty); set => SetValue(HeadingProperty, value); } public static readonly StyledProperty HomeAzimuthProperty = AvaloniaProperty.Register< CompassUavIndicator, double >(nameof(HomeAzimuth), double.NaN); public double HomeAzimuth { get => GetValue(HomeAzimuthProperty); set => SetValue(HomeAzimuthProperty, value); } public static readonly DirectProperty< CompassUavIndicator, IEnumerable > CompassItemsProperty = AvaloniaProperty.RegisterDirect< CompassUavIndicator, IEnumerable >(nameof(CompassItems), indicator => indicator.CompassItems); public IEnumerable CompassItems { get; } public static readonly DirectProperty HeadingTextProperty = AvaloniaProperty.RegisterDirect( nameof(HeadingText), indicator => indicator.HeadingText, (indicator, value) => indicator.HeadingText = value ); public string HeadingText { get; private set => SetAndRaise(HeadingTextProperty, ref field, value); } = "0°"; public static readonly DirectProperty HomeMarkerRotationProperty = AvaloniaProperty.RegisterDirect( nameof(HomeMarkerRotation), indicator => indicator.HomeMarkerRotation, (indicator, value) => indicator.HomeMarkerRotation = value ); public double HomeMarkerRotation { get; private set => SetAndRaise(HomeMarkerRotationProperty, ref field, value); } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/CompassUavIndicator/CompassUavIndicatorStyles.axaml ================================================ ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/DeviceTelemetryDesignPreview.cs ================================================ using Asv.Avalonia; namespace Asv.Drones; internal static class DeviceTelemetryDesignPreview { public const AsvColorKind DefaultStatusColor = AsvColorKind.Info5; private static bool _isConfigured; public static IUnitService UnitService { get { ConfigureUnits(); return NullUnitService.Instance; } } public static IUnit Unit(string id) => UnitService.Units[id]; private static void ConfigureUnits() { if (_isConfigured) { return; } var unitService = NullUnitService.Instance; unitService.Extend( new VelocityUnit( DesignTime.Configuration, [new VelocityMetersPerSecondUnitItem(), new VelocityMilesPerHourUnitItem()] ) ); unitService.Extend( new ProgressUnit( DesignTime.Configuration, [new ProgressPercentUnitItem(), new ProgressInPartsUnitItem()] ) ); unitService.Extend( new CapacityUnit(DesignTime.Configuration, [new CapacityMilliAmperePerHourUnitItem()]) ); unitService.Extend( new AmperageUnit( DesignTime.Configuration, [new AmperageAmpereUnitItem(), new AmperageMilliAmpereUnitItem()] ) ); unitService.Extend( new VoltageUnit( DesignTime.Configuration, [new VoltageVoltUnitItem(), new VoltageMilliVoltUnitItem()] ) ); _isConfigured = true; } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/OldAttitudeIndicator/AttitudeIndicator.cs ================================================ using System; using System.Linq; using Avalonia; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Media; using R3; namespace Asv.Drones.OldAttitudeIndicator; public partial class AttitudeIndicator : TemplatedControl { private const int VelocityItemCount = 6; private const int VelocityValueRange = 5; private const double VelocityControlLengthPrc = 0.4; private const int AltitudeItemCount = 6; private const int AltitudeValueRange = 5; private const double AltitudeControlLengthPrc = 0.4; private const int HeadingItemCount = 10; private const double HeadingControlLengthPrc = 1.0; private const int HeadingValueRange = 15; private static double _headingPositionStep; private static double _headingCenterPosition; public double Scale { get; } public AttitudeIndicator() { if (Design.IsDesignMode) { var status = new[] { "Armed", "Disarmed" }; Observable .Timer(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1), TimeProvider.System) .Subscribe(_ => { StatusText = status[1 % 2]; }); StatusText = status[1]; } var smallerSide = Math.Min((double)InternalWidth, InternalHeight); Scale = smallerSide / 100; RollItems = new AvaloniaList( new OldAttitudeIndicator.RollItem(0), new OldAttitudeIndicator.RollItem(10), new OldAttitudeIndicator.RollItem(20), new OldAttitudeIndicator.RollItem(30), new OldAttitudeIndicator.RollItem(45), new OldAttitudeIndicator.RollItem(60), new OldAttitudeIndicator.RollItem(300), new OldAttitudeIndicator.RollItem(315), new OldAttitudeIndicator.RollItem(330), new OldAttitudeIndicator.RollItem(340), new OldAttitudeIndicator.RollItem(350) ); PitchItems = new AvaloniaList( new OldAttitudeIndicator.PitchItem(135, Scale, false), new OldAttitudeIndicator.PitchItem(130, Scale), new OldAttitudeIndicator.PitchItem(125, Scale, false), new OldAttitudeIndicator.PitchItem(120, Scale), new OldAttitudeIndicator.PitchItem(115, Scale, false), new OldAttitudeIndicator.PitchItem(110, Scale), new OldAttitudeIndicator.PitchItem(105, Scale, false), new OldAttitudeIndicator.PitchItem(100, Scale), new OldAttitudeIndicator.PitchItem(95, Scale, false), new OldAttitudeIndicator.PitchItem(90, Scale), new OldAttitudeIndicator.PitchItem(85, Scale, false), new OldAttitudeIndicator.PitchItem(80, Scale), new OldAttitudeIndicator.PitchItem(75, Scale, false), new OldAttitudeIndicator.PitchItem(70, Scale), new OldAttitudeIndicator.PitchItem(65, Scale, false), new OldAttitudeIndicator.PitchItem(60, Scale), new OldAttitudeIndicator.PitchItem(55, Scale, false), new OldAttitudeIndicator.PitchItem(50, Scale), new OldAttitudeIndicator.PitchItem(45, Scale, false), new OldAttitudeIndicator.PitchItem(40, Scale), new OldAttitudeIndicator.PitchItem(35, Scale, false), new OldAttitudeIndicator.PitchItem(30, Scale), new OldAttitudeIndicator.PitchItem(25, Scale, false), new OldAttitudeIndicator.PitchItem(20, Scale), new OldAttitudeIndicator.PitchItem(15, Scale, false), new OldAttitudeIndicator.PitchItem(10, Scale), new OldAttitudeIndicator.PitchItem(5, Scale, false), new OldAttitudeIndicator.PitchItem(0, Scale), new OldAttitudeIndicator.PitchItem(-5, Scale, false), new OldAttitudeIndicator.PitchItem(-10, Scale), new OldAttitudeIndicator.PitchItem(-15, Scale, false), new OldAttitudeIndicator.PitchItem(-20, Scale), new OldAttitudeIndicator.PitchItem(-25, Scale, false), new OldAttitudeIndicator.PitchItem(-30, Scale), new OldAttitudeIndicator.PitchItem(-35, Scale, false), new OldAttitudeIndicator.PitchItem(-40, Scale), new OldAttitudeIndicator.PitchItem(-45, Scale, false), new OldAttitudeIndicator.PitchItem(-50, Scale), new OldAttitudeIndicator.PitchItem(-55, Scale, false), new OldAttitudeIndicator.PitchItem(-60, Scale), new OldAttitudeIndicator.PitchItem(-65, Scale, false), new OldAttitudeIndicator.PitchItem(-70, Scale), new OldAttitudeIndicator.PitchItem(-75, Scale, false), new OldAttitudeIndicator.PitchItem(-80, Scale), new OldAttitudeIndicator.PitchItem(-85, Scale, false), new OldAttitudeIndicator.PitchItem(-90, Scale), new OldAttitudeIndicator.PitchItem(-95, Scale, false), new OldAttitudeIndicator.PitchItem(-100, Scale), new OldAttitudeIndicator.PitchItem(-105, Scale, false), new OldAttitudeIndicator.PitchItem(-110, Scale), new OldAttitudeIndicator.PitchItem(-115, Scale, false), new OldAttitudeIndicator.PitchItem(-120, Scale), new OldAttitudeIndicator.PitchItem(-125, Scale, false), new OldAttitudeIndicator.PitchItem(-130, Scale), new OldAttitudeIndicator.PitchItem(-135, Scale, false) ); var velocityControlLength = smallerSide * VelocityControlLengthPrc; var velocityItemLength = velocityControlLength / (VelocityItemCount - 1); VelocityItems = new AvaloniaList( Enumerable .Range(0, VelocityItemCount) .Select(_ => new OldAttitudeIndicator.ScaleItem( 0, VelocityValueRange, _, VelocityItemCount, velocityControlLength + velocityItemLength, velocityControlLength, showNegative: false )) ); var altitudeControlLength = smallerSide * AltitudeControlLengthPrc; var altitudeItemLength = altitudeControlLength / (AltitudeItemCount - 1); AltitudeItems = new AvaloniaList( Enumerable .Range(0, AltitudeItemCount) .Select(_ => new OldAttitudeIndicator.ScaleItem( 0, AltitudeValueRange, _, AltitudeItemCount, altitudeControlLength + altitudeItemLength, altitudeControlLength )) ); var headingControlLength = smallerSide * HeadingControlLengthPrc; var headingItemLength = headingControlLength / (HeadingItemCount - 1); HeadingItems = new AvaloniaList( Enumerable .Range(0, HeadingItemCount) .Select(_ => new HeadingScaleItem( 0, HeadingValueRange, _, HeadingItemCount, headingControlLength + headingItemLength, headingControlLength )) ); var headingItemStep = (headingControlLength + headingItemLength) / (HeadingItemCount % 2 != 0 ? HeadingItemCount - 1 : HeadingItemCount); _headingPositionStep = -1 * headingItemStep / HeadingValueRange; _headingCenterPosition = headingControlLength / 2; } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == AttitudeIndicator.VibrationXProperty) { UpdateColorX(change.Sender); } else if (change.Property == AttitudeIndicator.VibrationYProperty) { UpdateColorY(change.Sender); } else if (change.Property == AttitudeIndicator.VibrationZProperty) { UpdateColorZ(change.Sender); } else if (change.Property == AttitudeIndicator.VelocityProperty) { UpdateVelocityItems(change.Sender); } else if (change.Property == AttitudeIndicator.RollAngleProperty) { UpdateRollAngle(change.Sender); } else if (change.Property == AttitudeIndicator.PitchAngleProperty) { UpdateAngle(change.Sender); } else if (change.Property == AttitudeIndicator.AltitudeProperty) { UpdateAltitudeItems(change.Sender); } else if (change.Property == AttitudeIndicator.HeadingProperty) { UpdateHeadingItems(change.Sender); } else if (change.Property == AttitudeIndicator.HomeAzimuthProperty) { UpdateHomeAzimuthPosition(change.Sender); } } private static void UpdateColorX(AvaloniaObject source) { if (source is not AttitudeIndicator indicator) { return; } if (indicator.VibrationX < 30) { indicator.BrushVibrationX = Colors.Red; } else if (indicator.VibrationX is > 30 and < 60) { indicator.BrushVibrationX = Colors.Yellow; } else if (indicator.VibrationX > 60) { indicator.BrushVibrationX = Colors.GreenYellow; } } private static void UpdateColorY(AvaloniaObject source) { if (source is not AttitudeIndicator indicator) { return; } if (indicator.VibrationY < 30) { indicator.BrushVibrationY = Colors.Red; } else if (indicator.VibrationY > 30 & indicator.VibrationY < 60) { indicator.BrushVibrationY = Colors.Yellow; } else if (indicator.VibrationY > 60) { indicator.BrushVibrationY = Colors.GreenYellow; } } private static void UpdateColorZ(AvaloniaObject source) { if (source is not AttitudeIndicator indicator) { return; } if (indicator.VibrationZ < 30) { indicator.BrushVibrationZ = Colors.Red; } else if (indicator.VibrationZ > 30 & indicator.VibrationZ < 60) { indicator.BrushVibrationZ = Colors.Yellow; } else if (indicator.VibrationZ > 60) { indicator.BrushVibrationZ = Colors.GreenYellow; } } private static void UpdateAngle(AvaloniaObject source) { if (source is not AttitudeIndicator indicator) { return; } var pitch = indicator.PitchAngle; UpdateRollAngle(source); foreach (var item in indicator.PitchItems) { item.UpdateVisibility(pitch); } } private static void UpdateRollAngle(AvaloniaObject source) { if (source is not AttitudeIndicator indicator) { return; } var roll = indicator.RollAngle; var pitch = indicator.PitchAngle; indicator.PitchTranslateX = -pitch * indicator.Scale * Math.Cos((roll - 90.0) * Math.PI / 180.0); indicator.PitchTranslateY = pitch * indicator.Scale * Math.Sin((90 - roll) * Math.PI / 180.0); } private static void UpdateVelocityItems(AvaloniaObject source) { if (source is not AttitudeIndicator indicator) { return; } foreach (var item in indicator.VelocityItems) { item.UpdateValue(indicator.Velocity); } } private static void UpdateAltitudeItems(AvaloniaObject source) { if (source is not AttitudeIndicator indicator) { return; } foreach (var item in indicator.AltitudeItems) { item.UpdateValue(indicator.Altitude); } } private static void UpdateHeadingItems(AvaloniaObject source) { if (source is not AttitudeIndicator indicator) { return; } foreach (var item in indicator.HeadingItems) { item.UpdateValue(indicator.Heading); } indicator.HomeAzimuthPosition = GetHomeAzimuthPosition( indicator.HomeAzimuth, indicator.Heading ); } private static void UpdateHomeAzimuthPosition(AvaloniaObject source) { if (source is not AttitudeIndicator indicator) { return; } foreach (var item in indicator.HeadingItems) { item.UpdateValue(indicator.Heading); } indicator.HomeAzimuthPosition = GetHomeAzimuthPosition( indicator.HomeAzimuth, indicator.Heading ); } private static double GetHomeAzimuthPosition(double? value, double headingValue) { if (value == null) { return -100; } var distance = (headingValue - value.Value) % 360; if (distance < -180) { distance += 360; } else if (distance > 179) { distance -= 360; } return _headingCenterPosition + (distance * _headingPositionStep); } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/OldAttitudeIndicator/AttitudeIndicator.properties.cs ================================================ using System; using System.Collections.Generic; using Avalonia; using Avalonia.Media; namespace Asv.Drones.OldAttitudeIndicator; public partial class AttitudeIndicator { public static readonly DirectProperty< OldAttitudeIndicator.AttitudeIndicator, Color > BrushVibrationXProperty = AvaloniaProperty.RegisterDirect< OldAttitudeIndicator.AttitudeIndicator, Color >(nameof(BrushVibrationX), o => o.BrushVibrationX, (o, v) => o.BrushVibrationX = v); public Color BrushVibrationX { get; set => SetAndRaise(BrushVibrationXProperty, ref field, value); } public static readonly DirectProperty< OldAttitudeIndicator.AttitudeIndicator, Color > BrushVibrationYProperty = AvaloniaProperty.RegisterDirect< OldAttitudeIndicator.AttitudeIndicator, Color >(nameof(BrushVibrationY), o => o.BrushVibrationY, (o, v) => o.BrushVibrationY = v); public Color BrushVibrationY { get; set => SetAndRaise(BrushVibrationYProperty, ref field, value); } public static readonly DirectProperty< OldAttitudeIndicator.AttitudeIndicator, Color > BrushVibrationZProperty = AvaloniaProperty.RegisterDirect< OldAttitudeIndicator.AttitudeIndicator, Color >(nameof(BrushVibrationZ), o => o.BrushVibrationZ, (o, v) => o.BrushVibrationZ = v); public Color BrushVibrationZ { get; set => SetAndRaise(BrushVibrationZProperty, ref field, value); } public static readonly StyledProperty VibrationXProperty = AvaloniaProperty.Register< OldAttitudeIndicator.AttitudeIndicator, float >(nameof(VibrationX), defaultValue: -1); public float VibrationX { get => GetValue(VibrationXProperty); set => SetValue(VibrationXProperty, value); } public static readonly StyledProperty VibrationYProperty = AvaloniaProperty.Register< OldAttitudeIndicator.AttitudeIndicator, float >(nameof(VibrationY), defaultValue: -1); public float VibrationY { get => GetValue(VibrationYProperty); set => SetValue(VibrationYProperty, value); } public static readonly StyledProperty VibrationZProperty = AvaloniaProperty.Register< OldAttitudeIndicator.AttitudeIndicator, float >(nameof(VibrationZ), defaultValue: -1); public float VibrationZ { get => GetValue(VibrationZProperty); set => SetValue(VibrationZProperty, value); } public static readonly StyledProperty Clipping0Property = AvaloniaProperty.Register< OldAttitudeIndicator.AttitudeIndicator, uint >(nameof(Clipping0)); public uint Clipping0 { get => GetValue(Clipping0Property); set => SetValue(Clipping0Property, value); } public static readonly StyledProperty Clipping1Property = AvaloniaProperty.Register< OldAttitudeIndicator.AttitudeIndicator, uint >(nameof(Clipping1)); public uint Clipping1 { get => GetValue(Clipping1Property); set => SetValue(Clipping1Property, value); } public static readonly StyledProperty Clipping2Property = AvaloniaProperty.Register< OldAttitudeIndicator.AttitudeIndicator, uint >(nameof(Clipping2)); public uint Clipping2 { get => GetValue(Clipping2Property); set => SetValue(Clipping2Property, value); } public static readonly StyledProperty RollAngleProperty = AvaloniaProperty.Register< OldAttitudeIndicator.AttitudeIndicator, double >(nameof(RollAngle)); public double RollAngle { get => GetValue(RollAngleProperty); set => SetValue(RollAngleProperty, value); } public static readonly StyledProperty PitchAngleProperty = AvaloniaProperty.Register< OldAttitudeIndicator.AttitudeIndicator, double >(nameof(PitchAngle)); public double PitchAngle { get => GetValue(PitchAngleProperty); set => SetValue(PitchAngleProperty, value); } public static readonly StyledProperty VelocityProperty = AvaloniaProperty.Register< OldAttitudeIndicator.AttitudeIndicator, double >(nameof(Velocity)); public double Velocity { get => GetValue(VelocityProperty); set => SetValue(VelocityProperty, value); } public static readonly StyledProperty AltitudeProperty = AvaloniaProperty.Register< OldAttitudeIndicator.AttitudeIndicator, double >(nameof(Altitude)); public double Altitude { get => GetValue(AltitudeProperty); set => SetValue(AltitudeProperty, value); } public static readonly StyledProperty HeadingProperty = AvaloniaProperty.Register< OldAttitudeIndicator.AttitudeIndicator, double >(nameof(Heading)); public double Heading { get => GetValue(HeadingProperty); set => SetValue(HeadingProperty, value); } public static readonly StyledProperty HomeAzimuthProperty = AvaloniaProperty.Register< OldAttitudeIndicator.AttitudeIndicator, double >(nameof(HomeAzimuth)); public double HomeAzimuth { get => GetValue(HomeAzimuthProperty); set => SetValue(HomeAzimuthProperty, value); } public static readonly StyledProperty IsArmedProperty = AvaloniaProperty.Register< OldAttitudeIndicator.AttitudeIndicator, bool >(nameof(IsArmed)); public bool IsArmed { get => GetValue(IsArmedProperty); set => SetValue(IsArmedProperty, value); } public static readonly DirectProperty< OldAttitudeIndicator.AttitudeIndicator, string > StatusTextProperty = AvaloniaProperty.RegisterDirect< OldAttitudeIndicator.AttitudeIndicator, string >(nameof(StatusText), _ => _.StatusText, (_, value) => _.StatusText = value); public string StatusText { get; set => SetAndRaise(StatusTextProperty, ref field, value); } public static readonly DirectProperty< OldAttitudeIndicator.AttitudeIndicator, string > RightStatusTextProperty = AvaloniaProperty.RegisterDirect< OldAttitudeIndicator.AttitudeIndicator, string >(nameof(RightStatusText), _ => _.RightStatusText, (_, value) => _.RightStatusText = value); public string RightStatusText { get; set => SetAndRaise(RightStatusTextProperty, ref field, value); } public static readonly StyledProperty ArmedTimeProperty = AvaloniaProperty.Register< OldAttitudeIndicator.AttitudeIndicator, TimeSpan >(nameof(ArmedTime)); public TimeSpan ArmedTime { get => GetValue(ArmedTimeProperty); set => SetValue(ArmedTimeProperty, value); } #region Internal direct property public static readonly DirectProperty< OldAttitudeIndicator.AttitudeIndicator, double > InternalWidthProperty = AvaloniaProperty.RegisterDirect< OldAttitudeIndicator.AttitudeIndicator, double >(nameof(InternalWidth), _ => _.InternalWidth, (_, value) => _.InternalWidth = value); public double InternalWidth { get; set => SetAndRaise(InternalWidthProperty, ref field, value); } = 1000; public static readonly DirectProperty< OldAttitudeIndicator.AttitudeIndicator, double > InternalHeightProperty = AvaloniaProperty.RegisterDirect< OldAttitudeIndicator.AttitudeIndicator, double >(nameof(InternalHeight), _ => _.InternalHeight, (_, value) => _.InternalHeight = value); public double InternalHeight { get; set => SetAndRaise(InternalHeightProperty, ref field, value); } = 1000; public static readonly DirectProperty< OldAttitudeIndicator.AttitudeIndicator, double > PitchTranslateXProperty = AvaloniaProperty.RegisterDirect< OldAttitudeIndicator.AttitudeIndicator, double >(nameof(PitchTranslateX), _ => _.PitchTranslateX, (_, value) => _.PitchTranslateX = value); private double PitchTranslateX { get; set => SetAndRaise(PitchTranslateXProperty, ref field, value); } public static readonly DirectProperty< OldAttitudeIndicator.AttitudeIndicator, double > PitchTranslateYProperty = AvaloniaProperty.RegisterDirect< OldAttitudeIndicator.AttitudeIndicator, double >(nameof(PitchTranslateY), _ => _.PitchTranslateY, (_, value) => _.PitchTranslateY = value); public double PitchTranslateY { get; set => SetAndRaise(PitchTranslateYProperty, ref field, value); } public static readonly DirectProperty< OldAttitudeIndicator.AttitudeIndicator, IEnumerable > RollItemsProperty = AvaloniaProperty.RegisterDirect< OldAttitudeIndicator.AttitudeIndicator, IEnumerable >(nameof(RollItems), _ => _.RollItems, (_, value) => _.RollItems = value); public IEnumerable RollItems { get; set => SetAndRaise(RollItemsProperty, ref field, value); } public static readonly DirectProperty< OldAttitudeIndicator.AttitudeIndicator, IEnumerable > PitchItemsProperty = AvaloniaProperty.RegisterDirect< OldAttitudeIndicator.AttitudeIndicator, IEnumerable >(nameof(PitchItems), _ => _.PitchItems, (_, value) => _.PitchItems = value); public IEnumerable PitchItems { get; set => SetAndRaise(PitchItemsProperty, ref field, value); } public static readonly DirectProperty< OldAttitudeIndicator.AttitudeIndicator, IEnumerable > VelocityItemsProperty = AvaloniaProperty.RegisterDirect< OldAttitudeIndicator.AttitudeIndicator, IEnumerable >(nameof(VelocityItems), _ => _.VelocityItems, (_, value) => _.VelocityItems = value); public IEnumerable VelocityItems { get; set => SetAndRaise(VelocityItemsProperty, ref field, value); } public static readonly DirectProperty< OldAttitudeIndicator.AttitudeIndicator, IEnumerable > AltitudeItemsProperty = AvaloniaProperty.RegisterDirect< OldAttitudeIndicator.AttitudeIndicator, IEnumerable >(nameof(AltitudeItems), _ => _.AltitudeItems, (_, value) => _.AltitudeItems = value); public IEnumerable AltitudeItems { get; set => SetAndRaise(AltitudeItemsProperty, ref field, value); } public static readonly DirectProperty< OldAttitudeIndicator.AttitudeIndicator, IEnumerable > HeadingItemsProperty = AvaloniaProperty.RegisterDirect< OldAttitudeIndicator.AttitudeIndicator, IEnumerable >(nameof(HeadingItems), _ => _.HeadingItems, (_, value) => _.HeadingItems = value); public IEnumerable HeadingItems { get; set => SetAndRaise(HeadingItemsProperty, ref field, value); } public static readonly DirectProperty< OldAttitudeIndicator.AttitudeIndicator, double > HomeAzimuthPositionProperty = AvaloniaProperty.RegisterDirect< OldAttitudeIndicator.AttitudeIndicator, double >( nameof(HomeAzimuthPosition), _ => _.HomeAzimuthPosition, (_, value) => _.HomeAzimuthPosition = value ); public double HomeAzimuthPosition { get; set => SetAndRaise(HomeAzimuthPositionProperty, ref field, value); } = -100; #endregion } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/OldAttitudeIndicator/AttitudeIndicatorStyles.axaml ================================================  ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/OldAttitudeIndicator/Items/Heading/HeadingScaleItem.cs ================================================ using System; namespace Asv.Drones.OldAttitudeIndicator; public class HeadingScaleItem : ScaleItem { public HeadingScaleItem( double value, double valueRange, int index, int itemCount, double fullLength, double length ) : base(value, valueRange, index, itemCount, fullLength, length, true) { } protected override string GetTitle(double value) { var v = value < 0 ? ((int)Math.Round(value) % 360) + 360 : (int)Math.Round(value) % 360; return v switch { 0 => RS.HeadingScaleItem_Direction_N, 45 => RS.HeadingScaleItem_Direction_NE, 90 => RS.HeadingScaleItem_Direction_E, 135 => RS.HeadingScaleItem_Direction_SE, 180 => RS.HeadingScaleItem_Direction_S, 225 => RS.HeadingScaleItem_Direction_SW, 270 => RS.HeadingScaleItem_Direction_W, 315 => RS.HeadingScaleItem_Direction_NW, 360 => RS.HeadingScaleItem_Direction_N, _ => v.ToString("F0"), }; } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/OldAttitudeIndicator/Items/Pitch/PitchItem.cs ================================================ using System; using Avalonia; namespace Asv.Drones.OldAttitudeIndicator; public partial class PitchItem : AvaloniaObject { private readonly int _pitch; public PitchItem( int pitch, double scale, bool titleIsVisible = true, double controlHeight = 284 ) { _pitch = pitch; Value = ((controlHeight / 2) - pitch) * scale; if (titleIsVisible) { Title = pitch.ToString(); StartLine = new Point(0 * scale, 0 * scale); StopLine = new Point(20 * scale, 0 * scale); } else { Title = string.Empty; StartLine = new Point(4 * scale, 0 * scale); StopLine = new Point(16 * scale, 0 * scale); } IsVisible = Math.Abs(pitch) <= 20; } public void UpdateVisibility(double pitch) { IsVisible = pitch >= _pitch - 20 && pitch <= _pitch + 20; } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/OldAttitudeIndicator/Items/Pitch/PitchItem.properties.cs ================================================ using Avalonia; namespace Asv.Drones.OldAttitudeIndicator; public partial class PitchItem { public static readonly DirectProperty TitleProperty = AvaloniaProperty.RegisterDirect( nameof(Title), _ => _.Title, (_, value) => _.Title = value ); public string Title { get; set => SetAndRaise(TitleProperty, ref field, value); } public static readonly DirectProperty ValueProperty = AvaloniaProperty.RegisterDirect( nameof(Value), _ => _.Value, (_, value) => _.Value = value ); public double Value { get; set => SetAndRaise(ValueProperty, ref field, value); } public static readonly DirectProperty IsVisibleProperty = AvaloniaProperty.RegisterDirect( nameof(IsVisible), _ => _.IsVisible, (_, value) => _.IsVisible = value ); public bool IsVisible { get; set => SetAndRaise(IsVisibleProperty, ref field, value); } public static readonly DirectProperty StartLineProperty = AvaloniaProperty.RegisterDirect( nameof(StartLine), _ => _.StartLine, (_, value) => _.StartLine = value ); public Point StartLine { get; set => SetAndRaise(StartLineProperty, ref field, value); } public static readonly DirectProperty StopLineProperty = AvaloniaProperty.RegisterDirect( nameof(StopLine), _ => _.StopLine, (_, value) => _.StopLine = value ); public Point StopLine { get; set => SetAndRaise(StopLineProperty, ref field, value); } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/OldAttitudeIndicator/Items/Roll/RollItem.cs ================================================ using System; using Avalonia; namespace Asv.Drones.OldAttitudeIndicator; public partial class RollItem : AvaloniaObject { public RollItem(int angle) { Value = angle; Title = Math.Abs(angle) > 180 ? (360 - Math.Abs(angle)).ToString() : Math.Abs(angle).ToString(); } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/OldAttitudeIndicator/Items/Roll/RollItem.properties.cs ================================================ using Avalonia; namespace Asv.Drones.OldAttitudeIndicator; public partial class RollItem { public static readonly DirectProperty TitleProperty = AvaloniaProperty.RegisterDirect( nameof(Title), _ => _.Title, (_, value) => _.Title = value ); public string Title { get; set => SetAndRaise(TitleProperty, ref field, value); } public static readonly DirectProperty ValueProperty = AvaloniaProperty.RegisterDirect( nameof(Value), _ => _.Value, (_, value) => _.Value = value ); public double Value { get; set => SetAndRaise(ValueProperty, ref field, value); } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/OldAttitudeIndicator/Items/Scale/ScaleItem.cs ================================================ using System; using Avalonia; namespace Asv.Drones.OldAttitudeIndicator; public partial class ScaleItem : AvaloniaObject { public ScaleItem( double value, double valueRange, int index, int itemCount, double fullLength, double length, bool isInverse = false, bool showNegative = true, string? fixedTitle = null ) { _valueRange = valueRange; _showNegative = showNegative; _isFixedTitle = fixedTitle != null; var step = fullLength / (itemCount % 2 != 0 ? itemCount - 1 : itemCount); _positionStep = step / valueRange; if (!isInverse) { _startPosition = ((length - fullLength) / 2.0) + (step * index); } else { _startPosition = ((length - fullLength) / 2.0) + (step * (itemCount - index)); _positionStep *= -1; } var centerIndex = itemCount % 2 == 0 ? itemCount / 2 : (itemCount / 2) + 1; var indexOffset = index - centerIndex; _valueOffset = -1 * valueRange * indexOffset; if (_isFixedTitle) { Title = fixedTitle; } UpdateValue(value); } public void UpdateValue(double value) { Value = GetValue(value); Position = GetPosition(value); if (!_isFixedTitle) { Title = GetTitle(Value); } IsVisible = _showNegative || Value >= 0; } protected virtual string? GetTitle(double value) { return Math.Round(value).ToString("F0"); } private double GetValue(double value) { return Math.Round(value) - (Math.Round(value) % _valueRange) + _valueOffset; } private double GetPosition(double value) { return _startPosition + (_positionStep * (Math.Round(value) % _valueRange)); } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/OldAttitudeIndicator/Items/Scale/ScaleItem.properties.cs ================================================ using Avalonia; namespace Asv.Drones.OldAttitudeIndicator; public partial class ScaleItem { private readonly double _valueRange; private readonly bool _showNegative; private readonly double _startPosition; private readonly double _positionStep; private readonly double _valueOffset; private readonly bool _isFixedTitle; public static readonly DirectProperty IsVisibleProperty = AvaloniaProperty.RegisterDirect( nameof(IsVisible), _ => _.IsVisible, (_, value) => _.IsVisible = value ); public bool IsVisible { get; set => SetAndRaise(IsVisibleProperty, ref field, value); } public static readonly DirectProperty TitleProperty = AvaloniaProperty.RegisterDirect( nameof(Title), _ => _.Title, (_, value) => _.Title = value ); public string? Title { get; set => SetAndRaise(TitleProperty, ref field, value); } public static readonly DirectProperty ValueProperty = AvaloniaProperty.RegisterDirect( nameof(Value), _ => _.Value, (_, value) => _.Value = value ); public double Value { get; set => SetAndRaise(ValueProperty, ref field, value); } public static readonly DirectProperty PositionProperty = AvaloniaProperty.RegisterDirect( nameof(Position), _ => _.Position, (_, value) => _.Position = value ); public double Position { get; set => SetAndRaise(PositionProperty, ref field, value); } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/RouteUavIndicator/RouteUavIndicator.cs ================================================ using System; using Asv.Avalonia; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Metadata; namespace Asv.Drones; [PseudoClasses(ProgressDisabledPseudoclass, ProgressCompletedPseudoclass)] public class RouteUavIndicator : IndicatorBase { private double _internalBorderWidth; public static readonly DirectProperty InternalBorderWidthProperty = AvaloniaProperty.RegisterDirect( nameof(InternalBorderWidth), o => o.InternalBorderWidth, (o, v) => o.InternalBorderWidth = v ); public double InternalBorderWidth { get => _internalBorderWidth; private set => SetAndRaise(InternalBorderWidthProperty, ref _internalBorderWidth, value); } private double _internalBorderLeft; public static readonly DirectProperty InternalBorderLeftProperty = AvaloniaProperty.RegisterDirect( nameof(InternalBorderLeft), o => o.InternalBorderLeft, (o, v) => o.InternalBorderLeft = v ); public double InternalBorderLeft { get => _internalBorderLeft; set => SetAndRaise(InternalBorderLeftProperty, ref _internalBorderLeft, value); } public static readonly StyledProperty ProgressProperty = AvaloniaProperty.Register< RouteUavIndicator, double >(nameof(Progress)); public double Progress { get => GetValue(ProgressProperty); set => SetValue(ProgressProperty, value); } private double _internalIndicatorLeft; public static readonly DirectProperty InternalIndicatorLeftProperty = AvaloniaProperty.RegisterDirect( nameof(InternalIndicatorLeft), o => o.InternalIndicatorLeft, (o, v) => o.InternalIndicatorLeft = v ); public double InternalIndicatorLeft { get => _internalIndicatorLeft; set => SetAndRaise(InternalIndicatorLeftProperty, ref _internalIndicatorLeft, value); } private string _internalProgressText; public static readonly DirectProperty InternalProgressTextProperty = AvaloniaProperty.RegisterDirect( nameof(InternalProgressText), o => o.InternalProgressText, (o, v) => o.InternalProgressText = v ); public string InternalProgressText { get => _internalProgressText; set => SetAndRaise(InternalProgressTextProperty, ref _internalProgressText, value); } public static readonly StyledProperty StatusTextProperty = AvaloniaProperty.Register< RouteUavIndicator, string >(nameof(StatusText)); public string StatusText { get => GetValue(StatusTextProperty); set => SetValue(StatusTextProperty, value); } public static readonly StyledProperty SubStatusTextProperty = AvaloniaProperty.Register< RouteUavIndicator, string >(nameof(SubStatusText)); public string SubStatusText { get => GetValue(SubStatusTextProperty); set => SetValue(SubStatusTextProperty, value); } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == ProgressProperty) { var progress = (double)change.NewValue!; if (double.IsNaN(progress)) { InternalProgressText = string.Empty; PseudoClasses.Add(ProgressDisabledPseudoclass); return; } PseudoClasses.Remove(ProgressDisabledPseudoclass); PseudoClasses.Set(ProgressCompletedPseudoclass, Math.Abs(Progress - 1.0) < 0.01); InternalProgressText = $"{progress * 100.0:F0} %"; if (progress <= 0.0) { progress = 0.0000001; } if (progress >= 1.0) { progress = 0.9999999; } InternalIndicatorLeft = 20 + (progress * 690.0); InternalBorderLeft = progress * 690.0; InternalBorderWidth = 800.0 - InternalBorderLeft; } } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/RouteUavIndicator/RouteUavIndicatorStyles.axaml ================================================  ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/Rtt/AltitudeUavIndicator/AltitudeUavIndicator.axaml ================================================ ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/Rtt/AltitudeUavIndicator/AltitudeUavIndicator.axaml.cs ================================================ using Avalonia.Controls; namespace Asv.Drones; public partial class AltitudeUavIndicator : UserControl { public AltitudeUavIndicator() { InitializeComponent(); } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/Rtt/AltitudeUavIndicator/AltitudeUavIndicatorViewModel.cs ================================================ using System; using System.Diagnostics.CodeAnalysis; using Asv.Avalonia; using Material.Icons; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones; #pragma warning disable SA1313 public record AltitudeRttBoxData(double AltitudeAgl, double AltitudeMsl, IUnitItem AltitudeUnit); #pragma warning restore SA1313 public class AltitudeUavIndicatorViewModel : TwoColumnRttBoxViewModel { [SetsRequiredMembers] public AltitudeUavIndicatorViewModel() : this( nameof(AltitudeUavIndicator), DesignTime.LoggerFactory, new ReactiveProperty(10), new ReactiveProperty(14), DeviceTelemetryDesignPreview.Unit(AltitudeUnit.Id).CurrentUnitItem, DeviceTelemetryDesignPreview.DefaultStatusColor ) { DesignTime.ThrowIfNotDesignMode(); } [SetsRequiredMembers] public AltitudeUavIndicatorViewModel( NavigationId id, ILoggerFactory loggerFactory, ReactiveProperty altitudeAgl, ReactiveProperty altitudeMsl, SynchronizedReactiveProperty currentUnitItem, AsvColorKind defaultStatusColor, TimeSpan? networkErrorTimeout = null ) : base( id, loggerFactory, altitudeAgl .CombineLatest( altitudeMsl, currentUnitItem, (agl, msl, unit) => new AltitudeRttBoxData(agl, msl, unit) ) .ObserveOnUIThreadDispatcher() .ThrottleLast(TimeSpan.FromMilliseconds(200)), networkErrorTimeout ) { Header = RS.UavRttItem_Altitude; Icon = MaterialIconKind.Altimeter; UpdateAction = (model, changes) => { model.Left.ValueString = changes.AltitudeUnit.PrintFromSi(changes.AltitudeAgl, "F2"); model.Right.ValueString = changes.AltitudeUnit.PrintFromSi(changes.AltitudeMsl, "F2"); model.Left.UnitSymbol = changes.AltitudeUnit.Symbol; model.Right.UnitSymbol = changes.AltitudeUnit.Symbol; }; Status = defaultStatusColor; Left.Header = RS.AltitudeUavIndicatorViewModel_Agl; Right.Header = RS.AltitudeUavIndicatorViewModel_Msl; } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/Rtt/AngleUavRttIndicator/AngleUavRttIndicator.axaml ================================================ ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/Rtt/AngleUavRttIndicator/AngleUavRttIndicator.axaml.cs ================================================ using Avalonia.Controls; namespace Asv.Drones; public partial class AngleUavRttIndicator : UserControl { public AngleUavRttIndicator() { InitializeComponent(); } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/Rtt/AngleUavRttIndicator/AngleUavRttIndicatorViewModel.cs ================================================ using System; using System.Diagnostics.CodeAnalysis; using Asv.Avalonia; using Material.Icons; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones; #pragma warning disable SA1313 public record AngleRttBoxData(double Pitch, double Roll, IUnitItem AngleUnit); #pragma warning restore SA1313 public class AngleUavRttIndicatorViewModel : TwoColumnRttBoxViewModel { [SetsRequiredMembers] public AngleUavRttIndicatorViewModel() : this( nameof(AngleUavRttIndicator), DesignTime.LoggerFactory, new ReactiveProperty(30), new ReactiveProperty(10), DeviceTelemetryDesignPreview.Unit(AngleUnit.Id).CurrentUnitItem, DeviceTelemetryDesignPreview.DefaultStatusColor ) { DesignTime.ThrowIfNotDesignMode(); } [SetsRequiredMembers] public AngleUavRttIndicatorViewModel( NavigationId id, ILoggerFactory loggerFactory, ReactiveProperty pitchAngle, ReactiveProperty rollAngle, SynchronizedReactiveProperty currentAngleUnitItem, AsvColorKind defaultStatusColor, TimeSpan? networkErrorTimeout = null ) : base( id, loggerFactory, pitchAngle .CombineLatest( rollAngle, currentAngleUnitItem, (agl, msl, unit) => new AngleRttBoxData(agl, msl, unit) ) .ObserveOnUIThreadDispatcher() .ThrottleLast(TimeSpan.FromMilliseconds(200)), networkErrorTimeout ) { Header = RS.AngleUavRttIndicatorViewModel_Angle; Icon = MaterialIconKind.Altimeter; UpdateAction = (model, changes) => { model.Left.ValueString = changes.AngleUnit.PrintFromSi(changes.Pitch, "F2"); model.Right.ValueString = changes.AngleUnit.PrintFromSi(changes.Roll, "F2"); model.Left.UnitSymbol = changes.AngleUnit.Symbol; model.Right.UnitSymbol = changes.AngleUnit.Symbol; }; Status = defaultStatusColor; Left.Header = RS.AngleUavRttIndicatorViewModel_Pitch; Right.Header = RS.AngleUavRttIndicatorViewModel_Roll; } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/Rtt/BatteryUavIndicator/BatteryUavIndicator.axaml ================================================ ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/Rtt/BatteryUavIndicator/BatteryUavIndicator.axaml.cs ================================================ using Avalonia.Controls; namespace Asv.Drones; public partial class BatteryUavIndicator : UserControl { public BatteryUavIndicator() { InitializeComponent(); } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/Rtt/BatteryUavIndicator/BatteryUavIndicatorViewModel.cs ================================================ using System; using System.Diagnostics.CodeAnalysis; using Asv.Avalonia; using Material.Icons; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones; #pragma warning disable SA1313 public record BatteryRttBoxData( double Charge, double Amperage, double Voltage, double Consumed, IUnitItem ProgressUnit, IUnitItem AmperageUnit, IUnitItem CapacityUnit, IUnitItem VoltageUnit ); #pragma warning restore SA1313 public class BatteryUavIndicatorViewModel : KeyValueRttBoxViewModel { private readonly AsvColorKind defaultStatusColor; [SetsRequiredMembers] public BatteryUavIndicatorViewModel() : this( nameof(BatteryUavIndicator), DesignTime.LoggerFactory, new ReactiveProperty(0.76), new ReactiveProperty(12.4), new ReactiveProperty(23.8), new ReactiveProperty(3900), DeviceTelemetryDesignPreview.Unit(ProgressUnit.Id).CurrentUnitItem, DeviceTelemetryDesignPreview.Unit(AmperageUnit.Id).CurrentUnitItem, DeviceTelemetryDesignPreview.Unit(CapacityUnit.Id).CurrentUnitItem, DeviceTelemetryDesignPreview.Unit(VoltageUnit.Id).CurrentUnitItem, DeviceTelemetryDesignPreview.DefaultStatusColor ) { DesignTime.ThrowIfNotDesignMode(); } [SetsRequiredMembers] public BatteryUavIndicatorViewModel( NavigationId id, ILoggerFactory loggerFactory, ReactiveProperty batteryCharge, ReactiveProperty batteryAmperage, ReactiveProperty batteryVoltage, ReactiveProperty batteryConsumed, SynchronizedReactiveProperty progressUnit, SynchronizedReactiveProperty amperageUnit, SynchronizedReactiveProperty capacityUnit, SynchronizedReactiveProperty voltageUnit, AsvColorKind defaultStatusColor, TimeSpan? networkErrorTimeout = null ) : base( id, loggerFactory, batteryCharge .CombineLatest( batteryAmperage, batteryVoltage, batteryConsumed, progressUnit, amperageUnit, capacityUnit, voltageUnit, (bC, bA, bV, bCo, prog, amp, cap, vol) => new BatteryRttBoxData(bC, bA, bV, bCo, prog, amp, cap, vol) ) .ObserveOnUIThreadDispatcher() .ThrottleLast(TimeSpan.FromMilliseconds(200)), networkErrorTimeout ) { this.defaultStatusColor = defaultStatusColor; Header = RS.UavRttItem_Battery; Icon = MaterialIconKind.Battery10; UpdateAction = (model, changes) => { model[ 0, RS.UavWidgetViewModel_BatteryRttBox_BatteryCharge_Header, changes.ProgressUnit.Symbol ].ValueString = changes.ProgressUnit.PrintFromSi(changes.Charge, "F2"); model[ 1, RS.UavWidgetViewModel_BatteryRttBox_BatteryAmperage_Header, changes.AmperageUnit.Symbol ].ValueString = changes.AmperageUnit.PrintFromSi(changes.Amperage, "F2"); model[ 2, RS.UavWidgetViewModel_BatteryRttBox_BatteryVoltage_Header, changes.VoltageUnit.Symbol ].ValueString = changes.VoltageUnit.PrintFromSi(changes.Voltage, "F2"); model[ 3, RS.UavWidgetViewModel_BatteryRttBox_BatteryConsumed_Header, changes.CapacityUnit.Symbol ].ValueString = changes.CapacityUnit.PrintFromSi(changes.Consumed, "F2"); ChangeBatteryStatus(changes.Charge); }; Status = defaultStatusColor; } private void ChangeBatteryStatus(double percent) { Status = percent switch { > 0.7d => defaultStatusColor, > 0.5d => AsvColorKind.Warning, > 0.4d => AsvColorKind.Warning | AsvColorKind.Blink, < 0.3d => AsvColorKind.Error | AsvColorKind.Blink, _ => defaultStatusColor, }; } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/Rtt/HeadingUavIndicator/HeadingUavIndicator.axaml ================================================ ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/Rtt/HeadingUavIndicator/HeadingUavIndicator.axaml.cs ================================================ using Avalonia.Controls; namespace Asv.Drones; public partial class HeadingUavIndicator : UserControl { public HeadingUavIndicator() { InitializeComponent(); } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/Rtt/HeadingUavIndicator/HeadingUavIndicatorViewModel.cs ================================================ using System; using Asv.Avalonia; using Material.Icons; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones; public class HeadingUavIndicatorViewModel : SplitDigitRttBoxViewModel { public HeadingUavIndicatorViewModel() : this( nameof(HeadingUavIndicator), DesignTime.LoggerFactory, DeviceTelemetryDesignPreview.UnitService, new ReactiveProperty(29), DeviceTelemetryDesignPreview.DefaultStatusColor ) { DesignTime.ThrowIfNotDesignMode(); } public HeadingUavIndicatorViewModel( NavigationId id, ILoggerFactory loggerFactory, IUnitService unitService, ReactiveProperty heading, AsvColorKind defaultStatusColor, TimeSpan? networkErrorTimeout = null ) : base(id, loggerFactory, unitService, AngleUnit.Id, heading, networkErrorTimeout) { Header = RS.HeadingUavIndicatorViewModel_Heading; ShortHeader = RS.HeadingUavIndicatorViewModel_Heading_Short; Icon = MaterialIconKind.SunAzimuth; Status = defaultStatusColor; FormatString = "F0"; } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/Rtt/HomeAzimuthUavIndicator/HomeAzimuthUavIndicator.axaml ================================================ ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/Rtt/HomeAzimuthUavIndicator/HomeAzimuthUavIndicator.axaml.cs ================================================ using Avalonia.Controls; namespace Asv.Drones; public partial class HomeAzimuthUavIndicator : UserControl { public HomeAzimuthUavIndicator() { InitializeComponent(); } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/Rtt/HomeAzimuthUavIndicator/HomeAzimuthUavIndicatorViewModel.cs ================================================ using System; using Asv.Avalonia; using Material.Icons; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones; public class HomeAzimuthUavIndicatorViewModel : SplitDigitRttBoxViewModel { public HomeAzimuthUavIndicatorViewModel() : this( nameof(HomeAzimuthUavIndicator), DesignTime.LoggerFactory, DeviceTelemetryDesignPreview.UnitService, new ReactiveProperty(30), DeviceTelemetryDesignPreview.DefaultStatusColor ) { DesignTime.ThrowIfNotDesignMode(); } public HomeAzimuthUavIndicatorViewModel( NavigationId id, ILoggerFactory loggerFactory, IUnitService unitService, ReactiveProperty homeAzimuth, AsvColorKind defaultStatusColor, TimeSpan? networkErrorTimeout = null ) : base(id, loggerFactory, unitService, AngleUnit.Id, homeAzimuth, networkErrorTimeout) { Header = RS.HomeAzimuthUavIndicatorViewModel_HomeAzimuth; ShortHeader = RS.HomeAzimuthUavIndicatorViewModel_HomeAzimuth_Short; Icon = MaterialIconKind.Home; Status = defaultStatusColor; FormatString = "F0"; } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/Rtt/VelocityUavIndicator/VelocityUavIndicator.axaml ================================================ ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/Rtt/VelocityUavIndicator/VelocityUavIndicator.axaml.cs ================================================ using Avalonia.Controls; namespace Asv.Drones; public partial class VelocityUavIndicator : UserControl { public VelocityUavIndicator() { InitializeComponent(); } } ================================================ FILE: src/Asv.Drones/Core/Controls/DeviceTelemetry/Rtt/VelocityUavIndicator/VelocityUavIndicatorViewModel.cs ================================================ using System; using Asv.Avalonia; using Material.Icons; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones; public class VelocityUavIndicatorViewModel : SplitDigitRttBoxViewModel { public VelocityUavIndicatorViewModel() : this( nameof(VelocityUavIndicator), DesignTime.LoggerFactory, DeviceTelemetryDesignPreview.UnitService, new ReactiveProperty(19.9), DeviceTelemetryDesignPreview.DefaultStatusColor ) { DesignTime.ThrowIfNotDesignMode(); } public VelocityUavIndicatorViewModel( NavigationId id, ILoggerFactory loggerFactory, IUnitService unitService, ReactiveProperty velocity, AsvColorKind defaultStatusColor, TimeSpan? networkErrorTimeout = null ) : base(id, loggerFactory, unitService, VelocityUnit.Id, velocity, networkErrorTimeout) { Header = RS.UavRttItem_Velocity; ShortHeader = RS.VelocityUavIndicatorViewModel_Velocity_Short; Icon = MaterialIconKind.Speedometer; Status = defaultStatusColor; FormatString = "F2"; } } ================================================ FILE: src/Asv.Drones/Core/Converters/Crc32StatusToColorConverter.cs ================================================ using System; using System.Globalization; using Asv.Avalonia; using Avalonia.Data.Converters; namespace Asv.Drones; /// /// Simple converter from Crc32Status to AsvColorKind /// public class Crc32StatusToColorConverter : IValueConverter { public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { if (value is Crc32Status status) { return status switch { Crc32Status.Correct => AsvColorKind.Success, Crc32Status.Incorrect => AsvColorKind.Error, Crc32Status.Default => AsvColorKind.Unknown, _ => AsvColorKind.None, }; } return AsvColorKind.None; } public object? ConvertBack( object? value, Type targetType, object? parameter, CultureInfo culture ) { if (value is AsvColorKind color) { return color switch { AsvColorKind.Success => Crc32Status.Correct, AsvColorKind.Error => Crc32Status.Incorrect, _ => Crc32Status.Default, }; } return Crc32Status.Default; } } ================================================ FILE: src/Asv.Drones/Core/Services/ClientDeviceWidgetFactory/ClientDeviceWidgetFactory.cs ================================================ using System; using System.Collections.Generic; using Asv.Drones.Api; using Asv.IO; namespace Asv.Drones; public class ClientDeviceWidgetFactory : IClientDeviceWidgetFactory { private readonly IReadOnlyDictionary _handlers; public ClientDeviceWidgetFactory(IEnumerable handlers) { ArgumentNullException.ThrowIfNull(handlers); var result = new Dictionary(); foreach (var handler in handlers) { if (!result.TryAdd(handler.DeviceType, handler)) { throw new ArgumentException( $"Duplicate widget handler for type '{handler.DeviceType.FullName}'." ); } } _handlers = result; } public IFlightWidget? CreateWidget(in IClientDevice device) { var currentType = device.GetType(); while (currentType is not null) { if (_handlers.TryGetValue(currentType, out var handler)) { return handler.Create(in device); } currentType = currentType.BaseType; } return null; } } ================================================ FILE: src/Asv.Drones/Core/Services/Devices/Gnss/GnssDeviceManagerExtentsion.cs ================================================ using Asv.Avalonia; using Asv.Avalonia.IO; using Asv.Gnss; using Asv.IO; using Avalonia.Media; using Material.Icons; namespace Asv.Drones; public class GnssDeviceManagerExtension : IDeviceManagerExtension { public void Configure(IProtocolBuilder builder) { builder.Features.RegisterEndpointIdTagFeature(); builder.Protocols.RegisterNmeaProtocol(); } public void Configure(IDeviceExplorerBuilder builder) { builder.Factories.RegisterGnssDevice(); } public bool TryGetIcon(DeviceId id, out MaterialIconKind? icon) { if (id is GnssDeviceId) { icon = MaterialIconKind.Satellite; return true; } icon = null; return false; } public bool TryGetDeviceBrush(DeviceId id, out AsvColorKind brush) { brush = AsvColorKind.None; return false; } public void Run(IDeviceManager deviceManager) { // do nothing } } ================================================ FILE: src/Asv.Drones/Core/Services/Files/BusyFlag.cs ================================================ using System; using R3; namespace Asv.Drones; public sealed class BusyFlag : IDisposable { // guarantees correct balancing // even if multiple operations overlap, and ensures that IsBusy only toggles // on the very first start and the very last completion. private readonly Subject _delta = new(); public Observable IsBusy { get; } public BusyFlag() { IsBusy = _delta .Scan(0, static (acc, d) => acc + d) .Select(static x => x > 0) .Prepend(false) .DistinctUntilChanged() .Publish() .RefCount(); } public IDisposable Enter() { _delta.OnNext(+1); return Disposable.Create(() => _delta.OnNext(-1)); } public void Dispose() => _delta.OnCompleted(); } ================================================ FILE: src/Asv.Drones/Core/Services/Files/Local/LocalFilesService.cs ================================================ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.IO.Abstractions; using System.Linq; using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Common; using Microsoft.Extensions.Logging; namespace Asv.Drones; public class LocalFilesService(IFileSystem? fileSystem = null) { private readonly IFileSystem _fileSystem = fileSystem ?? new FileSystem(); public IReadOnlyList LoadBrowserItems( string path, string root, FileBrowserBackend backend, ILoggerFactory loggerFactory, IDictionary? directoryCfgs, CancellationToken ct = default, ILogger? log = null ) { var result = new ConcurrentBag(); ProcessBrowserDirectory( path, root, ref result, backend, loggerFactory, directoryCfgs, ct, log ); log?.LogTrace("Directory processed ({Path})", path); return result.ToList(); } private void ProcessBrowserDirectory( string path, string root, ref ConcurrentBag items, FileBrowserBackend backend, ILoggerFactory loggerFactory, IDictionary? directoryCfgs, CancellationToken ct = default, ILogger? log = null ) { ct.ThrowIfCancellationRequested(); var rootInfo = new DirectoryInfo(root); var rootId = PathHelper.EncodePathToId(root); var rootVm = new DirectoryItemViewModel( rootId, null, root, rootInfo.Name, FtpBrowserSourceType.Local, loggerFactory ); rootVm.AttachBackend(backend); items.Add(rootVm); foreach (var dir in _fileSystem.Directory.EnumerateDirectories(path)) { ct.ThrowIfCancellationRequested(); var info = new DirectoryInfo(dir); var id = PathHelper.EncodePathToId(dir); var parent = info.Parent?.FullName ?? root; DirectoryItemViewModelConfig? cfg = null; directoryCfgs?.TryGetValue(dir, out cfg); var vm = new DirectoryItemViewModel( id, parent, dir, info.Name, FtpBrowserSourceType.Local, loggerFactory, cfg ); vm.AttachBackend(backend); items.Add(vm); ProcessBrowserDirectory( dir, root, ref items, backend, loggerFactory, directoryCfgs, ct ); } foreach (var file in _fileSystem.Directory.EnumerateFiles(path)) { ct.ThrowIfCancellationRequested(); try { var info = new FileInfo(file); var id = PathHelper.EncodePathToId(file); var parent = info.Directory?.FullName ?? root; var vm = new FileItemViewModel( id, parent, file, info.Name, info.Length, FtpBrowserSourceType.Local, loggerFactory ); vm.AttachBackend(backend); items.Add(vm); } catch (FileNotFoundException ex) { log?.LogWarning(ex, "Skipped file '{File}' during scanning", file); } } } public string RenameFile(string oldPath, string newPath, ILogger? log = null) { try { if (_fileSystem.File.Exists(newPath)) { var parentDir = _fileSystem.Path.GetDirectoryName(oldPath) ?? string.Empty; var baseName = _fileSystem.Path.GetFileNameWithoutExtension(newPath); var ext = _fileSystem.Path.GetExtension(newPath); var counter = 1; while (_fileSystem.File.Exists(newPath)) { newPath = _fileSystem.Path.Combine(parentDir, $"{baseName} ({counter++}){ext}"); } } _fileSystem.File.Move(oldPath, newPath); log?.LogInformation("File renamed to '{new}'", newPath); } catch (FileNotFoundException e) { log?.LogError(e, "Failed to rename file. Incorrect path: {Path}", oldPath); } return newPath; } public string RenameDirectory(string oldPath, string newPath, ILogger? log = null) { try { if (_fileSystem.Directory.Exists(newPath)) { var parentDir = _fileSystem.Path.GetDirectoryName(oldPath) ?? string.Empty; var baseName = _fileSystem.Path.GetFileNameWithoutExtension(newPath); var counter = 1; while (_fileSystem.Directory.Exists(newPath)) { newPath = _fileSystem.Path.Combine(parentDir, $"{baseName} ({counter++})"); } } _fileSystem.Directory.Move(oldPath, newPath); log?.LogInformation("Directory renamed to '{new}'", newPath); } catch (DirectoryNotFoundException e) { log?.LogError(e, "Failed to rename directory. Incorrect path: {Path}", oldPath); } return newPath; } public void CreateDirectory(string path, ILogger? log = null) { var folderNumber = 1; while (true) { var name = _fileSystem.Path.Combine(path, $"Folder{folderNumber}"); if (_fileSystem.Directory.Exists(name)) { folderNumber++; continue; } log?.LogInformation("Creating directory '{Path}'", name); _fileSystem.Directory.CreateDirectory(name); return; } } public void RemoveFile(string path, ILogger? log = null) { try { _fileSystem.File.Delete(path); log?.LogInformation("File removed: '{Path}'", path); } catch (FileNotFoundException e) { log?.LogError(e, "File '{Path}' not found", path); } } public void RemoveDirectory(string path, bool recursive, ILogger? log = null) { try { _fileSystem.Directory.Delete(path, recursive); log?.LogInformation("Directory removed: '{Path}'", path); } catch (FileNotFoundException e) { log?.LogError(e, "Directory '{Path}' not found", path); } } public async Task CalculateCrc32Async( string path, CancellationToken ct = default, ILogger? log = null ) { try { var crc32 = Crc32Mavlink.Accumulate(await _fileSystem.File.ReadAllBytesAsync(path, ct)); log?.LogInformation("File crc32: {crc32}", crc32); return crc32; } catch (FileNotFoundException e) { log?.LogError(e, "File '{Path}' not found", path); } return 0U; } } ================================================ FILE: src/Asv.Drones/Core/Services/Files/PathHelper.cs ================================================ using System; using Asv.Avalonia; namespace Asv.Drones; public static class PathHelper { public static NavigationId EncodePathToId(string path) { var utf8 = System.Text.Encoding.UTF8.GetBytes(path); return Convert .ToBase64String(utf8) .Replace('+', '.') .Replace('/', '_') .Replace("=", string.Empty); } } ================================================ FILE: src/Asv.Drones/Core/Services/Files/ProgressWithLock.cs ================================================ using System; using System.Threading; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using R3; namespace Asv.Drones; public sealed class ProgressWithLock(ILoggerFactory? loggerFactory = null) : IDisposable { private readonly ILogger _log = ( loggerFactory ?? NullLoggerFactory.Instance ).CreateLogger(); private CancellationTokenSource? _cts; private readonly Lock _sync = new(); /// Gets true while a transfer is in progress. public ReactiveProperty IsTransferInProgress { get; } = new(false); /// Gets controls progress overlay visibility. public ReactiveProperty IsProgressVisible { get; } = new(false); /// Gets progress value in range [0..1]. public ReactiveProperty Progress { get; } = new(0); /// /// Disposable scope that represents a single transfer session. /// When disposed, it finalizes the transfer by calling Complete(). /// public readonly struct TransferScope : IDisposable { private readonly ProgressWithLock _owner; /// Gets a linked cancellation token for the current transfer. public CancellationToken Token { get; } /// /// Gets a progress reporter bound to the owner's Report method. /// Prefer to reuse if you report often. /// public IProgress Reporter { get; } internal TransferScope(ProgressWithLock owner, CancellationToken token) { _owner = owner; Token = token; Reporter = new Progress(owner.Report); } public void Dispose() { _owner.Complete(); } } /// /// Starts a transfer session. /// Use using var t = _transfer.BeginScope(ct); and pass t.Token to async operations. /// /// A disposable scope. public TransferScope BeginScope(CancellationToken ct = default) { using (_sync.EnterScope()) { _cts?.Dispose(); _cts = ct.CanBeCanceled ? CancellationTokenSource.CreateLinkedTokenSource(ct) : new CancellationTokenSource(); IsTransferInProgress.OnNext(true); IsProgressVisible.OnNext(true); Progress.OnNext(0); _log.LogDebug("Transfer started"); return new TransferScope(this, _cts.Token); } } /// /// Finish current transfer (hides progress, disposes CTS). /// Safe to call multiple times. /// public void Complete() { using (_sync.EnterScope()) { if (IsTransferInProgress.Value || IsProgressVisible.Value) { IsTransferInProgress.OnNext(false); IsProgressVisible.OnNext(false); Progress.OnNext(0); _log.LogDebug("Transfer completed"); } _cts?.Dispose(); _cts = null; } } /// /// Update progress safely (0..1). Values outside the range are clamped. /// public void Report(double value) { if (double.IsNaN(value) || double.IsInfinity(value)) { value = 0; } if (value < 0) { value = 0; } if (value > 1) { value = 1; } Progress.OnNext(value); } /// /// Tries to cancel the current transfer if any. /// public bool TryCancel() { using (_sync.EnterScope()) { if (_cts == null) { return false; } try { _cts.Cancel(); _log.LogInformation("Transfer cancellation requested"); return true; } catch (ObjectDisposedException) { return false; } } } public void Dispose() { Complete(); IsTransferInProgress.Dispose(); IsProgressVisible.Dispose(); Progress.Dispose(); } } ================================================ FILE: src/Asv.Drones/Core/Services/Files/Remote/FtpClientService.cs ================================================ using System; using System.IO; using System.IO.Abstractions; using System.Linq; using System.Threading; using System.Threading.Tasks; using Asv.Mavlink; using Microsoft.Extensions.Logging; using ObservableCollections; using R3; namespace Asv.Drones; /// /// Unified API for manipulating with files via FTP /// public sealed class FtpClientService( IFtpClientEx ftp, ILoggerFactory logFactory, IFileSystem? fileSystem = null ) : IDisposable { private readonly IFileSystem _fileSystem = fileSystem ?? new FileSystem(); private readonly ILogger _log = logFactory.CreateLogger(); private readonly Subject _remoteChanged = new(); private readonly BusyFlag _busy = new(); /// /// Gets true when FTP-operation executes /// and false when the client is not busy anymore. /// public Observable RemoteChanging => _busy.IsBusy; /// Gets an observable that emits whenever this service changes the remote file system. public Observable RemoteChanged => _remoteChanged; public async Task DownloadAsync( string from, string to, FtpEntryType type, CancellationToken ct, byte partSize = MavlinkFtpHelper.MaxDataSize, IProgress? progress = null ) { ArgumentException.ThrowIfNullOrEmpty(from); ArgumentException.ThrowIfNullOrEmpty(to); if (!_fileSystem.Directory.Exists(to)) { _fileSystem.Directory.CreateDirectory(to); } switch (type) { case FtpEntryType.File: await DownloadFileAsync(from, to, ct, partSize, progress); break; case FtpEntryType.Directory: await DownloadDirectoryAsync(from, to, ct, partSize, progress); break; default: throw new ArgumentOutOfRangeException($"Unsupported FTP entry type: {type}"); } } public async Task BurstDownloadAsync( string from, string to, FtpEntryType type, CancellationToken ct, byte partSize = MavlinkFtpHelper.MaxDataSize, IProgress? progress = null ) { ArgumentException.ThrowIfNullOrEmpty(from); ArgumentException.ThrowIfNullOrEmpty(to); if (!_fileSystem.Directory.Exists(to)) { _fileSystem.Directory.CreateDirectory(to); } switch (type) { case FtpEntryType.File: await BurstDownloadFileAsync(from, to, ct, partSize, progress); break; case FtpEntryType.Directory: await BurstDownloadDirectoryAsync(from, to, ct, partSize, progress); break; default: throw new ArgumentOutOfRangeException($"Unsupported FTP entry type: {type}"); } } public async Task UploadAsync( string from, string to, FtpEntryType type, CancellationToken ct, IProgress? progress = null ) { ArgumentException.ThrowIfNullOrEmpty(from); ArgumentException.ThrowIfNullOrEmpty(to); switch (type) { case FtpEntryType.File: await UploadFileAsync(from, to, ct, progress); break; case FtpEntryType.Directory: await UploadDirectoryAsync(from, to, ct, progress); break; default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } } private async Task DownloadFileAsync( string remoteFilePath, string localDirectory, CancellationToken ct, byte partSize = MavlinkFtpHelper.MaxDataSize, IProgress? progress = null ) { using var busyScope = _busy.Enter(); ct.ThrowIfCancellationRequested(); await using var stream = new FileStream( _fileSystem.Path.Combine(localDirectory, GetRemoteFileName(remoteFilePath)), FileMode.Create, FileAccess.Write, FileShare.None ); await ftp.DownloadFile(remoteFilePath, stream, progress, partSize, ct); _log.LogInformation("Downloaded {path} -> {dir}", remoteFilePath, localDirectory); } private async Task BurstDownloadFileAsync( string remoteFilePath, string localDirectory, CancellationToken ct, byte partSize = MavlinkFtpHelper.MaxDataSize, IProgress? progress = null ) { using var busyScope = _busy.Enter(); ct.ThrowIfCancellationRequested(); await using var stream = new FileStream( _fileSystem.Path.Combine(localDirectory, GetRemoteFileName(remoteFilePath)), FileMode.Create, FileAccess.Write, FileShare.None ); await ftp.BurstDownloadFile(remoteFilePath, stream, progress, partSize, ct); _log.LogInformation("Burst-Downloaded {path} -> {dir}", remoteFilePath, localDirectory); } private async Task UploadFileAsync( string localFilePath, string remoteDirectory, CancellationToken ct, IProgress? progress = null ) { using var busyScope = _busy.Enter(); ct.ThrowIfCancellationRequested(); await using var stream = new FileStream( localFilePath, FileMode.Open, FileAccess.Read, FileShare.Read ); // TODO: sends "(Error to CreateFile: Fail)" after cancellation await ftp.UploadFile(remoteDirectory, stream, progress, ct); _log.LogInformation("Uploaded {file} -> {target}", localFilePath, remoteDirectory); _remoteChanged.OnNext(Unit.Default); } private async Task DownloadDirectoryAsync( string remoteDirectoryPath, string localDirectory, CancellationToken ct, byte partSize = MavlinkFtpHelper.MaxDataSize, IProgress? progress = null ) { using var busyScope = _busy.Enter(); ct.ThrowIfCancellationRequested(); const char dirSep = MavlinkFtpHelper.DirectorySeparator; var remoteRoot = remoteDirectoryPath.TrimEnd(dirSep); ArgumentException.ThrowIfNullOrEmpty(remoteRoot); var remoteRootName = remoteRoot[(remoteRoot.LastIndexOf(dirSep) + 1)..]; var destRoot = _fileSystem.Path.Combine(localDirectory, remoteRootName); _fileSystem.Directory.CreateDirectory(destRoot); var remotePrefix = $"{remoteRoot}{dirSep}"; var dirs = ftp .Entries.Where(e => e.Value.Type is FtpEntryType.Directory && e.Key.StartsWith(remotePrefix, StringComparison.Ordinal) ) .Select(e => e.Key) .Order() .ToList(); var files = ftp .Entries.Where(e => e.Value.Type is FtpEntryType.File && e.Key.StartsWith(remotePrefix, StringComparison.Ordinal) ) .Select(e => e.Key) .Order() .ToList(); foreach (var dir in dirs) { ct.ThrowIfCancellationRequested(); var relative = dir[remotePrefix.Length..].TrimEnd(dirSep); if (relative.Length == 0) { continue; } var localDir = _fileSystem.Path.Combine( destRoot, relative.Replace(dirSep, _fileSystem.Path.DirectorySeparatorChar) ); _fileSystem.Directory.CreateDirectory(localDir); } var total = files.Count; if (total == 0) { progress?.Report(1.0); _log.LogInformation( "Remote directory '{remote}' contains no files", remoteDirectoryPath ); return; } var completed = 0; foreach (var file in files) { ct.ThrowIfCancellationRequested(); var relative = file[remotePrefix.Length..]; var localFileDir = _fileSystem.Path.Combine( destRoot, _fileSystem .Path.GetDirectoryName(relative)! .Replace(dirSep, _fileSystem.Path.DirectorySeparatorChar) ); _fileSystem.Directory.CreateDirectory(localFileDir); var completedSafe = completed; var nested = progress is null ? null : new Progress(p => progress.Report((completedSafe + p) / total)); await DownloadFileAsync(file, localFileDir, ct, partSize, nested); completed++; progress?.Report((double)completed / total); } _log.LogInformation( "Downloaded directory '{remote}' -> '{dest}' (files: {count})", remoteDirectoryPath, destRoot, total ); } private async Task BurstDownloadDirectoryAsync( string remoteDirectoryPath, string localDirectory, CancellationToken ct, byte partSize = MavlinkFtpHelper.MaxDataSize, IProgress? progress = null ) { using var busyScope = _busy.Enter(); ct.ThrowIfCancellationRequested(); const char dirSep = MavlinkFtpHelper.DirectorySeparator; var remoteRoot = remoteDirectoryPath.TrimEnd(dirSep); ArgumentException.ThrowIfNullOrEmpty(remoteRoot); var remoteRootName = remoteRoot[(remoteRoot.LastIndexOf(dirSep) + 1)..]; var destRoot = _fileSystem.Path.Combine(localDirectory, remoteRootName); _fileSystem.Directory.CreateDirectory(destRoot); var remotePrefix = $"{remoteRoot}{dirSep}"; var dirs = ftp .Entries.Where(e => e.Value.Type == FtpEntryType.Directory && e.Key.StartsWith(remotePrefix, StringComparison.Ordinal) ) .Select(e => e.Key) .Order() .ToList(); var files = ftp .Entries.Where(e => e.Value.Type == FtpEntryType.File && e.Key.StartsWith(remotePrefix, StringComparison.Ordinal) ) .Select(e => e.Key) .Order() .ToList(); foreach (var dir in dirs) { ct.ThrowIfCancellationRequested(); var relative = dir[remotePrefix.Length..].TrimEnd(dirSep); if (relative.Length == 0) { continue; } var localDir = _fileSystem.Path.Combine( destRoot, relative.Replace(dirSep, _fileSystem.Path.DirectorySeparatorChar) ); _fileSystem.Directory.CreateDirectory(localDir); } var total = files.Count; if (total == 0) { progress?.Report(1.0); _log.LogInformation( "Remote directory '{remote}' contains no files", remoteDirectoryPath ); return; } var completed = 0; foreach (var file in files) { ct.ThrowIfCancellationRequested(); var relative = file[remotePrefix.Length..]; var localFileDir = _fileSystem.Path.Combine( destRoot, _fileSystem .Path.GetDirectoryName(relative)! .Replace(dirSep, _fileSystem.Path.DirectorySeparatorChar) ); _fileSystem.Directory.CreateDirectory(localFileDir); var completedSafe = completed; var nested = progress is null ? null : new Progress(p => progress.Report((completedSafe + p) / total)); // TODO: sends "(Timeout to execute 'FILE_TRANSFER_PROTOCOL')" when tries to TerminateSession(0) await BurstDownloadFileAsync(file, localFileDir, ct, partSize, nested); completed++; progress?.Report((double)completed / total); } _log.LogInformation( "Burst-Downloaded directory '{remote}' -> '{dest}' (files: {count})", remoteDirectoryPath, destRoot, total ); } private async Task UploadDirectoryAsync( string localDirectoryPath, string remoteDirectory, CancellationToken ct, IProgress? progress = null ) { using var busyScope = _busy.Enter(); ct.ThrowIfCancellationRequested(); var localRoot = new DirectoryInfo(localDirectoryPath); if (!localRoot.Exists) { throw new DirectoryNotFoundException(localDirectoryPath); } const char rSep = MavlinkFtpHelper.DirectorySeparator; var rootName = localRoot.Name; var remoteDirNorm = remoteDirectory; if (!remoteDirNorm.EndsWith(rSep)) { remoteDirNorm += rSep; } var lastSegment = remoteDirNorm .TrimEnd(rSep) .Split(rSep, StringSplitOptions.RemoveEmptyEntries) .LastOrDefault(); var remoteRoot = string.Equals(lastSegment, rootName, StringComparison.Ordinal) ? remoteDirNorm : $"{remoteDirNorm}{rootName}{rSep}"; await ftp.Base.CreateDirectory(remoteRoot, ct); var localDirs = _fileSystem.Directory.GetDirectories( localDirectoryPath, "*", SearchOption.AllDirectories ); foreach (var dir in localDirs) { var rel = _fileSystem .Path.GetRelativePath(localDirectoryPath, dir) .Replace(_fileSystem.Path.DirectorySeparatorChar, rSep); var remoteDir = MavlinkFtpHelper.Combine(remoteRoot, $"{rel}{rSep}"); await ftp.Base.CreateDirectory(remoteDir, ct); } var localFiles = _fileSystem.Directory.GetFiles( localDirectoryPath, "*", SearchOption.AllDirectories ); var totalBytes = localFiles.Sum(f => new FileInfo(f).Length); long uploaded = 0; foreach (var file in localFiles) { ct.ThrowIfCancellationRequested(); var rel = _fileSystem .Path.GetRelativePath(localDirectoryPath, file) .Replace(_fileSystem.Path.DirectorySeparatorChar, rSep); var remoteFilePath = MavlinkFtpHelper.Combine(remoteRoot, rel); await using var stream = new FileStream( file, FileMode.Open, FileAccess.Read, FileShare.Read ); var fileLength = stream.Length; var uploadedSafe = uploaded; var fileProgress = progress == null ? null : new Progress(p => { var current = uploadedSafe + (long)(fileLength * p); progress.Report(current / (double)totalBytes); }); await ftp.UploadFile(remoteFilePath, stream, fileProgress, ct); uploaded += fileLength; progress?.Report(uploaded / (double)totalBytes); _log.LogInformation("Uploaded {File} -> {Target}", file, remoteFilePath); } _log.LogInformation( "Uploaded folder {local} -> {remote} (files: {count})", localDirectoryPath, remoteDirectory, localFiles.Length ); _remoteChanged.OnNext(Unit.Default); } public async Task RemoveDirectoryAsync( string path, bool recursive = true, CancellationToken ct = default ) { try { if (recursive) { await RemoveDirectoryRecursive(path); } else { await ftp.Base.RemoveDirectory(path, ct); } _log.LogInformation("Directory removed: {path}", path); } catch (Exception e) { _log.LogError(e, "Failed to remove directory"); } finally { _remoteChanged.OnNext(Unit.Default); } return; async Task RemoveDirectoryRecursive(string directoryPath) { var itemsInDir = ftp.Entries.Where(x => x.Value.ParentPath == directoryPath).ToList(); foreach (var e in itemsInDir) { switch (e.Value.Type) { case FtpEntryType.Directory: await RemoveDirectoryRecursive(e.Key); break; case FtpEntryType.File: await ftp.Base.RemoveFile(e.Key, ct); break; default: _log.LogError("Unknown FTP entry type: ({type})", e.Value.Type); break; } } await ftp.Base.RemoveDirectory(directoryPath, ct); } } public async Task RemoveFileAsync(string path, CancellationToken ct = default) { await ftp.Base.RemoveFile(path, ct); _log.LogInformation("File removed: {path}", path); _remoteChanged.OnNext(Unit.Default); } public async Task CreateDirectoryAsync(string path, CancellationToken ct = default) { var folderNumber = 1; while (true) { var name = $"Folder{folderNumber}{MavlinkFtpHelper.DirectorySeparator}"; ftp.Entries.FirstOrDefault(x => x.Key == $"{path}{name}").Deconstruct(out var k, out _); if (!string.IsNullOrEmpty(k)) { folderNumber++; continue; } name = path == $"{MavlinkFtpHelper.DirectorySeparator}" ? $"{MavlinkFtpHelper.DirectorySeparator}{name}" : $"{path}{MavlinkFtpHelper.DirectorySeparator}{name}"; _log.LogInformation("Creating directory '{Path}'", name); await ftp.Base.CreateDirectory(name, ct); break; } _remoteChanged.OnNext(Unit.Default); } public async Task CalculateCrc32Async(string filePath, CancellationToken ct = default) { try { var crc32 = await ftp.Base.CalcFileCrc32(filePath, ct); _log.LogInformation("File crc32: {crc32}", crc32); return crc32; } catch (FileNotFoundException e) { _log.LogError(e, "File '{Path}' not found", filePath); } return 0U; } public async Task RenameAsync( string oldPath, string newPath, CancellationToken ct = default ) { var parentDir = ftp.Entries[oldPath].ParentPath; try { if (ftp.Entries.ContainsKey(newPath)) { var fullName = MavlinkFtpHelper.GetFileName(newPath); var baseName = _fileSystem.Path.GetFileNameWithoutExtension(fullName); var ext = _fileSystem.Path.GetExtension(fullName); var counter = 1; while (ftp.Entries.ContainsKey(newPath)) { newPath = MavlinkFtpHelper.Combine(parentDir, $"{baseName} ({counter++}){ext}"); } } else { await ftp.Base.Rename(oldPath, newPath, ct); _log.LogInformation("File renamed to '{new}'", newPath); } } catch (FileNotFoundException e) { _log.LogError(e, "Failed to rename file. Incorrect path: {Path}", oldPath); } _remoteChanged.OnNext(Unit.Default); return newPath; } public async Task> Refresh( CancellationToken ct = default ) { await ftp.Refresh(MavlinkFtpHelper.DirectorySeparator.ToString(), true, ct); return ftp.Entries; } private static string GetRemoteFileName(string path) { if (string.IsNullOrEmpty(path)) { return path; } var slash = path.LastIndexOf(MavlinkFtpHelper.DirectorySeparator); return slash >= 0 ? path[(slash + 1)..] : path; } public void Dispose() { _busy.Dispose(); _remoteChanged.OnCompleted(); ftp.Base.ResetSessions(); } } ================================================ FILE: src/Asv.Drones/Core/Services/Files/Remote/RemoteEntriesSync.cs ================================================ using System; using System.Linq; using Asv.Avalonia; using Asv.IO; using Asv.Mavlink; using Asv.Modeling; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using ObservableCollections; using R3; namespace Asv.Drones; /// /// Synchronizes source dictionary of IFtpEntry into a flat VM list. /// Applies canonicalization and centralizes add/replace/remove logic. /// public sealed class RemoteEntriesSync : IDisposable { private readonly ObservableDictionary _source; private readonly ObservableList _target; private readonly Func _factory; private readonly ILogger _log; private readonly IDisposable _subscriptions; private readonly char _separator; public RemoteEntriesSync( ObservableDictionary source, ObservableList target, Func factory, ILoggerFactory? loggerFactory = null, char separator = MavlinkFtpHelper.DirectorySeparator ) { ArgumentNullException.ThrowIfNull(source); ArgumentNullException.ThrowIfNull(target); ArgumentNullException.ThrowIfNull(factory); _separator = separator; _source = source; _target = target; _factory = factory; _log = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger(); var sub1 = _source .ObserveAdd() .Subscribe(kv => { var pair = kv.Value; var isDir = pair.Value.Type is FtpEntryType.Directory; var key = FtpBrowserPath.Normalize(pair.Key, isDir, _separator); var existing = _target.FirstOrDefault(i => i.Path == key); if (existing != null) { TrySyncMetadata(existing, pair.Value); return; } _target.Add(_factory(key, pair.Value)); }); var sub2 = _source .ObserveRemove() .Subscribe(kv => { var pair = kv.Value; var isDir = pair.Value.Type is FtpEntryType.Directory; var key = FtpBrowserPath.Normalize(pair.Key, isDir, _separator); var victim = _target.FirstOrDefault(i => i.Path == key); if (victim != null) { _target.Remove(victim); } }); var sub3 = _source .ObserveReplace() .Subscribe(kv => { var oldPair = kv.OldValue; var newPair = kv.NewValue; var isOldDir = oldPair.Value.Type is FtpEntryType.Directory; var oldKey = FtpBrowserPath.Normalize(oldPair.Key, isOldDir, _separator); var isNewDir = newPair.Value.Type is FtpEntryType.Directory; var newKey = FtpBrowserPath.Normalize(newPair.Key, isNewDir, _separator); var victim = _target.FirstOrDefault(i => i.Path == oldKey) ?? _target.FirstOrDefault(i => i.Path == newKey); if (victim != null) { _target.Remove(victim); } var existing = _target.FirstOrDefault(i => i.Path == newKey); if (existing != null) { TrySyncMetadata(existing, newPair.Value); return; } _target.Add(_factory(newKey, newPair.Value)); }); var sub4 = _source.ObserveReset().Subscribe(_ => _target.RemoveAll()); _subscriptions = Disposable.Combine(sub1, sub2, sub3, sub4); _log.LogDebug("RemoteEntriesSync started"); } private static void TrySyncMetadata(IBrowserItemViewModel vm, IFtpEntry entry) { if (vm is BrowserItemViewModel b && entry.Name != b.Name) { b.Name = entry.Name; } } public void Dispose() { _subscriptions.Dispose(); _log.LogDebug("RemoteEntriesSync stopped"); } } ================================================ FILE: src/Asv.Drones/RS.Designer.cs ================================================ //------------------------------------------------------------------------------ // // This code was generated by a tool. // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // //------------------------------------------------------------------------------ namespace Asv.Drones { using System; /// /// A strongly-typed resource class, for looking up localized strings, etc. /// // This class was auto-generated by the StronglyTypedResourceBuilder // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class RS { private static global::System.Resources.ResourceManager resourceMan; private static global::System.Globalization.CultureInfo resourceCulture; [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal RS() { } /// /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Asv.Drones.RS", typeof(RS).Assembly); resourceMan = temp; } return resourceMan; } } /// /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] public static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } set { resourceCulture = value; } } /// /// Looks up a localized string similar to Altitude. /// public static string AltitudeIndicatorStyles_ToolTip_Altitude { get { return ResourceManager.GetString("AltitudeIndicatorStyles_ToolTip_Altitude", resourceCulture); } } /// /// Looks up a localized string similar to Compass. /// public static string AltitudeIndicatorStyles_ToolTip_Compass { get { return ResourceManager.GetString("AltitudeIndicatorStyles_ToolTip_Compass", resourceCulture); } } /// /// Looks up a localized string similar to Velocity. /// public static string AltitudeIndicatorStyles_ToolTip_Velocity { get { return ResourceManager.GetString("AltitudeIndicatorStyles_ToolTip_Velocity", resourceCulture); } } /// /// Looks up a localized string similar to Vibration. /// public static string AltitudeIndicatorStyles_ToolTip_Vibration { get { return ResourceManager.GetString("AltitudeIndicatorStyles_ToolTip_Vibration", resourceCulture); } } /// /// Looks up a localized string similar to Enter the size of a download block. /// public static string BurstDownloadDialogView_Description_Text { get { return ResourceManager.GetString("BurstDownloadDialogView_Description_Text", resourceCulture); } } /// /// Looks up a localized string similar to Command that download an element by burst. /// public static string BurstDownloadItemCommand_CommandInfo_Description { get { return ResourceManager.GetString("BurstDownloadItemCommand_CommandInfo_Description", resourceCulture); } } /// /// Looks up a localized string similar to Burst-Download. /// public static string BurstDownloadItemCommand_CommandInfo_Name { get { return ResourceManager.GetString("BurstDownloadItemCommand_CommandInfo_Name", resourceCulture); } } /// /// Looks up a localized string similar to Command that checks a cyclic redundancy of a file (crc32). /// public static string CalculateCrc32Command_CommandInfo_Description { get { return ResourceManager.GetString("CalculateCrc32Command_CommandInfo_Description", resourceCulture); } } /// /// Looks up a localized string similar to Calculate CRC32. /// public static string CalculateCrc32Command_CommandInfo_Name { get { return ResourceManager.GetString("CalculateCrc32Command_CommandInfo_Name", resourceCulture); } } /// /// Looks up a localized string similar to Command that changes frame type. /// public static string ChangeFrameTypeCommand_CommandInfo_Description { get { return ResourceManager.GetString("ChangeFrameTypeCommand_CommandInfo_Description", resourceCulture); } } /// /// Looks up a localized string similar to Change frame type. /// public static string ChangeFrameTypeCommand_CommandInfo_Name { get { return ResourceManager.GetString("ChangeFrameTypeCommand_CommandInfo_Name", resourceCulture); } } /// /// Looks up a localized string similar to Command that clears all packets. /// public static string ClearAllPacketsCommand_CommandInfo_Description { get { return ResourceManager.GetString("ClearAllPacketsCommand_CommandInfo_Description", resourceCulture); } } /// /// Looks up a localized string similar to Clear all packets. /// public static string ClearAllPacketsCommand_CommandInfo_Name { get { return ResourceManager.GetString("ClearAllPacketsCommand_CommandInfo_Name", resourceCulture); } } /// /// Looks up a localized string similar to Command that creates a folder. /// public static string CreateDirectoryCommand_CommandInfo_Description { get { return ResourceManager.GetString("CreateDirectoryCommand_CommandInfo_Description", resourceCulture); } } /// /// Looks up a localized string similar to Create a folder. /// public static string CreateDirectoryCommand_CommandInfo_Name { get { return ResourceManager.GetString("CreateDirectoryCommand_CommandInfo_Name", resourceCulture); } } /// /// Looks up a localized string similar to Command that downloads remote entries. /// public static string DownloadItemCommand_CommandInfo_Description { get { return ResourceManager.GetString("DownloadItemCommand_CommandInfo_Description", resourceCulture); } } /// /// Looks up a localized string similar to Download. /// public static string DownloadItemCommand_CommandInfo_Name { get { return ResourceManager.GetString("DownloadItemCommand_CommandInfo_Name", resourceCulture); } } /// /// Looks up a localized string similar to Command that exports packets to a CSV file. /// public static string ExportPacketsToCsvCommand_CommandInfo_Description { get { return ResourceManager.GetString("ExportPacketsToCsvCommand_CommandInfo_Description", resourceCulture); } } /// /// Looks up a localized string similar to Export packets to CSV. /// public static string ExportPacketsToCsvCommand_CommandInfo_Name { get { return ResourceManager.GetString("ExportPacketsToCsvCommand_CommandInfo_Name", resourceCulture); } } /// /// Looks up a localized string similar to Connect to the device's FTP service.... /// public static string FileBrowserView_AwaitingScreen_Description { get { return ResourceManager.GetString("FileBrowserView_AwaitingScreen_Description", resourceCulture); } } /// /// Looks up a localized string similar to Connecting. /// public static string FileBrowserView_AwaitingScreen_Header { get { return ResourceManager.GetString("FileBrowserView_AwaitingScreen_Header", resourceCulture); } } /// /// Looks up a localized string similar to Burst download. /// public static string FileBrowserView_Button_BurstDownload_Content { get { return ResourceManager.GetString("FileBrowserView_Button_BurstDownload_Content", resourceCulture); } } /// /// Looks up a localized string similar to Download. /// public static string FileBrowserView_Button_Download_Content { get { return ResourceManager.GetString("FileBrowserView_Button_Download_Content", resourceCulture); } } /// /// Looks up a localized string similar to Local Search. /// public static string FileBrowserView_Watermark_Local { get { return ResourceManager.GetString("FileBrowserView_Watermark_Local", resourceCulture); } } /// /// Looks up a localized string similar to Remote Search. /// public static string FileBrowserView_Watermark_Remote { get { return ResourceManager.GetString("FileBrowserView_Watermark_Remote", resourceCulture); } } /// /// Looks up a localized string similar to Download. /// public static string FileBrowserViewModel_BurstDownloadDialog_PrimaryButtonText { get { return ResourceManager.GetString("FileBrowserViewModel_BurstDownloadDialog_PrimaryButtonText", resourceCulture); } } /// /// Looks up a localized string similar to Cancel. /// public static string FileBrowserViewModel_BurstDownloadDialog_SecondaryButtonText { get { return ResourceManager.GetString("FileBrowserViewModel_BurstDownloadDialog_SecondaryButtonText", resourceCulture); } } /// /// Looks up a localized string similar to Burst download. /// public static string FileBrowserViewModel_BurstDownloadDialog_Title { get { return ResourceManager.GetString("FileBrowserViewModel_BurstDownloadDialog_Title", resourceCulture); } } /// /// Looks up a localized string similar to Do you want to download file '{0}'?. /// public static string FileBrowserViewModel_DownloadDialog_Message { get { return ResourceManager.GetString("FileBrowserViewModel_DownloadDialog_Message", resourceCulture); } } /// /// Looks up a localized string similar to Download. /// public static string FileBrowserViewModel_DownloadDialog_Title { get { return ResourceManager.GetString("FileBrowserViewModel_DownloadDialog_Title", resourceCulture); } } /// /// Looks up a localized string similar to File downloaded successfully: {0}. /// public static string FileBrowserViewModel_FileDownloadedSuccessfully { get { return ResourceManager.GetString("FileBrowserViewModel_FileDownloadedSuccessfully", resourceCulture); } } /// /// Looks up a localized string similar to Do you want to remove the item?. /// public static string FileBrowserViewModel_RemoveDialog_Message { get { return ResourceManager.GetString("FileBrowserViewModel_RemoveDialog_Message", resourceCulture); } } /// /// Looks up a localized string similar to Remove. /// public static string FileBrowserViewModel_RemoveDialog_Title { get { return ResourceManager.GetString("FileBrowserViewModel_RemoveDialog_Title", resourceCulture); } } /// /// Looks up a localized string similar to Input new name. /// public static string FileBrowserViewModel_RenameDialog_Message { get { return ResourceManager.GetString("FileBrowserViewModel_RenameDialog_Message", resourceCulture); } } /// /// Looks up a localized string similar to Accept. /// public static string FileBrowserViewModel_RenameDialog_PrimaryButtonText { get { return ResourceManager.GetString("FileBrowserViewModel_RenameDialog_PrimaryButtonText", resourceCulture); } } /// /// Looks up a localized string similar to Cancel. /// public static string FileBrowserViewModel_RenameDialog_SecondaryButtonText { get { return ResourceManager.GetString("FileBrowserViewModel_RenameDialog_SecondaryButtonText", resourceCulture); } } /// /// Looks up a localized string similar to Rename. /// public static string FileBrowserViewModel_RenameDialog_Title { get { return ResourceManager.GetString("FileBrowserViewModel_RenameDialog_Title", resourceCulture); } } /// /// Looks up a localized string similar to Browser. /// public static string FileBrowserViewModel_Title { get { return ResourceManager.GetString("FileBrowserViewModel_Title", resourceCulture); } } /// /// Looks up a localized string similar to Do you want to upload file '{0}' to device?. /// public static string FileBrowserViewModel_UploadingDialog_Message { get { return ResourceManager.GetString("FileBrowserViewModel_UploadingDialog_Message", resourceCulture); } } /// /// Looks up a localized string similar to Uploading. /// public static string FileBrowserViewModel_UploadingDialog_Title { get { return ResourceManager.GetString("FileBrowserViewModel_UploadingDialog_Title", resourceCulture); } } /// /// Looks up a localized string similar to Command that finds a file selected in a remote tree in a local tree. /// public static string FindFileOnLocalCommand_CommandInfo_Description { get { return ResourceManager.GetString("FindFileOnLocalCommand_CommandInfo_Description", resourceCulture); } } /// /// Looks up a localized string similar to Find file. /// public static string FindFileOnLocalCommand_CommandInfo_Name { get { return ResourceManager.GetString("FindFileOnLocalCommand_CommandInfo_Name", resourceCulture); } } /// /// Looks up a localized string similar to Flight. /// public static string FlightPageViewModel_Title { get { return ResourceManager.GetString("FlightPageViewModel_Title", resourceCulture); } } /// /// Looks up a localized string similar to 2D position Fix. /// public static string GpsFixType_GpsFixType2dFix { get { return ResourceManager.GetString("GpsFixType_GpsFixType2dFix", resourceCulture); } } /// /// Looks up a localized string similar to 3D position Fix. /// public static string GpsFixType_GpsFixType3dFix { get { return ResourceManager.GetString("GpsFixType_GpsFixType3dFix", resourceCulture); } } /// /// Looks up a localized string similar to DGPS/SBAS. /// public static string GpsFixType_GpsFixTypeDgps { get { return ResourceManager.GetString("GpsFixType_GpsFixTypeDgps", resourceCulture); } } /// /// Looks up a localized string similar to No GPS connected. /// public static string GpsFixType_GpsFixTypeNoGps { get { return ResourceManager.GetString("GpsFixType_GpsFixTypeNoGps", resourceCulture); } } /// /// Looks up a localized string similar to PPP 3D position. /// public static string GpsFixType_GpsFixTypePpp { get { return ResourceManager.GetString("GpsFixType_GpsFixTypePpp", resourceCulture); } } /// /// Looks up a localized string similar to Rtk Fixed. /// public static string GpsFixType_GpsFixTypeRtkFixed { get { return ResourceManager.GetString("GpsFixType_GpsFixTypeRtkFixed", resourceCulture); } } /// /// Looks up a localized string similar to RTK Float. /// public static string GpsFixType_GpsFixTypeRtkFloat { get { return ResourceManager.GetString("GpsFixType_GpsFixTypeRtkFloat", resourceCulture); } } /// /// Looks up a localized string similar to Static Fix. /// public static string GpsFixType_GpsFixTypeStatic { get { return ResourceManager.GetString("GpsFixType_GpsFixTypeStatic", resourceCulture); } } /// /// Looks up a localized string similar to E. /// public static string HeadingScaleItem_Direction_E { get { return ResourceManager.GetString("HeadingScaleItem_Direction_E", resourceCulture); } } /// /// Looks up a localized string similar to N. /// public static string HeadingScaleItem_Direction_N { get { return ResourceManager.GetString("HeadingScaleItem_Direction_N", resourceCulture); } } /// /// Looks up a localized string similar to NE. /// public static string HeadingScaleItem_Direction_NE { get { return ResourceManager.GetString("HeadingScaleItem_Direction_NE", resourceCulture); } } /// /// Looks up a localized string similar to NW. /// public static string HeadingScaleItem_Direction_NW { get { return ResourceManager.GetString("HeadingScaleItem_Direction_NW", resourceCulture); } } /// /// Looks up a localized string similar to S. /// public static string HeadingScaleItem_Direction_S { get { return ResourceManager.GetString("HeadingScaleItem_Direction_S", resourceCulture); } } /// /// Looks up a localized string similar to SE. /// public static string HeadingScaleItem_Direction_SE { get { return ResourceManager.GetString("HeadingScaleItem_Direction_SE", resourceCulture); } } /// /// Looks up a localized string similar to SW. /// public static string HeadingScaleItem_Direction_SW { get { return ResourceManager.GetString("HeadingScaleItem_Direction_SW", resourceCulture); } } /// /// Looks up a localized string similar to W. /// public static string HeadingScaleItem_Direction_W { get { return ResourceManager.GetString("HeadingScaleItem_Direction_W", resourceCulture); } } /// /// Looks up a localized string similar to Edit mavlink device parameters. /// public static string HomePageParamsDeviceItemAction_ActionViewModel_Description { get { return ResourceManager.GetString("HomePageParamsDeviceItemAction_ActionViewModel_Description", resourceCulture); } } /// /// Looks up a localized string similar to Params editor. /// public static string HomePageParamsDeviceItemAction_ActionViewModel_Header { get { return ResourceManager.GetString("HomePageParamsDeviceItemAction_ActionViewModel_Header", resourceCulture); } } /// /// Looks up a localized string similar to Connecting to the device.... /// public static string MavParamsPageView_AwaitingScreen_Description { get { return ResourceManager.GetString("MavParamsPageView_AwaitingScreen_Description", resourceCulture); } } /// /// Looks up a localized string similar to Connecting. /// public static string MavParamsPageView_AwaitingScreen_Header { get { return ResourceManager.GetString("MavParamsPageView_AwaitingScreen_Header", resourceCulture); } } /// /// Looks up a localized string similar to Cancel. /// public static string MavParamsPageView_CancelButton_Content { get { return ResourceManager.GetString("MavParamsPageView_CancelButton_Content", resourceCulture); } } /// /// Looks up a localized string similar to Loading. /// public static string MavParamsPageView_Loading_Text { get { return ResourceManager.GetString("MavParamsPageView_Loading_Text", resourceCulture); } } /// /// Looks up a localized string similar to Unknown. /// public static string MavParamsPageViewModel_DeviceName_Unknown { get { return ResourceManager.GetString("MavParamsPageViewModel_DeviceName_Unknown", resourceCulture); } } /// /// Looks up a localized string similar to Params. /// public static string MavParamsPageViewModel_Title { get { return ResourceManager.GetString("MavParamsPageViewModel_Title", resourceCulture); } } /// /// Looks up a localized string similar to Download Mission. /// public static string MissionProgressView_DownloadButton_Text { get { return ResourceManager.GetString("MissionProgressView_DownloadButton_Text", resourceCulture); } } /// /// Looks up a localized string similar to Downloading mission. /// public static string MissionProgressView_DownLoadingMission { get { return ResourceManager.GetString("MissionProgressView_DownLoadingMission", resourceCulture); } } /// /// Looks up a localized string similar to Home. /// public static string MissionProgressView_HomeDistance { get { return ResourceManager.GetString("MissionProgressView_HomeDistance", resourceCulture); } } /// /// Looks up a localized string similar to Mission. /// public static string MissionProgressView_MissionDistanceRTT { get { return ResourceManager.GetString("MissionProgressView_MissionDistanceRTT", resourceCulture); } } /// /// Looks up a localized string similar to Refresh mission. /// public static string MissionProgressView_RefreshButton_Text { get { return ResourceManager.GetString("MissionProgressView_RefreshButton_Text", resourceCulture); } } /// /// Looks up a localized string similar to before complete. /// public static string MissionProgressView_SubStatusText { get { return ResourceManager.GetString("MissionProgressView_SubStatusText", resourceCulture); } } /// /// Looks up a localized string similar to Target. /// public static string MissionProgressView_TargetDistance { get { return ResourceManager.GetString("MissionProgressView_TargetDistance", resourceCulture); } } /// /// Looks up a localized string similar to Mission progress. /// public static string MissionProgressView_Title { get { return ResourceManager.GetString("MissionProgressView_Title", resourceCulture); } } /// /// Looks up a localized string similar to Total. /// public static string MissionProgressView_TotalDistanceRTT { get { return ResourceManager.GetString("MissionProgressView_TotalDistanceRTT", resourceCulture); } } /// /// Looks up a localized string similar to min. /// public static string MissionProgressViewModel_MissionFlightTime_Symbol { get { return ResourceManager.GetString("MissionProgressViewModel_MissionFlightTime_Symbol", resourceCulture); } } /// /// Looks up a localized string similar to Motor. /// public static string MotorItemView_Label_MotorId { get { return ResourceManager.GetString("MotorItemView_Label_MotorId", resourceCulture); } } /// /// Looks up a localized string similar to PWM. /// public static string MotorItemView_MonitoringLabel_Pwm { get { return ResourceManager.GetString("MotorItemView_MonitoringLabel_Pwm", resourceCulture); } } /// /// Looks up a localized string similar to Servo. /// public static string MotorItemView_MonitoringLabel_Servo { get { return ResourceManager.GetString("MotorItemView_MonitoringLabel_Servo", resourceCulture); } } /// /// Looks up a localized string similar to START/STOP. /// public static string MotorItemView_ToolTip_StartStop { get { return ResourceManager.GetString("MotorItemView_ToolTip_StartStop", resourceCulture); } } /// /// Looks up a localized string similar to N/A. /// public static string NotANumber { get { return ResourceManager.GetString("NotANumber", resourceCulture); } } /// /// Looks up a localized string similar to Command that opens file browser. /// public static string OpenFileBrowserCommand_CommandInfo_Description { get { return ResourceManager.GetString("OpenFileBrowserCommand_CommandInfo_Description", resourceCulture); } } /// /// Looks up a localized string similar to Open file browser. /// public static string OpenFileBrowserCommand_CommandInfo_Name { get { return ResourceManager.GetString("OpenFileBrowserCommand_CommandInfo_Name", resourceCulture); } } /// /// Looks up a localized string similar to Command that opens flight mode page. /// public static string OpenFlightModeCommand_CommandInfo_Description { get { return ResourceManager.GetString("OpenFlightModeCommand_CommandInfo_Description", resourceCulture); } } /// /// Looks up a localized string similar to Open flight mode. /// public static string OpenFlightModeCommand_CommandInfo_Name { get { return ResourceManager.GetString("OpenFlightModeCommand_CommandInfo_Name", resourceCulture); } } /// /// Looks up a localized string similar to Command opens mavlink params editor. /// public static string OpenMavParamsCommand_CommandInfo_Description { get { return ResourceManager.GetString("OpenMavParamsCommand_CommandInfo_Description", resourceCulture); } } /// /// Looks up a localized string similar to Open mavlink params. /// public static string OpenMavParamsCommand_CommandInfo_Name { get { return ResourceManager.GetString("OpenMavParamsCommand_CommandInfo_Name", resourceCulture); } } /// /// Looks up a localized string similar to Command that opens packet viewer. /// public static string OpenPacketViewerCommand_CommandInfo_Description { get { return ResourceManager.GetString("OpenPacketViewerCommand_CommandInfo_Description", resourceCulture); } } /// /// Looks up a localized string similar to Open packet viewer. /// public static string OpenPacketViewerCommand_CommandInfo_Name { get { return ResourceManager.GetString("OpenPacketViewerCommand_CommandInfo_Name", resourceCulture); } } /// /// Looks up a localized string similar to Command that opens setup page for the drone. /// public static string OpenSetupCommand_CommandInfo_Description { get { return ResourceManager.GetString("OpenSetupCommand_CommandInfo_Description", resourceCulture); } } /// /// Looks up a localized string similar to Open Setup page. /// public static string OpenSetupCommand_CommandInfo_Name { get { return ResourceManager.GetString("OpenSetupCommand_CommandInfo_Name", resourceCulture); } } /// /// Looks up a localized string similar to Date. /// public static string PacketMessageViewModel_CsvColumn_Date { get { return ResourceManager.GetString("PacketMessageViewModel_CsvColumn_Date", resourceCulture); } } /// /// Looks up a localized string similar to Message. /// public static string PacketMessageViewModel_CsvColumn_Message { get { return ResourceManager.GetString("PacketMessageViewModel_CsvColumn_Message", resourceCulture); } } /// /// Looks up a localized string similar to Source. /// public static string PacketMessageViewModel_CsvColumn_Source { get { return ResourceManager.GetString("PacketMessageViewModel_CsvColumn_Source", resourceCulture); } } /// /// Looks up a localized string similar to Type. /// public static string PacketMessageViewModel_CsvColumn_Type { get { return ResourceManager.GetString("PacketMessageViewModel_CsvColumn_Type", resourceCulture); } } /// /// Looks up a localized string similar to Sources. /// public static string PacketViewerView_ExpanderFilterBySources_Header { get { return ResourceManager.GetString("PacketViewerView_ExpanderFilterBySources_Header", resourceCulture); } } /// /// Looks up a localized string similar to Types. /// public static string PacketViewerView_ExpanderFilterByTypes_Header { get { return ResourceManager.GetString("PacketViewerView_ExpanderFilterByTypes_Header", resourceCulture); } } /// /// Looks up a localized string similar to Check All. /// public static string PacketViewerView_Filters_CheckAll { get { return ResourceManager.GetString("PacketViewerView_Filters_CheckAll", resourceCulture); } } /// /// Looks up a localized string similar to Clear All. /// public static string PacketViewerView_ToolTip_ClearAll { get { return ResourceManager.GetString("PacketViewerView_ToolTip_ClearAll", resourceCulture); } } /// /// Looks up a localized string similar to PLAY/PAUSE. /// public static string PacketViewerView_ToolTip_PlayPause { get { return ResourceManager.GetString("PacketViewerView_ToolTip_PlayPause", resourceCulture); } } /// /// Looks up a localized string similar to Save. /// public static string PacketViewerView_ToolTip_Save { get { return ResourceManager.GetString("PacketViewerView_ToolTip_Save", resourceCulture); } } /// /// Looks up a localized string similar to Cancel. /// public static string PacketViewerViewModel_SavePacketMessagesDialog_CloseButtonText { get { return ResourceManager.GetString("PacketViewerViewModel_SavePacketMessagesDialog_CloseButtonText", resourceCulture); } } /// /// Looks up a localized string similar to Accept. /// public static string PacketViewerViewModel_SavePacketMessagesDialog_PrimaryButtonText { get { return ResourceManager.GetString("PacketViewerViewModel_SavePacketMessagesDialog_PrimaryButtonText", resourceCulture); } } /// /// Looks up a localized string similar to Select Separator:. /// public static string PacketViewerViewModel_SavePacketMessagesDialog_Title { get { return ResourceManager.GetString("PacketViewerViewModel_SavePacketMessagesDialog_Title", resourceCulture); } } /// /// Looks up a localized string similar to Packet Viewer. /// public static string PacketViewerViewModel_Title { get { return ResourceManager.GetString("PacketViewerViewModel_Title", resourceCulture); } } /// /// Looks up a localized string similar to Remove all pinned parameters. /// public static string ParametersEditorPageView_PinsOffButton_ToolTip { get { return ResourceManager.GetString("ParametersEditorPageView_PinsOffButton_ToolTip", resourceCulture); } } /// /// Looks up a localized string similar to Star this parameter. /// public static string ParametersEditorPageView_StarButton_ToolTip { get { return ResourceManager.GetString("ParametersEditorPageView_StarButton_ToolTip", resourceCulture); } } /// /// Looks up a localized string similar to Show only starred parameters. /// public static string ParametersEditorPageView_StarsToggleButton_ToolTip { get { return ResourceManager.GetString("ParametersEditorPageView_StarsToggleButton_ToolTip", resourceCulture); } } /// /// Looks up a localized string similar to Update all parameters. /// public static string ParametersEditorPageView_UpdateButton_ToolTip { get { return ResourceManager.GetString("ParametersEditorPageView_UpdateButton_ToolTip", resourceCulture); } } /// /// Looks up a localized string similar to Search. /// public static string ParametersEditorPageViewModel_Search { get { return ResourceManager.GetString("ParametersEditorPageViewModel_Search", resourceCulture); } } /// /// Looks up a localized string similar to Total: {0}. /// public static string ParametersEditorPageViewModel_Total { get { return ResourceManager.GetString("ParametersEditorPageViewModel_Total", resourceCulture); } } /// /// Looks up a localized string similar to Reboot required. /// public static string ParamItemView_InlineNotification_RebootRequired { get { return ResourceManager.GetString("ParamItemView_InlineNotification_RebootRequired", resourceCulture); } } /// /// Looks up a localized string similar to On/ off pin for this parameter. /// public static string ParamItemView_PinToggleButton_ToolTip { get { return ResourceManager.GetString("ParamItemView_PinToggleButton_ToolTip", resourceCulture); } } /// /// Looks up a localized string similar to Refresh. /// public static string ParamItemView_UpdateButton_Text { get { return ResourceManager.GetString("ParamItemView_UpdateButton_Text", resourceCulture); } } /// /// Looks up a localized string similar to Update this parameter from UAV. /// public static string ParamItemView_UpdateButton_ToolTip { get { return ResourceManager.GetString("ParamItemView_UpdateButton_ToolTip", resourceCulture); } } /// /// Looks up a localized string similar to Set. /// public static string ParamItemView_WriteButton_Text { get { return ResourceManager.GetString("ParamItemView_WriteButton_Text", resourceCulture); } } /// /// Looks up a localized string similar to Write this parameter to UAV. /// public static string ParamItemView_WriteButton_ToolTip { get { return ResourceManager.GetString("ParamItemView_WriteButton_ToolTip", resourceCulture); } } /// /// Looks up a localized string similar to Cancel. /// public static string ParamPageViewModel_DataLossDialog_CloseButtonText { get { return ResourceManager.GetString("ParamPageViewModel_DataLossDialog_CloseButtonText", resourceCulture); } } /// /// Looks up a localized string similar to You have unsaved changes. Do you want to save them?. /// public static string ParamPageViewModel_DataLossDialog_Content { get { return ResourceManager.GetString("ParamPageViewModel_DataLossDialog_Content", resourceCulture); } } /// /// Looks up a localized string similar to Save. /// public static string ParamPageViewModel_DataLossDialog_PrimaryButtonText { get { return ResourceManager.GetString("ParamPageViewModel_DataLossDialog_PrimaryButtonText", resourceCulture); } } /// /// Looks up a localized string similar to Don't save. /// public static string ParamPageViewModel_DataLossDialog_SecondaryButtonText { get { return ResourceManager.GetString("ParamPageViewModel_DataLossDialog_SecondaryButtonText", resourceCulture); } } /// /// Looks up a localized string similar to Potential data loss warning. /// public static string ParamPageViewModel_DataLossDialog_Title { get { return ResourceManager.GetString("ParamPageViewModel_DataLossDialog_Title", resourceCulture); } } /// /// Looks up a localized string similar to Command that updates param from the mavlink device. /// public static string ReadParamCommand_CommandInfo_Description { get { return ResourceManager.GetString("ReadParamCommand_CommandInfo_Description", resourceCulture); } } /// /// Looks up a localized string similar to Update param. /// public static string ReadParamCommand_CommandInfo_Name { get { return ResourceManager.GetString("ReadParamCommand_CommandInfo_Name", resourceCulture); } } /// /// Looks up a localized string similar to Command that removes item. /// public static string RemoveItemCommand_CommandInfo_Description { get { return ResourceManager.GetString("RemoveItemCommand_CommandInfo_Description", resourceCulture); } } /// /// Looks up a localized string similar to Remove item. /// public static string RemoveItemCommand_CommandInfo_Name { get { return ResourceManager.GetString("RemoveItemCommand_CommandInfo_Name", resourceCulture); } } /// /// Looks up a localized string similar to Command that commits an item renaming. /// public static string RenameItemCommand_CommandInfo_Description { get { return ResourceManager.GetString("RenameItemCommand_CommandInfo_Description", resourceCulture); } } /// /// Looks up a localized string similar to Commit item rename. /// public static string RenameItemCommand_CommandInfo_Name { get { return ResourceManager.GetString("RenameItemCommand_CommandInfo_Name", resourceCulture); } } /// /// Looks up a localized string similar to TAB. /// public static string SavePacketMessagesDialogView_Separator_Tab { get { return ResourceManager.GetString("SavePacketMessagesDialogView_Separator_Tab", resourceCulture); } } /// /// Looks up a localized string similar to Take off. /// public static string SetAltitudeDialogViewModel_ApplyDialog_PrimaryButton_TakeOff { get { return ResourceManager.GetString("SetAltitudeDialogViewModel_ApplyDialog_PrimaryButton_TakeOff", resourceCulture); } } /// /// Looks up a localized string similar to Cancel. /// public static string SetAltitudeDialogViewModel_ApplyDialog_SecondaryButton_Cancel { get { return ResourceManager.GetString("SetAltitudeDialogViewModel_ApplyDialog_SecondaryButton_Cancel", resourceCulture); } } /// /// Looks up a localized string similar to Set. /// public static string SetupFrameTypeView_ApplyFrame { get { return ResourceManager.GetString("SetupFrameTypeView_ApplyFrame", resourceCulture); } } /// /// Looks up a localized string similar to Current frame. /// public static string SetupFrameTypeView_CurrentFrame { get { return ResourceManager.GetString("SetupFrameTypeView_CurrentFrame", resourceCulture); } } /// /// Looks up a localized string similar to Description. /// public static string SetupFrameTypeView_CurrentFrame_Meta { get { return ResourceManager.GetString("SetupFrameTypeView_CurrentFrame_Meta", resourceCulture); } } /// /// Looks up a localized string similar to Refresh current frame. /// public static string SetupFrameTypeView_CurrentFrame_Refresh { get { return ResourceManager.GetString("SetupFrameTypeView_CurrentFrame_Refresh", resourceCulture); } } /// /// Looks up a localized string similar to Metadata. /// public static string SetupFrameTypeView_FrameMetadata { get { return ResourceManager.GetString("SetupFrameTypeView_FrameMetadata", resourceCulture); } } /// /// Looks up a localized string similar to Please wait. /// public static string SetupFrameTypeView_Loader_Description { get { return ResourceManager.GetString("SetupFrameTypeView_Loader_Description", resourceCulture); } } /// /// Looks up a localized string similar to Loading.... /// public static string SetupFrameTypeView_Loader_Header { get { return ResourceManager.GetString("SetupFrameTypeView_Loader_Header", resourceCulture); } } /// /// Looks up a localized string similar to Do you want to change frame type?. /// public static string SetupFrameTypeViewModel_ApplyConfirmation_Message { get { return ResourceManager.GetString("SetupFrameTypeViewModel_ApplyConfirmation_Message", resourceCulture); } } /// /// Looks up a localized string similar to Change frame type. /// public static string SetupFrameTypeViewModel_ApplyConfirmation_Title { get { return ResourceManager.GetString("SetupFrameTypeViewModel_ApplyConfirmation_Title", resourceCulture); } } /// /// Looks up a localized string similar to Unknown. /// public static string SetupFrameTypeViewModel_CurrentFrame_Unknown { get { return ResourceManager.GetString("SetupFrameTypeViewModel_CurrentFrame_Unknown", resourceCulture); } } /// /// Looks up a localized string similar to Frame type. /// public static string SetupFrameTypeViewModel_Name { get { return ResourceManager.GetString("SetupFrameTypeViewModel_Name", resourceCulture); } } /// /// Looks up a localized string similar to s. /// public static string SetupMotorsView_DurationLabel_TimeUnit { get { return ResourceManager.GetString("SetupMotorsView_DurationLabel_TimeUnit", resourceCulture); } } /// /// Looks up a localized string similar to Duration. /// public static string SetupMotorsView_TestDuration { get { return ResourceManager.GetString("SetupMotorsView_TestDuration", resourceCulture); } } /// /// Looks up a localized string similar to Motor Test. /// public static string SetupMotorsViewModel_Name { get { return ResourceManager.GetString("SetupMotorsViewModel_Name", resourceCulture); } } /// /// Looks up a localized string similar to Setup. /// public static string SetupPageViewModel_Title { get { return ResourceManager.GetString("SetupPageViewModel_Title", resourceCulture); } } /// /// Looks up a localized string similar to Command to stop receiving parameters from the mavlink device. /// public static string StopUpdateParamsCommand_CommandInfo_Description { get { return ResourceManager.GetString("StopUpdateParamsCommand_CommandInfo_Description", resourceCulture); } } /// /// Looks up a localized string similar to Stop refreshing params. /// public static string StopUpdateParamsCommand_CommandInfo_Name { get { return ResourceManager.GetString("StopUpdateParamsCommand_CommandInfo_Name", resourceCulture); } } /// /// Looks up a localized string similar to Set an Auto mode. Use it to continue mission after interrupt. /// public static string UavAction_AutoMode_Description { get { return ResourceManager.GetString("UavAction_AutoMode_Description", resourceCulture); } } /// /// Looks up a localized string similar to Auto mode. /// public static string UavAction_AutoMode_Name { get { return ResourceManager.GetString("UavAction_AutoMode_Name", resourceCulture); } } /// /// Looks up a localized string similar to Guided mode. /// public static string UavAction_GuidedMode { get { return ResourceManager.GetString("UavAction_GuidedMode", resourceCulture); } } /// /// Looks up a localized string similar to Set a Guided mode. /// public static string UavAction_GuidedMode_Description { get { return ResourceManager.GetString("UavAction_GuidedMode_Description", resourceCulture); } } /// /// Looks up a localized string similar to Land. /// public static string UavAction_Land { get { return ResourceManager.GetString("UavAction_Land", resourceCulture); } } /// /// Looks up a localized string similar to Immediately Land. /// public static string UavAction_Land_Description { get { return ResourceManager.GetString("UavAction_Land_Description", resourceCulture); } } /// /// Looks up a localized string similar to Return to launch. /// public static string UavAction_Rtl_Description { get { return ResourceManager.GetString("UavAction_Rtl_Description", resourceCulture); } } /// /// Looks up a localized string similar to RTL. /// public static string UavAction_Rtl_Name { get { return ResourceManager.GetString("UavAction_Rtl_Name", resourceCulture); } } /// /// Looks up a localized string similar to Start Mission. /// public static string UavAction_StartMission { get { return ResourceManager.GetString("UavAction_StartMission", resourceCulture); } } /// /// Looks up a localized string similar to Begins uploaded mission. /// public static string UavAction_StartMission_Description { get { return ResourceManager.GetString("UavAction_StartMission_Description", resourceCulture); } } /// /// Looks up a localized string similar to TakeOff. /// public static string UavAction_TakeOff { get { return ResourceManager.GetString("UavAction_TakeOff", resourceCulture); } } /// /// Looks up a localized string similar to Start takeoff. /// public static string UavAction_TakeOff_Description { get { return ResourceManager.GetString("UavAction_TakeOff_Description", resourceCulture); } } /// /// Looks up a localized string similar to Altitude. /// public static string UavRttItem_Altitude { get { return ResourceManager.GetString("UavRttItem_Altitude", resourceCulture); } } /// /// Looks up a localized string similar to Azimuth. /// public static string UavRttItem_Azimuth { get { return ResourceManager.GetString("UavRttItem_Azimuth", resourceCulture); } } /// /// Looks up a localized string similar to Battery. /// public static string UavRttItem_Battery { get { return ResourceManager.GetString("UavRttItem_Battery", resourceCulture); } } /// /// Looks up a localized string similar to GNSS. /// public static string UavRttItem_GNSS { get { return ResourceManager.GetString("UavRttItem_GNSS", resourceCulture); } } /// /// Looks up a localized string similar to Link. /// public static string UavRttItem_Link { get { return ResourceManager.GetString("UavRttItem_Link", resourceCulture); } } /// /// Looks up a localized string similar to Mode. /// public static string UavRttItem_Mode { get { return ResourceManager.GetString("UavRttItem_Mode", resourceCulture); } } /// /// Looks up a localized string similar to Velocity. /// public static string UavRttItem_Velocity { get { return ResourceManager.GetString("UavRttItem_Velocity", resourceCulture); } } /// /// Looks up a localized string similar to GS. /// public static string VelocityUavIndicatorViewModel_Velocity_Short { get { return ResourceManager.GetString("VelocityUavIndicatorViewModel_Velocity_Short", resourceCulture); } } /// /// Looks up a localized string similar to Heading. /// public static string HeadingUavIndicatorViewModel_Heading { get { return ResourceManager.GetString("HeadingUavIndicatorViewModel_Heading", resourceCulture); } } /// /// Looks up a localized string similar to HDG. /// public static string HeadingUavIndicatorViewModel_Heading_Short { get { return ResourceManager.GetString("HeadingUavIndicatorViewModel_Heading_Short", resourceCulture); } } /// /// Looks up a localized string similar to Home azimuth. /// public static string HomeAzimuthUavIndicatorViewModel_HomeAzimuth { get { return ResourceManager.GetString("HomeAzimuthUavIndicatorViewModel_HomeAzimuth", resourceCulture); } } /// /// Looks up a localized string similar to HOME. /// public static string HomeAzimuthUavIndicatorViewModel_HomeAzimuth_Short { get { return ResourceManager.GetString("HomeAzimuthUavIndicatorViewModel_HomeAzimuth_Short", resourceCulture); } } /// /// Looks up a localized string similar to Angle. /// public static string AngleUavRttIndicatorViewModel_Angle { get { return ResourceManager.GetString("AngleUavRttIndicatorViewModel_Angle", resourceCulture); } } /// /// Looks up a localized string similar to AGL. /// public static string AltitudeUavIndicatorViewModel_Agl { get { return ResourceManager.GetString("AltitudeUavIndicatorViewModel_Agl", resourceCulture); } } /// /// Looks up a localized string similar to MSL. /// public static string AltitudeUavIndicatorViewModel_Msl { get { return ResourceManager.GetString("AltitudeUavIndicatorViewModel_Msl", resourceCulture); } } /// /// Looks up a localized string similar to Pitch. /// public static string AngleUavRttIndicatorViewModel_Pitch { get { return ResourceManager.GetString("AngleUavRttIndicatorViewModel_Pitch", resourceCulture); } } /// /// Looks up a localized string similar to Roll. /// public static string AngleUavRttIndicatorViewModel_Roll { get { return ResourceManager.GetString("AngleUavRttIndicatorViewModel_Roll", resourceCulture); } } /// /// Looks up a localized string similar to Amperage. /// public static string UavWidgetViewModel_BatteryRttBox_BatteryAmperage_Header { get { return ResourceManager.GetString("UavWidgetViewModel_BatteryRttBox_BatteryAmperage_Header", resourceCulture); } } /// /// Looks up a localized string similar to Charge. /// public static string UavWidgetViewModel_BatteryRttBox_BatteryCharge_Header { get { return ResourceManager.GetString("UavWidgetViewModel_BatteryRttBox_BatteryCharge_Header", resourceCulture); } } /// /// Looks up a localized string similar to Capacity. /// public static string UavWidgetViewModel_BatteryRttBox_BatteryConsumed_Header { get { return ResourceManager.GetString("UavWidgetViewModel_BatteryRttBox_BatteryConsumed_Header", resourceCulture); } } /// /// Looks up a localized string similar to Voltage. /// public static string UavWidgetViewModel_BatteryRttBox_BatteryVoltage_Header { get { return ResourceManager.GetString("UavWidgetViewModel_BatteryRttBox_BatteryVoltage_Header", resourceCulture); } } /// /// Looks up a localized string similar to Hdop. /// public static string UavWidgetViewModel_GnssRttBox_Hdop_Header { get { return ResourceManager.GetString("UavWidgetViewModel_GnssRttBox_Hdop_Header", resourceCulture); } } /// /// Looks up a localized string similar to Mode. /// public static string UavWidgetViewModel_GnssRttBox_Mode_Header { get { return ResourceManager.GetString("UavWidgetViewModel_GnssRttBox_Mode_Header", resourceCulture); } } /// /// Looks up a localized string similar to Satellites. /// public static string UavWidgetViewModel_GnssRttBox_SatellitesCount_Header { get { return ResourceManager.GetString("UavWidgetViewModel_GnssRttBox_SatellitesCount_Header", resourceCulture); } } /// /// Looks up a localized string similar to Vdop. /// public static string UavWidgetViewModel_GnssRttBox_Vdop_Header { get { return ResourceManager.GetString("UavWidgetViewModel_GnssRttBox_Vdop_Header", resourceCulture); } } /// /// Looks up a localized string similar to Input Altitude. /// public static string UavWidgetViewModel_SetAltitudeDialog_Title { get { return ResourceManager.GetString("UavWidgetViewModel_SetAltitudeDialog_Title", resourceCulture); } } /// /// Looks up a localized string similar to Armed. /// public static string UavWidgetViewModel_StatusText_Armed { get { return ResourceManager.GetString("UavWidgetViewModel_StatusText_Armed", resourceCulture); } } /// /// Looks up a localized string similar to Disarmed. /// public static string UavWidgetViewModel_StatusText_DisArmed { get { return ResourceManager.GetString("UavWidgetViewModel_StatusText_DisArmed", resourceCulture); } } /// /// Looks up a localized string similar to Pull Up. /// public static string UavWidgetViewModel_StatusText_PullUp { get { return ResourceManager.GetString("UavWidgetViewModel_StatusText_PullUp", resourceCulture); } } /// /// Looks up a localized string similar to byte. /// public static string Unit_Byte_Abbreviation { get { return ResourceManager.GetString("Unit_Byte_Abbreviation", resourceCulture); } } /// /// Looks up a localized string similar to GB. /// public static string Unit_Gigabyte_Abbreviation { get { return ResourceManager.GetString("Unit_Gigabyte_Abbreviation", resourceCulture); } } /// /// Looks up a localized string similar to KB. /// public static string Unit_Kilobyte_Abbreviation { get { return ResourceManager.GetString("Unit_Kilobyte_Abbreviation", resourceCulture); } } /// /// Looks up a localized string similar to MB. /// public static string Unit_Megabyte_Abbreviation { get { return ResourceManager.GetString("Unit_Megabyte_Abbreviation", resourceCulture); } } /// /// Looks up a localized string similar to TB. /// public static string Unit_Terabyte_Abbreviation { get { return ResourceManager.GetString("Unit_Terabyte_Abbreviation", resourceCulture); } } /// /// Looks up a localized string similar to Command removes all pinned items from the params page. /// public static string UnpinAllParamsCommand_CommandInfo_Description { get { return ResourceManager.GetString("UnpinAllParamsCommand_CommandInfo_Description", resourceCulture); } } /// /// Looks up a localized string similar to Unpin all params. /// public static string UnpinAllParamsCommand_CommandInfo_Name { get { return ResourceManager.GetString("UnpinAllParamsCommand_CommandInfo_Name", resourceCulture); } } /// /// Looks up a localized string similar to Command that refreshes params from the mavlink device. /// public static string UpdateParamsCommand_CommandInfo_Description { get { return ResourceManager.GetString("UpdateParamsCommand_CommandInfo_Description", resourceCulture); } } /// /// Looks up a localized string similar to Refresh params. /// public static string UpdateParamsCommand_CommandInfo_Name { get { return ResourceManager.GetString("UpdateParamsCommand_CommandInfo_Name", resourceCulture); } } /// /// Looks up a localized string similar to Command that uploads files to the remote device. /// public static string UploadItemCommand_CommandInfo_Description { get { return ResourceManager.GetString("UploadItemCommand_CommandInfo_Description", resourceCulture); } } /// /// Looks up a localized string similar to Upload. /// public static string UploadItemCommand_CommandInfo_Name { get { return ResourceManager.GetString("UploadItemCommand_CommandInfo_Name", resourceCulture); } } /// /// Looks up a localized string similar to Command that writes the param to the mavlink device. /// public static string WriteParamCommand_CommandInfo_Description { get { return ResourceManager.GetString("WriteParamCommand_CommandInfo_Description", resourceCulture); } } /// /// Looks up a localized string similar to Write param. /// public static string WritePatamCommand_CommandInfo_Name { get { return ResourceManager.GetString("WritePatamCommand_CommandInfo_Name", resourceCulture); } } } } ================================================ FILE: src/Asv.Drones/RS.resx ================================================ text/microsoft-resx 1.3 System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 On/ off pin for this parameter Update this parameter from UAV Refresh Write this parameter to UAV Set Reboot required Show only starred parameters Update all parameters Remove all pinned parameters Search Total: {0} Star this parameter You have unsaved changes. Do you want to save them? Potential data loss warning Save Don't save Cancel byte KB MB GB TB Uploading Do you want to upload file '{0}' to device? Download Do you want to download file '{0}'? File downloaded successfully: {0} Remove Do you want to remove the item? Burst download Download Cancel Unknown N/A Mission progress before complete Mission Total Home Target Downloading mission Auto mode Guided mode TakeOff RTL Land Start Mission Begins uploaded mission Mode Altitude Velocity Azimuth Battery GNSS Link Armed Pull Up Disarmed Flight Start takeoff Return to launch Immediately Land Set an Auto mode. Use it to continue mission after interrupt Set a Guided mode 2D position Fix RTK Float Rtk Fixed DGPS/SBAS PPP 3D position 3D position Fix Static Fix No GPS connected Take off Cancel min Local Search Remote Search Download Burst download Unpin all params Command that writes the param to the mavlink device Write param Command removes all pinned items from the params page Stop refreshing params Command to stop receiving parameters from the mavlink device Refresh params Command that refreshes params from the mavlink device Update param Command that updates param from the mavlink device Open mavlink params Command opens mavlink params editor Params editor Edit mavlink device parameters Select Separator: TAB Save Clear All PLAY/PAUSE Date Source Type Message Sources Types Check All Cancel Accept Rename Accept Cancel Input new name Open flight mode Command that opens flight mode page Open file browser Command that opens file browser Open packet viewer Command that opens packet viewer Velocity Altitude Compass Vibration Browser Input Altitude Connecting Connecting to the device... Cancel Loading Packet Viewer Params Clear all packets Command that clears all packets Export packets to CSV Command that exports packets to a CSV file Commit item rename Command that commits an item renaming Connecting Connect to the device's FTP service... Remove item Command that removes item Command that checks a cyclic redundancy of a file (crc32) Calculate CRC32 Create a folder Command that creates a folder Burst-Download Command that download an element by burst Download Command that downloads remote entries Upload Command that uploads files to the remote device Find file Command that finds a file selected in a remote tree in a local tree Setup Frame type Command that changes frame type Change frame type Loading... Please wait Current frame Metadata Set Change frame type Do you want to change frame type? Unknown Open Setup page Command that opens setup page for the drone Refresh current frame Description Refresh mission Download Mission Duration s Motor Servo PWM START/STOP Motor Test N NE E SE S SW W NW Enter the size of a download block Charge Amperage Voltage Capacity Satellites Hdop Vdop Mode Heading HDG Home azimuth HOME GS Angle AGL MSL Pitch Roll ================================================ FILE: src/Asv.Drones/RS.ru.resx ================================================ text/microsoft-resx 1.3 System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Включить/ выключить закрепление этого параметра Обновить этот парметр из БПЛА Обновить Записать этот парметр в БПЛА Установить Требуется перезагрузка Показать только избранные параметры Обновить все параметры Убрать все прикреплённые параметры Найти Всего: {0} Добавить параметр в избранные У вас есть несохраненные данные. Хотите сохранить их? Предупреждение о возможной потере данных Сохранить Не сохранять Отмена байт ГБ КБ МБ ТБ Выгрузка Вы хотите загрузить файл '{0}' на устройство? Загрузка Вы хотите скачать файл '{0}'? Файл успешно загружен: {0} Удаление Вы хотите удалить элемент? Загрузка (пакетная) Загрузить Отмена Неизвестно Локальный поиск Удалённый поиск Загрузить Загрузить (пакетами) Н/Д Стартовая точка Миссия до завершения Цель Прогресс миссии Общая дистанция Загрузка миссии Авто режим Ручной режим Посадка Домой Начать миссию Взлететь Высота Азимут Батарея ГНСС Связь с НСУ Режим Скорость Наберите высоту Разблокировано Заблокировано Полет Начать взлёт Возврат на точку запуска Автоматический режим. Используйте для продолжения миссии после прерывания Незамедлительная посадка Устанавливает ручной режим 2Д позиция Фикс 3Д позиция Фикс DGRS/SBAS Нет подключения PPP 3Д позиция RTK фиксированный RTK свободный Статичный Запуск ранее загруженной миссии Отмена Взлететь мин Снять пины со всех параметров Команда записывает параметр в мавлинк устройство Записать параметр Команда снимает пины со всех параметров на странице параметров Команда останавливает получение параметров с устройства мавлинк Остановить получение параметров Обновить параметры Команда обновляет параметры с устройства мавлинк Обновить параметр Команда обновляет один параметр с устройства мавлинк Открыть параметры устройства мавлинк Команда открывает реадактор мавлинк параметров Редактор параметров Изменить параметры мавлинк устройства Выберите разделитель: Табуляция Сохранить Очистить все ЗАПУСТИТЬ/ОСТАНОВИТЬ Дата Сообщение Источник Тип Источники Типы Выбрать все Отменить Подтвердить Переименовать Подтвердить Отменить Введите новое имя Открыть режим полета Команда, которая открывает страницу режима полета Открыть браузер файлов Команда открывает файл браузер Открыть просмотр пакетов Команда открывает просмотр пакетов Скорость Высота Компас Вибрация Браузер Введите высоту Подключение Подключение к устройству... Отмена Загрузка... Просмотр пакетов Параметры Подтвердить переименование элемента Команда, которая подтверждает переименование элемента Очистить все пакеты Команда, которая очищает все пакеты Экспортировать пакеты в CSV файл Команда экспортирует пакеты в файл с CSV расширением Подключение к FTP сервису устройства... Подключение Удалить элемент Команда, которая удаляет элемент Команда, которая проверяет циклическую избыточность файла (crc32) Рассчитать CRC32 Создать папку Команда, которая создаёт папку Загрузить (пакетами) Команда, которая загружает элемент пакетами Загрузка Команда, которая загружает удалённые элементы Отгрузка Команда, которая отгружает файлы на удалённое устройство Найти файл Команда, которая находит файл, выбранный в удаленном дереве, в локальном дереве Настройка Тип рамы Команда, которая изменяет тип рамы Изменить тип рамы Загрузка... Пожалуйста, подождите Установить Мета-данные Текущая рама Смена типа рамы Вы хотите сменить тип рамы? Неизвестно Открыть страницу настройки Команда открывает страницу для настройки дрона Обновить текущую раму Описание Обновить миссию Скачать миссию Время теста сек Мотор Сервопривод ШИМ ЗАПУСТИТЬ/ОСТАНОВИТЬ Тест моторов В С СВ СЗ Ю ЮВ ЮЗ З Введите размер блока для загрузки Заряд Емкость Сила тока Напряжение Спутники Hdop Vdop Режим Курс КУРС Азимут дома ДОМ СК Углы AGL MSL Тангаж Крен ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/FileBrowser/Contracts/FileBrowserBackend.cs ================================================ using System; namespace Asv.Drones; public sealed class FileBrowserBackend(LocalFilesService local, FtpClientService ftp) { private LocalBrowserItemsOperations LocalItemsOperations { get; } = new(local); private RemoteBrowserItemsOperations RemoteItemsOperations { get; } = new(ftp); public IBrowserItemsOperations ResolveOps(FtpBrowserSourceType type) { return type switch { FtpBrowserSourceType.Local => LocalItemsOperations, FtpBrowserSourceType.Remote => RemoteItemsOperations, _ => throw new ArgumentOutOfRangeException(nameof(type), type, null), }; } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/FileBrowser/Contracts/IBrowserItemsOperations.cs ================================================ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace Asv.Drones; public interface IBrowserItemsOperations { char Separator { get; } ValueTask RenameFileAsync( string oldPath, string newPath, ILogger logger, CancellationToken ct ); ValueTask RenameDirectoryAsync( string oldPath, string newPath, ILogger logger, CancellationToken ct ); ValueTask RemoveDirectoryAsync(string path, ILogger logger, CancellationToken ct); ValueTask RemoveFileAsync(string path, ILogger logger, CancellationToken ct); ValueTask CreateDirectoryAsync(string path, ILogger logger, CancellationToken ct); ValueTask CalculateCrc32Async(string path, ILogger logger, CancellationToken ct); } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/FileBrowser/Contracts/LocalBrowserItemsOperations.cs ================================================ using System; using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace Asv.Drones; public sealed class LocalBrowserItemsOperations(LocalFilesService local) : IBrowserItemsOperations { private readonly LocalFilesService _local = local ?? throw new ArgumentNullException(nameof(local)); public char Separator => Path.DirectorySeparatorChar; public ValueTask RenameDirectoryAsync( string oldPath, string newPath, ILogger logger, CancellationToken ct ) { var result = _local.RenameDirectory(oldPath, newPath, logger); return ValueTask.FromResult(result); } public ValueTask RenameFileAsync( string oldPath, string newPath, ILogger logger, CancellationToken ct ) { var result = _local.RenameFile(oldPath, newPath, logger); return ValueTask.FromResult(result); } public ValueTask RemoveDirectoryAsync(string path, ILogger logger, CancellationToken ct) { ct.ThrowIfCancellationRequested(); _local.RemoveDirectory(path, true, logger); return ValueTask.CompletedTask; } public ValueTask RemoveFileAsync(string path, ILogger logger, CancellationToken ct) { ct.ThrowIfCancellationRequested(); _local.RemoveFile(path, logger); return ValueTask.CompletedTask; } public ValueTask CreateDirectoryAsync(string path, ILogger logger, CancellationToken ct) { _local.CreateDirectory(path, logger); return ValueTask.CompletedTask; } public async ValueTask CalculateCrc32Async( string path, ILogger logger, CancellationToken ct ) { return await _local.CalculateCrc32Async(path, ct, logger); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/FileBrowser/Contracts/RemoteBrowserItemsOperations.cs ================================================ using System; using System.Threading; using System.Threading.Tasks; using Asv.Mavlink; using Microsoft.Extensions.Logging; namespace Asv.Drones; public sealed class RemoteBrowserItemsOperations(FtpClientService ftp) : IBrowserItemsOperations { private readonly FtpClientService _ftp = ftp ?? throw new ArgumentNullException(nameof(ftp)); public char Separator => MavlinkFtpHelper.DirectorySeparator; public async ValueTask RenameDirectoryAsync( string oldPath, string newPath, ILogger logger, CancellationToken ct ) { return await _ftp.RenameAsync(oldPath, newPath, ct); } public async ValueTask RenameFileAsync( string oldPath, string newPath, ILogger logger, CancellationToken ct ) { return await _ftp.RenameAsync(oldPath, newPath, ct); } public async ValueTask RemoveDirectoryAsync(string path, ILogger logger, CancellationToken ct) { await _ftp.RemoveDirectoryAsync(path, true, ct); } public async ValueTask RemoveFileAsync(string path, ILogger logger, CancellationToken ct) { await _ftp.RemoveFileAsync(path, ct); } public async ValueTask CreateDirectoryAsync(string path, ILogger logger, CancellationToken ct) { await _ftp.CreateDirectoryAsync(path, ct); } public async ValueTask CalculateCrc32Async( string path, ILogger logger, CancellationToken ct ) { return await _ftp.CalculateCrc32Async(path, ct); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/FileBrowser/Dialogs/BurstDownloadDialogView.axaml ================================================ ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/FileBrowser/Dialogs/BurstDownloadDialogView.axaml.cs ================================================ using Asv.Avalonia; using Avalonia.Controls; namespace Asv.Drones; public partial class BurstDownloadDialogView : UserControl { public BurstDownloadDialogView() { InitializeComponent(); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/FileBrowser/Dialogs/BurstDownloadDialogViewModel.cs ================================================ using System.Collections.Generic; using Asv.Avalonia; using Asv.Common; using Asv.Mavlink; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones; public class BurstDownloadDialogViewModel : DialogViewModelBase { private const string DialogId = $"{BaseId}.burst"; public BurstDownloadDialogViewModel() : this(DesignTime.LoggerFactory) { DesignTime.ThrowIfNotDesignMode(); } public BurstDownloadDialogViewModel(ILoggerFactory loggerFactory) : base(DialogId, loggerFactory) { PacketSize = new BindableReactiveProperty( MavlinkFtpHelper.MaxDataSize ).DisposeItWith(Disposable); PacketSize .EnableValidationRoutable( arg => { if (arg is < 1 or > MavlinkFtpHelper.MaxDataSize) { return ValidationResult.FailAsOutOfRange( "1", MavlinkFtpHelper.MaxDataSize.ToString() ); } return ValidationResult.Success; }, this, isForceValidation: true ) .DisposeItWith(Disposable); } public BindableReactiveProperty PacketSize { get; } public override void ApplyDialog(ContentDialog dialog) { dialog.DefaultButton = ContentDialogButton.Primary; _sub.Disposable = IsValid.Subscribe(b => dialog.IsPrimaryButtonEnabled = b); } public override IEnumerable GetChildren() { return []; } private readonly SerialDisposable _sub = new(); protected override void Dispose(bool disposing) { if (disposing) { _sub.Dispose(); } base.Dispose(disposing); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/FileBrowser/FileBrowserView.axaml ================================================  Info5 Info10 Info5 ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/FileBrowser/FileBrowserView.axaml.cs ================================================ using System.Collections.Generic; using System.Linq; using Asv.Avalonia; using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.VisualTree; namespace Asv.Drones; public class FileBrowserViewConfig : IGridColumnLayoutConfig { public IDictionary Columns { get; set; } = new Dictionary(); } public partial class FileBrowserView : UserControl { private readonly ILayoutService _layoutService; private int _realLocalFilesColumnOrder; private int _realGridSplitterColumnOrder; private int _realRemoteFilesColumnOrder; private FileBrowserViewConfig? _config; public FileBrowserView() : this(NullLayoutService.Instance) { DesignTime.ThrowIfNotDesignMode(); } public FileBrowserView(ILayoutService layoutService) { _layoutService = layoutService; InitializeComponent(); } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { LoadLayout(); base.OnAttachedToVisualTree(e); } private void GridSplitter_Dragged(object? sender, VectorEventArgs e) { if (Design.IsDesignMode) { return; } if (sender is not GridSplitter) { return; } SaveLayout(); } private void LoadLayout() { if (Design.IsDesignMode) { return; } _config = _layoutService.Get(this); _realLocalFilesColumnOrder = Grid.GetColumn(LocalFilesColumn); _realGridSplitterColumnOrder = Grid.GetColumn(GridSplitterColumn); _realRemoteFilesColumnOrder = Grid.GetColumn(RemoteFilesColumn); if (_config.Columns.Count == 0) { return; } if ( MainGrid.ColumnDefinitions.Count != _config.Columns.Keys.Count || _config.Columns.All(kvp => kvp.Value.Width.Value == 0) || !_config.Columns.TryGetValue( LocalFilesColumn.Name ?? string.Empty, out var localFilesColumn ) || !_config.Columns.TryGetValue( GridSplitterColumn.Name ?? string.Empty, out var gridSplitterColumn ) || !_config.Columns.TryGetValue( RemoteFilesColumn.Name ?? string.Empty, out var remoteFilesColumn ) || _realLocalFilesColumnOrder != localFilesColumn.Order || _realGridSplitterColumnOrder != gridSplitterColumn.Order || _realRemoteFilesColumnOrder != remoteFilesColumn.Order ) { _config = new FileBrowserViewConfig(); return; } MainGrid.ColumnDefinitions[_realLocalFilesColumnOrder].Width = new GridLength( localFilesColumn.Width.Value, GridUnitType.Star ); MainGrid.ColumnDefinitions[_realGridSplitterColumnOrder].Width = new GridLength( gridSplitterColumn.Width.Value, GridUnitType.Star ); MainGrid.ColumnDefinitions[_realRemoteFilesColumnOrder].Width = new GridLength( remoteFilesColumn.Width.Value, GridUnitType.Star ); } private void SaveLayout() { if (Design.IsDesignMode) { return; } if (_config is null) { return; } if (DataContext is null) { return; } if ( LocalFilesColumn.Name is null || GridSplitterColumn.Name is null || RemoteFilesColumn.Name is null ) { return; } _config.Columns = new Dictionary { [LocalFilesColumn.Name] = new() { Order = _realLocalFilesColumnOrder, Width = new GridLengthConfig { Value = MainGrid.ColumnDefinitions[_realLocalFilesColumnOrder].ActualWidth, GridUnitType = GridUnitType.Star, }, }, [GridSplitterColumn.Name] = new() { Order = _realGridSplitterColumnOrder, Width = new GridLengthConfig { Value = MainGrid.ColumnDefinitions[_realGridSplitterColumnOrder].ActualWidth, GridUnitType = GridUnitType.Star, }, }, [RemoteFilesColumn.Name] = new() { Order = _realRemoteFilesColumnOrder, Width = new GridLengthConfig { Value = MainGrid.ColumnDefinitions[_realRemoteFilesColumnOrder].ActualWidth, GridUnitType = GridUnitType.Star, }, }, }; _layoutService.SetInMemory(this, _config); } private void TreeView_OnPointerPressed(object? sender, PointerPressedEventArgs e) { if (Design.IsDesignMode) { return; } if (sender is not TreeView tree) { return; } var hit = tree.InputHitTest(e.GetPosition(tree)); if (IsInsideTreeViewItem(hit)) { return; } tree.SelectedItem = null; } private static bool IsInsideTreeViewItem(IInputElement? hit) { var v = hit as Visual; while (v is not null && v is not TreeViewItem) { v = v.GetVisualParent(); } return v is TreeViewItem; } } public static class TreeViewBehaviors { public static readonly AttachedProperty IgnoreEnterProperty = AvaloniaProperty.RegisterAttached("IgnoreEnter", typeof(TreeViewBehaviors)); static TreeViewBehaviors() { IgnoreEnterProperty.Changed.AddClassHandler(OnIgnoreEnterChanged); } public static void SetIgnoreEnter(TreeView element, bool value) => element.SetValue(IgnoreEnterProperty, value); public static bool GetIgnoreEnter(TreeView element) => element.GetValue(IgnoreEnterProperty); private static void OnIgnoreEnterChanged(TreeView tree, AvaloniaPropertyChangedEventArgs e) { var enabled = e.GetNewValue(); if (enabled) { tree.AddHandler(InputElement.KeyDownEvent, OnTreeKeyDown, RoutingStrategies.Tunnel); } else { tree.RemoveHandler(InputElement.KeyDownEvent, OnTreeKeyDown); } } private static void OnTreeKeyDown(object? sender, KeyEventArgs e) { if (e.Key == Key.Enter) { e.Handled = true; } } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/FileBrowser/FileBrowserViewModel.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Avalonia.IO; using Asv.Cfg; using Asv.Common; using Asv.Drones.Api; using Asv.IO; using Asv.Mavlink; using Asv.Modeling; using Material.Icons; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using NuGet.Packaging; using ObservableCollections; using R3; namespace Asv.Drones; public sealed class FileBrowserViewModelConfig { public string LocalSearchText { get; set; } = string.Empty; public string RemoteSearchText { get; set; } = string.Empty; public string? LocalSelectedItemKey { get; set; } public string? RemoteSelectedItemKey { get; set; } public IDictionary LocalDirectories { get; set; } = new Dictionary(); public IDictionary RemoteDirectories { get; set; } = new Dictionary(); } public class FileBrowserViewModel : DevicePageViewModel, IFileBrowserViewModel, ISupportRefresh, ITransferFtpEntries { public const string PageId = "files.browser"; public const MaterialIconKind PageIcon = MaterialIconKind.FolderEye; private const int SearchThrottleMs = 500; private readonly LocalFilesService _localFilesService; private readonly YesOrNoDialogPrefab _yesNoDialog; private readonly ILoggerFactory _loggerFactory; private readonly INavigationService _navigation; private readonly ProgressWithLock _transfer; private readonly string _localRootPath; private readonly string _remoteRootPath; private readonly ObservableDictionary _rawRemoteEntries; private readonly ObservableList _localItems; private readonly ObservableList _remoteItems; private FtpClientService? _ftpService; private FileBrowserBackend? _backend; private FileBrowserViewModelConfig? _config; public FileBrowserViewModel() : this( DesignTime.CommandService, NullDeviceManager.Instance, AppHost.Instance.Services.GetRequiredService(), NullLayoutService.Instance, NullLoggerFactory.Instance, DesignTime.Navigation, NullDialogService.Instance, NullExtensionService.Instance ) { RemoteItemsTree = new BrowserTree( new ObservableList { new DirectoryItemViewModel( NavigationId.Empty, "/", "/folder/", "folder", FtpBrowserSourceType.Remote, _loggerFactory ), new FileItemViewModel( NavigationId.Empty, "/", "/file.txt", "file.txt", 111, FtpBrowserSourceType.Remote, _loggerFactory ), new FileItemViewModel( NavigationId.Empty, "/", "/file2.txt", "file2.txt", 2222, FtpBrowserSourceType.Remote, _loggerFactory ), new FileItemViewModel( NavigationId.Empty, "/", "/file3.txt", "file3.txt", 333333, FtpBrowserSourceType.Remote, _loggerFactory ), new FileItemViewModel( NavigationId.Empty, "/", "/file4.txt", "file4.txt", 44444444, FtpBrowserSourceType.Remote, _loggerFactory ), }, "/" ); LocalItemsTree = new BrowserTree( new ObservableList { new DirectoryItemViewModel( NavigationId.Empty, "/", "/folder/", "folder", FtpBrowserSourceType.Remote, _loggerFactory ), new FileItemViewModel( NavigationId.Empty, "/", "/file.txt", "file.txt", 128, FtpBrowserSourceType.Remote, _loggerFactory ), new FileItemViewModel( NavigationId.Empty, "/", "/file2.txt", "file2.txt", 64544, FtpBrowserSourceType.Remote, _loggerFactory ), new FileItemViewModel( NavigationId.Empty, "/", "/file3.txt", "file3.txt", 23512612, FtpBrowserSourceType.Remote, _loggerFactory ), new FileItemViewModel( NavigationId.Empty, "/", "/file4.txt", "file4.txt", 23423, FtpBrowserSourceType.Remote, _loggerFactory ), }, "/" ); } public FileBrowserViewModel( ICommandService cmd, IDeviceManager devices, IHostEnvironment appPath, ILayoutService layoutService, ILoggerFactory loggerFactory, INavigationService navigation, IDialogService dialogService, IExtensionService ext ) : base(PageId, devices, cmd, layoutService, loggerFactory, dialogService, ext) { _localRootPath = appPath.ContentRootPath; _remoteRootPath = MavlinkFtpHelper.DirectorySeparator.ToString(); _loggerFactory = loggerFactory; _navigation = navigation; _yesNoDialog = dialogService.GetDialogPrefab(); _localFilesService = new LocalFilesService(); _transfer = new ProgressWithLock(loggerFactory).DisposeItWith(Disposable); IsTransferInProgress = _transfer .IsTransferInProgress.ToBindableReactiveProperty() .DisposeItWith(Disposable); IsProgressVisible = _transfer .IsProgressVisible.ToBindableReactiveProperty() .DisposeItWith(Disposable); Progress = _transfer.Progress.ToBindableReactiveProperty().DisposeItWith(Disposable); var watcher = new FileSystemWatcher(_localRootPath) { NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.Size, IncludeSubdirectories = true, EnableRaisingEvents = true, }.DisposeItWith(Disposable); Observable .Merge( Observable.FromEvent( h => (_, _) => h(Unit.Default), h => watcher.Created += h, h => watcher.Created -= h ), Observable.FromEvent( h => (_, _) => h(Unit.Default), h => watcher.Deleted += h, h => watcher.Deleted -= h ), Observable.FromEvent( h => (_, _) => h(Unit.Default), h => watcher.Changed += h, h => watcher.Changed -= h ), Observable.FromEvent( h => (_, _) => h(Unit.Default), h => watcher.Renamed += h, h => watcher.Renamed -= h ) ) .ThrottleLast(TimeSpan.FromMilliseconds(150)) .Subscribe(_ => RefreshLocalCommand?.Execute(Unit.Default)) .DisposeItWith(Disposable); _localItems = []; _remoteItems = []; _localItems.DisposeRemovedItems().DisposeItWith(Disposable); _remoteItems.DisposeRemovedItems().DisposeItWith(Disposable); _localItems.SetRoutableParent(this).DisposeItWith(Disposable); _remoteItems.SetRoutableParent(this).DisposeItWith(Disposable); // TODO: The sync may be done by ObservableTree in Asv.Avalonia instead _rawRemoteEntries = new ObservableDictionary(); _ = new RemoteEntriesSync( _rawRemoteEntries, _remoteItems, RemoteEntryToBrowserItem, _loggerFactory ).DisposeItWith(Disposable); LocalItemsTree = new BrowserTree(_localItems, _localRootPath).DisposeItWith(Disposable); RemoteItemsTree = new BrowserTree( _remoteItems, MavlinkFtpHelper.DirectorySeparator.ToString() ).DisposeItWith(Disposable); LocalSelectedItem = new BindableReactiveProperty(null).DisposeItWith( Disposable ); RemoteSelectedItem = new BindableReactiveProperty(null).DisposeItWith( Disposable ); IsDownloadPopupOpen = new BindableReactiveProperty(false).DisposeItWith(Disposable); IsUiBlocked = new BindableReactiveProperty(false).DisposeItWith(Disposable); CanDownload .Where(b => !b) .Subscribe(_ => IsDownloadPopupOpen.OnNext(false)) .DisposeItWith(Disposable); LocalSearch = new SearchBoxViewModel( nameof(LocalSearch), loggerFactory, PerformLocalSearch, TimeSpan.FromMilliseconds(SearchThrottleMs) ) .SetRoutableParent(this) .DisposeItWith(Disposable); RemoteSearch = new SearchBoxViewModel( nameof(RemoteSearch), loggerFactory, PerformRemoteSearch, TimeSpan.FromMilliseconds(SearchThrottleMs) ) .SetRoutableParent(this) .DisposeItWith(Disposable); InitCommands(); Events.Subscribe(InternalCatchEvent).DisposeItWith(Disposable); } #region Properties public BrowserTree LocalItemsTree { get; } public BrowserTree RemoteItemsTree { get; } public BindableReactiveProperty LocalSelectedItem { get; } public BindableReactiveProperty RemoteSelectedItem { get; } public SearchBoxViewModel LocalSearch { get; } public SearchBoxViewModel RemoteSearch { get; } public BindableReactiveProperty Progress { get; } public BindableReactiveProperty IsDownloadPopupOpen { get; } public BindableReactiveProperty IsUiBlocked { get; } public BindableReactiveProperty IsProgressVisible { get; } public BindableReactiveProperty IsTransferInProgress { get; } #endregion #region Commands public ReactiveCommand ShowDownloadPopupCommand { get; private set; } public ReactiveCommand UploadCommand { get; private set; } public ReactiveCommand DownloadCommand { get; private set; } public ReactiveCommand BurstDownloadCommand { get; private set; } public ReactiveCommand CreateRemoteFolderCommand { get; private set; } public ReactiveCommand CreateLocalFolderCommand { get; private set; } public ReactiveCommand RefreshRemoteCommand { get; private set; } public ReactiveCommand RefreshLocalCommand { get; private set; } public ReactiveCommand RemoveLocalItemCommand { get; private set; } public ReactiveCommand RemoveRemoteItemCommand { get; private set; } public ReactiveCommand LocalRenameCommand { get; private set; } public ReactiveCommand RemoteRenameCommand { get; private set; } public ReactiveCommand CompareSelectedItemsCommand { get; private set; } public ReactiveCommand FindFileOnLocalCommand { get; private set; } public ReactiveCommand CalculateLocalCrc32Command { get; private set; } public ReactiveCommand CalculateRemoteCrc32Command { get; private set; } public ReactiveCommand CancelTransferCommand { get; private set; } #endregion #region Observables private Observable CanUpload => LocalSelectedItem.Select(x => x is not null); private Observable CanDownload => RemoteSelectedItem.Select(x => x is not null); private Observable CanRemoveLocal => LocalSelectedItem.Select(x => x is { Base.EditMode: false }); private Observable CanRemoveRemote => RemoteSelectedItem.Select(x => x is { Base.EditMode: false }); private Observable CanFindFileOnLocal => RemoteSelectedItem.Select(x => x is { Base: { EditMode: false, FtpEntryType: FtpEntryType.File } } ); private Observable CanCompareSelectedItems => LocalSelectedItem.CombineLatest( RemoteSelectedItem, (local, remote) => local is { Base: { EditMode: false, FtpEntryType: FtpEntryType.File } } && remote is { Base: { EditMode: false, FtpEntryType: FtpEntryType.File } } ); private Observable CanCalculateRemoteCrc32 => RemoteSelectedItem.Select(x => x is { Base: { EditMode: false, FtpEntryType: FtpEntryType.File } } ); private Observable CanCalculateLocalCrc32 => LocalSelectedItem.Select(x => x is { Base: { EditMode: false, FtpEntryType: FtpEntryType.File } } ); private Observable CanRenameLocal => LocalSelectedItem.Select(x => x is { Base.EditMode: false }); private Observable CanRenameRemote => RemoteSelectedItem.Select(x => x is { Base.EditMode: false }); #endregion #region Commands Impl private void InitCommands() { FindFileOnLocalCommand = CanFindFileOnLocal .ToReactiveCommand( async (_, ct) => await this.ExecuteCommand(FindFileCommand.Id, CommandArg.Empty, ct), awaitOperation: AwaitOperation.Drop ) .DisposeItWith(Disposable); CompareSelectedItemsCommand = CanCompareSelectedItems .ToReactiveCommand( async (_, ct) => await CompareSelectedItemsImpl(ct), awaitOperation: AwaitOperation.Drop ) .DisposeItWith(Disposable); CalculateLocalCrc32Command = CanCalculateLocalCrc32 .ToReactiveCommand( async (node, ct) => await CalculateCrc32Impl(node, ct), awaitOperation: AwaitOperation.Drop ) .DisposeItWith(Disposable); CalculateRemoteCrc32Command = CanCalculateRemoteCrc32 .ToReactiveCommand( async (node, ct) => await CalculateCrc32Impl(node, ct), awaitOperation: AwaitOperation.Drop ) .DisposeItWith(Disposable); RefreshRemoteCommand = new ReactiveCommand( async (_, ct) => await RefreshRemoteImpl(ct) ).DisposeItWith(Disposable); RefreshLocalCommand = new ReactiveCommand(RefreshLocalImpl).DisposeItWith(Disposable); LocalRenameCommand = CanRenameLocal .ToReactiveCommand(SetEditModeImpl, awaitOperation: AwaitOperation.Drop) .DisposeItWith(Disposable); RemoteRenameCommand = CanRenameRemote .ToReactiveCommand( async (node, ct) => await SetEditModeImpl(node, ct), awaitOperation: AwaitOperation.Drop ) .DisposeItWith(Disposable); // TODO: Upload incorrectly processes cancellation (prob. something wrong at Asv.Mavlink) UploadCommand = CanUpload .ToReactiveCommand(async (node, ct) => await UploadImpl(node, ct)) .DisposeItWith(Disposable); DownloadCommand = CanDownload .ToReactiveCommand(async (node, ct) => await DownloadImpl(node, ct)) .DisposeItWith(Disposable); BurstDownloadCommand = CanDownload .ToReactiveCommand(async (node, ct) => await BurstDownloadImpl(node, ct)) .DisposeItWith(Disposable); CancelTransferCommand = new ReactiveCommand(_ => _transfer.TryCancel()).DisposeItWith( Disposable ); CreateRemoteFolderCommand = new ReactiveCommand( async (node, ct) => { if (node is null) { var item = _remoteItems.FirstOrDefault(x => x.Path.Equals(_remoteRootPath)); if (item != null) { await CreateFolderImpl(item, ct); } } else { await CreateFolderImpl(node.Base, ct); } }, awaitOperation: AwaitOperation.Drop ).DisposeItWith(Disposable); CreateLocalFolderCommand = new ReactiveCommand( async (node, ct) => { if (node is null) { var item = _localItems.FirstOrDefault(x => x.Path.Equals(_localRootPath)); if (item != null) { await CreateFolderImpl(item, ct); } } else { await CreateFolderImpl(node.Base, ct); } }, awaitOperation: AwaitOperation.Drop ).DisposeItWith(Disposable); RemoveLocalItemCommand = CanRemoveLocal .ToReactiveCommand(RemoveItemImpl, awaitOperation: AwaitOperation.Drop) .DisposeItWith(Disposable); RemoveRemoteItemCommand = CanRemoveRemote .ToReactiveCommand( async (node, ct) => await RemoveItemImpl(node, ct), awaitOperation: AwaitOperation.Drop ) .DisposeItWith(Disposable); RefreshRemoteCommand.IgnoreOnErrorResume(e => { if (e is not FtpNackEndOfFileException) { throw e; } }); ShowDownloadPopupCommand = CanDownload .ToReactiveCommand(_ => IsDownloadPopupOpen.OnNext(!IsDownloadPopupOpen.Value)) .DisposeItWith(Disposable); RefreshRemoteCommand.SubscribeOnUIThreadDispatcher(); RefreshLocalCommand.SubscribeOnUIThreadDispatcher(); } private async Task UploadImpl(BrowserNode item, CancellationToken ct) { var payload = new YesOrNoDialogPayload { Title = RS.FileBrowserViewModel_UploadingDialog_Title, Message = string.Format( RS.FileBrowserViewModel_UploadingDialog_Message, item.Base.Name ), }; var res = await _yesNoDialog.ShowDialogAsync(payload); if (res) { string remoteDirectory; var remoteNode = RemoteSelectedItem.Value; var localName = LocalSelectedItem.Value?.Base.Name ?? FtpBrowserNamingPolicy.BlankName; const char sep = MavlinkFtpHelper.DirectorySeparator; if (remoteNode != null) { var remotePath = remoteNode.Base.Path; if (remoteNode.Base.FtpEntryType is FtpEntryType.Directory) { remoteDirectory = remotePath + localName; } else { var parentDir = remotePath[..remotePath.LastIndexOf(sep)]; remoteDirectory = parentDir + sep + localName; } } else { remoteDirectory = sep + localName; } await this.ExecuteCommand( UploadItemCommand.Id, CommandArg.CreateDictionary( new Dictionary { { TransferCommandBase.SourcePath, CommandArg.CreateString(item.Base.Path) }, { TransferCommandBase.DestinationPath, CommandArg.CreateString(remoteDirectory) }, { TransferCommandBase.EntryType, CommandArg.CreateString(item.Base.FtpEntryType.ToString()) }, } ), ct ); } } private async ValueTask DownloadImpl(BrowserNode item, CancellationToken ct) { if (_ftpService is null) { return; } var localDirectory = _localRootPath; if (RemoteSelectedItem.Value is null) { return; } if (LocalSelectedItem.Value != null) { if (LocalSelectedItem.Value.Base.HasChildren) { localDirectory = LocalSelectedItem.Value.Base.Path; } else { localDirectory = LocalSelectedItem.Value.Base.Path[ ..LocalSelectedItem.Value.Base.Path.LastIndexOf(Path.DirectorySeparatorChar) ]; } } var payload = new YesOrNoDialogPayload { Title = RS.FileBrowserViewModel_DownloadDialog_Title, Message = string.Format(RS.FileBrowserViewModel_DownloadDialog_Message, item.Base.Name), }; var res = await _yesNoDialog.ShowDialogAsync(payload); if (res) { await this.ExecuteCommand( BurstDownloadItemCommand.Id, CommandArg.CreateDictionary( new Dictionary { { TransferCommandBase.SourcePath, CommandArg.CreateString(item.Base.Path) }, { TransferCommandBase.DestinationPath, CommandArg.CreateString(localDirectory) }, { TransferCommandBase.PartSize, CommandArg.CreateInteger(MavlinkFtpHelper.MaxDataSize) }, { TransferCommandBase.EntryType, CommandArg.CreateString(item.Base.FtpEntryType.ToString()) }, } ), ct ); } } private async ValueTask BurstDownloadImpl(BrowserNode item, CancellationToken ct) { var localDirectory = _localRootPath; if (LocalSelectedItem.Value != null) { if (LocalSelectedItem.Value.Base.HasChildren) { localDirectory = LocalSelectedItem.Value.Base.Path; } else { localDirectory = LocalSelectedItem.Value.Base.Path[ ..LocalSelectedItem.Value.Base.Path.LastIndexOf(Path.DirectorySeparatorChar) ]; } } using var viewModel = new BurstDownloadDialogViewModel(_loggerFactory); var dialog = new ContentDialog(viewModel, _navigation) { Title = RS.FileBrowserViewModel_BurstDownloadDialog_Title, PrimaryButtonText = RS.FileBrowserViewModel_BurstDownloadDialog_PrimaryButtonText, SecondaryButtonText = RS.FileBrowserViewModel_BurstDownloadDialog_SecondaryButtonText, IsPrimaryButtonEnabled = viewModel.IsValid.Value, IsSecondaryButtonEnabled = true, }; viewModel.ApplyDialog(dialog); var result = await dialog.ShowAsync(); if (result == ContentDialogResult.Primary) { await this.ExecuteCommand( BurstDownloadItemCommand.Id, CommandArg.CreateDictionary( new Dictionary { { TransferCommandBase.SourcePath, CommandArg.CreateString(item.Base.Path) }, { TransferCommandBase.DestinationPath, CommandArg.CreateString(localDirectory) }, { TransferCommandBase.PartSize, CommandArg.CreateInteger( viewModel.PacketSize.Value ?? MavlinkFtpHelper.MaxDataSize ) }, { TransferCommandBase.EntryType, CommandArg.CreateString(item.Base.FtpEntryType.ToString()) }, } ), ct ); } } private async ValueTask RemoveItemImpl(BrowserNode item, CancellationToken ct) { var payload = new YesOrNoDialogPayload { Title = RS.FileBrowserViewModel_RemoveDialog_Title, Message = RS.FileBrowserViewModel_RemoveDialog_Message, }; var res = await _yesNoDialog.ShowDialogAsync(payload); if (!res) { return; } await item.Base.ExecuteCommand(RemoveItemCommand.Id, CommandArg.Empty, ct); } private static async ValueTask CreateFolderImpl( IBrowserItemViewModel item, CancellationToken ct ) { await item.ExecuteCommand(CreateDirectoryCommand.Id, CommandArg.Empty, ct); } private async Task RefreshRemoteImpl(CancellationToken ct) { if (_ftpService is null) { return; } var items = await _ftpService.Refresh(ct); var selected = RemoteSelectedItem.Value?.Key; var toRemove = _rawRemoteEntries.Where(oldItem => items.All(newItem => { if (oldItem.Key != newItem.Key) { return true; } if (oldItem.Value is not FtpFile of || newItem.Value is not FtpFile nf) { return false; } return of.Size != nf.Size; }) ); foreach (var item in toRemove) { _rawRemoteEntries.Remove(item); } var toAdd = items.Where(newItem => _rawRemoteEntries.All(oldItem => { if (newItem.Key != oldItem.Key) { return true; } if (newItem.Value is not FtpFile nf || oldItem.Value is not FtpFile of) { return false; } return of.Size != nf.Size; }) ); _rawRemoteEntries.AddRange(toAdd); if (selected is null) { return; } RemoteSelectedItem.Value = RemoteItemsTree.FindNode(item => item.Key == selected) as BrowserNode; } private ValueTask RefreshLocalImpl(Unit arg, CancellationToken ct) { if (_backend is null) { return ValueTask.CompletedTask; } var newItems = _localFilesService.LoadBrowserItems( _localRootPath, _localRootPath, _backend, _loggerFactory, _config?.LocalDirectories ); var selected = LocalSelectedItem.Value?.Key; var toRemove = _localItems .Where(ls => newItems.All(n => n.Path != ls.Path || n.Size != ls.Size)) .ToArray(); foreach (var item in toRemove) { _localItems.Remove(item); } var existing = new HashSet<(string path, FileSize? size)>( _localItems.Select(i => (i.Path, i.Size)) ); var toAdd = newItems.Where(n => !existing.Contains((n.Path, n.Size))); var toDispose = newItems.Where(n => existing.Contains((n.Path, n.Size))); _localItems.AddRange(toAdd); toDispose.ForEach(i => i.Dispose()); if (selected is null) { return ValueTask.CompletedTask; } LocalSelectedItem.Value = LocalItemsTree.FindNode(item => item.Key == selected) as BrowserNode; return ValueTask.CompletedTask; } private ValueTask SetEditModeImpl(BrowserNode? node, CancellationToken ct) { if (node?.Base is not BrowserItemViewModel item) { return ValueTask.CompletedTask; } item.EditedName.Value = item.Name; item.EditMode = true; LocalSelectedItem.OnNext(node); return ValueTask.CompletedTask; } private static async ValueTask CalculateCrc32Impl(BrowserNode item, CancellationToken ct) { if (item.Base is not FileItemViewModel fileItem) { return; } await fileItem.ExecuteCommand(CalculateCrc32Command.Id, CommandArg.Empty, ct); } private async ValueTask CompareSelectedItemsImpl(CancellationToken ct) { if (_ftpService is null) { return; } if (LocalSelectedItem.Value?.Base is not FileItemViewModel localFileItem) { return; } if (RemoteSelectedItem.Value?.Base is not FileItemViewModel remoteFileItem) { return; } if (localFileItem.Crc32 == null) { await localFileItem.ExecuteCommand(CalculateCrc32Command.Id, CommandArg.Empty, ct); } var localCrc32 = localFileItem.Crc32 ?? 0; if (remoteFileItem.Crc32 == null) { await remoteFileItem.ExecuteCommand(CalculateCrc32Command.Id, CommandArg.Empty, ct); } var remoteCrc32 = remoteFileItem.Crc32 ?? 0; if (localCrc32 == remoteCrc32 && (localCrc32 != 0 || remoteCrc32 != 0)) { localFileItem.Crc32Status = Crc32Status.Correct; remoteFileItem.Crc32Status = Crc32Status.Correct; } else { localFileItem.Crc32Status = Crc32Status.Incorrect; remoteFileItem.Crc32Status = Crc32Status.Incorrect; } } public void FindFileOnLocal() { if (RemoteSelectedItem.Value?.Base is not FileItemViewModel remoteFile) { return; } var foundNode = LocalItemsTree.FindNode(n => n.Base is FileItemViewModel lf && string.Equals(lf.Name, remoteFile.Name, StringComparison.OrdinalIgnoreCase) ); if (foundNode is not BrowserNode node) { Logger.LogWarning("Local file \"{Header}\" not found", remoteFile.Name); return; } ExpandParents(node); LocalSelectedItem.OnNext(node); } #endregion #region Transfer public async ValueTask UploadItem( string source, string destination, FtpEntryType type, CancellationToken ct ) { if (_ftpService is null) { return; } using var transfer = _transfer.BeginScope(ct); try { await _ftpService.UploadAsync( source, destination, type, transfer.Token, transfer.Reporter ); } catch (OperationCanceledException) { Logger.LogWarning("Upload {Path} cancelled by user", source); } } public async ValueTask DownloadItem( string source, string destination, byte partSize, FtpEntryType type, CancellationToken ct ) { if (_ftpService is null) { return; } using var transfer = _transfer.BeginScope(ct); try { await _ftpService.DownloadAsync( source, destination, type, transfer.Token, partSize, transfer.Reporter ); } catch (OperationCanceledException) { Logger.LogWarning("Download {Path} cancelled by user", source); } } public async ValueTask BurstDownloadItem( string source, string destination, byte partSize, FtpEntryType type, CancellationToken ct ) { if (_ftpService is null) { return; } using var transfer = _transfer.BeginScope(ct); try { await _ftpService.BurstDownloadAsync( source, destination, type, transfer.Token, partSize, transfer.Reporter ); } catch (OperationCanceledException) { Logger.LogWarning("Burst-Download {Path} cancelled by user", source); } } #endregion public void Refresh() { RefreshLocalCommand.Execute(Unit.Default); RefreshRemoteCommand.Execute(Unit.Default); } #region Helpers private Task PerformLocalSearch( string? query, IProgress progress, CancellationToken cancel ) { PerformSearch(LocalItemsTree, LocalSelectedItem, query); return Task.CompletedTask; } private Task PerformRemoteSearch( string? query, IProgress progress, CancellationToken cancel ) { PerformSearch(RemoteItemsTree, RemoteSelectedItem, query); return Task.CompletedTask; } private static void PerformSearch( BrowserTree tree, BindableReactiveProperty target, string? pattern ) { if (string.IsNullOrWhiteSpace(pattern)) { return; } var found = tree.FindNode(n => n.Base is BrowserItemViewModel f && f.Name.Contains(pattern, StringComparison.OrdinalIgnoreCase) ); if (found is not BrowserNode node) { return; } ExpandParents(node); target.OnNext(node); } private static void ExpandParents(BrowserNode node) { foreach (var ancestor in node.GetAllMenuFromRoot()) { switch (ancestor.Base) { case BrowserItemViewModel bi: { if (bi.IsExpanded) { bi.IsExpanded = false; // reset field to trigger UI } bi.IsExpanded = true; break; } } } } private IBrowserItemViewModel RemoteEntryToBrowserItem(string key, IFtpEntry entry) { if (_backend == null) { throw new InvalidOperationException("Backend is not initialized"); } const char sep = MavlinkFtpHelper.DirectorySeparator; IBrowserItemViewModel vm; switch (entry.Type) { case FtpEntryType.Directory: { DirectoryItemViewModelConfig? config = null; var dirPath = FtpBrowserPath.Normalize(key, true, sep); _config?.RemoteDirectories.TryGetValue(dirPath, out config); vm = new DirectoryItemViewModel( PathHelper.EncodePathToId(dirPath), FtpBrowserPath.ParentDirOf(dirPath, sep), dirPath, entry.Name, FtpBrowserSourceType.Remote, _loggerFactory, config ); break; } case FtpEntryType.File: { var filePath = FtpBrowserPath.Normalize(key, false, sep); vm = new FileItemViewModel( PathHelper.EncodePathToId(filePath), FtpBrowserPath.ParentDirOf(filePath, sep), filePath, entry.Name, ((FtpFile)entry).Size, FtpBrowserSourceType.Remote, _loggerFactory ); break; } default: throw new ArgumentOutOfRangeException(nameof(entry)); } vm.AttachBackend(_backend); return vm; } #endregion #region Routable private ValueTask InternalCatchEvent(IRoutable src, AsyncRoutedEvent e) { switch (e) { case SaveLayoutEvent saveLayoutEvent: { if (!IsDeviceInitialized.Value) { if (saveLayoutEvent.IsFlushToFile) { saveLayoutEvent.LayoutService.FlushFromMemoryViewModelAndView(this); } break; } if (_config is null) { break; } this.HandleSaveLayout( saveLayoutEvent, _config, cfg => { cfg.LocalSearchText = LocalSearch.Text.ViewValue.Value ?? string.Empty; cfg.RemoteSearchText = RemoteSearch.Text.ViewValue.Value ?? string.Empty; cfg.LocalSelectedItemKey = LocalSelectedItem.Value?.Key; cfg.RemoteSelectedItemKey = RemoteSelectedItem.Value?.Key; cfg.LocalDirectories = _localItems .Where(item => item is { FtpEntryType: FtpEntryType.Directory, IsExpanded: true } ) .ToDictionary( item => item.Path, item => new DirectoryItemViewModelConfig { IsExpanded = item.IsExpanded, } ); cfg.RemoteDirectories = _remoteItems .Where(item => item is { FtpEntryType: FtpEntryType.Directory, IsExpanded: true } ) .ToDictionary( item => item.Path, item => new DirectoryItemViewModelConfig { IsExpanded = item.IsExpanded, } ); }, FlushingStrategy.FlushBothViewModelAndView ); break; } case LoadLayoutEvent loadLayoutEvent: { _config = this.HandleLoadLayout( loadLayoutEvent, cfg => { LocalSearch.Text.ModelValue.Value = cfg.LocalSearchText; RemoteSearch.Text.ModelValue.Value = cfg.RemoteSearchText; LocalSelectedItem.Value = LocalItemsTree.FindNode(n => n.Key == cfg.LocalSelectedItemKey) as BrowserNode; RemoteSelectedItem.Value = RemoteItemsTree.FindNode(n => n.Key == cfg.LocalSelectedItemKey) as BrowserNode; _localItems .Where(item => item is { FtpEntryType: FtpEntryType.Directory }) .ForEach(it => { cfg.LocalDirectories.TryGetValue(it.Path, out var config); it.IsExpanded = config?.IsExpanded ?? it.IsExpanded; }); _remoteItems .Where(item => item is { FtpEntryType: FtpEntryType.Directory }) .ForEach(it => { cfg.RemoteDirectories.TryGetValue(it.Path, out var config); it.IsExpanded = config?.IsExpanded ?? it.IsExpanded; }); } ); break; } } return ValueTask.CompletedTask; } public override IEnumerable GetChildren() { yield return LocalSearch; yield return RemoteSearch; foreach (var item in _localItems) { yield return item; } foreach (var item in _remoteItems) { yield return item; } } protected override void AfterLoadExtensions() { } #endregion #region Dispose protected override void Dispose(bool disposing) { if (disposing) { _localItems.RemoveAll(); _remoteItems.RemoveAll(); } base.Dispose(disposing); } #endregion protected override void AfterDeviceInitialized( IClientDevice device, CancellationToken onDisconnectedToken ) { Title = $"{RS.FileBrowserViewModel_Title}[{device.Id}]"; Icon = DeviceIconMixin.GetIcon(device.Id) ?? PageIcon; var client = device.GetMicroservice(); ArgumentNullException.ThrowIfNull(client); var clientEx = device.GetMicroservice() ?? new FtpClientEx(client); _ftpService = new FtpClientService(clientEx, _loggerFactory).DisposeItWith(Disposable); _ftpService.RegisterTo(onDisconnectedToken); _ftpService .RemoteChanged.ThrottleLast(TimeSpan.FromMilliseconds(200)) .SubscribeAwait(async (_, _) => await RefreshRemoteImpl(onDisconnectedToken)) .RegisterTo(onDisconnectedToken); _ftpService .RemoteChanging.Subscribe(isBusy => IsUiBlocked.OnNext(isBusy)) .RegisterTo(onDisconnectedToken); _backend = new FileBrowserBackend(_localFilesService, _ftpService); onDisconnectedToken.Register(() => { try { _transfer.TryCancel(); } catch { // ignored } IsUiBlocked.OnNext(false); IsProgressVisible.OnNext(false); IsTransferInProgress.OnNext(false); Progress.OnNext(0); _rawRemoteEntries.Clear(); _remoteItems.Clear(); RemoteSelectedItem.OnNext(null); _ftpService = null; }); Refresh(); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/FileBrowser/HomePageFileBrowserDeviceItemAction.cs ================================================ using Asv.Avalonia; using Asv.Avalonia.IO; using Asv.IO; using Asv.Mavlink; using Microsoft.Extensions.Logging; namespace Asv.Drones; public class HomePageFileBrowserDeviceItemAction(ILoggerFactory loggerFactory) : HomePageDeviceItemAction { protected override IActionViewModel? TryCreateAction( IClientDevice device, HomePageDeviceItem context ) { if (device.GetMicroservice() == null) { return null; } return new ActionViewModel("browser", loggerFactory) { Header = OpenFileBrowserCommand.StaticInfo.Name, Description = OpenFileBrowserCommand.StaticInfo.Description, Icon = OpenFileBrowserCommand.StaticInfo.Icon, Command = new BindableAsyncCommand(OpenFileBrowserCommand.Id, context), CommandParameter = new StringArg($"dev_id={device.Id}"), // TODO: replate with DevicePageViewModelMixin.CreateOpenPageArgs }; } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/FileBrowser/IO/FileSize.cs ================================================ using System; namespace Asv.Drones; public readonly struct FileSize(long size) : IEquatable { private const long OneKilobyte = 1024; private const long OneMegabyte = OneKilobyte * 1024; private const long OneGigabyte = OneMegabyte * 1024; private const long OneTerabyte = OneGigabyte * 1024; private long Size => size; public override string ToString() { string unit; double value; switch (size) { case < OneKilobyte: unit = RS.Unit_Byte_Abbreviation; value = size; break; case < OneMegabyte: unit = RS.Unit_Kilobyte_Abbreviation; value = size / (double)OneKilobyte; break; case < OneGigabyte: unit = RS.Unit_Megabyte_Abbreviation; value = size / (double)OneMegabyte; break; case < OneTerabyte: unit = RS.Unit_Gigabyte_Abbreviation; value = size / (double)OneGigabyte; break; default: unit = RS.Unit_Terabyte_Abbreviation; value = size / (double)OneTerabyte; break; } return $"{value:0.##} {unit}"; } public int CompareTo(FileSize other) { return Size.CompareTo(other.Size); } public bool Equals(FileSize other) { return Size.Equals(other.Size); } public override bool Equals(object? obj) { return obj is FileSize fileSize && Equals(fileSize); } public static bool operator ==(FileSize left, FileSize right) { return left.Equals(right); } public static bool operator !=(FileSize left, FileSize right) { return !(left == right); } public override int GetHashCode() { return Size.GetHashCode(); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/FileBrowser/IO/FtpBrowserNamingPolicy.cs ================================================ using System; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using Asv.Common; namespace Asv.Drones; public static partial class FtpBrowserNamingPolicy { // MAVLINK_MSG_ID_FILE_TRANSFER_PROTOCOL spec. says that only 248 symbols allowed, // that is an approximate value, not including a path length public const int MaxNameLength = 100; public const int MinNameLength = 1; public static readonly string BlankName = Guid.NewGuid().ToString(); // TODO: prohibit empty names private static readonly Regex AllowedChars = AllowedCharsRegex(); private const string AllowedCharsPattern = @"[A-Za-z0-9_.\-() ]"; private const string AllowedNamePattern = "^" + AllowedCharsPattern + "+$"; private static readonly Regex AllowedName = AllowedNameRegex(); [GeneratedRegex(AllowedCharsPattern, RegexOptions.Compiled)] private static partial Regex AllowedCharsRegex(); [GeneratedRegex(AllowedNamePattern, RegexOptions.Compiled)] private static partial Regex AllowedNameRegex(); public static string SanitizeForDisplay(string? value) { if (string.IsNullOrEmpty(value)) { return string.Empty; } var sb = new StringBuilder(value.Length); foreach (var ch in value) { sb.Append(AllowedChars.IsMatch(ch.ToString()) ? ch : '*'); } return sb.ToString(); } public static ValidationResult Validate(string name) { if (string.IsNullOrWhiteSpace(name)) { return ValidationResult.FailAsNullOrWhiteSpace; } if (name.Any(Path.GetInvalidFileNameChars().Contains)) { return ValidationResult.FailAsInvalidCharacters; } if (!AllowedName.IsMatch(name)) { return ValidationResult.FailAsInvalidCharacters; } if (name.Length > MaxNameLength) { return ValidationResult.FailAsOutOfRange( MinNameLength.ToString(), MaxNameLength.ToString() ); } return ValidationResult.Success; } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/FileBrowser/IO/FtpBrowserPath.cs ================================================ namespace Asv.Drones; /// /// Utility methods for working with FTP browser paths. /// public static class FtpBrowserPath { /// /// Normalizes a path ensuring it ends with the correct separator if it is a directory, /// or trims trailing separators if it is a file. /// /// The original path string. /// True if the path is a directory; false if it is a file. /// Directory separator character. /// A normalized path string. public static string Normalize(string path, bool isDirectory, char sep) => isDirectory ? EnsureDir(path, sep) : EnsureFile(path, sep); /// /// Combines a parent directory with a child directory name and ensures the result ends with a separator. /// /// The parent directory path. /// The child directory name. /// Directory separator character. /// A full directory path ending with a separator. public static string CombineDir(string parentDir, string name, char sep) => EnsureDir(parentDir, sep) + name + sep; /// /// Combines a parent directory with a file name. /// /// The parent directory path. /// The file name. /// Directory separator character. /// A full file path. public static string CombineFile(string parentDir, string name, char sep) => EnsureDir(parentDir, sep) + name; /// /// Gets the parent directory of the given path. /// /// The full path string. /// Directory separator character. /// /// Parent directory path (ending with a separator), or empty string if the path is root or invalid. /// public static string ParentDirOf(string path, char sep) { if (string.IsNullOrWhiteSpace(path) || (path.Length == 1 && path[0] == sep)) { return string.Empty; } var isDir = path[^1] == sep; var p = isDir ? path.TrimEnd(sep) : path; var idx = p.LastIndexOf(sep); return idx switch { < 0 => string.Empty, 0 => sep.ToString(), _ => p[..(idx + 1)], }; } /// /// Extracts the name (file or directory) from the given path. /// /// The full path string. /// Directory separator character. /// The file or directory name, or empty string if invalid. public static string NameOf(string? path, char sep) { if (string.IsNullOrEmpty(path) || (path.Length == 1 && path[0] == sep)) { return string.Empty; } var end = path.Length; while (end > 1 && path[end - 1] == sep) { end--; } var idx = path.LastIndexOf(sep, end - 1); return idx < 0 ? path[..end] : path.Substring(idx + 1, end - (idx + 1)); } /// /// Ensures that the given path represents a directory (ends with a separator). /// private static string EnsureDir(string path, char sep) { if (string.IsNullOrWhiteSpace(path) || (path.Length == 1 && path[0] == sep)) { return sep.ToString(); } return path[^1] == sep ? path : path + sep; } /// /// Ensures that the given path represents a file (removes trailing separators). /// private static string EnsureFile(string path, char sep) { if (string.IsNullOrWhiteSpace(path) || (path.Length == 1 && path[0] == sep)) { return string.Empty; } var end = path.Length; while (end > 1 && path[end - 1] == sep) { end--; } return path[..end]; } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/FileBrowser/IO/Types/Crc32Status.cs ================================================ namespace Asv.Drones; public enum Crc32Status { /// /// Basic status. /// Default, /// /// Indicates that the CRC32 check was successful. /// Correct, /// /// Indicates that the CRC32 check was unsuccessful. /// Incorrect, } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/FileBrowser/IO/Types/FtpBrowserSourceType.cs ================================================ namespace Asv.Drones; public enum FtpBrowserSourceType { Local, Remote, } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/FileBrowser/Tree/BrowserNode.cs ================================================ using System; using System.Collections.Generic; using Asv.Avalonia; using ObservableCollections; namespace Asv.Drones; public class BrowserNode( IBrowserItemViewModel baseItem, IReadOnlyObservableList source, Func keySelector, Func parentSelector, IComparer comparer, CreateNodeDelegate factory, ObservableTreeNode? parentNode = null ) : ObservableTreeNode( baseItem, source, keySelector, parentSelector, comparer, factory, parentNode ); ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/FileBrowser/Tree/BrowserTree.cs ================================================ using Asv.Avalonia; using ObservableCollections; namespace Asv.Drones; public class BrowserTree(IReadOnlyObservableList flatList, string rootKey) : ObservableTree( flatList, rootKey, static x => x.Path, static x => { var p = x.ParentPath ?? string.Empty; return p == x.Path ? string.Empty : p; }, BrowserItemComparer.Instance, NodeFactory ) { private static readonly CreateNodeDelegate NodeFactory = static ( item, list, key, parent, comparer, factory, parentNode ) => new BrowserNode(item, list, key, parent, comparer, factory, parentNode); } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/FileBrowser/Tree/Items/BrowserItemComparer.cs ================================================ using System.Collections.Generic; namespace Asv.Drones; public sealed class BrowserItemComparer : IComparer { public static readonly BrowserItemComparer Instance = new(); private BrowserItemComparer() { } public int Compare(IBrowserItemViewModel? x, IBrowserItemViewModel? y) { if (x is DirectoryItemViewModel && y is not DirectoryItemViewModel) { return -1; } if (y is DirectoryItemViewModel && x is not DirectoryItemViewModel) { return 1; } return x switch { DirectoryItemViewModel dirX when y is DirectoryItemViewModel dirY => DirectoryItemComparer.Instance.Compare(dirX, dirY), FileItemViewModel fileX when y is FileItemViewModel fileY => FileItemComparer.Instance.Compare(fileX, fileY), _ => 0, }; } } public sealed class DirectoryItemComparer : IComparer { public static readonly DirectoryItemComparer Instance = new(); private DirectoryItemComparer() { } public int Compare(DirectoryItemViewModel? x, DirectoryItemViewModel? y) { switch (x) { case null when y == null: return 0; case null: return -1; } if (y == null) { return 1; } var idComparison = x.Id.CompareTo(y.Id); return idComparison != 0 ? idComparison : string.CompareOrdinal(x.ParentPath, y.ParentPath); } } public sealed class FileItemComparer : IComparer { public static readonly FileItemComparer Instance = new(); private FileItemComparer() { } public int Compare(FileItemViewModel? x, FileItemViewModel? y) { switch (x) { case null when y == null: return 0; case null: return -1; } if (y == null) { return 1; } var idComparison = x.Id.CompareTo(y.Id); if (idComparison != 0) { return idComparison; } var parentPathComparison = string.CompareOrdinal(x.ParentPath, y.ParentPath); if (parentPathComparison != 0) { return parentPathComparison; } if (x.Size.HasValue && y.Size.HasValue) { return x.Size.Value.CompareTo(y.Size.Value); } if (x.Size.HasValue) { return 1; } if (y.Size.HasValue) { return -1; } return 0; } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/FileBrowser/Tree/Items/BrowserItemViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Common; using Asv.Mavlink; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones; public abstract class BrowserItemViewModel : RoutableViewModel, IBrowserItemViewModel { private readonly char _separator; private IBrowserItemsOperations? _ops; protected IBrowserItemsOperations ItemsOperations => _ops ?? throw new InvalidOperationException( $"{nameof(IBrowserItemsOperations)} are not attached. Call {nameof(AttachBackend)} first." ); protected BrowserItemViewModel( NavigationId id, string? parentPath, string path, FtpBrowserSourceType type, ILoggerFactory loggerFactory ) : base(id, loggerFactory) { ParentPath = parentPath; Path = path; Type = type; _separator = Type is FtpBrowserSourceType.Local ? System.IO.Path.DirectorySeparatorChar : MavlinkFtpHelper.DirectorySeparator; EditedName = new BindableReactiveProperty().DisposeItWith(Disposable); EditedName .EnableValidation(x => { var result = FtpBrowserNamingPolicy.Validate(x); return !result.IsSuccess ? result.ValidationException : null; }) .ForceValidate(); CommitRename = CanRename .ToReactiveCommand( async (_, ct) => await CommitRenameImpl(ct), awaitOperation: AwaitOperation.Drop ) .DisposeItWith(Disposable); } #region Properties public string Name { get => FtpBrowserNamingPolicy.SanitizeForDisplay(field); set => SetField(ref field, value); } = string.Empty; public string Path { get; set => SetField(ref field, value); } public string? ParentPath { get; set => SetField(ref field, value); } public FileSize? Size { get; set => SetField(ref field, value); } public bool HasChildren { get; set => SetField(ref field, value); } public bool IsExpanded { get; set => SetField(ref field, value); } public bool IsSelected { get; set => SetField(ref field, value); } public FtpBrowserSourceType Type { get; set => SetField(ref field, value); } public bool EditMode { get; set => SetField(ref field, value); } public string? Crc32Hex { get; protected set => SetField(ref field, value); } public Crc32Status Crc32Status { get; set => SetField(ref field, value); } = Crc32Status.Default; public FtpEntryType FtpEntryType { get; protected init => SetField(ref field, value); } public BindableReactiveProperty EditedName { get; set; } #endregion public Observable CanRename => EditedName.Select(_ => !EditedName.HasErrors).DistinctUntilChanged().Share(); #region Commands public ReactiveCommand CommitRename { get; } #endregion public abstract ValueTask RenameAsync( string oldValue, string newValue, CancellationToken ct ); public abstract ValueTask RemoveAsync(CancellationToken ct); public abstract ValueTask CalculateCrc32Async(CancellationToken ct); public abstract ValueTask CreateDirectoryAsync(CancellationToken ct); private async ValueTask CommitRenameImpl(CancellationToken ct) { var oldName = Name; var oldPath = Path; var newName = string.IsNullOrWhiteSpace(EditedName.Value) ? FtpBrowserNamingPolicy.BlankName : EditedName.Value; var parentDir = FtpBrowserPath.ParentDirOf(oldPath, _separator); var isDirectory = FtpEntryType == FtpEntryType.Directory; var newPath = isDirectory ? FtpBrowserPath.CombineDir(parentDir, newName, _separator) : FtpBrowserPath.CombineFile(parentDir, newName, _separator); EditMode = false; if ( string.IsNullOrEmpty(newPath) || string.Equals(newName, oldName, StringComparison.Ordinal) ) { EditedName.Value = oldName; return; } await this.ExecuteCommand( CommitRenameCommand.Id, CommandArg.CreateDictionary( new Dictionary { { CommitRenameCommand.NewValue, CommandArg.CreateString(newPath) }, { CommitRenameCommand.OldValue, CommandArg.CreateString(oldPath) }, } ), ct ); } /// /// Attach backend context once right after VM creation. /// /// Backend context from VM. public void AttachBackend(FileBrowserBackend backend) { _ops = backend.ResolveOps(Type); } public override IEnumerable GetChildren() { return []; } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/FileBrowser/Tree/Items/DirectoryItemViewModel.cs ================================================ using System; using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Mavlink; using Microsoft.Extensions.Logging; namespace Asv.Drones; public class DirectoryItemViewModelConfig { public bool IsExpanded { get; set; } } public class DirectoryItemViewModel : BrowserItemViewModel { public DirectoryItemViewModel( NavigationId id, string? parentPath, string path, string name, FtpBrowserSourceType type, ILoggerFactory loggerFactory, DirectoryItemViewModelConfig? layoutConfig = null ) : base(id, parentPath, path, type, loggerFactory) { HasChildren = true; Name = name; FtpEntryType = FtpEntryType.Directory; IsExpanded = layoutConfig?.IsExpanded ?? IsExpanded; } public override async ValueTask RenameAsync( string oldValue, string newValue, CancellationToken ct ) { ArgumentException.ThrowIfNullOrEmpty(oldValue); ArgumentException.ThrowIfNullOrEmpty(newValue); var sep = ItemsOperations.Separator; var oldPath = FtpBrowserPath.Normalize(oldValue, true, sep); var newPath = FtpBrowserPath.Normalize(newValue, true, sep); var result = await ItemsOperations.RenameDirectoryAsync(oldPath, newPath, Logger, ct); EditMode = false; EditedName.Value = FtpBrowserPath.NameOf(result, sep); IsSelected = true; return result; } public override async ValueTask RemoveAsync(CancellationToken ct) { await ItemsOperations.RemoveDirectoryAsync(Path, Logger, ct); } public override ValueTask CalculateCrc32Async(CancellationToken ct) { Logger.LogError("Cannot calculate CRC32 for directory"); return new ValueTask(0); } public override async ValueTask CreateDirectoryAsync(CancellationToken ct) { await ItemsOperations.CreateDirectoryAsync(Path, Logger, ct); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/FileBrowser/Tree/Items/FileItemViewModel.cs ================================================ using System; using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Mavlink; using Microsoft.Extensions.Logging; namespace Asv.Drones; public class FileItemViewModel : BrowserItemViewModel { public FileItemViewModel( NavigationId id, string parentPath, string path, string name, long size, FtpBrowserSourceType type, ILoggerFactory loggerFactory ) : base(id, parentPath, path, type, loggerFactory) { HasChildren = false; Name = name; Size = new FileSize(size); FtpEntryType = FtpEntryType.File; } public uint? Crc32 { get; set { SetField(ref field, value); if (value is null) { Crc32Hex = null; return; } Crc32Hex = Crc32ToHex((uint)value); } } private static string Crc32ToHex(uint crc32) => crc32.ToString("X8"); public override async ValueTask RenameAsync( string oldValue, string newValue, CancellationToken ct ) { ArgumentException.ThrowIfNullOrEmpty(oldValue); ArgumentException.ThrowIfNullOrEmpty(newValue); var sep = ItemsOperations.Separator; var oldPath = FtpBrowserPath.Normalize(oldValue, false, sep); var newPath = FtpBrowserPath.Normalize(newValue, false, sep); var result = await ItemsOperations.RenameFileAsync(oldPath, newPath, Logger, ct); EditMode = false; EditedName.Value = FtpBrowserPath.NameOf(result, sep); IsSelected = true; return newPath; } public override async ValueTask RemoveAsync(CancellationToken ct) { await ItemsOperations.RemoveFileAsync(Path, Logger, ct); } public override async ValueTask CalculateCrc32Async(CancellationToken ct) { var crc32 = await ItemsOperations.CalculateCrc32Async(Path, Logger, ct); Crc32 = crc32; Crc32Status = Crc32Status.Default; return crc32; } public override async ValueTask CreateDirectoryAsync(CancellationToken ct) { var path = FtpBrowserPath.ParentDirOf(Path, ItemsOperations.Separator); await ItemsOperations.CreateDirectoryAsync(path, Logger, ct); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/FileBrowser/Tree/Items/IBrowserItemViewModel.cs ================================================ using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Drones.Api; using Asv.Mavlink; using R3; namespace Asv.Drones; public interface IBrowserItemViewModel : ISupportRename, ISupportRemove { string Name { get; set; } string Path { get; set; } string? ParentPath { get; set; } FileSize? Size { get; } bool HasChildren { get; } bool IsExpanded { get; set; } bool IsSelected { get; set; } bool EditMode { get; set; } FtpBrowserSourceType Type { get; } string? Crc32Hex { get; } Crc32Status Crc32Status { get; } FtpEntryType FtpEntryType { get; } ReactiveCommand CommitRename { get; } BindableReactiveProperty EditedName { get; set; } void AttachBackend(FileBrowserBackend backend); ValueTask CreateDirectoryAsync(CancellationToken ct); ValueTask CalculateCrc32Async(CancellationToken ct); } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/MavParams/Dialog/TryCloseWithApprovalDialogView.axaml ================================================ ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/MavParams/Dialog/TryCloseWithApprovalDialogView.axaml.cs ================================================ using Asv.Avalonia; using Avalonia.Controls; namespace Asv.Drones; public partial class TryCloseWithApprovalDialogView : UserControl { public TryCloseWithApprovalDialogView() { InitializeComponent(); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/MavParams/Dialog/TryCloseWithApprovalDialogViewModel.cs ================================================ using System.Collections.Generic; using Asv.Avalonia; using Microsoft.Extensions.Logging; namespace Asv.Drones; public class TryCloseWithApprovalDialogViewModel : DialogViewModelBase { public const string DialogId = $"{BaseId}.close-with-approval"; public TryCloseWithApprovalDialogViewModel() : this(DesignTime.LoggerFactory) { DesignTime.ThrowIfNotDesignMode(); } public TryCloseWithApprovalDialogViewModel(ILoggerFactory loggerFactory) : base(DialogId, loggerFactory) { Message = RS.ParamPageViewModel_DataLossDialog_Content; } public string Message { get; } public override IEnumerable GetChildren() { return []; } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/MavParams/HomePageParamsDeviceItemAction.cs ================================================ using Asv.Avalonia; using Asv.Avalonia.IO; using Asv.IO; using Asv.Mavlink; using Material.Icons; using Microsoft.Extensions.Logging; namespace Asv.Drones; public class HomePageParamsDeviceItemAction(ILoggerFactory loggerFactory) : HomePageDeviceItemAction { protected override IActionViewModel? TryCreateAction( IClientDevice device, HomePageDeviceItem context ) { if (device.GetMicroservice() == null) { return null; } return new ActionViewModel("params", loggerFactory) { Icon = MaterialIconKind.CogTransferOutline, Header = RS.HomePageParamsDeviceItemAction_ActionViewModel_Header, Description = RS.HomePageParamsDeviceItemAction_ActionViewModel_Description, Command = new BindableAsyncCommand(OpenMavParamsCommand.Id, context), CommandParameter = new StringArg($"dev_id={device.Id}"), // TODO: replace with DevicePageViewModelMixin.CreateOpenPageArgs }; } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/MavParams/MavParamsPageView.axaml ================================================  30 ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/MavParams/MavParamsPageView.axaml.cs ================================================ using System.Collections.Generic; using System.Linq; using Asv.Avalonia; using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; using R3; namespace Asv.Drones; public class MavParamsPageViewConfig : IGridColumnLayoutConfig { public IDictionary Columns { get; set; } = new Dictionary(); } public partial class MavParamsPageView : UserControl { private readonly ILayoutService _layoutService; private int _realAllParamsColumnOrder; private int _realGridSplitterColumnOrder; private int _realViewedParamsColumnOrder; private MavParamsPageViewConfig? _config; public MavParamsPageView() : this(NullLayoutService.Instance) { DesignTime.ThrowIfNotDesignMode(); } public MavParamsPageView(ILayoutService layoutService) { _layoutService = layoutService; InitializeComponent(); } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { LoadLayout(); base.OnAttachedToVisualTree(e); } private void GridSplitter_Dragged(object? sender, VectorEventArgs e) { if (Design.IsDesignMode) { return; } if (sender is not GridSplitter) { return; } SaveLayout(); } private void LoadLayout() { if (Design.IsDesignMode) { return; } _config = _layoutService.Get(this); _realAllParamsColumnOrder = Grid.GetColumn(AllParamsColumn); _realGridSplitterColumnOrder = Grid.GetColumn(GridSplitterColumn); _realViewedParamsColumnOrder = Grid.GetColumn(ViewedParamsColumn); if (_config.Columns.Count == 0) { return; } if ( MainGrid.ColumnDefinitions.Count != _config.Columns.Keys.Count || _config.Columns.All(kvp => kvp.Value.Width.Value == 0) || !_config.Columns.TryGetValue( AllParamsColumn.Name ?? string.Empty, out var allParamsColumn ) || !_config.Columns.TryGetValue( GridSplitterColumn.Name ?? string.Empty, out var gridSplitterColumn ) || !_config.Columns.TryGetValue( ViewedParamsColumn.Name ?? string.Empty, out var viewedParamsColumn ) || _realAllParamsColumnOrder != allParamsColumn.Order || _realGridSplitterColumnOrder != gridSplitterColumn.Order || _realViewedParamsColumnOrder != viewedParamsColumn.Order ) { _config = new MavParamsPageViewConfig(); return; } MainGrid.ColumnDefinitions[_realAllParamsColumnOrder].Width = new GridLength( allParamsColumn.Width.Value, GridUnitType.Star ); MainGrid.ColumnDefinitions[_realGridSplitterColumnOrder].Width = new GridLength( gridSplitterColumn.Width.Value, GridUnitType.Star ); MainGrid.ColumnDefinitions[_realViewedParamsColumnOrder].Width = new GridLength( viewedParamsColumn.Width.Value, GridUnitType.Star ); } private void SaveLayout() { if (Design.IsDesignMode) { return; } if (_config is null) { return; } if (DataContext is null) { return; } if ( AllParamsColumn.Name is null || GridSplitterColumn.Name is null || ViewedParamsColumn.Name is null ) { return; } _config.Columns = new Dictionary { [AllParamsColumn.Name] = new() { Order = _realAllParamsColumnOrder, Width = new GridLengthConfig { Value = MainGrid.ColumnDefinitions[_realAllParamsColumnOrder].ActualWidth, GridUnitType = GridUnitType.Star, }, }, [GridSplitterColumn.Name] = new() { Order = _realGridSplitterColumnOrder, Width = new GridLengthConfig { Value = MainGrid.ColumnDefinitions[_realGridSplitterColumnOrder].ActualWidth, GridUnitType = GridUnitType.Star, }, }, [ViewedParamsColumn.Name] = new() { Order = _realViewedParamsColumnOrder, Width = new GridLengthConfig { Value = MainGrid.ColumnDefinitions[_realViewedParamsColumnOrder].ActualWidth, GridUnitType = GridUnitType.Star, }, }, }; _layoutService.SetInMemory(this, _config); } private void ItemDockPanel_DoubleTapped(object? sender, RoutedEventArgs e) { if (Design.IsDesignMode) { return; } if (DataContext is not MavParamsPageViewModel viewModel) { return; } if (sender is not DockPanel { DataContext: { } item }) { return; } if (ReferenceEquals(viewModel.SelectedItem.Value, item)) { viewModel.SelectedItem.Value?.PinItem.Execute(Unit.Default); } } private void Button_DoubleTapped(object? sender, RoutedEventArgs e) { e.Handled = true; } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/MavParams/MavParamsPageViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows.Input; using Asv.Avalonia; using Asv.Avalonia.IO; using Asv.Cfg; using Asv.Common; using Asv.Drones.Api; using Asv.IO; using Asv.Mavlink; using Asv.Modeling; using Avalonia.Controls; using Material.Icons; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using ObservableCollections; using R3; namespace Asv.Drones; public sealed class MavParamsPageViewModelConfig { public IDictionary Params { get; set; } = new Dictionary(); public string SearchText { get; set; } = string.Empty; public bool IsStarredOnly { get; set; } } public class MavParamsPageViewModel : DevicePageViewModel, IMavParamsPageViewModel { public const string PageId = "mav-params"; public const MaterialIconKind PageIcon = MaterialIconKind.CogTransferOutline; private readonly ILayoutService _layoutService; private readonly ILoggerFactory _loggerFactory; private readonly INavigationService _nav; private readonly ObservableList _viewedParamsList; // TODO: Separate views for this collection and all params private readonly ReactiveProperty _isStarredOnly; private readonly SynchronizedViewFilter< KeyValuePair, ParamItemViewModel > _fullFilter; private readonly Lock _cancelLock = new(); private DeviceId _deviceId; private IParamsClientEx? _paramsClient; private CancellationTokenSource? _cancellationTokenSource; private ISynchronizedView, ParamItemViewModel> _view; private MavParamsPageViewModelConfig? _config; public MavParamsPageViewModel() : this( NullDeviceManager.Instance, NullCommandService.Instance, NullLoggerFactory.Instance, NullLayoutService.Instance, NullNavigationService.Instance, DesignTime.DialogService, DesignTime.ExtensionService ) { DesignTime.ThrowIfNotDesignMode(); var list = new ObservableList { new() { DisplayName = "Param 1" }, new() { DisplayName = "Param 2" }, new() { DisplayName = "Param 3" }, }; var viewedList = new ObservableList { list[0] }; AllParams = list.ToNotifyCollectionChangedSlim().DisposeItWith(Disposable); ViewedParams = viewedList.ToNotifyCollectionChangedSlim().DisposeItWith(Disposable); } public MavParamsPageViewModel( IDeviceManager devices, ICommandService cmd, ILoggerFactory loggerFactory, ILayoutService layoutService, INavigationService nav, IDialogService dialogService, IExtensionService ext ) : base(PageId, devices, cmd, layoutService, loggerFactory, dialogService, ext) { ArgumentNullException.ThrowIfNull(devices); ArgumentNullException.ThrowIfNull(cmd); ArgumentNullException.ThrowIfNull(layoutService); ArgumentNullException.ThrowIfNull(loggerFactory); ArgumentNullException.ThrowIfNull(nav); Title = RS.MavParamsPageViewModel_Title; _layoutService = layoutService; _loggerFactory = loggerFactory; _nav = nav; _isStarredOnly = new ReactiveProperty().DisposeItWith(Disposable); _viewedParamsList = []; ViewedParams = _viewedParamsList .ToNotifyCollectionChangedSlim(SynchronizationContextCollectionEventDispatcher.Current) .DisposeItWith(Disposable); Search = new SearchBoxViewModel( nameof(Search), loggerFactory, UpdateImpl, TimeSpan.FromMilliseconds(500) ) .SetRoutableParent(this) .DisposeItWith(Disposable); IsStarredOnly = new HistoricalBoolProperty( nameof(IsStarredOnly), _isStarredOnly, loggerFactory ) .SetRoutableParent(this) .DisposeItWith(Disposable); IsRefreshing = new BindableReactiveProperty().DisposeItWith(Disposable); SelectedItem = new BindableReactiveProperty().DisposeItWith( Disposable ); Progress = new BindableReactiveProperty().DisposeItWith(Disposable); SelectedItem .Subscribe(value => { var itemsToDelete = _viewedParamsList .Where(_ => !_.IsPinned.ViewValue.Value) .ToArray(); foreach (var item in itemsToDelete) { _viewedParamsList.Remove(item); } if (value is null) { return; } if (!_viewedParamsList.Contains(value)) { _viewedParamsList.Add(value); } }) .DisposeItWith(Disposable); Disposable.AddAction(StopUpdateParamsImpl); UpdateParams = new BindableAsyncCommand(UpdateParamsCommand.Id, this); StopUpdateParams = new BindableAsyncCommand(StopUpdateParamsCommand.Id, this); RemoveAllPins = new ReactiveCommand( async (_, ct) => { if (!AllParams?.Any(item => item.IsPinned.ViewValue.Value) ?? false) { return; } await this.ExecuteCommand( RemoveAllPinsCommand.Id, CommandArg.CreateDictionary(), ct ); } ).DisposeItWith(Disposable); IsStarredOnly .ViewValue.ThrottleLast(TimeSpan.FromMilliseconds(500)) .Subscribe(_ => UpdateFilter()) .DisposeItWith(Disposable); Progress .Where(p => p.ApproximatelyEquals(1.0)) .Subscribe(_ => IsRefreshing.Value = false) .DisposeItWith(Disposable); IsRefreshing .Where(isRefreshing => isRefreshing) .Subscribe(_ => Progress.Value = 0) .DisposeItWith(Disposable); _fullFilter = new SynchronizedViewFilter< KeyValuePair, ParamItemViewModel >( (_, model) => model.Filter( Search.Text.ViewValue.Value ?? string.Empty, IsStarredOnly.ViewValue.Value ) ); Events.Subscribe(InternalCatchEvent).DisposeItWith(Disposable); } private Task UpdateImpl(string? query, IProgress progress, CancellationToken cancel) { UpdateFilter(); return Task.CompletedTask; } private void UpdateFilter() { if ( string.IsNullOrWhiteSpace(Search.Text.ViewValue.Value) && !IsStarredOnly.ViewValue.Value ) { _view?.ResetFilter(); return; } _view?.AttachFilter(_fullFilter); } private void InternalInit(CancellationToken cancel) { if (_paramsClient is null) { throw new Exception($"Service of type {nameof(IParamsClientEx)} was not found"); } Total = _paramsClient.RemoteCount.ToReadOnlyBindableReactiveProperty(); Total.RegisterTo(cancel); _view = _paramsClient.Items.CreateView(kvp => { ParamItemViewModelConfig? config = null; _config?.Params.TryGetValue(kvp.Key, out config); return new ParamItemViewModel(kvp.Key, kvp.Value, _loggerFactory, config); }); _view.RegisterTo(cancel); _view.DisposeMany().RegisterTo(cancel); _view.SetRoutableParent(this).RegisterTo(cancel); foreach (var item in _view.Where(item => item.IsPinned.ViewValue.Value)) { if (_viewedParamsList.Contains(item)) { continue; } _viewedParamsList.Add(item); } _view .ObserveAdd(cancellationToken: cancel) .SubscribeAwait( async (e, ct) => { await e.Value.View.RequestLoadLayout(_layoutService, ct); if (!e.Value.View.IsPinned.ViewValue.Value) { return; } if (_viewedParamsList.Contains(e.Value.View)) { return; } _viewedParamsList.Add(e.Value.View); } ) .RegisterTo(cancel); AllParams = _view.ToNotifyCollectionChanged( SynchronizationContextCollectionEventDispatcher.Current ); AllParams.RegisterTo(cancel); Search.Refresh(); } internal void StopUpdateParamsImpl() { using (_cancelLock.EnterScope()) { if (_cancellationTokenSource is not null) { _cancellationTokenSource?.Cancel(); _cancellationTokenSource?.Dispose(); _cancellationTokenSource = null; } } if (!IsRefreshing.IsDisposed) { IsRefreshing.Value = false; } } internal async Task UpdateParamsImpl(CancellationToken cancel = default) { if (IsRefreshing.Value) { return; } if (_paramsClient is null) { return; } cancel.ThrowIfCancellationRequested(); if (_cancellationTokenSource is not null) { StopUpdateParamsImpl(); } SelectedItem.Value = null; IsRefreshing.Value = true; using (_cancelLock.EnterScope()) { _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancel); } try { await _paramsClient.ReadAll( new Progress(i => Progress.Value = i), cancel: _cancellationTokenSource.Token ); } catch (OperationCanceledException) { Logger.LogInformation("User canceled updating params"); } catch (Exception e) { Logger.LogError(e, "Error to read all param items"); } } protected override void AfterDeviceInitialized(IClientDevice device, CancellationToken cancel) { Title = $"{RS.MavParamsPageViewModel_Title}[{device.Id}]"; _paramsClient = device.GetMicroservice(); DeviceName = device .Name.Select(x => x ?? RS.MavParamsPageViewModel_DeviceName_Unknown) .ToReadOnlyBindableReactiveProperty(); DeviceName.RegisterTo(cancel); _deviceId = device.Id; Icon = DeviceIconMixin.GetIcon(_deviceId) ?? PageIcon; InternalInit(cancel); } public HistoricalBoolProperty IsStarredOnly { get; } public BindableReactiveProperty IsRefreshing { get; } public BindableReactiveProperty Progress { get; } public ICommand UpdateParams { get; } public ICommand StopUpdateParams { get; } public ReactiveCommand RemoveAllPins { get; } public SearchBoxViewModel Search { get; } public INotifyCollectionChangedSynchronizedViewList? AllParams { get; private set; } public IReadOnlyBindableReactiveProperty Total { get; private set; } public INotifyCollectionChangedSynchronizedViewList ViewedParams { get; } public IReadOnlyBindableReactiveProperty DeviceName { get; private set; } public BindableReactiveProperty SelectedItem { get; } public override IEnumerable GetChildren() { yield return Search; yield return IsStarredOnly; if (AllParams is not null) { foreach (var paramItemViewModel in AllParams) { yield return paramItemViewModel; } } } protected override void AfterLoadExtensions() { } private async ValueTask InternalCatchEvent(IRoutable src, AsyncRoutedEvent e) { switch (e) { case ParamItemChangedEvent { Sender: ParamItemViewModel param } paramChanged: { if (paramChanged.TrackedObject is HistoricalBoolProperty caller) { if (caller.Id == param.IsPinned.Id) { UpdateViewedItems(param); } } e.IsHandled = true; break; } case PageCloseAttemptEvent { Sender: MavParamsPageViewModel } closeEvent: { var isCloseReady = await TryCloseWithApproval(); if (!isCloseReady) { closeEvent.AddRestriction(new Restriction(this)); } break; } case SaveLayoutEvent saveLayoutEvent: { if (!IsDeviceInitialized.Value) { if (saveLayoutEvent.IsFlushToFile) { saveLayoutEvent.LayoutService.FlushFromMemoryViewModelAndView(this); } break; } if (_config is null) { break; } this.HandleSaveLayout( saveLayoutEvent, _config, cfg => { cfg.SearchText = Search.Text.ViewValue.Value ?? string.Empty; cfg.IsStarredOnly = IsStarredOnly.ViewValue.Value; cfg.Params = _view .Where(p => p.IsLayoutChanged.CurrentValue) .ToDictionary( p => p.Name, p => new ParamItemViewModelConfig { Name = p.Name, IsPinned = p.IsPinned.ViewValue.Value, IsStarred = p.IsStarred.ViewValue.Value, } ); }, FlushingStrategy.FlushBothViewModelAndView ); break; } case LoadLayoutEvent loadLayoutEvent: { _config = this.HandleLoadLayout( loadLayoutEvent, cfg => { Search.Text.ModelValue.Value = cfg.SearchText; IsStarredOnly.ModelValue.Value = cfg.IsStarredOnly; } ); break; } } } private void UpdateViewedItems(ParamItemViewModel param) { var itemsToDelete = _viewedParamsList .Where(_ => !_.IsPinned.ViewValue.Value && _.Id != SelectedItem.Value?.Id) .ToArray(); foreach (var item in itemsToDelete) { _viewedParamsList.Remove(item); return; } if (!_viewedParamsList.Contains(param)) { _viewedParamsList.Add(param); } } private async Task TryCloseWithApproval(CancellationToken cancel = default) { cancel.ThrowIfCancellationRequested(); var notSyncedParams = _viewedParamsList.Where(param => !param.IsSynced.Value).ToArray(); if (notSyncedParams.Length == 0) { return true; } using var vm = new TryCloseWithApprovalDialogViewModel(_loggerFactory); var dialog = new ContentDialog(vm, _nav) { Title = RS.ParamPageViewModel_DataLossDialog_Title, IsSecondaryButtonEnabled = true, PrimaryButtonText = RS.ParamPageViewModel_DataLossDialog_PrimaryButtonText, SecondaryButtonText = RS.ParamPageViewModel_DataLossDialog_SecondaryButtonText, CloseButtonText = RS.ParamPageViewModel_DataLossDialog_CloseButtonText, }; var result = await dialog.ShowAsync(); if (result == ContentDialogResult.None) { return false; } if (result == ContentDialogResult.Primary) { foreach (var param in notSyncedParams) { param.Write.Execute(Unit.Default); } } return true; } } file class ParamsKvpComparer : IComparer> { public static ParamsKvpComparer Instance { get; } = new(); public int Compare(KeyValuePair x, KeyValuePair y) { return string.CompareOrdinal(x.Value.Name, y.Value.Name); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/MavParams/ParamItem/ParamItemView.axaml ================================================ ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/Setup/Subpage/FrameType/DroneFrameItem/DroneFrameItemView.axaml.cs ================================================ using Asv.Avalonia; using Avalonia.Controls; namespace Asv.Drones; public partial class DroneFrameItemView : UserControl { public DroneFrameItemView() { InitializeComponent(); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/Setup/Subpage/FrameType/DroneFrameItem/DroneFrameItemViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Common; using Asv.Mavlink; using Asv.Modeling; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using R3; namespace Asv.Drones; public sealed class DroneFrameItemViewModel : RoutableViewModel { public const string BaseId = "frame-item"; public DroneFrameItemViewModel() : this(NullDroneFrame.Instance, NullLoggerFactory.Instance) { DesignTime.ThrowIfNotDesignMode(); ApplyCommand = new ReactiveCommand( async (_, cancel) => { if (!IsCurrent.Value) { await Task.Delay(1000, cancel); } } ).DisposeItWith(Disposable); } public DroneFrameItemViewModel(IDroneFrame model, ILoggerFactory loggerFactory) : base(new NavigationId(BaseId, model.Id), loggerFactory) { Model = model; IsCurrent = new BindableReactiveProperty(false).DisposeItWith(Disposable); ApplyCommand = new ReactiveCommand( async (_, ct) => { if (!IsCurrent.Value) { await this.Rise(new CurrentDroneFrameChangeEvent(this), ct); } } ).DisposeItWith(Disposable); } public IDroneFrame Model { get; } public BindableReactiveProperty IsCurrent { get; } public ReactiveCommand ApplyCommand { get; } public bool Filter(string? searchText) { if (string.IsNullOrWhiteSpace(searchText)) { return true; } return Model.Id.Contains(searchText, StringComparison.InvariantCultureIgnoreCase); } public override IEnumerable GetChildren() { return []; } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/Setup/Subpage/FrameType/DroneFrameItem/NullDroneFrame.cs ================================================ using System.Collections.Generic; using Asv.Mavlink; namespace Asv.Drones; public class NullDroneFrame : IDroneFrame { public static IDroneFrame Instance { get; } = new NullDroneFrame { Id = "frame-id-1" }; public required string Id { get; init; } public IReadOnlyDictionary? Meta { get; init; } = new Dictionary { ["meta1"] = "val1", ["meta2"] = "val2" }; } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/Setup/Subpage/FrameType/DroneFrameItem/RoutedEvents/CurrentDroneFrameChangeEvent.cs ================================================ using Asv.Avalonia; using Asv.Common; using Asv.Modeling; namespace Asv.Drones; /// /// Represents an event triggered when the current drone frame item changes. /// /// . public sealed class CurrentDroneFrameChangeEvent(DroneFrameItemViewModel source) : AsyncRoutedEvent(source, RoutingStrategy.Bubble) { } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/Setup/Subpage/FrameType/FrameTypeSetupPageExtension.cs ================================================ using System.Linq; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Common; using Asv.Drones.Api; using Asv.IO; using Asv.Mavlink; using Material.Icons; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones; public class FrameTypeSetupPageExtension(ILoggerFactory loggerFactory) : IExtensionFor { public void Extend(ISetupPage context, CompositeDisposable contextDispose) { context .Target.Where(w => w is not null) .Subscribe(wrapper => { if (wrapper is null) { return; } var frameClient = wrapper.Value.Device.GetMicroservice(); if ( frameClient is null || context.Nodes.Any(node => node.Id == SetupFrameTypeViewModel.PageId) ) { return; } context.Nodes.Add( new TreePage( SetupFrameTypeViewModel.PageId, RS.SetupFrameTypeViewModel_Name, MaterialIconKind.ThemeLightDark, SetupFrameTypeViewModel.PageId, NavigationId.Empty, loggerFactory ).DisposeItWith(contextDispose) ); }) .DisposeItWith(contextDispose); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/Setup/Subpage/FrameType/SetupFrameTypeView.axaml ================================================ ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/Setup/Subpage/FrameType/SetupFrameTypeView.axaml.cs ================================================ using Asv.Avalonia; using Avalonia.Controls; namespace Asv.Drones; public partial class SetupFrameTypeView : UserControl { public SetupFrameTypeView() { InitializeComponent(); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/Setup/Subpage/FrameType/SetupFrameTypeViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Common; using Asv.Drones.Api; using Asv.IO; using Asv.Mavlink; using Asv.Modeling; using Avalonia.Threading; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using ObservableCollections; using R3; namespace Asv.Drones; public sealed class SetupFrameTypeViewModel : SetupSubpage { public const string PageId = "frame-type"; private readonly SynchronizedReactiveProperty _isRefreshing; private readonly SynchronizedReactiveProperty _isChangingFrame; private readonly ILoggerFactory _loggerFactory; private readonly YesOrNoDialogPrefab _yesOrNoDialog; private IFrameClient? _frameClient; private ISynchronizedView< KeyValuePair, DroneFrameItemViewModel >? _framesView; private readonly ISynchronizedViewFilter< KeyValuePair, DroneFrameItemViewModel > _framesViewFilter; public SetupFrameTypeViewModel() : this(NullLoggerFactory.Instance, NullDialogService.Instance) { var frames = new ObservableList( Enumerable .Range(1, 10) .Select(n => new DroneFrameItemViewModel( new NullDroneFrame { Id = $"frame-id-{n}" }, _loggerFactory )) ); CurrentFrame = new BindableReactiveProperty(frames[0].Model).DisposeItWith( Disposable ); CurrentFrameLabel = new BindableReactiveProperty( CurrentFrame?.Value?.Id ?? RS.SetupFrameTypeViewModel_CurrentFrame_Unknown ).DisposeItWith(Disposable); Frames = frames.ToNotifyCollectionChangedSlim(); RefreshCommand = new ReactiveCommand( async (_, cancel) => { _isRefreshing.Value = true; await Task.Delay(500, cancel); _isRefreshing.Value = false; } ).DisposeItWith(Disposable); } public SetupFrameTypeViewModel(ILoggerFactory loggerFactory, IDialogService dialogService) : base(PageId, loggerFactory) { _yesOrNoDialog = dialogService.GetDialogPrefab(); _loggerFactory = loggerFactory; _isRefreshing = new SynchronizedReactiveProperty(false).DisposeItWith(Disposable); _isChangingFrame = new SynchronizedReactiveProperty(false).DisposeItWith(Disposable); IsUpdating = _isRefreshing .ObserveOnUIThreadDispatcher() .CombineLatest(_isChangingFrame, (r, c) => r || c) .ToReadOnlyBindableReactiveProperty() .DisposeItWith(Disposable); SelectedFrame = new BindableReactiveProperty(null).DisposeItWith( Disposable ); RefreshCommand = new ReactiveCommand(Refresh, AwaitOperation.Drop).DisposeItWith( Disposable ); Search = new SearchBoxViewModel( nameof(Search), loggerFactory, UpdateImpl, TimeSpan.FromMilliseconds(500) ) .SetRoutableParent(this) .DisposeItWith(Disposable); _framesViewFilter = new SynchronizedViewFilter< KeyValuePair, DroneFrameItemViewModel >((_, model) => model.Filter(Search.Text.ViewValue.Value)); Events.Subscribe(InternalCatchEvent).DisposeItWith(Disposable); } public IReadOnlyBindableReactiveProperty IsUpdating { get; } public IReadOnlyBindableReactiveProperty? CurrentFrameLabel { get; private set; } public IReadOnlyBindableReactiveProperty? CurrentFrame { get; private set; } public SearchBoxViewModel Search { get; } public IReadOnlyDictionary MetaFallBack => ImmutableDictionary.Empty; public INotifyCollectionChangedSynchronizedViewList? Frames { get; private set; } public BindableReactiveProperty SelectedFrame { get; } public ReactiveCommand RefreshCommand { get; } public override ValueTask Init(ISetupPage context) { _frameClient = context.Target.CurrentValue?.Device.GetMicroservice() ?? throw new Exception($"{nameof(IFrameClient)} should not be null"); CurrentFrame = _frameClient .CurrentFrame.ToReadOnlyBindableReactiveProperty() .DisposeItWith(Disposable); CurrentFrameLabel = _frameClient .CurrentFrame.ObserveOnUIThreadDispatcher() .Select(droneFrame => string.IsNullOrWhiteSpace(droneFrame?.Id) ? RS.SetupFrameTypeViewModel_CurrentFrame_Unknown : droneFrame.Id ) .ToReadOnlyBindableReactiveProperty() .DisposeItWith(Disposable); _framesView = _frameClient .Frames.CreateView(droneFrame => new DroneFrameItemViewModel( droneFrame.Value, _loggerFactory )) .DisposeItWith(Disposable); _framesView.SetRoutableParent(this).DisposeItWith(Disposable); _framesView.DisposeMany().DisposeItWith(Disposable); Frames = _framesView .ToNotifyCollectionChanged(SynchronizationContextCollectionEventDispatcher.Current) .DisposeItWith(Disposable); _frameClient .CurrentFrame.ObserveOnUIThreadDispatcher() .Subscribe(currentFrame => { if (currentFrame is null) { return; } foreach (var frame in Frames) { frame.IsCurrent.Value = frame.Model.Id == currentFrame.Id; } }) .DisposeItWith(Disposable); return ValueTask.CompletedTask; } public override IEnumerable GetChildren() { yield return Search; foreach (var childRoutable in base.GetChildren()) { yield return childRoutable; } if (Frames is not null) { foreach (var vm in Frames) { yield return vm; } } } internal async Task ChangeFrameType(string frameId, CancellationToken cancel = default) { if (_frameClient is null) { return; } if (_frameClient.Frames.TryGetValue(frameId, out var droneFrame)) { try { var payload = new YesOrNoDialogPayload { Title = RS.SetupFrameTypeViewModel_ApplyConfirmation_Title, Message = RS.SetupFrameTypeViewModel_ApplyConfirmation_Message, }; var saveChanges = await _yesOrNoDialog.ShowDialogAsync(payload); if (!saveChanges) { return; } _isChangingFrame.Value = true; await _frameClient.SetFrame(droneFrame, cancel); Logger.LogInformation("Frame set to: {FrameId}", droneFrame.Id); } catch (Exception ex) { Logger.LogError(ex, "Failed to set frame"); } finally { _isChangingFrame.Value = false; } } } private async ValueTask InternalCatchEvent(IRoutable src, AsyncRoutedEvent e) { switch (e) { case CurrentDroneFrameChangeEvent { Sender: DroneFrameItemViewModel param }: { await this.ExecuteCommand( ChangeFrameTypeCommand.Id, CommandArg.CreateString(param.Model.Id) ); e.IsHandled = true; break; } } } private Task UpdateImpl(string? query, IProgress progress, CancellationToken cancel) { if (string.IsNullOrWhiteSpace(query)) { _framesView?.ResetFilter(); return Task.CompletedTask; } _framesView?.AttachFilter(_framesViewFilter); return Task.CompletedTask; } private async ValueTask Refresh(Unit unit, CancellationToken cancel = default) { if (_frameClient is null) { return; } try { _isRefreshing.Value = true; await _frameClient.RefreshCurrentFrame(cancel); await _frameClient.RefreshAvailableFrames(cancel); } catch (Exception ex) { Logger.LogError(ex, "Failed to refresh frame"); } finally { _isRefreshing.Value = false; } } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/Setup/Subpage/Motors/MotorItem/MotorItemView.axaml ================================================ ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/Setup/Subpage/Motors/MotorItem/MotorItemView.axaml.cs ================================================ using Asv.Avalonia; using Avalonia.Controls; namespace Asv.Drones; public partial class MotorItemView : UserControl { public MotorItemView() { InitializeComponent(); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/Setup/Subpage/Motors/MotorItem/MotorItemViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Common; using Asv.Mavlink; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using R3; namespace Asv.Drones; public sealed class MotorItemViewModel : RoutableViewModel { public const string BaseId = "motor-item"; private readonly SynchronizedReactiveProperty _throttle; private readonly SynchronizedReactiveProperty _isEnabled; public MotorItemViewModel() : base(new NavigationId(BaseId, "1"), NullLoggerFactory.Instance) { DesignTime.ThrowIfNotDesignMode(); IsEnabled = new BindableReactiveProperty(true); Pwm = new BindableReactiveProperty(1000); ServoChannel = 1; IsTestIdle = new BindableReactiveProperty(true); } public MotorItemViewModel( ITestMotor motor, ReactiveProperty duration, IUnitService unit, ILoggerFactory loggerFactory ) : base(new NavigationId(BaseId, motor.Id.ToString()), loggerFactory) { Motor = motor; Timeout = duration; _isEnabled = new SynchronizedReactiveProperty().DisposeItWith(Disposable); IsEnabled = _isEnabled.ToBindableReactiveProperty().DisposeItWith(Disposable); _throttle = new SynchronizedReactiveProperty(0).DisposeItWith(Disposable); Throttle = new HistoricalUnitProperty( nameof(Throttle), _throttle, unit.Units[ThrottleUnit.Id], loggerFactory ) .SetRoutableParent(this) .DisposeItWith(Disposable); Throttle .ViewValue.Subscribe(_ => _isEnabled.Value = !Throttle.ViewValue.HasErrors) .DisposeItWith(Disposable); Pwm = motor.Pwm.ToBindableReactiveProperty().DisposeItWith(Disposable); ServoChannel = motor.ServoChannel; IsTestIdle = motor .IsTestRun.ObserveOnUIThreadDispatcher() .Select(x => !x) .ToReadOnlyBindableReactiveProperty() .DisposeItWith(Disposable); ThrottleSymbol = Throttle .Unit.CurrentUnitItem.Select(item => item.Symbol) .ToReadOnlyBindableReactiveProperty() .DisposeItWith(Disposable); RunTestCommand = new ReactiveCommand( async (_, cts) => await RunMotorTest(motor, cts) ).DisposeItWith(Disposable); } public ITestMotor Motor { get; } public BindableReactiveProperty Pwm { get; } public int ServoChannel { get; } public IReadOnlyBindableReactiveProperty IsTestIdle { get; } public ReactiveCommand RunTestCommand { get; } public HistoricalUnitProperty Throttle { get; } public IReadOnlyBindableReactiveProperty ThrottleSymbol { get; } public ReactiveProperty Timeout { get; } public BindableReactiveProperty IsEnabled { get; } public override IEnumerable GetChildren() { yield return Throttle; } private async Task RunMotorTest(ITestMotor motor, CancellationToken cts) { if (IsTestIdle.Value) { await motor.StartTest((int)Throttle.ModelValue.CurrentValue, (int)Timeout.Value, cts); return; } await motor.StopTest(cts); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/Setup/Subpage/Motors/MotorsSetupPageExtension.cs ================================================ using System.Linq; using Asv.Avalonia; using Asv.Common; using Asv.Drones.Api; using Asv.IO; using Asv.Mavlink; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones; public class MotorsSetupPageExtension(ILoggerFactory loggerFactory) : IExtensionFor { public void Extend(ISetupPage context, CompositeDisposable contextDispose) { context .Target.Where(w => w is not null) .Subscribe(wrapper => { if (wrapper is null) { return; } var client = wrapper.Value.Device.GetMicroservice(); if ( client is null || context.Nodes.Any(node => node.Id == SetupMotorsViewModel.PageId) ) { return; } context.Nodes.Add( new TreePage( SetupMotorsViewModel.PageId, RS.SetupMotorsViewModel_Name, SetupMotorsViewModel.Icon, SetupMotorsViewModel.PageId, NavigationId.Empty, loggerFactory ).DisposeItWith(contextDispose) ); }) .DisposeItWith(contextDispose); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/Setup/Subpage/Motors/SetupMotorsView.axaml ================================================ ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/Setup/Subpage/Motors/SetupMotorsView.axaml.cs ================================================ using Asv.Avalonia; using Avalonia.Controls; namespace Asv.Drones; public partial class SetupMotorsView : UserControl { public SetupMotorsView() { InitializeComponent(); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/Setup/Subpage/Motors/SetupMotorsViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Common; using Asv.Drones.Api; using Asv.IO; using Asv.Mavlink; using Asv.Modeling; using Material.Icons; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using ObservableCollections; using R3; namespace Asv.Drones; public sealed class SetupMotorsViewModel : SetupSubpage { public const string PageId = "motor-test"; public const MaterialIconKind Icon = MaterialIconKind.Motor; private const int DefaultTestDurationInSeconds = 3; private readonly ILoggerFactory _loggerFactory; private readonly IUnitService _unit; private readonly ReactiveProperty _duration; private readonly SynchronizedReactiveProperty _isEnabled; private ISynchronizedView? _motorsView; private IMotorTestClient? _motorTestClient; public SetupMotorsViewModel() : this(NullLoggerFactory.Instance, NullUnitService.Instance) { } public SetupMotorsViewModel(ILoggerFactory loggerFactory, IUnitService unit) : base(PageId, loggerFactory) { _loggerFactory = loggerFactory; _unit = unit; _isEnabled = new SynchronizedReactiveProperty().DisposeItWith(Disposable); IsEnabled = _isEnabled.ToBindableReactiveProperty().DisposeItWith(Disposable); _duration = new ReactiveProperty(DefaultTestDurationInSeconds).DisposeItWith( Disposable ); Duration = new HistoricalUnitProperty( nameof(Duration), _duration, unit.Units[TimeSpanUnit.Id], loggerFactory ) .SetRoutableParent(this) .DisposeItWith(Disposable); Duration .ViewValue.Subscribe(_ => _isEnabled.Value = !Duration.ViewValue.HasErrors) .DisposeItWith(Disposable); DurationSymbol = Duration .Unit.CurrentUnitItem.Select(item => item.Symbol) .ToReadOnlyBindableReactiveProperty() .DisposeItWith(Disposable); } public HistoricalUnitProperty Duration { get; } public IReadOnlyBindableReactiveProperty DurationSymbol { get; } public BindableReactiveProperty IsEnabled { get; } public INotifyCollectionChangedSynchronizedViewList? Motors { get; private set; } public override ValueTask Init(ISetupPage context) { _motorTestClient = context.Target.CurrentValue?.Device.GetMicroservice() ?? throw new Exception($"{nameof(IMotorTestClient)} should not be null"); _motorsView = _motorTestClient .TestMotors.CreateView(motor => new MotorItemViewModel( motor, _duration, _unit, _loggerFactory )) .DisposeItWith(Disposable); _motorsView.SetRoutableParent(this).DisposeItWith(Disposable); _motorsView.DisposeMany().DisposeItWith(Disposable); Motors = _motorsView .ToNotifyCollectionChanged(SynchronizationContextCollectionEventDispatcher.Current) .DisposeItWith(Disposable); return ValueTask.CompletedTask; } public override IEnumerable GetChildren() { foreach (var childRoutable in base.GetChildren()) { yield return childRoutable; } if (Motors is null) { yield break; } foreach (var vm in Motors) { yield return vm; } yield return Duration; } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Device/Setup/Subpage/SetupSubpage.cs ================================================ using Asv.Avalonia; using Asv.Drones.Api; using Microsoft.Extensions.Logging; namespace Asv.Drones; public abstract class SetupSubpage : TreeSubpage, ISetupSubpage { protected SetupSubpage(NavigationId id, ILoggerFactory loggerFactory) : base(id, loggerFactory) { } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Flight/Anchors/FlightUavAnchorsExtension.cs ================================================ using Asv.Avalonia; using Asv.Avalonia.IO; using Asv.Common; using Asv.Drones.Api; using Asv.IO; using Asv.Mavlink; using Asv.Modeling; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones; public class FlightUavAnchorsExtension(IDeviceManager conn, ILoggerFactory loggerFactory) : IExtensionFor { public void Extend(IFlightMode context, CompositeDisposable contextDispose) { conn.Explorer.InitializedDevices.PopulateTo( context.MapViewModel.Anchors, TryCreateAnchor, RemoveAnchor ) .DisposeItWith(contextDispose); } private UavAnchor? TryCreateAnchor(IClientDevice device) { var pos = device.GetMicroservice(); return pos != null ? new UavAnchor(device.Id, conn, device, pos, loggerFactory) : null; } private static bool RemoveAnchor(IClientDevice dev, UavAnchor anchor) { return anchor.DeviceId == dev.Id; } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Flight/Anchors/MissionAnchor.cs ================================================ using Asv.Avalonia; using Asv.Avalonia.GeoMap; using Asv.Common; using Avalonia.Media; using Material.Icons; using Microsoft.Extensions.Logging; namespace Asv.Drones; public class MissionAnchor : MapAnchor { public MissionAnchor(int index, GeoPoint current, GeoPoint next, ILoggerFactory loggerFactory) : base($"wayPoint{index}", loggerFactory) // TODO: Use a more descriptive ID with drone ID { Location = current; Title = index.ToString(); IsReadOnly = true; IsVisible = true; Icon = MaterialIconKind.MapMarker; CenterY = new VerticalOffset(VerticalOffsetEnum.Bottom, 0); Foreground = Brushes.Red; Polygon.Add(current); Polygon.Add(next); } public MissionAnchor(int index, GeoPoint current, ILoggerFactory loggerFactory) : base($"wayPoint{index}", loggerFactory) { Location = current; Title = index.ToString(); IsReadOnly = true; IsVisible = true; Icon = MaterialIconKind.MapMarker; CenterY = new VerticalOffset(VerticalOffsetEnum.Bottom, 0); Foreground = Brushes.Red; } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Flight/Anchors/UavAnchor.cs ================================================ using System; using Asv.Avalonia; using Asv.Avalonia.GeoMap; using Asv.Avalonia.IO; using Asv.Common; using Asv.Drones.Api; using Asv.IO; using Asv.Mavlink; using Material.Icons; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones; public class UavAnchor : MapAnchor { public const string UavAnchorIdBase = "uav"; private const uint CurrentUavPositionChangeThrottleMs = 200; public DeviceId DeviceId { get; } public UavAnchor() : base(DesignTime.Id, DesignTime.LoggerFactory) { DesignTime.ThrowIfNotDesignMode(); } public UavAnchor( DeviceId deviceId, IDeviceManager mng, IClientDevice dev, IPositionClientEx pos, ILoggerFactory loggerFactory ) : base(UavAnchorIdBase, loggerFactory) { DeviceId = deviceId; InitArgs(deviceId.AsString()); IsReadOnly = true; IsVisible = true; Icon = mng.GetIcon(deviceId) ?? MaterialIconKind.Memory; IconColor = mng.GetDeviceColor(deviceId); CenterX = DeviceIconMixin.GetIconCenterX(deviceId); CenterY = DeviceIconMixin.GetIconCenterY(deviceId); UseMapRotation = true; dev.Name.ObserveOnUIThreadDispatcher() .Subscribe(x => Title = x ?? string.Empty) .DisposeItWith(Disposable); pos.Current.ObserveOnUIThreadDispatcher() .Subscribe(x => Location = x) .DisposeItWith(Disposable); pos.Yaw.ObserveOnUIThreadDispatcher().Subscribe(x => Azimuth = x).DisposeItWith(Disposable); var currentUavLocation = pos.Current.CurrentValue; var currentHomeLocation = pos.Home.CurrentValue ?? GeoPoint.Zero; pos.Home.ObserveOnUIThreadDispatcher() .Subscribe(x => { Polygon.Remove(currentHomeLocation); if (x is null) { return; } Polygon.Add(x.Value); currentHomeLocation = x.Value; }) .DisposeItWith(Disposable); pos.Current.ObserveOnUIThreadDispatcher() .ThrottleLast(TimeSpan.FromMilliseconds(CurrentUavPositionChangeThrottleMs)) .Subscribe(x => { Polygon.Remove(currentUavLocation); Polygon.Add(x); currentUavLocation = x; }) .DisposeItWith(Disposable); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Flight/FlightPageView.axaml ================================================  ================================================ FILE: src/Asv.Drones/Shell/Pages/Flight/FlightPageView.axaml.cs ================================================ using Asv.Avalonia; using Avalonia; using Avalonia.Controls; namespace Asv.Drones; public sealed class FlightPageViewConfig { public double LeftColumnActualWidth { get; set; } = -1; public double CenterColumnActualWidth { get; set; } = -1; public double RightColumnActualWidth { get; set; } = -1; public double CenterRowActualHeight { get; set; } = -1; public double BottomRowActualHeight { get; set; } = -1; } public partial class FlightPageView : UserControl { private readonly ILayoutService _layoutService; private FlightPageViewConfig? _config; private WorkspacePanel? _workspace; public FlightPageView() : this(NullLayoutService.Instance) { DesignTime.ThrowIfNotDesignMode(); } public FlightPageView(ILayoutService layoutService) { _layoutService = layoutService; InitializeComponent(); } private void WorkspacePanel_OnAttachedToVisualTree( object? sender, VisualTreeAttachmentEventArgs e ) { if (Design.IsDesignMode) { return; } _workspace = sender as WorkspacePanel; LoadLayout(); } private void OnWorkspaceChanged(object? sender, WorkspaceEventArgs workspaceEventArgs) { if (Design.IsDesignMode) { return; } SaveLayout(workspaceEventArgs); } private void LoadLayout() { _config = _layoutService.Get(this); if (_workspace is null) { return; } if (_config.LeftColumnActualWidth >= 0) { _workspace.LeftWidth = new GridLength(_config.LeftColumnActualWidth); } if (_config.CenterColumnActualWidth >= 0) { _workspace.CentralWidth = new GridLength(_config.CenterColumnActualWidth); } if (_config.RightColumnActualWidth >= 0) { _workspace.RightWidth = new GridLength(_config.RightColumnActualWidth); } if (_config.CenterRowActualHeight >= 0) { _workspace.CentralHeight = new GridLength(_config.CenterRowActualHeight); } if (_config.BottomRowActualHeight >= 0) { _workspace.BottomHeight = new GridLength(_config.BottomRowActualHeight); } } private void SaveLayout(WorkspaceEventArgs workspaceEventArgs) { if (_config is null) { return; } if (DataContext is null) { return; } _config.LeftColumnActualWidth = workspaceEventArgs.LeftColumnActualWidth; _config.CenterColumnActualWidth = workspaceEventArgs.CenterColumnActualWidth; _config.RightColumnActualWidth = workspaceEventArgs.RightColumnActualWidth; _config.CenterRowActualHeight = workspaceEventArgs.CenterRowActualHeight; _config.BottomRowActualHeight = workspaceEventArgs.BottomRowActualHeight; _layoutService.SetInMemory(this, _config); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Flight/FlightPageViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Avalonia.GeoMap; using Asv.Common; using Asv.Drones.Api; using Asv.Modeling; using Material.Icons; using Microsoft.Extensions.Logging; using ObservableCollections; namespace Asv.Drones; public sealed class FlightPageViewModelConfig { public GeoPoint MapCenter { get; set; } = GeoPoint.Zero; public int Zoom { get; set; } = IZoomService.MinZoomLevel; public double Rotation { get; set; } = 0.0; } public class FlightPageViewModel : PageViewModel, IFlightMode { public const string PageId = "flight"; public const MaterialIconKind PageIcon = MaterialIconKind.MapSearch; private FlightPageViewModelConfig? _config; public FlightPageViewModel() : this( NullMapService.Instance, DesignTime.CommandService, DesignTime.LoggerFactory, DesignTime.DialogService, DesignTime.ExtensionService ) { DesignTime.ThrowIfNotDesignMode(); var drone = new MapAnchor(DesignTime.Id, DesignTime.LoggerFactory) { Icon = MaterialIconKind.Navigation, Location = new GeoPoint(53, 53, 100), }; MapViewModel.Anchors.Add(drone); var azimuth = 0; TimeProvider.System.CreateTimer( x => { drone.Azimuth = (azimuth++ * 10) % 360; }, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1) ); Widgets.Add(new UavWidgetViewModel { Header = "Device11" }); } public FlightPageViewModel( IMapService mapService, ICommandService cmd, ILoggerFactory loggerFactory, IDialogService dialogService, IExtensionService ext ) : base(PageId, cmd, loggerFactory, dialogService, ext) { Title = RS.FlightPageViewModel_Title; Icon = PageIcon; MapViewModel = new MapViewModel(nameof(MapViewModel), loggerFactory, mapService) .SetRoutableParent(this) .DisposeItWith(Disposable); Widgets = []; Widgets.SetRoutableParent(this).DisposeItWith(Disposable); Widgets.DisposeRemovedItems().DisposeItWith(Disposable); WidgetsView = Widgets.ToNotifyCollectionChangedSlim().DisposeItWith(Disposable); Events.Subscribe(InternalCatchEvent).DisposeItWith(Disposable); } public NotifyCollectionChangedSynchronizedViewList WidgetsView { get; } public ObservableList Widgets { get; } public IMap MapViewModel { get; } public override IEnumerable GetChildren() { yield return MapViewModel; foreach (var widget in WidgetsView) { yield return widget; } } private ValueTask InternalCatchEvent(IRoutable src, AsyncRoutedEvent e) { switch (e) { case SaveLayoutEvent saveLayoutEvent: if (_config is null) { break; } this.HandleSaveLayout( saveLayoutEvent, _config, cfg => { cfg.MapCenter = MapViewModel.CenterMap.Value; cfg.Zoom = MapViewModel.Zoom.Value; cfg.Rotation = MapViewModel.Rotation.Value; }, FlushingStrategy.FlushBothViewModelAndView ); break; case LoadLayoutEvent loadLayoutEvent: _config = this.HandleLoadLayout( loadLayoutEvent, cfg => { MapViewModel.CenterMap.Value = cfg.MapCenter; MapViewModel.Zoom.Value = cfg.Zoom switch { < IZoomService.MinZoomLevel => IZoomService.MinZoomLevel, > IZoomService.MaxZoomLevel => IZoomService.MaxZoomLevel, _ => cfg.Zoom, }; MapViewModel.Rotation.Value = cfg.Rotation; } ); break; } return ValueTask.CompletedTask; } protected override void AfterLoadExtensions() { // nothing to do } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Flight/HomePageFlightExtension.cs ================================================ using Asv.Avalonia; using Asv.Common; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones; public class HomePageFlightExtension(ILoggerFactory loggerFactory) : IExtensionFor { public void Extend(IHomePage context, CompositeDisposable contextDispose) { context.Tools.Add( OpenFlightModeCommand .StaticInfo.CreateAction(loggerFactory) .DisposeItWith(contextDispose) ); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Flight/Widgets/FlightWidgetsExtension.cs ================================================ using Asv.Avalonia; using Asv.Avalonia.IO; using Asv.Common; using Asv.Drones.Api; using Asv.IO; using Asv.Mavlink; using Asv.Modeling; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones; public class FlightWidgetsExtension( IDeviceManager conn, INavigationService navigationService, IUnitService unitService, ILoggerFactory loggerFactory ) : IExtensionFor { public void Extend(IFlightMode context, CompositeDisposable contextDispose) { conn.Explorer.InitializedDevices.PopulateTo(context.Widgets, TryCreateWidget, RemoveWidget) .DisposeItWith(contextDispose); } private UavWidgetViewModel? TryCreateWidget(IClientDevice device) { var isInit = device.State.CurrentValue == ClientDeviceState.Complete; if (!isInit) { return null; } if (device.GetMicroservice() is null) { return null; } return new UavWidgetViewModel(device, navigationService, unitService, conn, loggerFactory); } private bool RemoveWidget(IClientDevice model, UavWidgetViewModel vm) { return model.Id == vm.Device.Id; } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Flight/Widgets/UavWidget/Dialogs/SetAltitudeDialogView.axaml ================================================ ================================================ FILE: src/Asv.Drones/Shell/Pages/Flight/Widgets/UavWidget/Dialogs/SetAltitudeDialogView.axaml.cs ================================================ using Asv.Avalonia; using Avalonia.Controls; namespace Asv.Drones; public partial class SetAltitudeDialogView : UserControl { public SetAltitudeDialogView() { InitializeComponent(); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Flight/Widgets/UavWidget/Dialogs/SetAltitudeDialogViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using Asv.Avalonia; using Asv.Common; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones; public class SetAltitudeDialogViewModel : DialogViewModelBase { public const string DialogId = $"{BaseId}.altitude"; public SetAltitudeDialogViewModel() : this( NullUnitService.Instance.Units.Values.First(u => u.UnitId == AltitudeUnit.Id), DesignTime.LoggerFactory ) { DesignTime.ThrowIfNotDesignMode(); } public SetAltitudeDialogViewModel(IUnit unit, ILoggerFactory loggerFactory) : base(DialogId, loggerFactory) { var altitudeUnit = unit as AltitudeUnit ?? throw new InvalidCastException($"Unit must be an {nameof(AltitudeUnit)}"); Altitude = new BindableReactiveProperty(string.Empty).DisposeItWith(Disposable); AltitudeUnitSymbol = altitudeUnit .CurrentUnitItem.ObserveOnUIThreadDispatcher() .Select(item => item.Symbol) .ToReadOnlyBindableReactiveProperty() .DisposeItWith(Disposable); Altitude .EnableValidationRoutable( s => { var valid = altitudeUnit.CurrentUnitItem.Value.ValidateValue(s); return valid; }, this, true ) .DisposeItWith(Disposable); } public override void ApplyDialog(ContentDialog dialog) { _sub2.Disposable = IsValid.Subscribe(enabled => dialog.IsPrimaryButtonEnabled = enabled); } public override IEnumerable GetChildren() { return []; } public BindableReactiveProperty Altitude { get; } public IReadOnlyBindableReactiveProperty AltitudeUnitSymbol { get; } #region Dispose private readonly SerialDisposable _sub2 = new(); protected override void Dispose(bool disposing) { if (disposing) { _sub2.Dispose(); } base.Dispose(disposing); } #endregion } ================================================ FILE: src/Asv.Drones/Shell/Pages/Flight/Widgets/UavWidget/MissionProgress/MissionProgressView.axaml ================================================  ================================================ FILE: src/Asv.Drones/Shell/Pages/Flight/Widgets/UavWidget/MissionProgress/MissionProgressView.axaml.cs ================================================ using Avalonia.Controls; namespace Asv.Drones; public partial class MissionProgressView : UserControl { public MissionProgressView() { InitializeComponent(); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Flight/Widgets/UavWidget/MissionProgress/MissionProgressViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Common; using Asv.IO; using Asv.Mavlink; using Asv.Mavlink.Common; using Material.Icons; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones; public class MissionProgressViewModel : RoutableViewModel { public const string ViewModelId = "mission.progress"; private const AsvColorKind DefaultStatusColor = AsvColorKind.Info5; private readonly IClientDevice _device; private readonly IPositionClientEx _positionClient; private readonly IMissionClientEx _missionClient; private readonly IGnssClientEx _gnssClientEx; private readonly SynchronizedReactiveProperty _currentIndex; private readonly SynchronizedReactiveProperty _reachedIndex; private double _passedDistance; private bool _isOnMission; public MissionProgressViewModel() : base(ViewModelId, DesignTime.LoggerFactory) { DesignTime.ThrowIfNotDesignMode(); new CancellationTokenSource().DisposeItWith(Disposable); _currentIndex = new SynchronizedReactiveProperty(0).DisposeItWith(Disposable); _reachedIndex = new SynchronizedReactiveProperty(0).DisposeItWith(Disposable); var missionDistance = new ReactiveProperty(1000).DisposeItWith(Disposable); var targetDistance = new ReactiveProperty(100).DisposeItWith(Disposable); var totalDistance = new ReactiveProperty(1100).DisposeItWith(Disposable); var homeDistance = new ReactiveProperty(100).DisposeItWith(Disposable); PathProgress = new BindableReactiveProperty(0).DisposeItWith(Disposable); IsDownloaded = new BindableReactiveProperty(false).DisposeItWith(Disposable); DownloadProgress = new BindableReactiveProperty().DisposeItWith(Disposable); MissionFlightTime = new BindableReactiveProperty( $"- {RS.MissionProgressViewModel_MissionFlightTime_Symbol}" ).DisposeItWith(Disposable); var unitService = NullUnitService.Instance; MissionDistanceRttBox = new SplitDigitRttBoxViewModel( nameof(MissionDistanceRttBox), DesignTime.LoggerFactory, unitService, DistanceUnit.Id, missionDistance, null ) { Icon = MaterialIconKind.MapMarkerDistance, Header = RS.MissionProgressView_MissionDistanceRTT, Status = DefaultStatusColor, } .SetRoutableParent(this) .DisposeItWith(Disposable); TotalDistanceRttBox = new SplitDigitRttBoxViewModel( nameof(TotalDistanceRttBox), DesignTime.LoggerFactory, unitService, DistanceUnit.Id, totalDistance, null ) { Icon = MaterialIconKind.LocationDistance, Header = RS.MissionProgressView_TotalDistanceRTT, Status = DefaultStatusColor, } .SetRoutableParent(this) .DisposeItWith(Disposable); HomeDistanceRttBox = new SplitDigitRttBoxViewModel( nameof(HomeDistanceRttBox), DesignTime.LoggerFactory, unitService, DistanceUnit.Id, homeDistance, null ) { Icon = MaterialIconKind.Home, Header = RS.MissionProgressView_HomeDistance, Status = DefaultStatusColor, } .SetRoutableParent(this) .DisposeItWith(Disposable); TargetDistanceRttBox = new SplitDigitRttBoxViewModel( nameof(TargetDistanceRttBox), DesignTime.LoggerFactory, unitService, DistanceUnit.Id, targetDistance, null ) { Icon = MaterialIconKind.Target, Header = RS.MissionProgressView_TargetDistance, Status = DefaultStatusColor, } .SetRoutableParent(this) .DisposeItWith(Disposable); IsDownloaded.Value = true; MissionFlightTime.Value = "15 min"; PathProgress.Value = 0.7; } public MissionProgressViewModel( IClientDevice device, IUnitService unitService, ILoggerFactory loggerFactory ) : base(ViewModelId, loggerFactory) { ArgumentNullException.ThrowIfNull(device); ArgumentNullException.ThrowIfNull(unitService); ArgumentNullException.ThrowIfNull(loggerFactory); _device = device; _missionClient = device.GetMicroservice() ?? throw new Exception($"Unable to load {nameof(IMissionClientEx)} from {device.Id}"); _gnssClientEx = device.GetMicroservice() ?? throw new Exception($"Unable to load {nameof(IGnssClientEx)} from {device.Id}"); _positionClient = device.GetMicroservice() ?? throw new Exception($"Unable to load {nameof(IPositionClientEx)} from {device.Id}"); var mode = device.GetMicroservice() ?? throw new Exception($"Unable to load {nameof(IModeClient)} from {device.Id}"); UpdateMission = new BindableAsyncCommand(UpdateMissionCommand.Id, this); _currentIndex = new SynchronizedReactiveProperty(0).DisposeItWith(Disposable); _reachedIndex = new SynchronizedReactiveProperty(0).DisposeItWith(Disposable); var missionDistance = new ReactiveProperty().DisposeItWith(Disposable); var targetDistance = new ReactiveProperty().DisposeItWith(Disposable); var totalDistance = new ReactiveProperty().DisposeItWith(Disposable); var homeDistance = new ReactiveProperty().DisposeItWith(Disposable); MissionDistanceRttBox = new SplitDigitRttBoxViewModel( nameof(MissionDistanceRttBox), loggerFactory, unitService, DistanceUnit.Id, missionDistance, null ) { Icon = MaterialIconKind.MapMarkerDistance, Header = RS.MissionProgressView_MissionDistanceRTT, Status = DefaultStatusColor, FormatString = "F2", } .SetRoutableParent(this) .DisposeItWith(Disposable); TotalDistanceRttBox = new SplitDigitRttBoxViewModel( nameof(TotalDistanceRttBox), loggerFactory, unitService, DistanceUnit.Id, totalDistance, null ) { Icon = MaterialIconKind.LocationDistance, Header = RS.MissionProgressView_TotalDistanceRTT, Status = DefaultStatusColor, FormatString = "F2", } .SetRoutableParent(this) .DisposeItWith(Disposable); HomeDistanceRttBox = new SplitDigitRttBoxViewModel( nameof(HomeDistanceRttBox), loggerFactory, unitService, DistanceUnit.Id, homeDistance, null ) { Icon = MaterialIconKind.Home, Header = RS.MissionProgressView_HomeDistance, Status = DefaultStatusColor, FormatString = "F2", } .SetRoutableParent(this) .DisposeItWith(Disposable); TargetDistanceRttBox = new SplitDigitRttBoxViewModel( nameof(TargetDistanceRttBox), loggerFactory, unitService, DistanceUnit.Id, targetDistance, null ) { Icon = MaterialIconKind.Target, Header = RS.MissionProgressView_TargetDistance, Status = DefaultStatusColor, FormatString = "F2", } .SetRoutableParent(this) .DisposeItWith(Disposable); PathProgress = new BindableReactiveProperty(0).DisposeItWith(Disposable); IsDownloaded = new BindableReactiveProperty(false).DisposeItWith(Disposable); DownloadProgress = new BindableReactiveProperty().DisposeItWith(Disposable); MissionFlightTime = new BindableReactiveProperty( $"- {RS.MissionProgressViewModel_MissionFlightTime_Symbol}" ).DisposeItWith(Disposable); _missionClient .AllMissionsDistance.ObserveOnUIThreadDispatcher() .Subscribe(d => { missionDistance.Value = d * 1000; var start = _missionClient.MissionItems.FirstOrDefault(); var stop = _missionClient.MissionItems.LastOrDefault(missionItem => missionItem.Command.Value != MavCmd.MavCmdNavReturnToLaunch ); if (start != null && stop != null) { missionDistance.Value += GeoMath.Distance( start.Location.Value, _positionClient.Home.CurrentValue ); missionDistance.Value += GeoMath.Distance( stop.Location.Value, _positionClient.Home.CurrentValue ); } if (missionDistance.Value < 1) { missionDistance.Value = d * 1000; } }) .DisposeItWith(Disposable); _positionClient .HomeDistance.ObserveOnUIThreadDispatcher() .Subscribe(d => homeDistance.Value = d) .DisposeItWith(Disposable); _positionClient .TargetDistance.ObserveOnUIThreadDispatcher() .Subscribe(d => targetDistance.Value = d) .DisposeItWith(Disposable); _missionClient .AllMissionsDistance.ObserveOnUIThreadDispatcher() .Select(v => v * 1000) .Subscribe(v => { var distanceBeforeMission = _missionClient.MissionItems.Count == 0 ? 0 : GeoMath.Distance( _positionClient.Current.CurrentValue, _missionClient.MissionItems[0].Location.Value ); var rtl = _missionClient.MissionItems.FirstOrDefault(_ => _.Command.Value == MavCmd.MavCmdNavReturnToLaunch ); if (rtl is not null) { totalDistance.Value = v + (distanceBeforeMission * 2000); return; } totalDistance.Value = v + (distanceBeforeMission * 1000); }) .DisposeItWith(Disposable); mode.CurrentMode.ObserveOnUIThreadDispatcher() .Subscribe(m => { if (m == ArduCopterMode.Auto || m == ArduPlaneMode.Auto) { if (_isOnMission) { return; } _isOnMission = true; return; } if (m == ArduCopterMode.Rtl || m == ArduPlaneMode.Rtl) { _isOnMission = false; _reachedIndex.Value = 0; PathProgress.Value = 0; } }) .DisposeItWith(Disposable); PathProgress .ObserveOnUIThreadDispatcher() .Where(x => x is < 0 or > 1) .Subscribe(p => { var clamped = Math.Clamp(p, 0, 1); PathProgress.Value = clamped; }) .DisposeItWith(Disposable); _currentIndex .ObserveOnUIThreadDispatcher() .Subscribe(c => { if (_missionClient.MissionItems.Count == 0) { return; } _passedDistance = 0; var items = _missionClient .MissionItems.Where(item => item.Index <= c && item.Command.Value != MavCmd.MavCmdDoChangeSpeed ) .ToList(); if (items.Count < 2) { return; } for (var i = 1; i < items.Count; i++) { _passedDistance += GeoMath.Distance( items[i - 1].Location.Value, items[i].Location.Value ); } }) .DisposeItWith(Disposable); _missionClient .Reached.ObserveOnUIThreadDispatcher() .Subscribe(i => _reachedIndex.Value = i) .DisposeItWith(Disposable); _missionClient .Current.ObserveOnUIThreadDispatcher() .Subscribe(i => _currentIndex.Value = i) .DisposeItWith(Disposable); Observable .Timer(TimeSpan.Zero, TimeSpan.FromSeconds(1)) .ObserveOnUIThreadDispatcher() .Subscribe(_ => CalculateMissionProgress()) .DisposeItWith(Disposable); } public BindableAsyncCommand UpdateMission { get; set; } public BindableReactiveProperty MissionFlightTime { get; } public BindableReactiveProperty DownloadProgress { get; } public BindableReactiveProperty IsDownloaded { get; } public BindableReactiveProperty PathProgress { get; } public SplitDigitRttBoxViewModel MissionDistanceRttBox { get; } public SplitDigitRttBoxViewModel TotalDistanceRttBox { get; } public SplitDigitRttBoxViewModel HomeDistanceRttBox { get; } public SplitDigitRttBoxViewModel TargetDistanceRttBox { get; } public override IEnumerable GetChildren() { yield return MissionDistanceRttBox; yield return TotalDistanceRttBox; yield return HomeDistanceRttBox; yield return TargetDistanceRttBox; } internal async Task InitiateMissionPoints(CancellationToken cancel) { await DownloadMissionsImpl(cancel); } private async ValueTask DownloadMissionsImpl(CancellationToken cancel) { await _missionClient.Download(cancel, p => DownloadProgress.Value = p * 100); double homeAlt = 0; if (_positionClient.Home.CurrentValue is not null) { homeAlt = _positionClient.Home.CurrentValue.Value.Altitude; } IsDownloaded.Value = true; for (var i = 0; i < _missionClient.MissionItems.Count; i++) { if (i == 0 && _device is ArduPlaneClientDevice) { continue; } var item = _missionClient.MissionItems[i]; if ( item.Command.Value == MavCmd.MavCmdNavWaypoint && item.Location.Value.Altitude <= homeAlt ) { // TODO: Notify user on alt lower than start value } } } private void CalculateMissionProgress() { if (!_isOnMission) { return; } var toTargetDistance = GeoMath.Distance( _positionClient.Target.CurrentValue, _positionClient.Current.CurrentValue ); var missionDistance = _missionClient.AllMissionsDistance.CurrentValue * 1000; var distance = Math.Abs(missionDistance - _passedDistance + toTargetDistance); var time = distance / _gnssClientEx.Main.GroundVelocity.CurrentValue; PathProgress.Value = CalculatePathProgressValue(missionDistance, distance); MissionFlightTime.Value = CalculateMissionFlightTime(time); } private string CalculateMissionFlightTime(double time) { if (time is double.NaN or double.PositiveInfinity || !_isOnMission) { return $"- {RS.MissionProgressViewModel_MissionFlightTime_Symbol}"; } var minute = Math.Round(time / 60); if (minute < 1) { return $"<1 {RS.MissionProgressViewModel_MissionFlightTime_Symbol}"; } return $"{minute} {RS.MissionProgressViewModel_MissionFlightTime_Symbol}"; } private double CalculatePathProgressValue(double missionDistance, double distance) // TODO: extend logic for plane clients { if (!_isOnMission) { return Math.Abs((missionDistance - distance) / missionDistance); } switch (_device) { case ArduCopterClientDevice: if (_isOnMission && _reachedIndex.Value > 0) { return Math.Abs((missionDistance - distance) / missionDistance); } if (_currentIndex.Value == _missionClient.MissionItems.Count) { return 1; } return 0; default: return Math.Abs((missionDistance - distance) / missionDistance); } } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Flight/Widgets/UavWidget/UavWidgetView.axaml ================================================  ================================================ FILE: src/Asv.Drones/Shell/Pages/Flight/Widgets/UavWidget/UavWidgetView.axaml.cs ================================================ using Asv.Avalonia; using Avalonia.Controls; namespace Asv.Drones; public partial class UavWidgetView : UserControl { public UavWidgetView() { InitializeComponent(); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/Flight/Widgets/UavWidget/UavWidgetViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Windows.Input; using Asv.Avalonia; using Asv.Avalonia.GeoMap; using Asv.Avalonia.IO; using Asv.Common; using Asv.Drones.Api; using Asv.IO; using Asv.Mavlink; using Material.Icons; using Microsoft.Extensions.Logging; using R3; using Observable = R3.Observable; namespace Asv.Drones; public class UavWidgetViewModel : MapWidget, IUavFlightWidget { private const string WidgetId = "widget-uav"; private const AsvColorKind DefaultStatusColor = AsvColorKind.Info5; private readonly IUnit _altitudeUnit; private readonly IUnit _capacityUnit; private readonly IUnit _amperageUnit; private readonly IUnit _voltageUnit; private readonly IUnit _progressUnit; private readonly IGnssClientEx? _gnssClient; private const int CriticalAltitude = 40; private const int DangerHighSpeed = 10; private const int DangerSatelliteCount = 10; private static readonly Range WarningSatelliteAmount = 15..20; #pragma warning disable SA1313 private record BatteryRttBoxData( double Charge, double Amperage, double Voltage, double Consumed, IUnitItem ProgressUnit, IUnitItem AmperageUnit, IUnitItem CapacityUnit, IUnitItem VoltageUnit ); private record AltitudeRttBoxData( double AltitudeAgl, double AltitudeMsl, IUnitItem AltitudeUnit ); private record GnssRttBoxData( int Sattelites, double HdopCount, double VdopCount, Mavlink.Common.GpsFixType Mode ); #pragma warning restore SA1313 public UavWidgetViewModel() : base(WidgetId, DesignTime.LoggerFactory, NullMapService.Instance) { DesignTime.ThrowIfNotDesignMode(); TakeOff = new ReactiveCommand(); AutoMode = new ReactiveCommand(); Rtl = new ReactiveCommand(); Guided = new ReactiveCommand(); StartMission = new ReactiveCommand(); Land = new ReactiveCommand(); InitArgs("1"); MissionProgress = new MissionProgressViewModel().DisposeItWith(Disposable); NullUnitService.Instance.Extend( new VelocityUnit( DesignTime.Configuration, [new VelocityMetersPerSecondUnitItem(), new VelocityMilesPerHourUnitItem()] ) ); NullUnitService.Instance.Extend( new ProgressUnit( DesignTime.Configuration, [new ProgressPercentUnitItem(), new ProgressInPartsUnitItem()] ) ); NullUnitService.Instance.Extend( new CapacityUnit(DesignTime.Configuration, [new CapacityMilliAmperePerHourUnitItem()]) ); NullUnitService.Instance.Extend( new AmperageUnit( DesignTime.Configuration, [new AmperageAmpereUnitItem(), new AmperageMilliAmpereUnitItem()] ) ); NullUnitService.Instance.Extend( new VoltageUnit( DesignTime.Configuration, [new VoltageVoltUnitItem(), new VoltageMilliVoltUnitItem()] ) ); var unitService = NullUnitService.Instance; Icon = MaterialIconKind.AccountFile; IconColor = AsvColorKind.Info5; _altitudeUnit = unitService.Units[AltitudeUnit.Id]; _capacityUnit = unitService.Units[CapacityUnit.Id]; _amperageUnit = unitService.Units[AmperageUnit.Id]; _voltageUnit = unitService.Units[VoltageUnit.Id]; _progressUnit = unitService.Units[ProgressUnit.Id]; var linkQuality = new ReactiveProperty(100).DisposeItWith(Disposable); var altitudeAgl = new ReactiveProperty(10).DisposeItWith(Disposable); var altitudeMsl = new ReactiveProperty(14).DisposeItWith(Disposable); var heading = new ReactiveProperty(29).DisposeItWith(Disposable); var azimuth = new ReactiveProperty(39).DisposeItWith(Disposable); var homeAzimuth = new ReactiveProperty(30).DisposeItWith(Disposable); var satelliteCount = new ReactiveProperty(10).DisposeItWith(Disposable); var hdopCount = new ReactiveProperty(2).DisposeItWith(Disposable); var vdopCount = new ReactiveProperty(4).DisposeItWith(Disposable); var gpsFixType = new ReactiveProperty( Mavlink.Common.GpsFixType.GpsFixTypeDgps ).DisposeItWith(Disposable); var velocity = new ReactiveProperty(199).DisposeItWith(Disposable); var batteryAmperage = new ReactiveProperty(39).DisposeItWith(Disposable); var batteryVoltage = new ReactiveProperty(34).DisposeItWith(Disposable); var batteryCharge = new ReactiveProperty(123).DisposeItWith(Disposable); var batteryConsumed = new ReactiveProperty(39).DisposeItWith(Disposable); var currentFlightMode = new ReactiveProperty("Unknown").DisposeItWith(Disposable); CurrentFlightModeRttBox = new SingleRttBoxViewModel( nameof(CurrentFlightModeRttBox), DesignTime.LoggerFactory, currentFlightMode, null ) { Header = RS.UavRttItem_Mode, Icon = MaterialIconKind.FlightMode, UpdateAction = (model, mode) => model.ValueString = mode, Status = DefaultStatusColor, } .SetRoutableParent(this) .DisposeItWith(Disposable); AltitudeRttBox = new TwoColumnRttBoxViewModel( nameof(AltitudeRttBox), DesignTime.LoggerFactory, altitudeAgl .CombineLatest( altitudeMsl, _altitudeUnit.CurrentUnitItem, (agl, msl, unit) => new AltitudeRttBoxData(agl, msl, unit) ) .ObserveOnUIThreadDispatcher() .ThrottleLast(TimeSpan.FromMilliseconds(200)), null ) { Header = RS.UavRttItem_Altitude, Icon = MaterialIconKind.Altimeter, UpdateAction = (model, changes) => { model.Left.ValueString = changes.AltitudeUnit.PrintFromSi( changes.AltitudeAgl, "F2" ); model.Right.ValueString = changes.AltitudeUnit.PrintFromSi( changes.AltitudeMsl, "F2" ); model.Left.UnitSymbol = changes.AltitudeUnit.Symbol; model.Right.UnitSymbol = changes.AltitudeUnit.Symbol; }, Status = DefaultStatusColor, } .SetRoutableParent(this) .DisposeItWith(Disposable); AltitudeRttBox.Left.Header = "AGL"; AltitudeRttBox.Right.Header = "MSL"; VelocityRttBox = new SplitDigitRttBoxViewModel( nameof(VelocityRttBox), DesignTime.LoggerFactory, unitService, VelocityUnit.Id, velocity, null ) { Header = RS.UavRttItem_Velocity, ShortHeader = "GS", Icon = MaterialIconKind.Speedometer, Status = DefaultStatusColor, FormatString = "F2", } .SetRoutableParent(this) .DisposeItWith(Disposable); AzimuthRttBox = new SplitDigitRttBoxViewModel( nameof(AzimuthRttBox), DesignTime.LoggerFactory, unitService, AngleUnit.Id, azimuth, null ) { Header = RS.UavRttItem_Azimuth, Icon = MaterialIconKind.SunAzimuth, Status = DefaultStatusColor, FormatString = "F2", } .SetRoutableParent(this) .DisposeItWith(Disposable); BatteryRttBox = new KeyValueRttBoxViewModel( nameof(BatteryRttBox), DesignTime.LoggerFactory, batteryCharge .CombineLatest( batteryAmperage, batteryVoltage, batteryConsumed, _progressUnit.CurrentUnitItem, _amperageUnit.CurrentUnitItem, _capacityUnit.CurrentUnitItem, _voltageUnit.CurrentUnitItem, (bC, bA, bV, bCo, prog, amp, cap, vol) => new BatteryRttBoxData(bC, bA, bV, bCo, prog, amp, cap, vol) ) .ObserveOnUIThreadDispatcher() .ThrottleLast(TimeSpan.FromMilliseconds(200)), null ) { Header = RS.UavRttItem_Battery, Icon = MaterialIconKind.Battery10, UpdateAction = (model, changes) => { model[ 0, RS.UavWidgetViewModel_BatteryRttBox_BatteryCharge_Header, changes.ProgressUnit.Symbol ].ValueString = changes.ProgressUnit.PrintFromSi(changes.Charge, "F2"); model[ 1, RS.UavWidgetViewModel_BatteryRttBox_BatteryAmperage_Header, changes.AmperageUnit.Symbol ].ValueString = changes.AmperageUnit.PrintFromSi(changes.Amperage, "F2"); model[ 2, RS.UavWidgetViewModel_BatteryRttBox_BatteryVoltage_Header, changes.VoltageUnit.Symbol ].ValueString = changes.VoltageUnit.PrintFromSi(changes.Voltage, "F2"); model[ 3, RS.UavWidgetViewModel_BatteryRttBox_BatteryConsumed_Header, changes.CapacityUnit.Symbol ].ValueString = changes.CapacityUnit.PrintFromSi(changes.Consumed, "F2"); ChangeBatteryStatus(changes.Charge); }, Status = DefaultStatusColor, } .SetRoutableParent(this) .DisposeItWith(Disposable); GnssRttBox = new KeyValueRttBoxViewModel( nameof(GnssRttBox), DesignTime.LoggerFactory, satelliteCount .CombineLatest( hdopCount, vdopCount, gpsFixType, (satellites, hdops, vdops, mode) => new GnssRttBoxData(satellites, hdops, vdops, mode) ) .ObserveOnUIThreadDispatcher() .ThrottleLast(TimeSpan.FromMilliseconds(200)), null ) { Header = RS.UavRttItem_GNSS, Icon = MaterialIconKind.GpsFixed, UpdateAction = (model, changes) => { model[ 0, RS.UavWidgetViewModel_GnssRttBox_SatellitesCount_Header, null ].ValueString = changes.Sattelites.ToString(); model[1, RS.UavWidgetViewModel_GnssRttBox_Hdop_Header, null].ValueString = changes.HdopCount.ToString("F2"); model[2, RS.UavWidgetViewModel_GnssRttBox_Vdop_Header, null].ValueString = changes.VdopCount.ToString("F2"); model[3, RS.UavWidgetViewModel_GnssRttBox_Mode_Header, null].ValueString = GpsFixTypeToString(changes.Mode); ChangeGnssStatus(changes.Sattelites, changes.Mode); }, Status = DefaultStatusColor, } .SetRoutableParent(this) .DisposeItWith(Disposable); LinkQualityRttBox = new SplitDigitRttBoxViewModel( nameof(LinkQualityRttBox), DesignTime.LoggerFactory, unitService, ProgressUnit.Id, linkQuality, null ) { Header = RS.UavRttItem_Link, Icon = MaterialIconKind.Wifi, Status = DefaultStatusColor, FormatString = "F2", } .SetRoutableParent(this) .DisposeItWith(Disposable); AltitudeAgl = altitudeAgl.ToReadOnlyBindableReactiveProperty().DisposeItWith(Disposable); AltitudeMsl = altitudeMsl.ToReadOnlyBindableReactiveProperty().DisposeItWith(Disposable); Azimuth = azimuth.ToReadOnlyBindableReactiveProperty().DisposeItWith(Disposable); Heading = heading.ToReadOnlyBindableReactiveProperty().DisposeItWith(Disposable); HomeAzimuth = homeAzimuth.ToReadOnlyBindableReactiveProperty().DisposeItWith(Disposable); Velocity = velocity.ToReadOnlyBindableReactiveProperty().DisposeItWith(Disposable); Roll = new BindableReactiveProperty().DisposeItWith(Disposable); Pitch = new BindableReactiveProperty().DisposeItWith(Disposable); IsArmed = new BindableReactiveProperty().DisposeItWith(Disposable); StatusText = new BindableReactiveProperty().DisposeItWith(Disposable); VibrationX = new BindableReactiveProperty().DisposeItWith(Disposable); VibrationY = new BindableReactiveProperty().DisposeItWith(Disposable); VibrationZ = new BindableReactiveProperty().DisposeItWith(Disposable); Clipping0 = new BindableReactiveProperty().DisposeItWith(Disposable); Clipping1 = new BindableReactiveProperty().DisposeItWith(Disposable); Clipping2 = new BindableReactiveProperty().DisposeItWith(Disposable); ArmedTime = new BindableReactiveProperty().DisposeItWith(Disposable); Roll = new BindableReactiveProperty().DisposeItWith(Disposable); Pitch = new BindableReactiveProperty().DisposeItWith(Disposable); IsArmed = new BindableReactiveProperty().DisposeItWith(Disposable); StatusText = new BindableReactiveProperty().DisposeItWith(Disposable); VibrationX = new BindableReactiveProperty().DisposeItWith(Disposable); VibrationY = new BindableReactiveProperty().DisposeItWith(Disposable); VibrationZ = new BindableReactiveProperty().DisposeItWith(Disposable); Clipping0 = new BindableReactiveProperty().DisposeItWith(Disposable); Clipping1 = new BindableReactiveProperty().DisposeItWith(Disposable); Clipping2 = new BindableReactiveProperty().DisposeItWith(Disposable); ArmedTime = new BindableReactiveProperty().DisposeItWith(Disposable); } public UavWidgetViewModel( IClientDevice device, INavigationService navigation, IUnitService unitService, IDeviceManager dev, ILoggerFactory loggerFactory ) : base(WidgetId, loggerFactory, NullMapService.Instance) { ArgumentNullException.ThrowIfNull(device); Device = device; Position = WorkspaceDock.Left; Icon = dev.GetIcon(device.Id); IconColor = dev.GetDeviceColor(device.Id); _altitudeUnit = unitService.Units[AltitudeUnit.Id]; _capacityUnit = unitService.Units[CapacityUnit.Id]; _amperageUnit = unitService.Units[AmperageUnit.Id]; _voltageUnit = unitService.Units[VoltageUnit.Id]; _progressUnit = unitService.Units[ProgressUnit.Id]; device.Name.Subscribe(x => Header = x).DisposeItWith(Disposable); InitArgs(device.Id.AsString()); MissionProgress = new MissionProgressViewModel(device, unitService, loggerFactory) .SetRoutableParent(this) .DisposeItWith(Disposable); var positionClientEx = device.GetMicroservice() ?? throw new ArgumentException( $"Unable to load {nameof(PositionClientEx)} from {device.Id}" ); _gnssClient = device.GetMicroservice() ?? throw new ArgumentException( $"Unable to load {nameof(GnssClientEx)} from {device.Id}" ); var telemetryClient = device.GetMicroservice() ?? throw new ArgumentException( $"Unable to load {nameof(TelemetryClientEx)} from {device.Id}" ); var heartbeatClient = device.GetMicroservice() ?? throw new ArgumentException( $"Unable to load {nameof(HeartbeatClient)} from {device.Id}" ); var modeClient = device.GetMicroservice() ?? throw new ArgumentException($"Unable to load {nameof(IModeClient)}"); TakeOff = new ReactiveCommand( async (_, ct) => { using var vm = new SetAltitudeDialogViewModel(_altitudeUnit, loggerFactory); var dialog = new ContentDialog(vm, navigation) { Title = RS.UavWidgetViewModel_SetAltitudeDialog_Title, PrimaryButtonText = RS.SetAltitudeDialogViewModel_ApplyDialog_PrimaryButton_TakeOff, SecondaryButtonText = RS.SetAltitudeDialogViewModel_ApplyDialog_SecondaryButton_Cancel, IsSecondaryButtonEnabled = true, }; var result = await dialog.ShowAsync(); if (result == ContentDialogResult.Primary) { await this.ExecuteCommand( TakeOffCommand.Id, new DoubleArg( _altitudeUnit.CurrentUnitItem.Value.ParseToSi(vm.Altitude.Value) ), ct ); } } ).DisposeItWith(Disposable); Rtl = new BindableAsyncCommand(RTLCommand.Id, this); Land = new BindableAsyncCommand(LandCommand.Id, this); Guided = new BindableAsyncCommand(GuidedModeCommand.Id, this); AutoMode = new BindableAsyncCommand(AutoModeCommand.Id, this); StartMission = new BindableAsyncCommand(StartMissionCommand.Id, this); var altitudeAgl = positionClientEx .Base.GlobalPosition.ObserveOnUIThreadDispatcher() .Select(pld => Math.Truncate((pld?.RelativeAlt ?? double.NaN) / 1000d)); var altitudeMsl = positionClientEx .Base.GlobalPosition.ObserveOnUIThreadDispatcher() .Select(pld => Math.Truncate((pld?.Alt ?? double.NaN) / 1000d)); var velocity = _gnssClient .Main.GroundVelocity.ObserveOnUIThreadDispatcher() .Select(Math.Truncate); var azimuth = positionClientEx .Yaw.ObserveOnUIThreadDispatcher() .Select(d => Math.Round(d, 2)); var heading = positionClientEx.Yaw.ObserveOnUIThreadDispatcher().Select(Math.Truncate); var homeAzimuth = positionClientEx .Current.ObserveOnUIThreadDispatcher() .Where(_ => positionClientEx.Home.CurrentValue.HasValue) .ThrottleLast(TimeSpan.FromMilliseconds(200)) .Select(p => p.Azimuth(positionClientEx.Home.CurrentValue ?? GeoPoint.NaN)); var batteryAmperage = telemetryClient.BatteryCurrent.ObserveOnUIThreadDispatcher(); var batteryCharge = telemetryClient.BatteryCharge.ObserveOnUIThreadDispatcher(); var batteryVoltage = telemetryClient.BatteryVoltage.ObserveOnUIThreadDispatcher(); var batteryConsumed = telemetryClient .BatteryCurrent.ObserveOnUIThreadDispatcher() .Select(d => d == 0 ? double.NaN : Math.Round(d * positionClientEx.ArmedTime.CurrentValue.TotalHours, 2) ); var vdop = _gnssClient .Main.Info.ObserveOnUIThreadDispatcher() .Select(info => info.Vdop ?? double.NaN); var hdop = _gnssClient .Main.Info.Select(info => info.Hdop ?? double.NaN) .ObserveOnUIThreadDispatcher(); var satelliteCount = _gnssClient .Main.Info.Select(info => info.SatellitesVisible) .ObserveOnUIThreadDispatcher(); var rtkMode = _gnssClient .Main.Info.Select(gpsInfo => gpsInfo.FixType) .ObserveOnUIThreadDispatcher(); AltitudeAgl = altitudeAgl.ToReadOnlyBindableReactiveProperty().DisposeItWith(Disposable); AltitudeMsl = altitudeMsl.ToReadOnlyBindableReactiveProperty().DisposeItWith(Disposable); Azimuth = azimuth.ToReadOnlyBindableReactiveProperty().DisposeItWith(Disposable); Heading = heading.ToReadOnlyBindableReactiveProperty().DisposeItWith(Disposable); Velocity = velocity.ToReadOnlyBindableReactiveProperty().DisposeItWith(Disposable); HomeAzimuth = homeAzimuth.ToReadOnlyBindableReactiveProperty().DisposeItWith(Disposable); Roll = positionClientEx.Roll.ToBindableReactiveProperty().DisposeItWith(Disposable); Pitch = positionClientEx.Pitch.ToBindableReactiveProperty().DisposeItWith(Disposable); IsArmed = positionClientEx .IsArmed.DistinctUntilChanged() .ObserveOnUIThreadDispatcher() .Select(b => b) .ToBindableReactiveProperty() .DisposeItWith(Disposable); StatusText = positionClientEx .IsArmed.ObserveOnUIThreadDispatcher() .Select(b => b ? RS.UavWidgetViewModel_StatusText_Armed : RS.UavWidgetViewModel_StatusText_DisArmed ) .ToBindableReactiveProperty() .DisposeItWith(Disposable); VibrationX = new BindableReactiveProperty().DisposeItWith(Disposable); VibrationY = new BindableReactiveProperty().DisposeItWith(Disposable); VibrationZ = new BindableReactiveProperty().DisposeItWith(Disposable); Clipping0 = new BindableReactiveProperty().DisposeItWith(Disposable); Clipping1 = new BindableReactiveProperty().DisposeItWith(Disposable); Clipping2 = new BindableReactiveProperty().DisposeItWith(Disposable); ArmedTime = new BindableReactiveProperty().DisposeItWith(Disposable); Observable .Timer(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(4)) .Subscribe(_ => StatusText.Value = string.Empty) .DisposeItWith(Disposable); heartbeatClient .Link.State.ObserveOnUIThreadDispatcher() .Skip(1) .Subscribe(ChangeLinkStatus) .DisposeItWith(Disposable); CurrentFlightModeRttBox = new SingleRttBoxViewModel( nameof(CurrentFlightModeRttBox), loggerFactory, modeClient.CurrentMode.Select(mode => mode.Name), null ) { Header = RS.UavRttItem_Mode, Icon = MaterialIconKind.FlightMode, UpdateAction = (model, mode) => model.ValueString = mode, Status = DefaultStatusColor, } .SetRoutableParent(this) .DisposeItWith(Disposable); AltitudeRttBox = new TwoColumnRttBoxViewModel( nameof(AltitudeRttBox), loggerFactory, altitudeAgl .CombineLatest( altitudeMsl, _altitudeUnit.CurrentUnitItem, (agl, msl, unit) => new AltitudeRttBoxData(agl, msl, unit) ) .ObserveOnUIThreadDispatcher() .ThrottleLast(TimeSpan.FromMilliseconds(200)), null ) { Header = RS.UavRttItem_Altitude, Icon = MaterialIconKind.Altimeter, UpdateAction = (model, changes) => { model.Left.ValueString = changes.AltitudeUnit.PrintFromSi( changes.AltitudeAgl, "F2" ); model.Right.ValueString = changes.AltitudeUnit.PrintFromSi( changes.AltitudeMsl, "F2" ); model.Left.UnitSymbol = changes.AltitudeUnit.Symbol; model.Right.UnitSymbol = changes.AltitudeUnit.Symbol; CheckSpeedAltitude( changes.AltitudeAgl, Math.Round(_gnssClient.Main.GroundVelocity.CurrentValue) ); }, Status = DefaultStatusColor, } .SetRoutableParent(this) .DisposeItWith(Disposable); AltitudeRttBox.Left.Header = "AGL"; AltitudeRttBox.Right.Header = "MSL"; VelocityRttBox = new SplitDigitRttBoxViewModel( nameof(VelocityRttBox), loggerFactory, unitService, VelocityUnit.Id, velocity, null ) { Header = RS.UavRttItem_Velocity, ShortHeader = "GS", Icon = MaterialIconKind.Speedometer, Status = DefaultStatusColor, FormatString = "F2", } .SetRoutableParent(this) .DisposeItWith(Disposable); AzimuthRttBox = new SplitDigitRttBoxViewModel( nameof(AzimuthRttBox), loggerFactory, unitService, AngleUnit.Id, azimuth, null ) { Header = RS.UavRttItem_Azimuth, Icon = MaterialIconKind.SunAzimuth, Status = DefaultStatusColor, FormatString = "F2", } .SetRoutableParent(this) .DisposeItWith(Disposable); BatteryRttBox = new KeyValueRttBoxViewModel( nameof(BatteryRttBox), loggerFactory, batteryCharge .CombineLatest( batteryAmperage, batteryVoltage, batteryConsumed, _progressUnit.CurrentUnitItem, _amperageUnit.CurrentUnitItem, _capacityUnit.CurrentUnitItem, _voltageUnit.CurrentUnitItem, (bC, bA, bV, bCo, prog, amp, cap, vol) => new BatteryRttBoxData(bC, bA, bV, bCo, prog, amp, cap, vol) ) .ObserveOnUIThreadDispatcher() .ThrottleLast(TimeSpan.FromMilliseconds(200)), null ) { Header = RS.UavRttItem_Battery, Icon = MaterialIconKind.Battery10, UpdateAction = (model, changes) => { model[ 0, RS.UavWidgetViewModel_BatteryRttBox_BatteryCharge_Header, changes.ProgressUnit.Symbol ].ValueString = changes.ProgressUnit.PrintFromSi(changes.Charge, "F2"); model[ 1, RS.UavWidgetViewModel_BatteryRttBox_BatteryAmperage_Header, changes.AmperageUnit.Symbol ].ValueString = changes.AmperageUnit.PrintFromSi(changes.Amperage, "F2"); model[ 2, RS.UavWidgetViewModel_BatteryRttBox_BatteryVoltage_Header, changes.VoltageUnit.Symbol ].ValueString = changes.VoltageUnit.PrintFromSi(changes.Voltage, "F2"); model[ 3, RS.UavWidgetViewModel_BatteryRttBox_BatteryConsumed_Header, changes.CapacityUnit.Symbol ].ValueString = changes.CapacityUnit.PrintFromSi(changes.Consumed, "F2"); ChangeBatteryStatus(changes.Charge); }, Status = DefaultStatusColor, } .SetRoutableParent(this) .DisposeItWith(Disposable); GnssRttBox = new KeyValueRttBoxViewModel( nameof(GnssRttBox), loggerFactory, satelliteCount .CombineLatest( hdop, vdop, rtkMode, (satellites, hdopValue, vdopValue, mode) => new GnssRttBoxData(satellites, hdopValue, vdopValue, mode) ) .ObserveOnUIThreadDispatcher() .ThrottleLast(TimeSpan.FromMilliseconds(200)), null ) { Header = RS.UavRttItem_GNSS, Icon = MaterialIconKind.GpsFixed, UpdateAction = (model, changes) => { model[ 0, RS.UavWidgetViewModel_GnssRttBox_SatellitesCount_Header, null ].ValueString = changes.Sattelites.ToString(); model[1, RS.UavWidgetViewModel_GnssRttBox_Hdop_Header, null].ValueString = changes.HdopCount.ToString("F2"); model[2, RS.UavWidgetViewModel_GnssRttBox_Vdop_Header, null].ValueString = changes.VdopCount.ToString("F2"); model[3, RS.UavWidgetViewModel_GnssRttBox_Mode_Header, null].ValueString = GpsFixTypeToString(changes.Mode); ChangeGnssStatus(changes.Sattelites, changes.Mode); }, Status = DefaultStatusColor, } .SetRoutableParent(this) .DisposeItWith(Disposable); LinkQualityRttBox = new SplitDigitRttBoxViewModel( nameof(LinkQualityRttBox), loggerFactory, unitService, ProgressUnit.Id, heartbeatClient.LinkQuality, null ) { Header = RS.UavRttItem_Link, Icon = MaterialIconKind.Wifi, Status = DefaultStatusColor, FormatString = "F2", } .SetRoutableParent(this) .DisposeItWith(Disposable); } public ReactiveCommand TakeOff { get; } public ICommand AutoMode { get; } public ICommand Rtl { get; } public ICommand Land { get; } public ICommand Guided { get; } public ICommand StartMission { get; } public MissionProgressViewModel MissionProgress { get; } public SingleRttBoxViewModel CurrentFlightModeRttBox { get; } public TwoColumnRttBoxViewModel AltitudeRttBox { get; } public SplitDigitRttBoxViewModel VelocityRttBox { get; } public SplitDigitRttBoxViewModel AzimuthRttBox { get; } public SplitDigitRttBoxViewModel LinkQualityRttBox { get; } public KeyValueRttBoxViewModel BatteryRttBox { get; } public KeyValueRttBoxViewModel GnssRttBox { get; } public BindableReactiveProperty VibrationX { get; } public BindableReactiveProperty VibrationY { get; } public BindableReactiveProperty VibrationZ { get; } public BindableReactiveProperty Clipping0 { get; } public BindableReactiveProperty Clipping1 { get; } public BindableReactiveProperty Clipping2 { get; } public BindableReactiveProperty Roll { get; } public BindableReactiveProperty Pitch { get; } public IReadOnlyBindableReactiveProperty Velocity { get; } public IReadOnlyBindableReactiveProperty AltitudeAgl { get; } public IReadOnlyBindableReactiveProperty AltitudeMsl { get; } public IReadOnlyBindableReactiveProperty Heading { get; } public IReadOnlyBindableReactiveProperty HomeAzimuth { get; } public IReadOnlyBindableReactiveProperty Azimuth { get; } public BindableReactiveProperty StatusText { get; } public BindableReactiveProperty IsArmed { get; } public BindableReactiveProperty ArmedTime { get; } public IClientDevice Device { get; } public override IEnumerable GetChildren() { yield return MissionProgress; yield return CurrentFlightModeRttBox; yield return AltitudeRttBox; yield return VelocityRttBox; yield return AzimuthRttBox; yield return LinkQualityRttBox; yield return BatteryRttBox; yield return GnssRttBox; } private void ChangeBatteryStatus(double percent) { BatteryRttBox.Status = percent switch { > 0.7d => DefaultStatusColor, > 0.5d => AsvColorKind.Warning, > 0.4d => AsvColorKind.Warning | AsvColorKind.Blink, < 0.3d => AsvColorKind.Error | AsvColorKind.Blink, _ => DefaultStatusColor, }; } private void ChangeLinkStatus(LinkState state) { LinkQualityRttBox.Status = state switch { Common.LinkState.Connected => AsvColorKind.Success, Common.LinkState.Downgrade => AsvColorKind.Warning, Common.LinkState.Disconnected => AsvColorKind.Error, _ => AsvColorKind.None, }; } private void CheckSpeedAltitude(double alt, double gs) { if (gs > DangerHighSpeed && alt < CriticalAltitude) { StatusText.Value = RS.UavWidgetViewModel_StatusText_PullUp; AltitudeRttBox.StatusText = RS.UavWidgetViewModel_StatusText_PullUp; AltitudeRttBox.Status = AsvColorKind.Warning | AsvColorKind.Blink; } else { StatusText.Value = string.Empty; AltitudeRttBox.StatusText = string.Empty; AltitudeRttBox.Status = DefaultStatusColor; } } private void ChangeGnssStatus(int satellitesCount, Mavlink.Common.GpsFixType mode) { if (_gnssClient is null) { return; } if ( mode == Mavlink.Common.GpsFixType.GpsFixTypeRtkFloat || satellitesCount > WarningSatelliteAmount.Start.Value || satellitesCount < WarningSatelliteAmount.End.Value ) { GnssRttBox.Status = AsvColorKind.Warning; return; } if ( mode != Mavlink.Common.GpsFixType.GpsFixTypeRtkFixed || _gnssClient.Main.Info.CurrentValue.SatellitesVisible < DangerSatelliteCount ) { GnssRttBox.Status = AsvColorKind.Error; return; } GnssRttBox.Status = DefaultStatusColor; } private string GpsFixTypeToString(Mavlink.Common.GpsFixType type) { return type switch { Mavlink.Common.GpsFixType.GpsFixType2dFix => RS.GpsFixType_GpsFixType2dFix, Mavlink.Common.GpsFixType.GpsFixTypeRtkFloat => RS.GpsFixType_GpsFixTypeRtkFloat, Mavlink.Common.GpsFixType.GpsFixTypeRtkFixed => RS.GpsFixType_GpsFixTypeRtkFixed, Mavlink.Common.GpsFixType.GpsFixTypeDgps => RS.GpsFixType_GpsFixTypeDgps, Mavlink.Common.GpsFixType.GpsFixTypePpp => RS.GpsFixType_GpsFixTypePpp, Mavlink.Common.GpsFixType.GpsFixType3dFix => RS.GpsFixType_GpsFixType3dFix, Mavlink.Common.GpsFixType.GpsFixTypeStatic => RS.GpsFixType_GpsFixTypeStatic, Mavlink.Common.GpsFixType.GpsFixTypeNoGps => RS.GpsFixType_GpsFixTypeNoGps, _ => string.Empty, }; } } ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/Anchors/FlightModeAnchorsExtension.cs ================================================ using Asv.Avalonia; using Asv.Avalonia.IO; using Asv.Common; using Asv.Drones.Api; using Asv.IO; using Asv.Mavlink; using Asv.Modeling; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones; public class FlightModeAnchorsExtension(IDeviceManager conn, ILoggerFactory loggerFactory) : IExtensionFor { public void Extend(IFlightModePage context, CompositeDisposable contextDispose) { conn.Explorer.InitializedDevices.PopulateTo( context.Map.Anchors, TryCreateAnchor, RemoveAnchor ) .DisposeItWith(contextDispose); } private UavAnchor? TryCreateAnchor(IClientDevice device) { var pos = device.GetMicroservice(); return pos != null ? new UavAnchor(device.Id, conn, device, pos, loggerFactory) : null; } private static bool RemoveAnchor(IClientDevice dev, UavAnchor anchor) { return anchor.DeviceId == dev.Id; } } ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/FlightModePageView.axaml ================================================  ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/FlightModePageView.axaml.cs ================================================ using Asv.Avalonia; using Avalonia; using Avalonia.Controls; namespace Asv.Drones; public sealed class FlightModePageViewConfig { public double LeftColumnActualWidth { get; set; } = -1; public double CenterColumnActualWidth { get; set; } = -1; public double RightColumnActualWidth { get; set; } = -1; public double CenterRowActualHeight { get; set; } = -1; public double BottomRowActualHeight { get; set; } = -1; } public partial class FlightModePageView : UserControl { private readonly ILayoutService _layoutService; private FlightModePageViewConfig? _config; private WorkspacePanel? _workspace; public FlightModePageView() : this(NullLayoutService.Instance) { DesignTime.ThrowIfNotDesignMode(); } public FlightModePageView(ILayoutService layoutService) { _layoutService = layoutService; InitializeComponent(); } private void WorkspacePanel_OnAttachedToVisualTree( object? sender, VisualTreeAttachmentEventArgs e ) { if (Design.IsDesignMode) { return; } _workspace = sender as WorkspacePanel; LoadLayout(); } private void OnWorkspaceChanged(object? sender, WorkspaceEventArgs workspaceEventArgs) { if (Design.IsDesignMode) { return; } SaveLayout(workspaceEventArgs); } private void LoadLayout() { _config = _layoutService.Get(this); if (_workspace is null) { return; } if (_config.LeftColumnActualWidth >= 0) { _workspace.LeftWidth = new GridLength(_config.LeftColumnActualWidth); } if (_config.CenterColumnActualWidth >= 0) { _workspace.CentralWidth = new GridLength(_config.CenterColumnActualWidth); } if (_config.RightColumnActualWidth >= 0) { _workspace.RightWidth = new GridLength(_config.RightColumnActualWidth); } if (_config.CenterRowActualHeight >= 0) { _workspace.CentralHeight = new GridLength(_config.CenterRowActualHeight); } if (_config.BottomRowActualHeight >= 0) { _workspace.BottomHeight = new GridLength(_config.BottomRowActualHeight); } } private void SaveLayout(WorkspaceEventArgs workspaceEventArgs) { if (_config is null) { return; } if (DataContext is null) { return; } _config.LeftColumnActualWidth = workspaceEventArgs.LeftColumnActualWidth; _config.CenterColumnActualWidth = workspaceEventArgs.CenterColumnActualWidth; _config.RightColumnActualWidth = workspaceEventArgs.RightColumnActualWidth; _config.CenterRowActualHeight = workspaceEventArgs.CenterRowActualHeight; _config.BottomRowActualHeight = workspaceEventArgs.BottomRowActualHeight; _layoutService.SetInMemory(this, _config); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/FlightModePageViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Avalonia.GeoMap; using Asv.Common; using Asv.Drones.Api; using Asv.Modeling; using Material.Icons; using Microsoft.Extensions.Logging; using ObservableCollections; using R3; namespace Asv.Drones; public sealed class FlightModePageViewModelConfig { public GeoPoint MapCenter { get; set; } = GeoPoint.Zero; public int Zoom { get; set; } = 0; } public class FlightModePageViewModel : PageViewModel, IFlightModePage { public const string PageId = "flight-mode"; public const MaterialIconKind PageIcon = MaterialIconKind.MapSearch; private FlightModePageViewModelConfig? _config; public FlightModePageViewModel() : this( NullMapService.Instance, DesignTime.CommandService, DesignTime.LoggerFactory, DesignTime.DialogService, DesignTime.ExtensionService ) { } public FlightModePageViewModel( IMapService mapService, ICommandService cmd, ILoggerFactory loggerFactory, IDialogService dialogService, IExtensionService ext ) : base(PageId, cmd, loggerFactory, dialogService, ext) { Title = "Flight (BETA)"; Icon = PageIcon; Widgets = []; Widgets.SetRoutableParent(this).DisposeItWith(Disposable); Widgets.DisposeRemovedItems().DisposeItWith(Disposable); Widgets .ObserveAdd() .ObserveOnUIThreadDispatcher() .Subscribe(_ => Widgets.Sort(FlightWidgetsComparer.Instance)) .DisposeItWith(Disposable); WidgetsView = Widgets.ToNotifyCollectionChangedSlim().DisposeItWith(Disposable); Map = new MapViewModel(nameof(Map), loggerFactory, mapService) .SetRoutableParent(this) .DisposeItWith(Disposable); Events.Subscribe(InternalCatchEvent).DisposeItWith(Disposable); } public ObservableList Widgets { get; } public NotifyCollectionChangedSynchronizedViewList WidgetsView { get; } public IMap Map { get; } public override IEnumerable GetChildren() { yield return Map; foreach (var widget in WidgetsView) { yield return widget; } } private ValueTask InternalCatchEvent(IRoutable src, AsyncRoutedEvent e) { switch (e) { case SaveLayoutEvent saveLayoutEvent: if (_config is null) { break; } this.HandleSaveLayout( saveLayoutEvent, _config, cfg => { cfg.MapCenter = Map.CenterMap.Value; cfg.Zoom = Map.Zoom.Value; }, FlushingStrategy.FlushBothViewModelAndView ); break; case LoadLayoutEvent loadLayoutEvent: _config = this.HandleLoadLayout( loadLayoutEvent, cfg => { Map.CenterMap.Value = cfg.MapCenter; Map.Zoom.Value = cfg.Zoom switch { < IZoomService.MinZoomLevel => IZoomService.MinZoomLevel, > IZoomService.MaxZoomLevel => IZoomService.MaxZoomLevel, _ => cfg.Zoom, }; } ); break; } return ValueTask.CompletedTask; } protected override void AfterLoadExtensions() { // nothing to do } } ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/HomePageFlightModeExtension.cs ================================================ using Asv.Avalonia; using Asv.Common; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones; public class HomePageFlightModeExtension(ILoggerFactory loggerFactory) : IExtensionFor { public void Extend(IHomePage context, CompositeDisposable contextDispose) { context.Tools.Add( OpenFlightCommand.StaticInfo.CreateAction(loggerFactory).DisposeItWith(contextDispose) ); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/Widgets/Device/FlightModeClientDeviceWidgetExtension.cs ================================================ using Asv.Avalonia; using Asv.Avalonia.IO; using Asv.Common; using Asv.Drones.Api; using Asv.IO; using Asv.Modeling; using R3; namespace Asv.Drones; public class FlightModeClientDeviceWidgetExtension( IDeviceManager conn, IClientDeviceWidgetFactory factory ) : IExtensionFor { public void Extend(IFlightModePage context, CompositeDisposable contextDispose) { conn.Explorer.InitializedDevices.PopulateTo( context.Widgets, TryCreateWidget, IsRemoveWidget ) .DisposeItWith(contextDispose); } private IFlightWidget? TryCreateWidget(IClientDevice device) { return factory.CreateWidget(in device); } private static bool IsRemoveWidget(IClientDevice model, IFlightWidget vm) { return model.Id.ToString() == vm.Id.Args; } } ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/Widgets/Device/Mavlink/Drone/DroneFlightWidgetViewModel.cs ================================================ using Asv.Avalonia; using Asv.Avalonia.IO; using Asv.Drones.Api; using Asv.Mavlink; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; namespace Asv.Drones; public class DroneFlightWidgetViewModel( IDeviceManager deviceManager, ILoggerFactory loggerFactory, IExtensionService ext ) : DroneFlightWidgetViewModelBase( WidgetId, deviceManager, loggerFactory, ext ), IDroneFlightWidget { public const string WidgetId = "drone"; public DroneFlightWidgetViewModel() : this( NullDeviceManager.Instance, NullLoggerFactory.Instance, NullExtensionService.Instance ) { } protected override void AfterLoadExtensions() { // nothing to do } } ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/Widgets/Device/Mavlink/Drone/DroneWidgetCreationHandler.cs ================================================ using System; using Asv.Drones.Api; using Asv.IO; using Asv.Mavlink; using Microsoft.Extensions.DependencyInjection; namespace Asv.Drones; public class DroneWidgetCreationHandler(IServiceProvider services) : IClientDeviceWidgetCreationHandler { public Type DeviceType => typeof(MavlinkClientDevice); public IFlightWidget? Create(in IClientDevice device) { if (device.GetMicroservice() is not null) { if (device is not MavlinkClientDevice mavlinkDevice) { return null; } var widget = services.GetService(); widget?.InitWith(mavlinkDevice); return widget; } return null; } } ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/Widgets/Device/Mavlink/Drone/Plane/PlaneWidgetCreationHandler.cs ================================================ using System; using Asv.Drones.Api; using Asv.IO; using Asv.Mavlink; using Microsoft.Extensions.DependencyInjection; namespace Asv.Drones.Plane; public class PlaneWidgetCreationHandler(IServiceProvider services) : IClientDeviceWidgetCreationHandler { public Type DeviceType => typeof(ArduPlaneClientDevice); public IFlightWidget? Create(in IClientDevice device) { if (device.GetMicroservice() is not null) { if (device is not ArduPlaneClientDevice plane) { return null; } var widget = services.GetService(); widget?.InitWith(plane); return widget; } return null; } } ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/Widgets/Device/Mavlink/Drone/Plane/PlaneWidgetViewModel.cs ================================================ using Asv.Avalonia; using Asv.Avalonia.IO; using Asv.Drones.Api; using Asv.Mavlink; using Microsoft.Extensions.Logging; namespace Asv.Drones.Plane; public interface IPlaneWidget : IPlaneWidget { } public interface IPlaneWidget : IDroneFlightWidget where TPlane : ArduPlaneClientDevice { } public class PlaneWidgetViewModel : PlaneWidgetViewModelBase, IPlaneWidget { public const string WidgetId = "plane"; public PlaneWidgetViewModel( IDeviceManager deviceManager, ILoggerFactory loggerFactory, IExtensionService ext ) : base(WidgetId, deviceManager, loggerFactory, ext) { } } public abstract class PlaneWidgetViewModelBase : DroneFlightWidgetViewModelBase where TSelf : class, IFlightWidget where TPlane : ArduPlaneClientDevice { protected PlaneWidgetViewModelBase( NavigationId id, IDeviceManager deviceManager, ILoggerFactory loggerFactory, IExtensionService ext ) : base(id, deviceManager, loggerFactory, ext) { } } ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/Widgets/Device/Mavlink/Drone/Plane/Sections/PlaneSectionExtension.cs ================================================ using System; using Asv.Avalonia; using Microsoft.Extensions.DependencyInjection; namespace Asv.Drones.Plane; public class PlaneSectionExtension(IServiceProvider services) : IExtensionFor { public void Extend(IPlaneWidget context, R3.CompositeDisposable contextDispose) { var section = services.GetRequiredKeyedService( PlaneSectionViewModel.SectionId ); context.Sections.Add(section); section.InitWith(context.Device ?? throw new NullReferenceException()); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/Widgets/Device/Mavlink/Drone/Plane/Sections/PlaneSectionView.axaml ================================================  ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/Widgets/Device/Mavlink/Drone/Plane/Sections/PlaneSectionView.axaml.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; namespace Asv.Drones.Plane; public partial class PlaneSectionView : UserControl { public PlaneSectionView() { InitializeComponent(); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/Widgets/Device/Mavlink/Drone/Plane/Sections/PlaneSectionViewModel.cs ================================================ using System.Collections.Generic; using Asv.Avalonia; using Asv.Drones.Api; using Asv.Mavlink; using Microsoft.Extensions.Logging; namespace Asv.Drones.Plane; public class PlaneSectionViewModel : RoutableViewModel, IFlightWidgetSection { public const string SectionId = "plane-widget-section"; public PlaneSectionViewModel() : this(DesignTime.LoggerFactory) { } public PlaneSectionViewModel(ILoggerFactory loggerFactory) : base(SectionId, loggerFactory) { Order = -1; } public string? Type { get; set => SetField(ref field, value); } public string? Text { get; set => SetField(ref field, value); } public void InitWith(ArduPlaneClientDevice context) { Type = context.GetType().Name; Text = context.Name.CurrentValue; } public int Order { get; } public override IEnumerable GetChildren() { return []; } } ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/Widgets/Device/Mavlink/Drone/Sections/AttitudeIndicator/AttitudeIndicatorSectionView.axaml ================================================  ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/Widgets/Device/Mavlink/Drone/Sections/AttitudeIndicator/AttitudeIndicatorSectionView.axaml.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; namespace Asv.Drones; public partial class AttitudeIndicatorSectionView : UserControl { public AttitudeIndicatorSectionView() { InitializeComponent(); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/Widgets/Device/Mavlink/Drone/Sections/AttitudeIndicator/AttitudeIndicatorSectionViewModel.cs ================================================ using System; using System.Collections.Generic; using Asv.Avalonia; using Asv.Common; using Asv.Drones.Api; using Asv.IO; using Asv.Mavlink; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones; public class AttitudeIndicatorSectionViewModel : RoutableViewModel, IFlightWidgetSection { public const string SectionId = "alt-indicator-widget-section"; public AttitudeIndicatorSectionViewModel(ILoggerFactory loggerFactory) : base(SectionId, loggerFactory) { } public IReadOnlyBindableReactiveProperty Roll { get; private set; } public IReadOnlyBindableReactiveProperty Pitch { get; private set; } public IReadOnlyBindableReactiveProperty Heading { get; private set; } public IReadOnlyBindableReactiveProperty HomeAzimuth { get; private set; } public int Order => 0; public void InitWith(MavlinkClientDevice device) { ArgumentNullException.ThrowIfNull(device); var positionClientEx = device.GetMicroservice() ?? throw new ArgumentException( $"Unable to load {nameof(PositionClientEx)} from {device.Id}" ); Roll = positionClientEx.Roll.ToReadOnlyBindableReactiveProperty().DisposeItWith(Disposable); Pitch = positionClientEx .Pitch.ToReadOnlyBindableReactiveProperty() .DisposeItWith(Disposable); Heading = positionClientEx .Yaw.ObserveOnUIThreadDispatcher() .ThrottleLast(TimeSpan.FromMilliseconds(200)) .Select(Math.Truncate) .ToReadOnlyBindableReactiveProperty() .DisposeItWith(Disposable); HomeAzimuth = positionClientEx .Current.ObserveOnUIThreadDispatcher() .Where(_ => positionClientEx.Home.CurrentValue.HasValue) .ThrottleLast(TimeSpan.FromMilliseconds(200)) .Select(p => p.Azimuth(positionClientEx.Home.CurrentValue ?? GeoPoint.NaN)) .ToReadOnlyBindableReactiveProperty() .DisposeItWith(Disposable); } public override IEnumerable GetChildren() { return []; } } ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/Widgets/Device/Mavlink/Drone/Sections/AttitudeIndicator/DroneFlightWidgetExtensionAttitudeIndicatorSection.cs ================================================ using System; using Asv.Avalonia; using Asv.Drones.Api; using Asv.Mavlink; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones; public sealed class DroneFlightWidgetExtensionAttitudeIndicatorSection( ILoggerFactory loggerFactory, IServiceProvider services ) : IExtensionFor { public void Extend(IDroneFlightWidget context, CompositeDisposable contextDispose) { var section = services.GetRequiredKeyedService( AttitudeIndicatorSectionViewModel.SectionId ); context.Sections.Add(section); section.InitWith(context.Device ?? throw new NullReferenceException()); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/Widgets/Device/Mavlink/Drone/Sections/FlightControl/DroneFlightWidgetFlightControlSectionExtension.cs ================================================ using System; using Asv.Avalonia; using Asv.Drones.Api; using Microsoft.Extensions.DependencyInjection; using R3; namespace Asv.Drones; public sealed class DroneFlightWidgetFlightControlSectionExtension(IServiceProvider services) : IExtensionFor { public void Extend(IDroneFlightWidget context, CompositeDisposable contextDispose) { var flightControlViewModel = services.GetRequiredKeyedService( FlightControlSectionViewModel.SectionId ); context.Sections.Add(flightControlViewModel); flightControlViewModel.InitWith(context.Device ?? throw new NullReferenceException()); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/Widgets/Device/Mavlink/Drone/Sections/FlightControl/FlightControlSectionView.axaml ================================================  ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/Widgets/Device/Mavlink/Drone/Sections/FlightControl/FlightControlSectionView.axaml.cs ================================================ using Asv.Avalonia; using Avalonia.Controls; namespace Asv.Drones; public partial class FlightControlSectionView : UserControl { public FlightControlSectionView() { InitializeComponent(); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/Widgets/Device/Mavlink/Drone/Sections/FlightControl/FlightControlSectionViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Windows.Input; using Asv.Avalonia; using Asv.Avalonia.IO; using Asv.Common; using Asv.Drones.Api; using Asv.IO; using Asv.Mavlink; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones; public class FlightControlSectionViewModel : RoutableViewModel, IFlightWidgetSection { public const string SectionId = "flight-control-widget-section"; private readonly IUnit _altitudeUnit; public FlightControlSectionViewModel() : base(SectionId, DesignTime.LoggerFactory) { DesignTime.ThrowIfNotDesignMode(); TakeOff = new ReactiveCommand(); AutoMode = new ReactiveCommand(); Rtl = new ReactiveCommand(); Guided = new ReactiveCommand(); Land = new ReactiveCommand(); InitArgs("1"); } public FlightControlSectionViewModel( INavigationService navigation, IUnitService unitService, ILoggerFactory loggerFactory ) : base(SectionId, loggerFactory) { _altitudeUnit = unitService.Units[AltitudeUnit.Id]; TakeOff = new ReactiveCommand( async (_, ct) => { using var vm = new SetAltitudeDialogViewModel(_altitudeUnit, loggerFactory); var dialog = new ContentDialog(vm, navigation) { Title = RS.UavWidgetViewModel_SetAltitudeDialog_Title, PrimaryButtonText = RS.SetAltitudeDialogViewModel_ApplyDialog_PrimaryButton_TakeOff, SecondaryButtonText = RS.SetAltitudeDialogViewModel_ApplyDialog_SecondaryButton_Cancel, IsSecondaryButtonEnabled = true, }; var result = await dialog.ShowAsync(); if (result == ContentDialogResult.Primary) { await this.ExecuteCommand( TakeOffCommand.Id, new DoubleArg( _altitudeUnit.CurrentUnitItem.Value.ParseToSi(vm.Altitude.Value) ), ct ); } } ).DisposeItWith(Disposable); Rtl = new BindableAsyncCommand(RTLCommand.Id, this); Land = new BindableAsyncCommand(LandCommand.Id, this); Guided = new BindableAsyncCommand(GuidedModeCommand.Id, this); AutoMode = new BindableAsyncCommand(AutoModeCommand.Id, this); } public ReactiveCommand TakeOff { get; } public ICommand AutoMode { get; } public ICommand Rtl { get; } public ICommand Land { get; } public ICommand Guided { get; } public int Order => 1; public IClientDevice Device { get; private set; } public void InitWith(MavlinkClientDevice device) { ArgumentNullException.ThrowIfNull(device); Device = device; InitArgs(device.Id.AsString()); } public override IEnumerable GetChildren() { return []; } } ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/Widgets/Device/Mavlink/Drone/Sections/Telemetry/DroneFlightWidgetTelemetrySectionExtension.cs ================================================ using System; using Asv.Avalonia; using Asv.Drones.Api; using Microsoft.Extensions.DependencyInjection; using R3; namespace Asv.Drones; public sealed class DroneFlightWidgetTelemetrySectionExtension(IServiceProvider services) : IExtensionFor { public void Extend(IDroneFlightWidget context, CompositeDisposable contextDispose) { var vm = services.GetRequiredKeyedService( TelemetrySectionViewModel.SectionId ); context.Sections.Add(vm); vm.InitWith(context.Device ?? throw new NullReferenceException()); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/Widgets/Device/Mavlink/Drone/Sections/Telemetry/TelemetrySectionView.axaml ================================================  ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/Widgets/Device/Mavlink/Drone/Sections/Telemetry/TelemetrySectionView.axaml.cs ================================================ using Asv.Avalonia; using Avalonia.Controls; namespace Asv.Drones; public partial class TelemetrySectionView : UserControl { public TelemetrySectionView() { InitializeComponent(); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/Widgets/Device/Mavlink/Drone/Sections/Telemetry/TelemetrySectionViewModel.cs ================================================ using System; using System.Collections.Generic; using Asv.Avalonia; using Asv.Common; using Asv.Drones.Api; using Asv.IO; using Asv.Mavlink; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones; public class TelemetrySectionViewModel : RoutableViewModel, IFlightWidgetSection { public const string SectionId = "telemetry-widget-sectioin"; private readonly ILoggerFactory _loggerFactory; private readonly IUnitService _unitService; private const AsvColorKind DefaultStatusColor = AsvColorKind.Info5; public TelemetrySectionViewModel() : base(SectionId, DesignTime.LoggerFactory) { DesignTime.ThrowIfNotDesignMode(); InitArgs("1"); } public TelemetrySectionViewModel(ILoggerFactory loggerFactory, IUnitService unitService) : base(SectionId, loggerFactory) { _loggerFactory = loggerFactory; _unitService = unitService; } public AltitudeUavIndicatorViewModel AltitudeUavIndicator { get; private set; } public BatteryUavIndicatorViewModel BatteryUavIndicator { get; private set; } public VelocityUavIndicatorViewModel VelocityUavIndicator { get; private set; } public AngleUavRttIndicatorViewModel AngleUavRttIndicator { get; private set; } public int Order => 2; public void InitWith(MavlinkClientDevice device) { ArgumentNullException.ThrowIfNull(device); InitArgs(device.Id.AsString()); var positionClientEx = device.GetMicroservice() ?? throw new ArgumentException( $"Unable to load {nameof(PositionClientEx)} from {device.Id}" ); var gnssClient = device.GetMicroservice() ?? throw new ArgumentException( $"Unable to load {nameof(GnssClientEx)} from {device.Id}" ); var telemetryClient = device.GetMicroservice() ?? throw new ArgumentException( $"Unable to load {nameof(TelemetryClientEx)} from {device.Id}" ); var heartbeatClient = device.GetMicroservice() ?? throw new ArgumentException( $"Unable to load {nameof(HeartbeatClient)} from {device.Id}" ); var modeClient = device.GetMicroservice() ?? throw new ArgumentException($"Unable to load {nameof(IModeClient)}"); var altitudeAgl = new ReactiveProperty(10).DisposeItWith(Disposable); var altitudeMsl = new ReactiveProperty(14).DisposeItWith(Disposable); var velocity = new ReactiveProperty(199).DisposeItWith(Disposable); var batteryAmperage = new ReactiveProperty(39).DisposeItWith(Disposable); var batteryVoltage = new ReactiveProperty(34).DisposeItWith(Disposable); var batteryCharge = new ReactiveProperty(123).DisposeItWith(Disposable); var batteryConsumed = new ReactiveProperty(39).DisposeItWith(Disposable); var roll = new ReactiveProperty(10).DisposeItWith(Disposable); var pitch = new ReactiveProperty(30).DisposeItWith(Disposable); positionClientEx .Base.GlobalPosition.ObserveOnUIThreadDispatcher() .Select(pld => Math.Truncate((pld?.RelativeAlt ?? double.NaN) / 1000d)) .Subscribe(x => altitudeAgl.Value = x) .DisposeItWith(Disposable); positionClientEx .Base.GlobalPosition.ObserveOnUIThreadDispatcher() .Select(pld => Math.Truncate((pld?.Alt ?? double.NaN) / 1000d)) .Subscribe(x => altitudeMsl.Value = x) .DisposeItWith(Disposable); gnssClient .Main.GroundVelocity.ObserveOnUIThreadDispatcher() .Select(Math.Truncate) .Subscribe(x => velocity.Value = x) .DisposeItWith(Disposable); telemetryClient .BatteryCurrent.ObserveOnUIThreadDispatcher() .Subscribe(x => batteryAmperage.Value = x) .DisposeItWith(Disposable); telemetryClient .BatteryVoltage.ObserveOnUIThreadDispatcher() .Subscribe(x => batteryVoltage.Value = x) .DisposeItWith(Disposable); telemetryClient .BatteryCharge.ObserveOnUIThreadDispatcher() .Subscribe(x => batteryCharge.Value = x) .DisposeItWith(Disposable); telemetryClient .BatteryCurrent.ObserveOnUIThreadDispatcher() .Select(d => d == 0 ? double.NaN : Math.Round(d * positionClientEx.ArmedTime.CurrentValue.TotalHours, 2) ) .Subscribe(x => batteryConsumed.Value = x) .DisposeItWith(Disposable); positionClientEx.Roll.Subscribe(x => roll.Value = x).DisposeItWith(Disposable); positionClientEx.Pitch.Subscribe(x => pitch.Value = x).DisposeItWith(Disposable); var altitudeUnit = _unitService.Units[AltitudeUnit.Id]; var capacityUnit = _unitService.Units[CapacityUnit.Id]; var amperageUnit = _unitService.Units[AmperageUnit.Id]; var voltageUnit = _unitService.Units[VoltageUnit.Id]; var progressUnit = _unitService.Units[ProgressUnit.Id]; var angleUnit = _unitService.Units[AngleUnit.Id]; AltitudeUavIndicator = new AltitudeUavIndicatorViewModel( nameof(AltitudeUavIndicator), _loggerFactory, altitudeAgl, altitudeMsl, altitudeUnit.CurrentUnitItem, DefaultStatusColor ) .SetRoutableParent(this) .DisposeItWith(Disposable); BatteryUavIndicator = new BatteryUavIndicatorViewModel( nameof(BatteryUavIndicator), _loggerFactory, batteryCharge, batteryAmperage, batteryVoltage, batteryConsumed, progressUnit.CurrentUnitItem, amperageUnit.CurrentUnitItem, capacityUnit.CurrentUnitItem, voltageUnit.CurrentUnitItem, DefaultStatusColor ) .SetRoutableParent(this) .DisposeItWith(Disposable); VelocityUavIndicator = new VelocityUavIndicatorViewModel( nameof(VelocityUavIndicator), _loggerFactory, _unitService, velocity, DefaultStatusColor ) .SetRoutableParent(this) .DisposeItWith(Disposable); AngleUavRttIndicator = new AngleUavRttIndicatorViewModel( nameof(AngleUavRttIndicator), _loggerFactory, pitch, roll, angleUnit.CurrentUnitItem, DefaultStatusColor ) .SetRoutableParent(this) .DisposeItWith(Disposable); } public override IEnumerable GetChildren() { yield return AltitudeUavIndicator; yield return BatteryUavIndicator; yield return VelocityUavIndicator; yield return AngleUavRttIndicator; } } ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/Widgets/Test/PluginFlightItemView.axaml ================================================  Plugin exposes some info ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/Widgets/Test/PluginFlightItemView.axaml.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; namespace Asv.Drones; public partial class PluginFlightItemView : UserControl { public PluginFlightItemView() { InitializeComponent(); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/Widgets/Test/PluginFlightItemViewModel.cs ================================================ using Asv.Avalonia; using Asv.Drones.Api; using Asv.IO; using Asv.Mavlink; using Material.Icons; using Microsoft.Extensions.Logging; using ObservableCollections; namespace Asv.Drones; public class PluginFlightItemViewModel : FlightWidgetViewModel // TODO: Remove on new FlightMode release { private const string WidgetId = "test-widget"; public PluginFlightItemViewModel(ILoggerFactory loggerFactory, IExtensionService ext) : base(WidgetId, loggerFactory, ext) { } public override int Order => 1; } ================================================ FILE: src/Asv.Drones/Shell/Pages/FlightMode/Widgets/Test/PluginFlightItemWidgetExtension.cs ================================================ using System; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Avalonia.GeoMap; using Asv.Avalonia.IO; using Asv.Common; using Asv.Drones.Api; using Asv.Mavlink; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones; public class PluginFlightItemWidgetExtension(ILoggerFactory loggerFactory, IExtensionService ext) : IExtensionFor { public void Extend(IFlightModePage context, CompositeDisposable contextDispose) { #if RELEASE return; #endif var vm = new PluginFlightItemViewModel(loggerFactory, ext); vm.Header = "Plugin payload for something"; context.Widgets.Add(vm); Task.Run(async () => { while (!contextDispose.IsDisposed) { if (context.IsDisposed) { return; } await Task.Delay(5000); context.Widgets.Remove(vm); await Task.Delay(5000); context.Widgets.Add(vm); } }); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/PacketViewer/Dialogs/SavePacketMessagesDialogView.axaml ================================================ ================================================ FILE: src/Asv.Drones/Shell/Pages/PacketViewer/Dialogs/SavePacketMessagesDialogView.axaml.cs ================================================ using Asv.Avalonia; using Avalonia.Controls; namespace Asv.Drones; public partial class SavePacketMessagesDialogView : UserControl { public SavePacketMessagesDialogView() { InitializeComponent(); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/PacketViewer/Dialogs/SavePacketMessagesDialogViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Collections.Specialized; using Asv.Avalonia; using Asv.Common; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones; public class SavePacketMessagesDialogViewModel : DialogViewModelBase { public const string ViewModelId = $"{PacketViewerViewModel.PageId}.{BaseId}.separator"; public const string DefaultSeparator = ";"; public const string DefaultShieldSymbol = ","; public SavePacketMessagesDialogViewModel() : this(DesignTime.LoggerFactory) { DesignTime.ThrowIfNotDesignMode(); } public SavePacketMessagesDialogViewModel(ILoggerFactory loggerFactory) : base(ViewModelId, loggerFactory) { IsSemicolon = new BindableReactiveProperty(true).DisposeItWith(Disposable); IsComa = new BindableReactiveProperty(false).DisposeItWith(Disposable); IsTab = new BindableReactiveProperty(false).DisposeItWith(Disposable); Separator = DefaultSeparator; ShieldSymbol = DefaultShieldSymbol; _sub2 = IsSemicolon .Where(value => value) .Subscribe(_ => { Separator = DefaultSeparator; ShieldSymbol = DefaultShieldSymbol; }); _sub3 = IsComa .Where(value => value) .Subscribe(_ => { Separator = ","; ShieldSymbol = ";"; }); _sub4 = IsTab .Where(value => value) .Subscribe(_ => { Separator = "\t"; ShieldSymbol = DefaultShieldSymbol; }); } public BindableReactiveProperty IsSemicolon { get; } public BindableReactiveProperty IsComa { get; } public BindableReactiveProperty IsTab { get; } public string Separator { get; set => SetField(ref field, value); } public string ShieldSymbol { get; set => SetField(ref field, value); } public override void ApplyDialog(ContentDialog dialog) { ArgumentNullException.ThrowIfNull(dialog); _sub5.Disposable = IsValid.Subscribe(isValid => { dialog.IsPrimaryButtonEnabled = isValid; }); } public override IEnumerable GetChildren() => []; #region Dispose private readonly IDisposable _sub2; private readonly IDisposable _sub3; private readonly IDisposable _sub4; private readonly SerialDisposable _sub5 = new(); protected override void Dispose(bool isDisposing) { if (isDisposing) { _sub2.Dispose(); _sub3.Dispose(); _sub4.Dispose(); _sub5.Dispose(); } base.Dispose(isDisposing); } #endregion } ================================================ FILE: src/Asv.Drones/Shell/Pages/PacketViewer/Filters/Comparers/PacketFilterComparerBase.cs ================================================ using System; using System.Collections.Generic; namespace Asv.Drones; public abstract class PacketFilterComparerBase : IEqualityComparer> where TFilter : PacketFilterViewModelBase { public virtual bool Equals( PacketFilterViewModelBase? x, PacketFilterViewModelBase? y ) { if (ReferenceEquals(x, y)) { return true; } if (x is null) { return false; } if (y is null) { return false; } if (x.GetType() != y.GetType()) { return false; } return x.Id.Equals(y.Id) && x.FilterValue.Equals(y.FilterValue) && x.MessageRateText.ModelValue.Value.Equals(y.MessageRateText.ModelValue.Value); } public virtual int GetHashCode(PacketFilterViewModelBase obj) { return HashCode.Combine(obj.Id, obj.FilterValue, obj.MessageRateText.ModelValue); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/PacketViewer/Filters/Comparers/SourcePacketFilterComparer.cs ================================================ namespace Asv.Drones; public class SourcePacketFilterComparer : PacketFilterComparerBase { public static SourcePacketFilterComparer Instance => new(); } ================================================ FILE: src/Asv.Drones/Shell/Pages/PacketViewer/Filters/Comparers/TypePacketFilterComparer.cs ================================================ namespace Asv.Drones; public class TypePacketFilterComparer : PacketFilterComparerBase { public static TypePacketFilterComparer Instance => new(); } ================================================ FILE: src/Asv.Drones/Shell/Pages/PacketViewer/Filters/PacketFilterViewModelBase.cs ================================================ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Common; using Asv.Modeling; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones; public class PacketFilterViewModelBaseConfig { public bool IsChecked { get; set; } = true; } public abstract class PacketFilterViewModelBase : RoutableViewModel where TFilter : PacketFilterViewModelBase { public static string BaseId => $"packet-filter.{typeof(TFilter).Name}"; private const int BaseMovingAverageSize = 3; private readonly IUnit _unit; private readonly ReactiveProperty _isChecked; private readonly ReactiveProperty _messageRate; private readonly IncrementalRateCounter _packetRate = new(BaseMovingAverageSize); private volatile int _cnt; protected PacketFilterViewModelBaseConfig? Config; public abstract string FilterValue { get; } public IReadOnlyBindableReactiveProperty MessageRateTextUnit { get; } public HistoricalUnitProperty MessageRateText { get; } public HistoricalBoolProperty IsChecked { get; } protected PacketFilterViewModelBase( string idArg, IUnitService unitService, ILoggerFactory loggerFactory ) : base(new NavigationId(BaseId, idArg), loggerFactory) { _unit = unitService.Units[FrequencyUnit.Id]; _isChecked = new ReactiveProperty(true).DisposeItWith(Disposable); _messageRate = new ReactiveProperty().DisposeItWith(Disposable); MessageRateText = new HistoricalUnitProperty( nameof(MessageRateText), _messageRate, _unit, loggerFactory, "F1" ) .SetRoutableParent(this) .DisposeItWith(Disposable); IsChecked = new HistoricalBoolProperty(nameof(IsChecked), _isChecked, loggerFactory) .SetRoutableParent(this) .DisposeItWith(Disposable); MessageRateTextUnit = MessageRateText .Unit.CurrentUnitItem.Select(item => item.Symbol) .ToBindableReactiveProperty() .DisposeItWith(Disposable); IncreaseRatesCounterSafe(); Observable .Timer(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1)) .Subscribe(_ => UpdateRateText()) .DisposeItWith(Disposable); Events.Subscribe(InternalCatchEvent).DisposeItWith(Disposable); } public void IncreaseRatesCounterSafe() { Interlocked.Increment(ref _cnt); } public override IEnumerable GetChildren() { yield return IsChecked; } private ValueTask InternalCatchEvent(IRoutable src, AsyncRoutedEvent e) { switch (e) { case SaveLayoutEvent saveLayoutEvent: if (Config is null) { break; } this.HandleSaveLayout( saveLayoutEvent, Config, cfg => cfg.IsChecked = IsChecked.ViewValue.Value ); break; case LoadLayoutEvent loadLayoutEvent: Config = this.HandleLoadLayout( loadLayoutEvent, cfg => IsChecked.ModelValue.Value = cfg.IsChecked ); break; } return ValueTask.CompletedTask; } private void UpdateRateText() { MessageRateText.ModelValue.Value = Math.Round(_packetRate.Calculate(_cnt), 1); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/PacketViewer/Filters/SourcePacketFilterViewModel.cs ================================================ using Asv.Avalonia; using Microsoft.Extensions.Logging; namespace Asv.Drones; public sealed class SourcePacketFilterViewModel( PacketMessageViewModel pkt, IUnitService unitService, ILoggerFactory loggerFactory ) : PacketFilterViewModelBase(pkt.Source, unitService, loggerFactory) { public override string FilterValue { get; } = pkt.Source; } ================================================ FILE: src/Asv.Drones/Shell/Pages/PacketViewer/Filters/TypePacketFilterViewModel.cs ================================================ using Asv.Avalonia; using Microsoft.Extensions.Logging; namespace Asv.Drones; public sealed class TypePacketFilterViewModel( PacketMessageViewModel pkt, IUnitService unitService, ILoggerFactory loggerFactory ) : PacketFilterViewModelBase(pkt.Type, unitService, loggerFactory) { public override string FilterValue { get; } = pkt.Type; } ================================================ FILE: src/Asv.Drones/Shell/Pages/PacketViewer/HomePacketViewerExtension.cs ================================================ using Asv.Avalonia; using Asv.Common; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones; public class HomePacketViewerExtension(ILoggerFactory loggerFactory) : IExtensionFor { public void Extend(IHomePage context, CompositeDisposable contextDispose) { context.Tools.Add( OpenPacketViewerCommand .StaticInfo.CreateAction(loggerFactory) .DisposeItWith(contextDispose) ); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/PacketViewer/PacketConverter/DefaultMavlinkPacketConverter.cs ================================================ using System; using Asv.Drones.Api; using Asv.Mavlink; using Newtonsoft.Json; namespace Asv.Drones; /// /// Default packet converter. Used when there is no specialized converter for some packet type. /// public class DefaultMavlinkPacketConverter : IPacketConverter { public int Order => int.MaxValue; public bool CanConvert(MavlinkMessage packet) { if (packet == null) { throw new ArgumentException("Incoming packet was not initialized!"); } return true; } public string Convert( MavlinkMessage packet, PacketFormatting formatting = PacketFormatting.None ) { if (packet == null) { throw new ArgumentException("Incoming packet was not initialized!"); } if (!CanConvert(packet)) { throw new ArgumentException("Converter can not convert incoming packet!"); } return formatting switch { PacketFormatting.None => JsonConvert.SerializeObject( packet.GetPayload(), Formatting.None ), PacketFormatting.Indented => JsonConvert.SerializeObject( packet.GetPayload(), Formatting.Indented ), _ => throw new ArgumentException("Wrong packet formatting!"), }; } } ================================================ FILE: src/Asv.Drones/Shell/Pages/PacketViewer/PacketMessage/PacketMessageView.axaml ================================================  ================================================ FILE: src/Asv.Drones/Shell/Pages/PacketViewer/PacketMessage/PacketMessageView.axaml.cs ================================================ using Asv.Avalonia; using Avalonia.Controls; namespace Asv.Drones; public partial class PacketMessageView : UserControl { public PacketMessageView() { InitializeComponent(); } } ================================================ FILE: src/Asv.Drones/Shell/Pages/PacketViewer/PacketMessage/PacketMessageViewModel.cs ================================================ using System; using System.Collections.Generic; using Asv.Avalonia; using Asv.Drones.Api; using Asv.Mavlink; using Microsoft.Extensions.Logging; namespace Asv.Drones; public class PacketMessageViewModel : RoutableViewModel { public const string PageId = "packet-message"; public DateTime DateTime { get; } public string Source { get; } public int Size { get; } public string Message { get; } public string Type { get; } public string Description { get; } public bool Highlight { get; set => SetField(ref field, value); } public PacketMessageViewModel() : base(DesignTime.Id, DesignTime.LoggerFactory) { DesignTime.ThrowIfNotDesignMode(); DateTime = DateTime.Now; Source = "[1,1]"; Message = "[1000] information"; Description = "Some description"; Type = "HEARTBEAT"; Size = 10; } public PacketMessageViewModel( MavlinkMessage packet, IPacketConverter converter, ILoggerFactory loggerFactory ) : base( NavigationId.GenerateByHash( packet.SystemId, packet.ComponentId, packet.Sequence, packet.Id ), loggerFactory ) { DateTime = DateTime.Now; Source = $"[{packet.SystemId},{packet.ComponentId}]"; Message = $"[{packet.Sequence:000}] {converter.Convert(packet)}"; Description = converter.Convert(packet, PacketFormatting.Indented); Type = packet.Name; Size = packet.GetByteSize(); } public override IEnumerable GetChildren() { return []; } } ================================================ FILE: src/Asv.Drones/Shell/Pages/PacketViewer/PacketViewerView.axaml ================================================ ================================================ FILE: src/Asv.Drones/Shell/Pages/PacketViewer/PacketViewerView.axaml.cs ================================================ using Asv.Avalonia; using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; namespace Asv.Drones; public sealed class PacketViewerViewConfig { public bool IsSourcesExpanded { get; set; } = true; public bool IsTypesExpanded { get; set; } = true; } public partial class PacketViewerView : UserControl { private readonly ILayoutService _layoutService; private PacketViewerViewConfig? _config; public PacketViewerView() : this(NullLayoutService.Instance) { DesignTime.ThrowIfNotDesignMode(); } public PacketViewerView(ILayoutService layoutService) { _layoutService = layoutService; InitializeComponent(); } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { LoadLayout(); base.OnAttachedToVisualTree(e); } private void Expander_StateChanged(object? sender, RoutedEventArgs e) { if (sender is not Expander exp) { return; } SaveLayout(); } private void LoadLayout() { if (Design.IsDesignMode) { return; } _config = _layoutService.Get(this); SourcesExpander.IsExpanded = _config.IsSourcesExpanded; TypesExpander.IsExpanded = _config.IsTypesExpanded; } private void SaveLayout() { if (Design.IsDesignMode) { return; } if (_config is null) { return; } if (DataContext is null) { return; } if (!HasChanges()) { return; } _config.IsSourcesExpanded = SourcesExpander.IsExpanded; _config.IsTypesExpanded = TypesExpander.IsExpanded; _layoutService.SetInMemory(this, _config); } private bool HasChanges() { if ( _config?.IsSourcesExpanded == SourcesExpander.IsExpanded && _config.IsTypesExpanded == TypesExpander.IsExpanded ) { return false; } return true; } } ================================================ FILE: src/Asv.Drones/Shell/Pages/PacketViewer/PacketViewerViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows.Input; using Asv.Avalonia; using Asv.Avalonia.IO; using Asv.Common; using Asv.Drones.Api; using Asv.IO; using Asv.Mavlink; using Asv.Modeling; using Material.Icons; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using ObservableCollections; using R3; namespace Asv.Drones; public sealed class PacketViewerViewModelConfig { public string SearchText { get; set; } = string.Empty; public bool IsCheckedAllSources { get; set; } = true; public bool IsCheckedAllTypes { get; set; } = true; } public class PacketViewerViewModel : PageViewModel { public const string PageId = "packet-viewer"; public const MaterialIconKind PageIcon = MaterialIconKind.Package; private const int MaxPacketsAmount = 1000; private const int PacketsReceiveDelayInSeconds = 1; private readonly ReactiveProperty _isPaused; private readonly ReactiveProperty _isCheckedAllSources; private readonly ReactiveProperty _isCheckedAllTypes; private readonly IHostEnvironment _app; private readonly ILoggerFactory _loggerFactory; private readonly IUnitService _unit; private readonly IDeviceManager _deviceManager; private readonly INavigationService _navigationService; private readonly IEnumerable _converters; private readonly ObservableFixedSizeRingBuffer _packetsBuffer; private readonly ObservableHashSet _filtersBySourceSet; private readonly ObservableHashSet _filtersByTypeSet; private readonly ReactiveProperty _filterChangeTrigger; private PacketViewerViewModelConfig? _config; public ICommand ClearAll { get; } public ICommand ExportToCsv { get; } public HistoricalBoolProperty IsPaused { get; } public SearchBoxViewModel Search { get; } public HistoricalBoolProperty IsCheckedAllSources { get; } public HistoricalBoolProperty IsCheckedAllTypes { get; } public BindableReactiveProperty SelectedPacket { get; } public INotifyCollectionChangedSynchronizedViewList Packets { get; } public INotifyCollectionChangedSynchronizedViewList FiltersBySource { get; } public INotifyCollectionChangedSynchronizedViewList FiltersByType { get; } public PacketViewerViewModel() : this( DesignTime.CommandService, AppHost.Instance.Services.GetRequiredService(), NullLayoutService.Instance, NullLoggerFactory.Instance, NullUnitService.Instance, [], NullDeviceManager.Instance, DesignTime.Navigation, DesignTime.DialogService, DesignTime.ExtensionService ) { DesignTime.ThrowIfNotDesignMode(); _packetsBuffer.AddLastRange( new[] { new PacketMessageViewModel(), new PacketMessageViewModel(), new PacketMessageViewModel(), } ); } public PacketViewerViewModel( ICommandService cmd, IHostEnvironment app, ILayoutService layoutService, ILoggerFactory loggerFactory, IUnitService unit, IEnumerable converters, IDeviceManager deviceManager, INavigationService navigationService, IDialogService dialogService, IExtensionService ext ) : base(PageId, cmd, loggerFactory, dialogService, ext) { Title = RS.PacketViewerViewModel_Title; _app = app; _unit = unit; _converters = converters; _deviceManager = deviceManager; _navigationService = navigationService; _loggerFactory = loggerFactory; _filterChangeTrigger = new ReactiveProperty(false).DisposeItWith(Disposable); _isPaused = new ReactiveProperty().DisposeItWith(Disposable); _isCheckedAllSources = new ReactiveProperty(true).DisposeItWith(Disposable); _isCheckedAllTypes = new ReactiveProperty(true).DisposeItWith(Disposable); _packetsBuffer = new ObservableFixedSizeRingBuffer( MaxPacketsAmount ); _filtersBySourceSet = new ObservableHashSet( SourcePacketFilterComparer.Instance ); _filtersByTypeSet = new ObservableHashSet( TypePacketFilterComparer.Instance ); _packetsBuffer.SetRoutableParent(this).DisposeItWith(Disposable); _filtersBySourceSet.SetRoutableParent(this).DisposeItWith(Disposable); _filtersByTypeSet.SetRoutableParent(this).DisposeItWith(Disposable); _packetsBuffer.DisposeRemovedItems().DisposeItWith(Disposable); _filtersBySourceSet.DisposeRemovedItems().DisposeItWith(Disposable); _filtersByTypeSet.DisposeRemovedItems().DisposeItWith(Disposable); var packetsView = _packetsBuffer.CreateView(x => x).DisposeItWith(Disposable); Packets = packetsView .ToNotifyCollectionChanged(SynchronizationContextCollectionEventDispatcher.Current) .DisposeItWith(Disposable); FiltersBySource = _filtersBySourceSet .ToNotifyCollectionChanged(SynchronizationContextCollectionEventDispatcher.Current) .DisposeItWith(Disposable); FiltersByType = _filtersByTypeSet .ToNotifyCollectionChanged(SynchronizationContextCollectionEventDispatcher.Current) .DisposeItWith(Disposable); IsPaused = new HistoricalBoolProperty(nameof(IsPaused), _isPaused, loggerFactory) .SetRoutableParent(this) .DisposeItWith(Disposable); Search = new SearchBoxViewModel( nameof(Search), loggerFactory, (_, _, _) => Task.CompletedTask, TimeSpan.FromMilliseconds(500) ) .SetRoutableParent(this) .DisposeItWith(Disposable); IsCheckedAllSources = new HistoricalBoolProperty( nameof(IsCheckedAllSources), _isCheckedAllSources, loggerFactory ) .SetRoutableParent(this) .DisposeItWith(Disposable); IsCheckedAllTypes = new HistoricalBoolProperty( nameof(IsCheckedAllTypes), _isCheckedAllTypes, loggerFactory ) .SetRoutableParent(this) .DisposeItWith(Disposable); SelectedPacket = new BindableReactiveProperty(null).DisposeItWith( Disposable ); ExportToCsv = new BindableAsyncCommand(ExportPacketsToCsvCommand.Id, this); ClearAll = new BindableAsyncCommand(ClearAllPacketsCommand.Id, this); IsPaused.ViewValue.Subscribe(_ => SelectedPacket.Value = null).DisposeItWith(Disposable); IsCheckedAllSources .ViewValue.Subscribe(isChecked => { foreach (var filter in _filtersBySourceSet) { filter.IsChecked.ModelValue.Value = isChecked; } }) .DisposeItWith(Disposable); IsCheckedAllTypes .ViewValue.Subscribe(isChecked => { foreach (var filter in _filtersByTypeSet) { filter.IsChecked.ModelValue.Value = isChecked; } }) .DisposeItWith(Disposable); _packetsBuffer .ObserveAdd() .Subscribe(item => UpdateFilters(item.Value)) .DisposeItWith(Disposable); _deviceManager .Router.OnRxMessage.Where(_ => !IsPaused.ViewValue.Value) .FilterByType() .Chunk(TimeSpan.FromSeconds(PacketsReceiveDelayInSeconds)) .Select(ConvertToPacketMessage) .Subscribe(_packetsBuffer.AddLastRange) .DisposeItWith(Disposable); _deviceManager .Router.OnTxMessage.Where(_ => !IsPaused.ViewValue.Value) .FilterByType() .Chunk(TimeSpan.FromSeconds(PacketsReceiveDelayInSeconds)) .Select(ConvertToPacketMessage) .Subscribe(_packetsBuffer.AddLastRange) .DisposeItWith(Disposable); SelectedPacket .WhereNotNull() .Subscribe(selectedPacket => { foreach (var item in _packetsBuffer) { item.Highlight = item.Type == selectedPacket.Type; } }) .DisposeItWith(Disposable); var viewFilter = CreateSynchronizedViewFilter(); _filtersBySourceSet // TODO: Switch to a special routable event when ready .ObserveAdd() .SubscribeAwait( async (filter, ct) => { await filter.Value.RequestLoadLayout(layoutService, ct); filter .Value.IsChecked.ViewValue.Subscribe(_ => _filterChangeTrigger.Value = !_filterChangeTrigger.Value ) .DisposeItWith(Disposable); } ) .DisposeItWith(Disposable); _filtersByTypeSet // TODO: Switch to a special routable event when ready .ObserveAdd() .SubscribeAwait( async (filter, ct) => { await filter.Value.RequestLoadLayout(layoutService, ct); filter .Value.IsChecked.ViewValue.Subscribe(_ => _filterChangeTrigger.Value = !_filterChangeTrigger.Value ) .DisposeItWith(Disposable); } ) .DisposeItWith(Disposable); Observable .Merge( _filtersBySourceSet.ObserveChanged().Select(_ => Unit.Default), _filtersByTypeSet.ObserveChanged().Select(_ => Unit.Default), Search.Text.ViewValue.Select(_ => Unit.Default), _filterChangeTrigger.Select(_ => Unit.Default) ) .ThrottleLast(TimeSpan.FromMilliseconds(500)) .Subscribe(_ => { var allSourcesSelected = _filtersBySourceSet.All(x => x.IsChecked.ViewValue.Value); var allTypesSelected = _filtersByTypeSet.All(x => x.IsChecked.ViewValue.Value); var hasNoSearchString = string.IsNullOrEmpty(Search.Text.ViewValue.Value); if (hasNoSearchString && allSourcesSelected && allTypesSelected) { packetsView.ResetFilter(); return; } packetsView.AttachFilter(viewFilter); }) .DisposeItWith(Disposable); Events.Subscribe(InternalCatchEvent).DisposeItWith(Disposable); } public override IEnumerable GetChildren() { foreach (var item in _packetsBuffer) { yield return item; } foreach (var item in _filtersBySourceSet) { yield return item; } foreach (var item in _filtersByTypeSet) { yield return item; } yield return IsPaused; yield return IsCheckedAllSources; yield return IsCheckedAllTypes; yield return Search; } internal void ClearAllImpl() { _packetsBuffer.RemoveAll(); } internal async ValueTask ExportToCsvImpl(CancellationToken cancel = default) { cancel.ThrowIfCancellationRequested(); using var vm = new SavePacketMessagesDialogViewModel(_loggerFactory); var dialog = new ContentDialog(vm, _navigationService) { Title = RS.PacketViewerViewModel_SavePacketMessagesDialog_Title, CloseButtonText = RS.PacketViewerViewModel_SavePacketMessagesDialog_CloseButtonText, PrimaryButtonText = RS.PacketViewerViewModel_SavePacketMessagesDialog_PrimaryButtonText, DefaultButton = ContentDialogButton.Primary, }; var res = await dialog.ShowAsync(); if (res != ContentDialogResult.Primary) { return; } var filePath = Path.Join( _app.ContentRootPath, $"packets{DateTime.Now:yyyy-M-d h-mm-ss}.csv" ); try { CsvHelper.SaveToCsv( _packetsBuffer.ToImmutableList(), filePath, vm.Separator, vm.ShieldSymbol, new CsvColumn( RS.PacketMessageViewModel_CsvColumn_Date, x => x.DateTime.ToString("G") ), new CsvColumn( RS.PacketMessageViewModel_CsvColumn_Type, x => x.Type ), new CsvColumn( RS.PacketMessageViewModel_CsvColumn_Source, x => x.Source ), new CsvColumn( RS.PacketMessageViewModel_CsvColumn_Message, x => x.Message ) ); Logger.LogInformation("Export file saved to: {filePath}", filePath); } catch (Exception ex) { Logger.LogError(ex, "Аn error occurred while saving the file: {filePath}", filePath); } } protected override void AfterLoadExtensions() { } private ISynchronizedViewFilter< PacketMessageViewModel, PacketMessageViewModel > CreateSynchronizedViewFilter() { return new SynchronizedViewFilter( (_, packet) => { var hasRequiredType = _filtersByTypeSet.Any(f => f.IsChecked.ViewValue.Value && f.FilterValue == packet.Type ); var hasRequiredSource = _filtersBySourceSet.Any(f => f.IsChecked.ViewValue.Value && f.FilterValue == packet.Source ); if (!hasRequiredSource) { return false; } if (!hasRequiredType) { return false; } if (_filtersBySourceSet.All(x => !x.IsChecked.ViewValue.Value)) { return false; } if (_filtersByTypeSet.All(x => !x.IsChecked.ViewValue.Value)) { return false; } if (string.IsNullOrWhiteSpace(Search.Text.ViewValue.Value)) { return true; } return packet.Message.Contains( Search.Text.ViewValue.Value, StringComparison.OrdinalIgnoreCase ); } ); } private ValueTask InternalCatchEvent(IRoutable src, AsyncRoutedEvent e) { switch (e) { case SaveLayoutEvent saveLayoutEvent: if (_config is null) { break; } this.HandleSaveLayout( saveLayoutEvent, _config, cfg => { cfg.SearchText = Search.Text.ViewValue.Value ?? string.Empty; cfg.IsCheckedAllSources = IsCheckedAllSources.ViewValue.Value; cfg.IsCheckedAllTypes = IsCheckedAllTypes.ViewValue.Value; }, FlushingStrategy.FlushBothViewModelAndView ); break; case LoadLayoutEvent loadLayoutEvent: _config = this.HandleLoadLayout( loadLayoutEvent, cfg => { Search.Text.ModelValue.Value = cfg.SearchText; IsCheckedAllSources.ModelValue.Value = cfg.IsCheckedAllSources; IsCheckedAllTypes.ModelValue.Value = cfg.IsCheckedAllTypes; } ); break; } return ValueTask.CompletedTask; } private IEnumerable ConvertToPacketMessage( IEnumerable messages ) { foreach (var packet in messages) { var converter = _converters.FirstOrDefault(c => c.CanConvert(packet)) ?? new DefaultMavlinkPacketConverter(); var vm = new PacketMessageViewModel(packet, converter, _loggerFactory); yield return vm; } } private void UpdateFilters(PacketMessageViewModel vm) { UpdateSourceFilters(vm); UpdateTypeFilters(vm); } private void UpdateSourceFilters(PacketMessageViewModel vm) { var filter = _filtersBySourceSet.FirstOrDefault(x => x.FilterValue == vm.Source); if (filter is not null) { filter.IncreaseRatesCounterSafe(); return; } var newFilter = new SourcePacketFilterViewModel(vm, _unit, _loggerFactory); var isAdded = _filtersBySourceSet.Add(newFilter); if (!isAdded) { newFilter.Dispose(); } Logger.LogInformation("Added new source filter: {Source}", vm.Source); } private void UpdateTypeFilters(PacketMessageViewModel vm) { var filter = _filtersByTypeSet.FirstOrDefault(x => x.FilterValue == vm.Type); if (filter is not null) { filter.IncreaseRatesCounterSafe(); return; } var newFilter = new TypePacketFilterViewModel(vm, _unit, _loggerFactory); var isAdded = _filtersByTypeSet.Add(newFilter); if (!isAdded) { newFilter.Dispose(); } Logger.LogInformation("Added new type filter: {Type}", vm.Type); } #region Dispose protected override void Dispose(bool disposing) { if (disposing) { _packetsBuffer.RemoveAll(); _filtersBySourceSet.ClearWithItemsDispose(); _filtersByTypeSet.ClearWithItemsDispose(); } base.Dispose(disposing); } #endregion } ================================================ FILE: src/Asv.Drones.Android/Asv.Drones.Android.csproj ================================================ Exe net9.0-android 21 enable me.asv.drones 1 1.0 apk false ../CodeStyle.ruleset CS0169, CS0618, CS1502, CS1503, CS8524, CS8600, CS8601, CS8602, CS8603, CS8604, CS8625, CS8629, CS8762, CA1510, CA1851 Resources\drawable\Icon.png all runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive ================================================ FILE: src/Asv.Drones.Android/MainActivity.cs ================================================ using Android.App; using Android.Content.PM; using Android.OS; using Avalonia; using Avalonia.Android; namespace Asv.Drones.Android; [Activity( Label = "Asv.Drones.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", MainLauncher = true, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode )] public class MainActivity : AvaloniaMainActivity { protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) { // this is required to use the AndroidHttpClientHandler in main thread StrictMode.SetThreadPolicy(new StrictMode.ThreadPolicy.Builder().PermitAll().Build()); return base.CustomizeAppBuilder(builder).WithInterFont(); } } ================================================ FILE: src/Asv.Drones.Android/Properties/AndroidManifest.xml ================================================  ================================================ FILE: src/Asv.Drones.Android/Resources/AboutResources.txt ================================================ Images, layout descriptions, binary blobs and string dictionaries can be included in your application as resource files. Various Android APIs are designed to operate on the resource IDs instead of dealing with images, strings or binary blobs directly. For example, a sample Android app that contains a user interface layout (main.axml), an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png) would keep its resources in the "Resources" directory of the application: Resources/ drawable/ icon.png layout/ main.axml values/ strings.xml In order to get the build system to recognize Android resources, set the build action to "AndroidResource". The native Android APIs do not operate directly with filenames, but instead operate on resource IDs. When you compile an Android application that uses resources, the build system will package the resources for distribution and generate a class called "R" (this is an Android convention) that contains the tokens for each one of the resources included. For example, for the above Resources layout, this is what the R class would expose: public class R { public class drawable { public const int icon = 0x123; } public class layout { public const int main = 0x456; } public class strings { public const int first_string = 0xabc; public const int second_string = 0xbcd; } } You would then use R.drawable.icon to reference the drawable/icon.png file, or R.layout.main to reference the layout/main.axml file, or R.strings.first_string to reference the first string in the dictionary file values/strings.xml. ================================================ FILE: src/Asv.Drones.Android/Resources/drawable/splash_screen.xml ================================================  ================================================ FILE: src/Asv.Drones.Android/Resources/drawable-night-v31/avalonia_anim.xml ================================================ ================================================ FILE: src/Asv.Drones.Android/Resources/drawable-v31/avalonia_anim.xml ================================================ ================================================ FILE: src/Asv.Drones.Android/Resources/values/colors.xml ================================================  #FFFFFF ================================================ FILE: src/Asv.Drones.Android/Resources/values/styles.xml ================================================  ================================================ FILE: src/Asv.Drones.Android/Resources/values-night/colors.xml ================================================  #212121 ================================================ FILE: src/Asv.Drones.Android/Resources/values-v31/styles.xml ================================================  ================================================ FILE: src/Asv.Drones.Api/Asv.Drones.Api.csproj ================================================  $(TargetFramework) $(ApiVersion) $(ApiVersion) $(ApiVersion) https://github.com/asv-soft https://github.com/asv-soft https://github.com/asv-soft true preview enable enable Asv.Drones.Api Asv Soft https://github.com/asv-soft/asv-drones https://github.com/asv-soft/asv-drones/blob/main/LICENSE https://github.com/asv-soft/asv-drones git ../CodeStyle.ruleset CS0169, CS0618, CS1502, CS1503, CS8524, CS8600, CS8601, CS8602, CS8603, CS8604, CS8625, CS8629, CS8762, CA1510, CA1851 all runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive ResXFileCodeGenerator RS.Designer.cs True True RS.resx FlightWidgetView.axaml Code FlightWidgetView.axaml Code ================================================ FILE: src/Asv.Drones.Api/Asv.Drones.Api.csproj.DotSettings ================================================  True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True ================================================ FILE: src/Asv.Drones.Api/Core/Commands/Behaviour/Remove/IRemoveItemCommand.cs ================================================ using Asv.Avalonia; namespace Asv.Drones.Api; public interface IRemoveItemCommand { public const string CommandId = $"{AsyncCommand.BaseId}.remove"; } ================================================ FILE: src/Asv.Drones.Api/Core/Commands/Behaviour/Remove/ISupportRemove.cs ================================================ using Asv.Avalonia; namespace Asv.Drones.Api; public interface ISupportRemove : IRoutable { ValueTask RemoveAsync(CancellationToken ct); } ================================================ FILE: src/Asv.Drones.Api/Core/Commands/Behaviour/Rename/ICommitRenameCommand.cs ================================================ using Asv.Avalonia; namespace Asv.Drones.Api; public interface ICommitRenameCommand { public const string CommandId = $"{AsyncCommand.BaseId}.rename.commit"; } ================================================ FILE: src/Asv.Drones.Api/Core/Commands/Behaviour/Rename/ISupportRename.cs ================================================ using Asv.Avalonia; namespace Asv.Drones.Api; public interface ISupportRename : IRoutable { public const string CommandId = "cmd.rename.commit"; ValueTask RenameAsync(string oldValue, string newValue, CancellationToken ct); } ================================================ FILE: src/Asv.Drones.Api/Core/Commands/Commands.cs ================================================ using Asv.Avalonia; using Microsoft.Extensions.DependencyInjection; namespace Asv.Drones.Api; public static class Commands { private static IFlightModeCommands? _flightMode; private static IMavlinkCommands? _mavlink; public static IMavlinkCommands Mavlink => _mavlink ??= AppHost.Instance.Services.GetRequiredService(); public static IFlightModeCommands FlightMode => _flightMode ??= AppHost.Instance.Services.GetRequiredService(); } ================================================ FILE: src/Asv.Drones.Api/Core/Commands/Flight/IFlightModeCommands.cs ================================================ namespace Asv.Drones.Api; public interface IFlightModeCommands { } ================================================ FILE: src/Asv.Drones.Api/Core/Commands/Mavlink/IMavlinkCommands.cs ================================================ using Asv.Avalonia; using Asv.Mavlink; namespace Asv.Drones.Api; public interface IMavlinkCommands { ICommandInfo WriteParamInfo { get; } ValueTask WriteParam( IRoutable context, string name, MavParamValue value, CancellationToken cancel = default ); ICommandInfo ReadParamInfo { get; } ValueTask ReadParam(IRoutable context, string name, CancellationToken cancel = default); } ================================================ FILE: src/Asv.Drones.Api/Core/Commands/MavlinkMicroserviceCommand.cs ================================================ using Asv.Avalonia; using Asv.Avalonia.IO; using Asv.IO; namespace Asv.Drones.Api; public abstract class MavlinkMicroserviceCommand : ContextCommand where TArg : CommandArg where TMicroservice : class { public override bool CanExecute( IRoutable context, CommandArg parameter, out IRoutable targetContext ) { if (base.CanExecute(context, parameter, out targetContext) == false) { return false; } if (targetContext is IDevicePage devicePage) { return devicePage.Target.CurrentValue?.Device.GetMicroservice() != null; } return false; } public override ValueTask InternalExecute( IDevicePage context, TArg arg, CancellationToken cancel ) { var microservice = context.Target.CurrentValue?.Device.GetMicroservice(); if (microservice == null) { throw new Exception( $"Error to execute command {GetType().Name}[{Info.Id}]: device microservice {nameof(TMicroservice)} not found" ); } return InternalExecute(microservice, arg, cancel); } protected abstract ValueTask InternalExecute( TMicroservice microservice, TArg arg, CancellationToken cancel ); } ================================================ FILE: src/Asv.Drones.Api/Core/Controls/FlightWidget/FlightWidgetView.axaml ================================================  ================================================ FILE: src/Asv.Drones.Api/Core/Controls/FlightWidget/FlightWidgetView.axaml.cs ================================================ using Avalonia.Controls; namespace Asv.Drones.Api; public partial class FlightWidgetView : UserControl { public FlightWidgetView() { InitializeComponent(); } } ================================================ FILE: src/Asv.Drones.Api/Core/Controls/FlightWidget/FlightWidgetViewModel.cs ================================================ using Asv.Avalonia; using Asv.Common; using Asv.Modeling; using Material.Icons; using Microsoft.Extensions.Logging; using ObservableCollections; using R3; namespace Asv.Drones.Api; public abstract class FlightWidgetViewModel( NavigationId id, ILoggerFactory loggerFactory, IExtensionService ext ) : FlightWidgetViewModel(id, loggerFactory, ext), IFlightWidget where TContext : class where TSelf : class, IFlightWidget { public abstract void InitWith(TContext context); } public abstract class FlightWidgetViewModel : ExtendableViewModel, IFlightWidget where TSelf : class, IFlightWidget { protected FlightWidgetViewModel( NavigationId id, ILoggerFactory loggerFactory, IExtensionService ext ) : base(id, loggerFactory, ext) { Menu.SetRoutableParent(this).DisposeItWith(Disposable); Menu.DisposeRemovedItems().DisposeItWith(Disposable); MenuView = new MenuTree(Menu).DisposeItWith(Disposable); Sections = []; Sections.SetRoutableParent(this).DisposeItWith(Disposable); Sections.DisposeRemovedItems().DisposeItWith(Disposable); Sections .ObserveAdd() .ObserveOnUIThreadDispatcher() .Subscribe(_ => Sections.Sort(FlightWidgetSectionsComparer.Instance)) .DisposeItWith(Disposable); SectionsView = Sections.ToNotifyCollectionChangedSlim().DisposeItWith(Disposable); } public MaterialIconKind? Icon { get; set => SetField(ref field, value); } public AsvColorKind IconColor { get; set => SetField(ref field, value); } public string? Header { get; set => SetField(ref field, value); } public WorkspaceDock Position { get; set => SetField(ref field, value); } public bool IsExpanded { get; set => SetField(ref field, value); } public bool CanExpand { get; set => SetField(ref field, value); } public bool IsVisible { get; set => SetField(ref field, value); } = true; public ObservableList Menu { get; } = []; public MenuTree? MenuView { get; } public abstract int Order { get; } public ObservableList Sections { get; } public INotifyCollectionChangedSynchronizedViewList SectionsView { get; } public override IEnumerable GetChildren() { foreach (var item in SectionsView) { yield return item; } foreach (var item in Menu) { yield return item; } } protected override void AfterLoadExtensions() { // nothing to do } } ================================================ FILE: src/Asv.Drones.Api/Core/Controls/FlightWidget/FlightWidgetsComparer.cs ================================================ namespace Asv.Drones.Api; public class FlightWidgetsComparer : IComparer { public static readonly FlightWidgetsComparer Instance = new(); public int Compare(IFlightWidget? x, IFlightWidget? y) { if (x is null && y is null) { return 0; } if (x is null) { return -1; } if (y is null) { return 1; } var typeComparison = CompareWidgetGroups(x, y); if (typeComparison != 0) { return typeComparison; } return x.Order.CompareTo(y.Order); } private static int CompareWidgetGroups(IFlightWidget x, IFlightWidget y) { return StringComparer.Ordinal.Compare(GetGroupKey(x), GetGroupKey(y)); } private static string GetGroupKey(IFlightWidget widget) { return widget.GetType().FullName ?? string.Empty; } } ================================================ FILE: src/Asv.Drones.Api/Core/Controls/FlightWidget/IFlightWidget.cs ================================================ using Asv.Avalonia; using ObservableCollections; namespace Asv.Drones.Api; public interface IFlightWidget : IFlightWidget where TContext : class { void InitWith(TContext context); } public interface IFlightWidget : IWorkspaceWidget { int Order { get; } ObservableList Sections { get; } INotifyCollectionChangedSynchronizedViewList SectionsView { get; } } ================================================ FILE: src/Asv.Drones.Api/Core/Controls/FlightWidget/Section/FlightWidgetSectionsComparer.cs ================================================ namespace Asv.Drones.Api; public class FlightWidgetSectionsComparer : IComparer { public static readonly FlightWidgetSectionsComparer Instance = new(); public int Compare(IFlightWidgetSection? x, IFlightWidgetSection? y) { if (x is null && y is null) { return 0; } if (x is null) { return -1; } if (y is null) { return 1; } return x.Order.CompareTo(y.Order); } } ================================================ FILE: src/Asv.Drones.Api/Core/Controls/FlightWidget/Section/IFlightWidgetSection.cs ================================================ using Asv.Avalonia; namespace Asv.Drones.Api; public interface IAsyncFlightWidgetSection : IFlightWidgetSection where TContext : class { ValueTask InitWithAsync(TContext context, CancellationToken cancel = default); } public interface IFlightWidgetSection : IFlightWidgetSection where TContext : class { void InitWith(TContext context); } public interface IFlightWidgetSection : IRoutable { int Order { get; } } ================================================ FILE: src/Asv.Drones.Api/Core/Controls/MavParam/Button/MavParamButtonView.axaml ================================================  ================================================ FILE: src/Asv.Drones.Api/Core/Controls/MavParam/Button/MavParamButtonView.axaml.cs ================================================ using Asv.Avalonia; using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; namespace Asv.Drones.Api; public partial class MavParamButtonView : UserControl { public MavParamButtonView() { InitializeComponent(); } } ================================================ FILE: src/Asv.Drones.Api/Core/Controls/MavParam/Button/MavParamButtonViewModel.cs ================================================ using Asv.Avalonia; using Asv.Mavlink; using Asv.Mavlink.Common; using Material.Icons; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones.Api; public class MavParamButtonViewModel : MavParamViewModel { public MavParamButtonViewModel() : this( new MavParamInfo( new MavParamTypeMetadata( "A" + NavigationId .GenerateRandomAsString(15) .Replace('.', '_') .Replace('-', '_'), MavParamType.MavParamTypeInt32 ) { Units = "MHz", RebootRequired = false, Volatile = false, MinValue = new MavParamValue(-100), ShortDesc = "Test param", LongDesc = "Long description for test param [icon=power]", Group = "System", Category = "System", MaxValue = new MavParamValue(100), DefaultValue = new MavParamValue(50), Increment = new MavParamValue(1), } ), new Subject(), (_, _) => ValueTask.FromResult(new MavParamValue(100)), DesignTime.LoggerFactory ) { DesignTime.ThrowIfNotDesignMode(); } public MavParamButtonViewModel( MavParamInfo info, Observable update, InitialReadParamDelegate initReadCallback, ILoggerFactory loggerFactory ) : base(info, update, initReadCallback, loggerFactory) { ShowHeader = false; } public void InternalWrite() { Value.Value = 0; Value.Value = 1; Write(); } } ================================================ FILE: src/Asv.Drones.Api/Core/Controls/MavParam/ComboBox/MavParamComboBoxView.axaml ================================================  ================================================ FILE: src/Asv.Drones.Api/Core/Controls/MavParam/ComboBox/MavParamComboBoxView.axaml.cs ================================================ using Asv.Avalonia; using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; namespace Asv.Drones.Api; public partial class MavParamComboBoxView : UserControl { public MavParamComboBoxView() { InitializeComponent(); } } ================================================ FILE: src/Asv.Drones.Api/Core/Controls/MavParam/ComboBox/MavParamComboboxViewModel.cs ================================================ using Asv.Avalonia; using Asv.Common; using Asv.Mavlink; using Asv.Mavlink.Common; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones.Api; public class MavParamComboBoxViewModel : MavParamViewModel { private bool _internalChange; public MavParamComboBoxViewModel() : this( new MavParamInfo( new MavParamTypeMetadata( "A" + NavigationId .GenerateRandomAsString(15) .Replace('.', '_') .Replace('-', '_'), MavParamType.MavParamTypeInt32 ) { Units = "MHz", RebootRequired = false, Volatile = false, MinValue = new MavParamValue(-100), ShortDesc = "Test param", LongDesc = "Long description for test param", Group = "System", Category = "System", MaxValue = new MavParamValue(100), DefaultValue = new MavParamValue(50), Increment = new MavParamValue(1), Values = [ new ValueTuple( 1, "TX1 [icon=numeric-1-box-outline]" ), new ValueTuple( 2, "TX2 [icon=numeric-2-box-outline]" ), ], } ), new Subject(), (_, _) => ValueTask.FromResult(new MavParamValue(100)), DesignTime.LoggerFactory ) { DesignTime.ThrowIfNotDesignMode(); } public MavParamComboBoxViewModel( MavParamInfo info, Observable update, InitialReadParamDelegate initReadCallback, ILoggerFactory loggerFactory ) : base(info, update, initReadCallback, loggerFactory) { Items = info.GetPredefinedValues().ToArray(); Value .Where(_ => _internalChange == false) .Subscribe(x => { _internalChange = true; SelectedItem = Items.FirstOrDefault(i => i.Value.Equals(x)); _internalChange = false; }) .DisposeItWith(Disposable); this.ObservePropertyChanged(x => x.SelectedItem, pushCurrentValueOnSubscribe: false) .Where(_ => _internalChange == false) .Subscribe(x => { if (x == null) { return; } _internalChange = true; Value.Value = x.Value; _internalChange = false; Write(); }) .DisposeItWith(Disposable); } public MavParamValueItem? SelectedItem { get; set => SetField(ref field, value); } public MavParamValueItem[] Items { get; } } ================================================ FILE: src/Asv.Drones.Api/Core/Controls/MavParam/Geopoint/MavParamAltitudeTextBoxView.cs ================================================ using Asv.Avalonia; namespace Asv.Drones.Api; public class MavParamAltitudeTextBoxView : MavParamTextBoxView; ================================================ FILE: src/Asv.Drones.Api/Core/Controls/MavParam/Geopoint/MavParamAltitudeTextBoxViewModel.cs ================================================ using Asv.Avalonia; using Asv.Mavlink; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones.Api; public class MavParamAltitudeTextBoxViewModel : MavParamTextBoxViewModel { private readonly IUnitItem _unit; public MavParamAltitudeTextBoxViewModel( MavParamInfo param, Observable update, InitialReadParamDelegate initReadCallback, ILoggerFactory loggerFactory, IUnitService measureService ) : base(param, update, initReadCallback, loggerFactory) { _unit = measureService[AltitudeUnit.Id]?.CurrentUnitItem.CurrentValue ?? throw new ArgumentNullException(LatitudeUnit.Id); } protected override string ValueToText(ValueType remoteValue) { var meter = MavlinkTypesHelper.AltFromMmToDoubleMeter((int)remoteValue); return _unit.PrintFromSi(meter); } public override string? Units => _unit.Symbol; protected override Exception? TextToValue(string valueAsString, out ValueType value) { var result = _unit.ValidateValue(valueAsString); if (result.IsSuccess == false) { value = Info.DefaultValue; return result.ValidationException; } var degE6 = _unit.ParseToSi(valueAsString); value = MavlinkTypesHelper.AltFromDoubleMeterToInt32Mm(degE6); return null; } } ================================================ FILE: src/Asv.Drones.Api/Core/Controls/MavParam/Geopoint/MavParamLatLonTextBoxView.cs ================================================ using Asv.Avalonia; namespace Asv.Drones.Api; public class MavParamLatLonTextBoxView : MavParamTextBoxView; ================================================ FILE: src/Asv.Drones.Api/Core/Controls/MavParam/Geopoint/MavParamLatLonTextBoxViewModel.cs ================================================ using Asv.Avalonia; using Asv.Mavlink; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones.Api; public class MavParamLatLonTextBoxViewModel : MavParamTextBoxViewModel { private readonly IUnitItem _unit; public MavParamLatLonTextBoxViewModel( MavParamInfo param, Observable update, InitialReadParamDelegate initReadCallback, ILoggerFactory loggerFactory, IUnitService measureService, bool isLatitude ) : base(param, update, initReadCallback, loggerFactory) { _unit = measureService[isLatitude ? LatitudeUnit.Id : LongitudeUnit.Id] ?.CurrentUnitItem .CurrentValue ?? throw new ArgumentNullException(LatitudeUnit.Id); } public override string? Units => null; protected override string ValueToText(ValueType remoteValue) { var degree = MavlinkTypesHelper.LatLonFromInt32E7ToDegDouble((int)remoteValue); return _unit.PrintFromSi(degree); } protected override Exception? TextToValue(string valueAsString, out ValueType value) { var result = _unit.ValidateValue(valueAsString); if (result.IsSuccess == false) { value = Info.DefaultValue; return result.ValidationException; } var degE6 = _unit.ParseToSi(valueAsString); value = MavlinkTypesHelper.LatLonDegDoubleToFromInt32E7To(degE6); return null; } } ================================================ FILE: src/Asv.Drones.Api/Core/Controls/MavParam/MavParamFactory.cs ================================================ using Asv.Avalonia; using Asv.Mavlink; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones.Api; public static class MavParamFactory { public static MavParamViewModel Create( IMavParamTypeMetadata metadata, Observable update, InitialReadParamDelegate initReadCallback, ILoggerFactory loggerFactory, IUnitService measureService ) { var info = new MavParamInfo(metadata); return info.WidgetType switch { MavParamWidgetType.AsciiChars => new MavParamAsciiCharViewModel( info, update, initReadCallback, loggerFactory ), MavParamWidgetType.Altitude => new MavParamAltitudeTextBoxViewModel( info, update, initReadCallback, loggerFactory, measureService ), MavParamWidgetType.Latitude => new MavParamLatLonTextBoxViewModel( info, update, initReadCallback, loggerFactory, measureService, true ), MavParamWidgetType.Longitude => new MavParamLatLonTextBoxViewModel( info, update, initReadCallback, loggerFactory, measureService, false ), MavParamWidgetType.Button => new MavParamButtonViewModel( info, update, initReadCallback, loggerFactory ), MavParamWidgetType.ComboBox => new MavParamComboBoxViewModel( info, update, initReadCallback, loggerFactory ), _ => new MavParamTextBoxViewModel(info, update, initReadCallback, loggerFactory), }; } public static MavParamViewModel Create( IMavParamTypeMetadata param, IParamsClientEx svc, ILoggerFactory loggerFactory, IUnitService measureService ) { return Create( param, svc.Filter(param.Name), svc.GetFromCacheOrReadOnce, loggerFactory, measureService ); } } ================================================ FILE: src/Asv.Drones.Api/Core/Controls/MavParam/MavParamInfo.cs ================================================ using System.Collections.Immutable; using System.Diagnostics; using System.Globalization; using System.Text.RegularExpressions; using Asv.Avalonia; using Asv.Common; using Asv.Mavlink; using Asv.Mavlink.Common; using Material.Icons; namespace Asv.Drones.Api; public enum MavParamWidgetType { TextBox, ComboBox, Button, Latitude, Longitude, Altitude, AsciiChars, } public partial class MavParamInfo { #region Parsing metadata long description private const string LongDescRegexString = @"^(?[^\[\]]*?)(?:\s*\[(?[^\]]*)\])?$"; [GeneratedRegex(LongDescRegexString, RegexOptions.Compiled)] private static partial Regex CreateLongDescRegex(); private static readonly Regex LongDescRegex = CreateLongDescRegex(); private const string MetadataRegexString = @"(?[^\;=]+)=(?[^\;]+)(?:;|$)"; [GeneratedRegex(MetadataRegexString, RegexOptions.Compiled)] private static partial Regex CreateMetadataRegex(); private static readonly Regex MetadataRegex = CreateMetadataRegex(); private static void ParseAdditionalInfo( string? metadataLongDesc, out ImmutableDictionary additionalInfo, out string description ) { description = string.Empty; additionalInfo = ImmutableDictionary.Empty; if (string.IsNullOrWhiteSpace(metadataLongDesc)) { return; } var match = LongDescRegex.Match(metadataLongDesc.Trim()); if (!match.Success) { return; } description = match.Groups["description"].Value.Trim(); var metadataString = match.Groups["metadata"].Value.Trim(); if (string.IsNullOrWhiteSpace(metadataString)) { return; } var metadataMatches = MetadataRegex.Matches(metadataString); var builder = ImmutableDictionary.CreateBuilder(); foreach (Match m in metadataMatches) { if (m.Groups["key"].Success && m.Groups["value"].Success) { builder[m.Groups["key"].Value.Trim()] = m.Groups["value"].Value.Trim(); } } additionalInfo = builder.ToImmutable(); } #endregion #region Well known keys public const string WidgetTypeKey = "widget"; public const string FormatStringKey = "format"; public const string IconKey = "icon"; public const string IconColorKey = "icon-color"; public const string OrderKey = "order"; #endregion private readonly IMavParamTypeMetadata _metadata; private readonly ImmutableDictionary _additionalInfo; private readonly string _description; public MavParamInfo(IMavParamTypeMetadata metadata) { _metadata = metadata; ParseAdditionalInfo(metadata.LongDesc, out _additionalInfo, out _description); } #region AdditionalInfo public ImmutableDictionary AdditionalInfo => _additionalInfo; private static T? GetAdditionalAsEnum(ImmutableDictionary dict, string key) where T : struct { if (dict.TryGetValue(key, out var value)) { value = NormalizeAdditionalValue(value); if (Enum.TryParse(value, true, out var result)) { return result; } } return null; } private static int GetAdditionalAsInt( ImmutableDictionary dict, string key, int defaultValue = 0 ) { if (dict.TryGetValue(key, out var value)) { value = NormalizeAdditionalValue(value); if (int.TryParse(value, out var result)) { return result; } } return defaultValue; } private static double GetAdditionalAsDouble( ImmutableDictionary dict, string key, double defaultValue = 0 ) { if (dict.TryGetValue(key, out var value)) { value = NormalizeAdditionalValue(value).Replace(',', Units.DecimalSeparator); if (double.TryParse(value, NumberStyles.Any, null, out var result)) { return result; } } return defaultValue; } private static string NormalizeAdditionalValue(string value) { return value.Trim().Replace("-", string.Empty); } #endregion public string Description => _description; public IMavParamTypeMetadata Metadata => _metadata; public string Id => _metadata.Name; public ValueType DefaultValue => Convert(Metadata.DefaultValue); public ValueType Max => Convert(Metadata.MaxValue); public ValueType Min => Convert(Metadata.MinValue); public ValueType Increment => Convert(Metadata.Increment); public string? FormatString => CollectionExtensions.GetValueOrDefault(_additionalInfo, FormatStringKey); public MavParamWidgetType WidgetType => GetAdditionalAsEnum(_additionalInfo, WidgetTypeKey) ?? MavParamWidgetType.TextBox; public MaterialIconKind? Icon => GetAdditionalAsEnum(_additionalInfo, IconKey); public AsvColorKind? IconColor => GetAdditionalAsEnum(_additionalInfo, IconColorKey); public int Order => GetAdditionalAsInt(_additionalInfo, OrderKey); public string Title => Metadata.ShortDesc ?? Metadata.Name; public IEnumerable GetPredefinedValues() { if (Metadata.Values == null) { yield break; } foreach (var value in Metadata.Values) { ParseAdditionalInfo(value.Item2, out var dict, out var desc); var icon = GetAdditionalAsEnum(dict, IconKey); var iconColor = GetAdditionalAsEnum(_additionalInfo, IconKey); yield return new MavParamValueItem( icon, iconColor, desc, value.Item1, Convert(value.Item1) ); } } public MavParamValue Convert(ValueType value) { return Metadata.Type switch { MavParamType.MavParamTypeUint8 => new MavParamValue(System.Convert.ToByte(value)), MavParamType.MavParamTypeInt8 => new MavParamValue(System.Convert.ToSByte(value)), MavParamType.MavParamTypeUint16 => new MavParamValue(System.Convert.ToUInt16(value)), MavParamType.MavParamTypeInt16 => new MavParamValue(System.Convert.ToInt16(value)), MavParamType.MavParamTypeUint32 => new MavParamValue(System.Convert.ToUInt32(value)), MavParamType.MavParamTypeInt32 => new MavParamValue(System.Convert.ToInt32(value)), MavParamType.MavParamTypeReal32 => new MavParamValue(System.Convert.ToSingle(value)), _ => throw new ArgumentOutOfRangeException(), }; } public ValueType Convert(MavParamValue value) { Debug.Assert( value.Type == Metadata.Type, $"Value type {value.Type} does not match metadata type {Metadata.Type} for param {Metadata.Name}" ); switch (Metadata.Type) { case MavParamType.MavParamTypeUint8: return (byte)value; case MavParamType.MavParamTypeInt8: return (sbyte)value; case MavParamType.MavParamTypeUint16: return (ushort)value; case MavParamType.MavParamTypeInt16: return (short)value; case MavParamType.MavParamTypeUint32: return (uint)value; case MavParamType.MavParamTypeInt32: return (int)value; case MavParamType.MavParamTypeReal32: return (float)value; case MavParamType.MavParamTypeUint64: case MavParamType.MavParamTypeInt64: case MavParamType.MavParamTypeReal64: default: throw new ArgumentOutOfRangeException(); } } public string? Print(ValueType? value) { if (value == null) { return null; } if (FormatString == null) { return value.ToString(); } switch (Metadata.Type) { case MavParamType.MavParamTypeUint8: return ((byte)value).ToString(FormatString, CultureInfo.InvariantCulture); case MavParamType.MavParamTypeInt8: return ((sbyte)value).ToString(FormatString, CultureInfo.InvariantCulture); case MavParamType.MavParamTypeUint16: return ((ushort)value).ToString(FormatString, CultureInfo.InvariantCulture); case MavParamType.MavParamTypeInt16: return ((short)value).ToString(FormatString, CultureInfo.InvariantCulture); case MavParamType.MavParamTypeUint32: return ((uint)value).ToString(FormatString, CultureInfo.InvariantCulture); case MavParamType.MavParamTypeInt32: return ((int)value).ToString(FormatString, CultureInfo.InvariantCulture); case MavParamType.MavParamTypeReal32: return ((float)value).ToString(FormatString, CultureInfo.InvariantCulture); case MavParamType.MavParamTypeUint64: case MavParamType.MavParamTypeInt64: case MavParamType.MavParamTypeReal64: default: throw new ArgumentOutOfRangeException(); } } public string? GetError(ValueType? value) { if (value is null) { return IsNullOrWhiteSpaceValidationException.Instance.LocalizedMessage ?? IsNullOrWhiteSpaceValidationException.Instance.Message; } switch (Metadata.Type) { case MavParamType.MavParamTypeUint8: var byteVal = System.Convert.ToByte(value); if (byteVal > System.Convert.ToByte(Max) || byteVal < System.Convert.ToByte(Min)) { return ValidationResult .FailAsOutOfRange( Min.ToString() ?? string.Empty, Max.ToString() ?? string.Empty ) .ValidationException?.GetExceptionWithLocalizationOrSelf() .Message; } break; case MavParamType.MavParamTypeInt8: var sbyteVal = System.Convert.ToSByte(value); if ( sbyteVal > System.Convert.ToSByte(Max) || sbyteVal < System.Convert.ToSByte(Min) ) { return ValidationResult .FailAsOutOfRange( Min.ToString() ?? string.Empty, Max.ToString() ?? string.Empty ) .ValidationException?.GetExceptionWithLocalizationOrSelf() .Message; } break; case MavParamType.MavParamTypeUint16: var ushortVal = System.Convert.ToUInt16(value); if ( ushortVal > System.Convert.ToUInt16(Max) || ushortVal < System.Convert.ToUInt16(Min) ) { return ValidationResult .FailAsOutOfRange( Min.ToString() ?? string.Empty, Max.ToString() ?? string.Empty ) .ValidationException?.GetExceptionWithLocalizationOrSelf() .Message; } break; case MavParamType.MavParamTypeInt16: var shortVal = System.Convert.ToInt16(value); if ( shortVal > System.Convert.ToInt16(Max) || shortVal < System.Convert.ToInt16(Min) ) { return ValidationResult .FailAsOutOfRange( Min.ToString() ?? string.Empty, Max.ToString() ?? string.Empty ) .ValidationException?.GetExceptionWithLocalizationOrSelf() .Message; } break; case MavParamType.MavParamTypeUint32: var uintVal = System.Convert.ToUInt32(value); if ( uintVal > System.Convert.ToUInt32(Max) || uintVal < System.Convert.ToUInt32(Min) ) { return ValidationResult .FailAsOutOfRange( Min.ToString() ?? string.Empty, Max.ToString() ?? string.Empty ) .ValidationException?.GetExceptionWithLocalizationOrSelf() .Message; } break; case MavParamType.MavParamTypeInt32: var intVal = System.Convert.ToInt32(value); if (intVal > System.Convert.ToInt32(Max) || intVal < System.Convert.ToInt32(Min)) { return ValidationResult .FailAsOutOfRange( Min.ToString() ?? string.Empty, Max.ToString() ?? string.Empty ) .ValidationException?.GetExceptionWithLocalizationOrSelf() .Message; } break; case MavParamType.MavParamTypeReal32: var floatVal = System.Convert.ToSingle(value); if ( floatVal > System.Convert.ToSingle(Max) || floatVal < System.Convert.ToSingle(Min) ) { return ValidationResult .FailAsOutOfRange( Min.ToString() ?? string.Empty, Max.ToString() ?? string.Empty ) .ValidationException?.GetExceptionWithLocalizationOrSelf() .Message; } break; case MavParamType.MavParamTypeUint64: case MavParamType.MavParamTypeInt64: case MavParamType.MavParamTypeReal64: default: throw new ArgumentOutOfRangeException(); } return null; } public bool IsValid(ValueType? value) { if (value == null) { return false; } switch (Metadata.Type) { case MavParamType.MavParamTypeUint8: var byteVal = System.Convert.ToByte(value); if (byteVal > System.Convert.ToByte(Max) || byteVal < System.Convert.ToByte(Min)) { return false; } break; case MavParamType.MavParamTypeInt8: var sbyteVal = System.Convert.ToSByte(value); if ( sbyteVal > System.Convert.ToSByte(Max) || sbyteVal < System.Convert.ToSByte(Min) ) { return false; } break; case MavParamType.MavParamTypeUint16: var ushortVal = System.Convert.ToUInt16(value); if ( ushortVal > System.Convert.ToUInt16(Max) || ushortVal < System.Convert.ToUInt16(Min) ) { return false; } break; case MavParamType.MavParamTypeInt16: var shortVal = System.Convert.ToInt16(value); if ( shortVal > System.Convert.ToInt16(Max) || shortVal < System.Convert.ToInt16(Min) ) { return false; } break; case MavParamType.MavParamTypeUint32: var uintVal = System.Convert.ToUInt32(value); if ( uintVal > System.Convert.ToUInt32(Max) || uintVal < System.Convert.ToUInt32(Min) ) { return false; } break; case MavParamType.MavParamTypeInt32: var intVal = System.Convert.ToInt32(value); if (intVal > System.Convert.ToInt32(Max) || intVal < System.Convert.ToInt32(Min)) { return false; } break; case MavParamType.MavParamTypeReal32: var floatVal = System.Convert.ToSingle(value); if ( floatVal > System.Convert.ToSingle(Max) || floatVal < System.Convert.ToSingle(Min) ) { return false; } break; case MavParamType.MavParamTypeUint64: case MavParamType.MavParamTypeInt64: case MavParamType.MavParamTypeReal64: default: throw new ArgumentOutOfRangeException(); } return true; } public Exception? ValidateString(string valueAsString, out ValueType value) { value = DefaultValue; if (string.IsNullOrWhiteSpace(valueAsString)) { return IsNullOrWhiteSpaceValidationException.Instance.GetExceptionWithLocalizationOrSelf(); } valueAsString = valueAsString .Replace(',', Units.DecimalSeparator) .Trim(' ') .Replace(" ", string.Empty); if (valueAsString.Length == 0) { return IsNullOrWhiteSpaceValidationException.Instance.GetExceptionWithLocalizationOrSelf(); } var lastChar = valueAsString[^1]; int multiply; switch (lastChar) { case 'M' or 'm' or 'М' or 'м': multiply = 1_000_000; valueAsString = valueAsString[..^1]; break; case 'K' or 'k' or 'К' or 'к': multiply = 1_000; valueAsString = valueAsString[..^1]; break; case 'G' or 'g' or 'Г' or 'г': multiply = 1_000_000_000; valueAsString = valueAsString[..^1]; break; default: multiply = 1; break; } if ( double.TryParse( valueAsString, NumberStyles.Any, CultureInfo.InvariantCulture, out var doubleValue ) ) { doubleValue *= multiply; } else { return new Exception(RS.MavParamInfo_ValidationException_InvalidFormat); } switch (Metadata.Type) { case MavParamType.MavParamTypeUint8: value = (byte)doubleValue; break; case MavParamType.MavParamTypeInt8: value = (sbyte)doubleValue; break; case MavParamType.MavParamTypeUint16: value = (ushort)doubleValue; break; case MavParamType.MavParamTypeInt16: value = (short)doubleValue; break; case MavParamType.MavParamTypeUint32: value = (uint)doubleValue; break; case MavParamType.MavParamTypeInt32: value = (int)doubleValue; break; case MavParamType.MavParamTypeReal32: value = (float)doubleValue; break; case MavParamType.MavParamTypeReal64: case MavParamType.MavParamTypeUint64: case MavParamType.MavParamTypeInt64: default: throw new ArgumentOutOfRangeException(); } if (!IsValid(value)) { return new Exception(GetError(value)); } return null; } } public class MavParamValueItem( MaterialIconKind? icon, AsvColorKind? iconColor, string title, MavParamValue mavlinkValue, ValueType value ) { public MaterialIconKind? Icon => icon; public AsvColorKind IconColor => iconColor ?? AsvColorKind.None; public string Title => title; public MavParamValue MavValue => mavlinkValue; public ValueType Value => value; } ================================================ FILE: src/Asv.Drones.Api/Core/Controls/MavParam/MavParamViewModel.cs ================================================ using System.Collections.Specialized; using System.Diagnostics; using System.Globalization; using Asv.Avalonia; using Asv.Common; using Asv.Mavlink; using Asv.Mavlink.Common; using Material.Icons; using Microsoft.Extensions.Logging; using R3; using ZLogger; namespace Asv.Drones.Api; public delegate ValueTask InitialReadParamDelegate( string paramName, CancellationToken cancel ); public class MavParamViewModel : RoutableViewModel, ISupportRefresh, ISupportCancel, IComparable, IComparable { protected MavParamViewModel( MavParamInfo metadata, Observable update, InitialReadParamDelegate initReadCallback, ILoggerFactory loggerFactory ) : base(metadata.Id, loggerFactory) { Info = metadata; update .ObserveOnCurrentSynchronizationContext() .Subscribe(InternalOnRemoteChanged) .DisposeItWith(Disposable); Value = new BindableReactiveProperty(metadata.DefaultValue).DisposeItWith( Disposable ); Value .Where(_ => IsRemoteChange == false) .Subscribe(_ => IsSync = false) .DisposeItWith(Disposable); // this is random delay for initial read to avoid many requests at the same time Observable .Timer(TimeSpan.FromMilliseconds(Random.Shared.Next(1, 1000))) .Take(1) .Subscribe(initReadCallback, (_, callback) => Init(callback)) .DisposeItWith(Disposable); } public MavParamInfo Info { get; } private async void Init(InitialReadParamDelegate callback) { try { var value = await callback(Info.Metadata.Name, DisposeCancel); InternalOnRemoteChanged(value); } catch (Exception e) { Logger.ZLogError( e, $"Failed to read initial value for param {Info.Metadata.Name}:{e.Message}" ); IsNetworkError = true; NetworkErrorMessage = e.Message; } } private void InternalOnRemoteChanged(MavParamValue value) { if (IsInEditMode) { return; } IsRemoteChange = true; Value.OnNext(Info.Convert(value)); IsSync = true; IsNetworkError = false; IsRemoteChange = false; } public void ResetToDefault() { Value.Value = Info.DefaultValue; Write(); } public async void Refresh() { try { IsBusy = true; IsNetworkError = false; NetworkErrorMessage = null; IsInEditMode = false; await Api.Commands.Mavlink.ReadParam(this, Info.Metadata.Name); } catch (Exception e) { IsNetworkError = true; NetworkErrorMessage = e.Message; } finally { IsBusy = false; } } public async void Write() { if (HasValidationErrors) { return; } var lastValue = IsInEditMode; try { NetworkErrorMessage = null; IsNetworkError = false; IsBusy = true; await Commands.Mavlink.WriteParam(this, Info.Metadata.Name, Info.Convert(Value.Value)); } catch (Exception e) { IsNetworkError = true; NetworkErrorMessage = e.Message; } finally { IsInEditMode = lastValue; IsBusy = false; } } public bool HasValidationErrors { get; set => SetField(ref field, value); } public bool IsFocused { get; set => SetField(ref field, value); } public bool IsNetworkError { get; set => SetField(ref field, value); } public string? NetworkErrorMessage { get; set => SetField(ref field, value); } public bool IsSync { get; set => SetField(ref field, value); } = true; public BindableReactiveProperty Value { get; } public bool IsRemoteChange { get; set => SetField(ref field, value); } public bool IsInEditMode { get; set => SetField(ref field, value); } public bool IsBusy { get; set => SetField(ref field, value); } public bool ShowHeader { get; set => SetField(ref field, value); } = true; public bool IsVisible { get; set => SetField(ref field, value); } = true; public override IEnumerable GetChildren() { yield break; } public void Cancel() { } public int CompareTo(MavParamViewModel? other) { if (ReferenceEquals(this, other)) { return 0; } if (other is null) { return 1; } return Info.Order.CompareTo(other.Info.Order); } public int CompareTo(object? obj) { if (obj is null) { return 1; } if (ReferenceEquals(this, obj)) { return 0; } return obj is MavParamViewModel other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(MavParamViewModel)}"); } public static bool operator <(MavParamViewModel? left, MavParamViewModel? right) { return Comparer.Default.Compare(left, right) < 0; } public static bool operator >(MavParamViewModel? left, MavParamViewModel? right) { return Comparer.Default.Compare(left, right) > 0; } public static bool operator <=(MavParamViewModel? left, MavParamViewModel? right) { return Comparer.Default.Compare(left, right) <= 0; } public static bool operator >=(MavParamViewModel? left, MavParamViewModel? right) { return Comparer.Default.Compare(left, right) >= 0; } } ================================================ FILE: src/Asv.Drones.Api/Core/Controls/MavParam/String/MavParamAsciiCharView.cs ================================================ using Asv.Avalonia; namespace Asv.Drones.Api; public class MavParamAsciiCharView : MavParamTextBoxView; ================================================ FILE: src/Asv.Drones.Api/Core/Controls/MavParam/String/MavParamAsciiCharViewModel.cs ================================================ using System.Buffers.Binary; using System.Text; using Asv.Mavlink; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones.Api; public class MavParamAsciiCharViewModel : MavParamTextBoxViewModel { public MavParamAsciiCharViewModel( MavParamInfo param, Observable update, InitialReadParamDelegate initReadCallback, ILoggerFactory loggerFactory ) : base(param, update, initReadCallback, loggerFactory) { } protected override string ValueToText(ValueType remoteValue) { Span raw = stackalloc byte[4]; BinaryPrimitives.WriteInt32BigEndian(raw, (int)remoteValue); var sb = new StringBuilder(4); foreach (char c in raw) { if (!char.IsControl(c) && !char.IsWhiteSpace(c) && char.IsLetterOrDigit(c)) { sb.Append(c); } } return sb.ToString(); } protected override Exception? TextToValue(string valueAsString, out ValueType value) { if (string.IsNullOrWhiteSpace(valueAsString)) { value = 0; return null; } var filtered = string.Concat( valueAsString.Where(c => !char.IsControl(c) && !char.IsWhiteSpace(c) && char.IsLetterOrDigit(c) ) ); if (filtered.Length == 0) { value = 0; return null; } if (filtered.Length > 4) { filtered = filtered.Substring(0, 4); } Span buffer = stackalloc byte[4]; Encoding.ASCII.GetBytes(filtered, buffer); value = BinaryPrimitives.ReadInt32BigEndian(buffer); return null; } } ================================================ FILE: src/Asv.Drones.Api/Core/Controls/MavParam/TextBox/MavParamTextBoxView.axaml ================================================  ================================================ FILE: src/Asv.Drones.Api/Core/Controls/MavParam/TextBox/MavParamTextBoxView.axaml.cs ================================================ using Asv.Avalonia; using Avalonia.Controls; using Avalonia.Input; using R3; namespace Asv.Drones.Api; public partial class MavParamTextBoxView : UserControl { public MavParamTextBoxView() { InitializeComponent(); } private void PART_TextBox_OnGotFocus(object? sender, GotFocusEventArgs e) { if (sender is TextBox textBox) { Observable .TimerFrame(1) .Subscribe(x => { textBox.SelectAll(); }); } } } ================================================ FILE: src/Asv.Drones.Api/Core/Controls/MavParam/TextBox/MavParamTextBoxViewModel.cs ================================================ using System.ComponentModel; using System.Globalization; using Asv.Avalonia; using Asv.Common; using Asv.IO; using Asv.Mavlink; using Asv.Mavlink.Common; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones.Api; public class MavParamTextBoxViewModel : MavParamViewModel { private readonly BindableReactiveProperty _textValue; private bool _internalChange; public MavParamTextBoxViewModel() : this( new MavParamInfo( new MavParamTypeMetadata( "A" + NavigationId .GenerateRandomAsString(15) .Replace('.', '_') .Replace('-', '_'), MavParamType.MavParamTypeInt32 ) { Units = "MHz", RebootRequired = false, Volatile = false, MinValue = new MavParamValue(-100), ShortDesc = "Test param", LongDesc = "Long description for test param", Group = "System", Category = "System", MaxValue = new MavParamValue(100), DefaultValue = new MavParamValue(50), Increment = new MavParamValue(1), } ), new Subject(), (_, _) => ValueTask.FromResult(new MavParamValue(100)), DesignTime.LoggerFactory ) { DesignTime.ThrowIfNotDesignMode(); Task.Run(async () => { while (true) { IsSync = false; await Task.Delay(3000); IsNetworkError = true; NetworkErrorMessage = "Network error occurred. Please try again later."; await Task.Delay(5000); IsNetworkError = false; IsBusy = true; await Task.Delay(3000); IsBusy = false; IsRemoteChange = true; await Task.Delay(1000); IsRemoteChange = false; _textValue.Value = "asdasdasd"; await Task.Delay(3000); _textValue.Value = "123450"; } }); } public MavParamTextBoxViewModel( MavParamInfo param, Observable update, InitialReadParamDelegate initReadCallback, ILoggerFactory loggerFactory ) : base(param, update, initReadCallback, loggerFactory) { _textValue = new BindableReactiveProperty().DisposeItWith(Disposable); _internalChange = true; Value .Where(_ => _internalChange == false) .Subscribe(x => _textValue.Value = ValueToText(x)) .DisposeItWith(Disposable); // we don't subscribe to value changes here, because we set Value at Validator TextValue.EnableValidation(Validator).DisposeItWith(Disposable); Observable .FromEventHandler( h => _textValue.ErrorsChanged += h, h => _textValue.ErrorsChanged -= h ) .Subscribe(_ => HasValidationErrors = _textValue.HasErrors) .DisposeItWith(Disposable); _textValue.DistinctUntilChanged().Subscribe(_ => IsSync = false).DisposeItWith(Disposable); _internalChange = false; } public virtual string? Units => Info.Metadata.Units; protected virtual string ValueToText(ValueType remoteValue) { return Info.Print(remoteValue) ?? string.Empty; } protected virtual Exception? TextToValue(string valueAsString, out ValueType value) { return Info.ValidateString(valueAsString, out value); } private Exception? Validator(string valueAsString) { var err = TextToValue(valueAsString, out var value); if (err != null) { return err; } _internalChange = true; Value.Value = value; _internalChange = false; return null; } public IReadOnlyBindableReactiveProperty TextValue => _textValue; } ================================================ FILE: src/Asv.Drones.Api/Core/Services/ClientDeviceWidgetFactory/IClientDeviceWidgetCreationHandler.cs ================================================ using Asv.IO; namespace Asv.Drones.Api; public interface IClientDeviceWidgetCreationHandler { Type DeviceType { get; } IFlightWidget? Create(in IClientDevice device); } ================================================ FILE: src/Asv.Drones.Api/Core/Services/ClientDeviceWidgetFactory/IClientDeviceWidgetFactory.cs ================================================ using Asv.IO; namespace Asv.Drones.Api; public interface IClientDeviceWidgetFactory { public IFlightWidget? CreateWidget(in IClientDevice device); } ================================================ FILE: src/Asv.Drones.Api/Core/Services/Converters/IPacketConverter.cs ================================================ using Asv.Mavlink; namespace Asv.Drones.Api; /// /// Represents the formatting options for packet data. /// public enum PacketFormatting { /// /// One-line formatting /// None, /// /// Represents the formatting options for packet content. /// Indented, } /// /// Represents an interface for converting packet payloads to string representation. /// public interface IPacketConverter { /// /// Gets the order of the converter in the list of all converters. /// int Order { get; } /// /// Checks whether the converter can convert the payload of a given packet. /// /// The packet to convert /// Returns true if the converter can convert the payload, false otherwise bool CanConvert(MavlinkMessage packet); /// /// Converts packet's payload to string. /// /// The packet to convert. /// /// The formatting of the result string. This is optional and is used to create packets with special formatting. The default value is 'None'. /// /// /// A string representation of the packet. /// string Convert(MavlinkMessage packet, PacketFormatting formatting = PacketFormatting.None); } ================================================ FILE: src/Asv.Drones.Api/Core/Services/Devices/Mavlink/IMavlinkHost.cs ================================================ using System.Collections.Immutable; using Asv.IO; using Asv.Mavlink; namespace Asv.Drones.Api; public interface IMavlinkMessagesExtension { void Extend(ImmutableDictionary>.Builder builder); } public interface IMavlinkHost { IProtocolMessageFactory MessageFactory { get; } IMavlinkContext Context { get; } MavlinkIdentity Identity { get; } IHeartbeatServer? Heartbeat { get; } } ================================================ FILE: src/Asv.Drones.Api/RS.Designer.cs ================================================ //------------------------------------------------------------------------------ // // This code was generated by a tool. // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // //------------------------------------------------------------------------------ namespace Asv.Drones.Api { using System; /// /// A strongly-typed resource class, for looking up localized strings, etc. /// // This class was auto-generated by the StronglyTypedResourceBuilder // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class RS { private static global::System.Resources.ResourceManager resourceMan; private static global::System.Globalization.CultureInfo resourceCulture; [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal RS() { } /// /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Asv.Drones.Api.RS", typeof(RS).Assembly); resourceMan = temp; } return resourceMan; } } /// /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } set { resourceCulture = value; } } /// /// Looks up a localized string similar to Value must be a number with optional suffix (K, M, G). /// internal static string MavParamInfo_ValidationException_InvalidFormat { get { return ResourceManager.GetString("MavParamInfo_ValidationException_InvalidFormat", resourceCulture); } } } } ================================================ FILE: src/Asv.Drones.Api/RS.resx ================================================  text/microsoft-resx 1.3 System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Value must be a number with optional suffix (K, M, G) ================================================ FILE: src/Asv.Drones.Api/RS.ru.resx ================================================  text/microsoft-resx 1.3 System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Значение должно быть числом с опциональным суффиксом (K, M, G) ================================================ FILE: src/Asv.Drones.Api/Shell/Pages/FileBrowser/IFileBrowserViewModel.cs ================================================ using Asv.Avalonia.IO; namespace Asv.Drones.Api; public interface IFileBrowserViewModel : IDevicePage { } ================================================ FILE: src/Asv.Drones.Api/Shell/Pages/Flight/FlightMode.cs ================================================ using Material.Icons; namespace Asv.Drones.Api; public static class FlightMode { public static MaterialIconKind PageIcon { get; set; } = MaterialIconKind.MapSearch; public const string PageId = "flight"; } ================================================ FILE: src/Asv.Drones.Api/Shell/Pages/Flight/IFlightMode.cs ================================================ using Asv.Avalonia; using Asv.Avalonia.GeoMap; using ObservableCollections; using R3; namespace Asv.Drones.Api; public interface IFlightMode : IPage { ObservableList Widgets { get; } IMap MapViewModel { get; } } ================================================ FILE: src/Asv.Drones.Api/Shell/Pages/Flight/Widgets/UavWidget/IUavFlightWidget.cs ================================================ using Asv.Avalonia; using Asv.IO; namespace Asv.Drones.Api; public interface IUavFlightWidget : IWorkspaceWidget { IClientDevice Device { get; } } ================================================ FILE: src/Asv.Drones.Api/Shell/Pages/FlightMode/IFlightModePage.cs ================================================ using Asv.Avalonia; using Asv.Avalonia.GeoMap; using ObservableCollections; namespace Asv.Drones.Api; public interface IFlightModePage : IPage { ObservableList Widgets { get; } IMap Map { get; } } ================================================ FILE: src/Asv.Drones.Api/Shell/Pages/FlightMode/Widgets/Device/DeviceFlightWidgetViewModelBase.cs ================================================ using Asv.Avalonia; using Asv.Avalonia.IO; using Asv.Common; using Asv.IO; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones.Api; public abstract class DeviceFlightWidgetViewModelBase : FlightWidgetViewModel where TDeviceContext : class, IClientDevice where TSelf : class, IFlightWidget { private readonly IDeviceManager _deviceManager; protected DeviceFlightWidgetViewModelBase( NavigationId id, IDeviceManager deviceManager, ILoggerFactory loggerFactory, IExtensionService ext ) : base(id, loggerFactory, ext) { ArgumentNullException.ThrowIfNull(deviceManager); _deviceManager = deviceManager; Position = WorkspaceDock.Left; } public TDeviceContext? Device { get; private set; } public override void InitWith(TDeviceContext device) { ArgumentNullException.ThrowIfNull(device); InitArgs(device.Id.AsString()); Device = device; Header = device.Id.ToString(); Icon = _deviceManager.GetIcon(device.Id); IconColor = _deviceManager.GetDeviceColor(device.Id); device .Name.ObserveOnUIThreadDispatcher() .Subscribe(x => Header = x) .DisposeItWith(Disposable); } } ================================================ FILE: src/Asv.Drones.Api/Shell/Pages/FlightMode/Widgets/Device/IDeviceFlightWidget.cs ================================================ using Asv.IO; namespace Asv.Drones.Api; public interface IDeviceFlightWidget : IFlightWidget where TDeviceContext : class, IClientDevice { public TDeviceContext? Device { get; } } ================================================ FILE: src/Asv.Drones.Api/Shell/Pages/FlightMode/Widgets/Device/Mavlink/Drone/DroneFlightWidgetViewModelBase.cs ================================================ using Asv.Avalonia; using Asv.Avalonia.IO; using Asv.Mavlink; using Microsoft.Extensions.Logging; namespace Asv.Drones.Api; public class DroneFlightWidgetViewModelBase( NavigationId id, IDeviceManager deviceManager, ILoggerFactory loggerFactory, IExtensionService ext ) : MavlinkDeviceFlightWidgetViewModelBase(id, deviceManager, loggerFactory, ext) where TSelf : class, IFlightWidget where TDrone : MavlinkClientDevice { } ================================================ FILE: src/Asv.Drones.Api/Shell/Pages/FlightMode/Widgets/Device/Mavlink/Drone/IDroneFlightWidget.cs ================================================ using Asv.Mavlink; namespace Asv.Drones.Api; public interface IDroneFlightWidget : IDroneFlightWidget { } public interface IDroneFlightWidget : IMavlinkDeviceFlightWidget where TDrone : MavlinkClientDevice { } ================================================ FILE: src/Asv.Drones.Api/Shell/Pages/FlightMode/Widgets/Device/Mavlink/IMavlinkDeviceFlightWidget.cs ================================================ using Asv.Mavlink; namespace Asv.Drones.Api; public interface IMavlinkDeviceFlightWidget : IMavlinkDeviceFlightWidget { } public interface IMavlinkDeviceFlightWidget : IDeviceFlightWidget where TMavlinkClientDevice : MavlinkClientDevice { } ================================================ FILE: src/Asv.Drones.Api/Shell/Pages/FlightMode/Widgets/Device/Mavlink/MavlinkDeviceFlightWidgetViewModelBase.cs ================================================ using Asv.Avalonia; using Asv.Avalonia.IO; using Asv.Mavlink; using Microsoft.Extensions.Logging; namespace Asv.Drones.Api; public abstract class MavlinkDeviceFlightWidgetViewModelBase( NavigationId id, IDeviceManager deviceManager, ILoggerFactory loggerFactory, IExtensionService ext ) : DeviceFlightWidgetViewModelBase( id, deviceManager, loggerFactory, ext ) where TSelf : class, IFlightWidget where TMavlinkClientDevice : MavlinkClientDevice { private int _order; public override int Order => _order; public override void InitWith(TMavlinkClientDevice device) { base.InitWith(device); var mavlinkId = device.Id as MavlinkClientDeviceId ?? throw new Exception($"Should be {typeof(MavlinkClientDeviceId)}"); _order = CreateOrderFromId(mavlinkId); } private static int CreateOrderFromId(MavlinkClientDeviceId id) { return (id.Id.Target.SystemId * 1000) + id.Id.Target.ComponentId; } } ================================================ FILE: src/Asv.Drones.Api/Shell/Pages/MavParams/IMavParamsPageViewModel.cs ================================================ using Asv.Avalonia.IO; namespace Asv.Drones.Api; public interface IMavParamsPageViewModel : IDevicePage { } ================================================ FILE: src/Asv.Drones.Api/Shell/Pages/Setup/ISetupPage.cs ================================================ using Asv.Avalonia; using Asv.Avalonia.IO; namespace Asv.Drones.Api; public interface ISetupPage : ITreePageViewModel, IDevicePage { } ================================================ FILE: src/Asv.Drones.Api/Shell/Pages/Setup/Subpage/ISetupSubpage.cs ================================================ using Asv.Avalonia; namespace Asv.Drones.Api; public interface ISetupSubpage : ITreeSubpage { } ================================================ FILE: src/Asv.Drones.Api/Tools/Mavlink/DeviceIconMixin.cs ================================================ using Asv.Avalonia.GeoMap; using Asv.IO; using Asv.Mavlink; using Material.Icons; namespace Asv.Drones.Api; public static class DeviceIconMixin { public static MaterialIconKind? GetIcon(DeviceId deviceId) { return deviceId.DeviceClass switch { Vehicles.PlaneDeviceClass => MaterialIconKind.Plane, Vehicles.CopterDeviceClass => MaterialIconKind.Navigation, GbsClientDevice.DeviceClass => MaterialIconKind.RouterWireless, _ => null, }; } public static HorizontalOffset GetIconCenterX(DeviceId deviceId) { switch (deviceId.DeviceClass) { case Vehicles.PlaneDeviceClass: return HorizontalOffset.Default; case Vehicles.CopterDeviceClass: return HorizontalOffset.Default; case GbsClientDevice.DeviceClass: return HorizontalOffset.Default; default: return HorizontalOffset.Default; } } public static VerticalOffset GetIconCenterY(DeviceId deviceId) { switch (deviceId.DeviceClass) { case Vehicles.PlaneDeviceClass: return VerticalOffset.Default; case Vehicles.CopterDeviceClass: return VerticalOffset.Default; case GbsClientDevice.DeviceClass: return VerticalOffset.Default; default: return VerticalOffset.Default; } } } ================================================ FILE: src/Asv.Drones.Api/Tools/Mavlink/MavlinkHost.cs ================================================ using Asv.Avalonia; using Asv.Avalonia.IO; using Asv.Cfg; using Asv.IO; using Asv.Mavlink; using Asv.Mavlink.Ardupilotmega; using Asv.Mavlink.AsvAudio; using Asv.Mavlink.AsvAudio; using Asv.Mavlink.AsvChart; using Asv.Mavlink.AsvChart; using Asv.Mavlink.AsvGbs; using Asv.Mavlink.AsvRadio; using Asv.Mavlink.AsvRfsa; using Asv.Mavlink.AsvRsga; using Asv.Mavlink.AsvSdr; using Asv.Mavlink.Avssuas; using Asv.Mavlink.Common; using Asv.Mavlink.Csairlink; using Asv.Mavlink.Cubepilot; using Asv.Mavlink.Icarous; using Asv.Mavlink.Minimal; using Asv.Mavlink.Storm32; using Asv.Mavlink.Ualberta; using Asv.Mavlink.Uavionix; using Material.Icons; using Microsoft.Extensions.Hosting; using R3; using MavType = Asv.Mavlink.Minimal.MavType; namespace Asv.Drones.Api; public class MavlinkDeviceManagerExtensionConfig { public byte SystemId { get; set; } = 255; public byte ComponentId { get; set; } = 255; } public class MavlinkHost : IDeviceManagerExtension, IMavlinkHost, IHostedService { private readonly IConfiguration _cfgSvc; private readonly IPacketSequenceCalculator _seq; private readonly MavlinkDeviceManagerExtensionConfig _cfg; private readonly IProtocolMessageFactory _messageFactory; public MavlinkHost( IConfiguration cfgSvc, IPacketSequenceCalculator seq, IEnumerable extensions ) { _cfgSvc = cfgSvc; _seq = seq; _cfg = cfgSvc.Get(); Identity = new MavlinkIdentity(_cfg.SystemId, _cfg.ComponentId); _messageFactory = MavlinkV2Protocol.CreateMessageFactory(builder => { // TODO: replace with extension in the future: RegisterDefault builder.RegisterMinimalDialect(); builder.RegisterCommonDialect(); builder.RegisterArdupilotmegaDialect(); builder.RegisterIcarousDialect(); builder.RegisterUalbertaDialect(); builder.RegisterStorm32Dialect(); builder.RegisterAvssuasDialect(); builder.RegisterUavionixDialect(); builder.RegisterCubepilotDialect(); builder.RegisterCsairlinkDialect(); builder.RegisterAsvGbsDialect(); builder.RegisterAsvSdrDialect(); builder.RegisterAsvAudioDialect(); builder.RegisterAsvRadioDialect(); builder.RegisterAsvRfsaDialect(); builder.RegisterAsvChartDialect(); builder.RegisterAsvRsgaDialect(); foreach (var ext in extensions) { ext.Extend(builder); } }); } public void Configure(IProtocolBuilder builder) { builder.RegisterMavlinkV2Protocol(_messageFactory); builder.Features.RegisterMavlinkV2WrapFeature(_messageFactory); builder.Features.RegisterBroadcastFeature(); } public void Configure(IDeviceExplorerBuilder builder) { builder.Factories.RegisterDefaultDevices(Identity, _seq, _cfgSvc, _messageFactory); } public bool TryGetIcon(DeviceId id, out MaterialIconKind? icon) { icon = DeviceIconMixin.GetIcon(id); return icon != null; } public bool TryGetDeviceBrush(DeviceId id, out AsvColorKind brush) { brush = AsvColorKind.None; return false; } public void Run(IDeviceManager deviceManager) { var config = _cfgSvc.Get(); Context = new CoreServices( _seq, deviceManager.Router, deviceManager.ProtocolFactory, _messageFactory ); Heartbeat = new HeartbeatServer(Identity, config, Context); Heartbeat.Set(m => { m.Autopilot = MavAutopilot.MavAutopilotInvalid; m.Type = MavType.MavTypeGcs; m.BaseMode = MavModeFlag.MavModeFlagCustomModeEnabled; }); Heartbeat.Start(); } public IHeartbeatServer? Heartbeat { get; private set; } public IProtocolMessageFactory MessageFactory => _messageFactory; public IMavlinkContext Context { get; private set; } public MavlinkIdentity Identity { get; } public Task StartAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } } ================================================ FILE: src/Asv.Drones.Desktop/Asv.Drones.Desktop.csproj ================================================  WinExe $(TargetFramework) $(ProductVersion) $(ProductVersion) https://github.com/asv-soft https://github.com/asv-soft https://github.com/asv-soft enable preview true ../CodeStyle.ruleset CS0169, CS0618, CS1502, CS1503, CS8524, CS8600, CS8601, CS8602, CS8603, CS8604, CS8625, CS8629, CS8762, CA1510, CA1851 app.ico app.manifest all runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive Always Always Always ================================================ FILE: src/Asv.Drones.Desktop/Program.cs ================================================ using System; using System.IO; using System.Reflection; using System.Threading.Tasks; using Asv.Avalonia; using Asv.Avalonia.GeoMap; using Asv.Avalonia.IO; using Asv.Avalonia.Plugins; using Asv.Common; using Asv.Drones.Api; using Avalonia; using Avalonia.Controls; using DotNext; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using R3; namespace Asv.Drones.Desktop; sealed class Program { // Initialization code. Don't use any Avalonia, third-party APIs or any // SynchronizationContext-reliant code before AppMain is called: things aren't initialized // yet and stuff might break. [STAThread] public static void Main(string[] args) { try { BuildAvaloniaApp() .StartWithClassicDesktopLifetime(args, ShutdownMode.OnMainWindowClose); AppHost.Instance.StopAsync().GetAwaiter().GetResult(); Task.Run(AppHost.Instance.Dispose).GetAwaiter().GetResult(); } catch (Exception e) { AppHost.HandleApplicationCrash(e); } } // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() { return AppBuilder .Configure() .UsePlatformDetect() .With(new Win32PlatformOptions { OverlayPopups = true }) // Windows .With(new X11PlatformOptions { OverlayPopups = true, UseDBusFilePicker = false }) // Unix/Linux .With(new AvaloniaNativePlatformOptions { OverlayPopups = true }) // Mac .WithInterFont() .LogToTrace() .UseAsv(builder => { builder .UseDefault() .UseAppInfo(configure => configure.FillFromAssembly(typeof(App).Assembly)) .UseOptionalLogToFile() .UseOptionalLogViewer() .UseOptionalSoloRun(opt => opt.WithArgumentForwarding()) .UsePluginManager(options => { options.WithApiPackage(typeof(MavlinkHost).Assembly); options.WithPluginPrefix("Asv.Drones.Plugin."); }) .UseDesktopShell() .UseModulePlugins(configure => { configure .WithApiPackage(typeof(MavlinkHost).Assembly) .UseOptionalInstalled() // register installed plugins page .UseOptionalMarket(); // register market plugins page }) .UseModuleGeoMap() .UseModuleIo() .UseDronesApp(); }); } } ================================================ FILE: src/Asv.Drones.Desktop/app.manifest ================================================  ================================================ FILE: src/Asv.Drones.Desktop/appsettings.Development.json ================================================ { } ================================================ FILE: src/Asv.Drones.Desktop/appsettings.Production.json ================================================ { } ================================================ FILE: src/Asv.Drones.Desktop/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Trace", "Microsoft": "Warning", "System": "Warning" } }, "UnhandledExceptions": { "R3": { "PublishToShell": true, "PublishToLogger": true, "ForceApplicationCrash": false }, "TaskScheduler": { "PublishToShell": true, "PublishToLogger": true, "ForceApplicationCrash": false } }, "Plugins": { "PluginDataFolder": "plugins", "AdditionalFolderPerPlugin": [], "ApiPackageName": "Asv.Drones.Api", "PluginAssemblyPrefix": "Asv.Drones.Plugin." }, "UserConfiguration": { "FilePath": "user_settings.json", "AutoSaveMs": 500 }, "SoloRun": { "Mutex": "Asv.Drones.Api", "ArgForward": true, "Pipe": "Asv.Drones.Api" }, "LogViewer": { "RollingSizeKb": 50, "Folder": "logs" } } ================================================ FILE: src/Asv.Drones.iOS/AppDelegate.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.iOS; using Avalonia.Media; using Foundation; using UIKit; namespace Asv.Drones.iOS; // The UIApplicationDelegate for the application. This class is responsible for launching the // User Interface of the application, as well as listening (and optionally responding) to // application events from iOS. [Register("AppDelegate")] #pragma warning disable CA1711 // Identifiers should not have incorrect suffix public partial class AppDelegate : AvaloniaAppDelegate #pragma warning restore CA1711 // Identifiers should not have incorrect suffix { protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) { return base.CustomizeAppBuilder(builder).WithInterFont(); } } ================================================ FILE: src/Asv.Drones.iOS/Asv.Drones.iOS.csproj ================================================  Exe net9.0-ios 13.0 enable ../CodeStyle.ruleset CS0169, CS0618, CS1502, CS1503, CS8524, CS8600, CS8601, CS8602, CS8603, CS8604, CS8625, CS8629, CS8762, CA1510, CA1851 all runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive ================================================ FILE: src/Asv.Drones.iOS/Entitlements.plist ================================================ ================================================ FILE: src/Asv.Drones.iOS/Info.plist ================================================ CFBundleDisplayName Asv.Drones CFBundleIdentifier companyName.Asv.Drones CFBundleShortVersionString 1.0 CFBundleVersion 1.0 LSRequiresIPhoneOS MinimumOSVersion 13.0 UIDeviceFamily 1 2 UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight ================================================ FILE: src/Asv.Drones.iOS/Main.cs ================================================ using UIKit; namespace Asv.Drones.iOS; public class Application { // This is the main entry point of the application. static void Main(string[] args) { // if you want to use a different Application Delegate class from "AppDelegate" // you can specify it here. UIApplication.Main(args, null, typeof(AppDelegate)); } } ================================================ FILE: src/Asv.Drones.iOS/Resources/LaunchScreen.xib ================================================ ================================================ FILE: src/Asv.Drones.slnx ================================================ ================================================ FILE: src/CodeStyle.ruleset ================================================ None <--> Error <--> ================================================ FILE: src/Directory.Build.props ================================================ enable 2.5.1 net10.0 2.5.1 11.3.12 2.5.0-dev.3 4.2.0 2.1.0 5.1.57 ================================================ FILE: src/global.json ================================================ { "scripts": { "husky": "cd ../ & dotnet husky install", "husky-unix": "cd .. && dotnet husky install" } } ================================================ FILE: win-64-install.nsi ================================================ ; Main constants - define following constants as you want them displayed in your installation wizard !define PRODUCT_NAME "Asv.Drones" !define PRODUCT_PUBLISHER "Cursir" ; Following constants you should never change !define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" !define PRODUCT_UNINST_ROOT_KEY "HKLM" !include "MUI.nsh" !include "nsDialogs.nsh" !define MUI_ABORTWARNING !define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\modern-uninstall.ico" ; Wizard pages !insertmacro MUI_PAGE_WELCOME !insertmacro MUI_PAGE_COMPONENTS !insertmacro MUI_PAGE_DIRECTORY !insertmacro MUI_PAGE_INSTFILES !insertmacro MUI_PAGE_FINISH !insertmacro MUI_UNPAGE_INSTFILES !insertmacro MUI_LANGUAGE "English" Name "Asv Drones " OutFile "AsvDronesInstaller.exe" InstallDir "$PROGRAMFILES\Asv\Asv-Drones" ShowInstDetails show ShowUnInstDetails show ; Following lists the files you want to include, go through this list carefully! Section "Core Files (required)" SEC01 SetOutPath "$INSTDIR" ; Check dir in yml where your project is stored File /r "./publish/app\\*.*" SectionEnd ; Section for shortcuts Section "Start Menu and Desktop Shortcuts" SEC02 ; Create a shortcut on the desktop CreateShortcut "$DESKTOP\Asv Drones.lnk" "$INSTDIR\Asv.Drones.Desktop.exe" "" "$INSTDIR\Asv.Drones.Desktop.exe" 0 ; Create a shortcut in the Start menu CreateShortcut "$SMPROGRAMS\Asv Drones\Asv Drones.lnk" "$INSTDIR\Asv.Drones.Desktop.exe" "" "$INSTDIR\Asv.Drones.Desktop.exe" 0 SectionEnd Section -Post ;Following lines will make uninstaller work - do not change anything, unless you really want to. WriteUninstaller "$INSTDIR\uninst.exe" WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayName" "$(^Name)" WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "UninstallString" "$INSTDIR\uninst.exe" SectionEnd ; Replace the following strings to suite your needs Function un.onUninstSuccess HideWindow MessageBox MB_ICONINFORMATION|MB_OK "Application was successfully removed from your computer." FunctionEnd Function un.onInit MessageBox MB_ICONQUESTION|MB_YESNO|MB_DEFBUTTON2 "Are you sure you want to completely remove Asv Drones and all of its components?" IDYES +2 Abort FunctionEnd ; Remove any file that you have added above - removing uninstallation and folders last. ; Note: if there is any file changed or added to these folders, they will not be removed. Also, parent folder (which in my example ; is company name ZWare) will not be removed if there is any other application installed in it. Section Uninstall ; Remove the installed files Delete "$INSTDIR\*.*" ; Remove the installation directory RMDir /r "$INSTDIR" ; Remove the shortcuts Delete "$DESKTOP\Asv Drones.lnk" Delete "$SMPROGRAMS\Asv Drones\Asv Drones.lnk" RMDir "$SMPROGRAMS\Asv Drones" DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" SetAutoClose true SectionEnd ================================================ FILE: win-arm-install.iss ================================================ #define MyAppName "Asv.Drones" #define MyAppVersion "0.2.2" #define MyAppPublisher "Asv.Soft LLC" #define MyAppURL "https://www.asv.me" #define MyAppExeName "asv-drones-win-arm.exe" [Setup] AppId={{AD0A5B4D-D17A-4301-A387-892193920F9D} AppName={#MyAppName} AppVersion={#MyAppVersion} AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} AppSupportURL={#MyAppURL} AppUpdatesURL={#MyAppURL} DefaultDirName={autopf}\AsvDrones DisableProgramGroupPage=yes LicenseFile=License OutputDir=publish\win-arm OutputBaseFilename=asv-drones-win-arm-install SetupIconFile=src\Asv.Drones.Gui\Assets\icon.ico Compression=lzma SolidCompression=yes WizardStyle=modern [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked [Files] Source: "publish\win-arm\app\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion [Icons] Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon [Run] Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent ================================================ FILE: win-arm64-install.iss ================================================ #define MyAppName "Asv.Drones" #define MyAppVersion "0.2.2" #define MyAppPublisher "Asv.Soft LLC" #define MyAppURL "https://www.asv.me" #define MyAppExeName "asv-drones-win-arm64.exe" [Setup] AppId={{57EC272E-2E6B-4234-8FCB-2B339D3D9EE2} AppName={#MyAppName} AppVersion={#MyAppVersion} AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} AppSupportURL={#MyAppURL} AppUpdatesURL={#MyAppURL} DefaultDirName={autopf}\{#MyAppName} DisableProgramGroupPage=yes LicenseFile=License OutputDir=publish\win-arm64 OutputBaseFilename=asv-drones-win-arm64-install SetupIconFile=src\Asv.Drones.Gui\Assets\icon.ico Compression=lzma SolidCompression=yes WizardStyle=modern [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked [Files] Source: "publish\win-arm64\app\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion [Icons] Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon [Run] Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent ================================================ FILE: win-x64-install.iss ================================================ #define MyAppName "Asv.Drones" #define MyAppVersion "0.2.2" #define MyAppPublisher "Asv.Soft LLC" #define MyAppURL "https://www.asv.me" #define MyAppExeName "asv-drones-win-x64.exe" [Setup] AppId={{C5CE850B-61D6-417A-8855-AD02C9BEA9FD} AppName={#MyAppName} AppVersion={#MyAppVersion} AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} AppSupportURL={#MyAppURL} AppUpdatesURL={#MyAppURL} DefaultDirName={autopf}\{#MyAppName} DisableProgramGroupPage=yes LicenseFile=License OutputDir=publish\win-x64 OutputBaseFilename=asv-drones-win-x64-install SetupIconFile=src\Asv.Drones.Gui\Assets\icon.ico Compression=lzma SolidCompression=yes WizardStyle=modern [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked [Files] Source: "publish\win-x64\app\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion [Icons] Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon [Run] Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent ================================================ FILE: win-x86-install.iss ================================================ #define MyAppName "Asv.Drones" #define MyAppVersion "0.2.2" #define MyAppPublisher "Asv.Soft LLC" #define MyAppURL "https://www.asv.me" #define MyAppExeName "asv-drones-win-x86.exe" [Setup] AppId={{D799FAA2-F3B5-49CB-A5C3-E1DC4204E61F} AppName={#MyAppName} AppVersion={#MyAppVersion} AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} AppSupportURL={#MyAppURL} AppUpdatesURL={#MyAppURL} DefaultDirName={autopf}\{#MyAppName} DisableProgramGroupPage=yes LicenseFile=License OutputDir=publish\win-x86 OutputBaseFilename=asv-drones-win-x86-install SetupIconFile=src\Asv.Drones.Gui\Assets\icon.ico Compression=lzma SolidCompression=yes WizardStyle=modern [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked [Files] Source: "publish\win-x86\app\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion [Icons] Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon [Run] Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent