├── .dockerignore ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CommonBin └── Tak.Client.deps.json ├── Directory.Packages.props ├── LICENSE ├── Meshtastic.Cli ├── .vscode │ └── settings.json ├── Binders │ ├── ChannelBinder.cs │ ├── CommandContextBinder.cs │ └── DeviceConnectionBinder.cs ├── CommandContext.cs ├── CommandHandlers │ ├── CaptureCommandHandler.cs │ ├── ChannelCommandHandler.cs │ ├── DeviceCommandHandler.cs │ ├── DfuCommandHandler.cs │ ├── ExportCommandHandler.cs │ ├── FactoryResetCommandHandler.cs │ ├── FileCommandHandler.cs │ ├── FixedPositionCommandHandler.cs │ ├── GetCannedMessagesCommandHandler.cs │ ├── GetCommandHandler.cs │ ├── ImportCommandHandler.cs │ ├── InfoCommandHandler.cs │ ├── ListCommandHandler.cs │ ├── LiveCommandHandler.cs │ ├── MetadataCommandHandler.cs │ ├── MonitorCommandHandler.cs │ ├── MqttProxyCommandHandler.cs │ ├── RebootCommandHandler.cs │ ├── RegisterCommandHandler.cs │ ├── RemoveNodeCommandHandler.cs │ ├── RequestTelemetryCommandHandler.cs │ ├── ResetNodeDbCommandHandler.cs │ ├── SendTextCommandHandler.cs │ ├── SendWaypointCommandHandler.cs │ ├── SetCannedMessagesCommandHandler.cs │ ├── SetCommandHandler.cs │ ├── TraceRouteCommandHandler.cs │ ├── UpdateCommandHandler.cs │ └── UrlCommandHandler.cs ├── Commands │ ├── CannedMessagesCommand.cs │ ├── CaptureCommand.cs │ ├── ChannelCommand.cs │ ├── DfuCommand.cs │ ├── ExportCommand.cs │ ├── FactoryResetCommand.cs │ ├── FileCommand.cs │ ├── FixedPositionCommand.cs │ ├── GetCommand.cs │ ├── ImportCommand.cs │ ├── InfoCommand.cs │ ├── ListCommand.cs │ ├── LiveCommand.cs │ ├── MetadataCommand.cs │ ├── MonitorCommand.cs │ ├── MqttProxyCommand.cs │ ├── RebootCommand.cs │ ├── RegisterCommand.cs │ ├── RemoveNodeCommand.cs │ ├── RequestTelemetryCommand.cs │ ├── ResetNodeDbCommand.cs │ ├── SendTextCommand.cs │ ├── SendWaypointCommand.cs │ ├── SetCommand.cs │ ├── TraceRouteCommand.cs │ ├── UpdateCommand.cs │ └── UrlCommand.cs ├── DeviceConnectionContext.cs ├── Display │ └── ProtobufPrinter.cs ├── Enums │ ├── ChannelOperation.cs │ ├── ExportOption.cs │ ├── GetSetOperation.cs │ └── OutputFormat.cs ├── GlobalSuppressions.cs ├── Logging │ ├── LoggingExtensions.cs │ ├── PrettyConsoleLogger.cs │ ├── PrettyConsoleLoggerConfiguration.cs │ └── PrettyConsoleLoggerProvider.cs ├── Mappings │ └── HardwareModelMappings.cs ├── Meshtastic.Cli.csproj ├── Meshtastic.Cli.sln ├── Parsers │ ├── Parser.cs │ ├── SettingParser.cs │ └── UrlParser.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Serialization │ └── FilteredTypeInspector.cs ├── StyleResources.cs ├── Utilities │ ├── FirmwarePackageService.cs │ └── ReleaseZipService.cs └── appSettings.json ├── Meshtastic.Test ├── CommandHandlers │ ├── CommandHandlerTestBase.cs │ └── CommandHandlerTests.cs ├── Commands │ ├── CannedMessagesCommandTests.cs │ ├── ChannelCommandTests.cs │ ├── CommandTestBase.cs │ ├── FactoryResetCommandTests.cs │ ├── FakeConsole.cs │ ├── FakeStdIOWriter.cs │ ├── FileCommandTests.cs │ ├── FixedPositionCommandTests.cs │ ├── GetCommandTests.cs │ ├── InfoCommandTests.cs │ ├── ListCommandTests.cs │ ├── MetadataCommandTests.cs │ ├── RebootCommandTests.cs │ ├── ResetNodeDbCommandTests.cs │ ├── SendTextCommandTests.cs │ ├── SendWaypointCommandTests.cs │ ├── SetCommandTests.cs │ ├── TraceRouteCommandTests.cs │ └── UrlCommandTests.cs ├── Crypto │ └── PacketCryptoTests.cs ├── Data │ ├── AdminMessageFactoryTests.cs │ ├── DeviceStateContainerTests.cs │ ├── FakeLogger.cs │ ├── FromDeviceMessageTests.cs │ ├── PositionMessageFactoryTests.cs │ ├── TextMessageFactoryTests.cs │ ├── ToRadioFactoryTests.cs │ ├── TraceRouteMessageFactoryTests.cs │ └── WaypointMessageFactoryTests.cs ├── Extensions │ ├── DateTimeExtensionsTests.cs │ ├── DisplayExtensionsTests.cs │ ├── FromRadioExtensionsTests.cs │ └── ReflectionExtensionsTests.cs ├── Meshtastic.Test.csproj ├── Parsers │ ├── SettingParserTests.cs │ └── UrlParserTests.cs ├── Reflection │ └── ReflectionExtensionsTests.cs ├── TestCategories.cs ├── Usings.cs ├── Utilities │ ├── FirmwarePackageServiceTests.cs │ └── ReleaseZipServiceTests.cs └── coverlet.runsettings ├── Meshtastic.Virtual.Service ├── Dockerfile ├── Meshtastic.Virtual.Service.csproj ├── Network │ └── InterfaceUtility.cs ├── Persistance │ ├── FilePaths.cs │ ├── FilePersistance.cs │ ├── IFilePersistance.cs │ ├── IVirtualStore.cs │ └── VirtualStore.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── VirtualWorker.cs ├── appsettings.Development.json ├── appsettings.json ├── config.proto ├── module.store └── node.proto ├── Meshtastic.sln ├── Meshtastic ├── Connections │ ├── DeviceConnection.cs │ ├── PacketFraming.cs │ ├── SerialConnection.cs │ ├── SimulatedConnection.cs │ └── TcpConnection.cs ├── Crypto │ ├── NonceGenerator.cs │ └── PacketEncryption.cs ├── Data │ ├── DeviceStateContainer.cs │ ├── FromRadioMessage.cs │ └── MessageFactories │ │ ├── AdminMessageFactory.cs │ │ ├── PositionMessageFactory.cs │ │ ├── TelemetryMessageFactory.cs │ │ ├── TextMessageFactory.cs │ │ ├── ToRadioMessageFactory.cs │ │ ├── TraceRouteMessageFactory.cs │ │ └── WaypointMessageFactory.cs ├── Extensions │ ├── DateTimeExtensions.cs │ ├── DisplayExtensions.cs │ ├── FromRadioExtensions.cs │ ├── ReflectionExtensions.cs │ └── ToRadioExtensions.cs ├── Generated │ ├── Admin.cs │ ├── Apponly.cs │ ├── Atak.cs │ ├── Cannedmessages.cs │ ├── Channel.cs │ ├── Clientonly.cs │ ├── Config.cs │ ├── ConnectionStatus.cs │ ├── DeviceUi.cs │ ├── Interdevice.cs │ ├── Localonly.cs │ ├── Mesh.cs │ ├── ModuleConfig.cs │ ├── Mqtt.cs │ ├── Paxcount.cs │ ├── Portnums.cs │ ├── Powermon.cs │ ├── RemoteHardware.cs │ ├── Rtttl.cs │ ├── Storeforward.cs │ ├── Telemetry.cs │ └── Xmodem.cs ├── Meshtastic.csproj ├── Properties │ └── launchSettings.json └── Resources.cs ├── README.md ├── logo.png ├── scripts ├── buildinfo.py ├── bump_version.py ├── readprops.py ├── regen-protos.bat └── regen-protos.sh └── version.properties /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: thebentern 4 | open_collective: meshtastic 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "protobufs"] 2 | path = protobufs 3 | url = https://github.com/meshtastic/protobufs 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | // Use IntelliSense to find out which attributes exist for C# debugging 6 | // Use hover for the description of the existing attributes 7 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/bin/Debug/net7.0/c-sharp.dll", 14 | "args": [],//["info", "--port", "/dev/cu.wchusbserial54350265191"], 15 | "cwd": "${workspaceFolder}", 16 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 17 | "console": "internalConsole", 18 | "stopAtEntry": false 19 | }, 20 | { 21 | "name": ".NET Core Attach", 22 | "type": "coreclr", 23 | "request": "attach" 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dotnet.defaultSolution": "Meshtastic.sln" 3 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/c-sharp.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/c-sharp.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "--project", 36 | "${workspaceFolder}/c-sharp.csproj" 37 | ], 38 | "problemMatcher": "$msCompile" 39 | }, 40 | { 41 | "label": "regen protos", 42 | "command": "protoc", 43 | "type": "process", 44 | "args": [ 45 | "--proto_path=protobufs", 46 | "--csharp_out=./generated", 47 | "--csharp_opt=base_namespace=Meshtastic.Protobufs", 48 | "protobufs/*.proto" 49 | ], 50 | }, 51 | { 52 | "label": "run --noproto", 53 | "command": "dotnet", 54 | "type": "process", 55 | "args": [ 56 | "run", 57 | "--project", 58 | "${workspaceFolder}/c-sharp.csproj", 59 | "--noproto" 60 | ], 61 | "problemMatcher": "$msCompile" 62 | }, 63 | ] 64 | } -------------------------------------------------------------------------------- /CommonBin/Tak.Client.deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "runtimeTarget": { 3 | "name": ".NETCoreApp,Version=v7.0", 4 | "signature": "" 5 | }, 6 | "compilationOptions": {}, 7 | "targets": { 8 | ".NETCoreApp,Version=v7.0": { 9 | "Tak.Client/1.0.0": { 10 | "dependencies": { 11 | "dpp.cot": "1.0.4" 12 | }, 13 | "runtime": { 14 | "Tak.Client.dll": {} 15 | } 16 | }, 17 | "dpp.cot/1.0.4": { 18 | "dependencies": { 19 | "protobuf-net": "3.0.101" 20 | }, 21 | "runtime": { 22 | "lib/net5.0/dpp.cot.dll": { 23 | "assemblyVersion": "1.0.4.0", 24 | "fileVersion": "1.0.4.0" 25 | } 26 | } 27 | }, 28 | "protobuf-net/3.0.101": { 29 | "dependencies": { 30 | "protobuf-net.Core": "3.0.101" 31 | }, 32 | "runtime": { 33 | "lib/net5.0/protobuf-net.dll": { 34 | "assemblyVersion": "3.0.0.0", 35 | "fileVersion": "3.0.101.59408" 36 | } 37 | } 38 | }, 39 | "protobuf-net.Core/3.0.101": { 40 | "runtime": { 41 | "lib/net5.0/protobuf-net.Core.dll": { 42 | "assemblyVersion": "3.0.0.0", 43 | "fileVersion": "3.0.101.59408" 44 | } 45 | } 46 | } 47 | } 48 | }, 49 | "libraries": { 50 | "Tak.Client/1.0.0": { 51 | "type": "project", 52 | "serviceable": false, 53 | "sha512": "" 54 | }, 55 | "dpp.cot/1.0.4": { 56 | "type": "package", 57 | "serviceable": true, 58 | "sha512": "sha512-HKC33hI9QPEt82gDTmx6b2KOHwQsvtIZySbxrupWKJfQmmqoNhEJfL3ZZqqMsik/X+CEZIKq1sAKd6MftF6Xvw==", 59 | "path": "dpp.cot/1.0.4", 60 | "hashPath": "dpp.cot.1.0.4.nupkg.sha512" 61 | }, 62 | "protobuf-net/3.0.101": { 63 | "type": "package", 64 | "serviceable": true, 65 | "sha512": "sha512-mLm8VGfGylp9q+rbJDp3uw0BOeNzfGENecVz/EYGlzq3ss7tfajGXHCMoZm/GxJ9vSn9pcK22UXWZnQ5GJQMjA==", 66 | "path": "protobuf-net/3.0.101", 67 | "hashPath": "protobuf-net.3.0.101.nupkg.sha512" 68 | }, 69 | "protobuf-net.Core/3.0.101": { 70 | "type": "package", 71 | "serviceable": true, 72 | "sha512": "sha512-gf9QIF0RlctfjmOjbE6eOyMulkNEsH7tHncjKj/4BTu9tkuTyrH3fv5I+35GnolJqKJ6fh9kYaezvNRL3t75+Q==", 73 | "path": "protobuf-net.core/3.0.101", 74 | "hashPath": "protobuf-net.core.3.0.101.nupkg.sha512" 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Meshtastic.Cli/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dotnet.defaultSolution": "Meshtastic.Cli.sln" 3 | } -------------------------------------------------------------------------------- /Meshtastic.Cli/Binders/ChannelBinder.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Enums; 2 | using Meshtastic.Protobufs; 3 | using System.CommandLine.Binding; 4 | using System.Diagnostics.CodeAnalysis; 5 | 6 | namespace Meshtastic.Cli.Binders; 7 | 8 | public record ChannelOperationSettings(ChannelOperation Operation, int? Index, string? Name, Channel.Types.Role? Role, string? PSK, bool? UplinkEnabled, bool? DownlinkEnabled); 9 | 10 | [ExcludeFromCodeCoverage(Justification = "Container object")] 11 | public class ChannelBinder : BinderBase 12 | { 13 | private readonly Argument operation; 14 | private readonly Option indexOption; 15 | private readonly Option nameOption; 16 | private readonly Option roleOption; 17 | private readonly Option pskOption; 18 | private readonly Option uplinkOption; 19 | private readonly Option downlinkOption; 20 | 21 | public ChannelBinder(Argument operation, 22 | Option indexOption, 23 | Option nameOption, 24 | Option roleOption, 25 | Option pskOption, 26 | Option uplinkOption, 27 | Option downlinkOption) 28 | { 29 | this.operation = operation; 30 | this.indexOption = indexOption; 31 | this.nameOption = nameOption; 32 | this.roleOption = roleOption; 33 | this.pskOption = pskOption; 34 | this.uplinkOption = uplinkOption; 35 | this.downlinkOption = downlinkOption; 36 | } 37 | 38 | protected override ChannelOperationSettings GetBoundValue(BindingContext bindingContext) => 39 | new(bindingContext.ParseResult.GetValueForArgument(operation), 40 | bindingContext.ParseResult?.GetValueForOption(indexOption), 41 | bindingContext.ParseResult?.GetValueForOption(nameOption), 42 | bindingContext.ParseResult?.GetValueForOption(roleOption), 43 | bindingContext.ParseResult?.GetValueForOption(pskOption), 44 | bindingContext.ParseResult?.GetValueForOption(uplinkOption), 45 | bindingContext.ParseResult?.GetValueForOption(downlinkOption)) 46 | { 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Binders/CommandContextBinder.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Enums; 2 | using Meshtastic.Cli.Logging; 3 | using Microsoft.Extensions.Logging; 4 | using System.CommandLine.Binding; 5 | 6 | namespace Meshtastic.Cli.Binders; 7 | 8 | public class CommandContextBinder : BinderBase 9 | { 10 | private readonly Option logLevel; 11 | private readonly Option outputFormat; 12 | private readonly Option destination; 13 | private readonly Option selectDest; 14 | private readonly Option? channel; 15 | 16 | public CommandContextBinder(Option logLevel, 17 | Option outputFormat, 18 | Option destination, 19 | Option selectDest, 20 | Option? channel = null) 21 | { 22 | this.logLevel = logLevel; 23 | this.outputFormat = outputFormat; 24 | this.destination = destination; 25 | this.selectDest = selectDest; 26 | this.channel = channel; 27 | } 28 | 29 | protected override CommandContext GetBoundValue(BindingContext bindingContext) 30 | { 31 | return new CommandContext(GetLogger(bindingContext), 32 | bindingContext.ParseResult?.GetValueForOption(outputFormat) ?? OutputFormat.PrettyConsole, 33 | bindingContext.ParseResult?.GetValueForOption(destination), 34 | bindingContext.ParseResult?.GetValueForOption(selectDest) ?? false, 35 | channel != null ? bindingContext.ParseResult?.GetValueForOption(channel) : null); 36 | } 37 | 38 | public ILogger GetLogger(BindingContext bindingContext) 39 | { 40 | var level = bindingContext.ParseResult?.GetValueForOption(logLevel) ?? LogLevel.Information; 41 | var output = bindingContext.ParseResult?.GetValueForOption(outputFormat) ?? OutputFormat.PrettyConsole; 42 | var loggerFactory = LoggerFactory.Create(builder => 43 | { 44 | builder.AddPrettyConsole(new PrettyConsoleLoggerConfiguration() 45 | { 46 | // Don't allow non-console output formats to set chatty loglevels that will corrupt clean ouput 47 | LogLevel = output == OutputFormat.PrettyConsole ? level : LogLevel.Error, 48 | }); 49 | builder.SetMinimumLevel(level); 50 | }); 51 | return loggerFactory.CreateLogger(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Binders/DeviceConnectionBinder.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine.Binding; 2 | 3 | namespace Meshtastic.Cli.Binders; 4 | 5 | public class DeviceConnectionBinder : BinderBase 6 | { 7 | private readonly Option portOption; 8 | private readonly Option hostOption; 9 | 10 | public DeviceConnectionBinder(Option portOption, Option hostOption) 11 | { 12 | this.portOption = portOption; 13 | this.hostOption = hostOption; 14 | } 15 | 16 | protected override DeviceConnectionContext GetBoundValue(BindingContext bindingContext) => 17 | new(bindingContext.ParseResult?.GetValueForOption(portOption), 18 | bindingContext.ParseResult?.GetValueForOption(hostOption)); 19 | } 20 | -------------------------------------------------------------------------------- /Meshtastic.Cli/CommandContext.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Enums; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace Meshtastic.Cli; 5 | 6 | public class CommandContext 7 | { 8 | public CommandContext(ILogger logger, 9 | OutputFormat outputFormat, 10 | uint? destination = null, 11 | bool selectDestination = false, 12 | uint? channel = null) 13 | { 14 | Logger = logger; 15 | OutputFormat = outputFormat; 16 | Destination = destination; 17 | SelectDestination = selectDestination; 18 | Channel = channel; 19 | } 20 | 21 | public ILogger Logger { get; } 22 | public OutputFormat OutputFormat { get; } 23 | public uint? Destination { get; } 24 | public bool SelectDestination { get; } 25 | public uint? Channel { get; } 26 | } -------------------------------------------------------------------------------- /Meshtastic.Cli/CommandHandlers/CaptureCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Data; 2 | using Meshtastic.Data.MessageFactories; 3 | using Meshtastic.Display; 4 | using Meshtastic.Protobufs; 5 | using Meshtastic.Extensions; 6 | 7 | namespace Meshtastic.Cli.CommandHandlers; 8 | 9 | public class CaptureCommandHandler : DeviceCommandHandler 10 | { 11 | public CaptureCommandHandler(DeviceConnectionContext context, CommandContext commandContext) : base(context, commandContext) { } 12 | 13 | public async Task Handle() 14 | { 15 | var wantConfig = new ToRadioMessageFactory().CreateWantConfigMessage(); 16 | var container = await Connection.WriteToRadio(wantConfig, CompleteOnConfigReceived); 17 | Connection.Disconnect(); 18 | return container; 19 | } 20 | 21 | public override async Task OnCompleted(FromRadio packet, DeviceStateContainer container) 22 | { 23 | await Connection.ReadFromRadio((fromRadio, container) => 24 | { 25 | if (fromRadio!.PayloadVariantCase == FromRadio.PayloadVariantOneofCase.Packet) { 26 | var fromRadioDecoded = new FromRadioDecoded(fromRadio) 27 | { 28 | PortNum = fromRadio.Packet?.Decoded?.Portnum, 29 | PayloadSize = fromRadio.Packet?.Decoded?.Payload.Length ?? 0, 30 | ReceivedAt = DateTime.Now, 31 | }; 32 | if (fromRadio.GetPayload() != null) 33 | fromRadioDecoded.DecodedPayload = fromRadio.GetPayload(); 34 | else if (fromRadio.GetPayload() != null) 35 | fromRadioDecoded.DecodedPayload = fromRadio.GetPayload(); 36 | else if (fromRadio.GetPayload() != null) 37 | fromRadioDecoded.DecodedPayload = fromRadio.GetPayload(); 38 | else if (fromRadio.GetPayload() != null) 39 | fromRadioDecoded.DecodedPayload = fromRadio.GetPayload(); 40 | else if (fromRadio.GetPayload() != null) 41 | fromRadioDecoded.DecodedPayload = fromRadio.GetPayload(); 42 | else if (fromRadio.GetPayload() != null) 43 | fromRadioDecoded.DecodedPayload = fromRadio.GetPayload(); 44 | else if (fromRadio.GetPayload() != null) 45 | fromRadioDecoded.DecodedPayload = fromRadio.GetPayload(); 46 | else if (fromRadio.GetPayload() != null) 47 | fromRadioDecoded.DecodedPayload = fromRadio.GetPayload(); 48 | else if (fromRadio.GetPayload() != null) 49 | fromRadioDecoded.DecodedPayload = fromRadio.GetPayload(); 50 | } 51 | 52 | return Task.FromResult(false); 53 | }); 54 | } 55 | } 56 | 57 | public class FromRadioDecoded 58 | { 59 | public FromRadioDecoded(FromRadio fromRadio) 60 | { 61 | Packet = fromRadio; 62 | } 63 | 64 | public FromRadio Packet { get; set; } 65 | public object? DecodedPayload { get; set; } 66 | public PortNum? PortNum { get; set; } 67 | public int PayloadSize { get; set; } 68 | public int TotalSize { get; set; } 69 | public DateTime ReceivedAt { get; set; } 70 | } -------------------------------------------------------------------------------- /Meshtastic.Cli/CommandHandlers/DeviceCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Enums; 2 | using Meshtastic.Cli.Parsers; 3 | using Meshtastic.Connections; 4 | using Meshtastic.Data; 5 | using Meshtastic.Data.MessageFactories; 6 | using Meshtastic.Extensions; 7 | using Meshtastic.Protobufs; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace Meshtastic.Cli.CommandHandlers; 11 | 12 | public class DeviceCommandHandler 13 | { 14 | protected readonly DeviceConnectionContext ConnectionContext; 15 | protected readonly DeviceConnection Connection; 16 | protected readonly ToRadioMessageFactory ToRadioMessageFactory; 17 | protected readonly OutputFormat OutputFormat; 18 | protected readonly ILogger Logger; 19 | protected uint? Destination; 20 | protected readonly bool SelectDestination; 21 | 22 | public DeviceCommandHandler(DeviceConnectionContext connectionContext, CommandContext commandContext) 23 | { 24 | ConnectionContext = connectionContext; 25 | Connection = connectionContext.GetDeviceConnection(commandContext.Logger); 26 | ToRadioMessageFactory = new(); 27 | OutputFormat = commandContext.OutputFormat; 28 | Logger = commandContext.Logger; 29 | Destination = commandContext.Destination; 30 | SelectDestination = commandContext.SelectDestination; 31 | } 32 | 33 | protected static (SettingParserResult? result, bool isValid) ParseSettingOptions(IEnumerable settings, bool isGetOnly) 34 | { 35 | var parser = new SettingParser(settings); 36 | var parserResult = parser.ParseSettings(isGetOnly); 37 | 38 | if (parserResult.ValidationIssues.Any()) 39 | { 40 | foreach (var issue in parserResult.ValidationIssues) 41 | { 42 | AnsiConsole.MarkupLine($"[red]{issue}[/]"); 43 | } 44 | return (null, false); 45 | } 46 | 47 | return (parserResult, true); 48 | } 49 | 50 | public static async Task AnyResponseReceived(FromRadio fromRadio, DeviceStateContainer container) 51 | { 52 | await Task.Delay(100); 53 | return true; 54 | } 55 | 56 | public async Task CompleteOnConfigReceived(FromRadio fromRadio, DeviceStateContainer container) 57 | { 58 | if (fromRadio.PayloadVariantCase != FromRadio.PayloadVariantOneofCase.ConfigCompleteId) 59 | return false; 60 | 61 | if (SelectDestination) 62 | { 63 | var selection = new SelectionPrompt() 64 | .Title("Please select a destination node") 65 | .AddChoices(container.Nodes.Select(n => n.Num)); 66 | selection.Converter = (num) => container.GetNodeDisplayName(num); 67 | Destination = AnsiConsole.Prompt(selection); 68 | } 69 | await Task.Delay(500); 70 | await OnCompleted(fromRadio, container); 71 | return true; 72 | } 73 | 74 | public virtual async Task OnCompleted(FromRadio packet, DeviceStateContainer container) 75 | { 76 | await Task.CompletedTask; 77 | } 78 | 79 | protected async Task BeginEditSettings(AdminMessageFactory adminMessageFactory) 80 | { 81 | var message = adminMessageFactory.CreateBeginEditSettingsMessage(); 82 | await Connection.WriteToRadio(ToRadioMessageFactory.CreateMeshPacketMessage(message), 83 | AnyResponseReceived); 84 | Logger.LogInformation($"Starting edit transaction for settings..."); 85 | } 86 | 87 | protected async Task CommitEditSettings(AdminMessageFactory adminMessageFactory) 88 | { 89 | var message = adminMessageFactory.CreateCommitEditSettingsMessage(); 90 | await Connection.WriteToRadio(ToRadioMessageFactory.CreateMeshPacketMessage(message), 91 | AnyResponseReceived); 92 | Logger.LogInformation($"Commit edit transaction for settings..."); 93 | } 94 | } -------------------------------------------------------------------------------- /Meshtastic.Cli/CommandHandlers/DfuCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Data; 2 | using Meshtastic.Data.MessageFactories; 3 | using Meshtastic.Protobufs; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.CommandHandlers; 7 | 8 | public class DfuCommandHandler : DeviceCommandHandler 9 | { 10 | public DfuCommandHandler(DeviceConnectionContext context, 11 | CommandContext commandContext) : base(context, commandContext) 12 | { 13 | } 14 | public async Task Handle() 15 | { 16 | var wantConfig = new ToRadioMessageFactory().CreateWantConfigMessage(); 17 | var container = await Connection.WriteToRadio(wantConfig, CompleteOnConfigReceived); 18 | Connection.Disconnect(); 19 | return container; 20 | } 21 | 22 | public override async Task OnCompleted(FromRadio packet, DeviceStateContainer container) 23 | { 24 | Logger.LogInformation($"Attempting to enter DFU mode (only works on NRF52 devices)..."); 25 | var adminMessageFactory = new AdminMessageFactory(container, Destination); 26 | var adminMessage = adminMessageFactory.CreateEnterDfuMessage(); 27 | await Connection.WriteToRadio(ToRadioMessageFactory.CreateMeshPacketMessage(adminMessage), AnyResponseReceived); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Meshtastic.Cli/CommandHandlers/FactoryResetCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Data; 2 | using Meshtastic.Data.MessageFactories; 3 | using Meshtastic.Protobufs; 4 | using Microsoft.Extensions.Logging; 5 | using System.Diagnostics.CodeAnalysis; 6 | 7 | namespace Meshtastic.Cli.CommandHandlers; 8 | 9 | [ExcludeFromCodeCoverage(Justification = "Destructive")] 10 | public class FactoryResetCommandHandler : DeviceCommandHandler 11 | { 12 | public FactoryResetCommandHandler(DeviceConnectionContext context, 13 | CommandContext commandContext) : base(context, commandContext) { } 14 | 15 | public async Task Handle() 16 | { 17 | var wantConfig = new ToRadioMessageFactory().CreateWantConfigMessage(); 18 | var container = await Connection.WriteToRadio(wantConfig, CompleteOnConfigReceived); 19 | Connection.Disconnect(); 20 | return container; 21 | } 22 | 23 | public override async Task OnCompleted(FromRadio packet, DeviceStateContainer container) 24 | { 25 | Logger.LogInformation("Factory reseting device..."); 26 | var adminMessageFactory = new AdminMessageFactory(container, Destination); 27 | var adminMessage = adminMessageFactory.CreateFactoryResetMessage(); 28 | await Connection.WriteToRadio(ToRadioMessageFactory.CreateMeshPacketMessage(adminMessage), AnyResponseReceived); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Meshtastic.Cli/CommandHandlers/FileCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Google.Protobuf; 2 | using Meshtastic.Data; 3 | using Meshtastic.Data.MessageFactories; 4 | using Meshtastic.Extensions; 5 | using Meshtastic.Protobufs; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace Meshtastic.Cli.CommandHandlers; 9 | 10 | public class FileCommandHandler(string path, DeviceConnectionContext context, CommandContext commandContext) : DeviceCommandHandler(context, commandContext) 11 | { 12 | private readonly string path = path; 13 | private readonly MemoryStream memoryStream = new(); 14 | 15 | public async Task Handle() 16 | { 17 | var wantConfig = ToRadioMessageFactory.CreateWantConfigMessage(); 18 | var container = await Connection.WriteToRadio(wantConfig, CompleteOnConfigReceived); 19 | Connection.Disconnect(); 20 | return container; 21 | } 22 | 23 | public override async Task OnCompleted(FromRadio _, DeviceStateContainer container) 24 | { 25 | var fileRequest = ToRadioMessageFactory.CreateXmodemPacketMessage(); 26 | fileRequest.XmodemPacket.Buffer = ByteString.CopyFromUtf8(path); 27 | await Connection.WriteToRadio(fileRequest, async (fromRadio, container) => 28 | { 29 | var xmodem = fromRadio.GetPayload(); 30 | if (xmodem == null) 31 | return false; 32 | 33 | var bufferArray = xmodem.Buffer.ToArray(); 34 | await memoryStream.WriteAsync(bufferArray); 35 | 36 | if (xmodem.Control == XModem.Types.Control.Soh) 37 | { 38 | await Task.Delay(100); 39 | var ack = ToRadioMessageFactory.CreateXmodemPacketMessage(XModem.Types.Control.Ack); 40 | await Connection.WriteToRadio(ack); 41 | } 42 | else if (xmodem?.Control == XModem.Types.Control.Eot) 43 | { 44 | var fileName = Path.GetFileName(this.path); 45 | Logger.LogInformation("Retrieved file contents"); 46 | Logger.LogInformation($"Writing to {fileName}"); 47 | 48 | File.WriteAllBytes(Path.Combine(Environment.CurrentDirectory, fileName), memoryStream.ToArray()); 49 | return true; 50 | } 51 | return false; 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Meshtastic.Cli/CommandHandlers/FixedPositionCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Data; 2 | using Meshtastic.Data.MessageFactories; 3 | using Meshtastic.Extensions; 4 | using Meshtastic.Protobufs; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace Meshtastic.Cli.CommandHandlers; 8 | 9 | public class FixedPositionCommandHandler(decimal latitude, 10 | decimal longitude, 11 | int altitude, 12 | bool clear, 13 | DeviceConnectionContext context, 14 | CommandContext commandContext) : DeviceCommandHandler(context, commandContext) 15 | { 16 | private readonly decimal latitude = latitude; 17 | private readonly decimal longitude = longitude; 18 | private readonly int altitude = altitude; 19 | private readonly bool clear = clear; 20 | private readonly decimal divisor = new(1e-7); 21 | 22 | public async Task Handle() 23 | { 24 | var wantConfig = new ToRadioMessageFactory().CreateWantConfigMessage(); 25 | var container = await Connection.WriteToRadio(wantConfig, CompleteOnConfigReceived); 26 | Connection.Disconnect(); 27 | return container; 28 | } 29 | 30 | public override async Task OnCompleted(FromRadio packet, DeviceStateContainer container) 31 | { 32 | var adminMessageFactory = new AdminMessageFactory(container, Destination); 33 | 34 | var position = new Position() 35 | { 36 | LatitudeI = latitude != 0 ? decimal.ToInt32(latitude / divisor) : 0, 37 | LongitudeI = longitude != 0 ? decimal.ToInt32(longitude / divisor) : 0, 38 | Altitude = altitude, 39 | Time = DateTime.Now.GetUnixTimestamp(), 40 | Timestamp = DateTime.Now.GetUnixTimestamp(), 41 | }; 42 | 43 | var adminMessage = clear ? adminMessageFactory.RemovedFixedPositionMessage() : adminMessageFactory.CreateFixedPositionMessage(position); 44 | Logger.LogInformation($"Setting fixed position..."); 45 | 46 | await Connection.WriteToRadio(ToRadioMessageFactory.CreateMeshPacketMessage(adminMessage), 47 | (fromRadio, container) => 48 | { 49 | return Task.FromResult(fromRadio.GetPayload() != null); 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Meshtastic.Cli/CommandHandlers/GetCannedMessagesCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Data; 2 | using Meshtastic.Data.MessageFactories; 3 | using Meshtastic.Extensions; 4 | using Meshtastic.Protobufs; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace Meshtastic.Cli.CommandHandlers; 8 | 9 | public class GetCannedMessagesCommandHandler : DeviceCommandHandler 10 | { 11 | public GetCannedMessagesCommandHandler(DeviceConnectionContext context, CommandContext commandContext) : 12 | base(context, commandContext) 13 | { } 14 | 15 | public async Task Handle() 16 | { 17 | var wantConfig = new ToRadioMessageFactory().CreateWantConfigMessage(); 18 | var container = await Connection.WriteToRadio(wantConfig, CompleteOnConfigReceived); 19 | Connection.Disconnect(); 20 | return container; 21 | } 22 | 23 | public override async Task OnCompleted(FromRadio packet, DeviceStateContainer container) 24 | { 25 | Logger.LogInformation("Getting canned messages from device..."); 26 | var adminMessageFactory = new AdminMessageFactory(container, Destination); 27 | var adminMessage = adminMessageFactory.CreateGetCannedMessage(); 28 | await Connection.WriteToRadio(ToRadioMessageFactory.CreateMeshPacketMessage(adminMessage), 29 | (fromRadio, container) => 30 | { 31 | var adminMessage = fromRadio.GetPayload(); 32 | if (adminMessage?.PayloadVariantCase == AdminMessage.PayloadVariantOneofCase.GetCannedMessageModuleMessagesResponse) 33 | { 34 | Logger.LogInformation($"Canned messages: {adminMessage?.GetCannedMessageModuleMessagesResponse}"); 35 | return Task.FromResult(true); 36 | } 37 | return Task.FromResult(false); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Meshtastic.Cli/CommandHandlers/GetCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Parsers; 2 | using Meshtastic.Data; 3 | using Meshtastic.Data.MessageFactories; 4 | using Meshtastic.Display; 5 | using Meshtastic.Protobufs; 6 | 7 | namespace Meshtastic.Cli.CommandHandlers; 8 | 9 | public class GetCommandHandler : DeviceCommandHandler 10 | { 11 | public readonly IEnumerable? ParsedSettings; 12 | 13 | public GetCommandHandler(IEnumerable settings, 14 | DeviceConnectionContext context, 15 | CommandContext commandContext) : base(context, commandContext) 16 | { 17 | var (result, isValid) = ParseSettingOptions(settings, isGetOnly: true); 18 | if (!isValid) 19 | return; 20 | 21 | ParsedSettings = result!.ParsedSettings; 22 | } 23 | 24 | public async Task Handle() 25 | { 26 | var wantConfig = new ToRadioMessageFactory().CreateWantConfigMessage(); 27 | var container = await Connection.WriteToRadio(wantConfig, CompleteOnConfigReceived); 28 | Connection.Disconnect(); 29 | return container; 30 | } 31 | 32 | public override Task OnCompleted(FromRadio packet, DeviceStateContainer container) 33 | { 34 | var printer = new ProtobufPrinter(container, OutputFormat); 35 | printer.PrintSettings(ParsedSettings!); 36 | return Task.CompletedTask; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Meshtastic.Cli/CommandHandlers/InfoCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Data; 2 | using Meshtastic.Data.MessageFactories; 3 | using Meshtastic.Display; 4 | using Meshtastic.Protobufs; 5 | 6 | namespace Meshtastic.Cli.CommandHandlers; 7 | 8 | public class InfoCommandHandler : DeviceCommandHandler 9 | { 10 | public InfoCommandHandler(DeviceConnectionContext context, CommandContext commandContext) : base(context, commandContext) { } 11 | public async Task Handle() 12 | { 13 | var wantConfig = new ToRadioMessageFactory().CreateWantConfigMessage(); 14 | var container = await Connection.WriteToRadio(wantConfig, CompleteOnConfigReceived); 15 | Connection.Disconnect(); 16 | return container; 17 | } 18 | 19 | public override Task OnCompleted(FromRadio packet, DeviceStateContainer container) 20 | { 21 | var printer = new ProtobufPrinter(container, OutputFormat); 22 | printer.Print(); 23 | return Task.CompletedTask; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Meshtastic.Cli/CommandHandlers/ListCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Connections; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace Meshtastic.Cli.CommandHandlers; 5 | 6 | [ExcludeFromCodeCoverage(Justification = "Requires serial hardware")] 7 | public class ListCommandHandler 8 | { 9 | public static async Task Handle() 10 | { 11 | AnsiConsole.WriteLine("Found the following serial ports:"); 12 | foreach (var port in SerialConnection.ListPorts()) 13 | AnsiConsole.WriteLine(port); 14 | await Task.CompletedTask; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Meshtastic.Cli/CommandHandlers/LiveCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Data; 2 | using Meshtastic.Data.MessageFactories; 3 | using Meshtastic.Display; 4 | using Meshtastic.Protobufs; 5 | 6 | namespace Meshtastic.Cli.CommandHandlers; 7 | 8 | public class LiveCommandHandler : DeviceCommandHandler 9 | { 10 | public LiveCommandHandler(DeviceConnectionContext context, CommandContext commandContext) : base(context, commandContext) { } 11 | 12 | public async Task Handle() 13 | { 14 | var wantConfig = new ToRadioMessageFactory().CreateWantConfigMessage(); 15 | var container = await Connection.WriteToRadio(wantConfig, CompleteOnConfigReceived); 16 | Connection.Disconnect(); 17 | return container; 18 | } 19 | 20 | public override async Task OnCompleted(FromRadio packet, DeviceStateContainer container) 21 | { 22 | var layout = new Layout("Root") 23 | .SplitColumns( 24 | new Layout("Messages"), 25 | new Layout("Right") 26 | .SplitRows( 27 | new Layout("Nodes"), 28 | new Layout("Traffic"))); 29 | 30 | await AnsiConsole.Live(layout) 31 | .StartAsync(async ctx => 32 | { 33 | var printer = new ProtobufPrinter(container, OutputFormat); 34 | UpdateDashboard(layout, printer); 35 | 36 | await Connection.ReadFromRadio((fromRadio, container) => 37 | { 38 | printer = new ProtobufPrinter(container, OutputFormat); 39 | UpdateDashboard(layout, printer); 40 | 41 | ctx.Refresh(); 42 | return Task.FromResult(false); 43 | }); 44 | }); 45 | } 46 | 47 | 48 | private static void UpdateDashboard(Layout layout, ProtobufPrinter printer) 49 | { 50 | layout["Nodes"].Update(printer.PrintNodesPanel()); 51 | layout["Traffic"].Update(printer.PrintTrafficCharts()); 52 | layout["Messages"].Update(printer.PrintMessagesPanel()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Meshtastic.Cli/CommandHandlers/MetadataCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Data; 2 | using Meshtastic.Data.MessageFactories; 3 | using Meshtastic.Display; 4 | using Meshtastic.Extensions; 5 | using Meshtastic.Protobufs; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace Meshtastic.Cli.CommandHandlers; 9 | 10 | public class MetadataCommandHandler : DeviceCommandHandler 11 | { 12 | public MetadataCommandHandler(DeviceConnectionContext context, CommandContext commandContext) : base(context, commandContext) { } 13 | 14 | public async Task Handle() 15 | { 16 | var wantConfig = new ToRadioMessageFactory().CreateWantConfigMessage(); 17 | var container = await Connection.WriteToRadio(wantConfig, CompleteOnConfigReceived); 18 | Connection.Disconnect(); 19 | return container; 20 | } 21 | 22 | public override async Task OnCompleted(FromRadio packet, DeviceStateContainer container) 23 | { 24 | Logger.LogInformation("Getting device metadata..."); 25 | var adminMessageFactory = new AdminMessageFactory(container, Destination); 26 | var adminMessage = adminMessageFactory.CreateGetMetadataMessage(); 27 | await Connection.WriteToRadio(ToRadioMessageFactory.CreateMeshPacketMessage(adminMessage), 28 | (fromRadio, container) => 29 | { 30 | var adminMessage = fromRadio.GetPayload(); 31 | if (adminMessage?.PayloadVariantCase == AdminMessage.PayloadVariantOneofCase.GetDeviceMetadataResponse) 32 | { 33 | var printer = new ProtobufPrinter(container, OutputFormat); 34 | printer.PrintMetadata(adminMessage!.GetDeviceMetadataResponse); 35 | return Task.FromResult(true); 36 | } 37 | return Task.FromResult(false); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Meshtastic.Cli/CommandHandlers/MonitorCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace Meshtastic.Cli.CommandHandlers; 4 | 5 | [ExcludeFromCodeCoverage(Justification = "Requires serial hardware")] 6 | public class MonitorCommandHandler : DeviceCommandHandler 7 | { 8 | public MonitorCommandHandler(DeviceConnectionContext context, 9 | CommandContext commandContext) : base(context, commandContext) { } 10 | public async Task Handle() 11 | { 12 | await Connection.Monitor(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Meshtastic.Cli/CommandHandlers/RebootCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Data; 2 | using Meshtastic.Data.MessageFactories; 3 | using Meshtastic.Protobufs; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.CommandHandlers; 7 | 8 | public class RebootCommandHandler : DeviceCommandHandler 9 | { 10 | private readonly bool isOtaMode = false; 11 | private readonly int seconds = 5; 12 | 13 | public RebootCommandHandler(bool isOtaMode, 14 | int seconds, 15 | DeviceConnectionContext context, 16 | CommandContext commandContext) : base(context, commandContext) 17 | { 18 | this.isOtaMode = isOtaMode; 19 | this.seconds = seconds; 20 | } 21 | public async Task Handle() 22 | { 23 | var wantConfig = new ToRadioMessageFactory().CreateWantConfigMessage(); 24 | var container = await Connection.WriteToRadio(wantConfig, CompleteOnConfigReceived); 25 | Connection.Disconnect(); 26 | return container; 27 | } 28 | 29 | public override async Task OnCompleted(FromRadio packet, DeviceStateContainer container) 30 | { 31 | Logger.LogInformation($"Rebooting in {seconds} seconds..."); 32 | var adminMessageFactory = new AdminMessageFactory(container, Destination); 33 | var adminMessage = adminMessageFactory.CreateRebootMessage(seconds, isOtaMode); 34 | await Connection.WriteToRadio(ToRadioMessageFactory.CreateMeshPacketMessage(adminMessage), AnyResponseReceived); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Meshtastic.Cli/CommandHandlers/RegisterCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Data; 2 | using Meshtastic.Data.MessageFactories; 3 | using Meshtastic.Protobufs; 4 | using Microsoft.Extensions.Logging; 5 | using Spectre.Console.Json; 6 | using Newtonsoft.Json; 7 | 8 | namespace Meshtastic.Cli.CommandHandlers; 9 | 10 | public class RegisterCommandHandlerCommandHandler(DeviceConnectionContext context, CommandContext commandContext) : DeviceCommandHandler(context, commandContext) 11 | { 12 | public async Task Handle() 13 | { 14 | var wantConfig = new ToRadioMessageFactory().CreateWantConfigMessage(); 15 | var container = await Connection.WriteToRadio(wantConfig, CompleteOnConfigReceived); 16 | Connection.Disconnect(); 17 | return container; 18 | } 19 | 20 | public override async Task OnCompleted(FromRadio packet, DeviceStateContainer container) 21 | { 22 | Logger.LogInformation("Getting registration info..."); 23 | var key = container.MyNodeInfo.DeviceId; 24 | var user = container.Nodes.Find(n => n.Num == container.MyNodeInfo.MyNodeNum)?.User; 25 | #pragma warning disable CS0612 // Type or member is obsolete 26 | var macAddress = user?.Macaddr; 27 | #pragma warning restore CS0612 // Type or member is obsolete 28 | if (key == null || key.All(b => b == 0) || user == null || macAddress == null) 29 | { 30 | Logger.LogError("Device does not have a valid key or mac address, and cannot be registered."); 31 | return; 32 | } 33 | var jsonForm = JsonConvert.SerializeObject(new 34 | { 35 | MeshtasticDeviceId = Convert.ToHexString(key.ToByteArray()), 36 | MACAddress = Convert.ToHexString(macAddress.ToByteArray()), 37 | DeviceHardwareId = container.Metadata.HwModel, 38 | container.Metadata.FirmwareVersion, 39 | }); 40 | 41 | var json = new JsonText(jsonForm); 42 | 43 | AnsiConsole.Write( new Panel(json) 44 | .Header("Registration Information") 45 | .Collapse() 46 | .RoundedBorder() 47 | .BorderColor(Color.Blue)); 48 | 49 | await Task.CompletedTask; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Meshtastic.Cli/CommandHandlers/RemoveNodeCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Data; 2 | using Meshtastic.Data.MessageFactories; 3 | using Meshtastic.Protobufs; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.CommandHandlers; 7 | 8 | public class RemoveNodeCommandHandler : DeviceCommandHandler 9 | { 10 | private readonly uint nodeNum; 11 | 12 | public RemoveNodeCommandHandler(uint nodeNum, DeviceConnectionContext context, 13 | CommandContext commandContext) : base(context, commandContext) 14 | { 15 | this.nodeNum = nodeNum; 16 | } 17 | 18 | public async Task Handle() 19 | { 20 | var wantConfig = new ToRadioMessageFactory().CreateWantConfigMessage(); 21 | var container = await Connection.WriteToRadio(wantConfig, CompleteOnConfigReceived); 22 | Connection.Disconnect(); 23 | return container; 24 | } 25 | 26 | public override async Task OnCompleted(FromRadio packet, DeviceStateContainer container) 27 | { 28 | if (!container.Nodes.Any(n => n.Num == nodeNum)) 29 | { 30 | Logger.LogError($"Node with nodenum {nodeNum} not found in device's NodeDB"); 31 | return; 32 | } 33 | 34 | Logger.LogInformation("Removing device from NodeDB.."); 35 | var adminMessageFactory = new AdminMessageFactory(container, Destination); 36 | var adminMessage = adminMessageFactory.CreateRemoveByNodenumMessage(nodeNum); 37 | await Connection.WriteToRadio(ToRadioMessageFactory.CreateMeshPacketMessage(adminMessage), AnyResponseReceived); 38 | } 39 | } -------------------------------------------------------------------------------- /Meshtastic.Cli/CommandHandlers/RequestTelemetryCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Data; 2 | using Meshtastic.Data.MessageFactories; 3 | using Meshtastic.Protobufs; 4 | using Meshtastic.Extensions; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace Meshtastic.Cli.CommandHandlers; 8 | 9 | public class RequestTelemetryCommandHandler : DeviceCommandHandler 10 | { 11 | public RequestTelemetryCommandHandler(DeviceConnectionContext context, 12 | CommandContext commandContext) : base(context, commandContext) 13 | { 14 | } 15 | 16 | public async Task Handle() 17 | { 18 | if (Destination is null) 19 | throw new ApplicationException("Destination must be specified to request telemetry"); 20 | 21 | var wantConfig = new ToRadioMessageFactory().CreateWantConfigMessage(); 22 | var container = await Connection.WriteToRadio(wantConfig, CompleteOnConfigReceived); 23 | Connection.Disconnect(); 24 | return container; 25 | } 26 | 27 | public override async Task OnCompleted(FromRadio packet, DeviceStateContainer container) 28 | { 29 | var telemetryMessageFactory = new TelemetryMessageFactory(container, Destination); 30 | var message = telemetryMessageFactory.CreateTelemetryPacket(); 31 | await Connection.WriteToRadio(ToRadioMessageFactory.CreateMeshPacketMessage(message), async (packet, container) => 32 | { 33 | if (packet.Packet.Decoded.RequestId > 0 && packet.Packet.From == Destination!.Value && packet.GetPayload()?.DeviceMetrics is not null) { 34 | var metrics = packet.GetPayload()?.DeviceMetrics; 35 | Logger.LogInformation($"Received telemetry from destination ({Destination.Value}): \n{metrics}"); 36 | } 37 | return await Task.FromResult(false); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Meshtastic.Cli/CommandHandlers/ResetNodeDbCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Data; 2 | using Meshtastic.Data.MessageFactories; 3 | using Meshtastic.Protobufs; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.CommandHandlers; 7 | 8 | public class ResetNodeDbCommandHandler : DeviceCommandHandler 9 | { 10 | public ResetNodeDbCommandHandler(DeviceConnectionContext context, 11 | CommandContext commandContext) : base(context, commandContext) { } 12 | 13 | public async Task Handle() 14 | { 15 | var wantConfig = new ToRadioMessageFactory().CreateWantConfigMessage(); 16 | var container = await Connection.WriteToRadio(wantConfig, CompleteOnConfigReceived); 17 | Connection.Disconnect(); 18 | return container; 19 | } 20 | 21 | public override async Task OnCompleted(FromRadio packet, DeviceStateContainer container) 22 | { 23 | Logger.LogInformation("Reseting device node db..."); 24 | var adminMessageFactory = new AdminMessageFactory(container, Destination); 25 | var adminMessage = adminMessageFactory.CreateNodeDbResetMessage(); 26 | await Connection.WriteToRadio(ToRadioMessageFactory.CreateMeshPacketMessage(adminMessage), AnyResponseReceived); 27 | } 28 | } -------------------------------------------------------------------------------- /Meshtastic.Cli/CommandHandlers/SendTextCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Data; 2 | using Meshtastic.Data.MessageFactories; 3 | using Meshtastic.Extensions; 4 | using Meshtastic.Protobufs; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace Meshtastic.Cli.CommandHandlers; 8 | 9 | public class SendTextCommandHandler : DeviceCommandHandler 10 | { 11 | private readonly string message; 12 | 13 | public SendTextCommandHandler(string message, DeviceConnectionContext context, CommandContext commandContext) : 14 | base(context, commandContext) 15 | { 16 | this.message = message; 17 | } 18 | public async Task Handle() 19 | { 20 | var wantConfig = new ToRadioMessageFactory().CreateWantConfigMessage(); 21 | var container = await Connection.WriteToRadio(wantConfig, CompleteOnConfigReceived); 22 | Connection.Disconnect(); 23 | return container; 24 | } 25 | 26 | public override async Task OnCompleted(FromRadio packet, DeviceStateContainer container) 27 | { 28 | var textMessageFactory = new TextMessageFactory(container); 29 | var textMessage = textMessageFactory.CreateTextMessagePacket(message); 30 | Logger.LogInformation($"Sending text messagee..."); 31 | 32 | await Connection.WriteToRadio(ToRadioMessageFactory.CreateMeshPacketMessage(textMessage), 33 | (fromRadio, container) => 34 | { 35 | var routingResult = fromRadio.GetPayload(); 36 | if (routingResult != null && fromRadio.Packet.Priority == MeshPacket.Types.Priority.Ack) 37 | { 38 | if (routingResult.ErrorReason == Routing.Types.Error.None) 39 | Logger.LogInformation("Acknowledged"); 40 | else 41 | Logger.LogInformation($"Message delivery failed due to reason: {routingResult.ErrorReason}"); 42 | 43 | return Task.FromResult(true); 44 | } 45 | 46 | return Task.FromResult(fromRadio != null); 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Meshtastic.Cli/CommandHandlers/SendWaypointCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Data; 2 | using Meshtastic.Data.MessageFactories; 3 | using Meshtastic.Protobufs; 4 | using Microsoft.Extensions.Logging; 5 | using System.Text; 6 | 7 | namespace Meshtastic.Cli.CommandHandlers; 8 | 9 | public class SendWaypointCommandHandler : DeviceCommandHandler 10 | { 11 | private readonly decimal latitude; 12 | private readonly decimal longitude; 13 | private readonly string name; 14 | private readonly string description; 15 | private readonly string icon; 16 | private readonly bool locked; 17 | private readonly decimal divisor = new(1e-7); 18 | 19 | public SendWaypointCommandHandler(decimal latitude, 20 | decimal longitude, 21 | string name, 22 | string description, 23 | string icon, 24 | bool locked, 25 | DeviceConnectionContext context, 26 | CommandContext commandContext) : base(context, commandContext) 27 | { 28 | this.latitude = latitude; 29 | this.longitude = longitude; 30 | this.name = name; 31 | this.description = description; 32 | this.icon = icon; 33 | this.locked = locked; 34 | } 35 | 36 | public async Task Handle() 37 | { 38 | var wantConfig = new ToRadioMessageFactory().CreateWantConfigMessage(); 39 | var container = await Connection.WriteToRadio(wantConfig, CompleteOnConfigReceived); 40 | Connection.Disconnect(); 41 | return container; 42 | } 43 | 44 | public override async Task OnCompleted(FromRadio fromRadio, DeviceStateContainer container) 45 | { 46 | var factory = new WaypointMessageFactory(container, Destination); 47 | var message = factory.CreateWaypointPacket(new Waypoint() 48 | { 49 | Id = (uint)Math.Floor(Random.Shared.Next() * 1e9), 50 | LatitudeI = latitude != 0 ? decimal.ToInt32(latitude / divisor) : 0, 51 | LongitudeI = longitude != 0 ? decimal.ToInt32(longitude / divisor) : 0, 52 | Icon = BitConverter.ToUInt32(Encoding.UTF8.GetBytes(icon)), 53 | Name = name, 54 | Description = description ?? String.Empty, 55 | LockedTo = locked ? container.MyNodeInfo.MyNodeNum : 0, 56 | }); 57 | Logger.LogInformation($"Sending waypoint to device..."); 58 | await Connection.WriteToRadio(ToRadioMessageFactory.CreateMeshPacketMessage(message), 59 | (fromRadio, container) => 60 | { 61 | if (fromRadio.Packet?.Decoded?.Portnum == PortNum.RoutingApp && 62 | fromRadio?.Packet?.Priority == MeshPacket.Types.Priority.Ack) 63 | { 64 | var routingResult = Routing.Parser.ParseFrom(fromRadio.Packet.Decoded.Payload); 65 | if (routingResult.ErrorReason == Routing.Types.Error.None) 66 | Logger.LogInformation("Acknowledged"); 67 | else 68 | Logger.LogInformation($"Message delivery failed due to reason: {routingResult.ErrorReason}"); 69 | 70 | return Task.FromResult(true); 71 | } 72 | 73 | return Task.FromResult(false); 74 | }); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Meshtastic.Cli/CommandHandlers/SetCannedMessagesCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Data; 2 | using Meshtastic.Data.MessageFactories; 3 | using Meshtastic.Protobufs; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.CommandHandlers; 7 | 8 | public class SetCannedMessagesCommandHandler : DeviceCommandHandler 9 | { 10 | private readonly string messages; 11 | 12 | public SetCannedMessagesCommandHandler(string messages, DeviceConnectionContext context, CommandContext commandContext) : 13 | base(context, commandContext) 14 | { 15 | this.messages = messages; 16 | } 17 | public async Task Handle() 18 | { 19 | var wantConfig = new ToRadioMessageFactory().CreateWantConfigMessage(); 20 | var container = await Connection.WriteToRadio(wantConfig, CompleteOnConfigReceived); 21 | Connection.Disconnect(); 22 | return container; 23 | } 24 | 25 | public override async Task OnCompleted(FromRadio packet, DeviceStateContainer container) 26 | { 27 | var adminMessageFactory = new AdminMessageFactory(container, Destination); 28 | await BeginEditSettings(adminMessageFactory); 29 | Logger.LogInformation($"Setting canned messages on device..."); 30 | 31 | int index = 0; 32 | do 33 | { 34 | var upperBound = index + 200 > messages.Length ? messages.Length : index + 200; 35 | var setCannedMessage = adminMessageFactory.CreateSetCannedMessage(messages[index..upperBound]); 36 | await Connection.WriteToRadio(ToRadioMessageFactory.CreateMeshPacketMessage(setCannedMessage), AnyResponseReceived); 37 | 38 | index = upperBound + 1; 39 | } while (index < (messages.Length - 200)); 40 | 41 | await CommitEditSettings(adminMessageFactory); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Meshtastic.Cli/CommandHandlers/SetCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Parsers; 2 | using Meshtastic.Data; 3 | using Meshtastic.Data.MessageFactories; 4 | using Meshtastic.Protobufs; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace Meshtastic.Cli.CommandHandlers; 8 | 9 | public class SetCommandHandler : DeviceCommandHandler 10 | { 11 | public readonly IEnumerable? ParsedSettings; 12 | public SetCommandHandler(IEnumerable settings, DeviceConnectionContext context, CommandContext commandContext) : 13 | base(context, commandContext) 14 | { 15 | var (result, isValid) = ParseSettingOptions(settings, isGetOnly: false); 16 | if (!isValid) 17 | return; 18 | 19 | ParsedSettings = result!.ParsedSettings; 20 | } 21 | public async Task Handle() 22 | { 23 | var wantConfig = new ToRadioMessageFactory().CreateWantConfigMessage(); 24 | var container = await Connection.WriteToRadio(wantConfig, CompleteOnConfigReceived); 25 | Connection.Disconnect(); 26 | return container; 27 | } 28 | 29 | public override async Task OnCompleted(FromRadio packet, DeviceStateContainer container) 30 | { 31 | var adminMessageFactory = new AdminMessageFactory(container, Destination); 32 | await BeginEditSettings(adminMessageFactory); 33 | 34 | foreach (var setting in ParsedSettings!) 35 | { 36 | if (setting.Section.ReflectedType?.Name == nameof(container.LocalConfig)) 37 | await SetConfig(container, adminMessageFactory, setting); 38 | else 39 | await SetModuleConfig(container, adminMessageFactory, setting); 40 | Logger.LogInformation($"Setting {setting.Section.Name}.{setting.Setting.Name} to {setting.Value?.ToString() ?? string.Empty}..."); 41 | } 42 | await CommitEditSettings(adminMessageFactory); 43 | } 44 | 45 | private async Task SetModuleConfig(DeviceStateContainer container, AdminMessageFactory adminMessageFactory, ParsedSetting setting) 46 | { 47 | var instance = setting.Section.GetValue(container.LocalModuleConfig); 48 | setting.Setting.SetValue(instance, setting.Value); 49 | var adminMessage = adminMessageFactory.CreateSetModuleConfigMessage(instance!); 50 | await Connection.WriteToRadio(ToRadioMessageFactory.CreateMeshPacketMessage(adminMessage), 51 | AnyResponseReceived); 52 | } 53 | 54 | private async Task SetConfig(DeviceStateContainer container, AdminMessageFactory adminMessageFactory, ParsedSetting setting) 55 | { 56 | var instance = setting.Section.GetValue(container.LocalConfig); 57 | setting.Setting.SetValue(instance, setting.Value); 58 | var adminMessage = adminMessageFactory.CreateSetConfigMessage(instance!); 59 | await Connection.WriteToRadio(ToRadioMessageFactory.CreateMeshPacketMessage(adminMessage), 60 | AnyResponseReceived); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Meshtastic.Cli/CommandHandlers/TraceRouteCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Data; 2 | using Meshtastic.Data.MessageFactories; 3 | using Meshtastic.Display; 4 | using Meshtastic.Extensions; 5 | using Meshtastic.Protobufs; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace Meshtastic.Cli.CommandHandlers; 9 | 10 | public class TraceRouteCommandHandler : DeviceCommandHandler 11 | { 12 | public TraceRouteCommandHandler(DeviceConnectionContext context, CommandContext commandContext) : base(context, commandContext) { } 13 | 14 | public async Task Handle() 15 | { 16 | var wantConfig = new ToRadioMessageFactory().CreateWantConfigMessage(); 17 | var container = await Connection.WriteToRadio(wantConfig, CompleteOnConfigReceived); 18 | Connection.Disconnect(); 19 | return container; 20 | } 21 | 22 | public override async Task OnCompleted(FromRadio packet, DeviceStateContainer container) 23 | { 24 | Logger.LogInformation($"Tracing route to {container.GetNodeDisplayName(Destination!.Value)}..."); 25 | var messageFactory = new TraceRouteMessageFactory(container, Destination); 26 | var message = messageFactory.CreateRouteDiscoveryPacket(); 27 | await Connection.WriteToRadio(ToRadioMessageFactory.CreateMeshPacketMessage(message), 28 | (fromRadio, container) => 29 | { 30 | var routeDiscovery = fromRadio.GetPayload(); 31 | if (routeDiscovery != null) 32 | { 33 | if (routeDiscovery.Route.Count > 0) 34 | { 35 | var printer = new ProtobufPrinter(container, OutputFormat); 36 | printer.PrintRoute(routeDiscovery.Route); 37 | } 38 | else 39 | Logger.LogWarning("No routes discovered"); 40 | return Task.FromResult(true); 41 | } 42 | return Task.FromResult(false); 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Meshtastic.Cli/CommandHandlers/UrlCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Enums; 2 | using Meshtastic.Cli.Parsers; 3 | using Meshtastic.Data; 4 | using Meshtastic.Data.MessageFactories; 5 | using Meshtastic.Display; 6 | using Meshtastic.Protobufs; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace Meshtastic.Cli.CommandHandlers; 10 | 11 | public class UrlCommandHandler : DeviceCommandHandler 12 | { 13 | private readonly GetSetOperation operation; 14 | private readonly string? url; 15 | 16 | public UrlCommandHandler(GetSetOperation operation, string? url, DeviceConnectionContext context, CommandContext commandContext) : 17 | base(context, commandContext) 18 | { 19 | this.operation = operation; 20 | this.url = url; 21 | } 22 | 23 | public async Task Handle() 24 | { 25 | var wantConfig = ToRadioMessageFactory.CreateWantConfigMessage(); 26 | var container = await Connection.WriteToRadio(wantConfig, CompleteOnConfigReceived); 27 | Connection.Disconnect(); 28 | return container; 29 | } 30 | 31 | public override async Task OnCompleted(FromRadio packet, DeviceStateContainer container) 32 | { 33 | if (operation == GetSetOperation.Set && url!.Contains("/v/#")) { 34 | await SetContactFromUrl(container); 35 | } else if (operation == GetSetOperation.Set) { 36 | await SetChannelsFromUrl(container); 37 | } 38 | else if (operation == GetSetOperation.Get) 39 | { 40 | var printer = new ProtobufPrinter(container, OutputFormat); 41 | printer.PrintUrl(); 42 | } 43 | } 44 | private async Task SetContactFromUrl(DeviceStateContainer container) 45 | { 46 | var adminMessageFactory = new AdminMessageFactory(container, Destination); 47 | var urlParser = new UrlParser(url!); 48 | var contact = urlParser.ParseContact(); 49 | Logger.LogInformation($"Sending contact {contact.User.LongName} to device..."); 50 | var setContact = adminMessageFactory.CreateAddContactMessage(contact); 51 | await Connection.WriteToRadio(ToRadioMessageFactory.CreateMeshPacketMessage(setContact), AnyResponseReceived); 52 | } 53 | 54 | private async Task SetChannelsFromUrl(DeviceStateContainer container) 55 | { 56 | var adminMessageFactory = new AdminMessageFactory(container, Destination); 57 | await BeginEditSettings(adminMessageFactory); 58 | var urlParser = new UrlParser(url!); 59 | var channelSet = urlParser.ParseChannels(); 60 | int index = 0; 61 | foreach (var setting in channelSet.Settings) 62 | { 63 | var channel = new Channel 64 | { 65 | Index = index, 66 | Role = index == 0 ? Channel.Types.Role.Primary : Channel.Types.Role.Secondary, 67 | Settings = setting 68 | }; 69 | Logger.LogInformation($"Sending channel {index} to device..."); 70 | var setChannel = adminMessageFactory.CreateSetChannelMessage(channel); 71 | await Connection.WriteToRadio(ToRadioMessageFactory.CreateMeshPacketMessage(setChannel), AnyResponseReceived); 72 | index++; 73 | } 74 | Logger.LogInformation("Sending LoRA config to device..."); 75 | 76 | var setLoraConfig = adminMessageFactory.CreateSetConfigMessage(channelSet.LoraConfig); 77 | await Connection.WriteToRadio(ToRadioMessageFactory.CreateMeshPacketMessage(setLoraConfig), AnyResponseReceived); 78 | 79 | await CommitEditSettings(adminMessageFactory); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/CannedMessagesCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Binders; 2 | using Meshtastic.Cli.CommandHandlers; 3 | using Meshtastic.Cli.Enums; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.Commands; 7 | 8 | public class CannedMessagesCommand : Command 9 | { 10 | public CannedMessagesCommand(string name, string description, Option port, Option host, 11 | Option output, Option log, Option dest, Option selectDest) : 12 | base(name, description) 13 | { 14 | var operationArg = new Argument("operation", description: "Get or set canned messages"); 15 | operationArg.SetDefaultValue(GetSetOperation.Get); 16 | AddArgument(operationArg); 17 | 18 | var messagesArg = new Argument("messages", description: "Pipe | delimited canned message"); 19 | messagesArg.AddValidator(result => 20 | { 21 | if (result.GetValueForArgument(operationArg) == GetSetOperation.Set) 22 | { 23 | var messages = result.GetValueForArgument(messagesArg); 24 | if (messages == null || !messages.Contains('|')) 25 | result.ErrorMessage = "Must specify pipe delimited messages"; 26 | } 27 | }); 28 | messagesArg.SetDefaultValue(null); 29 | AddArgument(messagesArg); 30 | 31 | 32 | this.SetHandler(async (operation, messages, context, commandContext) => 33 | { 34 | if (operation == GetSetOperation.Get) 35 | { 36 | var handler = new GetCannedMessagesCommandHandler(context, commandContext); 37 | await handler.Handle(); 38 | } 39 | else 40 | { 41 | var handler = new SetCannedMessagesCommandHandler(messages!, context, commandContext); 42 | await handler.Handle(); 43 | } 44 | }, 45 | operationArg, 46 | messagesArg, 47 | new DeviceConnectionBinder(port, host), 48 | new CommandContextBinder(log, output, dest, selectDest)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/CaptureCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Binders; 2 | using Meshtastic.Cli.CommandHandlers; 3 | using Meshtastic.Cli.Enums; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.Commands; 7 | 8 | public class CaptureCommand : Command 9 | { 10 | public CaptureCommand(string name, string description, Option port, Option host, 11 | Option output, Option log) : base(name, description) 12 | { 13 | this.SetHandler(async (context, commandContext) => 14 | { 15 | var handler = new CaptureCommandHandler(context, commandContext); 16 | await handler.Handle(); 17 | }, 18 | new DeviceConnectionBinder(port, host), 19 | new CommandContextBinder(log, output, new Option("dest") { }, new Option("select-dest") { })); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/ChannelCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Binders; 2 | using Meshtastic.Cli.CommandHandlers; 3 | using Meshtastic.Cli.Enums; 4 | using Meshtastic.Protobufs; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace Meshtastic.Cli.Commands; 8 | 9 | public class ChannelCommand : Command 10 | { 11 | public ChannelCommand(string name, string description, Option port, Option host, 12 | Option output, Option log, Option dest, Option selectDest) : 13 | base(name, description) 14 | { 15 | var commandContextBinder = new CommandContextBinder(log, output, dest, selectDest, null); 16 | var operationArgument = new Argument("operation", "The type of channel operation"); 17 | operationArgument.AddCompletions(ctx => Enum.GetNames(typeof(ChannelOperation))); 18 | 19 | var indexOption = new Option("--index", description: "Channel index"); 20 | indexOption.AddAlias("-i"); 21 | indexOption.SetDefaultValue(0); 22 | indexOption.AddValidator(context => 23 | { 24 | var nonIndexZeroOperation = new[] { ChannelOperation.Disable, ChannelOperation.Enable }; 25 | if (context.GetValueForOption(indexOption) < 0 || context.GetValueForOption(indexOption) > 8) 26 | context.ErrorMessage = "Channel index is out of range (0-8)"; 27 | else if (nonIndexZeroOperation.Contains(context.GetValueForArgument(operationArgument)) && 28 | context.GetValueForOption(indexOption) == 0) 29 | { 30 | context.ErrorMessage = "Cannot enable / disable PRIMARY channel"; 31 | } 32 | }); 33 | var nameOption = new Option("--name", description: "Channel name"); 34 | nameOption.AddAlias("-n"); 35 | var roleOption = new Option("--role", description: "Channel role"); 36 | roleOption.AddAlias("-r"); 37 | var pskOption = new Option("--psk", description: "Channel pre-shared key"); 38 | pskOption.AddAlias("-p"); 39 | var uplinkOption = new Option("--uplink-enabled", description: "Channel uplink enabled"); 40 | uplinkOption.AddAlias("-u"); 41 | var downlinkOption = new Option("--downlink-enabled", description: "Channel downlink enabled"); 42 | downlinkOption.AddAlias("-d"); 43 | var channelBinder = new ChannelBinder(operationArgument, indexOption, nameOption, roleOption, pskOption, uplinkOption, downlinkOption); 44 | 45 | AddArgument(operationArgument); 46 | AddOption(indexOption); 47 | AddOption(nameOption); 48 | AddOption(roleOption); 49 | AddOption(pskOption); 50 | AddOption(uplinkOption); 51 | AddOption(downlinkOption); 52 | 53 | this.SetHandler(async (settings, context, commandContext) => 54 | { 55 | var handler = new ChannelCommandHandler(settings, context, commandContext); 56 | await handler.Handle(); 57 | }, 58 | channelBinder, 59 | new DeviceConnectionBinder(port, host), 60 | commandContextBinder); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/DfuCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Binders; 2 | using Meshtastic.Cli.CommandHandlers; 3 | using Meshtastic.Cli.Enums; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.Commands; 7 | 8 | public class DfuCommand : Command 9 | { 10 | public DfuCommand(string name, string description, Option port, Option host, 11 | Option output, Option log, Option dest, Option selectDest) : base(name, description) 12 | { 13 | this.SetHandler(async (context, commandContext) => 14 | { 15 | var handler = new DfuCommandHandler(context, commandContext); 16 | await handler.Handle(); 17 | }, 18 | new DeviceConnectionBinder(port, host), 19 | new CommandContextBinder(log, output, dest, selectDest)); 20 | } 21 | } -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/ExportCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Binders; 2 | using Meshtastic.Cli.CommandHandlers; 3 | using Meshtastic.Cli.Enums; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.Commands; 7 | public class ExportCommand : Command 8 | { 9 | public ExportCommand(string name, string description, Option port, Option host, 10 | Option output, Option log) : base(name, description) 11 | { 12 | var fileOption = new Option("file", "Path to export yaml"); 13 | AddOption(fileOption); 14 | 15 | this.SetHandler(async (file, context, commandContext) => 16 | { 17 | var handler = new ExportCommandHandler(file, context, commandContext); 18 | await handler.Handle(); 19 | }, 20 | fileOption, 21 | new DeviceConnectionBinder(port, host), 22 | new CommandContextBinder(log, output, new Option("dest"), new Option("select-dest"))); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/FactoryResetCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Binders; 2 | using Meshtastic.Cli.CommandHandlers; 3 | using Meshtastic.Cli.Enums; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.Commands; 7 | 8 | public class FactoryResetCommand : Command 9 | { 10 | public FactoryResetCommand(string name, string description, Option port, Option host, 11 | Option output, Option log, Option dest, Option selectDest) : 12 | base(name, description) 13 | { 14 | this.SetHandler(async (context, commandContext) => 15 | { 16 | var handler = new FactoryResetCommandHandler(context, commandContext); 17 | await handler.Handle(); 18 | }, 19 | new DeviceConnectionBinder(port, host), 20 | new CommandContextBinder(log, output, dest, selectDest)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/FileCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Binders; 2 | using Meshtastic.Cli.CommandHandlers; 3 | using Meshtastic.Cli.Enums; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.Commands; 7 | 8 | public class FileCommand : Command 9 | { 10 | public FileCommand(string name, string description, Option port, Option host, 11 | Option output, Option log) : base(name, description) 12 | { 13 | var pathArgument = new Argument("path", "The type of url operation"); 14 | this.AddArgument(pathArgument); 15 | 16 | this.SetHandler(async (path, context, commandContext) => 17 | { 18 | var handler = new FileCommandHandler(path, context, commandContext); 19 | await handler.Handle(); 20 | }, 21 | pathArgument, 22 | new DeviceConnectionBinder(port, host), 23 | new CommandContextBinder(log, output, new Option("dest") { }, new Option("select-dest") { })); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/FixedPositionCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Binders; 2 | using Meshtastic.Cli.CommandHandlers; 3 | using Meshtastic.Cli.Enums; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.Commands; 7 | 8 | public class FixedPositionCommand : Command 9 | { 10 | public FixedPositionCommand(string name, string description, Option port, Option host, 11 | Option output, Option log, Option dest, Option selectDest) : base(name, description) 12 | { 13 | var latArg = new Argument("lat", description: "Latitude of the node (decimal format)"); 14 | latArg.AddValidator(result => 15 | { 16 | if (Math.Abs(result.GetValueForArgument(latArg)) > 90) 17 | result.ErrorMessage = "Invalid latitude"; 18 | }); 19 | AddArgument(latArg); 20 | 21 | var lonArg = new Argument("lon", description: "Longitude of the node (decimal format)"); 22 | lonArg.AddValidator(result => 23 | { 24 | if (Math.Abs(result.GetValueForArgument(lonArg)) > 180) 25 | result.ErrorMessage = "Invalid longitude"; 26 | }); 27 | AddArgument(lonArg); 28 | 29 | var altArg = new Argument("altitude", description: "Altitude of the node (meters)"); 30 | altArg.SetDefaultValue(0); 31 | AddArgument(altArg); 32 | 33 | var clearOption = new Option("clear", description: "Clear fixed position"); 34 | clearOption.SetDefaultValue(false); 35 | AddOption(clearOption); 36 | 37 | this.SetHandler(async (lat, lon, alt, clear, context, commandContext) => 38 | { 39 | var handler = new FixedPositionCommandHandler(lat, lon, alt, clear, context, commandContext); 40 | await handler.Handle(); 41 | }, 42 | latArg, 43 | lonArg, 44 | altArg, 45 | clearOption, 46 | new DeviceConnectionBinder(port, host), 47 | new CommandContextBinder(log, output, dest, selectDest)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/GetCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Binders; 2 | using Meshtastic.Cli.CommandHandlers; 3 | using Meshtastic.Cli.Enums; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.Commands; 7 | 8 | public class GetCommand : Command 9 | { 10 | public GetCommand(string name, string description, Option> settings, Option port, Option host, 11 | Option output, Option log, Option dest, Option selectDest) : base(name, description) 12 | { 13 | this.SetHandler(async (settings, context, commandContext) => 14 | { 15 | var handler = new GetCommandHandler(settings, context, commandContext); 16 | await handler.Handle(); 17 | }, 18 | settings, 19 | new DeviceConnectionBinder(port, host), 20 | new CommandContextBinder(log, output, dest, selectDest)); 21 | this.AddOption(settings); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/ImportCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Binders; 2 | using Meshtastic.Cli.CommandHandlers; 3 | using Meshtastic.Cli.Enums; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.Commands; 7 | public class ImportCommand : Command 8 | { 9 | public ImportCommand(string name, string description, Option port, Option host, 10 | Option output, Option log) : base(name, description) 11 | { 12 | var fileOption = new Option("file", "Path to export yaml"); 13 | AddOption(fileOption); 14 | 15 | this.SetHandler(async (file, context, commandContext) => 16 | { 17 | var handler = new ImportCommandHandler(file, context, commandContext); 18 | await handler.Handle(); 19 | }, 20 | fileOption, 21 | new DeviceConnectionBinder(port, host), 22 | new CommandContextBinder(log, output, new Option("dest"), new Option("select-dest"))); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/InfoCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Binders; 2 | using Meshtastic.Cli.CommandHandlers; 3 | using Meshtastic.Cli.Enums; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.Commands; 7 | public class InfoCommand : Command 8 | { 9 | public InfoCommand(string name, string description, Option port, Option host, 10 | Option output, Option log, Option dest, Option selectDest) : base(name, description) 11 | { 12 | this.SetHandler(async (context, commandContext) => 13 | { 14 | var handler = new InfoCommandHandler(context, commandContext); 15 | await handler.Handle(); 16 | }, 17 | new DeviceConnectionBinder(port, host), 18 | new CommandContextBinder(log, output, dest, selectDest)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/ListCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.CommandHandlers; 2 | using Meshtastic.Cli.Enums; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace Meshtastic.Cli.Commands; 6 | 7 | public class ListCommand : Command 8 | { 9 | public ListCommand(string name, string description, Option _, Option __) 10 | : base(name, description) 11 | { 12 | var listCommandHandler = new ListCommandHandler(); 13 | this.SetHandler(ListCommandHandler.Handle); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/LiveCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Binders; 2 | using Meshtastic.Cli.CommandHandlers; 3 | using Meshtastic.Cli.Enums; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.Commands; 7 | public class LiveCommand : Command 8 | { 9 | public LiveCommand(string name, string description, Option port, Option host, 10 | Option output, Option log) : base(name, description) 11 | { 12 | this.SetHandler(async (context, commandContext) => 13 | { 14 | var handler = new LiveCommandHandler(context, commandContext); 15 | await handler.Handle(); 16 | }, 17 | new DeviceConnectionBinder(port, host), 18 | new CommandContextBinder(log, output, new Option("dest") { }, new Option("select-dest") { })); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/MetadataCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Binders; 2 | using Meshtastic.Cli.CommandHandlers; 3 | using Meshtastic.Cli.Enums; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.Commands; 7 | 8 | public class MetadataCommand : Command 9 | { 10 | public MetadataCommand(string name, string description, Option port, Option host, 11 | Option output, Option log, Option dest, Option selectDest) : 12 | base(name, description) 13 | { 14 | this.SetHandler(async (context, commandContext) => 15 | { 16 | var handler = new MetadataCommandHandler(context, commandContext); 17 | await handler.Handle(); 18 | }, 19 | new DeviceConnectionBinder(port, host), 20 | new CommandContextBinder(log, output, dest, selectDest)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/MonitorCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Binders; 2 | using Meshtastic.Cli.CommandHandlers; 3 | using Meshtastic.Cli.Enums; 4 | using Microsoft.Extensions.Logging; 5 | using System.Diagnostics.CodeAnalysis; 6 | 7 | namespace Meshtastic.Cli.Commands; 8 | 9 | [ExcludeFromCodeCoverage(Justification = "Requires serial hardware")] 10 | public class MonitorCommand : Command 11 | { 12 | public MonitorCommand(string name, string description, Option port, Option host, 13 | Option output, Option log) : base(name, description) 14 | { 15 | this.SetHandler(async (context, commandContext) => 16 | { 17 | var handler = new MonitorCommandHandler(context, commandContext); 18 | await handler.Handle(); 19 | }, 20 | new DeviceConnectionBinder(port, host), 21 | new CommandContextBinder(log, output, new Option("dest") { }, new Option("selectDest") { })); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/MqttProxyCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Binders; 2 | using Meshtastic.Cli.CommandHandlers; 3 | using Meshtastic.Cli.Enums; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.Commands; 7 | public class MqttProxyCommand : Command 8 | { 9 | public MqttProxyCommand(string name, string description, Option port, Option host, 10 | Option output, Option log) : base(name, description) 11 | { 12 | this.SetHandler(async (context, commandContext) => 13 | { 14 | var handler = new MqttProxyCommandHandler(context, commandContext); 15 | await handler.Handle(); 16 | }, 17 | new DeviceConnectionBinder(port, host), 18 | new CommandContextBinder(log, output, new Option("dest") { }, new Option("select-dest") { })); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/RebootCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Binders; 2 | using Meshtastic.Cli.CommandHandlers; 3 | using Meshtastic.Cli.Enums; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.Commands; 7 | 8 | public class RebootCommand : Command 9 | { 10 | public RebootCommand(string name, string description, Option port, Option host, 11 | Option output, Option log, Option dest, Option selectDest) : base(name, description) 12 | { 13 | var otaOption = new Option("ota", "Reboot into OTA update mode"); 14 | otaOption.SetDefaultValue(false); 15 | 16 | var secondsArgument = new Argument("seconds", "Number of seconds until reboot"); 17 | secondsArgument.SetDefaultValue(5); 18 | 19 | this.SetHandler(async (isOtaMode, seconds, context, commandContext) => 20 | { 21 | var handler = new RebootCommandHandler(isOtaMode, seconds, context, commandContext); 22 | await handler.Handle(); 23 | }, 24 | otaOption, 25 | secondsArgument, 26 | new DeviceConnectionBinder(port, host), 27 | new CommandContextBinder(log, output, dest, selectDest)); 28 | this.AddOption(otaOption); 29 | this.AddArgument(secondsArgument); 30 | } 31 | } -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/RegisterCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Binders; 2 | using Meshtastic.Cli.CommandHandlers; 3 | using Meshtastic.Cli.Enums; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.Commands; 7 | 8 | public class RegisterCommand : Command 9 | { 10 | public RegisterCommand(string name, string description, Option port, Option host, 11 | Option output, Option log) : base(name, description) 12 | { 13 | this.SetHandler(async (context, commandContext) => 14 | { 15 | var handler = new RegisterCommandHandlerCommandHandler(context, commandContext); 16 | await handler.Handle(); 17 | }, 18 | new DeviceConnectionBinder(port, host), 19 | new CommandContextBinder(log, output, new Option("dest"), new Option("select-dest"))); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/RemoveNodeCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Binders; 2 | using Meshtastic.Cli.CommandHandlers; 3 | using Meshtastic.Cli.Enums; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.Commands; 7 | 8 | public class RemoveNodeCommand : Command 9 | { 10 | public RemoveNodeCommand(string name, string description, Option port, Option host, 11 | Option output, Option log, Option dest, Option selectDest) : 12 | base(name, description) 13 | { 14 | var nodeNumArgument = new Argument("nodenum", "Nodenum of the node to remove from the device NodeDB"); 15 | AddArgument(nodeNumArgument); 16 | 17 | this.SetHandler(async (nodeNum, context, commandContext) => 18 | { 19 | var handler = new RemoveNodeCommandHandler(nodeNum, context, commandContext); 20 | await handler.Handle(); 21 | }, 22 | nodeNumArgument, 23 | new DeviceConnectionBinder(port, host), 24 | new CommandContextBinder(log, output, dest, selectDest)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/RequestTelemetryCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Binders; 2 | using Meshtastic.Cli.CommandHandlers; 3 | using Meshtastic.Cli.Enums; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.Commands; 7 | 8 | public class RequestTelemetryCommand : Command 9 | { 10 | public RequestTelemetryCommand(string name, string description, Option port, Option host, 11 | Option output, Option log, Option dest, Option selectDest) : base(name, description) 12 | { 13 | this.SetHandler(async (context, commandContext) => 14 | { 15 | var handler = new RequestTelemetryCommandHandler(context, commandContext); 16 | await handler.Handle(); 17 | }, 18 | new DeviceConnectionBinder(port, host), 19 | new CommandContextBinder(log, output, dest, selectDest)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/ResetNodeDbCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Binders; 2 | using Meshtastic.Cli.CommandHandlers; 3 | using Meshtastic.Cli.Enums; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.Commands; 7 | 8 | public class ResetNodeDbCommand : Command 9 | { 10 | public ResetNodeDbCommand(string name, string description, Option port, Option host, 11 | Option output, Option log, Option dest, Option selectDest) : 12 | base(name, description) 13 | { 14 | this.SetHandler(async (context, commandContext) => 15 | { 16 | var handler = new ResetNodeDbCommandHandler(context, commandContext); 17 | await handler.Handle(); 18 | }, 19 | new DeviceConnectionBinder(port, host), 20 | new CommandContextBinder(log, output, dest, selectDest)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/SendTextCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Binders; 2 | using Meshtastic.Cli.CommandHandlers; 3 | using Meshtastic.Cli.Enums; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.Commands; 7 | 8 | public class SendTextCommand : Command 9 | { 10 | public SendTextCommand(string name, string description, Option port, Option host, 11 | Option output, Option log, Option dest, Option selectDest) : base(name, description) 12 | { 13 | var messageArg = new Argument("message", description: "Text message contents"); 14 | messageArg.AddValidator(result => 15 | { 16 | if (String.IsNullOrWhiteSpace(result.GetValueForArgument(messageArg))) 17 | result.ErrorMessage = "Must specify a message"; 18 | }); 19 | AddArgument(messageArg); 20 | 21 | this.SetHandler(async (message, context, commandContext) => 22 | { 23 | var handler = new SendTextCommandHandler(message, context, commandContext); 24 | await handler.Handle(); 25 | }, 26 | messageArg, 27 | new DeviceConnectionBinder(port, host), 28 | new CommandContextBinder(log, output, dest, selectDest)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/SendWaypointCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Binders; 2 | using Meshtastic.Cli.CommandHandlers; 3 | using Meshtastic.Cli.Enums; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.Commands; 7 | 8 | public class SendWaypointCommand : Command 9 | { 10 | public SendWaypointCommand(string name, string description, Option port, Option host, 11 | Option output, Option log, Option dest, Option selectDest) : base(name, description) 12 | { 13 | var latArg = new Argument("lat", description: "Latitude of the node (decimal format)"); 14 | latArg.AddValidator(result => 15 | { 16 | if (Math.Abs(result.GetValueForArgument(latArg)) > 90) 17 | result.ErrorMessage = "Invalid latitude"; 18 | }); 19 | AddArgument(latArg); 20 | 21 | var lonArg = new Argument("lon", description: "Longitude of the waypoint (decimal format)"); 22 | lonArg.AddValidator(result => 23 | { 24 | if (Math.Abs(result.GetValueForArgument(lonArg)) > 180) 25 | result.ErrorMessage = "Invalid longitude"; 26 | }); 27 | AddArgument(lonArg); 28 | 29 | var nameOption = new Option("--name", description: "Name for the waypoint"); 30 | AddOption(nameOption); 31 | 32 | var descriptionOption = new Option("--description", description: "Description for the waypoint"); 33 | AddOption(descriptionOption); 34 | 35 | var iconOption = new Option("--icon", description: "Icon emoji for the waypoint"); 36 | iconOption.SetDefaultValue("📍"); 37 | iconOption.AddValidator(ctx => 38 | { 39 | var emoji = ctx.GetValueOrDefault()?.ToString(); 40 | if (emoji == null)// || !Regex.IsMatch(emoji, EmojiRegexPattern)) 41 | { 42 | ctx.ErrorMessage = "Must specifiy a valid emoji character for waypoint icon"; 43 | } 44 | }); 45 | AddOption(iconOption); 46 | 47 | var lockedOption = new Option("--locked", description: "Lock the waypoint from updates"); 48 | AddOption(lockedOption); 49 | 50 | this.SetHandler(async (lat, lon, name, description, icon, locked, context, commandContext) => 51 | { 52 | var handler = new SendWaypointCommandHandler(lat, lon, name, description, icon, locked, context, commandContext); 53 | await handler.Handle(); 54 | }, 55 | latArg, 56 | lonArg, 57 | nameOption, 58 | descriptionOption, 59 | iconOption, 60 | lockedOption, 61 | new DeviceConnectionBinder(port, host), 62 | new CommandContextBinder(log, output, dest, selectDest)); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/SetCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Binders; 2 | using Meshtastic.Cli.CommandHandlers; 3 | using Meshtastic.Cli.Enums; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.Commands; 7 | 8 | public class SetCommand : Command 9 | { 10 | public SetCommand(string name, string description, Option> settings, Option port, Option host, 11 | Option output, Option log, Option dest, Option selectDest) : 12 | base(name, description) 13 | { 14 | this.SetHandler(async (settings, context, commandContext) => 15 | { 16 | var handler = new SetCommandHandler(settings, context, commandContext); 17 | await handler.Handle(); 18 | }, 19 | settings, 20 | new DeviceConnectionBinder(port, host), 21 | new CommandContextBinder(log, output, dest, selectDest)); 22 | this.AddOption(settings); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/TraceRouteCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Binders; 2 | using Meshtastic.Cli.CommandHandlers; 3 | using Meshtastic.Cli.Enums; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.Commands; 7 | 8 | public class TraceRouteCommand : Command 9 | { 10 | public TraceRouteCommand(string name, string description, Option port, Option host, 11 | Option output, Option log, Option dest, Option selectDest) : 12 | base(name, description) 13 | { 14 | this.SetHandler(async (context, commandContext) => 15 | { 16 | var handler = new TraceRouteCommandHandler(context, commandContext); 17 | await handler.Handle(); 18 | }, 19 | new DeviceConnectionBinder(port, host), 20 | new CommandContextBinder(log, output, dest, selectDest)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/UpdateCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Binders; 2 | using Meshtastic.Cli.CommandHandlers; 3 | using Meshtastic.Cli.Enums; 4 | using Meshtastic.Cli.Utilities; 5 | using Microsoft.Extensions.Logging; 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | namespace Meshtastic.Cli.Commands; 9 | 10 | [ExcludeFromCodeCoverage(Justification = "Hardware access")] 11 | public class UpdateCommand : Command 12 | { 13 | public UpdateCommand(string name, string description, Option port, Option host, 14 | Option output, Option log) : 15 | base(name, description) 16 | { 17 | this.SetHandler(async (context, commandContext) => 18 | { 19 | var handler = new UpdateCommandHandler(new FirmwarePackageService(), new ReleaseZipService(), context, commandContext); 20 | await handler.Handle(); 21 | }, 22 | new DeviceConnectionBinder(port, host), 23 | new CommandContextBinder(log, output, new Option("dest") { }, new Option("select-dest") { })); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Commands/UrlCommand.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Binders; 2 | using Meshtastic.Cli.CommandHandlers; 3 | using Meshtastic.Cli.Enums; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Meshtastic.Cli.Commands; 7 | 8 | public class UrlCommand : Command 9 | { 10 | public UrlCommand(string name, string description, Option port, Option host, 11 | Option output, Option log) : base(name, description) 12 | { 13 | var urlOperationArgument = new Argument("operation", "The type of url operation"); 14 | urlOperationArgument.AddCompletions(ctx => Enum.GetNames(typeof(GetSetOperation))); 15 | var urlArgument = new Argument("url", "The channel url to set on the device"); 16 | urlArgument.SetDefaultValue(null); 17 | 18 | this.SetHandler(async (operation, url, context, commandContext) => 19 | { 20 | var handler = new UrlCommandHandler(operation, url, context, commandContext); 21 | await handler.Handle(); 22 | }, 23 | urlOperationArgument, 24 | urlArgument, 25 | new DeviceConnectionBinder(port, host), 26 | new CommandContextBinder(log, output, new Option("dest") { }, new Option("select-dest") { })); 27 | this.AddArgument(urlOperationArgument); 28 | this.AddArgument(urlArgument); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Meshtastic.Cli/DeviceConnectionContext.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Connections; 2 | using Microsoft.Extensions.Logging; 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | namespace Meshtastic.Cli; 6 | 7 | [ExcludeFromCodeCoverage(Justification = "Requires hardware")] 8 | public class DeviceConnectionContext 9 | { 10 | public readonly string? Port; 11 | public readonly string? Host; 12 | 13 | public DeviceConnectionContext(string? port, string? host) 14 | { 15 | this.Port = port; 16 | this.Host = host; 17 | } 18 | 19 | public DeviceConnection GetDeviceConnection(ILogger logger) 20 | { 21 | if (this.Port == "SIMPORT") 22 | return new SimulatedConnection(logger); 23 | 24 | if (!String.IsNullOrWhiteSpace(this.Host)) 25 | return new TcpConnection(logger, this.Host); 26 | else if (!String.IsNullOrWhiteSpace(this.Port)) 27 | return new SerialConnection(logger, this.Port); 28 | 29 | var ports = SerialConnection.ListPorts(); 30 | 31 | if (!ports.Any()) 32 | throw new InvalidOperationException("No port or hostname specified and could not find available serial ports"); 33 | if (ports.Length == 1) 34 | return new SerialConnection(logger, ports.First()); 35 | 36 | var selectedPort = AnsiConsole.Prompt(new SelectionPrompt() 37 | .Title("No port or host specified, please select a serial port") 38 | .AddChoices(ports)); 39 | 40 | return new SerialConnection(logger, selectedPort); 41 | } 42 | } -------------------------------------------------------------------------------- /Meshtastic.Cli/Enums/ChannelOperation.cs: -------------------------------------------------------------------------------- 1 | namespace Meshtastic.Cli.Enums; 2 | 3 | public enum ChannelOperation 4 | { 5 | Add, 6 | Save, 7 | Enable, 8 | Disable, 9 | } 10 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Enums/ExportOption.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Meshtastic.Cli.Enums; 8 | 9 | enum ExportOption 10 | { 11 | Name, 12 | Channels, 13 | Config, 14 | ModuleConfig 15 | } 16 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Enums/GetSetOperation.cs: -------------------------------------------------------------------------------- 1 | namespace Meshtastic.Cli.Enums; 2 | 3 | public enum GetSetOperation 4 | { 5 | Get, 6 | Set, 7 | } 8 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Enums/OutputFormat.cs: -------------------------------------------------------------------------------- 1 | namespace Meshtastic.Cli.Enums; 2 | 3 | public enum OutputFormat 4 | { 5 | PrettyConsole = 0, 6 | Json = 1, 7 | } 8 | -------------------------------------------------------------------------------- /Meshtastic.Cli/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "", Scope = "type", Target = "~T:Meshtastic.Cli.Utilities.FirmwarePackage")] 9 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Logging/LoggingExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace Meshtastic.Cli.Logging; 4 | 5 | internal static class LoggingExtensions 6 | { 7 | public static ILoggingBuilder AddPrettyConsole(this ILoggingBuilder loggingBuilder, PrettyConsoleLoggerConfiguration config) 8 | { 9 | loggingBuilder.AddProvider(new PrettyConsoleLoggerProvider(config)); 10 | return loggingBuilder; 11 | } 12 | } -------------------------------------------------------------------------------- /Meshtastic.Cli/Logging/PrettyConsoleLogger.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace Meshtastic.Cli.Logging; 4 | 5 | public class PrettyConsoleLogger : ILogger 6 | { 7 | public PrettyConsoleLogger() 8 | { 9 | this.config = new PrettyConsoleLoggerConfiguration(); 10 | 11 | var settings = config.ConsoleSettings ?? new AnsiConsoleSettings 12 | { 13 | Ansi = AnsiSupport.Detect, 14 | ColorSystem = ColorSystemSupport.Detect, 15 | }; 16 | console = AnsiConsole.Create(settings); 17 | config.ConsoleConfiguration?.Invoke(console); 18 | } 19 | 20 | private readonly PrettyConsoleLoggerConfiguration config; 21 | private readonly IAnsiConsole console; 22 | 23 | public PrettyConsoleLogger(PrettyConsoleLoggerConfiguration config) 24 | { 25 | this.config = config; 26 | 27 | var settings = config.ConsoleSettings ?? new AnsiConsoleSettings 28 | { 29 | Ansi = AnsiSupport.Detect, 30 | ColorSystem = ColorSystemSupport.Detect, 31 | }; 32 | console = AnsiConsole.Create(settings); 33 | config.ConsoleConfiguration?.Invoke(console); 34 | } 35 | 36 | #pragma warning disable CS8603 // Possible null reference return. 37 | IDisposable ILogger.BeginScope(TState state) => null; 38 | #pragma warning restore CS8603 // Possible null reference return. 39 | 40 | public bool IsEnabled(LogLevel logLevel) => logLevel >= config.LogLevel; 41 | 42 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) 43 | { 44 | if (!IsEnabled(logLevel)) 45 | return; 46 | 47 | var prefix = GetLevelMarkup(logLevel); 48 | var message = formatter(state, exception).EscapeMarkup(); 49 | console.MarkupLine($"{prefix}{message}[/]"); 50 | } 51 | 52 | private static string GetLevelMarkup(LogLevel level) 53 | { 54 | return level switch 55 | { 56 | LogLevel.Critical => "[bold underline white on red]", 57 | LogLevel.Error => "[bold red]", 58 | LogLevel.Warning => "[bold orange3]", 59 | LogLevel.Information => "[bold]", 60 | LogLevel.Debug => "[dim cyan1]", 61 | LogLevel.Trace => "[dim grey]", 62 | (LogLevel.None or _) => String.Empty, 63 | }; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Logging/PrettyConsoleLoggerConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace Meshtastic.Cli.Logging; 4 | 5 | public class PrettyConsoleLoggerConfiguration 6 | { 7 | public LogLevel LogLevel { get; set; } = LogLevel.Information; 8 | public int EventId { get; set; } = 0; 9 | public Action? ConsoleConfiguration { get; set; } 10 | public AnsiConsoleSettings? ConsoleSettings { get; set; } = null; 11 | } 12 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Logging/PrettyConsoleLoggerProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System.Collections.Concurrent; 3 | 4 | namespace Meshtastic.Cli.Logging; 5 | 6 | internal class PrettyConsoleLoggerProvider : ILoggerProvider 7 | { 8 | private readonly PrettyConsoleLoggerConfiguration _config; 9 | private readonly ConcurrentDictionary _loggers = new(); 10 | 11 | public PrettyConsoleLoggerProvider(PrettyConsoleLoggerConfiguration config) => _config = config; 12 | public ILogger CreateLogger(string categoryName) => 13 | _loggers.GetOrAdd(categoryName, name => new PrettyConsoleLogger(_config)); 14 | 15 | public void Dispose() => _loggers.Clear(); 16 | } -------------------------------------------------------------------------------- /Meshtastic.Cli/Mappings/HardwareModelMappings.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Protobufs; 2 | 3 | namespace Meshtastic.Cli.Mappings; 4 | 5 | internal static class HardwareModelMappings 6 | { 7 | public static readonly IEnumerable NrfHardwareModels = new[] 8 | { 9 | HardwareModel.TEcho, 10 | HardwareModel.Rak4631, 11 | HardwareModel.NanoG2Ultra, 12 | HardwareModel.Nrf52Unknown 13 | }; 14 | 15 | public static readonly Dictionary FileNameMappings = new() 16 | { 17 | { HardwareModel.HeltecV1, "heltec-v1" }, 18 | { HardwareModel.HeltecV20, "heltec-v2.0" }, 19 | { HardwareModel.HeltecV21, "heltec-v2.1" }, 20 | { HardwareModel.HeltecV3, "heltec-v3" }, 21 | { HardwareModel.HeltecWslV3, "heltec-wsl-v3" }, 22 | { HardwareModel.M5Stack, "m5stack-core" }, 23 | { HardwareModel.DiyV1, "meshtastic-diy-v1" }, 24 | { HardwareModel.DrDev, "meshtastic-dr-dev" }, 25 | { HardwareModel.NanoG1, "nano-g1" }, 26 | { HardwareModel.Rak11200, "rak11200" }, 27 | { HardwareModel.Rak4631, "rak4631" }, 28 | { HardwareModel.StationG1, "station-g1" }, 29 | { HardwareModel.Tbeam, "tbeam" }, 30 | { HardwareModel.TbeamV0P7, "tbeam0.7" }, 31 | { HardwareModel.LilygoTbeamS3Core, "tbeam-s3-core" }, 32 | { HardwareModel.TloraV1, "tlora-v1" }, 33 | { HardwareModel.TloraV11P3, "tlora-v1_3" }, 34 | { HardwareModel.TloraV2, "tlora-v2" }, 35 | { HardwareModel.TloraV211P6, "tlora-v2-1-1.6" }, 36 | { HardwareModel.TloraV211P8, "tlora-v2-1-1.8" }, 37 | { HardwareModel.TEcho, "t-echo" }, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Meshtastic.Cli.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Exe 4 | net9.0 5 | Meshtastic.Cli 6 | enable 7 | enable 8 | True 9 | logo.png 10 | README.md 11 | http://github.com/meshtastic/c-sharp 12 | Meshtastic LLC 13 | Meshtastic CLI 14 | True 15 | embedded 16 | LICENSE 17 | 18 | 19 | 20 | 21 | True 22 | \ 23 | 24 | 25 | True 26 | \ 27 | 28 | 29 | True 30 | \ 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Meshtastic.Cli.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.001.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meshtastic.Cli", "Meshtastic.Cli.csproj", "{A3FB32D2-954E-4D1D-B789-1E14AA5B2CB6}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {A3FB32D2-954E-4D1D-B789-1E14AA5B2CB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {A3FB32D2-954E-4D1D-B789-1E14AA5B2CB6}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {A3FB32D2-954E-4D1D-B789-1E14AA5B2CB6}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {A3FB32D2-954E-4D1D-B789-1E14AA5B2CB6}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {CF056994-0472-486E-8D07-0D470536C40D} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Parsers/Parser.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace Meshtastic.Cli.Parsers; 4 | 5 | public class Parser 6 | { 7 | protected static object ParseValue(PropertyInfo setting, string value) 8 | { 9 | if (setting.PropertyType == typeof(uint)) 10 | return uint.Parse(value); 11 | else if (setting.PropertyType == typeof(float)) 12 | return float.Parse(value); 13 | else if (setting.PropertyType == typeof(bool)) 14 | return bool.Parse(value); 15 | else if (setting.PropertyType == typeof(string)) 16 | return value; 17 | else 18 | return Enum.Parse(setting.PropertyType!, value, ignoreCase: true); 19 | } 20 | } 21 | 22 | public record SettingParserResult(IEnumerable ParsedSettings, IEnumerable ValidationIssues); 23 | public record ParsedSetting(PropertyInfo Section, PropertyInfo Setting, object? Value); -------------------------------------------------------------------------------- /Meshtastic.Cli/Parsers/SettingParser.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Extensions; 2 | using Meshtastic.Protobufs; 3 | using System.Reflection; 4 | 5 | namespace Meshtastic.Cli.Parsers; 6 | 7 | public class SettingParser : Parser 8 | { 9 | private readonly IEnumerable settings; 10 | 11 | public SettingParser(IEnumerable settings) 12 | { 13 | this.settings = settings; 14 | } 15 | 16 | public SettingParserResult ParseSettings(bool isGetOnly) 17 | { 18 | var parsedSettings = new List(); 19 | var validationIssues = new List(); 20 | 21 | foreach (var setting in this.settings) 22 | { 23 | if (isGetOnly) 24 | { 25 | TryParse(parsedSettings, validationIssues, setting, null); 26 | } 27 | else // Set settings 28 | { 29 | var segments = setting.Split('=', StringSplitOptions.TrimEntries); 30 | 31 | if (segments.Length != 2) 32 | validationIssues.Add($"Could not parse setting `{setting}`. Please use the format `mqtt.host=mywebsite.com`"); 33 | else 34 | TryParse(parsedSettings, validationIssues, segments[0], segments[1]); 35 | } 36 | } 37 | return new SettingParserResult(parsedSettings, validationIssues); 38 | } 39 | 40 | private void TryParse(List parsedSettings, List validationIssues, string setting, string? value) 41 | { 42 | var segments = setting.Split('.', StringSplitOptions.TrimEntries).Select(s => s.Replace("_", String.Empty)).ToArray(); 43 | if (segments.Length != 2) 44 | validationIssues.Add($"Could not parse setting `{setting}`. Please use the format `mqtt.host`"); 45 | else 46 | { 47 | var section = SearchConfigSections(segments.First()); 48 | if (section == null) 49 | validationIssues.Add($"Could not find section `{segments.First()}` in config or module config"); 50 | else 51 | { 52 | var sectionSetting = section.PropertyType.FindPropertyByName(segments[1]); 53 | if (sectionSetting == null) 54 | validationIssues.Add($"Could not find setting `{segments[1]}` under {section.Name}"); 55 | else 56 | { 57 | var parsedValue = value == null ? null : ParseValue(sectionSetting, value!); 58 | parsedSettings.Add(new ParsedSetting(section, sectionSetting, parsedValue)); 59 | } 60 | } 61 | } 62 | } 63 | 64 | private static PropertyInfo? SearchConfigSections(string section) 65 | { 66 | return typeof(LocalConfig).FindPropertyByName(section) ?? typeof(LocalModuleConfig).FindPropertyByName(section); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Parsers/UrlParser.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Protobufs; 2 | 3 | namespace Meshtastic.Cli.Parsers; 4 | 5 | public class UrlParser 6 | { 7 | private readonly string url; 8 | 9 | public UrlParser(string url) 10 | { 11 | if (string.IsNullOrWhiteSpace(url)) 12 | throw new ArgumentException($"'{nameof(url)}' must be provided.", nameof(url)); 13 | 14 | this.url = url; 15 | } 16 | 17 | public ChannelSet ParseChannels() 18 | { 19 | var split = this.url.Split("/#"); 20 | var base64ChannelSet = split.Last(); 21 | base64ChannelSet = base64ChannelSet.Replace('-', '+').Replace('_', '/'); 22 | var missingPadding = base64ChannelSet.Length % 4; 23 | if (missingPadding > 0) 24 | base64ChannelSet += new string('=', 4 - missingPadding); 25 | 26 | var base64EncodedBytes = Convert.FromBase64String(base64ChannelSet); 27 | return ChannelSet.Parser.ParseFrom(base64EncodedBytes); 28 | } 29 | 30 | public SharedContact ParseContact() 31 | { 32 | var split = this.url.Split("/#"); 33 | var content = split.Last(); 34 | 35 | var base64EncodedBytes = Convert.FromBase64String(content); 36 | return SharedContact.Parser.ParseFrom(base64EncodedBytes); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Serialization/FilteredTypeInspector.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Protobufs; 2 | using YamlDotNet.Serialization; 3 | using YamlDotNet.Serialization.TypeInspectors; 4 | 5 | namespace Meshtastic.Cli.Serialization; 6 | 7 | public class FilteredTypeInspector : TypeInspectorSkeleton 8 | { 9 | private readonly ITypeInspector inner; 10 | 11 | public FilteredTypeInspector(ITypeInspector inner) 12 | { 13 | this.inner = inner; 14 | } 15 | 16 | public override string GetEnumName(Type enumType, string name) => inner.GetEnumName(enumType, name); 17 | 18 | public override string GetEnumValue(object enumValue) => inner.GetEnumValue(enumValue); 19 | 20 | public override IEnumerable GetProperties(Type type, object? container) 21 | { 22 | var properties = inner.GetProperties(type, container) 23 | .Where(p => !p.Name.Equals("Version", StringComparison.OrdinalIgnoreCase)) 24 | .Where(p => !p.Name.StartsWith("Has", StringComparison.OrdinalIgnoreCase)); 25 | 26 | return properties; 27 | } 28 | } -------------------------------------------------------------------------------- /Meshtastic.Cli/StyleResources.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace Meshtastic.Cli; 4 | 5 | [ExcludeFromCodeCoverage(Justification = "Style")] 6 | internal class StyleResources 7 | { 8 | public static Color MESHTASTIC_GREEN => new(103, 234, 148); 9 | } 10 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Utilities/FirmwarePackageService.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | 3 | namespace Meshtastic.Cli.Utilities; 4 | 5 | public record FirmwareReleaseOptions(IEnumerable stable, IEnumerable alpha); 6 | public record FirmwarePackageOptions(FirmwareReleaseOptions releases, IEnumerable pullRequests); 7 | public record FirmwarePackage(string title, string zip_url); 8 | 9 | public class FirmwarePackageService 10 | { 11 | public FirmwarePackageService() 12 | { 13 | } 14 | 15 | public async Task GetFirmwareReleases() 16 | { 17 | var client = new HttpClient(); 18 | #pragma warning disable CS8603 // Possible null reference return. 19 | return await client.GetFromJsonAsync("https://api.meshtastic.org/github/firmware/list"); 20 | #pragma warning restore CS8603 // Possible null reference return. 21 | } 22 | 23 | public async Task DownloadRelease(FirmwarePackage firmwareRelease) 24 | { 25 | var memoryStream = new MemoryStream(); 26 | using var httpClient = new HttpClient(); 27 | var stream = await httpClient.GetStreamAsync(firmwareRelease.zip_url); 28 | await stream.CopyToAsync(memoryStream); 29 | return memoryStream; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Meshtastic.Cli/Utilities/ReleaseZipService.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Mappings; 2 | using Meshtastic.Protobufs; 3 | using System.IO.Compression; 4 | 5 | namespace Meshtastic.Cli.Utilities; 6 | 7 | public class ReleaseZipService 8 | { 9 | public ReleaseZipService() 10 | { 11 | } 12 | 13 | public string TempDirectory => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); 14 | 15 | public async Task ExtractUpdateBinary(MemoryStream memoryStream, HardwareModel hardwareModel) 16 | { 17 | var result = new ZipArchive(memoryStream); 18 | var updateBinEntry = result.Entries.First(entry => MatchUpdateBinary(entry, hardwareModel)); 19 | var localPath = Path.Join(TempDirectory, "Meshtast.Cli"); 20 | Directory.CreateDirectory(localPath); 21 | var filePath = Path.Join(localPath, updateBinEntry.Name); 22 | if (!File.Exists(filePath)) 23 | { 24 | using var fileStream = File.Create(filePath); 25 | await updateBinEntry.Open().CopyToAsync(fileStream); 26 | } 27 | return filePath; 28 | } 29 | 30 | private bool MatchUpdateBinary(ZipArchiveEntry entry, HardwareModel hardwareModel) 31 | { 32 | var isNrf = HardwareModelMappings.NrfHardwareModels.Contains(hardwareModel); 33 | var targetName = HardwareModelMappings.FileNameMappings[hardwareModel]; 34 | if (isNrf) 35 | return entry.Name.Contains(targetName) && entry.Name.EndsWith(".uf2"); 36 | else 37 | return entry.Name.Contains(targetName) && entry.Name.EndsWith("update.bin"); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Meshtastic.Cli/appSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } -------------------------------------------------------------------------------- /Meshtastic.Test/CommandHandlers/CommandHandlerTestBase.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace Meshtastic.Test.CommandHandlers 5 | { 6 | public class CommandHandlerTestBase 7 | { 8 | #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 9 | public Mock FakeLogger; 10 | public DeviceConnectionContext ConnectionContext; 11 | public CommandContext CommandContext; 12 | #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 13 | 14 | protected void DebugLogsContain(string messagePart, Times? times = null) 15 | { 16 | FakeLogger.VerifyLog(l => 17 | l.LogDebug(It.Is(message => message.StartsWith(messagePart))), times ?? Times.Once()); 18 | } 19 | protected void InformationLogsContain(string messagePart, Times? times = null) 20 | { 21 | FakeLogger.VerifyLog(l => 22 | l.LogInformation(It.Is(message => message.StartsWith(messagePart))), times ?? Times.Once()); 23 | } 24 | protected void ErrorLogsContain(string messagePart, Times? times = null) 25 | { 26 | FakeLogger.VerifyLog(l => 27 | l.LogError(It.Is(message => message.StartsWith(messagePart))), times ?? Times.Once()); 28 | } 29 | 30 | protected void ReceivedWantConfigPayloads() 31 | { 32 | DebugLogsContain("Sent: { \"wantConfigId\":"); 33 | DebugLogsContain("Received: { \"myInfo\": {"); 34 | DebugLogsContain("Received: { \"nodeInfo\": {", Times.AtLeastOnce()); 35 | DebugLogsContain("Received: { \"channel\": {", Times.AtLeast(7)); 36 | DebugLogsContain("Received: { \"config\": {", Times.AtLeastOnce()); 37 | DebugLogsContain("Received: { \"moduleConfig\": {", Times.AtLeastOnce()); 38 | DebugLogsContain("Received: { \"configCompleteId\":"); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /Meshtastic.Test/Commands/CannedMessagesCommandTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Commands; 2 | using System.CommandLine; 3 | 4 | namespace Meshtastic.Test.Commands; 5 | 6 | [TestFixture] 7 | public class CannedMessagesCommandTests : CommandTestBase 8 | { 9 | private RootCommand rootCommand; 10 | 11 | [SetUp] 12 | public void Setup() 13 | { 14 | rootCommand = GetRootCommand(); 15 | var command = new CannedMessagesCommand("canned-messages", "canned-messages description", portOption, hostOption, outputOption, logLevelOption, destOption, selectDestOption); 16 | rootCommand.AddCommand(command); 17 | } 18 | 19 | [Test] 20 | public async Task CannedMessagesCommand_Should_Fail_ForEmptyOrNullMessagesOnSet() 21 | { 22 | var result = await rootCommand.InvokeAsync("canned-messages set --port SIMPORT", Console); 23 | result.Should().BeGreaterThan(0); 24 | Out.Output.Should().Contain("Must specify pipe delimited messages"); 25 | } 26 | 27 | [Test] 28 | public async Task CannedMessagesCommand_Should_Fail_ForPipelessMessagesOnSet() 29 | { 30 | var result = await rootCommand.InvokeAsync("canned-messages set hello --port SIMPORT", Console); 31 | result.Should().BeGreaterThan(0); 32 | Out.Output.Should().Contain("Must specify pipe delimited messages"); 33 | } 34 | 35 | [Test] 36 | public async Task CannedMessagesCommand_Should_Succeed_ForValidGet() 37 | { 38 | var result = await rootCommand.InvokeAsync("canned-messages get --port SIMPORT", Console); 39 | result.Should().Be(0); 40 | } 41 | 42 | [Test] 43 | public async Task CannedMessagesCommand_Should_Succeed_ForValidSet() 44 | { 45 | var result = await rootCommand.InvokeAsync("canned-messages set \"I need an alpinist|Halp\" --port SIMPORT", Console); 46 | result.Should().Be(0); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Meshtastic.Test/Commands/ChannelCommandTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Commands; 2 | using System.CommandLine; 3 | 4 | namespace Meshtastic.Test.Commands; 5 | 6 | [TestFixture] 7 | public class ChannelCommandTests : CommandTestBase 8 | { 9 | private RootCommand rootCommand; 10 | 11 | [SetUp] 12 | public void Setup() 13 | { 14 | rootCommand = GetRootCommand(); 15 | var channelCommand = new ChannelCommand("channel", "channel description", portOption, hostOption, outputOption, logLevelOption, destOption, selectDestOption); 16 | rootCommand.AddCommand(channelCommand); 17 | } 18 | 19 | [Test] 20 | public async Task ChannelCommand_Should_Succeed_ForValidArgs() 21 | { 22 | var result = await rootCommand.InvokeAsync("channel save --index 0 --name \"Derp\"s --port SIMPORT", Console); 23 | result.Should().BeGreaterThan(0); 24 | } 25 | 26 | [Test] 27 | public async Task ChannelCommand_Should_Fail_ForIndexOutOfRange() 28 | { 29 | var result = await rootCommand.InvokeAsync("channel disable --index 10 --port SIMPORT", Console); 30 | result.Should().BeGreaterThan(0); 31 | Out.Output.Should().Contain("Channel index is out of range"); 32 | } 33 | 34 | [Test] 35 | public async Task ChannelCommand_Should_Fail_ForEnable_DisablePrimary() 36 | { 37 | var result = await rootCommand.InvokeAsync("channel disable --index 0 --port SIMPORT", Console); 38 | result.Should().BeGreaterThan(0); 39 | Out.Output.Should().Contain("Cannot enable / disable PRIMARY channel"); 40 | result = await rootCommand.InvokeAsync("channel enable --index 0 --port SIMPORT", Console); 41 | result.Should().BeGreaterThan(0); 42 | Out.Output.Should().Contain("Cannot enable / disable PRIMARY channel"); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Meshtastic.Test/Commands/CommandTestBase.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Enums; 2 | using Microsoft.Extensions.Logging; 3 | using System.CommandLine; 4 | 5 | namespace Meshtastic.Test.Commands 6 | { 7 | public class CommandTestBase 8 | { 9 | protected Option portOption = new("--port", ""); 10 | protected Option hostOption = new("--host", ""); 11 | protected Option outputOption = new("--output", ""); 12 | protected Option logLevelOption = new("--log", ""); 13 | protected Option destOption = new("--dest", ""); 14 | protected Option selectDestOption = new("--select-dest", ""); 15 | 16 | protected RootCommand GetRootCommand() 17 | { 18 | var root = new RootCommand("Meshtastic.Cli"); 19 | root.AddGlobalOption(portOption); 20 | root.AddGlobalOption(hostOption); 21 | root.AddGlobalOption(outputOption); 22 | root.AddGlobalOption(logLevelOption); 23 | root.AddGlobalOption(destOption); 24 | root.AddGlobalOption(selectDestOption); 25 | return root; 26 | } 27 | 28 | public FakeStdIOWriter Error { get; private set; } 29 | public FakeStdIOWriter Out { get; private set; } 30 | public FakeConsole Console { get; private set; } 31 | 32 | [SetUp] 33 | public void BaseSetup() 34 | { 35 | Error = new FakeStdIOWriter(); 36 | Out = new FakeStdIOWriter(); 37 | Console = new FakeConsole(Error, Out); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /Meshtastic.Test/Commands/FactoryResetCommandTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Commands; 2 | using System.CommandLine; 3 | 4 | namespace Meshtastic.Test.Commands; 5 | 6 | [TestFixture] 7 | public class FactoryResetCommandTests : CommandTestBase 8 | { 9 | private RootCommand rootCommand; 10 | 11 | [SetUp] 12 | public void Setup() 13 | { 14 | rootCommand = GetRootCommand(); 15 | var channelCommand = new ChannelCommand("factory-reset", "factory-reset description", portOption, hostOption, outputOption, logLevelOption, destOption, selectDestOption); 16 | rootCommand.AddCommand(channelCommand); 17 | } 18 | 19 | [Test] 20 | public async Task FactoryReset_Should_Succeed_ForValidArgs() 21 | { 22 | var result = await rootCommand.InvokeAsync("factory-reset --port SIMPORT", Console); 23 | result.Should().BeGreaterThan(0); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Meshtastic.Test/Commands/FakeConsole.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | using System.CommandLine.IO; 3 | 4 | namespace Meshtastic.Test.Commands 5 | { 6 | public class FakeConsole : IConsole 7 | { 8 | public FakeConsole(FakeStdIOWriter errorStd, FakeStdIOWriter outputStd) 9 | { 10 | ErrorStd = errorStd; 11 | OutputStd = outputStd; 12 | } 13 | 14 | public bool IsOutputRedirected => true; 15 | 16 | public bool IsErrorRedirected => true; 17 | 18 | public bool IsInputRedirected => true; 19 | 20 | public FakeStdIOWriter ErrorStd { get; } 21 | public FakeStdIOWriter OutputStd { get; } 22 | 23 | public IStandardStreamWriter Out => ErrorStd; 24 | 25 | public IStandardStreamWriter Error => OutputStd; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Meshtastic.Test/Commands/FakeStdIOWriter.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine.IO; 2 | 3 | namespace Meshtastic.Test.Commands 4 | { 5 | public class FakeStdIOWriter : IStandardStreamWriter 6 | { 7 | public string Output = String.Empty; 8 | public void Write(string? value) 9 | { 10 | Output += value ?? String.Empty; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Meshtastic.Test/Commands/FileCommandTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Commands; 2 | using System.CommandLine; 3 | 4 | namespace Meshtastic.Test.Commands; 5 | 6 | [TestFixture] 7 | public class FileCommandTests : CommandTestBase 8 | { 9 | private RootCommand rootCommand; 10 | 11 | [SetUp] 12 | public void Setup() 13 | { 14 | rootCommand = GetRootCommand(); 15 | var command = new FileCommand("file", "file description", portOption, hostOption, outputOption, logLevelOption); 16 | rootCommand.AddCommand(command); 17 | } 18 | 19 | [Test] 20 | public async Task FileCommand_Should_Fail_ForEmptyOrNullText() 21 | { 22 | var result = await rootCommand.InvokeAsync("file --port SIMPORT", Console); 23 | result.Should().BeGreaterThan(0); 24 | Out.Output.Should().Contain("Required argument missing for command: 'file'"); 25 | } 26 | 27 | [Test] 28 | public async Task FileCommand_Should_Succeed_ForValidText() 29 | { 30 | var result = await rootCommand.InvokeAsync("file 'Butt.txt' --port SIMPORT", Console); 31 | result.Should().Be(0); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Meshtastic.Test/Commands/FixedPositionCommandTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Commands; 2 | using System.CommandLine; 3 | 4 | namespace Meshtastic.Test.Commands; 5 | 6 | [TestFixture] 7 | public class FixedPositionCommandTests : CommandTestBase 8 | { 9 | private RootCommand rootCommand; 10 | 11 | [SetUp] 12 | public void Setup() 13 | { 14 | rootCommand = GetRootCommand(); 15 | var command = new FixedPositionCommand("fixed-position", "fixed-position description", portOption, hostOption, outputOption, logLevelOption, destOption, selectDestOption); 16 | rootCommand.AddCommand(command); 17 | } 18 | 19 | [Test] 20 | public async Task FixedPositionCommand_Should_Fail_ForInvalidLat() 21 | { 22 | var result = await rootCommand.InvokeAsync("fixed-position -91 -90.023 --port SIMPORT", Console); 23 | result.Should().BeGreaterThan(0); 24 | Out.Output.Should().Contain("Invalid latitude"); 25 | result = await rootCommand.InvokeAsync("fixed-position 91 -90.023 --port SIMPORT", Console); 26 | result.Should().BeGreaterThan(0); 27 | Out.Output.Should().Contain("Invalid latitude"); 28 | } 29 | 30 | [Test] 31 | public async Task FixedPositionCommand_Should_Fail_ForInvalidLon() 32 | { 33 | var result = await rootCommand.InvokeAsync("fixed-position 34.00 -181 --port SIMPORT", Console); 34 | result.Should().BeGreaterThan(0); 35 | Out.Output.Should().Contain("Invalid longitude"); 36 | result = await rootCommand.InvokeAsync("fixed-position 34.00 -181 --port SIMPORT", Console); 37 | result.Should().BeGreaterThan(0); 38 | Out.Output.Should().Contain("Invalid longitude"); 39 | } 40 | 41 | [Test] 42 | public async Task FixedPositionCommand_Should_Succeed_ForValidCoords() 43 | { 44 | var result = await rootCommand.InvokeAsync("fixed-position 34.00 -90 --port SIMPORT", Console); 45 | result.Should().Be(0); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Meshtastic.Test/Commands/GetCommandTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Commands; 2 | using Meshtastic.Cli.Extensions; 3 | using Meshtastic.Protobufs; 4 | using System.CommandLine; 5 | 6 | namespace Meshtastic.Test.Commands; 7 | 8 | [TestFixture] 9 | public class GetCommandTests : CommandTestBase 10 | { 11 | private RootCommand rootCommand; 12 | 13 | [SetUp] 14 | public void Setup() 15 | { 16 | var settingOption = new Option>("--setting", description: "Get or set a value on config / module-config") 17 | { 18 | AllowMultipleArgumentsPerToken = true, 19 | IsRequired = true, 20 | }; 21 | settingOption.AddAlias("-s"); 22 | settingOption.AddCompletions(ctx => new LocalConfig().GetSettingsOptions().Concat(new LocalModuleConfig().GetSettingsOptions())); 23 | rootCommand = GetRootCommand(); 24 | var command = new GetCommand("get", "get description", settingOption, portOption, hostOption, outputOption, logLevelOption, destOption, selectDestOption); 25 | rootCommand.AddCommand(command); 26 | } 27 | 28 | [Test] 29 | public async Task SetCommand_Should_Fail_ForEmptyOrNullSettings() 30 | { 31 | var result = await rootCommand.InvokeAsync("get --port SIMPORT", Console); 32 | result.Should().BeGreaterThan(0); 33 | Out.Output.Should().Contain("Option '--setting' is required"); 34 | } 35 | 36 | [Test] 37 | public async Task GetCommand_Should_Succeed_ForValidSetting() 38 | { 39 | var result = await rootCommand.InvokeAsync("get --setting Mqtt.Address --port SIMPORT", Console); 40 | result.Should().Be(0); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Meshtastic.Test/Commands/InfoCommandTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Commands; 2 | using System.CommandLine; 3 | 4 | namespace Meshtastic.Test.Commands; 5 | 6 | [TestFixture] 7 | public class InfoCommandTests : CommandTestBase 8 | { 9 | private RootCommand rootCommand; 10 | 11 | [SetUp] 12 | public void Setup() 13 | { 14 | rootCommand = GetRootCommand(); 15 | var command = new InfoCommand("info", "info description", portOption, hostOption, outputOption, logLevelOption, destOption, selectDestOption); 16 | rootCommand.AddCommand(command); 17 | } 18 | 19 | [Test] 20 | public async Task InfoCommand_Should_Succeed_ForValidCoords() 21 | { 22 | var result = await rootCommand.InvokeAsync("info --port SIMPORT", Console); 23 | result.Should().Be(0); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Meshtastic.Test/Commands/ListCommandTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Commands; 2 | using System.CommandLine; 3 | 4 | namespace Meshtastic.Test.Commands; 5 | 6 | [TestFixture] 7 | public class ListCommandTests : CommandTestBase 8 | { 9 | private RootCommand rootCommand; 10 | 11 | [SetUp] 12 | public void Setup() 13 | { 14 | rootCommand = GetRootCommand(); 15 | var command = new InfoCommand("list", "list description", portOption, hostOption, outputOption, logLevelOption, destOption, selectDestOption); 16 | rootCommand.AddCommand(command); 17 | } 18 | 19 | [Test] 20 | public async Task ListCommand_Should_Succeed_ForValidCoords() 21 | { 22 | var result = await rootCommand.InvokeAsync("list --port SIMPORT", Console); 23 | result.Should().Be(0); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Meshtastic.Test/Commands/MetadataCommandTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Commands; 2 | using System.CommandLine; 3 | 4 | namespace Meshtastic.Test.Commands; 5 | 6 | [TestFixture] 7 | public class MetadataCommandTests : CommandTestBase 8 | { 9 | private RootCommand rootCommand; 10 | 11 | [SetUp] 12 | public void Setup() 13 | { 14 | rootCommand = GetRootCommand(); 15 | var command = new MetadataCommand("metadata", "metadata description", portOption, hostOption, outputOption, logLevelOption, destOption, selectDestOption); 16 | rootCommand.AddCommand(command); 17 | } 18 | 19 | [Test] 20 | public async Task MetadataCommand_Should_Succeed_ForValidArgs() 21 | { 22 | var result = await rootCommand.InvokeAsync("metadata --port SIMPORT", Console); 23 | result.Should().Be(0); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Meshtastic.Test/Commands/RebootCommandTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Commands; 2 | using System.CommandLine; 3 | 4 | namespace Meshtastic.Test.Commands; 5 | 6 | [TestFixture] 7 | public class RebootCommandTests : CommandTestBase 8 | { 9 | private RootCommand rootCommand; 10 | 11 | [SetUp] 12 | public void Setup() 13 | { 14 | rootCommand = GetRootCommand(); 15 | var command = new RebootCommand("reboot", "reboot description", portOption, hostOption, outputOption, logLevelOption, destOption, selectDestOption); 16 | rootCommand.AddCommand(command); 17 | } 18 | 19 | [Test] 20 | public async Task RebootCommand_Should_Succeed_ForValidArgs() 21 | { 22 | var result = await rootCommand.InvokeAsync("reboot --port SIMPORT", Console); 23 | result.Should().Be(0); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Meshtastic.Test/Commands/ResetNodeDbCommandTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Commands; 2 | using System.CommandLine; 3 | 4 | namespace Meshtastic.Test.Commands; 5 | 6 | [TestFixture] 7 | public class ResetNodeDbCommandTests : CommandTestBase 8 | { 9 | private RootCommand rootCommand; 10 | 11 | [SetUp] 12 | public void Setup() 13 | { 14 | rootCommand = GetRootCommand(); 15 | var command = new ResetNodeDbCommand("reset-nodedb", "Reset Node Db description", portOption, hostOption, outputOption, logLevelOption, destOption, selectDestOption); 16 | rootCommand.AddCommand(command); 17 | } 18 | 19 | [Test] 20 | public async Task ResetNodeDbCommand_Should_Succeed_ForValidArgs() 21 | { 22 | var result = await rootCommand.InvokeAsync("reset-nodedb --port SIMPORT", Console); 23 | result.Should().Be(0); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Meshtastic.Test/Commands/SendTextCommandTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Commands; 2 | using System.CommandLine; 3 | 4 | namespace Meshtastic.Test.Commands; 5 | 6 | [TestFixture] 7 | public class SendTextCommandTests : CommandTestBase 8 | { 9 | private RootCommand rootCommand; 10 | 11 | [SetUp] 12 | public void Setup() 13 | { 14 | rootCommand = GetRootCommand(); 15 | var command = new SendTextCommand("text", "text description", portOption, hostOption, outputOption, logLevelOption, destOption, selectDestOption); 16 | rootCommand.AddCommand(command); 17 | } 18 | 19 | [Test] 20 | public async Task SendTextCommand_Should_Fail_ForEmptyOrNullText() 21 | { 22 | var result = await rootCommand.InvokeAsync("text --port SIMPORT", Console); 23 | result.Should().BeGreaterThan(0); 24 | Out.Output.Should().Contain("Required argument missing for command: 'text'"); 25 | } 26 | 27 | [Test] 28 | public async Task SendTextCommand_Should_Succeed_ForValidText() 29 | { 30 | var result = await rootCommand.InvokeAsync("text 'Butt' --port SIMPORT", Console); 31 | result.Should().Be(0); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Meshtastic.Test/Commands/SendWaypointCommandTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Commands; 2 | using System.CommandLine; 3 | 4 | namespace Meshtastic.Test.Commands; 5 | 6 | [TestFixture] 7 | public class SendWaypointCommandTests : CommandTestBase 8 | { 9 | private RootCommand rootCommand; 10 | 11 | [SetUp] 12 | public void Setup() 13 | { 14 | rootCommand = GetRootCommand(); 15 | var command = new SendWaypointCommand("waypoint", "waypoint description", portOption, hostOption, outputOption, logLevelOption, destOption, selectDestOption); 16 | rootCommand.AddCommand(command); 17 | } 18 | 19 | [Test] 20 | public async Task SendWaypointCommand_Should_Fail_ForInvalidLat() 21 | { 22 | var result = await rootCommand.InvokeAsync("waypoint -91 -90.023 --port SIMPORT", Console); 23 | result.Should().BeGreaterThan(0); 24 | Out.Output.Should().Contain("Invalid latitude"); 25 | result = await rootCommand.InvokeAsync("waypoint 91 -90.023 --port SIMPORT", Console); 26 | result.Should().BeGreaterThan(0); 27 | Out.Output.Should().Contain("Invalid latitude"); 28 | } 29 | 30 | [Test] 31 | public async Task SendWaypointCommand_Should_Fail_ForInvalidLon() 32 | { 33 | var result = await rootCommand.InvokeAsync("waypoint 34.00 -181 --port SIMPORT", Console); 34 | result.Should().BeGreaterThan(0); 35 | Out.Output.Should().Contain("Invalid longitude"); 36 | result = await rootCommand.InvokeAsync("waypoint 34.00 -181 --port SIMPORT", Console); 37 | result.Should().BeGreaterThan(0); 38 | Out.Output.Should().Contain("Invalid longitude"); 39 | } 40 | 41 | [Test] 42 | public async Task SendWaypointCommand_Should_Succeed_ForValidCoords() 43 | { 44 | var result = await rootCommand.InvokeAsync("waypoint 34.00 -90 --port SIMPORT", Console); 45 | result.Should().Be(0); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Meshtastic.Test/Commands/SetCommandTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Commands; 2 | using Meshtastic.Cli.Extensions; 3 | using Meshtastic.Protobufs; 4 | using System.CommandLine; 5 | 6 | namespace Meshtastic.Test.Commands; 7 | 8 | [TestFixture] 9 | public class SetCommandTests : CommandTestBase 10 | { 11 | private RootCommand rootCommand; 12 | 13 | [SetUp] 14 | public void Setup() 15 | { 16 | var settingOption = new Option>("--setting", description: "Get or set a value on config / module-config") 17 | { 18 | AllowMultipleArgumentsPerToken = true, 19 | IsRequired = true, 20 | }; 21 | settingOption.AddAlias("-s"); 22 | settingOption.AddCompletions(ctx => new LocalConfig().GetSettingsOptions().Concat(new LocalModuleConfig().GetSettingsOptions())); 23 | rootCommand = GetRootCommand(); 24 | var command = new SetCommand("set", "set description", settingOption, portOption, hostOption, outputOption, logLevelOption, destOption, selectDestOption); 25 | rootCommand.AddCommand(command); 26 | } 27 | 28 | [Test] 29 | public async Task SetCommand_Should_Fail_ForEmptyOrNullSettings() 30 | { 31 | var result = await rootCommand.InvokeAsync("set --port SIMPORT", Console); 32 | result.Should().BeGreaterThan(0); 33 | Out.Output.Should().Contain("Option '--setting' is required"); 34 | } 35 | 36 | [Test] 37 | public async Task SetCommand_Should_Succeed_ForValidSetting() 38 | { 39 | var result = await rootCommand.InvokeAsync("set --setting Mqtt.Address=yourmom.com --port SIMPORT", Console); 40 | result.Should().Be(0); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Meshtastic.Test/Commands/TraceRouteCommandTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Commands; 2 | using System.CommandLine; 3 | 4 | namespace Meshtastic.Test.Commands; 5 | 6 | [TestFixture] 7 | public class TraceRouteCommandTests : CommandTestBase 8 | { 9 | private RootCommand rootCommand; 10 | 11 | [SetUp] 12 | public void Setup() 13 | { 14 | rootCommand = GetRootCommand(); 15 | var command = new TraceRouteCommand("traceroute", "Traceroute description", portOption, hostOption, outputOption, logLevelOption, destOption, selectDestOption); 16 | rootCommand.AddCommand(command); 17 | } 18 | 19 | [Test] 20 | public async Task TraceRouteCommand_Should_Succeed_ForValidArgs() 21 | { 22 | var result = await rootCommand.InvokeAsync("traceroute --port SIMPORT", Console); 23 | result.Should().Be(0); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Meshtastic.Test/Commands/UrlCommandTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Commands; 2 | using System.CommandLine; 3 | 4 | namespace Meshtastic.Test.Commands; 5 | 6 | [TestFixture] 7 | public class UrlCommandTests : CommandTestBase 8 | { 9 | private RootCommand rootCommand; 10 | 11 | [SetUp] 12 | public void Setup() 13 | { 14 | rootCommand = GetRootCommand(); 15 | var command = new UrlCommand("url", "url description", portOption, hostOption, outputOption, logLevelOption); 16 | rootCommand.AddCommand(command); 17 | } 18 | 19 | [Test] 20 | public async Task UrlCommand_Should_Succeed_ForValidSetArgs() 21 | { 22 | var result = await rootCommand.InvokeAsync("url get --port SIMPORT", Console); 23 | result.Should().Be(0); 24 | } 25 | 26 | [Test] 27 | public async Task UrlCommand_Should_Succeed_ForValidGetArgs() 28 | { 29 | var result = await rootCommand.InvokeAsync("url set http://meshtastic.org/#e/1235 --port SIMPORT", Console); 30 | result.Should().Be(0); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Meshtastic.Test/Crypto/PacketCryptoTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Crypto; 2 | using Meshtastic.Protobufs; 3 | 4 | namespace Meshtastic.Test.Crypto; 5 | 6 | public class PacketCryptoTests 7 | { 8 | [SetUp] 9 | public void Setup() 10 | { 11 | } 12 | 13 | [Test] 14 | public void TestDefaultKeyDecrypt() 15 | { 16 | //{ "packet": { "from": 4202784164, "to": 4294967295, "channel": 8, "encrypted": "kiDV39nDDsi8AON+Czei6zUpy+F/7E+lyIpicxJR40KXBFmPkqFUEnobI5voQadha+s=", "id": 1777428186, "hopLimit": 3, "priority": "BACKGROUND", "hopStart": 3 }, "channelId": "LongFast", "gatewayId": "!fa8165a4" } 17 | var nonce = new NonceGenerator(4202784164, 1777428186).Create(); 18 | 19 | var decrypted = PacketEncryption.TransformPacket(Convert.FromBase64String("kiDV39nDDsi8AON+Czei6zUpy+F/7E+lyIpicxJR40KXBFmPkqFUEnobI5voQadha+s="), nonce, Resources.DEFAULT_PSK); 20 | var testMessage = Meshtastic.Protobufs.Data.Parser.ParseFrom(decrypted); 21 | testMessage.Portnum.Should().Be(PortNum.NodeinfoApp); 22 | var nodeInfo = User.Parser.ParseFrom(testMessage.Payload); 23 | nodeInfo.LongName.Should().Be("Meshtastic 65a4"); 24 | } 25 | } -------------------------------------------------------------------------------- /Meshtastic.Test/Data/FakeLogger.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace Meshtastic.Test.Data; 4 | 5 | public class FakeLogger : ILogger 6 | { 7 | public IDisposable? BeginScope(TState state) where TState : notnull 8 | { 9 | return null; 10 | } 11 | 12 | public bool IsEnabled(LogLevel logLevel) 13 | { 14 | return true; 15 | } 16 | 17 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) 18 | { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Meshtastic.Test/Data/FromDeviceMessageTests.cs: -------------------------------------------------------------------------------- 1 | using Google.Protobuf; 2 | using Meshtastic.Extensions; 3 | using Meshtastic.Protobufs; 4 | 5 | namespace Meshtastic.Test.Data; 6 | public class FromDeviceMessageTests 7 | { 8 | private FromDeviceMessage fromDeviceMessage; 9 | 10 | 11 | [SetUp] 12 | public void Setup() 13 | { 14 | fromDeviceMessage = new FromDeviceMessage(new FakeLogger()); 15 | } 16 | 17 | [Test] 18 | public void FromDeviceMessage_SwallowsException_Given_BadPayload() 19 | { 20 | var action = () => 21 | { 22 | fromDeviceMessage.ParsedFromRadio(BitConverter.GetBytes(123456)); 23 | }; 24 | action.Should().NotThrow(); 25 | } 26 | 27 | [Test] 28 | public void FromDeviceMessage_ReturnsNull_Given_NoVariantPayload() 29 | { 30 | var fromRadio = new FromRadio() { }; 31 | var message = fromDeviceMessage.ParsedFromRadio(fromRadio.ToByteArray()); 32 | message.Should().BeNull(); 33 | } 34 | 35 | [Test] 36 | public void FromDeviceMessage_GivesResult_Given_ValidFromRadioPayload() 37 | { 38 | var fromRadio = new FromRadio() 39 | { 40 | Packet = new MeshPacket() 41 | { 42 | Decoded = new Protobufs.Data() 43 | { 44 | Portnum = PortNum.AdminApp, 45 | Payload = new AdminMessage() { BeginEditSettings = true }.ToByteString(), 46 | } 47 | } 48 | }; 49 | var result = fromDeviceMessage.ParsedFromRadio(fromRadio.ToByteArray()); 50 | result!.GetPayload()!.BeginEditSettings.Should().BeTrue(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Meshtastic.Test/Data/PositionMessageFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Data.MessageFactories; 2 | using Meshtastic.Protobufs; 3 | using static Meshtastic.Protobufs.Config.Types; 4 | 5 | namespace Meshtastic.Test.Data; 6 | 7 | [TestFixture] 8 | public class PositionMessageFactoryTests 9 | { 10 | private DeviceStateContainer deviceStateContainer; 11 | private PositionMessageFactory factory; 12 | 13 | [SetUp] 14 | public void Setup() 15 | { 16 | deviceStateContainer = new DeviceStateContainer(); 17 | deviceStateContainer.MyNodeInfo.MyNodeNum = 100; 18 | deviceStateContainer.LocalConfig = new LocalConfig 19 | { 20 | Lora = new LoRaConfig() 21 | { 22 | HopLimit = 3, 23 | } 24 | }; 25 | factory = new PositionMessageFactory(deviceStateContainer); 26 | } 27 | 28 | [Test] 29 | public void CreatePositionPacket_Should_ReturnValidAdminMessage() 30 | { 31 | var result = factory.CreatePositionPacket(new Position()); 32 | result.Decoded.Portnum.Should().Be(PortNum.PositionApp); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Meshtastic.Test/Data/TextMessageFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Data.MessageFactories; 2 | using Meshtastic.Protobufs; 3 | using static Meshtastic.Protobufs.Config.Types; 4 | 5 | namespace Meshtastic.Test.Data; 6 | 7 | [TestFixture] 8 | public class TextMessageFactoryTests 9 | { 10 | private DeviceStateContainer deviceStateContainer; 11 | private TextMessageFactory factory; 12 | 13 | [SetUp] 14 | public void Setup() 15 | { 16 | deviceStateContainer = new DeviceStateContainer(); 17 | deviceStateContainer.MyNodeInfo.MyNodeNum = 100; 18 | deviceStateContainer.LocalConfig = new LocalConfig 19 | { 20 | Lora = new LoRaConfig() 21 | { 22 | HopLimit = 3, 23 | } 24 | }; 25 | factory = new TextMessageFactory(deviceStateContainer); 26 | } 27 | 28 | [Test] 29 | public void CreateTextMessagePacket_Should_ReturnValidAdminMessage() 30 | { 31 | var result = factory.CreateTextMessagePacket("Text"); 32 | result.Decoded.Portnum.Should().Be(PortNum.TextMessageApp); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Meshtastic.Test/Data/ToRadioFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Data.MessageFactories; 2 | using Meshtastic.Protobufs; 3 | 4 | namespace Meshtastic.Test.Data; 5 | 6 | [TestFixture] 7 | public class ToRadioMessageFactoryTests 8 | { 9 | private ToRadioMessageFactory factory; 10 | 11 | [SetUp] 12 | public void Setup() 13 | { 14 | factory = new ToRadioMessageFactory(); 15 | } 16 | 17 | [Test] 18 | public void CreateMeshPacketMessage_Should_ReturnValidMeshPacket() 19 | { 20 | var result = factory.CreateMeshPacketMessage(new MeshPacket() 21 | { 22 | }); 23 | result.Packet.Should().NotBeNull(); 24 | } 25 | 26 | [Test] 27 | public void CreateWantConfigMessage_Should_ReturnValidWantConfig() 28 | { 29 | var result = factory.CreateWantConfigMessage(); 30 | result.WantConfigId.Should().BeGreaterThan(0); 31 | } 32 | 33 | [Test] 34 | public void CreateXmodemPacketMessage_Should_ReturnValidXModemMessage() 35 | { 36 | var result = factory.CreateXmodemPacketMessage(); 37 | result.XmodemPacket.Should().NotBeNull(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Meshtastic.Test/Data/TraceRouteMessageFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Data.MessageFactories; 2 | using Meshtastic.Protobufs; 3 | using static Meshtastic.Protobufs.Config.Types; 4 | 5 | namespace Meshtastic.Test.Data; 6 | 7 | [TestFixture] 8 | public class TraceRouteMessageFactoryTests 9 | { 10 | private DeviceStateContainer deviceStateContainer; 11 | private TraceRouteMessageFactory factory; 12 | 13 | [SetUp] 14 | public void Setup() 15 | { 16 | deviceStateContainer = new DeviceStateContainer(); 17 | deviceStateContainer.MyNodeInfo.MyNodeNum = 100; 18 | deviceStateContainer.LocalConfig = new LocalConfig 19 | { 20 | Lora = new LoRaConfig() 21 | { 22 | HopLimit = 3, 23 | } 24 | }; 25 | factory = new TraceRouteMessageFactory(deviceStateContainer); 26 | } 27 | 28 | [Test] 29 | public void CreateRouteDiscoveryPacket_Should_ReturnValidAdminMessage() 30 | { 31 | factory = new TraceRouteMessageFactory(deviceStateContainer, 100); 32 | var result = factory.CreateRouteDiscoveryPacket(); 33 | result.Decoded.Portnum.Should().Be(PortNum.TracerouteApp); 34 | } 35 | 36 | [Test] 37 | public void CreateRouteDiscoveryPacket_Should_ThrowNotNullException() 38 | { 39 | factory = new TraceRouteMessageFactory(deviceStateContainer); 40 | var action = () => factory.CreateRouteDiscoveryPacket(); 41 | action.Should().Throw(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Meshtastic.Test/Data/WaypointMessageFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Data.MessageFactories; 2 | using Meshtastic.Protobufs; 3 | using static Meshtastic.Protobufs.Config.Types; 4 | 5 | namespace Meshtastic.Test.Data; 6 | 7 | [TestFixture] 8 | public class WaypointMessageFactoryTests 9 | { 10 | private DeviceStateContainer deviceStateContainer; 11 | private WaypointMessageFactory factory; 12 | 13 | [SetUp] 14 | public void Setup() 15 | { 16 | deviceStateContainer = new DeviceStateContainer(); 17 | deviceStateContainer.MyNodeInfo.MyNodeNum = 100; 18 | deviceStateContainer.LocalConfig = new LocalConfig 19 | { 20 | Lora = new LoRaConfig() 21 | { 22 | HopLimit = 3, 23 | } 24 | }; 25 | factory = new WaypointMessageFactory(deviceStateContainer); 26 | } 27 | 28 | [Test] 29 | public void CreateWaypointPacket_Should_ReturnValidAdminMessage() 30 | { 31 | var result = factory.CreateWaypointPacket(new Waypoint()); 32 | result.Decoded.Portnum.Should().Be(PortNum.WaypointApp); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Meshtastic.Test/Extensions/DateTimeExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Extensions; 2 | 3 | namespace Meshtastic.Test.Extensions; 4 | 5 | [TestFixture] 6 | public class DateTimeExtensionsTests 7 | { 8 | [Test] 9 | public void GetUnixTimestamp_Should_ReturnEpoch() 10 | { 11 | var zeroEpoch = new DateTime(1970, 1, 1); 12 | zeroEpoch.GetUnixTimestamp().Should().Be(0); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Meshtastic.Test/Extensions/DisplayExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Extensions; 2 | using Meshtastic.Protobufs; 3 | 4 | namespace Meshtastic.Test.Extensions 5 | { 6 | [TestFixture] 7 | public class DisplayExtensionsTests 8 | { 9 | [Test] 10 | public void ToDisplayString_Should_ReturnNotAvailable_GivenNullPosition() 11 | { 12 | #pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. 13 | Position position = null; 14 | #pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. 15 | #pragma warning disable CS8604 // Possible null reference argument. 16 | var result = position.ToDisplayString(); 17 | #pragma warning restore CS8604 // Possible null reference argument. 18 | result.Should().Be("Not available"); 19 | } 20 | 21 | [Test] 22 | public void ToDisplayString_Should_ReturnNotAvailable_GivenZeroLatAndLongPosition() 23 | { 24 | Position position = new() 25 | { 26 | LatitudeI = 0, 27 | LongitudeI = 0, 28 | }; 29 | var result = position.ToDisplayString(); 30 | result.Should().Be("Not available"); 31 | } 32 | 33 | [Test] 34 | public void ToDisplayString_Should_ReturnFormattedCoordinates_GivenValidLatAndLongPosition() 35 | { 36 | Position position = new() 37 | { 38 | LatitudeI = 341234567, 39 | LongitudeI = -921234567, 40 | }; 41 | var result = position.ToDisplayString(); 42 | result.Should().Be("34.1234567, -92.1234567"); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Meshtastic.Test/Extensions/FromRadioExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using Google.Protobuf; 2 | using Meshtastic.Extensions; 3 | using Meshtastic.Protobufs; 4 | 5 | namespace Meshtastic.Test.Extensions 6 | { 7 | [TestFixture] 8 | public class FromRadioExtensionsTests 9 | { 10 | [Test] 11 | public void GetMessage_Should_ReturnNullForBadPackets() 12 | { 13 | FromRadio fromRadio = new(); 14 | var result = fromRadio.GetPayload(); 15 | result.Should().BeNull(); 16 | fromRadio.Packet = new MeshPacket(); 17 | result = fromRadio.GetPayload(); 18 | result.Should().BeNull(); 19 | fromRadio.Packet = new MeshPacket 20 | { 21 | Decoded = new Protobufs.Data() 22 | }; 23 | result = fromRadio.GetPayload(); 24 | result.Should().BeNull(); 25 | } 26 | 27 | [Test] 28 | public void GetMessage_Should_ReturnValidAdminMessage() 29 | { 30 | FromRadio fromRadio = new(); 31 | var result = fromRadio.GetPayload(); 32 | fromRadio.Packet = new MeshPacket 33 | { 34 | Decoded = new Protobufs.Data() 35 | { 36 | Portnum = PortNum.AdminApp, 37 | Payload = new AdminMessage().ToByteString() 38 | } 39 | }; 40 | result = fromRadio.GetPayload(); 41 | result.Should().NotBeNull(); 42 | result.Should().BeOfType(); 43 | } 44 | 45 | [Test] 46 | public void GetMessage_Should_ReturnValidRouteDiscovery() 47 | { 48 | FromRadio fromRadio = new(); 49 | var result = fromRadio.GetPayload(); 50 | fromRadio.Packet = new MeshPacket 51 | { 52 | Decoded = new Protobufs.Data() 53 | { 54 | Portnum = PortNum.TracerouteApp, 55 | Payload = new RouteDiscovery().ToByteString() 56 | } 57 | }; 58 | result = fromRadio.GetPayload(); 59 | result.Should().NotBeNull(); 60 | result.Should().BeOfType(); 61 | } 62 | 63 | [Test] 64 | public void GetMessage_Should_ReturnValidRouting() 65 | { 66 | FromRadio fromRadio = new(); 67 | var result = fromRadio.GetPayload(); 68 | fromRadio.Packet = new MeshPacket 69 | { 70 | Decoded = new Protobufs.Data() 71 | { 72 | Portnum = PortNum.RoutingApp, 73 | Payload = new Routing().ToByteString() 74 | } 75 | }; 76 | result = fromRadio.GetPayload(); 77 | result.Should().NotBeNull(); 78 | result.Should().BeOfType(); 79 | } 80 | 81 | 82 | [Test] 83 | public void GetMessage_Should_ReturnValidXModemPacket() 84 | { 85 | FromRadio fromRadio = new() 86 | { 87 | XmodemPacket = new XModem() 88 | { 89 | Control = XModem.Types.Control.Stx 90 | } 91 | }; 92 | var result = fromRadio.GetPayload(); 93 | result.Should().NotBeNull(); 94 | result.Should().BeOfType(); 95 | result!.Control.Should().Be(XModem.Types.Control.Stx); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Meshtastic.Test/Extensions/ReflectionExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Extensions; 2 | using Meshtastic.Protobufs; 3 | using static Meshtastic.Protobufs.Config.Types; 4 | 5 | namespace Meshtastic.Test.Extensions 6 | { 7 | [TestFixture] 8 | public class ReflectionExtensionsTests 9 | { 10 | [Test] 11 | public void GetSettingsOptions_Should_ThrowException_GivenNullArg() 12 | { 13 | #pragma warning disable CS8631 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match constraint type. 14 | var action = () => default(LocalConfig).GetSettingsOptions(); 15 | #pragma warning restore CS8631 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match constraint type. 16 | action.Should().Throw(); 17 | } 18 | 19 | [Test] 20 | public void GetSettingsOptions_Should_EnumerateConfigSettings() 21 | { 22 | var list = new LocalConfig().GetSettingsOptions(); 23 | list.Should().Contain("Device.Role"); 24 | } 25 | 26 | [Test] 27 | public void GetSettingsOptions_Should_EnumerateModuleConfigSettings() 28 | { 29 | var list = new LocalModuleConfig().GetSettingsOptions(); 30 | list.Should().Contain("Mqtt.Address"); 31 | } 32 | 33 | [Test] 34 | public void GetProperties_Should_ReturnPropertyOnConfigSection() 35 | { 36 | var list = new LoRaConfig().GetProperties(); 37 | list.Should().Contain(p => p.Name == "TxPower"); 38 | } 39 | 40 | [Test] 41 | public void GetSettingValue_Should_ReturnPropertyValueOnConfigSection() 42 | { 43 | var lora = new LoRaConfig() { TxPower = 100 }; 44 | var property = lora.GetProperties().First(p => p.Name == "TxPower"); 45 | property.GetSettingValue(lora).Should().BeEquivalentTo(lora.TxPower.ToString()); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Meshtastic.Test/Meshtastic.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net9.0 4 | enable 5 | enable 6 | 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | all 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | 27 | 28 | all 29 | runtime; build; native; contentfiles; analyzers; buildtransitive 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /Meshtastic.Test/Parsers/UrlParserTests.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography.X509Certificates; 2 | using Google.Protobuf; 3 | using Meshtastic.Cli.Parsers; 4 | using Meshtastic.Protobufs; 5 | 6 | namespace Meshtastic.Test.Parsers; 7 | 8 | public class UrlParserTests 9 | { 10 | [SetUp] 11 | public void Setup() 12 | { 13 | } 14 | 15 | [Test] 16 | public void Parse_Should_ReturnChannelSet_GivenValidMeshtasticUrl() 17 | { 18 | var url = "https://meshtastic.org/e/#CjMSIK4kboySjOqKWHn8dCR2Gp7L0syIGv1_sySCrbZxneV2GgtCZW5zRnVuTGFuZCgBMAEKKRIgarYveKnCBHSGrOMkzVVStMEElngYZQir38xCiDKkj6UaBWFkbWluCisSIExfvRlaRwJWHDEvQYaUjzwUeN4FvkI_6nJX9P0ByHCOGgdQYXJ0eU9uEgoIATgBQANIAVAe"; 19 | var parser = new UrlParser(url); 20 | var result = parser.ParseChannels(); 21 | result.Settings[0].Name.Should().Be("BensFunLand"); 22 | } 23 | 24 | [Test] 25 | public void Parse_Should_ReturnContact_GivenValidMeshtasticUrl() 26 | { 27 | var contact = new SharedContact 28 | { 29 | NodeNum = 123456, 30 | User = new User 31 | { 32 | Id = $"!{Convert.ToHexString(BitConverter.GetBytes(123456))}", 33 | LongName = "Benben", 34 | ShortName = "B", 35 | PublicKey = ByteString.CopyFrom( 36 | [ 37 | 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 38 | 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 39 | 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 40 | 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20 41 | ]), 42 | } 43 | }; 44 | 45 | var serialized = contact.ToByteArray(); 46 | var base64 = Convert.ToBase64String(serialized); 47 | base64 = base64.Replace("-", String.Empty).Replace('+', '-').Replace('/', '_'); 48 | 49 | var url = $"https://meshtastic.org/v/#{base64}"; 50 | var parser = new UrlParser(url); 51 | var result = parser.ParseContact(); 52 | result.User.LongName.Should().Be("Benben"); 53 | } 54 | 55 | [Test] 56 | public void Should_ThrowException_GivenNoMeshtasticUrl() 57 | { 58 | #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. 59 | var action = () => new UrlParser(null); 60 | #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. 61 | action.Should().Throw(); 62 | } 63 | 64 | [Test] 65 | public void Should_ThrowException_GivenEmptyMeshtasticUrl() 66 | { 67 | var action = () => new UrlParser(String.Empty); 68 | action.Should().Throw(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Meshtastic.Test/Reflection/ReflectionExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Extensions; 2 | using Meshtastic.Protobufs; 3 | 4 | namespace Meshtastic.Test.Parsers; 5 | 6 | public class ReflectionExtensionsTests 7 | { 8 | [SetUp] 9 | public void Setup() 10 | { 11 | } 12 | 13 | [Test] 14 | public void ParseSettings_Should_ReturnResultWithPropertyInfo_GivenValidModuleConfigArgs_Get() 15 | { 16 | var configs = new LocalConfig().GetSettingsOptions(); 17 | 18 | configs.Should().Contain("Display.ScreenOnSecs"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Meshtastic.Test/TestCategories.cs: -------------------------------------------------------------------------------- 1 | namespace Meshtastic.Test 2 | { 3 | internal static class TestCategories 4 | { 5 | public const string SimulatedDeviceTests = "SimulatedDeviceTests"; 6 | public const string FullMeshSimulationTests = "FullMeshSimulationTests"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Meshtastic.Test/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Moq; 2 | global using FluentAssertions; 3 | global using Meshtastic.Data; 4 | global using NUnit.Framework; 5 | -------------------------------------------------------------------------------- /Meshtastic.Test/Utilities/FirmwarePackageServiceTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Utilities; 2 | 3 | namespace Meshtastic.Test.Utilities 4 | { 5 | [TestFixture] 6 | public class FirmwarePackageServiceTests 7 | { 8 | [Test] 9 | public async Task GetLast5Releases_Should_Return5Records() 10 | { 11 | var github = new FirmwarePackageService(); 12 | var releases = await github.GetFirmwareReleases(); 13 | releases.releases.stable.Should().HaveCountGreaterThan(0); 14 | releases.releases.stable.Should().HaveCountGreaterThan(0); 15 | } 16 | 17 | [Test] 18 | public async Task DownloadRelease_Should_ReturnMemoryStream() 19 | { 20 | var github = new FirmwarePackageService(); 21 | var releases = await github.GetFirmwareReleases(); 22 | var memoryStream = await github.DownloadRelease(releases.releases.alpha.First()); 23 | memoryStream.Length.Should().BeGreaterThan(0); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Meshtastic.Test/Utilities/ReleaseZipServiceTests.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Cli.Utilities; 2 | using Meshtastic.Protobufs; 3 | 4 | namespace Meshtastic.Test.Utilities 5 | { 6 | [TestFixture] 7 | public class ReleaseZipServiceTests 8 | { 9 | [Test] 10 | public async Task ExtractBinaries_Should_DownloadUf2_ForLatestRakRelease() 11 | { 12 | var service = new ReleaseZipService(); 13 | var github = new FirmwarePackageService(); 14 | var releases = await github.GetFirmwareReleases(); 15 | var memoryStream = await github.DownloadRelease(releases.releases.stable.First()); 16 | var path = await service.ExtractUpdateBinary(memoryStream, HardwareModel.Rak4631); 17 | path.Should().EndWith(".uf2"); 18 | File.Delete(path); 19 | } 20 | 21 | [Test] 22 | public async Task ExtractBinaries_Should_DownloadUpdateBin_ForLatestEsp32Release() 23 | { 24 | var service = new ReleaseZipService(); 25 | var github = new FirmwarePackageService(); 26 | var releases = await github.GetFirmwareReleases(); 27 | var memoryStream = await github.DownloadRelease(releases.releases.stable.First()); 28 | var path = await service.ExtractUpdateBinary(memoryStream, HardwareModel.Tbeam); 29 | path.Should().EndWith("update.bin"); 30 | File.Delete(path); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Meshtastic.Test/coverlet.runsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | json,cobertura,lcov,opencover 8 | [*]Meshtastic.Protobufs.*,[*]Meshtastic.Cli.Logging*,[*]Meshtastic.Connections.* 9 | Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverage 10 | true 11 | MissingAll,MissingAny,None 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Meshtastic.Virtual.Service/Dockerfile: -------------------------------------------------------------------------------- 1 | # See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | # This stage is used when running from VS in fast mode (Default for Debug configuration) 4 | FROM mcr.microsoft.com/dotnet/runtime:9.0 AS base 5 | USER $APP_UID 6 | WORKDIR /app 7 | 8 | 9 | # This stage is used to build the service project 10 | FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build 11 | ARG BUILD_CONFIGURATION=Release 12 | WORKDIR /src 13 | COPY ["Directory.Packages.props", "."] 14 | COPY ["Meshtastic.Virtual.Service/Meshtastic.Virtual.Service.csproj", "Meshtastic.Virtual.Service/"] 15 | RUN dotnet restore "./Meshtastic.Virtual.Service/Meshtastic.Virtual.Service.csproj" 16 | COPY . . 17 | WORKDIR "/src/Meshtastic.Virtual.Service" 18 | RUN dotnet build "./Meshtastic.Virtual.Service.csproj" -c $BUILD_CONFIGURATION -o /app/build 19 | 20 | # This stage is used to publish the service project to be copied to the final stage 21 | FROM build AS publish 22 | ARG BUILD_CONFIGURATION=Release 23 | RUN dotnet publish "./Meshtastic.Virtual.Service.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false 24 | 25 | # This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration) 26 | FROM base AS final 27 | WORKDIR /app 28 | COPY --from=publish /app/publish . 29 | ENTRYPOINT ["dotnet", "Meshtastic.Virtual.Service.dll"] -------------------------------------------------------------------------------- /Meshtastic.Virtual.Service/Meshtastic.Virtual.Service.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | dotnet-Meshtastic.Virtual.Service-f04ea7a8-15a0-4fec-b79a-3106e594e1a5 8 | Linux 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Meshtastic.Virtual.Service/Network/InterfaceUtility.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | using System.Net.NetworkInformation; 4 | 5 | namespace Meshtastic.Virtual.Service.Network; 6 | 7 | public static class InterfaceUtility 8 | { 9 | public static PhysicalAddress GetMacAddress() 10 | { 11 | return NetworkInterface 12 | .GetAllNetworkInterfaces() 13 | .Where(nic => nic.OperationalStatus == OperationalStatus.Up && nic.NetworkInterfaceType != NetworkInterfaceType.Loopback) 14 | .Select(nic => nic.GetPhysicalAddress()) 15 | .First(); 16 | } 17 | } -------------------------------------------------------------------------------- /Meshtastic.Virtual.Service/Persistance/FilePaths.cs: -------------------------------------------------------------------------------- 1 | namespace Meshtastic.Virtual.Service.Persistance; 2 | 3 | public static class FilePaths 4 | { 5 | public const string NODE_FILE = "node.proto"; 6 | public const string MYNODEINFO_FILE = "mynodeinfo.proto"; 7 | public const string CHANNELS_FILE = "channels.proto"; 8 | public const string CONFIG_FILE = "config.proto"; 9 | public const string MODULE_FILE = "module.store"; 10 | } -------------------------------------------------------------------------------- /Meshtastic.Virtual.Service/Persistance/FilePersistance.cs: -------------------------------------------------------------------------------- 1 | using static System.Environment; 2 | 3 | namespace Meshtastic.Virtual.Service.Persistance; 4 | 5 | public class FilePersistance(ILogger logger) : IFilePersistance 6 | { 7 | private static readonly string basePath = Path.Combine(GetFolderPath(SpecialFolder.LocalApplicationData, SpecialFolderOption.DoNotVerify), "meshtastic"); 8 | 9 | public async Task Save(string fileName, byte[] data) 10 | { 11 | try 12 | { 13 | // Ensure the directory and all its parents exist. 14 | Directory.CreateDirectory(basePath); 15 | await File.WriteAllBytesAsync(fileName, data); 16 | return true; 17 | } 18 | catch (Exception ex) 19 | { 20 | logger.LogError(ex, "Error saving file {FileName}", fileName); 21 | return false; 22 | } 23 | } 24 | 25 | public async Task Load(string fileName) 26 | { 27 | return await File.ReadAllBytesAsync(fileName); 28 | } 29 | 30 | public async Task Exists(string fileName) 31 | { 32 | return await Task.FromResult(File.Exists(fileName)); 33 | } 34 | 35 | public async Task Delete(string fileName) 36 | { 37 | try 38 | { 39 | if (File.Exists(fileName)) 40 | { 41 | File.Delete(fileName); 42 | return true; 43 | } 44 | return await Task.FromResult(false); 45 | } 46 | catch (Exception ex) 47 | { 48 | logger.LogError(ex, "Error deleting file {FileName}", fileName); 49 | return false; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /Meshtastic.Virtual.Service/Persistance/IFilePersistance.cs: -------------------------------------------------------------------------------- 1 | namespace Meshtastic.Virtual.Service.Persistance; 2 | 3 | public interface IFilePersistance 4 | { 5 | Task Delete(string fileName); 6 | Task Exists(string fileName); 7 | Task Load(string fileName); 8 | Task Save(string fileName, byte[] data); 9 | } 10 | -------------------------------------------------------------------------------- /Meshtastic.Virtual.Service/Persistance/IVirtualStore.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Protobufs; 2 | 3 | namespace Meshtastic.Virtual.Service.Persistance; 4 | 5 | public interface IVirtualStore 6 | { 7 | Task Load(); 8 | Task Save(); 9 | 10 | NodeInfo Node { get; } 11 | LocalConfig LocalConfig { get; } 12 | LocalModuleConfig LocalModuleConfig { get; } 13 | List Channels { get; } 14 | MyNodeInfo MyNodeInfo { get; } 15 | List Nodes { get; } 16 | } 17 | -------------------------------------------------------------------------------- /Meshtastic.Virtual.Service/Program.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Virtual.Service; 2 | using Meshtastic.Virtual.Service.Persistance; 3 | 4 | var builder = Host.CreateApplicationBuilder(args); 5 | builder.Services.AddLogging(); 6 | builder.Services.AddSingleton(); 7 | builder.Services.AddSingleton(); 8 | builder.Services.AddHostedService(); 9 | 10 | var host = builder.Build(); 11 | host.Run(); 12 | -------------------------------------------------------------------------------- /Meshtastic.Virtual.Service/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Meshtastic.Virtual.Service": { 4 | "commandName": "Project", 5 | "environmentVariables": { 6 | "DOTNET_ENVIRONMENT": "Development" 7 | }, 8 | "dotnetRunMessages": true 9 | }, 10 | "Container (Dockerfile)": { 11 | "commandName": "Docker" 12 | } 13 | }, 14 | "$schema": "https://json.schemastore.org/launchsettings.json" 15 | } -------------------------------------------------------------------------------- /Meshtastic.Virtual.Service/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.Hosting.Lifetime": "Information" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Meshtastic.Virtual.Service/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.Hosting.Lifetime": "Information" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Meshtastic.Virtual.Service/config.proto: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/c-sharp/68111e3471d9bd22c64b560e708d2d24f23198d0/Meshtastic.Virtual.Service/config.proto -------------------------------------------------------------------------------- /Meshtastic.Virtual.Service/module.store: -------------------------------------------------------------------------------- 1 | 2 | *mqtt.meshtastic.orgmeshdev" 3 | large4cats -------------------------------------------------------------------------------- /Meshtastic.Virtual.Service/node.proto: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/c-sharp/68111e3471d9bd22c64b560e708d2d24f23198d0/Meshtastic.Virtual.Service/node.proto -------------------------------------------------------------------------------- /Meshtastic.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.4.33122.133 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meshtastic.Cli", "Meshtastic.Cli\Meshtastic.Cli.csproj", "{85FD146B-D0B7-456A-90CF-C84D6A5B8E67}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meshtastic", "Meshtastic\Meshtastic.csproj", "{5A78C769-A243-469B-BC18-0E0A4E4F87D6}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meshtastic.Test", "Meshtastic.Test\Meshtastic.Test.csproj", "{E48770C0-4390-4B24-B9CB-610E5E7EC68A}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{020BC5F0-48A6-4870-8220-5A4B7A016094}" 13 | ProjectSection(SolutionItems) = preProject 14 | .editorconfig = .editorconfig 15 | Directory.Packages.props = Directory.Packages.props 16 | EndProjectSection 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Meshtastic.Virtual.Service", "Meshtastic.Virtual.Service\Meshtastic.Virtual.Service.csproj", "{F2226007-E2D7-4832-A94A-C7427EF2A3F1}" 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Release|Any CPU = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {85FD146B-D0B7-456A-90CF-C84D6A5B8E67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {85FD146B-D0B7-456A-90CF-C84D6A5B8E67}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {85FD146B-D0B7-456A-90CF-C84D6A5B8E67}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {85FD146B-D0B7-456A-90CF-C84D6A5B8E67}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {5A78C769-A243-469B-BC18-0E0A4E4F87D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {5A78C769-A243-469B-BC18-0E0A4E4F87D6}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {5A78C769-A243-469B-BC18-0E0A4E4F87D6}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {5A78C769-A243-469B-BC18-0E0A4E4F87D6}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {E48770C0-4390-4B24-B9CB-610E5E7EC68A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {E48770C0-4390-4B24-B9CB-610E5E7EC68A}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {E48770C0-4390-4B24-B9CB-610E5E7EC68A}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {E48770C0-4390-4B24-B9CB-610E5E7EC68A}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {F2226007-E2D7-4832-A94A-C7427EF2A3F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {F2226007-E2D7-4832-A94A-C7427EF2A3F1}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {F2226007-E2D7-4832-A94A-C7427EF2A3F1}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {F2226007-E2D7-4832-A94A-C7427EF2A3F1}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {76FD312D-9804-486E-891C-E6739BF9046A} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /Meshtastic/Connections/PacketFraming.cs: -------------------------------------------------------------------------------- 1 | namespace Meshtastic.Connections; 2 | 3 | public static class PacketFraming 4 | { 5 | public static byte[] PACKET_FRAME_START => new byte[] { 0x94, 0xc3 }; 6 | public static byte[] SERIAL_PREAMBLE => new byte[] 7 | { 8 | PACKET_FRAME_START[1], 9 | PACKET_FRAME_START[1], 10 | PACKET_FRAME_START[1], 11 | PACKET_FRAME_START[1] 12 | }; 13 | 14 | public const int PACKET_HEADER_LENGTH = 4; 15 | public static byte[] GetPacketHeader(byte[] data) => 16 | new byte[] { 17 | PACKET_FRAME_START[0], 18 | PACKET_FRAME_START[1], 19 | (byte)((data.Length >> 8) & 0xff), 20 | (byte)(data.Length & 0xff), 21 | }; 22 | 23 | public static byte[] CreatePacket(byte[] data) => 24 | GetPacketHeader(data) 25 | .Concat(data) 26 | .ToArray(); 27 | } 28 | -------------------------------------------------------------------------------- /Meshtastic/Connections/SimulatedConnection.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Data; 2 | using Meshtastic.Protobufs; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace Meshtastic.Connections; 6 | 7 | public class SimulatedConnection : DeviceConnection 8 | { 9 | public SimulatedConnection(ILogger logger) : base(logger) 10 | { 11 | Logger = logger; 12 | } 13 | 14 | public new ILogger Logger { get; } 15 | 16 | public override async Task ReadFromRadio(Func> isComplete, int readTimeoutMs = 15000) 17 | { 18 | await Task.CompletedTask; 19 | } 20 | 21 | public override async Task WriteToRadio(ToRadio toRadio, Func> isComplete) 22 | { 23 | return await Task.FromResult(new DeviceStateContainer()); 24 | } 25 | 26 | public override async Task WriteToRadio(ToRadio toRadio) 27 | { 28 | await Task.CompletedTask; 29 | } 30 | 31 | public override void Disconnect() 32 | { 33 | } 34 | } -------------------------------------------------------------------------------- /Meshtastic/Connections/TcpConnection.cs: -------------------------------------------------------------------------------- 1 | using Google.Protobuf; 2 | using Meshtastic.Data; 3 | using Meshtastic.Protobufs; 4 | using Microsoft.Extensions.Logging; 5 | using System.Net.Sockets; 6 | 7 | namespace Meshtastic.Connections; 8 | 9 | public class TcpConnection : DeviceConnection, IDisposable 10 | { 11 | private readonly TcpClient client; 12 | private NetworkStream? networkStream; 13 | private const int DEFAULT_BUFFER_SIZE = 32; 14 | 15 | public TcpConnection(ILogger logger, string host, int port = Resources.DEFAULT_TCP_PORT) : base(logger) 16 | { 17 | client = new TcpClient(host, port) 18 | { 19 | ReceiveBufferSize = DEFAULT_BUFFER_SIZE, 20 | NoDelay = true 21 | }; 22 | } 23 | 24 | public TcpConnection(ILogger logger, string host, DeviceStateContainer container, int port = Resources.DEFAULT_TCP_PORT) : base(logger) 25 | { 26 | client = new TcpClient(host, port) 27 | { 28 | ReceiveBufferSize = DEFAULT_BUFFER_SIZE, 29 | NoDelay = true 30 | }; 31 | DeviceStateContainer = container; 32 | } 33 | 34 | public override async Task WriteToRadio(ToRadio packet, Func> isComplete) 35 | { 36 | DeviceStateContainer.AddToRadio(packet); 37 | var toRadio = PacketFraming.CreatePacket(packet.ToByteArray()); 38 | networkStream = client.GetStream(); 39 | await networkStream.WriteAsync(toRadio); 40 | VerboseLogPacket(packet); 41 | if (isComplete != null) 42 | await ReadFromRadio(isComplete); 43 | 44 | return DeviceStateContainer; 45 | } 46 | 47 | public override async Task WriteToRadio(ToRadio packet) 48 | { 49 | DeviceStateContainer.AddToRadio(packet); 50 | var toRadio = PacketFraming.CreatePacket(packet.ToByteArray()); 51 | await networkStream!.WriteAsync(toRadio); 52 | await networkStream.FlushAsync(); 53 | VerboseLogPacket(packet); 54 | } 55 | 56 | public override async Task ReadFromRadio(Func> isComplete, int readTimeoutMs = Resources.DEFAULT_READ_TIMEOUT) 57 | { 58 | if (networkStream == null) 59 | throw new ApplicationException("Could not establish network stream"); 60 | 61 | var buffer = new byte[DEFAULT_BUFFER_SIZE]; 62 | while (networkStream.CanRead) 63 | { 64 | await networkStream.ReadExactlyAsync(buffer); 65 | foreach (var item in buffer) 66 | { 67 | if (await ParsePackets(item, isComplete)) 68 | return; 69 | } 70 | } 71 | } 72 | 73 | public override void Disconnect() 74 | { 75 | this.Dispose(); 76 | } 77 | 78 | public void Dispose() 79 | { 80 | client?.Dispose(); 81 | GC.SuppressFinalize(this); 82 | } 83 | } -------------------------------------------------------------------------------- /Meshtastic/Crypto/NonceGenerator.cs: -------------------------------------------------------------------------------- 1 | namespace Meshtastic.Crypto; 2 | public class NonceGenerator 3 | { 4 | private readonly byte[] nonce = new byte[16]; 5 | 6 | public NonceGenerator(uint fromNum, ulong packetId) 7 | { 8 | InitNonce(fromNum, packetId); 9 | } 10 | 11 | public byte[] Create() => nonce; 12 | 13 | private void InitNonce(uint fromNode, ulong packetId) 14 | { 15 | Array.Clear(nonce, 0, nonce.Length); 16 | 17 | Buffer.BlockCopy(BitConverter.GetBytes(packetId), 0, nonce, 0, sizeof(ulong)); 18 | Buffer.BlockCopy(BitConverter.GetBytes(fromNode), 0, nonce, sizeof(ulong), sizeof(uint)); 19 | } 20 | } -------------------------------------------------------------------------------- /Meshtastic/Crypto/PacketEncryption.cs: -------------------------------------------------------------------------------- 1 | using Org.BouncyCastle.Crypto; 2 | using Org.BouncyCastle.Crypto.Parameters; 3 | using Org.BouncyCastle.Security; 4 | 5 | namespace Meshtastic.Crypto; 6 | 7 | public static class PacketEncryption 8 | { 9 | private static readonly IBufferedCipher cipher = CipherUtilities.GetCipher("AES/CTR/NoPadding"); 10 | 11 | public static byte[] TransformPacket(byte[] cypherText, byte[] nonce, byte[] key) 12 | { 13 | cipher.Init(false, new ParametersWithIV(ParameterUtilities.CreateKeyParameter("AES", key), nonce)); 14 | byte[] output = new byte[cipher.GetOutputSize(cypherText.Length)]; 15 | _ = cipher.DoFinal(cypherText, output); 16 | 17 | return output; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Meshtastic/Data/FromRadioMessage.cs: -------------------------------------------------------------------------------- 1 | using Google.Protobuf; 2 | using Meshtastic.Protobufs; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace Meshtastic.Data 6 | { 7 | public class FromDeviceMessage 8 | { 9 | public FromDeviceMessage(ILogger logger) 10 | { 11 | Logger = logger; 12 | } 13 | 14 | public FromRadio? ParsedFromRadio(byte[] bytes) 15 | { 16 | try 17 | { 18 | var fromRadio = FromRadio.Parser.ParseFrom(bytes); 19 | if (fromRadio?.PayloadVariantCase == FromRadio.PayloadVariantOneofCase.None) 20 | { 21 | fromRadio = null; 22 | } 23 | 24 | return fromRadio; 25 | } 26 | catch (InvalidProtocolBufferException ex) 27 | { 28 | Logger.LogTrace(ex.ToString()); 29 | return null; 30 | } 31 | } 32 | 33 | public ILogger Logger { get; } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Meshtastic/Data/MessageFactories/PositionMessageFactory.cs: -------------------------------------------------------------------------------- 1 | using Google.Protobuf; 2 | using Meshtastic.Protobufs; 3 | 4 | namespace Meshtastic.Data.MessageFactories; 5 | 6 | public class PositionMessageFactory 7 | { 8 | private readonly DeviceStateContainer container; 9 | private readonly uint? dest; 10 | 11 | public PositionMessageFactory(DeviceStateContainer container, uint? dest = null) 12 | { 13 | this.container = container; 14 | this.dest = dest; 15 | } 16 | 17 | public MeshPacket CreatePositionPacket(Position message, uint channel = 0) 18 | { 19 | return new MeshPacket() 20 | { 21 | Channel = channel, 22 | WantAck = true, 23 | To = dest ?? container.MyNodeInfo.MyNodeNum, 24 | Id = (uint)Math.Floor(Random.Shared.Next() * 1e9), 25 | HopLimit = container?.GetHopLimitOrDefault() ?? 3, 26 | Decoded = new Protobufs.Data() 27 | { 28 | Portnum = PortNum.PositionApp, 29 | Payload = message.ToByteString(), 30 | }, 31 | }; 32 | } 33 | } -------------------------------------------------------------------------------- /Meshtastic/Data/MessageFactories/TelemetryMessageFactory.cs: -------------------------------------------------------------------------------- 1 | using Google.Protobuf; 2 | using Meshtastic.Protobufs; 3 | using Meshtastic.Extensions; 4 | 5 | namespace Meshtastic.Data.MessageFactories; 6 | 7 | public class TelemetryMessageFactory 8 | { 9 | private readonly DeviceStateContainer container; 10 | private readonly uint? dest; 11 | 12 | public TelemetryMessageFactory(DeviceStateContainer container, uint? dest = null) 13 | { 14 | this.container = container; 15 | this.dest = dest; 16 | } 17 | 18 | public MeshPacket CreateTelemetryPacket(uint channel = 0) 19 | { 20 | var telemetry = container?.FromRadioMessageLog 21 | .Where(fromRadio => fromRadio.Packet?.From == container.MyNodeInfo?.MyNodeNum) 22 | .FirstOrDefault(fromRadio => fromRadio.GetPayload()?.DeviceMetrics != null)?.GetPayload()?.DeviceMetrics; 23 | 24 | return new MeshPacket() 25 | { 26 | Channel = channel, 27 | WantAck = true, 28 | To = dest ?? 0, 29 | Id = (uint)Math.Floor(Random.Shared.Next() * 1e9), 30 | HopLimit = container?.GetHopLimitOrDefault() ?? 3, 31 | Decoded = new Protobufs.Data() 32 | { 33 | Portnum = PortNum.TelemetryApp, 34 | Payload = new Telemetry() 35 | { 36 | DeviceMetrics = telemetry ?? new DeviceMetrics() 37 | }.ToByteString(), 38 | WantResponse = true, 39 | }, 40 | }; 41 | } 42 | } -------------------------------------------------------------------------------- /Meshtastic/Data/MessageFactories/TextMessageFactory.cs: -------------------------------------------------------------------------------- 1 | using Google.Protobuf; 2 | using Meshtastic.Protobufs; 3 | 4 | namespace Meshtastic.Data.MessageFactories; 5 | 6 | public class TextMessageFactory 7 | { 8 | private readonly DeviceStateContainer container; 9 | private readonly uint? dest; 10 | 11 | public TextMessageFactory(DeviceStateContainer container, uint? dest = null) 12 | { 13 | this.container = container; 14 | this.dest = dest; 15 | } 16 | 17 | public MeshPacket CreateTextMessagePacket(string message, uint channel = 0) 18 | { 19 | return new MeshPacket() 20 | { 21 | Channel = channel, 22 | WantAck = true, 23 | To = dest ?? 0xffffffff, // Default to broadcast 24 | Id = (uint)Math.Floor(Random.Shared.Next() * 1e9), 25 | HopLimit = container?.GetHopLimitOrDefault() ?? 3, 26 | Decoded = new Protobufs.Data() 27 | { 28 | Portnum = PortNum.TextMessageApp, 29 | Payload = ByteString.CopyFromUtf8(message), 30 | }, 31 | }; 32 | } 33 | } -------------------------------------------------------------------------------- /Meshtastic/Data/MessageFactories/ToRadioMessageFactory.cs: -------------------------------------------------------------------------------- 1 | using Google.Protobuf; 2 | using Meshtastic.Protobufs; 3 | using static Meshtastic.Protobufs.XModem.Types; 4 | 5 | namespace Meshtastic.Data.MessageFactories; 6 | 7 | public class ToRadioMessageFactory 8 | { 9 | public ToRadioMessageFactory() 10 | { 11 | } 12 | 13 | // Create a ToRadio message with a empty payload 14 | public ToRadio CreateKeepAliveMessage() => 15 | new() 16 | { 17 | Heartbeat = new Heartbeat() 18 | }; 19 | 20 | public ToRadio CreateWantConfigMessage() => 21 | new() 22 | { 23 | WantConfigId = (uint)Random.Shared.Next(), 24 | }; 25 | 26 | public ToRadio CreateMeshPacketMessage(MeshPacket packet) => 27 | new() 28 | { 29 | Packet = packet 30 | }; 31 | 32 | public ToRadio CreateXmodemPacketMessage(Control control = XModem.Types.Control.Stx) => 33 | new() 34 | { 35 | XmodemPacket = new XModem() 36 | { 37 | Control = control 38 | } 39 | }; 40 | 41 | public ToRadio CreateMqttClientProxyMessage(string topic, byte[] payload, bool retain = false) => 42 | new() 43 | { 44 | MqttClientProxyMessage = new MqttClientProxyMessage() 45 | { 46 | Topic = topic, 47 | Data = ByteString.CopyFrom(payload), 48 | Retained = retain, 49 | } 50 | }; 51 | } -------------------------------------------------------------------------------- /Meshtastic/Data/MessageFactories/TraceRouteMessageFactory.cs: -------------------------------------------------------------------------------- 1 | using Google.Protobuf; 2 | using Meshtastic.Protobufs; 3 | 4 | namespace Meshtastic.Data.MessageFactories; 5 | 6 | public class TraceRouteMessageFactory 7 | { 8 | private readonly DeviceStateContainer container; 9 | private readonly uint? dest; 10 | 11 | public TraceRouteMessageFactory(DeviceStateContainer container, uint? dest = null) 12 | { 13 | this.container = container; 14 | this.dest = dest; 15 | } 16 | 17 | public MeshPacket CreateRouteDiscoveryPacket(uint channel = 0) 18 | { 19 | return new MeshPacket() 20 | { 21 | Channel = channel, 22 | To = dest!.Value, 23 | Id = (uint)Math.Floor(Random.Shared.Next() * 1e9), 24 | HopLimit = container?.GetHopLimitOrDefault() ?? 3, 25 | Decoded = new Protobufs.Data() 26 | { 27 | WantResponse = true, 28 | Portnum = PortNum.TracerouteApp, 29 | Payload = new RouteDiscovery().ToByteString(), // Traceroute just wants an empty bytestring 30 | }, 31 | }; 32 | } 33 | } -------------------------------------------------------------------------------- /Meshtastic/Data/MessageFactories/WaypointMessageFactory.cs: -------------------------------------------------------------------------------- 1 | using Google.Protobuf; 2 | using Meshtastic.Protobufs; 3 | 4 | namespace Meshtastic.Data.MessageFactories; 5 | 6 | public class WaypointMessageFactory 7 | { 8 | private readonly DeviceStateContainer container; 9 | private readonly uint? dest; 10 | 11 | public WaypointMessageFactory(DeviceStateContainer container, uint? dest = null) 12 | { 13 | this.container = container; 14 | this.dest = dest; 15 | } 16 | 17 | public MeshPacket CreateWaypointPacket(Waypoint waypoint, uint channel = 0) 18 | { 19 | return new MeshPacket() 20 | { 21 | Channel = channel, 22 | WantAck = true, 23 | To = dest ?? 0xffffffff, // Default to broadcast 24 | Id = (uint)Math.Floor(Random.Shared.Next() * 1e9), 25 | HopLimit = container?.GetHopLimitOrDefault() ?? 3, 26 | Decoded = new Protobufs.Data() 27 | { 28 | Portnum = PortNum.WaypointApp, 29 | Payload = waypoint.ToByteString(), 30 | }, 31 | }; 32 | } 33 | } -------------------------------------------------------------------------------- /Meshtastic/Extensions/DateTimeExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Meshtastic.Extensions; 2 | 3 | public static class DateTimeExtensions 4 | { 5 | public static uint GetUnixTimestamp(this DateTime dateTime) 6 | { 7 | return Convert.ToUInt32(dateTime.Subtract(new DateTime(1970, 1, 1)).TotalSeconds); 8 | } 9 | 10 | public static string AsTimeAgo(this DateTime dateTime) 11 | { 12 | TimeSpan timeSpan = DateTime.UtcNow.Subtract(dateTime); 13 | 14 | return timeSpan.TotalSeconds switch 15 | { 16 | <= 60 => $"{timeSpan.Seconds} seconds ago", 17 | 18 | _ => timeSpan.TotalMinutes switch 19 | { 20 | <= 1 => "A minute ago", 21 | < 60 => $"{timeSpan.Minutes} minutes ago", 22 | _ => timeSpan.TotalHours switch 23 | { 24 | <= 1 => "A hour ago", 25 | < 24 => $"{timeSpan.Hours} hours ago", 26 | _ => timeSpan.TotalDays switch 27 | { 28 | <= 1 => "yesterday", 29 | <= 30 => $"{timeSpan.Days} days ago", 30 | 31 | <= 60 => "A month ago", 32 | < 365 => $"{timeSpan.Days / 30} months ago", 33 | 34 | <= 365 * 2 => "A year ago", 35 | _ => $"{timeSpan.Days / 365} years ago" 36 | } 37 | } 38 | } 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Meshtastic/Extensions/DisplayExtensions.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Protobufs; 2 | 3 | namespace Meshtastic.Extensions 4 | { 5 | public static class DisplayExtensions 6 | { 7 | public static string ToDisplayString(this Position position) 8 | { 9 | var display = "Not available"; 10 | if (position == null || (position.LatitudeI == 0 && position.LongitudeI == 0)) 11 | return display; 12 | 13 | return $"{Math.Round(position.LatitudeI * 1e-7, 7)}, {Math.Round(position.LongitudeI * 1e-7, 7)}"; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Meshtastic/Extensions/FromRadioExtensions.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Data.MessageFactories; 2 | using Meshtastic.Protobufs; 3 | 4 | namespace Meshtastic.Extensions; 5 | 6 | public static class FromRadioExtensions 7 | { 8 | private static bool IsValidMeshPacket(FromRadio fromRadio) 9 | { 10 | return fromRadio.PayloadVariantCase == FromRadio.PayloadVariantOneofCase.Packet && 11 | fromRadio.Packet?.PayloadVariantCase == MeshPacket.PayloadVariantOneofCase.Decoded && 12 | fromRadio.Packet?.Decoded?.Payload != null; 13 | } 14 | 15 | public static TResult? GetPayload(this FromRadio fromRadio) where TResult : class 16 | { 17 | if (typeof(TResult) == typeof(XModem) && fromRadio.PayloadVariantCase == FromRadio.PayloadVariantOneofCase.XmodemPacket) 18 | return fromRadio.XmodemPacket as TResult; 19 | 20 | if (!IsValidMeshPacket(fromRadio)) 21 | return null; 22 | 23 | if (typeof(TResult) == typeof(AdminMessage) && fromRadio.Packet?.Decoded?.Portnum == PortNum.AdminApp) 24 | return AdminMessage.Parser.ParseFrom(fromRadio.Packet?.Decoded?.Payload) as TResult; 25 | 26 | else if (typeof(TResult) == typeof(RouteDiscovery) && fromRadio.Packet?.Decoded?.Portnum == PortNum.TracerouteApp) 27 | return RouteDiscovery.Parser.ParseFrom(fromRadio.Packet?.Decoded?.Payload) as TResult; 28 | 29 | else if (typeof(TResult) == typeof(Routing) && fromRadio.Packet?.Decoded?.Portnum == PortNum.RoutingApp) 30 | return Routing.Parser.ParseFrom(fromRadio.Packet?.Decoded?.Payload) as TResult; 31 | 32 | else if (typeof(TResult) == typeof(Position) && fromRadio.Packet?.Decoded?.Portnum == PortNum.PositionApp) 33 | return Position.Parser.ParseFrom(fromRadio.Packet?.Decoded?.Payload) as TResult; 34 | 35 | else if (typeof(TResult) == typeof(Telemetry) && fromRadio.Packet?.Decoded?.Portnum == PortNum.TelemetryApp) 36 | return Telemetry.Parser.ParseFrom(fromRadio.Packet?.Decoded?.Payload) as TResult; 37 | 38 | else if (typeof(TResult) == typeof(NodeInfo) && fromRadio.Packet?.Decoded?.Portnum == PortNum.NodeinfoApp) 39 | return NodeInfo.Parser.ParseFrom(fromRadio.Packet?.Decoded?.Payload) as TResult; 40 | 41 | else if (typeof(TResult) == typeof(Waypoint) && fromRadio.Packet?.Decoded?.Portnum == PortNum.WaypointApp) 42 | return NodeInfo.Parser.ParseFrom(fromRadio.Packet?.Decoded?.Payload) as TResult; 43 | 44 | else if (typeof(TResult) == typeof(string) && fromRadio.Packet?.Decoded?.Portnum == PortNum.TextMessageApp) 45 | return fromRadio.Packet?.Decoded?.Payload.ToStringUtf8() as TResult; 46 | 47 | else if (typeof(TResult) == typeof(string) && fromRadio.Packet?.Decoded?.Portnum == PortNum.SerialApp) 48 | return fromRadio.Packet?.Decoded?.Payload.ToStringUtf8() as TResult; 49 | 50 | return null; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Meshtastic/Extensions/ReflectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Google.Protobuf; 2 | using System.Reflection; 3 | 4 | namespace Meshtastic.Cli.Extensions; 5 | 6 | public static class ReflectionExtensions 7 | { 8 | private static string[] Exclusions => new[] { 9 | "Version", "Parser", "Descriptor", 10 | "Name", "ClrType", "ContainingType", 11 | "Fields", "Extensions", "NestedTypes", 12 | "EnumTypes", "Oneofs", "RealOneofCount", 13 | "FullName", "File", "Declaration", 14 | "IgnoreIncoming" 15 | }; 16 | 17 | public static IEnumerable GetSettingsOptions(this TSettings settings) where TSettings : IMessage 18 | { 19 | if (settings == null) 20 | throw new ArgumentNullException(nameof(settings)); 21 | 22 | return settings.Descriptor.Fields.InFieldNumberOrder() 23 | .Where(s => s.Name != "version") 24 | .SelectMany(section => 25 | { 26 | return section.MessageType.Fields.InFieldNumberOrder() 27 | .Select(setting => $"{section.PropertyName}.{setting.PropertyName}"); 28 | }); 29 | } 30 | 31 | public static PropertyInfo? FindPropertyByName(this Type type, string name) => 32 | GetProperties(type) 33 | .FirstOrDefault(prop => String.Equals(prop.Name, name.Trim(), StringComparison.InvariantCultureIgnoreCase)); 34 | 35 | public static IEnumerable GetProperties(this Type type) 36 | { 37 | return type 38 | .GetProperties() 39 | .Where(p => !Exclusions.Contains(p.Name)); 40 | } 41 | 42 | public static IEnumerable GetProperties(this object instance) 43 | { 44 | return instance 45 | .GetType() 46 | .GetProperties() 47 | .Where(p => !Exclusions.Contains(p.Name)); 48 | } 49 | 50 | public static string GetSettingValue(this PropertyInfo property, object instance) 51 | { 52 | if (property.PropertyType == typeof(ByteString)) { 53 | var byteString = (ByteString)property.GetValue(instance)!; 54 | return Convert.ToHexString(byteString.ToByteArray()); 55 | } 56 | return (property.GetValue(instance)?.ToString() ?? string.Empty).Replace("[", string.Empty).Replace("]", string.Empty); 57 | } 58 | } -------------------------------------------------------------------------------- /Meshtastic/Extensions/ToRadioExtensions.cs: -------------------------------------------------------------------------------- 1 | using Meshtastic.Protobufs; 2 | 3 | namespace Meshtastic.Extensions; 4 | 5 | public static class ToRadioExtensions 6 | { 7 | private static bool IsValidMeshPacket(ToRadio toRadio) 8 | { 9 | return toRadio.PayloadVariantCase == ToRadio.PayloadVariantOneofCase.Packet && 10 | toRadio.Packet?.PayloadVariantCase == MeshPacket.PayloadVariantOneofCase.Decoded && 11 | toRadio.Packet?.Decoded?.Payload != null; 12 | } 13 | 14 | public static TResult? GetPayload(this ToRadio toRadio) where TResult : class 15 | { 16 | if (typeof(TResult) == typeof(XModem) && toRadio.PayloadVariantCase == ToRadio.PayloadVariantOneofCase.XmodemPacket) 17 | return toRadio.XmodemPacket as TResult; 18 | 19 | if (!IsValidMeshPacket(toRadio)) 20 | return null; 21 | 22 | if (typeof(TResult) == typeof(AdminMessage) && toRadio.Packet?.Decoded?.Portnum == PortNum.AdminApp) 23 | return AdminMessage.Parser.ParseFrom(toRadio.Packet?.Decoded?.Payload) as TResult; 24 | 25 | else if (typeof(TResult) == typeof(RouteDiscovery) && toRadio.Packet?.Decoded?.Portnum == PortNum.TracerouteApp) 26 | return RouteDiscovery.Parser.ParseFrom(toRadio.Packet?.Decoded?.Payload) as TResult; 27 | 28 | else if (typeof(TResult) == typeof(Routing) && toRadio.Packet?.Decoded?.Portnum == PortNum.RoutingApp) 29 | return Routing.Parser.ParseFrom(toRadio.Packet?.Decoded?.Payload) as TResult; 30 | 31 | else if (typeof(TResult) == typeof(Position) && toRadio.Packet?.Decoded?.Portnum == PortNum.PositionApp) 32 | return Position.Parser.ParseFrom(toRadio.Packet?.Decoded?.Payload) as TResult; 33 | 34 | else if (typeof(TResult) == typeof(Telemetry) && toRadio.Packet?.Decoded?.Portnum == PortNum.TelemetryApp) 35 | return Telemetry.Parser.ParseFrom(toRadio.Packet?.Decoded?.Payload) as TResult; 36 | 37 | else if (typeof(TResult) == typeof(NodeInfo) && toRadio.Packet?.Decoded?.Portnum == PortNum.NodeinfoApp) 38 | return NodeInfo.Parser.ParseFrom(toRadio.Packet?.Decoded?.Payload) as TResult; 39 | 40 | else if (typeof(TResult) == typeof(Waypoint) && toRadio.Packet?.Decoded?.Portnum == PortNum.WaypointApp) 41 | return NodeInfo.Parser.ParseFrom(toRadio.Packet?.Decoded?.Payload) as TResult; 42 | 43 | else if (typeof(TResult) == typeof(string) && 44 | (toRadio.Packet?.Decoded?.Portnum == PortNum.TextMessageApp || toRadio.Packet?.Decoded?.Portnum == PortNum.DetectionSensorApp || toRadio.Packet?.Decoded?.Portnum == PortNum.RangeTestApp)) 45 | return toRadio.Packet?.Decoded?.Payload.ToStringUtf8() as TResult; 46 | 47 | return null; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Meshtastic/Meshtastic.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net9.0 4 | enable 5 | enable 6 | Meshtastic 7 | logo.png 8 | http://github.com/meshtastic/c-sharp 9 | Meshtastic LLC 10 | Meshtastic C# 11 | True 12 | embedded 13 | LICENSE 14 | 15 | 16 | 17 | 18 | True 19 | \ 20 | 21 | 22 | True 23 | \ 24 | 25 | 26 | True 27 | \ 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Meshtastic/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Meshtastic": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "applicationUrl": "https://localhost:31293;http://localhost:31294" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Meshtastic/Resources.cs: -------------------------------------------------------------------------------- 1 | namespace Meshtastic; 2 | public static class Resources 3 | { 4 | public const int DEFAULT_BAUD_RATE = 115200; 5 | public const int DEFAULT_TCP_PORT = 4403; 6 | 7 | public const int DEFAULT_READ_TIMEOUT = 15000; 8 | public const int MAX_TO_FROM_RADIO_LENGTH = 512; 9 | 10 | public static readonly byte[] DEFAULT_PSK = [0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59, 0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0x01]; 11 | } -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meshtastic/c-sharp/68111e3471d9bd22c64b560e708d2d24f23198d0/logo.png -------------------------------------------------------------------------------- /scripts/buildinfo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import configparser 3 | import sys 4 | from readprops import readProps 5 | 6 | verObj = readProps('version.properties') 7 | propName = sys.argv[1] 8 | print(f"{verObj[propName]}") -------------------------------------------------------------------------------- /scripts/bump_version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Bump the version number""" 3 | 4 | lines = None 5 | 6 | with open('version.properties', 'r', encoding='utf-8') as f: 7 | lines = f.readlines() 8 | 9 | with open('version.properties', 'w', encoding='utf-8') as f: 10 | for line in lines: 11 | if line.lstrip().startswith("build = "): 12 | words = line.split(" = ") 13 | ver = f'build = {int(words[1]) + 1}' 14 | f.write(f'{ver}\n') 15 | else: 16 | f.write(line) 17 | -------------------------------------------------------------------------------- /scripts/readprops.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import configparser 3 | import traceback 4 | import sys 5 | 6 | 7 | def readProps(prefsLoc): 8 | """Read the version of our project as a string""" 9 | 10 | config = configparser.RawConfigParser() 11 | config.read(prefsLoc) 12 | version = dict(config.items('VERSION')) 13 | verObj = dict(short = "{}.{}.{}".format(version["major"], version["minor"], version["build"]), 14 | long = "unset") 15 | 16 | # Try to find current build SHA if if the workspace is clean. This could fail if git is not installed 17 | try: 18 | sha = subprocess.check_output( 19 | ['git', 'rev-parse', '--short', 'HEAD']).decode("utf-8").strip() 20 | isDirty = subprocess.check_output( 21 | ['git', 'diff', 'HEAD']).decode("utf-8").strip() 22 | suffix = sha 23 | if isDirty: 24 | # short for 'dirty', we want to keep our verstrings source for protobuf reasons 25 | suffix = sha + "-d" 26 | verObj['long'] = "{}.{}.{}.{}".format( 27 | version["major"], version["minor"], version["build"], suffix) 28 | except: 29 | # print("Unexpected error:", sys.exc_info()[0]) 30 | # traceback.print_exc() 31 | verObj['long'] = verObj['short'] 32 | 33 | # print("firmware version " + verStr) 34 | return verObj 35 | # print("path is" + ','.join(sys.path)) 36 | -------------------------------------------------------------------------------- /scripts/regen-protos.bat: -------------------------------------------------------------------------------- 1 | cd ./protobufs/meshtastic/ 2 | rename deviceonly.proto deviceonly.ignoreproto 3 | cd ../../ 4 | 5 | %USERPROFILE%/.nuget/packages/google.protobuf.tools/3.29.3/tools/windows_x64/protoc.exe -I=protobufs --csharp_out=./Meshtastic/Generated --csharp_opt=base_namespace=Meshtastic.Protobufs ./protobufs/meshtastic/*.proto 6 | 7 | cd ./protobufs/meshtastic/ 8 | rename deviceonly.ignoreproto deviceonly.proto 9 | cd ../../ 10 | -------------------------------------------------------------------------------- /scripts/regen-protos.sh: -------------------------------------------------------------------------------- 1 | cd ./protobufs/meshtastic/ 2 | mv deviceonly.proto deviceonly.ignoreproto 3 | cd ../../ 4 | 5 | ~/.nuget/packages/google.protobuf.tools/3.29.3/tools/macosx_x64/protoc -I=protobufs --csharp_out=./Meshtastic/Generated --csharp_opt=base_namespace=Meshtastic.Protobufs ./protobufs/meshtastic/*.proto 6 | 7 | cd ./protobufs/meshtastic/ 8 | mv deviceonly.ignoreproto deviceonly.proto 9 | cd ../../ 10 | -------------------------------------------------------------------------------- /version.properties: -------------------------------------------------------------------------------- 1 | [VERSION] 2 | major = 2 3 | minor = 0 4 | build = 3 5 | --------------------------------------------------------------------------------