IdbMigrationStrategy for Device ExtensionPoints

I pushed a branch to my Modbus server driver that has WIP migration for 8.3.

One of the things it provides an example of is migration from settings to config: modbus-server-driver/msd-gateway/src/main/java/com/kevinherron/ignition/modbus/ModbusServerModuleHook.java at a00b3e4cfc85775f853156db76987ac0e862beeb · kevinherron/modbus-server-driver · GitHub

@pturmel you may find this useful if you haven't already worked this out.

TODO: config validation and default values

1 Like

Any reason I can't like this topic?

I don't know, maybe something about how every topic in this category seems to have a vote associated with it. Not sure who set this up.

edit: just removed the vote thing :person_shrugging:

1 Like

BTW, this ended up being very helpful. I stumbled around on this for quite a while until I figured out how to use the augmenter. (Due to some oddities in my modules to work around Wicket. :roll_eyes: And to continue using XML for some stuff.)

I'll probably have a pile of v8.3 alphas to share next week.

There's a PR waiting on QA that breaks some Milo API stuff (just a simple Search and Replace), and after that is merged and published I'll update the example with the API changes and see about adding config validation as well.

I haven't looked hard, but haven't stumbled across the new methods for supplying documentation and defaults for config items. Annotations?

Actually saw that in my Github notifications earlier. No worries.

Yeah, boatloads of ugly annotations that eventually make their way into the JSON schema for that resource type. But at least you don't have to create the schema by hand.

Link?

Not sure our standards for "ugly" are widely shared, fwiw.

Oh, is there support for option strings?

I don't have a link, but here's an example:

    public record Connectivity(
        @FormCategory("CONNECTIVITY")
        @FormField(FormFieldType.TEXT)
        @Label("Hostname *")
        @Required
        @Description("Hostname/IP address of the device.")
        String hostname,

        @FormCategory("CONNECTIVITY")
        @Label("Port *")
        @FormField(FormFieldType.NUMBER)
        @Maximum("65535")
        @Minimum("1")
        @DefaultValue("102")
        @Required
        @Description("Port to connect to.")
        int port,

        @FormCategory("CONNECTIVITY")
        @Label("Timeout *")
        @FormField(FormFieldType.NUMBER)
        @DefaultValue("30000")
        @Required
        @Description("Maximum amount of time to wait for a response in milliseconds.")
        Duration timeout
    ) {
        private void validate(ValidationErrors.Builder errors) {
            errors.requireNotBlank(
                "connectivity.hostname",
                hostname
            ).checkField(
                port >= 1 && port <= 65535,
                "connectivity.port",
                "Port must be between 1 and 65535"
            ).checkField(
                timeout.toMillis() >= 1 && timeout.toMillis() <= Integer.MAX_VALUE,
                "connectivity.timeout",
                "Timeout must not be negative and must be less than 24 days" // Int.MAX_VALUE in millis is ~24 days
            );
        }

    }
1 Like

I have my EtherNet/IP module building and installing cleanly, but it won't show a Web UI when I try to create an instance.

Web UI Component type not found

I tried to build and install your modbus server as a point of comparison, but it utterly crashes my v83 install. With this:

