FraunhoferIOSB / FAAAST-Service

FA³ST - Fraunhofer Advanced Asset Administration Shell Tools (for Digital Twins)
Other
59 stars 12 forks source link

How to connect AAS model with OPC UA server #221

Closed jongunee closed 1 year ago

jongunee commented 2 years ago

Describe the bug A clear and concise description of what the bug is.

To Reproduce Steps to reproduce the behavior:

  1. Start separate OPC UA server
  2. Run FA3ST service as a docker-compose with custom AAS model and config file

I followed your below example in your document, but i don't know how to apply exactly

{
    "@class": "de.fraunhofer.iosb.ilt.faaast.service.assetconnection.opcua.OpcUaAssetConnection",
    "host": "opc.tcp://localhost:4840",
    "valueProviders":
    {
        "(Submodel)[IRI]urn:aas:id:example:submodel:1,(Property)[ID_SHORT]Property1":
        {
            "nodeId": "some.node.id.property.1"
        },
        "(Submodel)[IRI]urn:aas:id:example:submodel:1,(Property)[ID_SHORT]Property2":
        {
            "nodeId": "some.node.id.property.2"
        }
    },
    "operationProviders":
    {
        "(Submodel)[IRI]urn:aas:id:example:submodel:1,(Operation)[ID_SHORT]Operation1":
        {
            "nodeId": "some.node.id.operation.1"
        }
    },
    "subscriptionProviders":
    {
        "(Submodel)[IRI]urn:aas:id:example:submodel:1,(Property)[ID_SHORT]Property3":
        {
            "nodeId": "some.node.id.property.3",
            "interval": 1000
        }
    }
}

Reference below:

AirCoditionerAAS-simple-operation.json ```json { "assetAdministrationShells": [ { "assetInformation": { "assetKind": "Instance", "globalAssetId": { "keys": [ { "idType": "Iri", "type": "Asset", "value": "https://example.com/ids/asset/9294_1051_9022_4665" } ] } }, "submodels": { "keys": [ { "type": "Submodel", "value": "https://example.com/ids/sm/3585_1051_9022_0802", "idType": "Iri" } ] }, "identification": { "idType": "Iri", "id": "https://example.com/ids/aas/5245_1051_9022_2232" }, "idShort": "AirConditionerAAS", "modelType": { "name": "AssetAdministrationShell" } } ], "assets": [ { "identification": { "idType": "Iri", "id": "https://example.com/ids/asset/9294_1051_9022_4665" }, "idShort": "AirConditioner", "modelType": { "name": "Asset" }, "kind": "Instance" } ], "submodels": [ { "semanticId": { "keys": [ { "idType": "Iri", "type": "GlobalReference", "value": "0112/2///61360_4#AAA650#001" } ] }, "identification": { "idType": "Iri", "id": "https://example.com/ids/sm/3585_1051_9022_0802" }, "idShort": "ConditionMonitoring", "modelType": { "name": "Submodel" }, "kind": "Instance", "submodelElements": [ { "value": "", "semanticId": { "keys": [ { "idType": "Iri", "type": "GlobalReference", "value": "0112/2///61360_4#AAA622#001" } ] }, "idShort": "Humidity", "category": "Variable", "modelType": { "name": "Property" }, "valueType": "double", "kind": "Instance", "descriptions": [ { "language": "en", "text": "degree of wetness of the atmosphere measured in the KETI meeting room" } ] }, { "value": "", "semanticId": { "keys": [ { "idType": "Iri", "type": "GlobalReference", "value": "0112/2///61360_4#AUA420#001" } ] }, "idShort": "State", "category": "Variable", "modelType": { "name": "Property" }, "valueType": "int", "kind": "Instance", "descriptions": [ { "language": "en", "text": "state of an object active or running" } ] }, { "value": "", "semanticId": { "keys": [ { "idType": "Iri", "type": "GlobalReference", "value": "https://example.com/ids/cd/5110_8051_9022_2974" } ] }, "idShort": "Start", "modelType": { "name": "Operation" }, "kind": "Instance", "descriptions": [ { "language": "en", "text": "call the method to start an air conditioner" } ] }, { "value": "", "semanticId": { "keys": [ { "idType": "Iri", "type": "GlobalReference", "value": "https://example.com/ids/cd/9301_8051_9022_9990" } ] }, "idShort": "Stop", "modelType": { "name": "Operation" }, "kind": "Instance", "descriptions": [ { "language": "en", "text": "call the method to stop an air conditioner" } ] } ], "descriptions": [] } ], "conceptDescriptions": [ { "modelType": { "name": "ConceptDescription" }, "administration": { "revision": "0", "version": "0.9" }, "identification": { "idType": "Iri", "id": "https://example.com/ids/cd/0491_5022_9022_2061" }, "idShort": "TestConceptDescription", "isCaseOf": [ { "keys": [ { "idType": "Iri", "type": "GlobalReference", "value": "http://example.com/DataSpecifications/ConceptDescriptions/TestConceptDescription" } ] } ], "description": [ { "language": "en-us", "text": "An example concept description for the test application" }, { "language": "de", "text": "Ein Beispiel-ConceptDescription für eine Test-Anwendung" } ] } ] } ```
AirConditionAASConf.json ```json { "core": { "requestHandlerThreadPoolSize": 2 }, "endpoints": [ { "@class": "de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.HttpEndpoint", "port": 8080 }, { "@class": "de.fraunhofer.iosb.ilt.faaast.service.endpoint.opcua.OpcUaEndpoint", "tcpPort": 18123 } ], "persistence": { "@class": "de.fraunhofer.iosb.ilt.faaast.service.persistence.memory.PersistenceInMemory", "initialModel": "AASEnv.json", "decoupleEnvironment": true }, "messageBus": { "@class": "de.fraunhofer.iosb.ilt.faaast.service.messagebus.internal.MessageBusInternal" }, "assetConnections": { "@class": "de.fraunhofer.iosb.ilt.faaast.service.assetconnection.opcua.OpcUaAssetConnection", "host": "opc.tcp://:", "valueProviders": { "(Submodel)(local)[IRI]0112/2///61360_4#AAA650#001,(GlobalReference)(local)[IRI]0112/2///61360_4#AUA420#001": { "nodeId": "ns=3;s=AirConditioner_1.State" } } } } ```

