Skip to content

Commit 33e2840

Browse files
committed
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.
1 parent 1456704 commit 33e2840

File tree

8 files changed

+192
-0
lines changed

8 files changed

+192
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.CommandLine;
2+
3+
namespace Harp.Toolkit.Generate;
4+
5+
public class GenerateCommand : Command
6+
{
7+
public GenerateCommand()
8+
: base("generate", "Generate firmware or interface code for Harp devices.")
9+
{
10+
Subcommands.Add(new GenerateInterfaceCommand());
11+
Subcommands.Add(new GenerateFirmwareCommand());
12+
}
13+
14+
internal static void WriteFileContents(string path, IEnumerable<KeyValuePair<string, string>> generatedFileContents)
15+
{
16+
foreach ((var fileName, var fileContents) in generatedFileContents)
17+
{
18+
Console.WriteLine($"Generating {fileName}...");
19+
File.WriteAllText(Path.Combine(path, fileName), fileContents);
20+
}
21+
}
22+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using System.CommandLine;
2+
using Harp.Generators;
3+
4+
namespace Harp.Toolkit.Generate;
5+
6+
class GenerateFirmwareCommand : Command
7+
{
8+
public GenerateFirmwareCommand()
9+
: base("firmware", "Generate firmware headers and implementation template.")
10+
{
11+
MetadataPathArgument metadataPathArgument = new();
12+
IOMetadataPathOption iosMetadataPathOption = new();
13+
OutputPathOption outputPathOption = new();
14+
15+
Option<bool> generateImplementationOption = new("--implementation")
16+
{
17+
Description = "Indicates whether to generate implementation (.c) files. The default is false."
18+
};
19+
20+
Arguments.Add(metadataPathArgument);
21+
Options.Add(iosMetadataPathOption);
22+
Options.Add(generateImplementationOption);
23+
Options.Add(outputPathOption);
24+
25+
SetAction(parseResult =>
26+
{
27+
var outputPath = parseResult.GetRequiredValue(outputPathOption);
28+
var registerMetadataFileName = parseResult.GetRequiredValue(metadataPathArgument).FullName;
29+
var iosMetadataFileName = parseResult.GetRequiredValue(iosMetadataPathOption).FullName;
30+
var generateImplementation = parseResult.GetValue(generateImplementationOption);
31+
32+
var deviceMetadata = GeneratorHelper.ReadDeviceMetadata(registerMetadataFileName);
33+
var portPinMetadata = GeneratorHelper.ReadPortPinMetadata(iosMetadataFileName);
34+
var generator = new FirmwareGenerator(deviceMetadata, portPinMetadata);
35+
var headers = generator.GenerateHeaders();
36+
var implementation = generateImplementation ? generator.GenerateImplementation() : default;
37+
if (GeneratorHelper.AssertNoGeneratorErrors(generator.Errors))
38+
{
39+
GenerateCommand.WriteFileContents(outputPath.FullName, headers);
40+
if (generateImplementation)
41+
GenerateCommand.WriteFileContents(outputPath.FullName, implementation);
42+
}
43+
});
44+
}
45+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System.CommandLine;
2+
using Harp.Generators;
3+
4+
namespace Harp.Toolkit.Generate;
5+
6+
class GenerateInterfaceCommand : Command
7+
{
8+
public GenerateInterfaceCommand()
9+
: base("interface", "Generate reactive programming API and async API.")
10+
{
11+
MetadataPathArgument metadataPathArgument = new();
12+
OutputPathOption outputPathOption = new();
13+
Option<string> namespaceOption = new("-ns", "--namespace")
14+
{
15+
Description = "The namespace for the generated code. The default is `Harp.DeviceName`."
16+
};
17+
18+
Arguments.Add(metadataPathArgument);
19+
Options.Add(namespaceOption);
20+
Options.Add(outputPathOption);
21+
22+
SetAction(parseResult =>
23+
{
24+
var outputPath = parseResult.GetRequiredValue(outputPathOption);
25+
var metadataPath = parseResult.GetRequiredValue(metadataPathArgument);
26+
var ns = parseResult.GetValue(namespaceOption);
27+
28+
var deviceMetadata = GeneratorHelper.ReadDeviceMetadata(metadataPath.FullName);
29+
var generator = new InterfaceGenerator(deviceMetadata, ns ?? $"Harp.{deviceMetadata.Device}");
30+
var implementation = generator.GenerateImplementation();
31+
if (GeneratorHelper.AssertNoGeneratorErrors(generator.Errors))
32+
GenerateCommand.WriteFileContents(outputPath.FullName, implementation);
33+
});
34+
}
35+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System.CodeDom.Compiler;
2+
using System.Text;
3+
using Harp.Generators;
4+
using YamlDotNet.Core;
5+
6+
namespace Harp.Toolkit.Generate;
7+
8+
public static class GeneratorHelper
9+
{
10+
public static DeviceInfo ReadDeviceMetadata(string path)
11+
{
12+
using var reader = new StreamReader(path);
13+
var parser = new MergingParser(new Parser(reader));
14+
return MetadataDeserializer.Instance.Deserialize<DeviceInfo>(parser);
15+
}
16+
17+
public static Dictionary<string, PortPinInfo> ReadPortPinMetadata(string path)
18+
{
19+
using var reader = new StreamReader(path);
20+
return MetadataDeserializer.Instance.Deserialize<Dictionary<string, PortPinInfo>>(reader);
21+
}
22+
23+
public static IEnumerable<KeyValuePair<string, T>> GetPortPinsOfType<T>(IDictionary<string, PortPinInfo> portPins) where T : PortPinInfo
24+
{
25+
return from item in portPins
26+
where item.Value is T
27+
select new KeyValuePair<string, T>(item.Key, (T)item.Value);
28+
}
29+
30+
public static bool AssertNoGeneratorErrors(CompilerErrorCollection errors)
31+
{
32+
if (errors.Count > 0)
33+
{
34+
var errorLog = new StringBuilder();
35+
errorLog.AppendLine("Code generation has completed with errors:");
36+
foreach (CompilerError error in errors)
37+
{
38+
var warningString = error.IsWarning ? "warning" : "error";
39+
errorLog.AppendLine($"{error.FileName}: {warningString}: {error.ErrorText}");
40+
}
41+
Console.Error.WriteLine(errorLog.ToString());
42+
return !errors.HasErrors;
43+
}
44+
45+
return true;
46+
}
47+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System.CommandLine;
2+
3+
namespace Harp.Toolkit.Generate;
4+
5+
public class IOMetadataPathOption : Option<FileInfo>
6+
{
7+
public IOMetadataPathOption()
8+
: base("--ios")
9+
{
10+
OptionValidation.AcceptExistingOnly(this);
11+
Description = "The path to the file describing the device IO pins.";
12+
DefaultValueFactory = _ => new FileInfo("ios.yml");
13+
}
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System.CommandLine;
2+
3+
namespace Harp.Toolkit.Generate;
4+
5+
public class MetadataPathArgument : Argument<FileInfo>
6+
{
7+
public MetadataPathArgument()
8+
: base("device.yml")
9+
{
10+
ArgumentValidation.AcceptExistingOnly(this);
11+
Description = "The path to the file describing the device registers.";
12+
Arity = ArgumentArity.ExactlyOne;
13+
}
14+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System.CommandLine;
2+
3+
namespace Harp.Toolkit.Generate;
4+
5+
public class OutputPathOption : Option<DirectoryInfo>
6+
{
7+
public OutputPathOption()
8+
: base("-o", "--output")
9+
{
10+
Description = "Location to place the generated output. The default is the current directory.";
11+
DefaultValueFactory = _ => new DirectoryInfo(Environment.CurrentDirectory);
12+
}
13+
}

src/Harp.Toolkit/Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.CommandLine;
22
using Bonsai.Harp;
3+
using Harp.Toolkit.Generate;
34

45
namespace Harp.Toolkit;
56

@@ -14,6 +15,7 @@ static async Task Main(string[] args)
1415
rootCommand.Options.Add(portTimeoutOption);
1516
rootCommand.Subcommands.Add(new ListCommand());
1617
rootCommand.Subcommands.Add(new UpdateFirmwareCommand());
18+
rootCommand.Subcommands.Add(new GenerateCommand());
1719
rootCommand.SetAction(async parseResult =>
1820
{
1921
var portName = parseResult.GetRequiredValue(portNameOption);

0 commit comments

Comments
 (0)