diff --git a/cmf-cli/Commands/new/iot/GenerateDriverCommand.cs b/cmf-cli/Commands/new/iot/GenerateDriverCommand.cs index 067963f14..085fa75cc 100644 --- a/cmf-cli/Commands/new/iot/GenerateDriverCommand.cs +++ b/cmf-cli/Commands/new/iot/GenerateDriverCommand.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.CommandLine; using System.IO.Abstractions; +using System.Linq; using System.Threading.Tasks; namespace Cmf.CLI.Commands.New.IoT @@ -84,7 +85,9 @@ public void Execute(IDirectoryInfo workingDir) driver.PackageFullName, driver.PackageVersion, driver.HasCommands, - driver.HasTemplates); + driver.HasTemplates, + driver.DriverBaseClass, + driver.DriverBasePackage); this.CommandName = "iot-driver"; base.RunCommand(args); } @@ -103,6 +106,20 @@ private DriverValues HandleDriver(DriverValues driver) driver.HasCommands = AnsiConsole.Prompt(new ConfirmationPrompt("Does the protocol support commands?") { DefaultValue = driver.HasCommands }); driver.HasTemplates = AnsiConsole.Prompt(new ConfirmationPrompt("Do you wish to use templates? (By saying no, you will only have events and commands from the driver definition)") { DefaultValue = driver.HasTemplates }); + if (ExecutionContext.Instance.ProjectConfig.MESVersion.Major >= 12) + { + driver.DriverBaseClass = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Which driver base class does this driver extend?") + .UseConverter(name => DriverBases.All.TryGetValue(name, out var metadata) + ? $"{name} - {metadata.Description}" + : name) + .AddChoices(DriverBases.All.Keys) + .DefaultValue("DeviceDriverBase")); + + driver.DriverBasePackage = DriverBases.GetPackageName(driver.DriverBaseClass); + } + return driver; } @@ -116,7 +133,9 @@ private List GenerateArgs( string packageName, string packageVersion, bool hasCommands, - bool hasTemplates) + bool hasTemplates, + string driverBaseClass, + string driverBasePackage) { var mesVersion = ExecutionContext.Instance.ProjectConfig.MESVersion; Log.Debug($"Creating IoT Driver at {packageLocation}"); @@ -132,7 +151,9 @@ private List GenerateArgs( "--packageVersion", packageVersion, "--npmRegistry", ExecutionContext.Instance.ProjectConfig.NPMRegistry.ToString(), "--hasCommands", hasCommands.ToString(), - "--hasTemplates", hasTemplates.ToString() + "--hasTemplates", hasTemplates.ToString(), + "--driverBaseClass", driverBaseClass, + "--driverBasePackage", driverBasePackage }); return args; diff --git a/cmf-cli/Utilities/IoTStructures.cs b/cmf-cli/Utilities/IoTStructures.cs index 78d8cb342..1da7e808b 100644 --- a/cmf-cli/Utilities/IoTStructures.cs +++ b/cmf-cli/Utilities/IoTStructures.cs @@ -164,6 +164,12 @@ public class DriverValues [JsonIgnore] public bool HasTemplates { get; set; } + + [JsonIgnore] + public string DriverBaseClass { get; set; } = "DeviceDriverBase"; + + [JsonIgnore] + public string DriverBasePackage { get; set; } = "@criticalmanufacturing/connect-iot-driver"; } [JsonObject] @@ -483,4 +489,39 @@ public static class TaskBases { "SystemRequestReplyTaskBase", "Exclusive to Data Flow. Sends replies to a SystemRequestListenerTaskBase (for example, \"Send System Action Reply\" task)." }, }; } + + public class DriverBaseMetadata + { + public string Description { get; set; } + public string PackageName { get; set; } + } + + public static class DriverBases + { + public static readonly Dictionary All = new() + { + { + "DeviceDriverBase", new DriverBaseMetadata + { + Description = "(Default): Base class for standard driver operations.", + PackageName = "@criticalmanufacturing/connect-iot-driver" + } + }, + { + "DeviceFileDriverBase", new DriverBaseMetadata + { + Description = "Extends standard driver behavior with file-based operations.", + PackageName = "@criticalmanufacturing/connect-iot-base-file-driver" + } + }, + }; + + public static string GetPackageName(string driverBaseClass) + { + return !string.IsNullOrWhiteSpace(driverBaseClass) + && All.TryGetValue(driverBaseClass, out var metadata) + ? metadata.PackageName + : "@criticalmanufacturing/connect-iot-driver"; + } + } } diff --git a/cmf-cli/resources/template_feed/iot/driver/.template.config/template.json b/cmf-cli/resources/template_feed/iot/driver/.template.config/template.json index d692ee8be..249e62316 100644 --- a/cmf-cli/resources/template_feed/iot/driver/.template.config/template.json +++ b/cmf-cli/resources/template_feed/iot/driver/.template.config/template.json @@ -107,6 +107,32 @@ "displayName": "Driver Has Templates", "defaultValue": "false" }, + "driverBaseClass": { + "type": "parameter", + "datatype": "string", + "description": "Driver base class", + "displayName": "Driver Base Class", + "defaultValue": "DeviceDriverBase", + "replaces": "<%= $CLI_PARAM_DriverBaseClass %>" + }, + "driverBasePackage": { + "type": "parameter", + "datatype": "string", + "description": "Package that contains the selected driver base class", + "displayName": "Driver Base Package", + "defaultValue": "@criticalmanufacturing/connect-iot-driver", + "replaces": "<%= $CLI_PARAM_DriverBasePackage %>" + }, + "isDeviceFileDriverBase": { + "type": "computed", + "datatype": "bool", + "value": "driverBaseClass == \"DeviceFileDriverBase\"" + }, + "isDeviceDriverBase": { + "type": "computed", + "datatype": "bool", + "value": "driverBaseClass == \"DeviceDriverBase\"" + }, "v1120OrGreater": { "type": "generated", "generator": "regexMatch", diff --git a/cmf-cli/resources/template_feed/iot/driver/src/%directoryname%/package.json b/cmf-cli/resources/template_feed/iot/driver/src/%directoryname%/package.json index aac01c0f8..1db7b33b0 100644 --- a/cmf-cli/resources/template_feed/iot/driver/src/%directoryname%/package.json +++ b/cmf-cli/resources/template_feed/iot/driver/src/%directoryname%/package.json @@ -44,6 +44,9 @@ }, "dependencies": { "@criticalmanufacturing/connect-iot-common": "<%= $CLI_PARAM_TargetSystemVersionProcessed %>", + //#if (isDeviceFileDriverBase) + "@criticalmanufacturing/connect-iot-base-file-driver": "<%= $CLI_PARAM_TargetSystemVersionProcessed %>", + //#endif "@criticalmanufacturing/connect-iot-driver": "<%= $CLI_PARAM_TargetSystemVersionProcessed %>", "inversify": "6.0.2", "moment": "2.29.4", diff --git a/cmf-cli/resources/template_feed/iot/driver/src/%directoryname%/src/driverImplementation.ts b/cmf-cli/resources/template_feed/iot/driver/src/%directoryname%/src/driverImplementation.ts index fa534a00b..e6e3f0ef9 100644 --- a/cmf-cli/resources/template_feed/iot/driver/src/%directoryname%/src/driverImplementation.ts +++ b/cmf-cli/resources/template_feed/iot/driver/src/%directoryname%/src/driverImplementation.ts @@ -1,6 +1,7 @@ import { injectable, inject, Container } from "inversify"; import { CommunicationState, PropertyValuePair } from "@criticalmanufacturing/connect-iot-driver"; -import { Property, EventOccurrence, PropertyValue, Command, Event as EquipmentEvent, DeviceDriverBase, CommandParameter } from "@criticalmanufacturing/connect-iot-driver"; +import { Property, EventOccurrence, PropertyValue, Command, Event as EquipmentEvent, CommandParameter } from "@criticalmanufacturing/connect-iot-driver"; +import { <%= $CLI_PARAM_DriverBaseClass %> } from "<%= $CLI_PARAM_DriverBasePackage %>"; import { <%= $CLI_PARAM_Identifier %>CommunicationSettings, <%= $CLI_PARAM_IdentifierCamel %>DefaultCommunicationSettings, validateCommunicationParameters } from "./communicationSettings"; import { validateProperties, validateEvents, validateEventProperties, validateCommands, validateCommandParameters } from "./extendedData/index"; import { Utils } from "@criticalmanufacturing/connect-iot-common"; @@ -10,7 +11,7 @@ import { ExtensionHandler } from "./extensions"; //#endif @injectable() -export class <%= $CLI_PARAM_Identifier %>DeviceDriver extends DeviceDriverBase { +export class <%= $CLI_PARAM_Identifier %>DeviceDriver extends <%= $CLI_PARAM_DriverBaseClass %> { private _communicationSettings: <%= $CLI_PARAM_Identifier %>CommunicationSettings; private _container: Container; diff --git a/cmf-cli/resources/template_feed/iot/driver/src/%directoryname%/src/inversify.config.ts b/cmf-cli/resources/template_feed/iot/driver/src/%directoryname%/src/inversify.config.ts index 4a1f054e7..9a05034d3 100644 --- a/cmf-cli/resources/template_feed/iot/driver/src/%directoryname%/src/inversify.config.ts +++ b/cmf-cli/resources/template_feed/iot/driver/src/%directoryname%/src/inversify.config.ts @@ -2,6 +2,9 @@ import "reflect-metadata"; import { Container } from "inversify"; import { DeviceDriver, TYPES as COMMUNICATION_TYPES, container as driverContainer } from "@criticalmanufacturing/connect-iot-driver"; +//#if (isDeviceFileDriverBase) +import { FileHandler, MountPointHandler } from "<%= $CLI_PARAM_DriverBasePackage %>"; +//#endif import { TYPES } from "./types"; import { <%= $CLI_PARAM_Identifier %>DeviceDriver } from "./driverImplementation"; //#if hasTemplates @@ -11,6 +14,10 @@ import { ExtensionHandler } from "./extensions"; const container = new Container(); container.parent = driverContainer; container.parent?.bind(TYPES.Injector).toConstantValue(container); +//#if (isDeviceFileDriverBase) +container.parent?.bind(COMMUNICATION_TYPES.FileHandler).to(FileHandler).inSingletonScope(); +container.parent?.bind(COMMUNICATION_TYPES.MountPointHandler).to(MountPointHandler).inSingletonScope(); +//#endif //#if hasTemplates container.bind(TYPES.ExtensionHandler).to(ExtensionHandler).inSingletonScope(); //#endif diff --git a/tests/Specs/New.cs b/tests/Specs/New.cs index 5ecb7528f..f37c66095 100644 --- a/tests/Specs/New.cs +++ b/tests/Specs/New.cs @@ -440,6 +440,10 @@ public void IoTDriverDefaultValues(string mesVersion) console.Input.PushTextWithEnter(""); // Identifier console.Input.PushTextWithEnter(""); // HasCommands console.Input.PushTextWithEnter(""); // HasTemplates + if (Version.Parse(mesVersion).Major >= 12) + { + console.Input.PushTextWithEnter(""); // DriverBaseClass: DeviceDriverBase (default) + } AnsiConsole.Console = console; // so that the prompts asked by the command use this console instance @@ -468,6 +472,87 @@ public void IoTDriverDefaultValues(string mesVersion) } } + [Theory, Trait("TestCategory", "Integration")] + [InlineData("12.0.0", "DeviceDriverBase")] + [InlineData("12.0.0", "DeviceFileDriverBase")] + public void IoTDriverBase(string mesVersion, string driverBaseClass) + { + string dir = TestUtilities.GetTmpDirectory(); + string packageId = "Cmf.Custom.IoT"; + string packageFolderPackages = "Cmf.Custom.IoT.Packages"; + + var cur = Directory.GetCurrentDirectory(); + + try + { + CopyNewFixture(dir, mesVersion: mesVersion); + RunNew(new IoTCommand(), packageId, dir); + + Directory.SetCurrentDirectory($"{dir}/{packageId}/{packageFolderPackages}"); + + TestingConsole.TestConsole console = new(); + console.Profile.Capabilities.Interactive = true; + console.Input.PushTextWithEnter(""); // directory name: driver-sample (default) + console.Input.PushTextWithEnter(""); // package scope (default) + console.Input.PushTextWithEnter(""); // package name (default) + console.Input.PushTextWithEnter(""); // package version (default) + console.Input.PushTextWithEnter(""); // identifier: SampleDriver (default) + console.Input.PushTextWithEnter(""); // HasCommands: No (default false) + console.Input.PushTextWithEnter(""); // HasTemplates: No (default false) + + var driverBaseChoices = new[] { "DeviceDriverBase", "DeviceFileDriverBase" }; + int driverBaseIndex = Array.IndexOf(driverBaseChoices, driverBaseClass); + for (int i = 0; i < driverBaseIndex; i++) + { + console.Input.PushKey(ConsoleKey.DownArrow); + } + console.Input.PushTextWithEnter(""); // confirm driver base selection + + AnsiConsole.Console = console; + + var cmd = new Command("x"); + var newCommand = new GenerateDriverCommand(); + newCommand.Configure(cmd); + TestUtilities.GetParser(cmd).Invoke(""); + + Directory.Exists(Path.GetFullPath("src/driver-sample")).Should().BeTrue("Driver folder should be generated"); + + var driverImpl = File.ReadAllText(Path.GetFullPath("src/driver-sample/src/driverImplementation.ts")); + var inversifyConfig = File.ReadAllText(Path.GetFullPath("src/driver-sample/src/inversify.config.ts")); + var packageJson = File.ReadAllText(Path.GetFullPath("src/driver-sample/package.json")); + + if (driverBaseClass == "DeviceFileDriverBase") + { + driverImpl.Should().Contain("DeviceFileDriverBase", + "Driver should import DeviceFileDriverBase when needsFileAccess is true"); + driverImpl.Should().NotContain("DeviceDriverBase", + "Driver should not import DeviceDriverBase when needsFileAccess is true"); + driverImpl.Should().Contain("extends DeviceFileDriverBase", + "Driver class should extend DeviceFileDriverBase when needsFileAccess is true"); + inversifyConfig.Should().Contain("FileHandler", + "inversify.config should bind FileHandler when needsFileAccess is true"); + packageJson.Should().Contain("connect-iot-base-file-driver", + "package.json should include connect-iot-base-file-driver when needsFileAccess is true"); + } + else + { + driverImpl.Should().NotContain("DeviceFileDriverBase", + "Driver should not reference DeviceFileDriverBase when needsFileAccess is false"); + driverImpl.Should().Contain("extends DeviceDriverBase", + "Driver class should extend DeviceDriverBase when needsFileAccess is false"); + inversifyConfig.Should().NotContain("FileHandler", + "inversify.config should not bind FileHandler when needsFileAccess is false"); + packageJson.Should().NotContain("connect-iot-base-file-driver", + "package.json should not include connect-iot-base-file-driver when needsFileAccess is false"); + } + } + finally + { + Directory.SetCurrentDirectory(cur); + Directory.Delete(dir, true); + } + } + [Theory, Trait("TestCategory", "Integration")] [InlineData("11.2.0", "testName", false)] [InlineData("11.3.0", "", false)]