Can i know how to apply with configuration json file? Is there any example or other detailed explanation?

Expected behavior AAS property and opc ua server data are mapped

Output It is built successfully without "assetConnections" part in configuration file. but, I cannot get opc ua server data on my aas model

mjacoby commented 2 years ago

There are several issues with your example

Your example (simplified) was

{
    "core": {...},
    "endpoints": [...],
    "persistence": {...},
    "messageBus":  {
        "@class": "de.fraunhofer.iosb.ilt.faaast.service.messagebus.internal.MessageBusInternal",
        "host": "opc.tcp://<myip>:<myport>",
        "valueProviders": {...}
    }
}

but should be

{
    "core": {...},
    "endpoints": [...],
    "persistence":{...},
    "messageBus": {...},
    "assetConnections": [
        {
            "@class": "de.fraunhofer.iosb.ilt.faaast.service.assetconnection.opcua.OpcUaAssetConnection",
            "host": "opc.tcp://<myip>:<myport>",
            "valueProviders": {...}
        }
    ]
}
Complete updated config file ```js { "core": { "requestHandlerThreadPoolSize": 2 }, "endpoints": [ { "@class": "de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.HttpEndpoint", "port": 8080 }, { "@class": "de.fraunhofer.iosb.ilt.faaast.service.endpoint.opcua.OpcUaEndpoint", "tcpPort": 18123 } ], "persistence": { "@class": "de.fraunhofer.iosb.ilt.faaast.service.persistence.memory.PersistenceInMemory", "initialModel": "AASEnv.json", "decoupleEnvironment": true }, "messageBus": { "@class": "de.fraunhofer.iosb.ilt.faaast.service.messagebus.internal.MessageBusInternal" }, "assetConnections": [ { "@class": "de.fraunhofer.iosb.ilt.faaast.service.assetconnection.opcua.OpcUaAssetConnection", "host": "opc.tcp://:", "operationProviders": { "(Submodel)[IRI]https://example.com/ids/sm/3585_1051_9022_0802,(Operation)[ID_SHORT]Start": { "nodeId": "ns=3;s=AirConditioner_1.State" } } } ] } ```
jongunee commented 2 years ago

Really thank you!!

It is working well, but i have one another question.

I changed the configuration file following your comments like below.

