├── Companion ├── VERSION ├── binaries │ ├── presets │ │ ├── high-power-fpv │ │ │ ├── sensor │ │ │ │ └── .keep │ │ │ └── preset-config.yaml │ │ └── create_preset.sh │ ├── gs.key │ ├── drone.key │ ├── sensors │ │ ├── imx307.bin │ │ ├── imx335.bin │ │ ├── imx415.bin │ │ ├── imx415_fpv.bin │ │ ├── imx335_milos6.bin │ │ ├── imx335_greg_fpv.bin │ │ ├── imx415_greg_fpv.bin │ │ ├── imx415_greg_fpvX.bin │ │ └── imx415_milos16.bin │ ├── clean │ │ ├── wfb.conf │ │ ├── telemetry.conf │ │ ├── majestic.yaml │ │ ├── telemetry_msposd_gs │ │ └── telemetry_msposd_extra │ └── stream.sh ├── Assets │ ├── Icons │ │ ├── OpenIPC.ico │ │ ├── OpenIPC.png │ │ ├── OpenIPC.icns │ │ ├── avalonia-logo.ico │ │ ├── drawer-open.svg │ │ ├── drawer-close.svg │ │ ├── drawer-handle.svg │ │ ├── folder-open.svg │ │ ├── iconoir_drag_dark.svg │ │ ├── iconoir_drag_light.svg │ │ ├── iconoir_wifi_dark.svg │ │ ├── iconoir_wifi_light.svg │ │ ├── icon_connect.svg │ │ ├── iconair_camera_light.svg │ │ ├── iconoir_presets_light.svg │ │ ├── iconoir_presets_dark.svg │ │ ├── iconair_advanced_light.svg │ │ ├── iconair_advanced_dark.svg │ │ ├── iconoir_settings_dark.svg │ │ ├── iconoir_settings_light.svg │ │ ├── iconair_firmware_dark.svg │ │ ├── iconair_firmware_light.svg │ │ ├── iconoir_cube.svg │ │ ├── ic-telegram.svg │ │ ├── mdi-github.svg │ │ ├── iconoir_github.svg │ │ ├── iconoir_camera_dark.svg │ │ └── ic-discord.svg │ ├── Resources.es.resx │ └── Resources.es.Designer.cs ├── Events │ ├── LogMessageEvent.cs │ ├── TabMessageEvent.cs │ ├── AlinkDroneStatusEvent.cs │ ├── TabSelectionChangeEvent.cs │ ├── DeviceTypeChangeEvent.cs │ ├── WfbConfContentUpdatedEvent.cs │ ├── WfbYamlContentUpdatedEvent.cs │ ├── MajesticContentUpdatedEvent.cs │ ├── TelemetryContentUpdatedEvent.cs │ ├── RadxaContentUpdateEvent.cs │ ├── AppMessageEvent.cs │ └── DeviceContentUpdateEvent.cs ├── Services │ ├── IWfbGsConfigParser.cs │ ├── IGitHubService.cs │ ├── IGlobalSettingsService.cs │ ├── IYamlConfigService.cs │ ├── INetworkCommands.cs │ ├── IMessageBoxService.cs │ ├── Presets │ │ ├── IGitHubPresetService.cs │ │ └── IPresetService.cs │ ├── NetworkCommands.cs │ ├── DeviceConfigValidator.cs │ ├── NetworkHelper.cs │ ├── VersionHelper.cs │ ├── EventSubscriptionService.cs │ ├── SysUpgradeService.cs │ ├── Utilities.cs │ ├── GlobalSettingsService.cs │ ├── GitHubService.cs │ ├── PingService.cs │ ├── UpdateChecker.cs │ ├── ISshClientService.cs │ ├── SettingsManager.cs │ └── WifiCardDetector.cs ├── Views │ ├── MainWindow.axaml.cs │ ├── PresetDetailsView.axaml.cs │ ├── PresetsAddRepoView.axaml.cs │ ├── SetupTabView.axaml.cs │ ├── SetupRadxaButtonsView.axaml.cs │ ├── FirmwareTabView.axaml.cs │ ├── LogViewer.axaml.cs │ ├── VRXTabView.axaml.cs │ ├── WfbGSTabView.axaml.cs │ ├── MainWindow.axaml │ ├── StatusBarView.axaml.cs │ ├── NetworkIPScannerView.axaml.cs │ ├── TelemetryTabView.axaml.cs │ ├── AdvancedTabView.axaml.cs │ ├── WfbTabView.axaml.cs │ ├── CameraSettingsTabView.axaml.cs │ ├── ConnectControlsView.axaml.cs │ ├── MainView.axaml.cs │ ├── HeaderView.axaml.cs │ ├── SetupCameraButtonsView.axaml.cs │ ├── SetupRadxaButtonsView.axaml │ ├── PresetsTabView.axaml.cs │ ├── WfbGSTabView.axaml │ ├── LogViewer.axaml │ └── PresetsAddRepoView.axaml ├── Models │ ├── TelemetryCommands.cs │ ├── Presets │ │ ├── FileModification.cs │ │ ├── GitHubFile.cs │ │ ├── RepositorySettings.cs │ │ ├── PresetIndex.cs │ │ └── PresetIndexEntry.cs │ ├── Wfb.cs │ ├── WfbConfig.cs │ ├── WfbYaml.cs │ ├── Majestic.cs │ ├── FrequencyMappings.cs │ └── Telemetry.cs ├── Converters │ ├── NullToBoolConverter.cs │ ├── BooleanToTextConverter.cs │ ├── BooleanToVisibilityConverter.cs │ ├── BooleanToWidthConverter.cs │ ├── InvertedBooleanConverter.cs │ ├── TagsToStringConverter.cs │ ├── EnumToBoolConverter.cs │ ├── BooleanGreaterThanConverter.cs │ ├── PowerThresholdColorConverter.cs │ └── CanConnectConverter.cs ├── appsettings.Development.json ├── App.axaml ├── ViewLocator.cs ├── Logging │ ├── EventAggregatorSink.cs │ └── LogQueue.cs └── ViewModels │ ├── PresetDetailsViewModel.cs │ ├── TabItemViewModel.cs │ ├── StatusBarViewModel.cs │ ├── PresetsAddRepoViewModel.cs │ └── ViewModelBase.cs ├── Companion.Tests ├── VERSION ├── DependencyInjectionTests.cs ├── Services │ ├── VersionHelperTests.cs │ ├── YamlConfigServiceTests.cs │ ├── UpdateCheckerTests.cs │ ├── WifiCardDetectorTests.cs │ ├── WfbGsConfigParserTests.cs │ └── WifiConfigParserTests.cs ├── ViewModels │ └── ViewModelTestBase.cs └── Companion.Tests.csproj ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml ├── release.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── build-release-android.yml ├── global.json ├── Companion.Android ├── Icon.png ├── Resources │ ├── drawable │ │ ├── Icon.png │ │ └── splash_screen.xml │ ├── values │ │ ├── colors.xml │ │ └── styles.xml │ ├── values-night │ │ └── colors.xml │ ├── values-v31 │ │ └── styles.xml │ └── AboutResources.txt ├── Properties │ └── AndroidManifest.xml ├── MainActivity.cs ├── Helpers │ └── AndroidFileHelper.cs └── Companion.Android.csproj ├── .vs └── AnotherConfigurator │ └── v17 │ ├── .wsuo │ └── DocumentLayout.json ├── Directory.Build.props ├── clean.sh ├── Companion.iOS ├── Entitlements.plist ├── Main.cs ├── AppDelegate.cs └── Info.plist ├── coverage-report.sh ├── Companion.Desktop ├── Program.cs ├── app.manifest └── Companion.Desktop.csproj ├── README-Android.md ├── get-latest-binaries.sh ├── Presets.md ├── test-script.sh └── README-Linux.md /Companion/VERSION: -------------------------------------------------------------------------------- 1 | v0.5.1 2 | -------------------------------------------------------------------------------- /Companion.Tests/VERSION: -------------------------------------------------------------------------------- 1 | v0.2.2 2 | -------------------------------------------------------------------------------- /Companion/binaries/presets/high-power-fpv/sensor/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { "version": "8.0.401", "rollForward": "latestPatch" } 3 | } 4 | -------------------------------------------------------------------------------- /Companion.Android/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/openipc-configurator/HEAD/Companion.Android/Icon.png -------------------------------------------------------------------------------- /Companion/binaries/gs.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/openipc-configurator/HEAD/Companion/binaries/gs.key -------------------------------------------------------------------------------- /Companion/binaries/drone.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/openipc-configurator/HEAD/Companion/binaries/drone.key -------------------------------------------------------------------------------- /.vs/AnotherConfigurator/v17/.wsuo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/openipc-configurator/HEAD/.vs/AnotherConfigurator/v17/.wsuo -------------------------------------------------------------------------------- /Companion/Assets/Icons/OpenIPC.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/openipc-configurator/HEAD/Companion/Assets/Icons/OpenIPC.ico -------------------------------------------------------------------------------- /Companion/Assets/Icons/OpenIPC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/openipc-configurator/HEAD/Companion/Assets/Icons/OpenIPC.png -------------------------------------------------------------------------------- /Companion/Assets/Icons/OpenIPC.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/openipc-configurator/HEAD/Companion/Assets/Icons/OpenIPC.icns -------------------------------------------------------------------------------- /Companion/binaries/sensors/imx307.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/openipc-configurator/HEAD/Companion/binaries/sensors/imx307.bin -------------------------------------------------------------------------------- /Companion/binaries/sensors/imx335.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/openipc-configurator/HEAD/Companion/binaries/sensors/imx335.bin -------------------------------------------------------------------------------- /Companion/binaries/sensors/imx415.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/openipc-configurator/HEAD/Companion/binaries/sensors/imx415.bin -------------------------------------------------------------------------------- /Companion/Assets/Icons/avalonia-logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/openipc-configurator/HEAD/Companion/Assets/Icons/avalonia-logo.ico -------------------------------------------------------------------------------- /Companion/binaries/sensors/imx415_fpv.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/openipc-configurator/HEAD/Companion/binaries/sensors/imx415_fpv.bin -------------------------------------------------------------------------------- /Companion/binaries/sensors/imx335_milos6.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/openipc-configurator/HEAD/Companion/binaries/sensors/imx335_milos6.bin -------------------------------------------------------------------------------- /Companion.Android/Resources/drawable/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/openipc-configurator/HEAD/Companion.Android/Resources/drawable/Icon.png -------------------------------------------------------------------------------- /Companion/binaries/sensors/imx335_greg_fpv.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/openipc-configurator/HEAD/Companion/binaries/sensors/imx335_greg_fpv.bin -------------------------------------------------------------------------------- /Companion/binaries/sensors/imx415_greg_fpv.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/openipc-configurator/HEAD/Companion/binaries/sensors/imx415_greg_fpv.bin -------------------------------------------------------------------------------- /Companion/binaries/sensors/imx415_greg_fpvX.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/openipc-configurator/HEAD/Companion/binaries/sensors/imx415_greg_fpvX.bin -------------------------------------------------------------------------------- /Companion/binaries/sensors/imx415_milos16.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/openipc-configurator/HEAD/Companion/binaries/sensors/imx415_milos16.bin -------------------------------------------------------------------------------- /Companion/Events/LogMessageEvent.cs: -------------------------------------------------------------------------------- 1 | using Prism.Events; 2 | 3 | namespace Companion.Events; 4 | 5 | public class LogMessageEvent : PubSubEvent 6 | { 7 | } -------------------------------------------------------------------------------- /Companion/Events/TabMessageEvent.cs: -------------------------------------------------------------------------------- 1 | using Prism.Events; 2 | 3 | namespace Companion.Events; 4 | 5 | public class TabMessageEvent : PubSubEvent 6 | { 7 | } -------------------------------------------------------------------------------- /Companion.Android/Resources/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | 5 | -------------------------------------------------------------------------------- /Companion.Android/Resources/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #212121 4 | 5 | -------------------------------------------------------------------------------- /Companion/Events/AlinkDroneStatusEvent.cs: -------------------------------------------------------------------------------- 1 | using Prism.Events; 2 | 3 | namespace Companion.Events; 4 | 5 | public class AlinkDroneStatusEvent : PubSubEvent 6 | { 7 | 8 | } -------------------------------------------------------------------------------- /Companion/Events/TabSelectionChangeEvent.cs: -------------------------------------------------------------------------------- 1 | using Prism.Events; 2 | 3 | namespace Companion.Events; 4 | 5 | public class TabSelectionChangeEvent : PubSubEvent 6 | { 7 | } -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | enable 4 | 11.2.3 5 | 6 | 7 | -------------------------------------------------------------------------------- /Companion/Events/DeviceTypeChangeEvent.cs: -------------------------------------------------------------------------------- 1 | using Companion.Models; 2 | using Prism.Events; 3 | 4 | namespace Companion.Events; 5 | 6 | public class DeviceTypeChangeEvent : PubSubEvent 7 | { 8 | } -------------------------------------------------------------------------------- /Companion/Services/IWfbGsConfigParser.cs: -------------------------------------------------------------------------------- 1 | namespace Companion.Services; 2 | 3 | public interface IWfbGsConfigParser 4 | { 5 | string TxPower { get; set; } 6 | string GetUpdatedConfigString(); 7 | } -------------------------------------------------------------------------------- /Companion/Services/IGitHubService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Companion.Services; 4 | 5 | public interface IGitHubService 6 | { 7 | Task GetGitHubDataAsync(string url); 8 | } -------------------------------------------------------------------------------- /clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Cleaning previous builds..." 4 | rm -rf build 5 | 6 | dotnet clean 7 | find . -name bin -exec rm -rf {} + 8 | find . -name obj -exec rm -rf {} + 9 | 10 | echo "Done." -------------------------------------------------------------------------------- /Companion/Assets/Icons/drawer-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Companion.iOS/Entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Companion/Services/IGlobalSettingsService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Companion.Services; 4 | 5 | public interface IGlobalSettingsService 6 | { 7 | bool IsWfbYamlEnabled { get; } 8 | Task ReadDevice(); 9 | } -------------------------------------------------------------------------------- /Companion/Assets/Icons/drawer-close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Companion/Views/MainWindow.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | 3 | namespace Companion.Views; 4 | 5 | public partial class MainWindow : Window 6 | { 7 | public MainWindow() 8 | { 9 | InitializeComponent(); 10 | } 11 | } -------------------------------------------------------------------------------- /Companion/Assets/Icons/drawer-handle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Companion/Assets/Icons/folder-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /Companion/Services/IYamlConfigService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Companion.Services; 4 | 5 | public interface IYamlConfigService 6 | { 7 | void ParseYaml(string content, Dictionary yamlConfig); 8 | string UpdateYaml(Dictionary yamlConfig); 9 | } -------------------------------------------------------------------------------- /Companion/Services/INetworkCommands.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Companion.Models; 3 | 4 | namespace Companion.Services; 5 | 6 | public interface INetworkCommands 7 | { 8 | extern Task Ping(DeviceConfig deviceConfig); 9 | 10 | Task Run(DeviceConfig deviceConfig, string command); 11 | } -------------------------------------------------------------------------------- /Companion/Views/PresetDetailsView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace Companion.Views; 6 | 7 | public partial class PresetDetailsView : UserControl 8 | { 9 | public PresetDetailsView() 10 | { 11 | InitializeComponent(); 12 | } 13 | } -------------------------------------------------------------------------------- /Companion/Views/PresetsAddRepoView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace Companion.Views; 6 | 7 | public partial class PresetsAddRepoView : UserControl 8 | { 9 | public PresetsAddRepoView() 10 | { 11 | if (!Design.IsDesignMode) 12 | InitializeComponent(); 13 | } 14 | } -------------------------------------------------------------------------------- /Companion/Assets/Icons/iconoir_drag_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Companion/Assets/Icons/iconoir_drag_light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Companion/binaries/presets/high-power-fpv/preset-config.yaml: -------------------------------------------------------------------------------- 1 | name: "high-power-fpv" 2 | author: "Your Name" 3 | description: "Description of high-power-fpv." 4 | category: "FPV" 5 | state: OFFICIAL 6 | sensor: "" # Set sensor file if needed 7 | files: 8 | wfb.yaml: 9 | wireless.txpower: "1" 10 | wireless.channel: "161" 11 | majestic.yaml: 12 | fpv.enabled: "false" 13 | system.logLevel: "debug" 14 | -------------------------------------------------------------------------------- /coverage-report.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | function clean { 3 | local targetDir=$1 4 | rm -rf "$targetDir/TestResults" 5 | } 6 | 7 | 8 | dotnet test --collect:"XPlat Code Coverage" --results-directory:"./.coverage" 9 | dotnet tool install -g dotnet-reportgenerator-globaltool 10 | reportgenerator "-reports:.coverage/**/*.cobertura.xml" "-targetdir:.coverage-report/" "-reporttypes:HTML;" 11 | 12 | clean "OpenIPC_Config.Tests" 13 | -------------------------------------------------------------------------------- /Companion/Events/WfbConfContentUpdatedEvent.cs: -------------------------------------------------------------------------------- 1 | using Prism.Events; 2 | 3 | namespace Companion.Events; 4 | 5 | public class WfbConfContentUpdatedEvent : PubSubEvent 6 | { 7 | } 8 | 9 | public class WfbConfContentUpdatedMessage 10 | { 11 | public WfbConfContentUpdatedMessage(string content) 12 | { 13 | Content = content; 14 | } 15 | 16 | public string Content { get; set; } 17 | } -------------------------------------------------------------------------------- /Companion/Events/WfbYamlContentUpdatedEvent.cs: -------------------------------------------------------------------------------- 1 | using Prism.Events; 2 | 3 | namespace Companion.Events; 4 | 5 | public class WfbYamlContentUpdatedEvent : PubSubEvent 6 | { 7 | } 8 | 9 | public class WfbYamlContentUpdatedMessage 10 | { 11 | public WfbYamlContentUpdatedMessage(string content) 12 | { 13 | Content = content; 14 | } 15 | 16 | public string Content { get; set; } 17 | } -------------------------------------------------------------------------------- /Companion.Android/Properties/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Companion/Events/MajesticContentUpdatedEvent.cs: -------------------------------------------------------------------------------- 1 | using Prism.Events; 2 | 3 | namespace Companion.Events; 4 | 5 | public class MajesticContentUpdatedEvent : PubSubEvent 6 | { 7 | } 8 | 9 | public class MajesticContentUpdatedMessage 10 | { 11 | public MajesticContentUpdatedMessage(string content) 12 | { 13 | Content = content; 14 | } 15 | 16 | public string Content { get; set; } 17 | } -------------------------------------------------------------------------------- /Companion/Events/TelemetryContentUpdatedEvent.cs: -------------------------------------------------------------------------------- 1 | using Prism.Events; 2 | 3 | namespace Companion.Events; 4 | 5 | public class TelemetryContentUpdatedEvent : PubSubEvent 6 | { 7 | } 8 | 9 | public class TelemetryContentUpdatedMessage 10 | { 11 | public TelemetryContentUpdatedMessage(string content) 12 | { 13 | Content = content; 14 | } 15 | 16 | public string Content { get; set; } 17 | } -------------------------------------------------------------------------------- /Companion/Views/SetupTabView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Companion.ViewModels; 4 | 5 | namespace Companion.Views; 6 | 7 | public partial class SetupTabView : UserControl 8 | { 9 | public SetupTabView() 10 | { 11 | InitializeComponent(); 12 | if (!Design.IsDesignMode) DataContext = App.ServiceProvider.GetService(); 13 | } 14 | } -------------------------------------------------------------------------------- /Companion/Models/TelemetryCommands.cs: -------------------------------------------------------------------------------- 1 | namespace Companion.Models; 2 | 3 | public class TelemetryCommands 4 | { 5 | public const string Extra = 6 | "sed -i 's/mavfwd --channels \\\"$channels\\\" --master \\\"$serial\\\" --baudrate \\\"$baud\\\" -p 100 -t -a \\\"$aggregate\\\" \\\\/mavfwd --channels \\\"$channels\\\" --master \\\"$serial\\\" --baudrate \\\"$baud\\\" -a \\\"$aggregate\\\" --wait 5 --persist 50 -t \\\\/' /usr/bin/telemetry"; 7 | } -------------------------------------------------------------------------------- /Companion/Assets/Icons/iconoir_wifi_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Companion/Assets/Icons/iconoir_wifi_light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Companion.Android/Resources/drawable/splash_screen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Companion/Views/SetupRadxaButtonsView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Companion.ViewModels; 4 | 5 | namespace Companion.Views; 6 | 7 | public partial class SetupRadxaButtonsView : UserControl 8 | { 9 | public SetupRadxaButtonsView() 10 | { 11 | InitializeComponent(); 12 | if (!Design.IsDesignMode) DataContext = App.ServiceProvider.GetService(); 13 | } 14 | } -------------------------------------------------------------------------------- /Companion.iOS/Main.cs: -------------------------------------------------------------------------------- 1 | using Companion.iOS; 2 | using UIKit; 3 | 4 | namespace OpenIPC.Companion.iOS; 5 | 6 | public class Application 7 | { 8 | // This is the main entry point of the application. 9 | private static void Main(string[] args) 10 | { 11 | // if you want to use a different Application Delegate class from "AppDelegate" 12 | // you can specify it here. 13 | UIApplication.Main(args, null, typeof(AppDelegate)); 14 | } 15 | } -------------------------------------------------------------------------------- /Companion/Services/IMessageBoxService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using MsBox.Avalonia.Enums; 3 | 4 | namespace Companion.Services; 5 | 6 | public interface IMessageBoxService 7 | { 8 | Task ShowMessageBox(string title, string message); 9 | Task ShowMessageBoxWithFolderLink(string title, string message, string filePath); 10 | Task ShowCustomMessageBox(string title, string message, ButtonEnum buttons, Icon icon = Icon.Info); 11 | } -------------------------------------------------------------------------------- /Companion/Views/FirmwareTabView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Companion.ViewModels; 4 | 5 | namespace Companion.Views; 6 | 7 | public partial class FirmwareTabView : UserControl 8 | { 9 | public FirmwareTabView() 10 | { 11 | InitializeComponent(); 12 | 13 | if (!Design.IsDesignMode) 14 | DataContext = App.ServiceProvider.GetService(); 15 | } 16 | } -------------------------------------------------------------------------------- /Companion/Models/Presets/FileModification.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.ObjectModel; 3 | 4 | namespace Companion.Models.Presets; 5 | 6 | public class FileModification 7 | { 8 | public string FileName { get; set; } 9 | public ObservableCollection> Changes { get; set; } 10 | 11 | public FileModification() 12 | { 13 | Changes = new ObservableCollection>(); 14 | } 15 | } -------------------------------------------------------------------------------- /Companion.Android/Resources/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /Companion/Views/LogViewer.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Companion.ViewModels; 4 | 5 | namespace Companion.Views; 6 | 7 | public partial class LogViewer : UserControl 8 | { 9 | public LogViewer() 10 | { 11 | InitializeComponent(); 12 | 13 | //if (!Design.IsDesignMode) DataContext = new LogViewerViewModel(); 14 | DataContext = App.ServiceProvider.GetService(); 15 | } 16 | } -------------------------------------------------------------------------------- /Companion/Views/VRXTabView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Companion.ViewModels; 4 | 5 | namespace Companion.Views; 6 | 7 | public partial class VRXTabView : UserControl 8 | { 9 | public VRXTabView() 10 | { 11 | InitializeComponent(); 12 | 13 | //if (!Design.IsDesignMode) DataContext = new VRXTabViewModel(); 14 | DataContext = App.ServiceProvider.GetService(); 15 | } 16 | } -------------------------------------------------------------------------------- /Companion/Views/WfbGSTabView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Companion.ViewModels; 4 | 5 | namespace Companion.Views; 6 | 7 | public partial class WfbGSTabView : UserControl 8 | { 9 | public WfbGSTabView() 10 | { 11 | InitializeComponent(); 12 | 13 | //if (!Design.IsDesignMode) DataContext = new WfbGSTabViewModel(); 14 | DataContext = App.ServiceProvider.GetService(); 15 | } 16 | } -------------------------------------------------------------------------------- /Companion/Views/MainWindow.axaml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Companion/Views/StatusBarView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Companion.ViewModels; 4 | 5 | namespace Companion.Views; 6 | 7 | public partial class StatusBarView : UserControl 8 | { 9 | public StatusBarView() 10 | { 11 | InitializeComponent(); 12 | 13 | //if (!Design.IsDesignMode) DataContext = new StatusBarViewModel(); 14 | DataContext = App.ServiceProvider.GetService(); 15 | } 16 | } -------------------------------------------------------------------------------- /Companion/binaries/clean/wfb.conf: -------------------------------------------------------------------------------- 1 | ## #10/28/2024 2 | ### unit: drone or gs 3 | unit=drone 4 | 5 | wlan=wlan0 6 | region=00 7 | ### By default used channel number, but, you may set freq instead. For ex: 2387M 8 | channel=161 9 | frequency= 10 | txpower=1 11 | driver_txpower_override=1 12 | bandwidth=20 13 | stbc=0 14 | ldpc=0 15 | mcs_index=1 16 | stream=0 17 | link_id=7669206 18 | udp_port=5600 19 | rcv_buf=456000 20 | frame_type=data 21 | fec_k=8 22 | fec_n=12 23 | pool_timeout=0 24 | guard_interval=long -------------------------------------------------------------------------------- /Companion/Views/NetworkIPScannerView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Companion.ViewModels; 4 | 5 | namespace Companion.Views; 6 | 7 | public partial class NetworkIPScannerView : UserControl 8 | { 9 | public NetworkIPScannerView() 10 | { 11 | InitializeComponent(); 12 | if (!Design.IsDesignMode) 13 | { 14 | DataContext = App.ServiceProvider.GetService(); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Companion/Views/TelemetryTabView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Companion.ViewModels; 4 | 5 | namespace Companion.Views; 6 | 7 | public partial class TelemetryTabView : UserControl 8 | { 9 | public TelemetryTabView() 10 | { 11 | InitializeComponent(); 12 | 13 | //if (!Design.IsDesignMode) DataContext = new TelemetryTabViewModel(); 14 | DataContext = App.ServiceProvider.GetService(); 15 | } 16 | } -------------------------------------------------------------------------------- /Companion/Views/AdvancedTabView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Companion.ViewModels; 5 | 6 | namespace Companion.Views; 7 | 8 | public partial class AdvancedTabView : UserControl 9 | { 10 | public AdvancedTabView() 11 | { 12 | if (!Design.IsDesignMode) 13 | DataContext = App.ServiceProvider.GetService(); 14 | 15 | InitializeComponent(); 16 | } 17 | 18 | 19 | } -------------------------------------------------------------------------------- /Companion/Views/WfbTabView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Companion.ViewModels; 4 | 5 | namespace Companion.Views; 6 | 7 | public partial class WfbTabView : UserControl 8 | { 9 | public WfbTabView() 10 | { 11 | InitializeComponent(); 12 | 13 | if (!Design.IsDesignMode) 14 | // Resolve the DataContext from the DI container at runtime 15 | DataContext = App.ServiceProvider.GetService(); 16 | } 17 | } -------------------------------------------------------------------------------- /Companion/Views/CameraSettingsTabView.axaml.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using Avalonia.Controls; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Companion.ViewModels; 5 | 6 | namespace Companion.Views; 7 | 8 | public partial class CameraSettingsTabView : UserControl, INotifyPropertyChanged 9 | { 10 | public CameraSettingsTabView() 11 | { 12 | InitializeComponent(); 13 | 14 | if (!Design.IsDesignMode) DataContext = App.ServiceProvider.GetService(); 15 | } 16 | } -------------------------------------------------------------------------------- /Companion/Views/ConnectControlsView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Companion.ViewModels; 4 | 5 | namespace Companion.Views; 6 | 7 | public partial class ConnectControlsView : UserControl 8 | { 9 | public ConnectControlsView() 10 | { 11 | InitializeComponent(); 12 | 13 | //if (!Design.IsDesignMode) DataContext = new ConnectControlsViewModel(); 14 | if (!Design.IsDesignMode) DataContext = App.ServiceProvider.GetService(); 15 | } 16 | } -------------------------------------------------------------------------------- /Companion/Converters/NullToBoolConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using Avalonia.Data.Converters; 4 | 5 | namespace Companion.Converters; 6 | 7 | public class NullToBoolConverter : IValueConverter 8 | { 9 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 10 | { 11 | return value != null; 12 | } 13 | 14 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 15 | { 16 | throw new NotImplementedException(); 17 | } 18 | } -------------------------------------------------------------------------------- /Companion/Services/Presets/IGitHubPresetService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Companion.Models.Presets; 4 | 5 | namespace Companion.Services.Presets; 6 | 7 | public interface IGitHubPresetService 8 | { 9 | Task> FetchPresetFilesAsync(Repository repository); 10 | 11 | // Task DownloadPresetAsync(string repoOwner, string repoName, string presetPath, string localBaseDirectory); 12 | Task> SyncRepositoryPresetsAsync(Repository repository, string localPresetsDirectory); 13 | } -------------------------------------------------------------------------------- /Companion/Models/Wfb.cs: -------------------------------------------------------------------------------- 1 | namespace Companion.Models; 2 | 3 | public static class Wfb 4 | { 5 | public const string Channel = "channel"; 6 | public const string Frequency = "frequency"; 7 | public const string DriverTxpowerOverride = "driver_txpower_override"; 8 | public const string Txpower = "txpower"; 9 | public const string Bandwidth = "bandwidth"; 10 | public const string Stbc = "stbc"; 11 | public const string Ldpc = "ldpc"; 12 | public const string McsIndex = "mcs_index"; 13 | public const string FecK = "fec_k"; 14 | public const string FecN = "fec_n"; 15 | } -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | authors: 6 | - octocat 7 | categories: 8 | - title: Breaking Changes 🛠 9 | labels: 10 | - breaking-change 11 | - title: New Features 🎉 12 | labels: 13 | - enhancement 14 | - title: Fixes 🐛 15 | labels: 16 | - bug 17 | - title: Translations 🌍 18 | labels: 19 | - translations 20 | - title: Other Changes 21 | labels: 22 | - "*" -------------------------------------------------------------------------------- /.vs/AnotherConfigurator/v17/DocumentLayout.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 1, 3 | "WorkspaceRootPath": "C:\\Users\\mcarr\\source\\repos\\AnotherConfigurator\\", 4 | "Documents": [], 5 | "DocumentGroupContainers": [ 6 | { 7 | "Orientation": 0, 8 | "VerticalTabListWidth": 256, 9 | "DocumentGroups": [ 10 | { 11 | "DockedWidth": 200, 12 | "SelectedChildIndex": -1, 13 | "Children": [ 14 | { 15 | "$type": "Bookmark", 16 | "Name": "ST:0:0:{e5c86464-96be-4d7c-9a8b-abcb3bbf5f92}" 17 | } 18 | ] 19 | } 20 | ] 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /Companion/Converters/BooleanToTextConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using Avalonia.Data.Converters; 4 | 5 | namespace Companion.Converters; 6 | 7 | public class BooleanToTextConverter : IValueConverter 8 | { 9 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 10 | { 11 | var texts = parameter.ToString().Split(','); 12 | return (bool)value ? texts[1] : texts[0]; 13 | } 14 | 15 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 16 | { 17 | throw new NotImplementedException(); 18 | } 19 | } -------------------------------------------------------------------------------- /Companion/Models/Presets/GitHubFile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Companion.Models.Presets; 4 | 5 | public class GitHubFile 6 | { 7 | /// 8 | /// The name of the file (e.g., "preset-config.yaml"). 9 | /// 10 | public string Name { get; set; } 11 | 12 | /// 13 | /// The relative path of the file in the repository (e.g., "presets/preset-config.yaml"). 14 | /// 15 | public string Path { get; set; } 16 | 17 | /// 18 | /// The URL to directly download the file content. 19 | /// 20 | public string DownloadUrl { get; set; } 21 | 22 | 23 | } -------------------------------------------------------------------------------- /Companion/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Serilog": { 3 | "WriteTo": [ 4 | { 5 | "Name": "File", 6 | "Args": { 7 | "path": "logs/log.txt", 8 | "rollingInterval": "Day" 9 | } 10 | }, 11 | { 12 | "Name": "Console", 13 | "Args": { 14 | "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level}] {SourceContext}: {Message}{NewLine}{Exception}" 15 | } 16 | } 17 | ], 18 | "MinimumLevel": { 19 | "Default": "Verbose", 20 | "Override": { 21 | "Microsoft": "Warning", 22 | "System": "Verbose" 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /Companion/Assets/Icons/icon_connect.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /Companion/Converters/BooleanToVisibilityConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using Avalonia.Data.Converters; 4 | using ExCSS; 5 | 6 | namespace Companion.Converters; 7 | 8 | public class BooleanToVisibilityConverter : IValueConverter 9 | { 10 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 11 | { 12 | var isVisible = (bool)value; 13 | return isVisible ? Visibility.Visible : Visibility.Collapse; 14 | } 15 | 16 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 17 | { 18 | throw new NotImplementedException(); 19 | } 20 | } -------------------------------------------------------------------------------- /Companion/Converters/BooleanToWidthConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using Avalonia.Data.Converters; 4 | 5 | namespace Companion.Converters; 6 | 7 | public class BooleanToWidthConverter : IValueConverter 8 | { 9 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 10 | { 11 | var widths = parameter.ToString().Split(','); 12 | return (bool)value ? double.Parse(widths[0]) : double.Parse(widths[1]); 13 | } 14 | 15 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 16 | { 17 | throw new NotImplementedException(); 18 | } 19 | } -------------------------------------------------------------------------------- /Companion/Converters/InvertedBooleanConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using Avalonia.Data.Converters; 4 | 5 | namespace Companion.Converters; 6 | 7 | public class InvertedBooleanConverter : IValueConverter 8 | 9 | { 10 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 11 | { 12 | if (value is bool booleanValue) return !booleanValue; 13 | return false; 14 | } 15 | 16 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 17 | { 18 | if (value is bool booleanValue) return !booleanValue; 19 | return false; 20 | } 21 | } -------------------------------------------------------------------------------- /Companion/App.axaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Companion/Converters/TagsToStringConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.ObjectModel; 3 | using System.Globalization; 4 | using Avalonia.Data.Converters; 5 | 6 | namespace Companion.Converters; 7 | 8 | public class TagsToStringConverter : IValueConverter 9 | { 10 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 11 | { 12 | if (value is ObservableCollection tags) 13 | { 14 | return string.Join(", ", tags); 15 | } 16 | return string.Empty; 17 | } 18 | 19 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 20 | { 21 | throw new NotImplementedException(); 22 | } 23 | } -------------------------------------------------------------------------------- /Companion/binaries/clean/telemetry.conf: -------------------------------------------------------------------------------- 1 | # 10/28/2024 2 | ### unit: drone or gs 3 | unit=drone 4 | 5 | serial=/dev/ttyS2 6 | baud=115200 7 | 8 | ### router: use simple mavfwd (0), classic mavlink-routerd (1) or msposd instead of mavfwd (2) 9 | router=0 10 | 11 | wlan=wlan0 12 | bandwidth=20 13 | stbc=1 14 | ldpc=1 15 | mcs_index=1 16 | stream_rx=144 17 | stream_tx=16 18 | link_id=7669206 19 | frame_type=data 20 | port_rx=14551 21 | port_tx=14550 22 | fec_k=1 23 | fec_n=2 24 | pool_timeout=0 25 | guard_interval=long 26 | one_way=false 27 | aggregate=15 28 | 29 | ### for mavfwd: RC override channels to parse after first 4 and call /usr/sbin/channels.sh $ch $val, default 0 30 | channels=8 31 | 32 | ### for msposd: OSD over video 33 | fps=20 34 | ahi=0 -------------------------------------------------------------------------------- /Companion/Assets/Icons/iconair_camera_light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Companion/ViewLocator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.Templates; 4 | using Companion.ViewModels; 5 | 6 | namespace Companion; 7 | 8 | public class ViewLocator : IDataTemplate 9 | { 10 | public Control? Build(object? data) 11 | { 12 | if (data is null) 13 | return null; 14 | 15 | var name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); 16 | var type = Type.GetType(name); 17 | 18 | if (type != null) return (Control)Activator.CreateInstance(type)!; 19 | 20 | return new TextBlock { Text = "Not Found: " + name }; 21 | } 22 | 23 | public bool Match(object? data) 24 | { 25 | return data is ViewModelBase; 26 | } 27 | } -------------------------------------------------------------------------------- /Companion/Events/RadxaContentUpdateEvent.cs: -------------------------------------------------------------------------------- 1 | using Prism.Events; 2 | 3 | namespace Companion.Events; 4 | 5 | public class RadxaContentUpdateChangeEvent : PubSubEvent 6 | { 7 | } 8 | 9 | public class RadxaContentUpdatedMessage 10 | { 11 | public string WifiBroadcastContent { get; set; } 12 | public string ScreenModeContent { get; set; } 13 | public string WfbConfContent { get; set; } 14 | 15 | public string DroneKeyContent { get; set; } 16 | 17 | 18 | public override string ToString() 19 | { 20 | return 21 | $"{nameof(WifiBroadcastContent)}: {WifiBroadcastContent}, {nameof(ScreenModeContent)}: {ScreenModeContent}, {nameof(WfbConfContent)}: {WfbConfContent}, {nameof(DroneKeyContent)}: {DroneKeyContent}"; 22 | } 23 | } -------------------------------------------------------------------------------- /Companion/Assets/Icons/iconoir_presets_light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Companion.iOS/AppDelegate.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.iOS; 3 | using Foundation; 4 | using Companion; 5 | 6 | namespace Companion.iOS; 7 | 8 | // The UIApplicationDelegate for the application. This class is responsible for launching the 9 | // User Interface of the application, as well as listening (and optionally responding) to 10 | // application events from iOS. 11 | [Register("AppDelegate")] 12 | #pragma warning disable CA1711 // Identifiers should not have incorrect suffix 13 | public class AppDelegate : AvaloniaAppDelegate 14 | #pragma warning restore CA1711 // Identifiers should not have incorrect suffix 15 | { 16 | protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) 17 | { 18 | return base.CustomizeAppBuilder(builder) 19 | .WithInterFont(); 20 | } 21 | } -------------------------------------------------------------------------------- /Companion/Models/Presets/RepositorySettings.cs: -------------------------------------------------------------------------------- 1 | namespace Companion.Models.Presets; 2 | 3 | /// 4 | /// Settings for a preset repository from configuration 5 | /// 6 | public class RepositorySettings 7 | { 8 | /// 9 | /// The full URL of the repository 10 | /// 11 | public string Url { get; set; } 12 | 13 | /// 14 | /// The branch to use for fetching presets 15 | /// 16 | public string Branch { get; set; } 17 | 18 | /// 19 | /// Optional description of the repository 20 | /// 21 | public string Description { get; set; } 22 | 23 | /// 24 | /// Indicates whether the repository is active 25 | /// 26 | public bool IsActive { get; set; } = true; 27 | } -------------------------------------------------------------------------------- /Companion/Assets/Icons/iconoir_presets_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Companion/Assets/Icons/iconair_advanced_light.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /Companion.Desktop/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Avalonia; 3 | using Companion; 4 | 5 | namespace Companion.Desktop; 6 | 7 | internal sealed class Program 8 | { 9 | // Initialization code. Don't use any Avalonia, third-party APIs or any 10 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized 11 | // yet and stuff might break. 12 | [STAThread] 13 | public static void Main(string[] args) 14 | { 15 | BuildAvaloniaApp() 16 | .StartWithClassicDesktopLifetime(args); 17 | } 18 | 19 | // Avalonia configuration, don't remove; also used by visual designer. 20 | public static AppBuilder BuildAvaloniaApp() 21 | { 22 | return AppBuilder.Configure() 23 | .UsePlatformDetect() 24 | .WithInterFont() 25 | .LogToTrace(); 26 | } 27 | } -------------------------------------------------------------------------------- /Companion/Assets/Icons/iconair_advanced_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /Companion/Models/WfbConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Companion.Models; 2 | 3 | public class WfbConfig 4 | { 5 | public string Unit { get; set; } 6 | public string Wlan { get; set; } 7 | public string Region { get; set; } 8 | public string Channel { get; set; } 9 | public int TxPower { get; set; } 10 | public int DriverTxPowerOverride { get; set; } 11 | public int Bandwidth { get; set; } 12 | public int Stbc { get; set; } 13 | public int Ldpc { get; set; } 14 | public int McsIndex { get; set; } 15 | public int Stream { get; set; } 16 | public long LinkId { get; set; } 17 | public int UdpPort { get; set; } 18 | public int RcvBuf { get; set; } 19 | public string FrameType { get; set; } 20 | public int FecK { get; set; } 21 | public int FecN { get; set; } 22 | public int PoolTimeout { get; set; } 23 | public string GuardInterval { get; set; } 24 | } -------------------------------------------------------------------------------- /README-Android.md: -------------------------------------------------------------------------------- 1 | # Android specific 2 | 3 | At this time, this is very alpha release. I have noticed that when using a tunnel on Radxa that the configurator would hang. 4 | This is a design issue so I am looking into this. It might have something to do with the command waiting for a response but the 5 | connection is broken and is unable to reconnect. 6 | * 7 | * Accessing AppData 8 | ```bash 9 | adb shell 10 | run-as org.openipc.Configurator 11 | ls /data/user/0/org.openipc.Configurator 12 | ``` 13 | 14 | or 15 | 16 | ```bash 17 | adb shell run-as org.openipc.Configurator ls /data/user/0/org.openipc.Configurator 18 | ``` 19 | 20 | * Accessing Binaries 21 | ```bash 22 | adb shell 23 | run-as org.openipc.Configurator 24 | ls -R /data/data/org.openipc.Configurator/files 25 | ``` 26 | 27 | or 28 | 29 | ```bash 30 | adb shell run-as org.openipc.Configurator ls /data/data/org.openipc.Configurator/files 31 | ``` -------------------------------------------------------------------------------- /Companion/Logging/EventAggregatorSink.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using Companion.Events; 4 | using Prism.Events; 5 | using Serilog.Core; 6 | using Serilog.Events; 7 | 8 | namespace Companion.Logging; 9 | 10 | public class EventAggregatorSink : ILogEventSink 11 | { 12 | private readonly IEventAggregator _eventAggregator; 13 | private readonly IFormatProvider _formatProvider; 14 | 15 | public EventAggregatorSink(IEventAggregator eventAggregator, IFormatProvider formatProvider = null) 16 | { 17 | _eventAggregator = eventAggregator; 18 | _formatProvider = formatProvider ?? CultureInfo.InvariantCulture; 19 | } 20 | 21 | public void Emit(LogEvent logEvent) 22 | { 23 | var message = logEvent.RenderMessage(_formatProvider); 24 | 25 | // Enqueue the log message instead of directly publishing 26 | LogQueue.Enqueue(message); 27 | } 28 | } -------------------------------------------------------------------------------- /Companion/Events/AppMessageEvent.cs: -------------------------------------------------------------------------------- 1 | using Companion.Models; 2 | using Prism.Events; 3 | 4 | namespace Companion.Events; 5 | 6 | public class AppMessageEvent : PubSubEvent 7 | { 8 | } 9 | 10 | public class AppMessage 11 | { 12 | private string _status = string.Empty; 13 | 14 | public bool UpdateLogView { get; set; } = false; 15 | 16 | public string Message { get; set; } = string.Empty; 17 | 18 | public string? Status 19 | { 20 | get => _status; 21 | set => _status = value; 22 | } 23 | 24 | public DeviceConfig DeviceConfig { get; set; } = DeviceConfig.Instance; 25 | 26 | 27 | public bool CanConnect { get; set; } 28 | 29 | public override string ToString() 30 | { 31 | return $"{nameof(Message)}: {Message}, {nameof(Status)}: {Status}, " + 32 | $"{nameof(DeviceConfig)}: {DeviceConfig}, {nameof(CanConnect)}: {CanConnect}"; 33 | } 34 | } -------------------------------------------------------------------------------- /Companion/ViewModels/PresetDetailsViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Runtime.CompilerServices; 3 | using Companion.Models.Presets; 4 | 5 | namespace Companion.ViewModels; 6 | 7 | public class PresetDetailsViewModel : INotifyPropertyChanged 8 | { 9 | private Preset? _preset; 10 | 11 | public Preset? Preset 12 | { 13 | get => _preset; 14 | set 15 | { 16 | if (_preset != value) 17 | { 18 | _preset = value; 19 | OnPropertyChanged(); 20 | } 21 | } 22 | } 23 | 24 | public event PropertyChangedEventHandler? PropertyChanged; 25 | 26 | protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) 27 | { 28 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 29 | } 30 | 31 | public PresetDetailsViewModel() 32 | { 33 | 34 | } 35 | } -------------------------------------------------------------------------------- /Companion.Android/Resources/values-v31/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 17 | 21 | 22 | -------------------------------------------------------------------------------- /Companion/Converters/EnumToBoolConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using Avalonia.Data.Converters; 4 | 5 | namespace Companion.Converters; 6 | 7 | public class EnumToBoolConverter : IValueConverter 8 | { 9 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 10 | { 11 | if (value != null && value.GetType().IsEnum) return (int)value == (int)parameter; 12 | return false; 13 | } 14 | 15 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 16 | { 17 | if (targetType.IsEnum) 18 | if (value is bool boolValue) 19 | { 20 | if (boolValue) 21 | return Enum.Parse(targetType, parameter.ToString()); 22 | return Enum.Parse(targetType, "None"); // or some other default value 23 | } 24 | 25 | throw new ArgumentException("Unsupported type", nameof(value)); 26 | } 27 | } -------------------------------------------------------------------------------- /get-latest-binaries.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | MSP_OSD_URL="https://github.com/OpenIPC/msposd/releases/download/latest/msposd_star6e" 4 | BIN_FOLDER="OpenIPC_Config/binaries" 5 | FONTS_FOLDER="${BIN_FOLDER}/fonts" 6 | 7 | echo "Downloading latest binaries..." 8 | 9 | curl -L "$MSP_OSD_URL" -o $BIN_FOLDER/msposd 10 | chmod +x OpenIPC/binaries/msposd 11 | 12 | ## Fonts 13 | 14 | ## INav 15 | 16 | echo "Downloading INav font..." 17 | 18 | curl -k -L -o $FONTS_FOLDER/inav/font.png https://raw.githubusercontent.com/openipc/msposd/main/fonts/font_inav.png 19 | curl -k -L -o $FONTS_FOLDER/inav/font_hd.png https://raw.githubusercontent.com/openipc/msposd/main/fonts/font_inav_hd.png 20 | 21 | ## Betaflight 22 | 23 | echo "Downloading Betaflight font..." 24 | 25 | curl -k -L -o $FONTS_FOLDER/bf/font.png https://raw.githubusercontent.com/openipc/msposd/main/fonts/font_btfl.png 26 | curl -k -L -o $FONTS_FOLDER/bf/font_hd.png https://raw.githubusercontent.com/openipc/msposd/main/fonts/font_btfl_hd.png 27 | -------------------------------------------------------------------------------- /Companion/Models/WfbYaml.cs: -------------------------------------------------------------------------------- 1 | namespace Companion.Models; 2 | 3 | /** 4 | * https://github.com/svpcom/wfb-ng 5 | * https://github.com/OpenIPC/firmware/tree/master/general/package/wifibroadcast-ng/files 6 | */ 7 | public class WfbYaml 8 | { 9 | public const string WfbTxPower = "wireless.txpower"; 10 | public const string WfbChannel = "wireless.channel"; 11 | public const string WfbBandwidth = "wireless.width"; 12 | public const string WfbMlink = "wireless.mlink"; 13 | 14 | public const string BroadcastMcsIndex = "broadcast.mcs_index"; 15 | public const string BroadcastFecK = "broadcast.fec_k"; 16 | public const string BroadcastFecN = "broadcast.fec_n"; 17 | public const string BroadcastStbc = "broadcast.stbc"; 18 | public const string BroadcastLdpc = "broadcast.ldpc"; 19 | 20 | public const string TelemetrySerialPort = "telemetry.serial"; 21 | public const string TelemetryRouter = "telemetry.router"; 22 | public const string TelemetryOsdFps = "telemetry.osd_fps"; 23 | 24 | 25 | 26 | } -------------------------------------------------------------------------------- /Companion.Desktop/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Companion/Services/NetworkCommands.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.NetworkInformation; 3 | using System.Threading.Tasks; 4 | using Companion.Models; 5 | using Serilog; 6 | 7 | namespace Companion.Services; 8 | 9 | public class NetworkCommands : INetworkCommands 10 | { 11 | // Create a new Ping instance 12 | private readonly Ping ping; 13 | 14 | public NetworkCommands() 15 | { 16 | ping = new Ping(); 17 | } 18 | 19 | public Task Run(DeviceConfig deviceConfig, string command) 20 | { 21 | throw new NotImplementedException(); 22 | } 23 | 24 | public Task Ping(string ipAddress) 25 | { 26 | // Send a ping request 27 | var reply = ping.Send(ipAddress); 28 | 29 | // Check the status of the ping request 30 | if (reply.Status == IPStatus.Success) 31 | { 32 | Log.Verbose($"Ping successful: {reply.Status}"); 33 | return Task.FromResult(true); 34 | } 35 | 36 | Log.Verbose($"Ping failed: {reply.Status}"); 37 | return Task.FromResult(false); 38 | } 39 | } -------------------------------------------------------------------------------- /Companion/Models/Presets/PresetIndex.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using YamlDotNet.Serialization; 4 | 5 | 6 | namespace Companion.Models.Presets; 7 | 8 | /// 9 | /// Represents the structure of the PRESET_INDEX.yaml file 10 | /// 11 | public class PresetIndex 12 | { 13 | /// 14 | /// Version of the preset index format 15 | /// 16 | [YamlMember(Alias = "version")] 17 | public string Version { get; set; } 18 | 19 | /// 20 | /// Timestamp of when the index was last updated 21 | /// 22 | [YamlMember(Alias = "last_updated")] 23 | public DateTime LastUpdated { get; set; } 24 | 25 | /// 26 | /// Total number of presets in the index 27 | /// 28 | [YamlMember(Alias = "total_presets")] 29 | public int TotalPresets { get; set; } 30 | 31 | /// 32 | /// Collection of preset entries 33 | /// 34 | [YamlMember(Alias = "presets")] 35 | public List Presets { get; set; } = new(); 36 | } 37 | 38 | -------------------------------------------------------------------------------- /Companion/Assets/Icons/iconoir_settings_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Companion/Assets/Icons/iconoir_settings_light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Companion/Converters/BooleanGreaterThanConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using Avalonia.Data.Converters; 4 | 5 | namespace Companion.Converters; 6 | 7 | /// 8 | /// Converts a numeric value to a boolean by comparing it with a threshold 9 | /// 10 | public class BooleanGreaterThanConverter : IValueConverter 11 | { 12 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 13 | { 14 | if (value == null || parameter == null) 15 | return false; 16 | 17 | double threshold; 18 | if (!double.TryParse(parameter.ToString(), out threshold)) 19 | return false; 20 | 21 | if (value is double doubleValue) 22 | return doubleValue > threshold; 23 | 24 | if (value is int intValue) 25 | return intValue > threshold; 26 | 27 | return false; 28 | } 29 | 30 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 31 | { 32 | throw new NotImplementedException(); 33 | } 34 | } -------------------------------------------------------------------------------- /Companion/Services/DeviceConfigValidator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Microsoft.Extensions.Configuration; 4 | using Companion.Models; 5 | 6 | namespace Companion.Services; 7 | 8 | public class DeviceConfigValidator 9 | { 10 | private readonly IConfiguration _configuration; 11 | private readonly Dictionary> _deviceHostnameMapping; 12 | 13 | public DeviceConfigValidator(IConfiguration configuration) 14 | { 15 | _configuration = configuration; 16 | 17 | _deviceHostnameMapping = _configuration.GetSection("DeviceHostnameMapping") 18 | .Get>>() 19 | ?? new Dictionary>(); 20 | } 21 | 22 | public bool IsDeviceConfigValid(DeviceConfig deviceConfig) 23 | { 24 | if (_deviceHostnameMapping.TryGetValue(deviceConfig.DeviceType, out var allowedHostnames)) 25 | return allowedHostnames.Any(hostname => deviceConfig.Hostname.Contains(hostname)); 26 | 27 | return false; // Invalid if no mapping exists 28 | } 29 | } -------------------------------------------------------------------------------- /Companion/Assets/Icons/iconair_firmware_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Companion/Assets/Icons/iconair_firmware_light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Companion/Assets/Icons/iconoir_cube.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Companion/Logging/LogQueue.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Collections.Generic; 3 | 4 | namespace Companion.Logging; 5 | 6 | public static class LogQueue 7 | { 8 | private static readonly ConcurrentQueue _queue = new ConcurrentQueue(); 9 | private static bool _isReady = false; 10 | private static readonly List> _subscribers = new List>(); 11 | 12 | public static void Enqueue(string message) 13 | { 14 | _queue.Enqueue(message); 15 | if (_isReady) 16 | { 17 | while (_queue.TryDequeue(out var queuedMessage)) 18 | { 19 | foreach (var subscriber in _subscribers) 20 | subscriber(queuedMessage); 21 | } 22 | } 23 | } 24 | 25 | public static void Subscribe(System.Action subscriber) 26 | { 27 | _subscribers.Add(subscriber); 28 | 29 | // If queue has messages, trigger the event immediately 30 | while (_queue.TryDequeue(out var message)) 31 | { 32 | subscriber(message); 33 | } 34 | 35 | _isReady = true; 36 | } 37 | } -------------------------------------------------------------------------------- /Companion/Assets/Icons/ic-telegram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Companion/Services/NetworkHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.NetworkInformation; 3 | using System.Net.Sockets; 4 | 5 | namespace Companion.Services; 6 | 7 | public class NetworkHelper 8 | { 9 | public static string GetLocalIPAddress() 10 | { 11 | try 12 | { 13 | var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces(); 14 | foreach (var netInterface in networkInterfaces) 15 | { 16 | // Ignore loopback and inactive interfaces 17 | if (netInterface.OperationalStatus != OperationalStatus.Up || 18 | netInterface.NetworkInterfaceType == NetworkInterfaceType.Loopback) 19 | continue; 20 | 21 | var properties = netInterface.GetIPProperties(); 22 | foreach (var address in properties.UnicastAddresses) 23 | if (address.Address.AddressFamily == AddressFamily.InterNetwork) // IPv4 24 | return address.Address.ToString(); 25 | } 26 | 27 | throw new Exception("No valid network interfaces found."); 28 | } 29 | catch (Exception ex) 30 | { 31 | Console.WriteLine($"Error: {ex.Message}"); 32 | return null; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /Companion/Events/DeviceContentUpdateEvent.cs: -------------------------------------------------------------------------------- 1 | using Companion.Models; 2 | using Prism.Events; 3 | 4 | namespace Companion.Events; 5 | 6 | public class DeviceContentUpdateEvent : PubSubEvent 7 | { 8 | } 9 | 10 | public class DeviceContentUpdatedMessage 11 | { 12 | // Dictionary to store content as key-value pairs 13 | 14 | 15 | //public Dictionary DeviceContent { get; set; } = new Dictionary(); 16 | 17 | public DeviceConfig DeviceConfig { get; set; } = new(); 18 | public string WifiBroadcastContent { get; set; } = string.Empty; 19 | public string ScreenModeContent { get; set; } = string.Empty; 20 | public string WfbConfContent { get; set; } = string.Empty; 21 | 22 | public string MajesticContent { get; set; } = string.Empty; 23 | 24 | public string TelemetryContent { get; set; } = string.Empty; 25 | 26 | 27 | public override string ToString() 28 | { 29 | return 30 | $"{nameof(DeviceConfig)}: {DeviceConfig}, {nameof(WifiBroadcastContent)}: {WifiBroadcastContent}, " + 31 | $"{nameof(ScreenModeContent)}: {ScreenModeContent}, {nameof(WfbConfContent)}: {WfbConfContent}, " + 32 | $"{nameof(MajesticContent)}: {MajesticContent}, {nameof(TelemetryContent)}: {TelemetryContent}"; 33 | } 34 | } -------------------------------------------------------------------------------- /Companion.Tests/DependencyInjectionTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Moq; 3 | using Companion.Services; 4 | using Companion.ViewModels; 5 | using Prism.Events; 6 | using Serilog; 7 | 8 | namespace OpenIPC.Companion.Tests; 9 | 10 | public class DependencyInjectionTests 11 | { 12 | [Test] 13 | public void ViewModel_CanBeResolvedFromDI() 14 | { 15 | // Arrange 16 | var services = new ServiceCollection(); 17 | 18 | services.AddSingleton(); 19 | services.AddSingleton(); 20 | services.AddSingleton(); 21 | services.AddSingleton(); 22 | 23 | 24 | var loggerMock = new Mock(); 25 | loggerMock.Setup(x => x.ForContext(It.IsAny())).Returns(loggerMock.Object); 26 | services.AddSingleton(sp => loggerMock.Object); 27 | 28 | services.AddTransient(); 29 | 30 | var serviceProvider = services.BuildServiceProvider(); 31 | 32 | // Act 33 | var viewModel = serviceProvider.GetService(); 34 | 35 | // Assert 36 | Assert.IsNotNull(viewModel); 37 | } 38 | } -------------------------------------------------------------------------------- /Companion/Views/MainView.axaml.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Avalonia.Controls; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Companion.Events; 5 | using Companion.Services; 6 | using Companion.ViewModels; 7 | 8 | namespace Companion.Views; 9 | 10 | public partial class MainView : UserControl 11 | { 12 | public MainView() 13 | { 14 | InitializeComponent(); 15 | 16 | if (!Design.IsDesignMode) 17 | { 18 | // Resolve MainViewModel from the DI container 19 | DataContext = App.ServiceProvider.GetRequiredService(); 20 | 21 | // Subscribe to TabSelectionChangeEvent 22 | var _eventSubscriptionService = App.ServiceProvider.GetRequiredService(); 23 | 24 | _eventSubscriptionService.Subscribe(OnTabSelectionChanged); 25 | } 26 | } 27 | 28 | private void OnTabSelectionChanged(string selectedTab) 29 | { 30 | var tabControl = this.FindControl("MainTabControl"); 31 | if (tabControl?.Items == null) return; 32 | 33 | var targetTab = tabControl.Items 34 | .OfType() 35 | .FirstOrDefault(tab => tab.TabName == selectedTab); 36 | 37 | if (targetTab != null) tabControl.SelectedItem = targetTab; 38 | } 39 | } -------------------------------------------------------------------------------- /Companion/Services/Presets/IPresetService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Companion.Models.Presets; 3 | 4 | namespace Companion.Services.Presets; 5 | 6 | /// 7 | /// Interface for Preset Service to apply and manage presets 8 | /// 9 | public interface IPresetService 10 | { 11 | /// 12 | /// Apply a preset to the device 13 | /// 14 | /// The preset to apply 15 | /// Task representing the asynchronous operation 16 | Task ApplyPresetAsync(Preset preset); 17 | 18 | /// 19 | /// Create a preset from current device configuration 20 | /// 21 | /// Name for the new preset 22 | /// Category for the new preset 23 | /// Description for the new preset 24 | /// The newly created preset 25 | Task CreatePresetFromCurrentConfigAsync(string name, string category, string description); 26 | 27 | /// 28 | /// Load the content of preset files from disk 29 | /// 30 | /// The preset to load files for 31 | /// Task representing the asynchronous operation 32 | Task LoadPresetFilesAsync(Preset preset); 33 | } -------------------------------------------------------------------------------- /Companion/Models/Majestic.cs: -------------------------------------------------------------------------------- 1 | namespace Companion.Models; 2 | 3 | public static class Majestic 4 | { 5 | public const string VideoFps = "video0.fps"; 6 | public const string VideoSize = "video0.size"; 7 | public const string VideoCodec = "video0.codec"; 8 | public const string VideoBitrate = "video0.bitrate"; 9 | public const string IspExposure = "isp.exposure"; 10 | public const string ImageContrast = "image.contrast"; 11 | public const string ImageHue = "image.hue"; 12 | public const string ImageSaturation = "image.saturation"; 13 | public const string ImageLuminance = "image.luminance"; 14 | public const string ImageFlip = "image.flip"; 15 | public const string ImageMirror = "image.mirror"; 16 | 17 | public const string FpvEnabled = "fpv.enabled"; 18 | public const string FpvNoiseLevel = "fpv.noiseLevel"; 19 | public const string FpvRoiQp = "fpv.roiQp"; 20 | public const string FpvRefEnhance = "fpv.refEnhance"; 21 | public const string FpvRefPred = "fpv.refPred"; 22 | public const string FpvIntraLine = "fpv.intraLine"; 23 | public const string FpvIntraQp = "fpv.intraQp"; 24 | public const string FpvRoiRect = "fpv.roiRect"; 25 | 26 | public const string RecordsEnabled = "records.enabled"; 27 | public const string AudioEnabled = "audio.enabled"; 28 | public const string RecordsNoTime = "records.notime"; 29 | 30 | } -------------------------------------------------------------------------------- /Companion.Android/MainActivity.cs: -------------------------------------------------------------------------------- 1 | using Android.App; 2 | using Android.Content.PM; 3 | using Android.OS; 4 | using Android.Views; 5 | using Avalonia; 6 | using Avalonia.Android; 7 | using Companion.Android; 8 | using OpenIPC.Companion.Android.Helpers; 9 | using Application = Android.App.Application; 10 | 11 | namespace Companion.Android; 12 | 13 | [Activity( 14 | Label = "OpenIPC.Companion.Android", 15 | Theme = "@style/MyTheme.NoActionBar", 16 | Icon = "@drawable/icon", 17 | MainLauncher = true, 18 | ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] 19 | public class MainActivity : AvaloniaMainActivity 20 | { 21 | // Adjust the layout when the keyboard is shown 22 | protected override void OnCreate(Bundle? savedInstanceState) 23 | { 24 | base.OnCreate(savedInstanceState); 25 | 26 | // Hide the soft keyboard initially 27 | Window.SetSoftInputMode(SoftInput.StateHidden); 28 | 29 | // Optionally, adjust the layout when the keyboard is shown 30 | Window.SetSoftInputMode(SoftInput.AdjustResize); 31 | } 32 | 33 | protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) 34 | { 35 | AndroidFileHelper.CopyAssetsToInternalStorage(Application.Context); 36 | 37 | return base.CustomizeAppBuilder(builder) 38 | .WithInterFont(); 39 | } 40 | } -------------------------------------------------------------------------------- /Companion/Assets/Icons/mdi-github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Companion/Views/HeaderView.axaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Avalonia.Controls; 3 | using Avalonia.Input; 4 | using Avalonia.Interactivity; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Companion.ViewModels; 7 | 8 | namespace Companion.Views; 9 | 10 | public partial class HeaderView : UserControl 11 | { 12 | 13 | private const string TelegramLink = "https://t.me/+BMyMoolVOpkzNWUy"; 14 | private const string GithubLink = "https://github.com/OpenIPC/"; 15 | //private const string DiscordLink = "https://discord.gg/KtWgDV6Y"; 16 | 17 | public HeaderView() 18 | { 19 | InitializeComponent(); 20 | 21 | if (!Design.IsDesignMode) DataContext = App.ServiceProvider.GetService(); 22 | } 23 | 24 | 25 | private void TelegramButton_OnClick(object? sender, RoutedEventArgs e) 26 | { 27 | var launcher = TopLevel.GetTopLevel(this).Launcher; 28 | launcher.LaunchUriAsync(new Uri(TelegramLink)); 29 | } 30 | 31 | private void GithubButton_OnClick(object? sender, RoutedEventArgs e) 32 | { 33 | var launcher = TopLevel.GetTopLevel(this).Launcher; 34 | launcher.LaunchUriAsync(new Uri(GithubLink)); 35 | } 36 | 37 | // private void DiscordButton_OnClick(object? sender, RoutedEventArgs e) 38 | // { 39 | // var launcher = TopLevel.GetTopLevel(this).Launcher; 40 | // launcher.LaunchUriAsync(new Uri(DiscordLink)); 41 | // } 42 | } -------------------------------------------------------------------------------- /Companion/binaries/clean/majestic.yaml: -------------------------------------------------------------------------------- 1 | #10/28/2024 2 | system: 3 | webPort: 80 4 | httpsPort: 443 5 | logLevel: debug 6 | isp: 7 | antiFlicker: disabled 8 | sensorConfig: /etc/sensors/imx415_fpv.bin 9 | exposure: 5 10 | image: 11 | mirror: false 12 | flip: false 13 | rotate: 0 14 | contrast: 50 15 | hue: 50 16 | saturation: 50 17 | luminance: 50 18 | video0: 19 | enabled: true 20 | codec: h265 21 | fps: 60 22 | bitrate: 4096 23 | rcMode: cbr 24 | gopSize: 1.0 25 | size: 1920x1080 26 | video1: 27 | enabled: false 28 | codec: h264 29 | size: 704x576 30 | fps: 15 31 | jpeg: 32 | enabled: false 33 | qfactor: 50 34 | fps: 5 35 | osd: 36 | enabled: false 37 | font: "/usr/share/fonts/truetype/UbuntuMono-Regular.ttf" 38 | template: "%d.%m.%Y %H:%M:%S" 39 | posX: 16 40 | posY: 16 41 | audio: 42 | enabled: false 43 | volume: 30 44 | srate: 8000 45 | codec: opus 46 | outputEnabled: false 47 | outputVolume: 30 48 | rtsp: 49 | enabled: true 50 | port: 554 51 | nightMode: 52 | colorToGray: true 53 | irCutSingleInvert: false 54 | lightMonitor: false 55 | lightSensorInvert: false 56 | motionDetect: 57 | enabled: false 58 | visualize: false 59 | debug: false 60 | records: 61 | enabled: false 62 | path: "/mnt/mmcblk0p1/%F" 63 | split: 20 64 | maxUsage: 95 65 | outgoing: 66 | enabled: true 67 | server: udp://127.0.0.1:5600 68 | watchdog: 69 | enabled: true 70 | timeout: 300 71 | hls: 72 | enabled: false -------------------------------------------------------------------------------- /Companion/Assets/Icons/iconoir_github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /Companion/Assets/Resources.es.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | text/microsoft-resx 12 | 13 | 14 | 1.3 15 | 16 | 17 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, 18 | PublicKeyToken=b77a5c561934e089 19 | 20 | 21 | 22 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, 23 | PublicKeyToken=b77a5c561934e089 24 | 25 | 26 | 27 | 1) Agregar la temperatura del SOC al OSD 28 | 2) Configurar baja latencia para los interruptores de channels.sh 29 | 30 | 31 | Muestra el estado del resultado de Ping cuando se ingresa una IP válida: 32 | 33 | • Verde: El resultado del ping es válido. 34 | • Rojo: El resultado del ping no es válido. 35 | 36 | -------------------------------------------------------------------------------- /Companion/Assets/Icons/iconoir_camera_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Companion/Converters/PowerThresholdColorConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using Avalonia.Data.Converters; 4 | using Avalonia.Media; 5 | 6 | namespace Companion.Converters; 7 | 8 | /// 9 | /// Converts a numeric value to a color based on a threshold 10 | /// 11 | public class PowerThresholdColorConverter : IValueConverter 12 | { 13 | /// 14 | /// Threshold value at which color changes 15 | /// 16 | public double Threshold { get; set; } = 25; 17 | 18 | /// 19 | /// Color when value is below threshold 20 | /// 21 | public ISolidColorBrush NormalColor { get; set; } = new SolidColorBrush(Colors.Black); 22 | 23 | /// 24 | /// Color when value exceeds threshold 25 | /// 26 | public ISolidColorBrush WarningColor { get; set; } = new SolidColorBrush(Colors.Red); 27 | 28 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 29 | { 30 | if (value is double doubleValue) 31 | { 32 | return doubleValue > Threshold ? WarningColor : NormalColor; 33 | } 34 | 35 | if (value is int intValue) 36 | { 37 | return intValue > Threshold ? WarningColor : NormalColor; 38 | } 39 | 40 | return NormalColor; 41 | } 42 | 43 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 44 | { 45 | throw new NotImplementedException(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Feature request 2 | description: Suggest an idea for this project 3 | title: "[Feature]: " 4 | labels: ["enhancement"] 5 | body: 6 | - type: checkboxes 7 | attributes: 8 | label: Requirements 9 | options: 10 | - label: I have updated Configurator to the latest available version 11 | required: true 12 | - label: I did a search to see if there is a similar issue or if a pull request is open. 13 | required: true 14 | - type: textarea 15 | attributes: 16 | label: Is your feature request related to a problem? 17 | description: > 18 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 19 | validations: 20 | required: true 21 | - type: textarea 22 | attributes: 23 | label: Describe the solution you'd like 24 | description: > 25 | A clear and concise description of what you want to happen. 26 | validations: 27 | required: true 28 | - type: textarea 29 | attributes: 30 | label: Describe alternatives you've considered 31 | description: > 32 | A clear and concise description of any alternative solutions or features you've considered. 33 | - type: textarea 34 | attributes: 35 | label: Additional context 36 | description: > 37 | Add any other context or screenshots about the feature request. 38 | - type: markdown 39 | attributes: 40 | value: > 41 | Thanks for contributing 🎉 42 | -------------------------------------------------------------------------------- /Companion/Views/SetupCameraButtonsView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Companion.ViewModels; 4 | 5 | namespace Companion.Views; 6 | 7 | public partial class SetupCameraButtonsView : UserControl 8 | { 9 | public SetupCameraButtonsView() 10 | { 11 | InitializeComponent(); 12 | if (!Design.IsDesignMode) DataContext = App.ServiceProvider.GetService<SetupTabViewModel>(); 13 | 14 | //ScriptFilesActionButton.IsEnabled = false; 15 | //CameraKeyActionButton.IsEnabled = false; 16 | } 17 | 18 | 19 | // private void ButtonsComboBox_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) 20 | // { 21 | // if (sender is ComboBox comboBox) 22 | // { 23 | // // Get the selected index 24 | // var selectedIndex = comboBox.SelectedIndex; 25 | // 26 | // // Example: Enable/disable a button based on the selected index 27 | // switch (comboBox.Name) 28 | // { 29 | // case "ScriptFilesActionComboBox": 30 | // ScriptFilesActionButton.IsEnabled = selectedIndex > -1; 31 | // break; 32 | // 33 | // case "CameraKeyActionButton": 34 | // CameraKeyActionButton.IsEnabled = selectedIndex > -1; 35 | // break; 36 | // 37 | // case "SensorComboBox": 38 | // SensorComboBox.IsEnabled = selectedIndex > -1; 39 | // break; 40 | // 41 | // default: 42 | // Log.Debug($"Unhandled ComboBox: {comboBox.Name}"); 43 | // break; 44 | // } 45 | // } 46 | // } 47 | } -------------------------------------------------------------------------------- /Presets.md: -------------------------------------------------------------------------------- 1 | # How Presets Work 2 | 3 | --- 4 | 5 | ## 1. Folder Structure 6 | 7 | Each preset is a self-contained folder under the `presets/` directory. It includes: 8 | 9 | - **`preset-config.yaml`**: Defines preset metadata, attributes, modified files and updated data. 10 | - **YAML configuration files** (e.g., `wfb.yaml`, `majestic.yaml`). 11 | - **Conf configuration files** (e.g., `wfb.conf`, `telemetry.conf`). 12 | - **Optional `sensor/` folder**: Not supported yet 13 | 14 | **Example Structure**: 15 | ``` 16 | presets/ 17 | ├── high_power_fpv/ 18 | │ ├── preset-config.yaml 19 | │ ├── sensor/ 20 | │ └── milos-sensor.bin 21 | ``` 22 | 23 | --- 24 | 25 | ## 2. Preset Definition (`preset-config.yaml`) 26 | 27 | The `preset-config.yaml` file defines: 28 | 29 | - **Metadata**: `name`, `author`, `description`, and `category`. 30 | - **Optional Sensor**: Specifies a binary file (e.g., `milos-sensor.bin`) to be transferred to the remote device. 31 | - **Files**: Specifies files and their key-value modifications. 32 | 33 | **Example**: 34 | ```yaml 35 | name: "High Power FPV" 36 | author: "OpenIPC" 37 | description: "Optimized settings for high-power FPV." 38 | sensor: "milos-sensor.bin" 39 | files: 40 | wfb.yaml: 41 | wireless.txpower: "30" 42 | wireless.channel: "161" 43 | majestic.yaml: 44 | fpv.enabled: "true" 45 | system.logLevel: "info" 46 | ``` 47 | 48 | --- 49 | 50 | ## 3. Preset Loading, two options 51 | 1) Submit pull request https://github.com/OpenIPC/fpv-presets 52 | 2) Local 53 | - The application scans the `presets/` directory. 54 | - It parses each `preset-config.yaml` to create a `Preset` object. 55 | - File modifications are transformed into a bindable `ObservableCollection<FileModification>` for the UI. 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /Companion/Services/VersionHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Serilog; 4 | 5 | namespace Companion.Services; 6 | 7 | public interface IFileSystem 8 | { 9 | bool Exists(string path); 10 | string ReadAllText(string path); 11 | } 12 | 13 | public class FileSystem : IFileSystem 14 | { 15 | public bool Exists(string path) 16 | { 17 | return File.Exists(path); 18 | } 19 | 20 | public string ReadAllText(string path) 21 | { 22 | return File.ReadAllText(path); 23 | } 24 | } 25 | 26 | public static class VersionHelper 27 | { 28 | private static IFileSystem _fileSystem = new FileSystem(); 29 | 30 | public static void SetFileSystem(IFileSystem fileSystem) 31 | { 32 | _fileSystem = fileSystem; 33 | } 34 | 35 | 36 | public static string GetAppVersion() 37 | { 38 | try 39 | { 40 | var versionFilePath = Path.Combine(AppContext.BaseDirectory, "VERSION"); 41 | if (_fileSystem.Exists(versionFilePath)) return _fileSystem.ReadAllText(versionFilePath).Trim(); 42 | return "v0.0.1"; 43 | 44 | 45 | // return Assembly.GetExecutingAssembly() 46 | // .GetCustomAttribute<AssemblyInformationalVersionAttribute>()? 47 | // .InformationalVersion ?? "Unknown Version"; 48 | } 49 | catch (Exception ex) 50 | { 51 | Log.Error($"Failed to get app version: {ex}"); 52 | return "Unknown Version"; 53 | } 54 | } 55 | 56 | private static bool IsDevelopment() 57 | { 58 | var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); 59 | return string.Equals(environment, "Development", StringComparison.OrdinalIgnoreCase); 60 | } 61 | } -------------------------------------------------------------------------------- /Companion/Services/EventSubscriptionService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Prism.Events; 3 | using Serilog; 4 | 5 | namespace Companion.Services; 6 | 7 | public interface IEventSubscriptionService 8 | { 9 | void Subscribe<TEvent, TPayload>(Action<TPayload> action) where TEvent : PubSubEvent<TPayload>, new(); 10 | 11 | void Publish<TEvent, TPayload>(TPayload payload) where TEvent : PubSubEvent<TPayload>, new(); 12 | } 13 | 14 | public class EventSubscriptionService : IEventSubscriptionService 15 | { 16 | private readonly IEventAggregator _eventAggregator; 17 | private readonly ILogger _logger; 18 | 19 | public EventSubscriptionService(IEventAggregator eventAggregator, ILogger logger) 20 | { 21 | _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); 22 | _logger = logger?.ForContext(GetType()) ?? 23 | throw new ArgumentNullException(nameof(logger)); 24 | } 25 | 26 | public void Subscribe<TEvent, TPayload>(Action<TPayload> action) where TEvent : PubSubEvent<TPayload>, new() 27 | { 28 | // Use BackgroundThread for testing to avoid the UI thread restriction 29 | _eventAggregator.GetEvent<TEvent>().Subscribe(action, ThreadOption.BackgroundThread); 30 | _logger.Verbose($"Subscribed to event {typeof(TEvent).Name}"); 31 | Console.WriteLine($"Subscribe: Payload received: {action}"); 32 | } 33 | 34 | public void Publish<TEvent, TPayload>(TPayload payload) where TEvent : PubSubEvent<TPayload>, new() 35 | { 36 | _eventAggregator.GetEvent<TEvent>().Publish(payload); 37 | _logger.Verbose($"Published event {typeof(TEvent).Name} with payload {payload}"); 38 | Console.WriteLine($"Publish: Payload received: {payload}"); 39 | } 40 | } -------------------------------------------------------------------------------- /Companion/ViewModels/TabItemViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Companion.ViewModels; 4 | 5 | /// <summary> 6 | /// ViewModel for managing individual tab items in the application 7 | /// </summary> 8 | public class TabItemViewModel 9 | { 10 | #region Public Properties 11 | /// <summary> 12 | /// Gets the display name of the tab 13 | /// </summary> 14 | public string TabName { get; } 15 | 16 | /// <summary> 17 | /// Gets the content associated with the tab 18 | /// </summary> 19 | public object Content { get; } 20 | 21 | /// <summary> 22 | /// Gets the icon path/name for the tab 23 | /// </summary> 24 | public string Icon { get; } 25 | 26 | /// <summary> 27 | /// Gets or sets whether the tabs are in collapsed state 28 | /// </summary> 29 | public bool IsTabsCollapsed { get; set; } 30 | #endregion 31 | 32 | #region Constructor 33 | /// <summary> 34 | /// Initializes a new instance of TabItemViewModel 35 | /// </summary> 36 | /// <param name="tabName">The name to display for the tab</param> 37 | /// <param name="icon">The icon to display for the tab</param> 38 | /// <param name="content">The content to display in the tab</param> 39 | /// <param name="isTabsCollapsed">Whether the tab should start collapsed</param> 40 | public TabItemViewModel( 41 | string tabName, 42 | string icon, 43 | object content, 44 | bool isTabsCollapsed) 45 | { 46 | TabName = tabName ?? throw new ArgumentNullException(nameof(tabName)); 47 | Icon = icon ?? throw new ArgumentNullException(nameof(icon)); 48 | Content = content ?? throw new ArgumentNullException(nameof(content)); 49 | IsTabsCollapsed = isTabsCollapsed; 50 | } 51 | #endregion 52 | } -------------------------------------------------------------------------------- /Companion.Tests/Services/VersionHelperTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using Companion.Services; 3 | 4 | namespace OpenIPC.Companion.Tests.Services; 5 | 6 | [TestFixture] 7 | public class VersionHelperTests 8 | { 9 | [SetUp] 10 | public void SetUp() 11 | { 12 | _mockFileSystem = new Mock<IFileSystem>(); 13 | VersionHelper.SetFileSystem(_mockFileSystem.Object); 14 | } 15 | 16 | [TearDown] 17 | public void TearDown() 18 | { 19 | // Reset the file system to the default implementation 20 | VersionHelper.SetFileSystem(new FileSystem()); 21 | } 22 | 23 | private Mock<IFileSystem> _mockFileSystem; 24 | 25 | [Test] 26 | public void GetAppVersion_ReturnsVersionFromFile_InDevelopmentEnvironment() 27 | { 28 | // Arrange 29 | Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development"); 30 | var expectedVersion = "1.0.0-test"; 31 | 32 | _mockFileSystem.Setup(fs => fs.Exists(It.IsAny<string>())).Returns(true); 33 | _mockFileSystem.Setup(fs => fs.ReadAllText(It.IsAny<string>())).Returns(expectedVersion); 34 | 35 | // Act 36 | var version = VersionHelper.GetAppVersion(); 37 | 38 | // Assert 39 | Assert.That(version, Is.EqualTo(expectedVersion)); 40 | } 41 | 42 | 43 | [Test] 44 | public void GetAppVersion_ReturnsUnknownVersion_OnException() 45 | { 46 | // Arrange 47 | Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development"); 48 | _mockFileSystem.Setup(fs => fs.Exists(It.IsAny<string>())).Throws(new Exception("Test exception")); 49 | 50 | // Act 51 | var version = VersionHelper.GetAppVersion(); 52 | 53 | // Assert 54 | Assert.That(version, Is.EqualTo("Unknown Version")); 55 | } 56 | } -------------------------------------------------------------------------------- /Companion/ViewModels/StatusBarViewModel.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | using Companion.Events; 3 | using Companion.Services; 4 | using Serilog; 5 | 6 | namespace Companion.ViewModels; 7 | 8 | public partial class StatusBarViewModel : ViewModelBase 9 | { 10 | [ObservableProperty] private string _appVersionText; 11 | 12 | [ObservableProperty] private string _hostNameText; 13 | 14 | [ObservableProperty] private string _messageText; 15 | 16 | [ObservableProperty] private string _statusText; 17 | 18 | public StatusBarViewModel(ILogger logger, 19 | ISshClientService sshClientService, 20 | IEventSubscriptionService eventSubscriptionService) 21 | : base(logger, sshClientService, eventSubscriptionService) 22 | { 23 | EventSubscriptionService.Subscribe<AppMessageEvent, AppMessage>(UpdateStatus); 24 | 25 | _appVersionText = GetFormattedAppVersion(); 26 | } 27 | 28 | private string GetFormattedAppVersion() 29 | { 30 | var fullVersion = VersionHelper.GetAppVersion(); 31 | 32 | // Extract the first part of the version (e.g., "1.0.0") 33 | var truncatedVersion = fullVersion.Split('+')[0]; // Handles semantic versions like "1.0.0+buildinfo" 34 | return truncatedVersion.Length > 10 ? truncatedVersion.Substring(0, 10) : truncatedVersion; 35 | } 36 | 37 | 38 | private void UpdateStatus(AppMessage appMessage) 39 | { 40 | Log.Verbose(appMessage.ToString()); 41 | 42 | 43 | if (!string.IsNullOrEmpty(appMessage.Status)) StatusText = appMessage.Status; 44 | if (!string.IsNullOrEmpty(appMessage.Message)) MessageText = appMessage.Message; 45 | 46 | if (!string.IsNullOrEmpty(appMessage.DeviceConfig.Hostname)) HostNameText = appMessage.DeviceConfig.Hostname; 47 | } 48 | } -------------------------------------------------------------------------------- /Companion.Tests/Services/YamlConfigServiceTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using Companion.Services; 3 | using Serilog; 4 | 5 | namespace OpenIPC.Companion.Tests.Services; 6 | 7 | [TestFixture] 8 | public class YamlConfigServiceTests 9 | { 10 | [SetUp] 11 | public void SetUp() 12 | { 13 | _mockLogger = new Mock<ILogger>(); 14 | 15 | // Mocking the ForContext method of the logger to return the mock logger itself 16 | _mockLogger.Setup(x => x.ForContext(It.IsAny<Type>())).Returns(_mockLogger.Object); 17 | 18 | _yamlConfigService = new YamlConfigService(_mockLogger.Object); 19 | } 20 | 21 | private Mock<ILogger> _mockLogger; 22 | private IYamlConfigService _yamlConfigService; 23 | 24 | [Test] 25 | public void ParseYaml_ValidContent_ParsesSuccessfully() 26 | { 27 | // Arrange 28 | var yamlContent = "video_size: 1920x1080\nvideo_fps: 30"; 29 | var yamlConfig = new Dictionary<string, string>(); 30 | 31 | // Act 32 | _yamlConfigService.ParseYaml(yamlContent, yamlConfig); 33 | 34 | // Assert 35 | Assert.That(yamlConfig["video_size"], Is.EqualTo("1920x1080")); 36 | Assert.That(yamlConfig["video_fps"], Is.EqualTo("30")); 37 | } 38 | 39 | [Test] 40 | public void UpdateYaml_ValidConfig_GeneratesYamlContent() 41 | { 42 | // Arrange 43 | var yamlConfig = new Dictionary<string, string> 44 | { 45 | { "video_size", "1920x1080" }, 46 | { "video_fps", "30" } 47 | }; 48 | 49 | // Act 50 | var result = _yamlConfigService.UpdateYaml(yamlConfig); 51 | 52 | // Assert 53 | Assert.That(result, Does.Contain("video_size: 1920x1080")); 54 | Assert.That(result, Does.Contain("video_fps: 30")); 55 | } 56 | } -------------------------------------------------------------------------------- /Companion/ViewModels/PresetsAddRepoViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Runtime.CompilerServices; 3 | using System.Threading.Tasks; 4 | using System.Windows.Input; 5 | using Avalonia.Controls; 6 | using CommunityToolkit.Mvvm.Input; 7 | 8 | namespace Companion.ViewModels; 9 | 10 | public class PresetsAddRepoViewModel : INotifyPropertyChanged 11 | { 12 | private string? _repoUrl; 13 | 14 | public string? RepoUrl 15 | { 16 | get => _repoUrl; 17 | set 18 | { 19 | if (_repoUrl != value) 20 | { 21 | _repoUrl = value; 22 | OnPropertyChanged(); 23 | // Raise CanExecuteChanged to re-evaluate the command's enabled state. 24 | AddRepositoryCommand.NotifyCanExecuteChanged(); 25 | } 26 | } 27 | } 28 | 29 | public PresetsAddRepoViewModel() 30 | { 31 | AddRepositoryCommand = new RelayCommand(AddRepository, CanAddRepository); 32 | } 33 | 34 | private bool CanAddRepository() 35 | { 36 | // Implement your logic here to determine if the button should be enabled. 37 | // For example, check if RepoUrl is not null and not empty. 38 | return !string.IsNullOrEmpty(RepoUrl); 39 | } 40 | 41 | private void AddRepository() 42 | { 43 | throw new System.NotImplementedException(); 44 | } 45 | 46 | #region Commands 47 | public RelayCommand AddRepositoryCommand { get; } // Changed ICommand to RelayCommand 48 | #endregion 49 | 50 | public event PropertyChangedEventHandler? PropertyChanged; 51 | 52 | protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) 53 | { 54 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 55 | } 56 | } -------------------------------------------------------------------------------- /Companion/binaries/presets/create_preset.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit on error 4 | set -e 5 | 6 | # Check if a preset name is provided 7 | if [ -z "$1" ]; then 8 | echo "Usage: $0 \"Preset Name\"" 9 | exit 1 10 | fi 11 | 12 | # Convert preset name to lowercase and replace spaces with underscores 13 | PRESET_NAME="$1" 14 | FOLDER_NAME=$(echo "$PRESET_NAME" | tr '[:upper:]' '[:lower:]' | tr ' ' '_') 15 | PRESET_DIR="./$FOLDER_NAME" 16 | 17 | # Create the preset directory 18 | mkdir -p "$PRESET_DIR/sensor" 19 | 20 | # Create the preset-config.yaml file 21 | cat <<EOF > "$PRESET_DIR/preset-config.yaml" 22 | name: "$PRESET_NAME" 23 | author: "Your Name" 24 | description: "Description of $PRESET_NAME." 25 | category: "FPV" 26 | sensor: "" # Set sensor file if needed 27 | files: 28 | wfb.yaml: 29 | wireless.txpower: "1" 30 | wireless.channel: "161" 31 | majestic.yaml: 32 | fpv.enabled: "false" 33 | system.logLevel: "debug" 34 | EOF 35 | 36 | # Create default configuration files 37 | cat <<EOF > "$PRESET_DIR/wfb.yaml" 38 | wireless: 39 | txpower: 1 40 | region: 00 41 | channel: 161 42 | mode: HT20 43 | broadcast: 44 | index: 1 45 | fec_k: 8 46 | fec_n: 12 47 | link_id: 7669206 48 | telemetry: 49 | index: 1 50 | router: msposd 51 | serial: /dev/ttyS2 52 | osd_fps: 20 53 | port_rx: 14551 54 | port_tx: 14555 55 | EOF 56 | 57 | cat <<EOF > "$PRESET_DIR/majestic.yaml" 58 | fpv: 59 | enabled: false 60 | system: 61 | logLevel: debug 62 | video0: 63 | bitrate: 4096 64 | records: 65 | enabled: false 66 | EOF 67 | 68 | # Create an empty sensor file as a placeholder 69 | touch "$PRESET_DIR/sensor/.keep" 70 | 71 | # Display success message 72 | echo "✅ Preset '$PRESET_NAME' created successfully in '$PRESET_DIR'" 73 | echo "Edit '$PRESET_DIR/preset-config.yaml' to configure the preset." 74 | -------------------------------------------------------------------------------- /Companion/Assets/Icons/ic-discord.svg: -------------------------------------------------------------------------------- 1 | <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <path d="M12.8467 3.55332C11.96 3.13999 11 2.83999 10 2.66666C9.98241 2.66691 9.96564 2.6741 9.95333 2.68666C9.83333 2.90666 9.69333 3.19332 9.6 3.41332C8.53932 3.25342 7.46067 3.25342 6.4 3.41332C6.30666 3.18666 6.16666 2.90666 6.04 2.68666C6.03333 2.67332 6.01333 2.66666 5.99333 2.66666C4.99333 2.83999 4.04 3.13999 3.14666 3.55332C3.14 3.55332 3.13333 3.55999 3.12666 3.56666C1.31333 6.27999 0.81333 8.91999 1.06 11.5333C1.06 11.5467 1.06666 11.56 1.08 11.5667C2.28 12.4467 3.43333 12.98 4.57333 13.3333C4.59333 13.34 4.61333 13.3333 4.62 13.32C4.88666 12.9533 5.12666 12.5667 5.33333 12.16C5.34666 12.1333 5.33333 12.1067 5.30666 12.1C4.92666 11.9533 4.56666 11.78 4.21333 11.58C4.18666 11.5667 4.18666 11.5267 4.20666 11.5067C4.28 11.4533 4.35333 11.3933 4.42666 11.34C4.44 11.3267 4.46 11.3267 4.47333 11.3333C6.76666 12.38 9.24 12.38 11.5067 11.3333C11.52 11.3267 11.54 11.3267 11.5533 11.34C11.6267 11.4 11.7 11.4533 11.7733 11.5133C11.8 11.5333 11.8 11.5733 11.7667 11.5867C11.42 11.7933 11.0533 11.96 10.6733 12.1067C10.6467 12.1133 10.64 12.1467 10.6467 12.1667C10.86 12.5733 11.1 12.96 11.36 13.3267C11.38 13.3333 11.4 13.34 11.42 13.3333C12.5667 12.98 13.72 12.4467 14.92 11.5667C14.9333 11.56 14.94 11.5467 14.94 11.5333C15.2333 8.51332 14.4533 5.89332 12.8733 3.56666C12.8667 3.55999 12.86 3.55332 12.8467 3.55332ZM5.68 9.93999C4.99333 9.93999 4.42 9.30666 4.42 8.52666C4.42 7.74666 4.98 7.11332 5.68 7.11332C6.38666 7.11332 6.94666 7.75332 6.94 8.52666C6.94 9.30666 6.38 9.93999 5.68 9.93999ZM10.3267 9.93999C9.64 9.93999 9.06666 9.30666 9.06666 8.52666C9.06666 7.74666 9.62666 7.11332 10.3267 7.11332C11.0333 7.11332 11.5933 7.75332 11.5867 8.52666C11.5867 9.30666 11.0333 9.93999 10.3267 9.93999Z" fill="#FDFDFD"/> 3 | </svg> 4 | -------------------------------------------------------------------------------- /Companion/Views/SetupRadxaButtonsView.axaml: -------------------------------------------------------------------------------- 1 | <UserControl xmlns="https://github.com/avaloniaui" 2 | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 3 | xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 4 | xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 5 | xmlns:vm="clr-namespace:Companion.ViewModels" 6 | xmlns:assets="clr-namespace:Companion.Assets" 7 | mc:Ignorable="d" 8 | d:DesignWidth="400" d:DesignHeight="400" 9 | x:Class="Companion.Views.SetupRadxaButtonsView" 10 | x:DataType="vm:SetupTabViewModel"> 11 | 12 | <!-- Left Panel --> 13 | <Grid> 14 | <Grid.RowDefinitions> 15 | <RowDefinition Height="*" /> 16 | </Grid.RowDefinitions> 17 | 18 | <Grid.ColumnDefinitions> 19 | <ColumnDefinition Width="Auto" /> 20 | <ColumnDefinition Width="Auto" /> 21 | <ColumnDefinition Width="Auto" /> 22 | </Grid.ColumnDefinitions> 23 | 24 | 25 | <ComboBox x:Name="RadxaKeyComboBox" Grid.Row="0" Grid.Column="0" PlaceholderText="Encryption Key Actions" 26 | ItemsSource="{Binding DroneKeyActionItems}" 27 | SelectedItem="{Binding SelectedDroneKeyAction}" 28 | HorizontalAlignment="Stretch" 29 | ToolTip.Tip="{x:Static assets:Resources.DroneKeyActionsToolTip}" /> 30 | 31 | <Button Grid.Row="0" Grid.Column="1" Content="Execute" 32 | Command="{Binding EncryptionKeyActionCommand}" 33 | CommandParameter="CameraKeyComboBox" 34 | VerticalAlignment="Top" 35 | ToolTip.Tip="{x:Static assets:Resources.DeviceChkSumToolTip}" 36 | HorizontalAlignment="Stretch" /> 37 | 38 | 39 | </Grid> 40 | 41 | </UserControl> -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | <!--- Describe your changes in detail --> 4 | 5 | ## Related Issue 6 | 7 | <!--- This project only accepts pull requests related to open issues --> 8 | <!--- If suggesting a new feature or change, please discuss it in an issue first --> 9 | <!--- If fixing a bug, there should be an issue describing it with steps to reproduce --> 10 | <!--- Please link to the issue here --> 11 | 12 | This PR fixes or closes issue: fixes # 13 | 14 | ## Motivation and Context 15 | 16 | <!--- Why is this change required? What problem does it solve? --> 17 | 18 | ## How Has This Been Tested 19 | 20 | <!--- Please describe in detail how you tested your changes. --> 21 | <!--- Include details of your testing environment, and the tests you ran to --> 22 | <!--- see how your change affects other areas of the code, etc. --> 23 | 24 | ## Types of changes 25 | 26 | <!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> 27 | 28 | - [ ] 🐛 Bug fix (non-breaking change which fixes an issue) 29 | - [ ] 🚀 New feature (non-breaking change which adds functionality) 30 | - [ ] 🌎 Translation (addition or update a translation) 31 | - [ ] ⚙️ Tech (code style improvement, performance improvement or dependencies bump) 32 | - [ ] 📚 Documentation (fix or addition in the documentation) 33 | - [ ] ⚠️ Breaking change (fix or feature that would cause existing functionality to change) 34 | 35 | ## Checklist 36 | 37 | <!--- Go over all the following points, and put an `x` in all the boxes that apply. --> 38 | <!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! --> 39 | 40 | - [ ] My code follows the code style of this project. 41 | - [ ] My change requires a change to the documentation. 42 | - [ ] I have updated the documentation accordingly. 43 | - [ ] I have tested the change locally. 44 | -------------------------------------------------------------------------------- /Companion.Android/Resources/AboutResources.txt: -------------------------------------------------------------------------------- 1 | Images, layout descriptions, binary blobs and string dictionaries can be included 2 | in your application as resource files. Various Android APIs are designed to 3 | operate on the resource IDs instead of dealing with images, strings or binary blobs 4 | directly. 5 | 6 | For example, a sample Android app that contains a user interface layout (main.axml), 7 | an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png) 8 | would keep its resources in the "Resources" directory of the application: 9 | 10 | Resources/ 11 | drawable/ 12 | icon.png 13 | 14 | layout/ 15 | main.axml 16 | 17 | values/ 18 | strings.xml 19 | 20 | In order to get the build system to recognize Android resources, set the build action to 21 | "AndroidResource". The native Android APIs do not operate directly with filenames, but 22 | instead operate on resource IDs. When you compile an Android application that uses resources, 23 | the build system will package the resources for distribution and generate a class called "R" 24 | (this is an Android convention) that contains the tokens for each one of the resources 25 | included. For example, for the above Resources layout, this is what the R class would expose: 26 | 27 | public class R { 28 | public class drawable { 29 | public const int icon = 0x123; 30 | } 31 | 32 | public class layout { 33 | public const int main = 0x456; 34 | } 35 | 36 | public class strings { 37 | public const int first_string = 0xabc; 38 | public const int second_string = 0xbcd; 39 | } 40 | } 41 | 42 | You would then use R.drawable.icon to reference the drawable/icon.png file, or R.layout.main 43 | to reference the layout/main.axml file, or R.strings.first_string to reference the first 44 | string in the dictionary file values/strings.xml. -------------------------------------------------------------------------------- /Companion/ViewModels/ViewModelBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CommunityToolkit.Mvvm.ComponentModel; 3 | using Companion.Events; 4 | using Companion.Models; 5 | using Companion.Services; 6 | using Serilog; 7 | 8 | namespace Companion.ViewModels; 9 | 10 | public abstract class ViewModelBase : ObservableObject 11 | { 12 | protected readonly IEventSubscriptionService EventSubscriptionService; 13 | protected readonly ILogger Logger; 14 | protected readonly ISshClientService SshClientService; 15 | 16 | 17 | protected ViewModelBase( 18 | ILogger logger, 19 | ISshClientService sshClientService, 20 | IEventSubscriptionService eventSubscriptionService) 21 | { 22 | //Logger = logger ?? throw new ArgumentNullException(nameof(logger)); 23 | Logger = logger?.ForContext(GetType()) ?? 24 | throw new ArgumentNullException(nameof(logger)); 25 | SshClientService = sshClientService ?? throw new ArgumentNullException(nameof(sshClientService)); 26 | EventSubscriptionService = eventSubscriptionService ?? 27 | throw new ArgumentNullException(nameof(eventSubscriptionService)); 28 | } 29 | 30 | /// <summary> 31 | /// Publishes a UI message via the event aggregator. 32 | /// </summary> 33 | /// <param name="message">The message to display.</param> 34 | public virtual void UpdateUIMessage(string message) 35 | { 36 | if (string.IsNullOrWhiteSpace(message)) 37 | { 38 | Logger.Warning("UpdateUIMessage called with an empty message."); 39 | return; 40 | } 41 | 42 | Logger.Verbose("Publishing UI message: {Message}", message); 43 | EventSubscriptionService.Publish<AppMessageEvent, AppMessage>(new AppMessage 44 | { 45 | Message = message, 46 | UpdateLogView = false, 47 | CanConnect = DeviceConfig.Instance.CanConnect 48 | }); 49 | } 50 | } -------------------------------------------------------------------------------- /Companion/Models/FrequencyMappings.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Companion.Models; 4 | 5 | public static class FrequencyMappings 6 | { 7 | public static readonly Dictionary<int, string> Frequency24GHz = new() 8 | { 9 | { 0, "" }, 10 | { 1, "2412 MHz [1]" }, 11 | { 2, "2417 MHz [2]" }, 12 | { 3, "2422 MHz [3]" }, 13 | { 4, "2427 MHz [4]" }, 14 | { 5, "2432 MHz [5]" }, 15 | { 6, "2437 MHz [6]" }, 16 | { 7, "2442 MHz [7]" }, 17 | { 8, "2447 MHz [8]" }, 18 | { 9, "2452 MHz [9]" }, 19 | { 10, "2457 MHz [10]" }, 20 | { 11, "2462 MHz [11]" }, 21 | { 12, "2467 MHz [12]" }, 22 | { 13, "2472 MHz [13]" }, 23 | { 14, "2484 MHz [14]" } 24 | }; 25 | 26 | public static readonly Dictionary<int, string> Frequency58GHz = new() 27 | { 28 | { 0, "" }, 29 | { 36, "5180 MHz [36]" }, 30 | { 40, "5200 MHz [40]" }, 31 | { 44, "5220 MHz [44]" }, 32 | { 48, "5240 MHz [48]" }, 33 | { 52, "5260 MHz [52]" }, 34 | { 56, "5280 MHz [56]" }, 35 | { 60, "5300 MHz [60]" }, 36 | { 64, "5320 MHz [64]" }, 37 | { 100, "5500 MHz [100]" }, 38 | { 104, "5520 MHz [104]" }, 39 | { 108, "5540 MHz [108]" }, 40 | { 112, "5560 MHz [112]" }, 41 | { 116, "5580 MHz [116]" }, 42 | { 120, "5600 MHz [120]" }, 43 | { 124, "5620 MHz [124]" }, 44 | { 128, "5640 MHz [128]" }, 45 | { 132, "5660 MHz [132]" }, 46 | { 136, "5680 MHz [136]" }, 47 | { 140, "5700 MHz [140]" }, 48 | { 144, "5720 MHz [144]" }, 49 | { 149, "5745 MHz [149]" }, 50 | { 153, "5765 MHz [153]" }, 51 | { 157, "5785 MHz [157]" }, 52 | { 161, "5805 MHz [161]" }, 53 | { 165, "5825 MHz [165]" }, 54 | { 169, "5845 MHz [169]" }, 55 | { 173, "5865 MHz [173]" }, 56 | { 177, "5885 MHz [177]" } 57 | }; 58 | } -------------------------------------------------------------------------------- /Companion.iOS/Info.plist: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 | <plist version="1.0"> 4 | <dict> 5 | <key>CFBundleDisplayName</key> 6 | <string>OpenIPC-Config</string> 7 | <key>CFBundleIdentifier</key> 8 | <string>org.openpilot.OpenIPC-Config</string> 9 | <key>CFBundleShortVersionString</key> 10 | <string>1.0</string> 11 | <key>CFBundleVersion</key> 12 | <string>1.0</string> 13 | <key>LSRequiresIPhoneOS</key> 14 | <true/> 15 | <key>MinimumOSVersion</key> 16 | <string>13.0</string> 17 | <key>UIDeviceFamily</key> 18 | <array> 19 | <integer>1</integer> 20 | <integer>2</integer> 21 | </array> 22 | <key>UILaunchStoryboardName</key> 23 | <string>LaunchScreen</string> 24 | <key>UIRequiredDeviceCapabilities</key> 25 | <array> 26 | <string>armv7</string> 27 | </array> 28 | <key>UIFileSharingEnabled</key> 29 | <true/> 30 | <key>LSSupportsOpeningDocumentsInPlace</key> 31 | <true/> 32 | <key>UISupportedInterfaceOrientations</key> 33 | <array> 34 | <string>UIInterfaceOrientationPortrait</string> 35 | <string>UIInterfaceOrientationPortraitUpsideDown</string> 36 | <string>UIInterfaceOrientationLandscapeLeft</string> 37 | <string>UIInterfaceOrientationLandscapeRight</string> 38 | </array> 39 | <key>UISupportedInterfaceOrientations~ipad</key> 40 | <array> 41 | <string>UIInterfaceOrientationPortrait</string> 42 | <string>UIInterfaceOrientationPortraitUpsideDown</string> 43 | <string>UIInterfaceOrientationLandscapeLeft</string> 44 | <string>UIInterfaceOrientationLandscapeRight</string> 45 | </array> 46 | <key>CFBundleExecutable</key> 47 | <string></string> 48 | </dict> 49 | </plist> 50 | -------------------------------------------------------------------------------- /Companion/Models/Presets/PresetIndexEntry.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using YamlDotNet.Serialization; 3 | 4 | namespace Companion.Models.Presets; 5 | 6 | /// <summary> 7 | /// Represents an individual preset entry in the index 8 | /// </summary> 9 | public class PresetIndexEntry 10 | { 11 | /// <summary> 12 | /// Name of the preset 13 | /// </summary> 14 | [YamlMember(Alias = "name")] 15 | public string Name { get; set; } = string.Empty; 16 | 17 | /// <summary> 18 | /// Relative path to the preset in the repository 19 | /// </summary> 20 | [YamlMember(Alias = "path")] 21 | public string Path { get; set; } = string.Empty; 22 | 23 | /// <summary> 24 | /// Category of the preset 25 | /// </summary> 26 | [YamlMember(Alias = "category")] 27 | public string Category { get; set; } = string.Empty; 28 | 29 | /// <summary> 30 | /// Author of the preset 31 | /// </summary> 32 | [YamlMember(Alias = "author")] 33 | public string Author { get; set; } = string.Empty; 34 | 35 | /// <summary> 36 | /// Description of the preset 37 | /// </summary> 38 | [YamlMember(Alias = "description")] 39 | public string Description { get; set; } = string.Empty; 40 | 41 | /// <summary> 42 | /// Tags associated with the preset 43 | /// </summary> 44 | [YamlMember(Alias = "tags")] 45 | public List<string> Tags { get; set; } = new(); 46 | 47 | /// <summary> 48 | /// Status of the preset (e.g., Community, Official, Draft) 49 | /// </summary> 50 | [YamlMember(Alias = "status")] 51 | public string Status { get; set; } = string.Empty; 52 | 53 | /// <summary> 54 | /// List of files modified by this preset 55 | /// </summary> 56 | [YamlMember(Alias = "files")] 57 | //public List<string> Files { get; set; } = new(); 58 | public Dictionary<string, Dictionary<string, string>> Files { get; set; } = new(); 59 | 60 | /// <summary> 61 | /// List of files modified by this preset 62 | /// </summary> 63 | [YamlMember(Alias = "additional_files")] 64 | public List<string> AdditionalFiles { get; set; } = new(); 65 | } -------------------------------------------------------------------------------- /Companion/Models/Telemetry.cs: -------------------------------------------------------------------------------- 1 | namespace Companion.Models; 2 | 3 | // ### telemetry.conf 4 | // ### key=value 5 | public static class Telemetry 6 | { 7 | public const string Serial = "serial"; 8 | public const string Baud = "baud"; 9 | public const string Router = "router"; 10 | public const string Fps = "fps"; 11 | public const string Wlan = "wlan"; 12 | public const string Bandwidth = "bandwidth"; 13 | public const string Stbc = "stbc"; 14 | public const string Ldpc = "ldpc"; 15 | public const string McsIndex = "mcs_index"; 16 | public const string StreamRx = "stream_rx"; 17 | public const string StreamTx = "stream_tx"; 18 | public const string LinkId = "link_id"; 19 | public const string FrameType = "frame_type"; 20 | public const string PortRx = "port_rx"; 21 | public const string PortTx = "port_tx"; 22 | public const string FecK = "fec_k"; 23 | public const string FecN = "fec_n"; 24 | public const string PoolTimeout = "pool_timeout"; 25 | public const string GuardInterval = "guard_interval"; 26 | public const string OneWay = "one_way"; 27 | public const string Aggregate = "aggregate"; 28 | public const string Channels = "channels"; 29 | public const string Value = "value"; 30 | public const string TelemetryConf = "telemetry.conf"; 31 | public const string RcChannel = "channels"; 32 | } 33 | 34 | // ### unit: drone or gs 35 | // unit=drone 36 | // 37 | // serial=/dev/ttyS2 38 | // baud=115200 39 | // 40 | // ### router: use simple mavfwd (0) or classic mavlink-routerd (1) 41 | // router=0 42 | // 43 | // wlan=wlan0 44 | // bandwidth=20 45 | // stbc=1 46 | // ldpc=1 47 | // mcs_index=1 48 | // stream_rx=144 49 | // stream_tx=16 50 | // link_id=7669206 51 | // frame_type=data 52 | // port_rx=14551 53 | // port_tx=14550 54 | // fec_k=1 55 | // fec_n=2 56 | // pool_timeout=0 57 | // guard_interval=long 58 | // one_way=false 59 | // aggregate=15 60 | // 61 | // ### for mavfwd: RC override channels to parse after first 4 and call /usr/sbin/channels.sh $ch $val, default 0 62 | // channels=80,100,144,160,200,240,280,320,360,400,440,480,520,560,600,640,680,720,760,800,840,880,920,960,1000 -------------------------------------------------------------------------------- /Companion/Services/SysUpgradeService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Companion.Models; 6 | using Serilog; 7 | 8 | namespace Companion.Services; 9 | 10 | public class SysUpgradeService 11 | { 12 | private readonly ISshClientService _sshClientService; 13 | private readonly ILogger _logger; 14 | 15 | public SysUpgradeService(ISshClientService sshClientService, ILogger logger) 16 | { 17 | _sshClientService = sshClientService; 18 | _logger = logger; 19 | } 20 | 21 | public async Task PerformSysupgradeAsync(DeviceConfig deviceConfig, string kernelPath, string rootfsPath, 22 | Action<string> updateProgress, CancellationToken cancellationToken) 23 | { 24 | try 25 | { 26 | updateProgress("Uploading kernel..."); 27 | string kernelFilename = Path.GetFileName(kernelPath); 28 | await _sshClientService.UploadFileAsync(deviceConfig, kernelPath, $"{OpenIPC.RemoteTempFolder}/{kernelFilename}"); 29 | updateProgress("Kernel binary uploaded successfully."); 30 | 31 | updateProgress("Uploading root filesystem..."); 32 | string rootfsFilename = Path.GetFileName(rootfsPath); 33 | await _sshClientService.UploadFileAsync(deviceConfig, rootfsPath, $"{OpenIPC.RemoteTempFolder}/{rootfsFilename}"); 34 | updateProgress("Root filesystem binary uploaded successfully."); 35 | 36 | //updateProgress("Starting sysupgrade..."); 37 | await _sshClientService.ExecuteCommandWithProgressAsync( 38 | deviceConfig, 39 | $"sysupgrade --force_ver -n --kernel={OpenIPC.RemoteTempFolder}/{kernelFilename} --rootfs={OpenIPC.RemoteTempFolder}/{rootfsFilename}", 40 | updateProgress, 41 | cancellationToken 42 | ); 43 | 44 | updateProgress("Sysupgrade process completed."); 45 | } 46 | catch (Exception ex) 47 | { 48 | _logger.Error(ex, "Error during sysupgrade."); 49 | updateProgress($"Error: {ex.Message}"); 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /Companion/Services/Utilities.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | using System.Text; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace Companion.Services; 6 | 7 | public static class Utilities 8 | { 9 | public static string RemoveSpecialCharacters(string input) 10 | { 11 | var pattern = @"[^a-zA-Z0-9\-\@]"; // Matches any character that is not a letter, number, or dash 12 | var output = Regex.Replace(input, pattern, ""); 13 | return output; 14 | } 15 | 16 | public static string RemoveLastChar(string input) 17 | { 18 | if (input.Length > 0) 19 | { 20 | return input.Substring(0, input.Length - 1); 21 | } 22 | else 23 | { 24 | return input; 25 | } 26 | } 27 | 28 | public static string ComputeMd5Hash(byte[] rawData) 29 | { 30 | // Use MD5 to compute the hash 31 | using (var md5Hash = MD5.Create()) 32 | { 33 | // Compute the hash for the byte array 34 | var bytes = md5Hash.ComputeHash(rawData); 35 | 36 | // Convert the bytes to a hexadecimal string 37 | var builder = new StringBuilder(); 38 | foreach (var b in bytes) builder.Append(b.ToString("x2")); 39 | 40 | return builder.ToString(); 41 | } 42 | } 43 | 44 | 45 | public static string ComputeSha256Hash(string rawData) 46 | { 47 | // Create a SHA256 instance 48 | using (var sha256Hash = SHA256.Create()) 49 | { 50 | // Compute the hash as a byte array 51 | var bytes = sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(rawData)); 52 | 53 | // Convert the byte array to a hex string 54 | var builder = new StringBuilder(); 55 | foreach (var b in bytes) builder.Append(b.ToString("x2")); 56 | 57 | return builder.ToString(); 58 | } 59 | } 60 | 61 | public static bool IsValidIpAddress(string ipAddress) 62 | { 63 | var pattern = @"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"; 64 | return Regex.IsMatch(ipAddress, pattern); 65 | } 66 | } -------------------------------------------------------------------------------- /Companion.Tests/Services/UpdateCheckerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using Microsoft.Extensions.Configuration; 3 | using Moq; 4 | using Moq.Protected; 5 | using Companion.Services; 6 | 7 | namespace OpenIPC.Companion.Tests.Services; 8 | 9 | public class UpdateCheckerTests 10 | { 11 | private const string MockUpdateJson = @"{ 12 | 'version': 'release-v1.2.0', 13 | 'release_notes': 'Bug fixes and performance improvements.', 14 | 'download_url': 'https://example.com/download' 15 | }"; 16 | 17 | [Test] 18 | public async Task CheckForUpdateAsync_ShouldReturnUpdateAvailable_WhenNewVersionExists() 19 | { 20 | // Arrange 21 | var mockHttpMessageHandler = new Mock<HttpMessageHandler>(); 22 | mockHttpMessageHandler 23 | .Protected() 24 | .Setup<Task<HttpResponseMessage>>( 25 | "SendAsync", 26 | ItExpr.IsAny<HttpRequestMessage>(), 27 | ItExpr.IsAny<CancellationToken>()) 28 | .ReturnsAsync(new HttpResponseMessage 29 | { 30 | StatusCode = HttpStatusCode.OK, 31 | Content = new StringContent(@"{ 32 | 'version': 'release-v1.2.0', 33 | 'release_notes': 'Bug fixes and performance improvements.', 34 | 'download_url': 'https://example.com/download' 35 | }") 36 | }); 37 | 38 | var mockHttpClient = new HttpClient(mockHttpMessageHandler.Object); 39 | 40 | var mockConfiguration = new Mock<IConfiguration>(); 41 | mockConfiguration.Setup(c => c["UpdateChecker:LatestJsonUrl"]).Returns("https://mock-url/latest.json"); 42 | 43 | var updateChecker = new UpdateChecker(mockHttpClient, mockConfiguration.Object); 44 | 45 | // Act 46 | var result = await updateChecker.CheckForUpdateAsync("v0.0.1"); 47 | 48 | // Assert 49 | Assert.That(result.HasUpdate, Is.True); 50 | Assert.That(result.ReleaseNotes, Is.EqualTo("Bug fixes and performance improvements.")); 51 | Assert.That(result.DownloadUrl, Is.EqualTo("https://example.com/download")); 52 | Assert.That(result.NewVersion, Is.EqualTo("release-v1.2.0")); 53 | } 54 | } -------------------------------------------------------------------------------- /Companion/Converters/CanConnectConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using Avalonia.Controls; 4 | using Avalonia.Data.Converters; 5 | using Avalonia.LogicalTree; 6 | using Companion.ViewModels; 7 | 8 | namespace Companion.Converters; 9 | 10 | public class CanConnectConverter : IValueConverter 11 | { 12 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 13 | { 14 | // Try to get the DataContext from the current view model 15 | if (Avalonia.Application.Current?.DataContext is PresetsTabViewModel viewModel) 16 | { 17 | return viewModel.CanConnect; 18 | } 19 | 20 | // Alternative approach: try to find the view model through the logical tree 21 | if (Avalonia.Application.Current?.ApplicationLifetime is 22 | Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop) 23 | { 24 | var mainWindow = desktop.MainWindow; 25 | if (mainWindow != null) 26 | { 27 | // Recursively search through logical children 28 | var mainViewModel = FindDataContext<PresetsTabViewModel>(mainWindow); 29 | if (mainViewModel != null) 30 | { 31 | return mainViewModel.CanConnect; 32 | } 33 | } 34 | } 35 | 36 | // Fallback to default 37 | return false; 38 | } 39 | 40 | private T? FindDataContext<T>(ILogical logical) where T : class 41 | { 42 | // Check the current logical's DataContext 43 | if (logical is Control control && control.DataContext is T matchingViewModel) 44 | { 45 | return matchingViewModel; 46 | } 47 | 48 | // Recursively search through logical children 49 | foreach (var child in logical.LogicalChildren) 50 | { 51 | var result = FindDataContext<T>(child); 52 | if (result != null) 53 | { 54 | return result; 55 | } 56 | } 57 | 58 | return null; 59 | } 60 | 61 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 62 | { 63 | throw new NotImplementedException(); 64 | } 65 | } -------------------------------------------------------------------------------- /test-script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Define the project paths and app bundle location 4 | desktop_project="OpenIPC/OpenIPC_Config.Desktop" 5 | output_dir="./build" 6 | app_bundle="$output_dir/OpenIPC_Config.app" 7 | binary_source="$output_dir/osx-arm64/OpenIPC_Config.Desktop" 8 | 9 | # Clean and prepare the output directory 10 | echo "Cleaning previous builds..." 11 | rm -rf "$output_dir" 12 | mkdir -p "$output_dir/osx-arm64" 13 | 14 | # Build for macOS 15 | echo "Building $desktop_project for macOS (osx-arm64)..." 16 | dotnet publish "$desktop_project" -c Release -r osx-arm64 --output "$output_dir/osx-arm64" --self-contained -v normal 17 | 18 | # Verify that the binary is for macOS 19 | file_type=$(file "$binary_source" | grep -o 'Mach-O 64-bit executable arm64') 20 | if [ -z "$file_type" ]; then 21 | echo "Error: macOS build did not produce a valid macOS executable." 22 | exit 1 23 | fi 24 | echo "macOS binary built successfully." 25 | 26 | # Create the .app bundle structure 27 | echo "Packaging the .app bundle..." 28 | mkdir -p "$app_bundle/Contents/MacOS" 29 | mkdir -p "$app_bundle/Contents/Resources" 30 | 31 | # Copy and rename the macOS binary to remove .dll extension 32 | cp "$binary_source" "$app_bundle/Contents/MacOS/OpenIPC_Config" 33 | 34 | # Add Info.plist for macOS app metadata 35 | cat > "$app_bundle/Contents/Info.plist" <<EOL 36 | <?xml version="1.0" encoding="UTF-8"?> 37 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 38 | <plist version="1.0"> 39 | <dict> 40 | <key>CFBundleName</key> 41 | <string>OpenIPC_Config</string> 42 | <key>CFBundleDisplayName</key> 43 | <string>OpenIPC Config</string> 44 | <key>CFBundleIdentifier</key> 45 | <string>com.openipc.config</string> 46 | <key>CFBundleVersion</key> 47 | <string>1.0</string> 48 | <key>CFBundleExecutable</key> 49 | <string>OpenIPC_Config</string> 50 | <key>CFBundlePackageType</key> 51 | <string>APPL</string> 52 | <key>CFBundleSignature</key> 53 | <string>????</string> 54 | <key>CFBundleInfoDictionaryVersion</key> 55 | <string>6.0</string> 56 | <key>LSMinimumSystemVersion</key> 57 | <string>10.12</string> 58 | </dict> 59 | </plist> 60 | EOL 61 | 62 | # Set executable permissions 63 | chmod +x "$app_bundle/Contents/MacOS/OpenIPC_Config" 64 | 65 | echo ".app bundle created at $app_bundle" 66 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug Report 2 | description: Create a report to help us improve 3 | title: "[Bug]: <title>" 4 | labels: ["bug"] 5 | body: 6 | - type: checkboxes 7 | attributes: 8 | label: Requirements 9 | options: 10 | - label: I checked the troubleshooting section in the README to verify that I have the latest Configurator version. 11 | required: true 12 | - label: I did a search to see if there is a similar issue or if a pull request is open. 13 | required: true 14 | - type: textarea 15 | attributes: 16 | label: Current Behavior 17 | description: A concise description of what you're experiencing. 18 | validations: 19 | required: false 20 | - type: textarea 21 | attributes: 22 | label: Expected Behavior 23 | description: A concise description of what you expected to happen. 24 | validations: 25 | required: false 26 | - type: textarea 27 | attributes: 28 | label: Steps To Reproduce 29 | description: Steps to reproduce the behavior. 30 | placeholder: | 31 | 1. In this environment... 32 | 2. With this config... 33 | 3. Run '...' 34 | 4. See error... 35 | validations: 36 | required: false 37 | - type: textarea 38 | attributes: 39 | label: Context 40 | description: | 41 | If your issue is about a certain config parameter enter config here .Otherwise, you can leave this section blank. 42 | value: | 43 | YAML state 44 | ```yaml 45 | 46 | ``` 47 | validations: 48 | required: false 49 | - type: textarea 50 | attributes: 51 | label: Environment 52 | description: | 53 | examples: 54 | - **OpenIPC Configurator**: arm64 2.3.4 55 | value: | 56 | - Config Version: 57 | render: markdown 58 | validations: 59 | required: false 60 | - type: textarea 61 | attributes: 62 | label: Anything else? 63 | description: | 64 | Links? References? Anything that will give us more context about the issue you are encountering! 65 | 66 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 67 | -------------------------------------------------------------------------------- /Companion/Services/GlobalSettingsService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Companion.Models; 5 | using Renci.SshNet; 6 | using Serilog; 7 | 8 | namespace Companion.Services; 9 | 10 | public class GlobalSettingsService : IGlobalSettingsService 11 | { 12 | private readonly ILogger _logger; 13 | private readonly ISshClientService _sshClientService; 14 | 15 | public bool IsWfbYamlEnabled { get; private set; } = false; 16 | 17 | public GlobalSettingsService(ILogger logger, ISshClientService sshClientService) 18 | { 19 | _logger = logger.ForContext<GlobalSettingsService>(); 20 | _sshClientService = sshClientService; 21 | } 22 | 23 | public async Task ReadDevice() 24 | { 25 | var cts = new CancellationTokenSource(30000); // 30 seconds timeout 26 | 27 | try 28 | { 29 | if (DeviceConfig.Instance.DeviceType != DeviceType.None) 30 | { 31 | await CheckWfbYamlSupport(cts.Token); 32 | } 33 | } 34 | catch (Exception ex) 35 | { 36 | _logger.Error(ex, "Error reading device configuration"); 37 | } 38 | finally 39 | { 40 | cts.Cancel(); 41 | cts.Dispose(); 42 | } 43 | } 44 | 45 | private async Task CheckWfbYamlSupport(CancellationToken cancellationToken) 46 | { 47 | try 48 | { 49 | var cmdResult = await GetIsWfbYamlSupported(cancellationToken); 50 | 51 | IsWfbYamlEnabled = bool.TryParse(Utilities.RemoveLastChar(cmdResult?.Result), out var result) && result; 52 | 53 | _logger.Debug($"WFB YAML support status: {IsWfbYamlEnabled}"); 54 | } 55 | catch (Exception ex) 56 | { 57 | _logger.Error("Error checking WFB YAML support: " + ex.Message); 58 | IsWfbYamlEnabled = false; 59 | } 60 | } 61 | 62 | private async Task<SshCommand?> GetIsWfbYamlSupported(CancellationToken cancellationToken) 63 | { 64 | var command = "test -f /etc/wfb.yaml && echo 'true' || echo 'false'"; 65 | return await _sshClientService.ExecuteCommandWithResponseAsync( 66 | DeviceConfig.Instance, 67 | command, 68 | cancellationToken); 69 | } 70 | } -------------------------------------------------------------------------------- /Companion/Services/GitHubService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.Caching.Memory; 5 | using Serilog; 6 | 7 | namespace Companion.Services 8 | { 9 | public class GitHubService : IGitHubService 10 | { 11 | private readonly IMemoryCache _cache; 12 | private readonly HttpClient _httpClient; 13 | private readonly string _cacheKey = "GitHubData"; // Unique key for your data 14 | private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(120); // Cache for 1 hour 15 | private readonly ILogger _logger; 16 | 17 | public GitHubService(IMemoryCache cache, HttpClient httpClient, ILogger logger) 18 | { 19 | _logger = logger?.ForContext(GetType()) ?? 20 | throw new ArgumentNullException(nameof(logger)); 21 | _cache = cache ?? throw new ArgumentNullException(nameof(cache)); 22 | _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); 23 | } 24 | 25 | public async Task<string> GetGitHubDataAsync(string url) 26 | { 27 | if (_cache.TryGetValue(_cacheKey, out string cachedData)) 28 | { 29 | _logger.Information("GitHub API data retrieved from cache."); 30 | return cachedData; 31 | } 32 | 33 | // Data not in cache, fetch from GitHub API 34 | _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (compatible; OpenIPC-Config/1.0)"); 35 | 36 | try 37 | { 38 | var response = await _httpClient.GetStringAsync(url); 39 | 40 | // Store the data in the cache 41 | var cacheEntryOptions = new MemoryCacheEntryOptions() 42 | .SetAbsoluteExpiration(_cacheDuration) // Data expires after this time 43 | .SetPriority(CacheItemPriority.Normal); // Low priority for eviction 44 | 45 | _cache.Set(_cacheKey, response, cacheEntryOptions); 46 | _logger.Information("Data retrieved from GitHub API and cached."); 47 | return response; 48 | } 49 | catch (HttpRequestException ex) 50 | { 51 | // Handle API errors gracefully (log, throw, etc.) 52 | _logger.Error($"Error calling GitHub API: {ex.Message}"); 53 | return null; // Or throw the exception, depending on your needs 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Companion/Views/PresetsTabView.axaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading.Tasks; 4 | using Avalonia.Controls; 5 | using Avalonia.Interactivity; 6 | using Avalonia.VisualTree; 7 | using Companion.ViewModels; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Companion.Models.Presets; 10 | 11 | namespace Companion.Views; 12 | 13 | public partial class PresetsTabView : UserControl 14 | { 15 | public PresetsTabView() 16 | { 17 | InitializeComponent(); 18 | 19 | if (!Design.IsDesignMode) 20 | { 21 | try 22 | { 23 | // Resolve the ViewModel from the DI container 24 | var viewModel = App.ServiceProvider.GetService<PresetsTabViewModel>(); 25 | if (viewModel == null) 26 | { 27 | throw new InvalidOperationException("Failed to resolve PresetsTabViewModel from the service provider."); 28 | } 29 | 30 | // Set the DataContext 31 | DataContext = viewModel; 32 | } 33 | catch (Exception ex) 34 | { 35 | Debug.WriteLine($"Error initializing PresetsTabView: {ex.Message}"); 36 | // Optionally, provide a fallback or handle errors gracefully 37 | } 38 | } 39 | 40 | } 41 | 42 | // In PresetsTabView.axaml.cs 43 | public bool GetCanConnect() 44 | { 45 | return (DataContext as PresetsTabViewModel)?.CanConnect ?? false; 46 | } 47 | 48 | private void OnShowPresetDetailsClicked(object? sender, RoutedEventArgs e) 49 | { 50 | // Get the Preset from the clicked button's DataContext 51 | var preset = (sender as Button)?.DataContext as Preset; 52 | 53 | // Get the DataContext of the current view 54 | if (DataContext is PresetsTabViewModel viewModel) 55 | { 56 | // Call the method to show preset details 57 | viewModel.ShowPresetDetails(preset); 58 | } 59 | } 60 | 61 | private void OnApplyPresetClicked(object? sender, RoutedEventArgs e) 62 | { 63 | // Get the Preset from the clicked button's DataContext 64 | var preset = (sender as Button)?.DataContext as Preset; 65 | 66 | // Get the DataContext of the current view 67 | if (DataContext is PresetsTabViewModel viewModel) 68 | { 69 | // Call the method to apply preset 70 | viewModel.ApplyPresetAsync(preset); 71 | } 72 | } 73 | 74 | 75 | } -------------------------------------------------------------------------------- /Companion.Tests/ViewModels/ViewModelTestBase.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using Companion.Events; 3 | using Companion.Services; 4 | using Companion.ViewModels; 5 | using Prism.Events; 6 | using Serilog; 7 | 8 | namespace OpenIPC.Companion.Tests.ViewModels; 9 | 10 | public abstract class ViewModelTestBase 11 | { 12 | // xUnit does not have [SetUp], so use a constructor for initialization. 13 | protected ViewModelTestBase() 14 | { 15 | SetUpMocks(); 16 | } 17 | 18 | protected Mock<ILogger> LoggerMock { get; private set; } 19 | protected Mock<IEventAggregator> EventAggregatorMock { get; private set; } 20 | protected Mock<ISshClientService> SshClientServiceMock { get; private set; } 21 | protected Mock<IYamlConfigService> YamlConfigServiceMock { get; private set; } 22 | 23 | protected Mock<IGlobalSettingsService> GlobalSettingsServiceMock { get; private set; } 24 | 25 | protected Mock<IEventSubscriptionService> EventSubscriptionServiceMock { get; private set; } 26 | 27 | protected Mock<WfbConfContentUpdatedEvent> WfbConfContentUpdatedEventMock { get; private set; } 28 | protected Mock<AppMessageEvent> AppMessageEventMock { get; private set; } 29 | protected Mock<MajesticContentUpdatedEvent> MajesticContentUpdatedEventMock { get; private set; } 30 | 31 | private void SetUpMocks() 32 | { 33 | LoggerMock = new Mock<ILogger>(); 34 | LoggerMock.Setup(x => x.ForContext(It.IsAny<Type>())).Returns(LoggerMock.Object); 35 | 36 | EventAggregatorMock = new Mock<IEventAggregator>(); 37 | SshClientServiceMock = new Mock<ISshClientService>(); 38 | WfbConfContentUpdatedEventMock = new Mock<WfbConfContentUpdatedEvent>(); 39 | AppMessageEventMock = new Mock<AppMessageEvent>(); 40 | MajesticContentUpdatedEventMock = new Mock<MajesticContentUpdatedEvent>(); 41 | YamlConfigServiceMock = new Mock<IYamlConfigService>(); 42 | EventSubscriptionServiceMock = new Mock<IEventSubscriptionService>(); 43 | GlobalSettingsServiceMock = new Mock<IGlobalSettingsService>(); 44 | 45 | EventAggregatorMock 46 | .Setup(x => x.GetEvent<WfbConfContentUpdatedEvent>()) 47 | .Returns(WfbConfContentUpdatedEventMock.Object); 48 | 49 | EventAggregatorMock 50 | .Setup(x => x.GetEvent<AppMessageEvent>()) 51 | .Returns(AppMessageEventMock.Object); 52 | 53 | EventAggregatorMock 54 | .Setup(x => x.GetEvent<MajesticContentUpdatedEvent>()) 55 | .Returns(MajesticContentUpdatedEventMock.Object); 56 | } 57 | } -------------------------------------------------------------------------------- /Companion/Assets/Resources.es.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // <auto-generated> 3 | // This code was generated by a tool. 4 | // 5 | // Changes to this file may cause incorrect behavior and will be lost if 6 | // the code is regenerated. 7 | // </auto-generated> 8 | //------------------------------------------------------------------------------ 9 | 10 | namespace Companion.Assets { 11 | using System; 12 | 13 | 14 | [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 15 | [System.Diagnostics.DebuggerNonUserCodeAttribute()] 16 | [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 17 | internal class Resources_es { 18 | 19 | private static System.Resources.ResourceManager resourceMan; 20 | 21 | private static System.Globalization.CultureInfo resourceCulture; 22 | 23 | [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 24 | internal Resources_es() { 25 | } 26 | 27 | [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] 28 | internal static System.Resources.ResourceManager ResourceManager { 29 | get { 30 | if (object.Equals(null, resourceMan)) { 31 | System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Companion.Assets.Resources_es", typeof(Resources_es).Assembly); 32 | resourceMan = temp; 33 | } 34 | return resourceMan; 35 | } 36 | } 37 | 38 | [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static System.Globalization.CultureInfo Culture { 40 | get { 41 | return resourceCulture; 42 | } 43 | set { 44 | resourceCulture = value; 45 | } 46 | } 47 | 48 | internal static string AddMavlinkExtraToolTip { 49 | get { 50 | return ResourceManager.GetString("AddMavlinkExtraToolTip", resourceCulture); 51 | } 52 | } 53 | 54 | internal static string PingStatusToolTip { 55 | get { 56 | return ResourceManager.GetString("PingStatusToolTip", resourceCulture); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Companion/Services/PingService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Net.NetworkInformation; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Serilog; 7 | 8 | namespace Companion.Services; 9 | 10 | public class PingService 11 | { 12 | private static PingService _instance; 13 | private static readonly object _lock = new object(); 14 | 15 | private readonly SemaphoreSlim _pingSemaphore = new SemaphoreSlim(1, 1); 16 | private readonly TimeSpan _defaultTimeout = TimeSpan.FromMilliseconds(500); 17 | private readonly ILogger _logger; 18 | 19 | // Private constructor for singleton pattern 20 | private PingService(ILogger logger) 21 | { 22 | _logger = logger; 23 | } 24 | 25 | // Singleton instance getter 26 | public static PingService Instance(ILogger logger) 27 | { 28 | if (_instance == null) 29 | { 30 | lock (_lock) 31 | { 32 | if (_instance == null) 33 | { 34 | _instance = new PingService(logger); 35 | } 36 | } 37 | } 38 | return _instance; 39 | } 40 | 41 | // Ping method with default timeout 42 | public async Task<PingReply> SendPingAsync(string ipAddress) 43 | { 44 | return await SendPingAsync(ipAddress, (int)_defaultTimeout.TotalMilliseconds); 45 | } 46 | 47 | // Ping method with custom timeout 48 | public async Task<PingReply> SendPingAsync(string ipAddress, int timeout) 49 | { 50 | // Log which IP is being pinged 51 | _logger.Verbose($"Attempting to ping IP: {ipAddress}"); 52 | 53 | if (await _pingSemaphore.WaitAsync(timeout)) 54 | { 55 | try 56 | { 57 | using (var ping = new Ping()) 58 | { 59 | return await ping.SendPingAsync(ipAddress, timeout); 60 | } 61 | } 62 | finally 63 | { 64 | // Release the semaphore when done 65 | _pingSemaphore.Release(); 66 | } 67 | } 68 | else 69 | { 70 | _logger.Warning("Timeout waiting to acquire ping semaphore for {IpAddress}", ipAddress); 71 | 72 | // Since we can't create a PingReply directly, throw a meaningful exception instead 73 | throw new TimeoutException($"Ping operation to {ipAddress} was delayed due to concurrent requests"); 74 | } 75 | } 76 | 77 | // Dispose method to clean up all resources 78 | public void Dispose() 79 | { 80 | _pingSemaphore.Dispose(); 81 | } 82 | } -------------------------------------------------------------------------------- /Companion/Views/WfbGSTabView.axaml: -------------------------------------------------------------------------------- 1 | <UserControl xmlns="https://github.com/avaloniaui" 2 | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 3 | xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 4 | xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 5 | xmlns:vm="clr-namespace:Companion.ViewModels" 6 | mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" 7 | x:Class="Companion.Views.WfbGSTabView" 8 | x:DataType="vm:WfbGSTabViewModel"> 9 | <Grid> 10 | <Grid.ColumnDefinitions> 11 | <ColumnDefinition Width="Auto" /> 12 | <ColumnDefinition Width="Auto" /> 13 | <ColumnDefinition Width="*" /> 14 | </Grid.ColumnDefinitions> 15 | <Grid.RowDefinitions> 16 | <RowDefinition Height="Auto" /> 17 | <RowDefinition Height="Auto" /> 18 | <RowDefinition Height="Auto" /> 19 | <RowDefinition Height="Auto" /> 20 | <RowDefinition Height="Auto" /> 21 | <RowDefinition Height="Auto" /> 22 | <RowDefinition Height="Auto" /> 23 | </Grid.RowDefinitions> 24 | 25 | <Label Grid.Column="0" Grid.Row="0" Content="Frequency" /> 26 | <Label Grid.Column="0" Grid.Row="1" Content="TX Power" /> 27 | <Label Grid.Column="0" Grid.Row="2" Content="Wifi Region" /> 28 | <Label Grid.Column="0" Grid.Row="3" Content="gs_mavlink" /> 29 | <Label Grid.Column="0" Grid.Row="4" Content="gs_video" /> 30 | 31 | <ComboBox Grid.Column="1" Grid.Row="0" 32 | ToolTip.Tip="Select Frequency" 33 | ItemsSource="{Binding Frequencies}" 34 | SelectedItem="{Binding SelectedFrequencyString, Mode=TwoWay}" /> 35 | 36 | 37 | <ComboBox Grid.Column="1" Grid.Row="1" ToolTip.Tip="Select TX Power" 38 | ItemsSource="{Binding Power}" 39 | SelectedItem="{Binding SelectedPower, Mode=TwoWay}" /> 40 | 41 | 42 | <TextBox Grid.Row="2" Grid.Column="1" 43 | Text="{Binding WifiRegion, Mode=TwoWay}" /> 44 | 45 | <TextBox Grid.Row="3" Grid.Column="1" 46 | Text="{Binding GsMavlink, Mode=TwoWay}" /> 47 | 48 | <TextBox Grid.Row="4" Grid.Column="1" 49 | Text="{Binding GsVideo, Mode=TwoWay}" /> 50 | 51 | <Separator Grid.Column="0" Grid.ColumnSpan="3" Grid.Row="5" /> 52 | 53 | <Button Grid.Column="0" HorizontalAlignment="Left" Grid.Row="6" Content="Restart WFB" 54 | Command="{Binding RestartWfbCommand}" 55 | IsEnabled="{Binding CanConnect}" /> 56 | 57 | </Grid> 58 | 59 | </UserControl> -------------------------------------------------------------------------------- /Companion/Views/LogViewer.axaml: -------------------------------------------------------------------------------- 1 | <UserControl xmlns="https://github.com/avaloniaui" 2 | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 3 | xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 4 | xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 5 | xmlns:vm="clr-namespace:Companion.ViewModels" 6 | mc:Ignorable="d" 7 | d:DesignHeight="80" 8 | 9 | x:Class="Companion.Views.LogViewer" 10 | x:DataType="vm:LogViewerViewModel"> 11 | 12 | 13 | 14 | <Border Background="#F0F0F0" CornerRadius="8" Padding="0,3,0,5" Margin="0"> 15 | <ScrollViewer VerticalScrollBarVisibility="Auto"> 16 | <Grid> 17 | <Grid.RowDefinitions> 18 | <!-- Label --> 19 | <!-- <RowDefinition Height="Auto" /> --> 20 | <!-- ListBox --> 21 | <RowDefinition Height="Auto" /> 22 | </Grid.RowDefinitions> 23 | 24 | <Grid.ColumnDefinitions> 25 | <ColumnDefinition Width="*" /> <!-- Single column for simplicity --> 26 | </Grid.ColumnDefinitions> 27 | 28 | <!-- Label for log file information --> 29 | <!-- <Label Grid.Row="0" --> 30 | <!-- FontStyle="Italic" --> 31 | <!-- FontSize="14" --> 32 | <!-- Margin="5,5,5,10" --> 33 | <!-- Content="Log File is also available in the System Application Folder" /> --> 34 | 35 | <!-- Log viewer ListBox --> 36 | <ListBox Grid.Row="0" 37 | Padding="0" 38 | Margin="0" 39 | CornerRadius="8" 40 | BorderBrush="Black" 41 | BorderThickness="0" 42 | ItemsSource="{Binding LogMessages}" 43 | ScrollViewer.VerticalScrollBarVisibility="Visible" 44 | ScrollViewer.HorizontalScrollBarVisibility="Disabled" 45 | HorizontalAlignment="Stretch"> 46 | <ListBox.ItemTemplate> 47 | <DataTemplate> 48 | <Grid HorizontalAlignment="Stretch"> 49 | <TextBlock Text="{Binding}" 50 | TextWrapping="Wrap" 51 | FontSize="12" 52 | Margin="0,0" /> 53 | </Grid> 54 | </DataTemplate> 55 | </ListBox.ItemTemplate> 56 | </ListBox> 57 | 58 | </Grid> 59 | </ScrollViewer> 60 | </Border> 61 | 62 | </UserControl> -------------------------------------------------------------------------------- /Companion.Tests/Services/WifiCardDetectorTests.cs: -------------------------------------------------------------------------------- 1 | using Companion.Services; 2 | 3 | namespace OpenIPC.Companion.Tests.Services; 4 | 5 | [TestFixture] 6 | public class WifiCardDetectorTests 7 | { 8 | [Test] 9 | public void DetectWifiCard_Realtek8812_Returns88XXau() 10 | { 11 | string lsusbOutput = @" 12 | Bus 001 Device 001: ID 1d6b:0002 13 | Bus 001 Device 002: ID 0bda:8812 14 | "; //The @ allows for a multiline string 15 | 16 | 17 | string expectedDriver = "88XXau"; 18 | string actualDriver = WifiCardDetector.DetectWifiCard(lsusbOutput); 19 | 20 | Assert.That(actualDriver, Is.EqualTo(expectedDriver), "Driver detection failed for Realtek 8812."); 21 | } 22 | 23 | [Test] 24 | public void DetectWifiCard_NoMatchingDevice_ReturnsNull() 25 | { 26 | string lsusbOutput = @" 27 | Bus 001 Device 001: ID 1d6b:0002 28 | Bus 001 Device 002: ID 1234:5678 29 | "; //No matching DeviceID 30 | 31 | 32 | string? expectedDriver = null; 33 | string? actualDriver = WifiCardDetector.DetectWifiCard(lsusbOutput); 34 | 35 | Assert.That(actualDriver, Is.EqualTo(expectedDriver), "Driver detection failed for no matching device ID."); 36 | } 37 | 38 | [Test] 39 | public void DetectWifiCard_DeviceIdWithLetters_ReturnsCorrectDriver() 40 | { 41 | string lsusbOutput = @" 42 | Bus 001 Device 001: ID 1d6b:0002 43 | Bus 001 Device 002: ID 0bda:881a 44 | "; //Device ID with letter 45 | 46 | string expectedDriver = "88XXau"; 47 | string actualDriver = WifiCardDetector.DetectWifiCard(lsusbOutput); 48 | 49 | Assert.That(actualDriver, Is.EqualTo(expectedDriver), "Driver detection failed for the correct device id."); 50 | } 51 | 52 | [Test] 53 | public void DetectWifiCard_DeviceIdBU_ReturnsCorrectDriver() 54 | { 55 | string lsusbOutput = @" 56 | Bus 001 Device 001: ID 1d6b:0002 57 | Bus 001 Device 002: ID 0bda:f72b 58 | "; //Device ID with letter 59 | 60 | string expectedDriver = "8733bu"; 61 | string actualDriver = WifiCardDetector.DetectWifiCard(lsusbOutput); 62 | 63 | Assert.That(actualDriver, Is.EqualTo(expectedDriver), "Driver detection failed for the correct device id."); 64 | } 65 | 66 | [Test] 67 | public void DetectWifiCard_DeviceIdEU_ReturnsCorrectDriver() 68 | { 69 | string lsusbOutput = @" 70 | Bus 001 Device 001: ID 1d6b:0002 71 | Bus 001 Device 002: ID 0bda:a81a 72 | "; //Device ID with letter 73 | 74 | string expectedDriver = "8812eu"; 75 | string actualDriver = WifiCardDetector.DetectWifiCard(lsusbOutput); 76 | 77 | Assert.That(actualDriver, Is.EqualTo(expectedDriver), "Driver detection failed for the correct device id."); 78 | } 79 | } -------------------------------------------------------------------------------- /Companion/Services/UpdateChecker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.Configuration; 5 | using Newtonsoft.Json; 6 | using Serilog; 7 | 8 | namespace Companion.Services; 9 | 10 | public class UpdateChecker 11 | { 12 | private readonly HttpClient _httpClient; 13 | private readonly string _latestJsonUrl; 14 | 15 | public UpdateChecker(HttpClient httpClient, IConfiguration configuration) 16 | { 17 | _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); 18 | _latestJsonUrl = configuration["UpdateChecker:LatestJsonUrl"]; 19 | } 20 | 21 | 22 | public async Task<(bool HasUpdate, string ReleaseNotes, string DownloadUrl, string NewVersion)> CheckForUpdateAsync( 23 | string currentVersion) 24 | { 25 | try 26 | { 27 | var response = await _httpClient.GetStringAsync(_latestJsonUrl); 28 | var updateInfo = JsonConvert.DeserializeObject<UpdateInfo>(response); 29 | 30 | if (updateInfo != null && IsNewerVersion(updateInfo.Version, currentVersion)) 31 | return (true, updateInfo.ReleaseNotes, updateInfo.DownloadUrl, updateInfo.Version); 32 | } 33 | catch (Exception ex) 34 | { 35 | Log.Error($"Error during update check: {ex.Message}"); 36 | } 37 | 38 | return (false, string.Empty, string.Empty, string.Empty); 39 | } 40 | 41 | private bool IsNewerVersion(string newVersion, string currentVersion) 42 | { 43 | // Helper function to remove the "-v" prefix and extract the version number 44 | string ExtractVersionNumber(string version) 45 | { 46 | const string prefix = "v"; 47 | return version.StartsWith(prefix) ? version.Substring(prefix.Length) : version; 48 | } 49 | 50 | // Helper function to remove the "release-v" prefix and extract the version number 51 | string ExtractGHVersionNumber(string version) 52 | { 53 | const string prefix = "release-v"; 54 | return version.StartsWith(prefix) ? version.Substring(prefix.Length) : version; 55 | } 56 | 57 | // Extract and parse the version numbers 58 | return Version.TryParse(ExtractGHVersionNumber(newVersion), out var newVer) && 59 | Version.TryParse(ExtractVersionNumber(currentVersion), out var currVer) && 60 | newVer > currVer; 61 | } 62 | 63 | public class UpdateInfo 64 | { 65 | public string Version { get; set; } 66 | 67 | [JsonProperty("release_notes")] public string ReleaseNotes { get; set; } 68 | 69 | [JsonProperty("download_url")] public string DownloadUrl { get; set; } 70 | } 71 | } -------------------------------------------------------------------------------- /Companion.Tests/Companion.Tests.csproj: -------------------------------------------------------------------------------- 1 | <Project Sdk="Microsoft.NET.Sdk"> 2 | 3 | <PropertyGroup> 4 | <TargetFramework>net8.0</TargetFramework> 5 | <ImplicitUsings>enable</ImplicitUsings> 6 | <Nullable>enable</Nullable> 7 | 8 | <IsPackable>false</IsPackable> 9 | <IsTestProject>true</IsTestProject> 10 | </PropertyGroup> 11 | 12 | <ItemGroup> 13 | <PackageReference Include="Avalonia.Desktop" Version="0.10.13"/> 14 | <PackageReference Include="Avalonia.Svg.Skia" Version="11.2.0.2"/> 15 | <PackageReference Include="Avalonia.Xaml.Interactivity" Version="11.2.0.12" /> 16 | <PackageReference Include="coverlet.collector" Version="6.0.0"/> 17 | <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2" /> 18 | <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0"/> 19 | <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/> 20 | <PackageReference Include="Moq" Version="4.20.72"/> 21 | <PackageReference Include="NUnit" Version="3.14.0"/> 22 | <PackageReference Include="NUnit.Analyzers" Version="3.9.0"/> 23 | <PackageReference Include="NUnit3TestAdapter" Version="4.5.0"/> 24 | <PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1"/> 25 | <PackageReference Include="Newtonsoft.Json" Version="13.0.3"/> 26 | <PackageReference Include="Prism.Core" Version="9.0.537"/> 27 | <PackageReference Include="ReactiveUI" Version="20.1.63"/> 28 | <PackageReference Include="Serilog" Version="4.1.1-dev-02318"/> 29 | <PackageReference Include="Serilog.Settings.AppSettings" Version="3.0.0"/> 30 | <PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4"/> 31 | <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0"/> 32 | <PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0"/> 33 | <PackageReference Include="Serilog.Sinks.File" Version="6.0.0"/> 34 | <PackageReference Include="Serilog.Sinks.TextWriter" Version="3.0.0"/> 35 | <PackageReference Include="SharpCompress" Version="0.39.0" /> 36 | <PackageReference Include="SSH.NET" Version="2024.1.0"/> 37 | <PackageReference Include="xunit" Version="2.9.2"/> 38 | <PackageReference Include="YamlDotNet" Version="16.3.0" /> 39 | </ItemGroup> 40 | 41 | <ItemGroup> 42 | <Using Include="NUnit.Framework"/> 43 | </ItemGroup> 44 | 45 | <ItemGroup> 46 | <ProjectReference Include="..\Companion\Companion.csproj"/> 47 | </ItemGroup> 48 | 49 | <ItemGroup> 50 | <None Update="VERSION"> 51 | <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> 52 | </None> 53 | </ItemGroup> 54 | 55 | </Project> 56 | -------------------------------------------------------------------------------- /Companion.Android/Helpers/AndroidFileHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Android.App; 4 | using Android.Content; 5 | using Android.Content.Res; 6 | using Android.Util; 7 | 8 | namespace OpenIPC.Companion.Android.Helpers; 9 | 10 | public class AndroidFileHelper 11 | { 12 | public static string ReadAssetFile(string relativePath) 13 | { 14 | // var assets = Android.App.Application.Context.Assets; 15 | var assets = Application.Context.Assets; 16 | 17 | using (var stream = assets.Open(relativePath)) 18 | using (var reader = new StreamReader(stream)) 19 | { 20 | return reader.ReadToEnd(); 21 | } 22 | } 23 | 24 | public static string[] ListAssetFiles(string folderPath) 25 | { 26 | var assets = Application.Context.Assets; 27 | return assets.List(folderPath); // Lists all files in the folder 28 | } 29 | 30 | public static void CopyAssetsToInternalStorage(Context context) 31 | { 32 | var assets = context.Assets; 33 | var internalStoragePath = context.FilesDir.AbsolutePath; // Internal storage path 34 | 35 | // Start recursive copy from the root "binaries" folder 36 | CopyFolder(assets, "binaries", internalStoragePath); 37 | } 38 | 39 | private static void CopyFolder(AssetManager assets, string sourceFolder, string destinationFolder) 40 | { 41 | try 42 | { 43 | // Ensure the destination folder exists 44 | Directory.CreateDirectory(destinationFolder); 45 | 46 | // List all items (files and folders) in the source folder 47 | string[] items = assets.List(sourceFolder); 48 | 49 | foreach (var item in items) 50 | { 51 | var sourcePath = string.IsNullOrEmpty(sourceFolder) ? item : $"{sourceFolder}/{item}"; 52 | var destinationPath = Path.Combine(destinationFolder, item); 53 | 54 | // Check if the item is a directory or file 55 | if (assets.List(sourcePath).Length > 0) 56 | { 57 | // If it's a folder, recursively copy its contents 58 | CopyFolder(assets, sourcePath, destinationPath); 59 | } 60 | else 61 | { 62 | // If it's a file, copy it 63 | using (var input = assets.Open(sourcePath)) 64 | using (var output = new FileStream(destinationPath, FileMode.Create)) 65 | { 66 | input.CopyTo(output); 67 | } 68 | 69 | Log.Debug("FileHelper", $"Copied file: {sourcePath} to {destinationPath}"); 70 | } 71 | } 72 | } 73 | catch (Exception ex) 74 | { 75 | Log.Error("FileHelper", $"Error copying assets: {ex.Message}"); 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /Companion/Views/PresetsAddRepoView.axaml: -------------------------------------------------------------------------------- 1 | <UserControl xmlns="https://github.com/avaloniaui" 2 | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 3 | xmlns:vm="clr-namespace:Companion.ViewModels" 4 | xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 5 | xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 6 | mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" 7 | x:Class="Companion.Views.PresetsAddRepoView" 8 | x:DataType="vm:PresetsAddRepoViewModel"> 9 | <Design.DataContext> 10 | <!-- For Preview Purposes, You Can Instantiate a Dummy ViewModel --> 11 | <vm:PresetsAddRepoViewModel /> 12 | </Design.DataContext> 13 | 14 | <Grid RowDefinitions="Auto, Auto, Auto, *" 15 | ColumnDefinitions="*" Margin="10"> 16 | 17 | <!-- Header with Information --> 18 | <Border Grid.Row="0" Background="#B0B0B0" CornerRadius="10" Padding="10" Margin="10"> 19 | <Grid> 20 | <Grid.ColumnDefinitions> 21 | <ColumnDefinition Width="Auto" /> 22 | <ColumnDefinition Width="*" /> 23 | </Grid.ColumnDefinitions> 24 | 25 | <Path Data="M10,20 A10,10 0 1,1 10,0 A10,10 0 1,1 10,20 Z M10,7 A1.5,1.5 0 1,0 10,4 A1.5,1.5 0 1,0 10,7 Z M9,8 L11,8 L11,14 L9,14 Z" 26 | Fill="White" 27 | Width="20" 28 | Height="20" 29 | VerticalAlignment="Center" 30 | HorizontalAlignment="Center" 31 | Margin="5" /> 32 | <TextBlock Grid.Column="1" 33 | Text="Add Github Repository URL where your custom presets are defined." 34 | Foreground="Black" 35 | FontSize="14" 36 | TextWrapping="Wrap" 37 | VerticalAlignment="Center" 38 | Margin="10,0,0,0" /> 39 | </Grid> 40 | </Border> 41 | 42 | <StackPanel Orientation="Horizontal" Grid.Row="1" Margin="0,0,0,10" Spacing="10"> 43 | <!-- URL Box --> 44 | <TextBox Width="200" 45 | Watermark="📂 Url of presets" 46 | Margin="10,0,0,0" 47 | Text="{Binding RepoUrl, Mode=TwoWay}"/> 48 | 49 | <Button Width="150" Height="35" Background="{StaticResource OpenIPCBlueBrush}" 50 | VerticalAlignment="Center" CornerRadius="10" 51 | Command="{Binding AddRepositoryCommand, Mode=TwoWay}"> 52 | <!-- Text --> 53 | <TextBlock Text="Add" FontSize="14" Foreground="White" VerticalAlignment="Center" /> 54 | </Button> 55 | 56 | </StackPanel> 57 | </Grid> 58 | 59 | </UserControl> 60 | -------------------------------------------------------------------------------- /Companion/Services/ISshClientService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Companion.Models; 5 | using Renci.SshNet; 6 | 7 | namespace Companion.Services; 8 | 9 | public interface ISshClientService 10 | { 11 | // Executes a command on the remote device and returns its response 12 | Task<SshCommand> ExecuteCommandWithResponseAsync(DeviceConfig deviceConfig, string command, 13 | CancellationToken cancellationToken); 14 | 15 | 16 | // Executes a command on the remote device asynchronously 17 | Task ExecuteCommandAsync(DeviceConfig deviceConfig, string command); 18 | 19 | // Uploads a file from a local path to a remote path asynchronously using SCP 20 | Task UploadFileAsync(DeviceConfig deviceConfig, string localFilePath, string remotePath); 21 | 22 | // Synchronously uploads a file from a local path to a remote path using SCP 23 | void UploadFile(DeviceConfig deviceConfig, string localFilePath, string remotePath); 24 | 25 | // Uploads string content to a file on the remote device asynchronously using SCP 26 | Task UploadFileStringAsync(DeviceConfig deviceConfig, string remotePath, string fileContent); 27 | 28 | // Downloads a file from the remote device and returns its content as a string 29 | Task<byte[]> DownloadFileBytesAsync(DeviceConfig deviceConfig, string remotePath); 30 | 31 | Task<string> DownloadFileAsync(DeviceConfig deviceConfig, string remotePath); 32 | 33 | // Downloads a file from the remote path to the local path asynchronously using SCP 34 | Task DownloadFileLocalAsync(DeviceConfig deviceConfig, string remotePath, string localPath); 35 | 36 | // Recursively downloads all files and directories from a remote directory to a local directory 37 | Task DownloadDirectoryAsync(DeviceConfig deviceConfig, string remoteDirectory, string localDirectory); 38 | 39 | // Recursively uploads all files and directories from a local directory to a remote directory asynchronously using SCP 40 | Task UploadDirectoryAsync(DeviceConfig deviceConfig, string localDirectory, string remoteDirectory); 41 | 42 | // Uploads a specific binary file by file name using SCP 43 | Task UploadBinaryAsync(DeviceConfig deviceConfig, string remoteDirectory, string fileName); 44 | 45 | // Uploads a specific binary file by file type and name using SCP 46 | Task UploadBinaryAsync(DeviceConfig deviceConfig, string remoteDirectory, OpenIPC.FileType fileType, 47 | string fileName); 48 | 49 | // Executes a command on the remote device asynchronously with progress updates 50 | // Task ExecuteCommandWithProgressAsync(DeviceConfig deviceConfig, string command, 51 | // Action<string> updateProgress, CancellationToken cancellationToken = default); 52 | 53 | Task ExecuteCommandWithProgressAsync( 54 | DeviceConfig deviceConfig, 55 | string command, 56 | Action<string> updateProgress, 57 | CancellationToken cancellationToken = default, 58 | TimeSpan? timeout = null, 59 | Func<string, bool> isCommandComplete = null); 60 | } -------------------------------------------------------------------------------- /Companion/binaries/stream.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCREEN_MODE=$(</config/scripts/screen-mode) 4 | REC_FPS=$(</config/scripts/rec-fps) 5 | 6 | 7 | RUNNING=0 8 | STREAMING=0 9 | i=0 10 | j=0 11 | gpio_chip="gpiochip4" 12 | gpio_offset="10" 13 | gpio_chip2="gpiochip3" 14 | gpio_offset2="1" 15 | gpio_offset3="2" 16 | gpio_offset4="9" 17 | gpio_offset5="10" 18 | freq_list=("5180" "5200" "5220" "5240" "5260" "5280" "5300" "5320" "5500" "5520" "5540" "5560" "5580" "5600" "5620" "5640" "5660" "5680" "5700" "5720" "5745" "5765" "5785" "5805" "5825") 19 | chan_list=("36" "40" "44" "48" "52" "56" "60" "64" "100" "104" "108" "112" "116" "120" "124" "128" "132" "136" "140" "144" "149" "153" "157" "161" "165") 20 | 21 | cd /media 22 | 23 | 24 | while true; do 25 | if [ $(gpioget $gpio_chip $gpio_offset) -eq 0 ]; then 26 | 27 | if [ $RUNNING -eq 0 ]; then 28 | kill -15 $STREAMING 29 | sleep 0.1 30 | current_date=$(date +'%m-%d-%Y_%H-%M-%S') 31 | 32 | pixelpilot --osd --osd-elements video,wfbng --screen-mode $SCREEN_MODE --dvr-framerate $REC_FPS --dvr-fmp4 --dvr record_${current_date}.mp4 & 33 | 34 | RUNNING=$! 35 | else 36 | kill -15 $RUNNING 37 | RUNNING=0 38 | STREAMING=0 39 | fi 40 | sleep 0.1 41 | elif [ $STREAMING -eq 0 ]; then 42 | 43 | pixelpilot --osd --osd-elements video,wfbng --screen-mode $SCREEN_MODE & 44 | 45 | STREAMING=$! 46 | 47 | fi 48 | if [ $(gpioget $gpio_chip2 $gpio_offset4) -eq 1 ]; then 49 | Freq=${freq_list[$i]} 50 | Chan=${chan_list[$i]} 51 | iw wlx04d4c464afea set freq $Freq 52 | sed -i "s/wifi_channel = .*/wifi_channel = $Chan/" /etc/wifibroadcast.cfg 53 | echo "$Freq" 54 | i=$((i+1)) 55 | if [[ $i -gt 24 ]] 56 | then 57 | i=0 58 | fi 59 | fi 60 | if [ $(gpioget $gpio_chip2 $gpio_offset5) -eq 1 ]; then 61 | i=$((i-1)) 62 | if [[ $i -lt 0 ]] 63 | then 64 | i=24 65 | fi 66 | Freq=${freq_list[$i]} 67 | Chan=${chan_list[$i]} 68 | iw wlx04d4c464afea set freq $Freq 69 | sed -i "s/wifi_channel = .*/wifi_channel = $Chan/" /etc/wifibroadcast.cfg 70 | echo "$Freq" 71 | fi 72 | if [ $(gpioget $gpio_chip2 $gpio_offset2) -eq 1 ]; then 73 | for Freq in ${freq_list[@]}; do 74 | Chan=${chan_list[$j]} 75 | if [ $(gpioget $gpio_chip2 $gpio_offset3) -eq 1 ]; then 76 | echo "exit loop" 77 | break 78 | fi 79 | iw wlx04d4c464afea set freq $Freq 80 | sed -i "s/wifi_channel = .*/wifi_channel = $Chan/" /etc/wifibroadcast.cfg 81 | j=$((j+1)) 82 | if [[ $j -gt 24 ]] 83 | then 84 | j=0 85 | fi 86 | echo "$Freq" 87 | sleep 3 88 | if [ $(gpioget $gpio_chip2 $gpio_offset3) -eq 1 ]; then 89 | echo "exit loop" 90 | break 91 | fi 92 | done 93 | fi 94 | sleep 0.2 95 | done 96 | -------------------------------------------------------------------------------- /Companion/Services/SettingsManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Newtonsoft.Json; 4 | using Companion.Models; 5 | using Serilog; 6 | 7 | namespace Companion.Services; 8 | 9 | public static class SettingsManager 10 | { 11 | private static readonly string AppSettingsName = "openipc_settings.json"; 12 | 13 | public static string AppSettingFilename 14 | { 15 | get; 16 | set; 17 | // Allow setting a custom filename for testing 18 | } = $"{OpenIPC.AppDataConfigDirectory}/openipc_settings.json"; 19 | 20 | 21 | /// <summary> 22 | /// Loads the device configuration settings from a JSON file. 23 | /// </summary> 24 | /// <returns> 25 | /// A <see cref="DeviceConfig" /> object containing the loaded settings. 26 | /// If the settings file does not exist, returns a <see cref="DeviceConfig" /> 27 | /// with default values. 28 | /// </returns> 29 | public static DeviceConfig? LoadSettings() 30 | { 31 | DeviceConfig deviceConfig; 32 | 33 | if (File.Exists(AppSettingFilename)) 34 | try 35 | { 36 | var json = File.ReadAllText(AppSettingFilename); 37 | deviceConfig = JsonConvert.DeserializeObject<DeviceConfig>(json); 38 | 39 | if (deviceConfig != null) 40 | // Optionally publish an event if needed 41 | // eventAggregator?.GetEvent<DeviceStateUpdatedEvent>()?.Publish( 42 | // new DeviceStateUpdatedMessage(true, deviceConfig)); 43 | return deviceConfig; 44 | 45 | Log.Error("LoadSettings: deviceConfig is null. The file content might be corrupted."); 46 | } 47 | catch (JsonException ex) 48 | { 49 | Log.Error($"LoadSettings: Failed to parse JSON. Exception: {ex.Message}"); 50 | } 51 | catch (IOException ex) 52 | { 53 | Log.Error($"LoadSettings: File IO error. Exception: {ex.Message}"); 54 | } 55 | catch (Exception ex) 56 | { 57 | Log.Error($"LoadSettings: Unexpected error. Exception: {ex.Message}"); 58 | } 59 | 60 | // Default values if no settings file exists or an error occurs 61 | return new DeviceConfig 62 | { 63 | IpAddress = "", 64 | Username = "", 65 | Password = "", 66 | DeviceType = DeviceType.Camera, 67 | }; 68 | } 69 | 70 | 71 | /// <summary> 72 | /// Saves the device configuration settings to a JSON file. 73 | /// </summary> 74 | /// <param name="settings">The <see cref="DeviceConfig" /> object containing the settings to be saved.</param> 75 | /// <remarks> 76 | /// This method serializes the provided <see cref="DeviceConfig" /> object into a JSON format and writes it to a file. 77 | /// </remarks> 78 | public static void SaveSettings(DeviceConfig settings) 79 | { 80 | var json = JsonConvert.SerializeObject(settings, Formatting.Indented); 81 | File.WriteAllText(AppSettingFilename, json); 82 | } 83 | } -------------------------------------------------------------------------------- /Companion.Desktop/Companion.Desktop.csproj: -------------------------------------------------------------------------------- 1 | <Project Sdk="Microsoft.NET.Sdk"> 2 | <PropertyGroup> 3 | <OutputType>WinExe</OutputType> 4 | <!--If you are willing to use Windows/MacOS native APIs you will need to create 3 projects. 5 | One for Windows with net8.0-windows TFM, one for MacOS with net8.0-macos and one with net8.0 TFM for Linux.--> 6 | <TargetFramework>net8.0</TargetFramework> 7 | <Nullable>enable</Nullable> 8 | <BuiltInComInteropSupport>true</BuiltInComInteropSupport> 9 | <RuntimeIdentifiers>osx-arm64;win-x64;win-arm64;linux-x64;linux-arm64</RuntimeIdentifiers> 10 | <SelfContained>true</SelfContained> 11 | <RootNamespace>Companion.Desktop</RootNamespace> 12 | <AssemblyName>Companion.Desktop</AssemblyName> 13 | <UseAppHost>true</UseAppHost> 14 | <MacCatalystIcon>Assets/Icons/OpenIPC.icns</MacCatalystIcon> 15 | </PropertyGroup> 16 | 17 | <PropertyGroup> 18 | <ApplicationManifest>app.manifest</ApplicationManifest> 19 | </PropertyGroup> 20 | 21 | <ItemGroup> 22 | <PackageReference Include="Avalonia.Desktop" Version="$(AvaloniaVersion)"/> 23 | <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.--> 24 | <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)"/> 25 | <PackageReference Include="Avalonia.Svg.Skia" Version="11.2.0.2"/> 26 | <PackageReference Include="Avalonia.Xaml.Interactivity" Version="11.2.0.12" /> 27 | <PackageReference Include="MessageBox.Avalonia" Version="3.2.0"/> 28 | <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2" /> 29 | <PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0-rc.2.24473.5"/> 30 | <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0-rc.2.24473.5"/> 31 | <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0"/> 32 | <PackageReference Include="Newtonsoft.Json" Version="13.0.3"/> 33 | <PackageReference Include="Prism.Core" Version="9.0.537"/> 34 | <PackageReference Include="ReactiveUI" Version="20.1.63"/> 35 | <PackageReference Include="Serilog" Version="4.1.1-dev-02318"/> 36 | <PackageReference Include="Serilog.Settings.AppSettings" Version="3.0.0"/> 37 | <PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4"/> 38 | <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0"/> 39 | <PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0"/> 40 | <PackageReference Include="Serilog.Sinks.File" Version="6.0.0"/> 41 | <PackageReference Include="Serilog.Sinks.TextWriter" Version="3.0.0"/> 42 | <PackageReference Include="SharpCompress" Version="0.39.0" /> 43 | <PackageReference Include="SSH.NET" Version="2024.1.0"/> 44 | <PackageReference Include="YamlDotNet" Version="16.3.0" /> 45 | </ItemGroup> 46 | 47 | <ItemGroup> 48 | <ProjectReference Include="..\Companion\Companion.csproj"/> 49 | </ItemGroup> 50 | </Project> 51 | -------------------------------------------------------------------------------- /Companion/binaries/clean/telemetry_msposd_gs: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | fw=$(grep "BUILD_OPTION" "/etc/os-release" | cut -d= -f2) 3 | keydir=/etc 4 | 5 | if [ -e /etc/datalink.conf ]; then 6 | . /etc/datalink.conf 7 | fi 8 | 9 | if [ -e /etc/telemetry.conf ]; then 10 | . /etc/telemetry.conf 11 | fi 12 | 13 | if [ ! -e /usr/bin/telemetry_rx ] || [ ! -e /usr/bin/telemetry_tx ]; then 14 | ln -fs /usr/bin/wfb_rx /usr/bin/telemetry_rx 15 | ln -fs /usr/bin/wfb_tx /usr/bin/telemetry_tx 16 | fi 17 | 18 | start_drone_telemetry() { 19 | if [ "$router" -lt 2 ]; then 20 | if [ "$one_way" = "false" ]; then 21 | telemetry_rx -p "$stream_rx" -u "$port_rx" -K "$keydir/$unit.key" -i "$link_id" "$wlan" > /dev/null & 22 | fi 23 | telemetry_tx -p "$stream_tx" -u "$port_tx" -K "$keydir/$unit.key" -B "$bandwidth" \ 24 | -M "$mcs_index" -S "$stbc" -L "$ldpc" -G "$guard_interval" -k "$fec_k" -n "$fec_n" \ 25 | -T "$pool_timeout" -i "$link_id" -f "$frame_type" "$wlan" > /dev/null & 26 | fi 27 | } 28 | 29 | start_gs_telemetry() { 30 | if [ "$one_way" = "false" ]; then 31 | telemetry_tx -p "$stream_tx" -u "$port_tx" -K "$keydir/$unit.key" -B "$bandwidth" \ 32 | -M "$mcs_index" -S "$stbc" -L "$ldpc" -G "$guard_interval" -k "$fec_k" -n "$fec_n" \ 33 | -T "$pool_timeout" -i "$link_id" -f "$frame_type" "$wlan" > /dev/null & 34 | fi 35 | telemetry_rx -p "$stream_rx" -u "$port_rx" -K "$keydir/$unit.key" -i "$link_id" "$wlan" > /dev/null & 36 | } 37 | 38 | case "$1" in 39 | start) 40 | echo "Loading MAVLink telemetry service..." 41 | if [ "$router" -eq 1 ] || [ "$fw" = "lte" ]; then 42 | mavlink-routerd -c /etc/mavlink.conf > /dev/null 2>&1 & 43 | else 44 | if [ "$router" -eq 2 ]; then 45 | msposd --channels "$channels" --master "$serial" --baudrate "$baud" \ 46 | --out 10.5.0.1:5000 -r "$fps" --ahi "$ahi" > /dev/null & 47 | sleep 5 48 | echo "&L70 &F35 CPU:&C &B Temp:&T" >/tmp/MSPOSD.msg & 49 | else 50 | mavfwd --channels "$channels" --master "$serial" --baudrate "$baud" -p 100 -t -a "$aggregate" \ 51 | --out 127.0.0.1:$port_tx --in 127.0.0.1:$port_rx > /dev/null & 52 | fi 53 | fi 54 | 55 | if [ "$fw" = "fpv" ] || [ "$fw" = "venc" ]; then 56 | start_${unit}_telemetry 57 | fi 58 | ;; 59 | 60 | stop) 61 | echo "Stopping telemetry services..." 62 | killall -q telemetry_rx 63 | killall -q telemetry_tx 64 | killall -q mavlink-routerd 65 | killall -q mavfwd 66 | killall -q msposd 67 | ;; 68 | 69 | *) 70 | echo "Usage: $0 {start|stop}" 71 | exit 1 72 | ;; 73 | esac 74 | -------------------------------------------------------------------------------- /.github/workflows/build-release-android.yml: -------------------------------------------------------------------------------- 1 | name: Android App Build 2 | 3 | concurrency: 4 | group: coverage-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | permissions: 8 | contents: write 9 | actions: read 10 | 11 | on: 12 | workflow_dispatch: 13 | # push: 14 | # branches: 15 | # - android # Trigger only on pushes to the android branch 16 | # push: 17 | # tags: 18 | # - 'release-v*' # Trigger for tags that start with 'v', e.g., v1.0.0, v2.1.3 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | strategy: 24 | matrix: 25 | os: [ubuntu-latest] 26 | arch: [x64, arm64] 27 | 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v3 31 | 32 | - name: Set up .NET SDK 33 | uses: actions/setup-dotnet@v3 34 | with: 35 | dotnet-version: '8.0.x' 36 | 37 | - name: Update .NET SDK Workloads 38 | run: | 39 | dotnet workload update 40 | 41 | - name: Install Android Workload 42 | run: | 43 | dotnet workload install android 44 | 45 | - name: Restore dependencies 46 | run: dotnet restore OpenIPC_Config.Android/OpenIPC_Config.Android.csproj 47 | 48 | - name: Install Android SDK 49 | run: | 50 | echo "ANDROID_HOME=$HOME/android-sdk" >> $GITHUB_ENV 51 | echo "ANDROID_SDK_ROOT=$HOME/android-sdk" >> $GITHUB_ENV 52 | mkdir -p $HOME/android-sdk 53 | curl -o sdk-tools.zip https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip 54 | unzip -o sdk-tools.zip -d $HOME/android-sdk/cmdline-tools 55 | mkdir -p $HOME/android-sdk/cmdline-tools/latest 56 | mv $HOME/android-sdk/cmdline-tools/cmdline-tools/* $HOME/android-sdk/cmdline-tools/latest/ 57 | yes | $HOME/android-sdk/cmdline-tools/latest/bin/sdkmanager --licenses 58 | $HOME/android-sdk/cmdline-tools/latest/bin/sdkmanager \ 59 | "platform-tools" \ 60 | "platforms;android-34" \ 61 | "build-tools;34.0.0" 62 | 63 | - name: Build Android APK 64 | run: | 65 | dotnet build OpenIPC_Config.Android/OpenIPC_Config.Android.csproj --configuration Release 66 | dotnet publish OpenIPC_Config.Android/OpenIPC_Config.Android.csproj -c Release -o ./output-apk 67 | shell: bash 68 | 69 | - name: Package Android APK 70 | run: | 71 | mkdir -p OpenIPC-Config-android-${{ matrix.arch }} 72 | cp ./output-apk/* OpenIPC-Config-android-${{ matrix.arch }}/ 73 | zip -r OpenIPC-Config-android-${{ matrix.arch }}.zip OpenIPC-Config-android-${{ matrix.arch }} 74 | shell: bash 75 | 76 | - name: Upload Android Artifact 77 | uses: actions/upload-artifact@v4 78 | with: 79 | name: OpenIPC-Config-android-${{ matrix.arch }} 80 | path: OpenIPC-Config-android-${{ matrix.arch }}.zip 81 | 82 | 83 | # - name: Upload APK to GitHub Release 84 | # uses: softprops/action-gh-release@v1 85 | # with: 86 | # files: | 87 | # OpenIPC-Config-android-${{ matrix.arch }}.zip 88 | # draft: false # Set to `true` if you want the release to remain in draft 89 | 90 | -------------------------------------------------------------------------------- /Companion/binaries/clean/telemetry_msposd_extra: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | fw=$(grep "BUILD_OPTION" "/etc/os-release" | cut -d= -f2) 3 | keydir=/etc 4 | 5 | if [ -e /etc/datalink.conf ]; then 6 | . /etc/datalink.conf 7 | fi 8 | 9 | if [ -e /etc/telemetry.conf ]; then 10 | . /etc/telemetry.conf 11 | fi 12 | 13 | if [ ! -e /usr/bin/telemetry_rx ] || [ ! -e /usr/bin/telemetry_tx ]; then 14 | ln -fs /usr/bin/wfb_rx /usr/bin/telemetry_rx 15 | ln -fs /usr/bin/wfb_tx /usr/bin/telemetry_tx 16 | fi 17 | 18 | start_drone_telemetry() { 19 | if [ "$router" -lt 2 ]; then 20 | if [ "$one_way" = "false" ]; then 21 | telemetry_rx -p "$stream_rx" -u "$port_rx" -K "$keydir/$unit.key" -i "$link_id" "$wlan" > /dev/null & 22 | fi 23 | telemetry_tx -p "$stream_tx" -u "$port_tx" -K "$keydir/$unit.key" -B "$bandwidth" \ 24 | -M "$mcs_index" -S "$stbc" -L "$ldpc" -G "$guard_interval" -k "$fec_k" -n "$fec_n" \ 25 | -T "$pool_timeout" -i "$link_id" -f "$frame_type" "$wlan" > /dev/null & 26 | fi 27 | } 28 | 29 | start_gs_telemetry() { 30 | if [ "$one_way" = "false" ]; then 31 | telemetry_tx -p "$stream_tx" -u "$port_tx" -K "$keydir/$unit.key" -B "$bandwidth" \ 32 | -M "$mcs_index" -S "$stbc" -L "$ldpc" -G "$guard_interval" -k "$fec_k" -n "$fec_n" \ 33 | -T "$pool_timeout" -i "$link_id" -f "$frame_type" "$wlan" > /dev/null & 34 | fi 35 | telemetry_rx -p "$stream_rx" -u "$port_rx" -K "$keydir/$unit.key" -i "$link_id" "$wlan" > /dev/null & 36 | } 37 | 38 | case "$1" in 39 | start) 40 | echo "Loading MAVLink telemetry service..." 41 | if [ "$router" -eq 1 ] || [ "$fw" = "lte" ]; then 42 | mavlink-routerd -c /etc/mavlink.conf > /dev/null 2>&1 & 43 | else 44 | if [ "$router" -eq 2 ]; then 45 | msposd --channels "$channels" --master "$serial" --baudrate "$baud" \ 46 | --out 127.0.0.1:$(($port_tx + 1)) -osd -r "$fps" --ahi "$ahi" > /dev/null & 47 | sleep 5 48 | echo "&L70 &F35 CPU:&C &B Temp:&T" >/tmp/MSPOSD.msg & 49 | else 50 | mavfwd --channels "$channels" --master "$serial" --baudrate "$baud" -p 100 -t -a "$aggregate" \ 51 | --out 127.0.0.1:$port_tx --in 127.0.0.1:$port_rx > /dev/null & 52 | fi 53 | fi 54 | 55 | if [ "$fw" = "fpv" ] || [ "$fw" = "venc" ]; then 56 | start_${unit}_telemetry 57 | fi 58 | ;; 59 | 60 | stop) 61 | echo "Stopping telemetry services..." 62 | killall -q telemetry_rx 63 | killall -q telemetry_tx 64 | killall -q mavlink-routerd 65 | killall -q mavfwd 66 | killall -q msposd 67 | ;; 68 | 69 | *) 70 | echo "Usage: $0 {start|stop}" 71 | exit 1 72 | ;; 73 | esac 74 | -------------------------------------------------------------------------------- /Companion/Services/WifiCardDetector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text.RegularExpressions; 4 | using Serilog; 5 | using Serilog.Core; 6 | 7 | namespace Companion.Services; 8 | 9 | public class WifiCardDetector 10 | { 11 | public static string DetectWifiCard(string lsusbOutput) 12 | { 13 | // Parse the lsusb output to extract device IDs. This is more robust than cutting by spaces. 14 | // Assumes each line in lsusbOutput contains "ID <vendorID>:<productID>" 15 | var deviceIds = lsusbOutput.Split(new[] { "\n" }, StringSplitOptions.RemoveEmptyEntries) 16 | .Select(line => 17 | { 18 | Match match = Regex.Match(line, @"ID\s+([0-9a-f]{4}):([0-9a-f]{4})", RegexOptions.IgnoreCase); 19 | if (match.Success) 20 | { 21 | return $"{match.Groups[1].Value}:{match.Groups[2].Value}".ToLower(); // Convert to lowercase for consistent matching 22 | } 23 | return null; 24 | }) 25 | .Where(id => !string.IsNullOrEmpty(id)) //Remove nulls 26 | .Distinct() // Mimic the "sort | uniq" 27 | .ToList(); 28 | 29 | string driver = null; // Initialize driver to null 30 | 31 | foreach (string card in deviceIds) 32 | { 33 | switch (card) 34 | { 35 | case "0bda:8812": 36 | case "0bda:881a": 37 | case "0b05:17d2": 38 | case "2357:0101": 39 | case "2604:0012": 40 | driver = "88XXau"; 41 | Log.Information($"Detected WiFi card: {card}, Driver: {driver}"); // Log the detection 42 | // Simulate modprobe with appropriate parameters (replace with actual C# equivalent if needed) 43 | //Modprobe("88XXau", $"rtw_tx_pwr_idx_override={driver_txpower_override}"); //Replace with the C# call 44 | break; 45 | 46 | case "0bda:a81a": 47 | driver = "8812eu"; 48 | Log.Information($"Detected WiFi card: {card}, Driver: {driver}"); 49 | // Simulate modprobe with appropriate parameters 50 | //Modprobe("8812eu", "rtw_regd_src=1 rtw_tx_pwr_by_rate=0 rtw_tx_pwr_lmt_enable=0"); //Replace with the C# call 51 | break; 52 | 53 | case "0bda:f72b": 54 | case "0bda:b733": 55 | driver = "8733bu"; 56 | Log.Information($"Read WiFi card: {card}, Driver: {driver}"); 57 | // Simulate modprobe with appropriate parameters 58 | //Modprobe("8733bu", "rtw_regd_src=1 rtw_tx_pwr_by_rate=0 rtw_tx_pwr_lmt_enable=0"); //Replace with the C# call 59 | break; 60 | 61 | case "0cf3:9271": 62 | case "040d:3801": 63 | driver = "ar9271"; 64 | Log.Information($"Read WiFi card: {card}, Driver: {driver}"); 65 | break; 66 | } 67 | 68 | if (driver != null) break; //Stop at the first match (mimics the bash script behavior) 69 | } 70 | 71 | return driver; // Return the driver if found, otherwise null 72 | } 73 | 74 | 75 | 76 | } 77 | -------------------------------------------------------------------------------- /Companion.Android/Companion.Android.csproj: -------------------------------------------------------------------------------- 1 | <Project Sdk="Microsoft.NET.Sdk"> 2 | <PropertyGroup> 3 | <OutputType>Exe</OutputType> 4 | <TargetFramework>net8.0-android34.0</TargetFramework> 5 | <SupportedOSPlatformVersion>21</SupportedOSPlatformVersion> 6 | <Nullable>enable</Nullable> 7 | <ApplicationId>org.openipc.Companion</ApplicationId> 8 | <ApplicationVersion>1</ApplicationVersion> 9 | <ApplicationDisplayVersion>1.0</ApplicationDisplayVersion> 10 | <AndroidPackageFormat>apk</AndroidPackageFormat> 11 | <AndroidEnableProfiledAot>false</AndroidEnableProfiledAot> 12 | <RuntimeIdentifiers>android-arm64;android-arm;android-x86;android-x64</RuntimeIdentifiers> 13 | <RootNamespace>Companion.Android</RootNamespace> 14 | <AssemblyName>Companion.Android</AssemblyName> 15 | </PropertyGroup> 16 | 17 | <PropertyGroup Condition="'$(TargetFramework)' == 'net8.0-android34.0'"> 18 | <DefineConstants>$(DefineConstants);ANDROID</DefineConstants> 19 | </PropertyGroup> 20 | 21 | <ItemGroup> 22 | <AndroidResource Include="Icon.png"> 23 | <Link>Resources\drawable\Icon.png</Link> 24 | </AndroidResource> 25 | </ItemGroup> 26 | 27 | <ItemGroup> 28 | <AndroidAsset Include="..\Companion\binaries\**\*"> 29 | <Link>assets\binaries\%(RecursiveDir)%(Filename)%(Extension)</Link> 30 | </AndroidAsset> 31 | </ItemGroup> 32 | 33 | 34 | <ItemGroup> 35 | <PackageReference Include="Avalonia.Android" Version="$(AvaloniaVersion)"/> 36 | <PackageReference Include="Avalonia.Svg.Skia" Version="11.2.0.2"/> 37 | <PackageReference Include="Avalonia.Xaml.Interactivity" Version="11.2.0.12" /> 38 | <PackageReference Include="MessageBox.Avalonia" Version="3.2.0"/> 39 | <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2" /> 40 | <PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0-rc.2.24473.5"/> 41 | <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0-rc.2.24473.5"/> 42 | <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0"/> 43 | <PackageReference Include="Newtonsoft.Json" Version="13.0.3"/> 44 | <PackageReference Include="Prism.Core" Version="9.0.537"/> 45 | <PackageReference Include="ReactiveUI" Version="20.1.63"/> 46 | <PackageReference Include="Serilog" Version="4.1.1-dev-02318"/> 47 | <PackageReference Include="Serilog.Settings.AppSettings" Version="3.0.0"/> 48 | <PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4"/> 49 | <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0"/> 50 | <PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0"/> 51 | <PackageReference Include="Serilog.Sinks.File" Version="6.0.0"/> 52 | <PackageReference Include="Serilog.Sinks.TextWriter" Version="3.0.0"/> 53 | <PackageReference Include="SharpCompress" Version="0.39.0" /> 54 | <PackageReference Include="SSH.NET" Version="2024.1.0"/> 55 | <PackageReference Include="Xamarin.AndroidX.Core.SplashScreen" Version="1.0.1.1"/> 56 | <PackageReference Include="YamlDotNet" Version="16.3.0" /> 57 | </ItemGroup> 58 | 59 | <ItemGroup> 60 | <ProjectReference Include="..\Companion\Companion.csproj" /> 61 | </ItemGroup> 62 | </Project> 63 | -------------------------------------------------------------------------------- /Companion.Tests/Services/WfbGsConfigParserTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using Companion.Services; 3 | using Serilog; 4 | using Serilog.Events; 5 | using Xunit; 6 | using Assert = Xunit.Assert; 7 | 8 | namespace OpenIPC.Companion.Tests.Services; 9 | 10 | public class WfbGsConfigParserTests : IDisposable 11 | { 12 | private readonly Mock<ILogger> _loggerMock; 13 | private readonly StringWriter _logOutput; 14 | private readonly WfbGsConfigParser _parser; 15 | 16 | public WfbGsConfigParserTests() 17 | { 18 | _loggerMock = new Mock<ILogger>(); 19 | _logOutput = new StringWriter(); 20 | Log.Logger = new LoggerConfiguration() 21 | .WriteTo.TextWriter(_logOutput) 22 | .CreateLogger(); 23 | _parser = new WfbGsConfigParser(); 24 | } 25 | 26 | public void Dispose() 27 | { 28 | _logOutput.Dispose(); 29 | Log.CloseAndFlush(); 30 | } 31 | 32 | [Fact] 33 | public void ParseConfigString_EmptyConfigContent_LogsError() 34 | { 35 | // Arrange 36 | var configContent = string.Empty; 37 | 38 | // Configure the logger to use the mock logger 39 | Log.Logger = _loggerMock.Object; 40 | 41 | // Act 42 | _parser.ParseConfigString(configContent); 43 | 44 | // Assert 45 | _loggerMock.Verify( 46 | l => l.Write( 47 | It.Is<LogEventLevel>(level => level == LogEventLevel.Error), 48 | It.Is<string>(msg => msg.Contains("Config content is empty or null."))), 49 | Times.Once); 50 | } 51 | 52 | [Fact] 53 | public void ParseConfigString_ValidConfigContent_ParsesTxPower() 54 | { 55 | // Arrange 56 | var configContent = "options 88XXau_wfb rtw_tx_pwr_idx_override=25"; 57 | 58 | // Act 59 | _parser.ParseConfigString(configContent); 60 | 61 | // Assert 62 | Assert.Equal("25", _parser.TxPower); 63 | } 64 | 65 | [Fact] 66 | public void ParseConfigString_ConfigContentWithComments_ParsesTxPower() 67 | { 68 | // Arrange 69 | var configContent = "# Comment\noptions 88XXau_wfb rtw_tx_pwr_idx_override=1"; 70 | 71 | // Act 72 | _parser.ParseConfigString(configContent); 73 | 74 | // Assert 75 | Assert.Equal("1", _parser.TxPower); 76 | } 77 | 78 | [Fact] 79 | public void GetUpdatedConfigString_UpdatedTxPower_ReturnsUpdatedConfig() 80 | { 81 | // Arrange 82 | var configContent = "options 88XXau_wfb rtw_tx_pwr_idx_override=1"; 83 | _parser.ParseConfigString(configContent); 84 | _parser.TxPower = "30"; 85 | 86 | // Act 87 | var updatedConfig = _parser.GetUpdatedConfigString(); 88 | 89 | // Assert 90 | Assert.Contains("rtw_tx_pwr_idx_override=30", updatedConfig); 91 | } 92 | 93 | [Fact] 94 | public void GetUpdatedConfigString_PreservesComments_ReturnsUpdatedConfig() 95 | { 96 | // Arrange 97 | var configContent = "# Comment\noptions 88XXau_wfb rtw_tx_pwr_idx_override=1"; 98 | _parser.ParseConfigString(configContent); 99 | _parser.TxPower = "2"; 100 | 101 | // Act 102 | var updatedConfig = _parser.GetUpdatedConfigString(); 103 | 104 | // Assert 105 | Assert.Contains("# Comment", updatedConfig); 106 | Assert.Contains("rtw_tx_pwr_idx_override=2", updatedConfig); 107 | } 108 | } -------------------------------------------------------------------------------- /README-Linux.md: -------------------------------------------------------------------------------- 1 | # Another Configurator 2 | 3 | 4 | 5 | ## Install required dependencies 6 | ```bash 7 | sudo apt-get install -y dotnet-sdk-8.0 8 | sudo apt-get install -y dotnet-runtime-8.0 9 | 10 | 11 | ``` 12 | 13 | ### Project Breakdown 14 | 15 | 1. **OpenIPC** 16 | - This appears to be the core or shared library project that other platform-specific projects reference. 17 | - It likely contains shared code, models, services, or ViewModels that can be used across all platforms (Desktop, Android, Browser, iOS). 18 | 19 | 2. **OpenIPC.Desktop** 20 | - This project is intended for **desktop platforms** (Windows, macOS, and Linux). 21 | - In Avalonia, a "Desktop" project can target multiple operating systems using `net7.0-windows`, `net7.0-macos`, and `net7.0-linux`. 22 | - You can configure it to build for any or all of these desktop operating systems. 23 | 24 | 3. **OpenIPC.Android** 25 | - This project targets **Android** devices. 26 | - It will have Android-specific configurations and might use `net8.0-android` or `net7.0-android` as the `TargetFramework`. 27 | - Contains Android-specific files and setup, like Android permissions, manifest configurations, etc. 28 | 29 | 4. **OpenIPC.Browser** 30 | - This project is meant for **web applications**, targeting **WebAssembly (WASM)** using Avalonia's browser support. 31 | - The target framework is typically `net8.0-browser`. 32 | - This allows you to run the app in a web browser, making use of WebAssembly technology to execute .NET code in a web environment. 33 | 34 | 5. **OpenIPC.iOS** 35 | - This project is intended for **iOS devices** (iPhones and iPads). 36 | - Uses a target framework like `net8.0-ios` or `net7.0-ios` with iOS-specific configurations. 37 | - Contains iOS-specific files such as `Info.plist` for app metadata, `Entitlements.plist` for permissions, and `AppDelegate.cs` for application lifecycle management. 38 | 39 | ### Summary of Target Platforms for Each Project 40 | 41 | | Project | Target Platform(s) | Description | 42 | |--------------------|------------------------|--------------------------------------------------------------------------------------------------| 43 | | **OpenIPC** | Shared (all platforms) | Core library or shared code used across all platform-specific projects. | 44 | | **OpenIPC.Desktop**| Windows, macOS, Linux | Targets desktop OSs, can be configured to support `win-x64`, `osx-arm64`, `linux-x64`, etc. | 45 | | **OpenIPC.Android**| Android | Targets Android devices with `net8.0-android` or `net7.0-android`. | 46 | | **OpenIPC.Browser**| Browser (WebAssembly) | Targets web browsers with WebAssembly using `net8.0-browser`. | 47 | | **OpenIPC.iOS** | iOS | Targets iOS devices with `net8.0-ios` or `net7.0-ios`. | 48 | 49 | ### Building Each Project 50 | 51 | To build each project, navigate to the specific project folder and run the `dotnet publish` command with the appropriate runtime identifier for that platform. 52 | 53 | For example: 54 | - **macOS Desktop**: Go to `OpenIPC.Desktop` and run: 55 | 56 | ```bash 57 | dotnet publish -c Release -r osx-arm64 --self-contained 58 | 59 | 60 | 61 | 62 | 63 | IOS: 64 | https://docs.avaloniaui.net/docs/guides/platforms/ios/build-and-run-your-application-on-your-iphone-or-ipad -------------------------------------------------------------------------------- /Companion.Tests/Services/WifiConfigParserTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using Companion.Services; 3 | using Serilog; 4 | 5 | namespace OpenIPC.Companion.Tests.Services; 6 | 7 | [TestFixture] 8 | public class WifiConfigParserTests 9 | { 10 | [SetUp] 11 | public void SetUp() 12 | { 13 | // Mock the logger 14 | _mockLogger = new Mock<ILogger>(); 15 | Log.Logger = _mockLogger.Object; 16 | 17 | // Initialize WifiConfigParser 18 | _wifiConfigParser = new WifiConfigParser(); 19 | } 20 | 21 | private Mock<ILogger> _mockLogger; 22 | private WifiConfigParser _wifiConfigParser; 23 | 24 | [Test] 25 | public void ParseConfigString_ValidConfig_SetsProperties() 26 | { 27 | // Arrange 28 | var configContent = """ 29 | wifi_channel = 6 30 | wifi_region = 'US' 31 | 32 | [gs_mavlink] 33 | peer = '192.168.0.2' 34 | 35 | [gs_video] 36 | peer = '192.168.0.3' 37 | """; 38 | 39 | // Act 40 | _wifiConfigParser.ParseConfigString(configContent); 41 | 42 | // Assert 43 | Assert.That(_wifiConfigParser.WifiChannel, Is.EqualTo(6)); 44 | Assert.That(_wifiConfigParser.WifiRegion, Is.EqualTo("US")); 45 | Assert.That(_wifiConfigParser.GsMavlinkPeer, Is.EqualTo("192.168.0.2")); 46 | Assert.That(_wifiConfigParser.GsVideoPeer, Is.EqualTo("192.168.0.3")); 47 | } 48 | 49 | [Test] 50 | public void GetUpdatedConfigString_ValidUpdates_ReturnsUpdatedConfig() 51 | { 52 | // Arrange 53 | var configContent = """ 54 | wifi_channel = 6 55 | wifi_region = 'US' 56 | 57 | [gs_mavlink] 58 | peer = '192.168.0.2' 59 | 60 | [gs_video] 61 | peer = '192.168.0.3' 62 | """; 63 | _wifiConfigParser.ParseConfigString(configContent); 64 | 65 | // Update properties 66 | _wifiConfigParser.WifiChannel = 11; 67 | _wifiConfigParser.WifiRegion = "EU"; 68 | _wifiConfigParser.GsMavlinkPeer = "192.168.1.1"; 69 | _wifiConfigParser.GsVideoPeer = "192.168.1.2"; 70 | 71 | // Act 72 | var updatedConfig = _wifiConfigParser.GetUpdatedConfigString(); 73 | 74 | // Assert 75 | Assert.That(updatedConfig, Does.Contain("wifi_channel = 11")); 76 | Assert.That(updatedConfig, Does.Contain("wifi_region = 'EU'")); 77 | Assert.That(updatedConfig, Does.Contain("peer = '192.168.1.1'")); 78 | Assert.That(updatedConfig, Does.Contain("peer = '192.168.1.2'")); 79 | } 80 | 81 | 82 | [Test] 83 | public void ParseConfigString_InvalidLine_IgnoresLine() 84 | { 85 | // Arrange 86 | var configContent = """ 87 | wifi_channel = 6 88 | invalid_line_without_equals 89 | wifi_region = 'US' 90 | """; 91 | 92 | // Act 93 | _wifiConfigParser.ParseConfigString(configContent); 94 | 95 | // Assert 96 | Assert.That(_wifiConfigParser.WifiChannel, Is.EqualTo(6)); 97 | Assert.That(_wifiConfigParser.WifiRegion, Is.EqualTo("US")); 98 | } 99 | } --------------------------------------------------------------------------------