From 33e28407a448587be6eb5c23d5601efd8983a8e3 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Mon, 16 Mar 2026 11:05:17 +0000 Subject: [PATCH] Add interface and firmware generator commands The backend is implemented using the new Harp.Generators API so that generation can run directly from the toolkit without additional dependencies. --- src/Harp.Toolkit/Generate/GenerateCommand.cs | 22 +++++++++ .../Generate/GenerateFirmwareCommand.cs | 45 ++++++++++++++++++ .../Generate/GenerateInterfaceCommand.cs | 35 ++++++++++++++ src/Harp.Toolkit/Generate/GeneratorHelper.cs | 47 +++++++++++++++++++ .../Generate/IOMetadataPathOption.cs | 14 ++++++ .../Generate/MetadataPathArgument.cs | 14 ++++++ src/Harp.Toolkit/Generate/OutputPathOption.cs | 13 +++++ src/Harp.Toolkit/Program.cs | 2 + 8 files changed, 192 insertions(+) create mode 100644 src/Harp.Toolkit/Generate/GenerateCommand.cs create mode 100644 src/Harp.Toolkit/Generate/GenerateFirmwareCommand.cs create mode 100644 src/Harp.Toolkit/Generate/GenerateInterfaceCommand.cs create mode 100644 src/Harp.Toolkit/Generate/GeneratorHelper.cs create mode 100644 src/Harp.Toolkit/Generate/IOMetadataPathOption.cs create mode 100644 src/Harp.Toolkit/Generate/MetadataPathArgument.cs create mode 100644 src/Harp.Toolkit/Generate/OutputPathOption.cs diff --git a/src/Harp.Toolkit/Generate/GenerateCommand.cs b/src/Harp.Toolkit/Generate/GenerateCommand.cs new file mode 100644 index 0000000..acfa4ec --- /dev/null +++ b/src/Harp.Toolkit/Generate/GenerateCommand.cs @@ -0,0 +1,22 @@ +using System.CommandLine; + +namespace Harp.Toolkit.Generate; + +public class GenerateCommand : Command +{ + public GenerateCommand() + : base("generate", "Generate firmware or interface code for Harp devices.") + { + Subcommands.Add(new GenerateInterfaceCommand()); + Subcommands.Add(new GenerateFirmwareCommand()); + } + + internal static void WriteFileContents(string path, IEnumerable> generatedFileContents) + { + foreach ((var fileName, var fileContents) in generatedFileContents) + { + Console.WriteLine($"Generating {fileName}..."); + File.WriteAllText(Path.Combine(path, fileName), fileContents); + } + } +} diff --git a/src/Harp.Toolkit/Generate/GenerateFirmwareCommand.cs b/src/Harp.Toolkit/Generate/GenerateFirmwareCommand.cs new file mode 100644 index 0000000..78f42d6 --- /dev/null +++ b/src/Harp.Toolkit/Generate/GenerateFirmwareCommand.cs @@ -0,0 +1,45 @@ +using System.CommandLine; +using Harp.Generators; + +namespace Harp.Toolkit.Generate; + +class GenerateFirmwareCommand : Command +{ + public GenerateFirmwareCommand() + : base("firmware", "Generate firmware headers and implementation template.") + { + MetadataPathArgument metadataPathArgument = new(); + IOMetadataPathOption iosMetadataPathOption = new(); + OutputPathOption outputPathOption = new(); + + Option generateImplementationOption = new("--implementation") + { + Description = "Indicates whether to generate implementation (.c) files. The default is false." + }; + + Arguments.Add(metadataPathArgument); + Options.Add(iosMetadataPathOption); + Options.Add(generateImplementationOption); + Options.Add(outputPathOption); + + SetAction(parseResult => + { + var outputPath = parseResult.GetRequiredValue(outputPathOption); + var registerMetadataFileName = parseResult.GetRequiredValue(metadataPathArgument).FullName; + var iosMetadataFileName = parseResult.GetRequiredValue(iosMetadataPathOption).FullName; + var generateImplementation = parseResult.GetValue(generateImplementationOption); + + var deviceMetadata = GeneratorHelper.ReadDeviceMetadata(registerMetadataFileName); + var portPinMetadata = GeneratorHelper.ReadPortPinMetadata(iosMetadataFileName); + var generator = new FirmwareGenerator(deviceMetadata, portPinMetadata); + var headers = generator.GenerateHeaders(); + var implementation = generateImplementation ? generator.GenerateImplementation() : default; + if (GeneratorHelper.AssertNoGeneratorErrors(generator.Errors)) + { + GenerateCommand.WriteFileContents(outputPath.FullName, headers); + if (generateImplementation) + GenerateCommand.WriteFileContents(outputPath.FullName, implementation); + } + }); + } +} diff --git a/src/Harp.Toolkit/Generate/GenerateInterfaceCommand.cs b/src/Harp.Toolkit/Generate/GenerateInterfaceCommand.cs new file mode 100644 index 0000000..5cd70ab --- /dev/null +++ b/src/Harp.Toolkit/Generate/GenerateInterfaceCommand.cs @@ -0,0 +1,35 @@ +using System.CommandLine; +using Harp.Generators; + +namespace Harp.Toolkit.Generate; + +class GenerateInterfaceCommand : Command +{ + public GenerateInterfaceCommand() + : base("interface", "Generate reactive programming API and async API.") + { + MetadataPathArgument metadataPathArgument = new(); + OutputPathOption outputPathOption = new(); + Option namespaceOption = new("-ns", "--namespace") + { + Description = "The namespace for the generated code. The default is `Harp.DeviceName`." + }; + + Arguments.Add(metadataPathArgument); + Options.Add(namespaceOption); + Options.Add(outputPathOption); + + SetAction(parseResult => + { + var outputPath = parseResult.GetRequiredValue(outputPathOption); + var metadataPath = parseResult.GetRequiredValue(metadataPathArgument); + var ns = parseResult.GetValue(namespaceOption); + + var deviceMetadata = GeneratorHelper.ReadDeviceMetadata(metadataPath.FullName); + var generator = new InterfaceGenerator(deviceMetadata, ns ?? $"Harp.{deviceMetadata.Device}"); + var implementation = generator.GenerateImplementation(); + if (GeneratorHelper.AssertNoGeneratorErrors(generator.Errors)) + GenerateCommand.WriteFileContents(outputPath.FullName, implementation); + }); + } +} diff --git a/src/Harp.Toolkit/Generate/GeneratorHelper.cs b/src/Harp.Toolkit/Generate/GeneratorHelper.cs new file mode 100644 index 0000000..1f1d10e --- /dev/null +++ b/src/Harp.Toolkit/Generate/GeneratorHelper.cs @@ -0,0 +1,47 @@ +using System.CodeDom.Compiler; +using System.Text; +using Harp.Generators; +using YamlDotNet.Core; + +namespace Harp.Toolkit.Generate; + +public static class GeneratorHelper +{ + public static DeviceInfo ReadDeviceMetadata(string path) + { + using var reader = new StreamReader(path); + var parser = new MergingParser(new Parser(reader)); + return MetadataDeserializer.Instance.Deserialize(parser); + } + + public static Dictionary ReadPortPinMetadata(string path) + { + using var reader = new StreamReader(path); + return MetadataDeserializer.Instance.Deserialize>(reader); + } + + public static IEnumerable> GetPortPinsOfType(IDictionary portPins) where T : PortPinInfo + { + return from item in portPins + where item.Value is T + select new KeyValuePair(item.Key, (T)item.Value); + } + + public static bool AssertNoGeneratorErrors(CompilerErrorCollection errors) + { + if (errors.Count > 0) + { + var errorLog = new StringBuilder(); + errorLog.AppendLine("Code generation has completed with errors:"); + foreach (CompilerError error in errors) + { + var warningString = error.IsWarning ? "warning" : "error"; + errorLog.AppendLine($"{error.FileName}: {warningString}: {error.ErrorText}"); + } + Console.Error.WriteLine(errorLog.ToString()); + return !errors.HasErrors; + } + + return true; + } +} diff --git a/src/Harp.Toolkit/Generate/IOMetadataPathOption.cs b/src/Harp.Toolkit/Generate/IOMetadataPathOption.cs new file mode 100644 index 0000000..ac9b843 --- /dev/null +++ b/src/Harp.Toolkit/Generate/IOMetadataPathOption.cs @@ -0,0 +1,14 @@ +using System.CommandLine; + +namespace Harp.Toolkit.Generate; + +public class IOMetadataPathOption : Option +{ + public IOMetadataPathOption() + : base("--ios") + { + OptionValidation.AcceptExistingOnly(this); + Description = "The path to the file describing the device IO pins."; + DefaultValueFactory = _ => new FileInfo("ios.yml"); + } +} diff --git a/src/Harp.Toolkit/Generate/MetadataPathArgument.cs b/src/Harp.Toolkit/Generate/MetadataPathArgument.cs new file mode 100644 index 0000000..c6f0952 --- /dev/null +++ b/src/Harp.Toolkit/Generate/MetadataPathArgument.cs @@ -0,0 +1,14 @@ +using System.CommandLine; + +namespace Harp.Toolkit.Generate; + +public class MetadataPathArgument : Argument +{ + public MetadataPathArgument() + : base("device.yml") + { + ArgumentValidation.AcceptExistingOnly(this); + Description = "The path to the file describing the device registers."; + Arity = ArgumentArity.ExactlyOne; + } +} diff --git a/src/Harp.Toolkit/Generate/OutputPathOption.cs b/src/Harp.Toolkit/Generate/OutputPathOption.cs new file mode 100644 index 0000000..bb54983 --- /dev/null +++ b/src/Harp.Toolkit/Generate/OutputPathOption.cs @@ -0,0 +1,13 @@ +using System.CommandLine; + +namespace Harp.Toolkit.Generate; + +public class OutputPathOption : Option +{ + public OutputPathOption() + : base("-o", "--output") + { + Description = "Location to place the generated output. The default is the current directory."; + DefaultValueFactory = _ => new DirectoryInfo(Environment.CurrentDirectory); + } +} diff --git a/src/Harp.Toolkit/Program.cs b/src/Harp.Toolkit/Program.cs index dab93e5..f269381 100644 --- a/src/Harp.Toolkit/Program.cs +++ b/src/Harp.Toolkit/Program.cs @@ -1,5 +1,6 @@ using System.CommandLine; using Bonsai.Harp; +using Harp.Toolkit.Generate; namespace Harp.Toolkit; @@ -14,6 +15,7 @@ static async Task Main(string[] args) rootCommand.Options.Add(portTimeoutOption); rootCommand.Subcommands.Add(new ListCommand()); rootCommand.Subcommands.Add(new UpdateFirmwareCommand()); + rootCommand.Subcommands.Add(new GenerateCommand()); rootCommand.SetAction(async parseResult => { var portName = parseResult.GetRequiredValue(portNameOption);