AirConditionAASConf.json ```json { "core": { "requestHandlerThreadPoolSize": 2 }, "endpoints": [ { "@class": "de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.HttpEndpoint", "port": 8080 }, { "@class": "de.fraunhofer.iosb.ilt.faaast.service.endpoint.opcua.OpcUaEndpoint", "tcpPort": 18123 } ], "persistence": { "@class": "de.fraunhofer.iosb.ilt.faaast.service.persistence.memory.PersistenceInMemory", "initialModel": "AASEnv.json", "decoupleEnvironment": true }, "messageBus": { "@class": "de.fraunhofer.iosb.ilt.faaast.service.messagebus.internal.MessageBusInternal" }, "assetConnections": [ { "@class": "de.fraunhofer.iosb.ilt.faaast.service.assetconnection.opcua.OpcUaAssetConnection", "host": "opc.tcp://172.21.50.35:48010", "valueProviders": { "(Submodel)[IRI]https://example.com/ids/sm/3585_1051_9022_0802,(Property)[ID_SHORT]State": { "nodeId": "ns=3;s=AirConditioner_1.State" } }, "operationProviders": { "(Submodel)[IRI]https://example.com/ids/sm/3585_1051_9022_0802,(Operation)[ID_SHORT]Start": { "nodeId": "ns=3;s=AirConditioner_1.Start" }, "(Submodel)[IRI]https://example.com/ids/sm/3585_1051_9022_0802,(Operation)[ID_SHORT]Stop": { "nodeId": "ns=3;s=AirConditioner_1.Stop" } }, "subscriptionProviders": { "(Submodel)[IRI]https://example.com/ids/sm/3585_1051_9022_0802,(Property)[ID_SHORT]Humidity": { "nodeId": "ns=3;s=AirConditioner_1.Humidity", "interval": 1000 } } } ] } ```

but problem is data type is not matched, because the data type of "AirConditioner_1.State" is customized with "ControllerState" type.

AirConditionAASConf.json ```json ... { "value": "", "semanticId": { "keys": [ { "idType": "Iri", "type": "GlobalReference", "value": "0112/2///61360_4#AUA420#001" } ] }, "idShort": "State", "category": "Variable", "modelType": { "name": "Property" }, "valueType": "int", "kind": "Instance", "descriptions": [ { "language": "en", "text": "state of an object active or running" } ] }, ... ```

If i enter "int" or "boolean" in "valueType", the type is not mapped, so error occurs with StatusCode - BadWaitingForInitialData (0x80320000) in OPC UA client. Can I know how to solve this situation with customized data type?

mjacoby commented 2 years ago

As for now, FA³ST only supports a limited set of OPC UA data types out-of-the-box, namely the ones defined as built-in datatypes by the used OPC UA client library (Eclipse Milo) .

However, ValueConverter allows you to register custom type mappings from OPC UA to AAS and vice-versa. The right place to add custom converter functionality would be inside the OpcUaAssetConnection. This requires that you build FA³ST yourself.

Probably at some time in the future adding custom type mappings might become more convenient, e.g. without the need to compile FA³ST yourself. I'll put this down in our backlog but it may take some time as we are currently working on other features.

jongunee commented 2 years ago

I also wonder how to check submodel property with http API.

If I check humidity property, how should I enter the API? What should be typed in {aasIdentifier} or {submodelIdentifier}?

It occurs error with below all cases