INFO   | jvm 1    | 2025/02/23 16:46:51 | E [C.BasicExecutionEngine        ] [16:46:51.135]: One-shot task com.inductiveautomation.ignition.gateway.IgnitionGateway$$Lambda$616/0x00000001003f2c10@449faeef threw uncaught exception. 
INFO   | jvm 1    | 2025/02/23 16:46:51 | java.lang.AbstractMethodError: Missing implementation of resolved method 'abstract java.util.List getDeviceExtensionPoints()' of abstract class com.inductiveautomation.ignition.gateway.opcua.server.api.AbstractDeviceModuleHook.
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at com.inductiveautomation.ignition.gateway.opcua.server.api.AbstractDeviceModuleHook.getExtensionPoints(AbstractDeviceModuleHook.java:53)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl.lambda$getExtensionPoints$16(ModuleManagerImpl.java:652)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at java.base/java.util.stream.ReferencePipeline$7$1.accept(Unknown Source)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at java.base/java.util.stream.ReferencePipeline$2$1.accept(Unknown Source)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at java.base/java.util.stream.ReferencePipeline$3$1.accept(Unknown Source)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at java.base/java.util.HashMap$ValueSpliterator.forEachRemaining(Unknown Source)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at java.base/java.util.stream.AbstractPipeline.copyInto(Unknown Source)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(Unknown Source)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at java.base/java.util.stream.AbstractPipeline.evaluate(Unknown Source)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at java.base/java.util.stream.AbstractPipeline.evaluateToArrayNode(Unknown Source)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at java.base/java.util.stream.ReferencePipeline.toArray(Unknown Source)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at java.base/java.util.stream.ReferencePipeline.toArray(Unknown Source)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at java.base/java.util.stream.ReferencePipeline.toList(Unknown Source)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at com.inductiveautomation.ignition.gateway.modules.ModuleManagerImpl.getExtensionPoints(ModuleManagerImpl.java:654)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at com.inductiveautomation.ignition.gateway.config.ImmutableExtensionPointCollection.build(ImmutableExtensionPointCollection.java:83)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at com.inductiveautomation.ignition.gateway.config.ImmutableExtensionPointCollection.build(ImmutableExtensionPointCollection.java:63)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at com.inductiveautomation.ignition.gateway.secrets.SecretProviderManagerImpl.<init>(SecretProviderManagerImpl.java:57)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at com.inductiveautomation.ignition.gateway.IgnitionGateway.startupInternal(IgnitionGateway.java:920)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at com.inductiveautomation.ignition.gateway.redundancy.RedundancyManagerImpl.startup(RedundancyManagerImpl.java:332)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at com.inductiveautomation.ignition.gateway.IgnitionGateway.initRedundancy(IgnitionGateway.java:751)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at com.inductiveautomation.ignition.gateway.IgnitionGateway.lambda$initInternal$2(IgnitionGateway.java:675)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at com.inductiveautomation.ignition.common.execution.impl.BasicExecutionEngine$ThrowableCatchingRunnable.run(BasicExecutionEngine.java:550)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(Unknown Source)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
INFO   | jvm 1    | 2025/02/23 16:46:51 |       at java.base/java.lang.Thread.run(Unknown Source)

Hints?

(I did upgrade to the latest deployable, which has Thursday's date on its files.)

Edit: Rebuilt the Modbus Server and it doesn't crash now. :man_shrugging:

However, it also gets Web UI Component type not found

I guess there's still some missing infrastructure?

Perhaps, I have no idea how the web stuff hooks up yet, or even if it’s a different story for third vs first party modules. I’ll have to look into this a bit.

I’ve only verified my Modbus Server driver compiles for 8.3 at this point.

Fair enough. I'll get all of my modules building and cleanly installing for now. Two down, five to go. (Plus a few non-critical ones.)

I had a quick look, and it's fairly trivial if all you need is an auto-generated config page for your device extension point.

See ~ basic extension point config UI · kevinherron/modbus-server-driver@1eafe2a · GitHub

Adding custom UI seems a little more involved, but it looks like it begins with:

  1. define some custom JS file you want to register:
private static final SystemJsModule JS_MODULE = new SystemJsModule(
  MODULE_ID, "/res/mitsubishi/js/web-ui/driver-mitsubishi.js");
  1. register it in GatewayModuleHook::setup:
context.getWebResourceManager().getSystemJsModuleRegistry().add(JS_MODULE);
  1. maybe some kind of menu action returned from your impl of DeviceExtensionPoint?
public List<MenuAction> getMenuActions() {
    return List.of(new MenuAction(
        "Addresses",
        "Device Configuration",
        new ReactComponentInfo("DeviceConfiguration", JS_MODULE))
    );
}

as for what is actually expected of your JS file... no idea. Probably some kind of react BS. I'll have to talk to some front end guys to see where we are at with an SDK example I guess.

2 Likes

For most, this will do.

I do want this for a few.

I saw hints of that while spelunking.

Huh. Missed that entirely. Will poke around some more.

Made progress, and now have config UIs showing up for both your module and mine.

However. Neither actually works. Unlike in v8.1, my Device implementation is getting called to browse/gather nodes that do not belong to my driver, and to read attributes that don't exist for variable nodes (UserExecutable and EventNotifier, in particular), and I can't seem get past that to get to a working subscription item.

(Your modbus server driver makes folders on browse where I expected variables, like HR0.)

(Your Logix driver is also timing out on browse.)

Sigh. Time for a break.

This is expected, there are some places in the server implementation where attributes are read indiscriminately before the NodeClass is known.

A call to gather is expected - gather is called when some Node you don't control is being browsed to see if you have a reference to contribute. browse should only get called for Nodes that pass the AddressSpaceFilter, though... I think.

What should I return for those?

Yes, I found those Milo locations from my backtraces. I tried returning nulls in the DataValue's Variant, but to no effect. :man_shrugging:

Bad_AttributeIdInvalid I think.

I tried that first. Boom.