From 9d726f21b7b22838f411a680b47c4a423629548f Mon Sep 17 00:00:00 2001 From: glopesdev Date: Fri, 4 Sep 2026 22:28:06 +0100 Subject: [PATCH] Generate the core register interface Metadata that names no device now generates an interface to the core register set: the register classes, their payload types and an address map, without the device operators. The seven device-scoped types are omitted, and five of them would otherwise collide with the hand-written operators of the same name in Bonsai.Harp. No asynchronous interface is generated, and the implementation no longer enumerates a file for it, so a consumer writing the generated files produces only one. DeviceMetadata gains IsApplicationDevice, which both targets now share rather than the Python target computing its own. The core register metadata can now drop its device name. --- src/Device.tt | 53 +++- src/Interface.cs | 6 + src/InterfaceGenerator.cs | 7 +- src/Python.cs | 4 +- src/core.yml | 1 - tests/ExpectedOutput/core.cs | 461 +------------------------------ tests/InterfaceGeneratorTests.cs | 5 +- 7 files changed, 68 insertions(+), 469 deletions(-) diff --git a/src/Device.tt b/src/Device.tt index 500fe69..ae2abd9 100644 --- a/src/Device.tt +++ b/src/Device.tt @@ -12,6 +12,11 @@ var publicRegisters = DeviceMetadata.Registers.Where(register => register.Value.Visibility == RegisterVisibility.Public).ToList(); var deviceRegisters = DeviceMetadata.Registers; var deviceName = DeviceMetadata.Device; +var isApplicationDevice = DeviceMetadata.IsApplicationDevice; +#> +<# +if (isApplicationDevice) +{ #> using Bonsai; using Bonsai.Harp; @@ -21,9 +26,25 @@ using System.ComponentModel; using System.Linq; using System.Reactive.Linq; using System.Xml.Serialization; +<# +} +else +{ +#> +using Bonsai.Harp; +using System; +using System.Collections.Generic; +using System.ComponentModel; +<# +} // isApplicationDevice +#> namespace <#= Namespace #> { +<# +if (isApplicationDevice) +{ +#> /// /// Generates events and processes commands for the <#= deviceName #> device connected /// at the specified serial port. @@ -53,6 +74,19 @@ namespace <#= Namespace #> (Bonsai.Harp.Device.RegisterMap.ToDictionary(entry => entry.Key, entry => entry.Value)) { <# +} +else +{ +#> + public partial class Device + { + /// + /// Gets a read-only mapping from address to register type. + /// + public static IReadOnlyDictionary RegisterMap { get; } = new Dictionary + { +<# +} // isApplicationDevice int registerIndex = 0; foreach (var register in deviceRegisters) { @@ -62,6 +96,10 @@ foreach (var register in deviceRegisters) } #> }; +<# +if (isApplicationDevice) +{ +#> /// /// Gets the contents of the metadata file describing the @@ -76,7 +114,14 @@ foreach (var register in deviceRegisters) using var streamReader = new System.IO.StreamReader(metadataStream); return streamReader.ReadToEnd(); } +<# +} // isApplicationDevice +#> } +<# +if (isApplicationDevice) +{ +#> /// /// Represents an operator that returns the contents of the metadata file @@ -388,6 +433,8 @@ foreach (var register in publicRegisters) string INamedElement.Name => $"{nameof(<#= deviceName #>)}.{GetElementDisplayName(Register)}"; } <# +} // deviceRegisters.Count > 0 +} // isApplicationDevice foreach (var registerMetadata in deviceRegisters) { var register = registerMetadata.Value; @@ -647,6 +694,8 @@ if (isPrivate) } <# } +if (isApplicationDevice && deviceRegisters.Count > 0) +{ #> /// @@ -691,6 +740,7 @@ foreach (var register in publicRegisters) string INamedElement.Name => $"{nameof(<#= deviceName #>)}.{GetElementDisplayName(Payload)}"; } <# +} // isApplicationDevice && deviceRegisters.Count > 0 foreach (var registerMetadata in publicRegisters) { var register = registerMetadata.Value; @@ -824,9 +874,6 @@ foreach (var registerMetadata in publicRegisters) } #> <# -} // deviceRegisters.Count > 0 -#> -<# var payloadTypes = new HashSet(); foreach (var registerMetadata in deviceRegisters) { diff --git a/src/Interface.cs b/src/Interface.cs index dbbe25d..e2a75b6 100644 --- a/src/Interface.cs +++ b/src/Interface.cs @@ -54,6 +54,12 @@ public class DeviceMetadata /// Specifies the collection of group masks available to be used with the different registers. /// public Dictionary GroupMasks = []; + + /// + /// Gets a value indicating whether the metadata describes an application device. + /// + [YamlIgnore] + public bool IsApplicationDevice => !string.IsNullOrEmpty(Device); } /// diff --git a/src/InterfaceGenerator.cs b/src/InterfaceGenerator.cs index 0d4ead4..ffe21b3 100644 --- a/src/InterfaceGenerator.cs +++ b/src/InterfaceGenerator.cs @@ -12,6 +12,7 @@ public sealed class InterfaceGenerator readonly Device _deviceTemplate = new(); readonly AsyncDevice _asyncDeviceTemplate = new(); readonly CompilerErrorCollection errors = []; + readonly bool isApplicationDevice; /// /// Initializes a new instance of the class with the @@ -28,6 +29,7 @@ public InterfaceGenerator(DeviceMetadata deviceMetadata, string ns) }; _deviceTemplate.Initialize(InterfaceImplementation.DeviceFileName, errors, session); _asyncDeviceTemplate.Initialize(InterfaceImplementation.AsyncDeviceFileName, errors, session); + isApplicationDevice = deviceMetadata.IsApplicationDevice; } /// @@ -41,7 +43,7 @@ public InterfaceGenerator(DeviceMetadata deviceMetadata, string ns) /// The generated device interface implementation. public InterfaceImplementation GenerateImplementation() => new(Device: _deviceTemplate.TransformText(), - AsyncDevice: _asyncDeviceTemplate.TransformText()); + AsyncDevice: isApplicationDevice ? _asyncDeviceTemplate.TransformText() : string.Empty); } /// @@ -72,7 +74,8 @@ public record struct InterfaceImplementation(string Device, string AsyncDevice) public readonly IEnumerator> GetEnumerator() { yield return new(DeviceFileName, Device); - yield return new(AsyncDeviceFileName, AsyncDevice); + if (!string.IsNullOrEmpty(AsyncDevice)) + yield return new(AsyncDeviceFileName, AsyncDevice); } readonly IEnumerator IEnumerable.GetEnumerator() diff --git a/src/Python.cs b/src/Python.cs index 5f9f146..9ba43f8 100644 --- a/src/Python.cs +++ b/src/Python.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Linq; using Bonsai.Harp; namespace Harp.Generators; @@ -77,7 +76,6 @@ internal sealed class PythonRegister internal static partial class TemplateHelper { - const int CoreRegisterAddressLimit = 32; const string CoreMetadataResourceName = "Harp.Generators.core.yml"; static readonly Dictionary PrimitiveNumpyTypes = new() @@ -215,7 +213,7 @@ public static PythonModule BuildPythonModule(DeviceMetadata deviceMetadata) BuildPythonRegister(module, registerMetadata.Key, registerMetadata.Value, deviceMetadata); module.ProtocolImports.Add("RegisterBase"); - module.IsApplicationDevice = deviceMetadata.Registers.Values.All(register => register.Address >= CoreRegisterAddressLimit); + module.IsApplicationDevice = deviceMetadata.IsApplicationDevice; return module; } diff --git a/src/core.yml b/src/core.yml index 2610747..507c516 100644 --- a/src/core.yml +++ b/src/core.yml @@ -1,6 +1,5 @@ # yaml-language-server: $schema=https://harp-tech.org/draft-02/schema/registers.json description: The core register set every Harp device carries, and its address space. -device: Tests registers: WhoAmI: address: 0 diff --git a/tests/ExpectedOutput/core.cs b/tests/ExpectedOutput/core.cs index 720b8d1..bd7b8c5 100644 --- a/tests/ExpectedOutput/core.cs +++ b/tests/ExpectedOutput/core.cs @@ -1,41 +1,16 @@ -using Bonsai; using Bonsai.Harp; using System; using System.Collections.Generic; using System.ComponentModel; -using System.Linq; -using System.Reactive.Linq; -using System.Xml.Serialization; namespace Harp.Generators.Tests { - /// - /// Generates events and processes commands for the Tests device connected - /// at the specified serial port. - /// - [Combinator(MethodName = nameof(Generate))] - [WorkflowElementCategory(ElementCategory.Source)] - [Description("Generates events and processes commands for the Tests device.")] - public partial class Device : Bonsai.Harp.Device, INamedElement + public partial class Device { - /// - /// Represents the unique identity class of the device. - /// This field is constant. - /// - public const int WhoAmI = 0; - - /// - /// Initializes a new instance of the class. - /// - public Device() : base(WhoAmI) { } - - string INamedElement.Name => nameof(Tests); - /// /// Gets a read-only mapping from address to register type. /// - public static new IReadOnlyDictionary RegisterMap { get; } = new Dictionary - (Bonsai.Harp.Device.RegisterMap.ToDictionary(entry => entry.Key, entry => entry.Value)) + public static IReadOnlyDictionary RegisterMap { get; } = new Dictionary { { 0, typeof(WhoAmI) }, { 1, typeof(HardwareVersionHigh) }, @@ -53,375 +28,6 @@ public Device() : base(WhoAmI) { } { 13, typeof(SerialNumber) }, { 14, typeof(ClockConfiguration) } }; - - /// - /// Gets the contents of the metadata file describing the - /// device registers. - /// - public static readonly string Metadata = GetDeviceMetadata(); - - static string GetDeviceMetadata() - { - var deviceType = typeof(Device); - using var metadataStream = deviceType.Assembly.GetManifestResourceStream($"{deviceType.Namespace}.device.yml"); - using var streamReader = new System.IO.StreamReader(metadataStream); - return streamReader.ReadToEnd(); - } - } - - /// - /// Represents an operator that returns the contents of the metadata file - /// describing the device registers. - /// - [Description("Returns the contents of the metadata file describing the Tests device registers.")] - public partial class GetDeviceMetadata : Source - { - /// - /// Returns an observable sequence with the contents of the metadata file - /// describing the device registers. - /// - /// - /// A sequence with a single object representing the - /// contents of the metadata file. - /// - public override IObservable Generate() - { - return Observable.Return(Device.Metadata); - } - } - - /// - /// Represents an operator that groups the sequence of " messages by register type. - /// - [Description("Groups the sequence of Tests messages by register type.")] - public partial class GroupByRegister : Combinator> - { - /// - /// Groups an observable sequence of messages - /// by register type. - /// - /// The sequence of Harp device messages. - /// - /// A sequence of observable groups, each of which corresponds to a unique - /// register. - /// - public override IObservable> Process(IObservable source) - { - return source.GroupBy(message => Device.RegisterMap[message.Address]); - } - } - - /// - /// Represents an operator that writes the sequence of " messages - /// to the standard Harp storage format. - /// - [DefaultProperty(nameof(Path))] - [Description("Writes the sequence of Tests messages to the standard Harp storage format.")] - public partial class DeviceDataWriter : Sink, INamedElement - { - const string BinaryExtension = ".bin"; - const string MetadataFileName = "device.yml"; - readonly Bonsai.Harp.MessageWriter writer = new(); - - string INamedElement.Name => nameof(Tests) + "DataWriter"; - - /// - /// Gets or sets the relative or absolute path on which to save the message data. - /// - [Description("The relative or absolute path of the directory on which to save the message data.")] - [Editor("Bonsai.Design.SaveFileNameEditor, Bonsai.Design", DesignTypes.UITypeEditor)] - public string Path - { - get => System.IO.Path.GetDirectoryName(writer.FileName); - set => writer.FileName = System.IO.Path.Combine(value, nameof(Tests) + BinaryExtension); - } - - /// - /// Gets or sets a value indicating whether element writing should be buffered. If , - /// the write commands will be queued in memory as fast as possible and will be processed - /// by the writer in a different thread. Otherwise, writing will be done in the same - /// thread in which notifications arrive. - /// - [Description("Indicates whether writing should be buffered.")] - public bool Buffered - { - get => writer.Buffered; - set => writer.Buffered = value; - } - - /// - /// Gets or sets a value indicating whether to overwrite the output file if it already exists. - /// - [Description("Indicates whether to overwrite the output file if it already exists.")] - public bool Overwrite - { - get => writer.Overwrite; - set => writer.Overwrite = value; - } - - /// - /// Gets or sets a value specifying how the message filter will use the matching criteria. - /// - [Description("Specifies how the message filter will use the matching criteria.")] - public FilterType FilterType - { - get => writer.FilterType; - set => writer.FilterType = value; - } - - /// - /// Gets or sets a value specifying the expected message type. If no value is - /// specified, all messages will be accepted. - /// - [Description("Specifies the expected message type. If no value is specified, all messages will be accepted.")] - public MessageType? MessageType - { - get => writer.MessageType; - set => writer.MessageType = value; - } - - private IObservable WriteDeviceMetadata(IObservable source) - { - var basePath = Path; - if (string.IsNullOrEmpty(basePath)) - return source; - - var metadataPath = System.IO.Path.Combine(basePath, MetadataFileName); - return Observable.Create(observer => - { - Bonsai.IO.PathHelper.EnsureDirectory(metadataPath); - if (System.IO.File.Exists(metadataPath) && !Overwrite) - { - throw new System.IO.IOException(string.Format("The file '{0}' already exists.", metadataPath)); - } - - System.IO.File.WriteAllText(metadataPath, Device.Metadata); - return source.SubscribeSafe(observer); - }); - } - - /// - /// Writes each Harp message in the sequence to the specified binary file, and the - /// contents of the device metadata file to a separate text file. - /// - /// The sequence of messages to write to the file. - /// - /// An observable sequence that is identical to the - /// sequence but where there is an additional side effect of writing the - /// messages to a raw binary file, and the contents of the device metadata file - /// to a separate text file. - /// - public override IObservable Process(IObservable source) - { - return source.Publish(ps => ps.Merge( - WriteDeviceMetadata(writer.Process(ps.GroupBy(message => message.Address))) - .IgnoreElements() - .Cast())); - } - - /// - /// Writes each Harp message in the sequence of observable groups to the - /// corresponding binary file, where the name of each file is generated from - /// the common group register address. The contents of the device metadata file are - /// written to a separate text file. - /// - /// - /// A sequence of observable groups, each of which corresponds to a unique register - /// address. - /// - /// - /// An observable sequence that is identical to the - /// sequence but where there is an additional side effect of writing the Harp - /// messages in each group to the corresponding file, and the contents of the device - /// metadata file to a separate text file. - /// - public IObservable> Process(IObservable> source) - { - return WriteDeviceMetadata(writer.Process(source)); - } - - /// - /// Writes each Harp message in the sequence of observable groups to the - /// corresponding binary file, where the name of each file is generated from - /// the common group register name. The contents of the device metadata file are - /// written to a separate text file. - /// - /// - /// A sequence of observable groups, each of which corresponds to a unique register - /// type. - /// - /// - /// An observable sequence that is identical to the - /// sequence but where there is an additional side effect of writing the Harp - /// messages in each group to the corresponding file, and the contents of the device - /// metadata file to a separate text file. - /// - public IObservable> Process(IObservable> source) - { - return WriteDeviceMetadata(writer.Process(source)); - } - } - - /// - /// Represents an operator that filters register-specific messages - /// reported by the device. - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - [XmlInclude(typeof(WhoAmI))] - [XmlInclude(typeof(HardwareVersionHigh))] - [XmlInclude(typeof(HardwareVersionLow))] - [XmlInclude(typeof(AssemblyVersion))] - [XmlInclude(typeof(CoreVersionHigh))] - [XmlInclude(typeof(CoreVersionLow))] - [XmlInclude(typeof(FirmwareVersionHigh))] - [XmlInclude(typeof(FirmwareVersionLow))] - [XmlInclude(typeof(TimestampSeconds))] - [XmlInclude(typeof(TimestampMicroseconds))] - [XmlInclude(typeof(OperationControl))] - [XmlInclude(typeof(ResetDevice))] - [XmlInclude(typeof(DeviceName))] - [XmlInclude(typeof(SerialNumber))] - [XmlInclude(typeof(ClockConfiguration))] - [Description("Filters register-specific messages reported by the Tests device.")] - public class FilterRegister : FilterRegisterBuilder, INamedElement - { - /// - /// Initializes a new instance of the class. - /// - public FilterRegister() - { - Register = new WhoAmI(); - } - - string INamedElement.Name - { - get => $"{nameof(Tests)}.{GetElementDisplayName(Register)}"; - } - } - - /// - /// Represents an operator which filters and selects specific messages - /// reported by the Tests device. - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - [XmlInclude(typeof(WhoAmI))] - [XmlInclude(typeof(HardwareVersionHigh))] - [XmlInclude(typeof(HardwareVersionLow))] - [XmlInclude(typeof(AssemblyVersion))] - [XmlInclude(typeof(CoreVersionHigh))] - [XmlInclude(typeof(CoreVersionLow))] - [XmlInclude(typeof(FirmwareVersionHigh))] - [XmlInclude(typeof(FirmwareVersionLow))] - [XmlInclude(typeof(TimestampSeconds))] - [XmlInclude(typeof(TimestampMicroseconds))] - [XmlInclude(typeof(OperationControl))] - [XmlInclude(typeof(ResetDevice))] - [XmlInclude(typeof(DeviceName))] - [XmlInclude(typeof(SerialNumber))] - [XmlInclude(typeof(ClockConfiguration))] - [XmlInclude(typeof(TimestampedWhoAmI))] - [XmlInclude(typeof(TimestampedHardwareVersionHigh))] - [XmlInclude(typeof(TimestampedHardwareVersionLow))] - [XmlInclude(typeof(TimestampedAssemblyVersion))] - [XmlInclude(typeof(TimestampedCoreVersionHigh))] - [XmlInclude(typeof(TimestampedCoreVersionLow))] - [XmlInclude(typeof(TimestampedFirmwareVersionHigh))] - [XmlInclude(typeof(TimestampedFirmwareVersionLow))] - [XmlInclude(typeof(TimestampedTimestampSeconds))] - [XmlInclude(typeof(TimestampedTimestampMicroseconds))] - [XmlInclude(typeof(TimestampedOperationControl))] - [XmlInclude(typeof(TimestampedResetDevice))] - [XmlInclude(typeof(TimestampedDeviceName))] - [XmlInclude(typeof(TimestampedSerialNumber))] - [XmlInclude(typeof(TimestampedClockConfiguration))] - [Description("Filters and selects specific messages reported by the Tests device.")] - public partial class Parse : ParseBuilder, INamedElement - { - /// - /// Initializes a new instance of the class. - /// - public Parse() - { - Register = new WhoAmI(); - } - - string INamedElement.Name => $"{nameof(Tests)}.{GetElementDisplayName(Register)}"; - } - - /// - /// Represents an operator which formats a sequence of values as specific - /// Tests register messages. - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - [XmlInclude(typeof(WhoAmI))] - [XmlInclude(typeof(HardwareVersionHigh))] - [XmlInclude(typeof(HardwareVersionLow))] - [XmlInclude(typeof(AssemblyVersion))] - [XmlInclude(typeof(CoreVersionHigh))] - [XmlInclude(typeof(CoreVersionLow))] - [XmlInclude(typeof(FirmwareVersionHigh))] - [XmlInclude(typeof(FirmwareVersionLow))] - [XmlInclude(typeof(TimestampSeconds))] - [XmlInclude(typeof(TimestampMicroseconds))] - [XmlInclude(typeof(OperationControl))] - [XmlInclude(typeof(ResetDevice))] - [XmlInclude(typeof(DeviceName))] - [XmlInclude(typeof(SerialNumber))] - [XmlInclude(typeof(ClockConfiguration))] - [Description("Formats a sequence of values as specific Tests register messages.")] - public partial class Format : FormatBuilder, INamedElement - { - /// - /// Initializes a new instance of the class. - /// - public Format() - { - Register = new WhoAmI(); - } - - string INamedElement.Name => $"{nameof(Tests)}.{GetElementDisplayName(Register)}"; } /// @@ -1899,69 +1505,6 @@ public static Timestamped GetPayload(HarpMessage messag } } - /// - /// Represents an operator which creates standard message payloads for the - /// Tests device. - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - [XmlInclude(typeof(CreateWhoAmIPayload))] - [XmlInclude(typeof(CreateHardwareVersionHighPayload))] - [XmlInclude(typeof(CreateHardwareVersionLowPayload))] - [XmlInclude(typeof(CreateAssemblyVersionPayload))] - [XmlInclude(typeof(CreateCoreVersionHighPayload))] - [XmlInclude(typeof(CreateCoreVersionLowPayload))] - [XmlInclude(typeof(CreateFirmwareVersionHighPayload))] - [XmlInclude(typeof(CreateFirmwareVersionLowPayload))] - [XmlInclude(typeof(CreateTimestampSecondsPayload))] - [XmlInclude(typeof(CreateTimestampMicrosecondsPayload))] - [XmlInclude(typeof(CreateOperationControlPayload))] - [XmlInclude(typeof(CreateResetDevicePayload))] - [XmlInclude(typeof(CreateDeviceNamePayload))] - [XmlInclude(typeof(CreateSerialNumberPayload))] - [XmlInclude(typeof(CreateClockConfigurationPayload))] - [XmlInclude(typeof(CreateTimestampedWhoAmIPayload))] - [XmlInclude(typeof(CreateTimestampedHardwareVersionHighPayload))] - [XmlInclude(typeof(CreateTimestampedHardwareVersionLowPayload))] - [XmlInclude(typeof(CreateTimestampedAssemblyVersionPayload))] - [XmlInclude(typeof(CreateTimestampedCoreVersionHighPayload))] - [XmlInclude(typeof(CreateTimestampedCoreVersionLowPayload))] - [XmlInclude(typeof(CreateTimestampedFirmwareVersionHighPayload))] - [XmlInclude(typeof(CreateTimestampedFirmwareVersionLowPayload))] - [XmlInclude(typeof(CreateTimestampedTimestampSecondsPayload))] - [XmlInclude(typeof(CreateTimestampedTimestampMicrosecondsPayload))] - [XmlInclude(typeof(CreateTimestampedOperationControlPayload))] - [XmlInclude(typeof(CreateTimestampedResetDevicePayload))] - [XmlInclude(typeof(CreateTimestampedDeviceNamePayload))] - [XmlInclude(typeof(CreateTimestampedSerialNumberPayload))] - [XmlInclude(typeof(CreateTimestampedClockConfigurationPayload))] - [Description("Creates standard message payloads for the Tests device.")] - public partial class CreateMessage : CreateMessageBuilder, INamedElement - { - /// - /// Initializes a new instance of the class. - /// - public CreateMessage() - { - Payload = new CreateWhoAmIPayload(); - } - - string INamedElement.Name => $"{nameof(Tests)}.{GetElementDisplayName(Payload)}"; - } - /// /// Represents an operator that creates a message payload /// that specifies the identity class of the device. diff --git a/tests/InterfaceGeneratorTests.cs b/tests/InterfaceGeneratorTests.cs index 4998b8c..af3a5bb 100644 --- a/tests/InterfaceGeneratorTests.cs +++ b/tests/InterfaceGeneratorTests.cs @@ -34,7 +34,10 @@ public void DeviceTemplate_GenerateAndBuildWithoutErrors(string metadataFileName { CompilerTestHelper.CompileFromSource(implementation.Device, implementation.AsyncDevice, payloadExtensions, customImplementation); TestHelper.AssertExpectedOutput(implementation.Device, deviceOutputFileName); - TestHelper.AssertExpectedOutput(implementation.AsyncDevice, asyncDeviceOutputFileName); + if (deviceMetadata.IsApplicationDevice) + TestHelper.AssertExpectedOutput(implementation.AsyncDevice, asyncDeviceOutputFileName); + else + Assert.AreEqual(string.Empty, implementation.AsyncDevice, "Metadata describing only common registers should generate no asynchronous interface."); } catch (AssertFailedException) {