AirCoditionerAAS-simple-operation.json ```json { "assetAdministrationShells": [ { "assetInformation": { "assetKind": "Instance", "globalAssetId": { "keys": [ { "idType": "Iri", "type": "Asset", "value": "https://example.com/ids/asset/9294_1051_9022_4665" } ] } }, "submodels": { "keys": [ { "type": "Submodel", "value": "https://example.com/ids/sm/3585_1051_9022_0802", "idType": "Iri" } ] }, "identification": { "idType": "Iri", "id": "https://example.com/ids/aas/5245_1051_9022_2232" }, "idShort": "AirConditionerAAS", "modelType": { "name": "AssetAdministrationShell" } } ], "assets": [ { "identification": { "idType": "Iri", "id": "https://example.com/ids/asset/9294_1051_9022_4665" }, "idShort": "AirConditioner", "modelType": { "name": "Asset" }, "kind": "Instance" } ], "submodels": [ { "semanticId": { "keys": [ { "idType": "Iri", "type": "GlobalReference", "value": "0112/2///61360_4#AAA650#001" } ] }, "identification": { "idType": "Iri", "id": "https://example.com/ids/sm/3585_1051_9022_0802" }, "idShort": "ConditionMonitoring", "modelType": { "name": "Submodel" }, "kind": "Instance", "submodelElements": [ { "value": "", "semanticId": { "keys": [ { "idType": "Iri", "type": "GlobalReference", "value": "0112/2///61360_4#AAA622#001" } ] }, "idShort": "Humidity", "category": "Variable", "modelType": { "name": "Property" }, "valueType": "double", "kind": "Instance", "descriptions": [ { "language": "en", "text": "degree of wetness of the atmosphere measured in the KETI meeting room" } ] }, { "value": "", "semanticId": { "keys": [ { "idType": "Iri", "type": "GlobalReference", "value": "0112/2///61360_4#AUA420#001" } ] }, "idShort": "State", "category": "Variable", "modelType": { "name": "Property" }, "valueType": "int", "kind": "Instance", "descriptions": [ { "language": "en", "text": "state of an object active or running" } ] }, { "value": "", "semanticId": { "keys": [ { "idType": "Iri", "type": "GlobalReference", "value": "https://example.com/ids/cd/5110_8051_9022_2974" } ] }, "idShort": "Start", "modelType": { "name": "Operation" }, "kind": "Instance", "descriptions": [ { "language": "en", "text": "call the method to start an air conditioner" } ] }, { "value": "", "semanticId": { "keys": [ { "idType": "Iri", "type": "GlobalReference", "value": "https://example.com/ids/cd/9301_8051_9022_9990" } ] }, "idShort": "Stop", "modelType": { "name": "Operation" }, "kind": "Instance", "descriptions": [ { "language": "en", "text": "call the method to stop an air conditioner" } ] } ], "descriptions": [] } ], "conceptDescriptions": [ { "modelType": { "name": "ConceptDescription" }, "administration": { "revision": "0", "version": "0.9" }, "identification": { "idType": "Iri", "id": "https://example.com/ids/cd/0491_5022_9022_2061" }, "idShort": "TestConceptDescription", "isCaseOf": [ { "keys": [ { "idType": "Iri", "type": "GlobalReference", "value": "http://example.com/DataSpecifications/ConceptDescriptions/TestConceptDescription" } ] } ], "description": [ { "language": "en-us", "text": "An example concept description for the test application" }, { "language": "de", "text": "Ein Beispiel-ConceptDescription für eine Test-Anwendung" } ] } ] } ```
mjacoby commented 2 years ago

Please have a look at the API specification, especially Section 10.3 which states

To avoid problems with IRIs in URLs the identifiers shall be BASE64-URL-encoded before using them as parameters in the HTTP-APIs. IdshortPaths are URL-encoded to handle including square brackets. In the example above “aHR0cHM6Ly9hZG1pbi1zaGVsbC5pby9zYW1wbGVTTQ” is the BASE64-URLencoding of “https://admin-shell.io/sampleSM”, “sme1.sme2%5B0%5D.p1” is the URL-encoding of “sme1.sme2[0].p1” and “sme1.sme2%5B0%5D” is the URL-encoding of “sme1.sme2[0]”.

In your case this would be

jongunee commented 2 years ago

Is it possible to opetate method with http endpoints?

For example. I have "Start"/"Stop" operation model type. It is working as a method in OPC UA server, it means it is mapped with method in OPC UA server

However, GET request just shows a json output.

GET: http://localhost:8080/submodels/aHR0cHM6Ly9leGFtcGxlLmNvbS9pZHMvc20vMzU4NV8xMDUxXzkwMjJfMDgwMg==/submodel/submodel-elements/Start

Should I use PUT/POST requests?

