Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/Harp.Toolkit/Generate/GenerateCommand.cs
Original file line number Diff line number Diff line change
@@ -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<KeyValuePair<string, string>> generatedFileContents)
{
foreach ((var fileName, var fileContents) in generatedFileContents)
{
Console.WriteLine($"Generating {fileName}...");
File.WriteAllText(Path.Combine(path, fileName), fileContents);
}
}
}
45 changes: 45 additions & 0 deletions src/Harp.Toolkit/Generate/GenerateFirmwareCommand.cs
Original file line number Diff line number Diff line change
@@ -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<bool> 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);
}
});
}
}
35 changes: 35 additions & 0 deletions src/Harp.Toolkit/Generate/GenerateInterfaceCommand.cs
Original file line number Diff line number Diff line change
@@ -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<string> 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);
});
}
}
47 changes: 47 additions & 0 deletions src/Harp.Toolkit/Generate/GeneratorHelper.cs
Original file line number Diff line number Diff line change
@@ -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<DeviceInfo>(parser);
}

public static Dictionary<string, PortPinInfo> ReadPortPinMetadata(string path)
{
using var reader = new StreamReader(path);
return MetadataDeserializer.Instance.Deserialize<Dictionary<string, PortPinInfo>>(reader);
}

public static IEnumerable<KeyValuePair<string, T>> GetPortPinsOfType<T>(IDictionary<string, PortPinInfo> portPins) where T : PortPinInfo
{
return from item in portPins
where item.Value is T
select new KeyValuePair<string, T>(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;
}
}
14 changes: 14 additions & 0 deletions src/Harp.Toolkit/Generate/IOMetadataPathOption.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.CommandLine;

namespace Harp.Toolkit.Generate;

public class IOMetadataPathOption : Option<FileInfo>
{
public IOMetadataPathOption()
: base("--ios")
{
OptionValidation.AcceptExistingOnly(this);
Description = "The path to the file describing the device IO pins.";
DefaultValueFactory = _ => new FileInfo("ios.yml");
}
}
14 changes: 14 additions & 0 deletions src/Harp.Toolkit/Generate/MetadataPathArgument.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.CommandLine;

namespace Harp.Toolkit.Generate;

public class MetadataPathArgument : Argument<FileInfo>
{
public MetadataPathArgument()
: base("device.yml")
{
ArgumentValidation.AcceptExistingOnly(this);
Description = "The path to the file describing the device registers.";
Arity = ArgumentArity.ExactlyOne;
}
}
13 changes: 13 additions & 0 deletions src/Harp.Toolkit/Generate/OutputPathOption.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.CommandLine;

namespace Harp.Toolkit.Generate;

public class OutputPathOption : Option<DirectoryInfo>
{
public OutputPathOption()
: base("-o", "--output")
{
Description = "Location to place the generated output. The default is the current directory.";
DefaultValueFactory = _ => new DirectoryInfo(Environment.CurrentDirectory);
}
}
2 changes: 2 additions & 0 deletions src/Harp.Toolkit/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.CommandLine;
using Bonsai.Harp;
using Harp.Toolkit.Generate;

namespace Harp.Toolkit;

Expand All @@ -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);
Expand Down
Loading