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) {