AirCoditionerAAS-simple-operation.json ```json { "assetAdministrationShells": [ { "assetInformation": { "assetKind": "Instance", "globalAssetId": { "keys": [ { "idType": "Iri", "type": "Asset", "value": "https://example.com/ids/asset/9294_1051_9022_4665" } ] } }, "submodels": { "keys": [ { "type": "Submodel", "value": "https://example.com/ids/sm/3585_1051_9022_0802", "idType": "Iri" } ] }, "identification": { "idType": "Iri", "id": "https://example.com/ids/aas/5245_1051_9022_2232" }, "idShort": "AirConditionerAAS", "modelType": { "name": "AssetAdministrationShell" } } ], "assets": [ { "identification": { "idType": "Iri", "id": "https://example.com/ids/asset/9294_1051_9022_4665" }, "idShort": "AirConditioner", "modelType": { "name": "Asset" }, "kind": "Instance" } ], "submodels": [ { "semanticId": { "keys": [ { "idType": "Iri", "type": "GlobalReference", "value": "0112/2///61360_4#AAA650#001" } ] }, "identification": { "idType": "Iri", "id": "https://example.com/ids/sm/3585_1051_9022_0802" }, "idShort": "ConditionMonitoring", "modelType": { "name": "Submodel" }, "kind": "Instance", "submodelElements": [ { "value": "", "semanticId": { "keys": [ { "idType": "Iri", "type": "GlobalReference", "value": "0112/2///61360_4#AAA622#001" } ] }, "idShort": "Humidity", "category": "Variable", "modelType": { "name": "Property" }, "valueType": "double", "kind": "Instance", "descriptions": [ { "language": "en", "text": "degree of wetness of the atmosphere measured in the KETI meeting room" } ] }, { "value": "", "semanticId": { "keys": [ { "idType": "Iri", "type": "GlobalReference", "value": "0112/2///61360_4#AUA420#001" } ] }, "idShort": "State", "category": "Variable", "modelType": { "name": "Property" }, "valueType": "int", "kind": "Instance", "descriptions": [ { "language": "en", "text": "state of an object active or running" } ] }, { "value": "", "semanticId": { "keys": [ { "idType": "Iri", "type": "GlobalReference", "value": "https://example.com/ids/cd/5110_8051_9022_2974" } ] }, "idShort": "Start", "modelType": { "name": "Operation" }, "kind": "Instance", "descriptions": [ { "language": "en", "text": "call the method to start an air conditioner" } ] }, { "value": "", "semanticId": { "keys": [ { "idType": "Iri", "type": "GlobalReference", "value": "https://example.com/ids/cd/9301_8051_9022_9990" } ] }, "idShort": "Stop", "modelType": { "name": "Operation" }, "kind": "Instance", "descriptions": [ { "language": "en", "text": "call the method to stop an air conditioner" } ] } ], "descriptions": [] } ], "conceptDescriptions": [ { "modelType": { "name": "ConceptDescription" }, "administration": { "revision": "0", "version": "0.9" }, "identification": { "idType": "Iri", "id": "https://example.com/ids/cd/0491_5022_9022_2061" }, "idShort": "TestConceptDescription", "isCaseOf": [ { "keys": [ { "idType": "Iri", "type": "GlobalReference", "value": "http://example.com/DataSpecifications/ConceptDescriptions/TestConceptDescription" } ] } ], "description": [ { "language": "en-us", "text": "An example concept description for the test application" }, { "language": "de", "text": "Ein Beispiel-ConceptDescription für eine Test-Anwendung" } ] } ] } ```
mjacoby commented 2 years ago

Yes, it is possible to map an AAS Operation to an HTTP server. For details how to do this, please have a look at the following documents

In short, you need to have an HTTP AssetConnection configured with an OperationProvider linked to your AAS operation which should something similar to this

{
    "core": {...},
    "endpoints": [...],
    "persistence": {...},
    "messageBus": {...},
    "assetConnections": [{
        "@class": "de.fraunhofer.iosb.ilt.faaast.service.assetconnection.http.HttpAssetConnection",
        "baseUrl": "http://example.com",    
        "operationProviders":
        {
            "(Submodel)[IRI]https://example.com/ids/sm/3585_1051_9022_0802,(Operation)[ID_SHORT]Start":
            {
                "format": "JSON",
                "path": "/foo/bar/start",
                "method": "POST",
                "template": "{\"input1\" : \"${in1}\", \"input2\" : \"${in2}\"}",
                "queries": {
                    "out1": "$.output1",
                    "out2": "$.output2"
                }
            }
        }
    }]
}

Where template specifies the payload to send from AAS to HTTP server and can contain the actual input and inoutput values defined in AAS via ${...} and queries does the mapping from the JSON returned by HTTP server to AAS output and inoutput variables of the operation via JSONPath. In this example the HTTP operation is expected to return JSON in the form

{
  "output1": ...,
  "output2": ...,
   ...
}

As it seems that the operations that you want to map to HTTP do not have any input-/output parameters, you probably don't need to specify template and queries. However, the HTTP Asset Connection has not been tested without any parameters, so please let us know if any exceptions happen in this case.

jongunee commented 2 years ago

I have checked following your answer, but it is not working. I want to check whether it is right.

{
  ...
  "assetConnections": [
    {
      ...
      "operationProviders": {
        "(Submodel)[IRI]https://example.com/ids/sm/3585_1051_9022_0802,(Operation)[ID_SHORT]Start": {
          "nodeId": "ns=3;s=AirConditioner_1.Start",
          "format": "JSON",
          "path": "/submodels/submodelElements/Start/",
          "method": "POST"
        }
      ...
    }
  ]
}

URL to operate the OPC UA method:

http://localhost:8080/submodels/aHR0cHM6Ly9leGFtcGxlLmNvbS9pZHMvc20vMzU4NV8xMDUxXzkwMjJfMDgwMg==/submodel/submodel-elements/Start/invoke

Return:

{
    "success": false,
    "messages": [
        {
            "messageType": "Error",
            "text": "error parsing body",
            "code": "",
            "timestamp": "2022-10-24T04:07:43.986+00:00"
        }
    ]
}

Always thanks for your support!

mjacoby commented 2 years ago

You are mixing configuration properties from OPC UA (nodeId) and HTTP (format, path, and method). Are you trying to use OPC UA via HTTPS or just HTTP(S) and forgot to remove the nodeId property?

What exactly are you trying to do? What kind of assets (i.e. legacy systems to FA³ST should access) do you have? Is it OPC UA, HTTP or both? Could you please elaborate in detail what your intention is?

Are you maybe mixing the protocol how to invoke an operation via FA³ST (i.e. what we call Endpoint) and the protocol that FA³ST uses to forward that call to the underlying asset (what we call AssetConnection)? If so, please be aware that those two are completely independant from each other, meaning you define an asset connection without any information on how this will be invoke via FA³ST. This only depends on which endpoints you choose to expose (HTTP, OPC UA or both). This works because FA³ST parses incoming external requests (e.g. to invoke an AAS operation) into a protocl-agnostic format that is then executed. For asset connections it is quite similar. As they all implement the same protocol-agnostic interface FA³ST checks if there is a provider defined for the requested property and if so calls it without knowing about the internals, e.g. which protocol that provider actually implements.

   DT API                 // can be HTTP, OPC UA, or both
 _____|_______
|             |
|    FA³ST    |
|_____________|
      |
   Asset Connection       // can be either HTTP, OPC UA, or MQTT (for each AAS SubmodelElement)
jongunee commented 2 years ago

I'll try to explain in detail.

What I have now:

  1. asset: OPC UA server - not HTTP
  2. AAS model: AirCoditionerAAS-simple-operation.json

What I checked:

What I am wondering now is:

Is it possible to call method with HTTP endpoints using REST API call like below example?

REST API call with HTTP endpoints -> Call method in my asset (OPC UA server) -> Start/Stop air conditioner

ex) http://localhost:8080/submodels/aHR0cHM6Ly9leGFtcGxlLmNvbS9pZHMvc20vMzU4NV8xMDUxXzkwMjJfMDgwMg==/submodel/submodel-elements/Start/invoke

Or is it REST API call only for HTTP asset POST: /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/invoke

mjacoby commented 2 years ago

Thank you, now I understand what you are trying to do.

As stated in my last post, asset connections and endpoint are not related in any way. This means that operations connected via any asset connection (HTTP, MQTT or OPC UA) can be invoked via both OPC UA and HTTP (assuming the corresponding endpoints are configured) without any additional configuration.

In your scenario that means that you should specify an OPC UA asset connection for your operation as well as an HTTP endpoint and then invoking the AAS operation via HTTP should work. I just tested it locally with an OPC UA operations that has neither input or output and it works as expected.

When doing the POST to .../invoke try adding some payload in the body (according to the specification) like

{
    "inputArguments": [],
    "timeout": 10000
}

and adding the header content-type: application/json.

If you don't provide any body FA³ST seems to fail parsing the HTTP request.