From cddf87423b25111ecab6f7f7a2b0a855cff442bb Mon Sep 17 00:00:00 2001 From: Ben Musson Date: Thu, 22 Jan 2026 18:21:03 -0500 Subject: [PATCH 1/9] working browsing refactor read interface introduce OidModel something I don't hate --- .../embr/snmp/agents/rpc/SnmpAgentRpc.kt | 2 +- .../embr/snmp/model/BasicOidValue.kt | 3 + .../embr/snmp/model/BasicQualifiedOidValue.kt | 2 +- .../mussonindustrial/embr/snmp/model/Oid.kt | 35 +--- .../embr/snmp/model/OidValue.kt | 6 + .../embr/snmp/model/Snmp4jOid.kt | 30 +++ .../embr/snmp/model/SnmpError.kt | 5 + .../snmp/agents/context/ConcurrentOidModel.kt | 68 ++++++ .../embr/snmp/agents/context/OidModel.kt | 87 ++++++++ .../snmp/agents/devices/SnmpAgentDevice.kt | 24 ++- .../agents/devices/SnmpAgentDeviceImpl.kt | 115 ++++++----- .../opc/BrowsableOidModelAddressSpace.kt | 113 ++++++++++ .../embr/snmp/agents/opc/OidAddressSpace.kt | 194 +++++++++--------- .../embr/snmp/agents/rpc/SnmpAgentRpcImpl.kt | 34 ++- .../embr/snmp/requests/OidReadRequest.kt | 8 - .../embr/snmp/requests/OidReadResult.kt | 30 --- .../embr/snmp/requests/OidWriteRequest.kt | 10 - .../embr/snmp/requests/OidWriteResult.kt | 10 - .../embr/snmp/typing/SnmpType.kt | 40 ++++ .../embr/snmp/utils/SnmpUtils.kt | 64 ++---- 20 files changed, 552 insertions(+), 328 deletions(-) create mode 100644 modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/BasicOidValue.kt create mode 100644 modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/OidValue.kt create mode 100644 modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/Snmp4jOid.kt create mode 100644 modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/SnmpError.kt create mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/context/ConcurrentOidModel.kt create mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/context/OidModel.kt create mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/BrowsableOidModelAddressSpace.kt delete mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/requests/OidReadRequest.kt delete mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/requests/OidReadResult.kt delete mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/requests/OidWriteRequest.kt delete mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/requests/OidWriteResult.kt create mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/typing/SnmpType.kt diff --git a/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/rpc/SnmpAgentRpc.kt b/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/rpc/SnmpAgentRpc.kt index 444cf9fe..648553ea 100644 --- a/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/rpc/SnmpAgentRpc.kt +++ b/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/rpc/SnmpAgentRpc.kt @@ -21,7 +21,7 @@ interface SnmpAgentRpc { fun read(agent: String, oids: List): List - fun write(agent: String, oids: List, values: List): List + fun write(agent: String, oids: List, values: List): List fun walk(agent: String, oids: List): List diff --git a/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/BasicOidValue.kt b/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/BasicOidValue.kt new file mode 100644 index 00000000..93e259a2 --- /dev/null +++ b/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/BasicOidValue.kt @@ -0,0 +1,3 @@ +package com.mussonindustrial.embr.snmp.model + +data class BasicOidValue(override val oid: Oid, override val value: T) : OidValue diff --git a/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/BasicQualifiedOidValue.kt b/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/BasicQualifiedOidValue.kt index 0e3c2cfa..623f8344 100644 --- a/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/BasicQualifiedOidValue.kt +++ b/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/BasicQualifiedOidValue.kt @@ -41,7 +41,7 @@ data class BasicQualifiedOidValue( 1 -> BasicQualifiedOidValue( value = payload["value"], - oid = Oid.fromNumeric(payload["oid"] as String), + oid = Snmp4jOid(payload["oid"] as String), quality = QualityCode((payload["quality"] as Number).toInt()), timeStamp = Date((payload["timestamp"] as Number).toLong()), ) diff --git a/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/Oid.kt b/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/Oid.kt index 12ece120..b3f96849 100644 --- a/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/Oid.kt +++ b/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/Oid.kt @@ -1,40 +1,9 @@ package com.mussonindustrial.embr.snmp.model -import org.snmp4j.smi.OID - -class Oid private constructor(private val oid: OID) { - - companion object { - fun fromNumeric(dotted: String): Oid = Oid(oid = OID(dotted)) - - fun fromSnmp4j(oid: OID): Oid = Oid(oid = OID(oid)) - } - +interface Oid { val numeric: String - get() = oid.toDottedString() - - @Suppress("UNUSED") - val dottedString: String - get() = numeric - - @Suppress("UNUSED") - fun toDottedString(): String { - return numeric - } + val symbolicName: String val index: Int - get() = OID(oid).removeLast() - val parent: Oid - get() { - val parentOid = OID(oid) - parentOid.removeLast() - return Oid(oid = parentOid) - } - - override fun toString(): String = numeric - - override fun equals(other: Any?): Boolean = other is Oid && oid == other.oid - - override fun hashCode(): Int = oid.hashCode() } diff --git a/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/OidValue.kt b/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/OidValue.kt new file mode 100644 index 00000000..11a80971 --- /dev/null +++ b/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/OidValue.kt @@ -0,0 +1,6 @@ +package com.mussonindustrial.embr.snmp.model + +interface OidValue { + val oid: Oid + val value: T +} diff --git a/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/Snmp4jOid.kt b/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/Snmp4jOid.kt new file mode 100644 index 00000000..31232751 --- /dev/null +++ b/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/Snmp4jOid.kt @@ -0,0 +1,30 @@ +package com.mussonindustrial.embr.snmp.model + +import org.snmp4j.smi.OID + +class Snmp4jOid(val oid: OID) : OID(oid), Oid { + + constructor(numeric: String) : this(OID(numeric)) + + override val numeric: String + get() = oid.toDottedString() + + override val symbolicName: String + get() = "unknownSymbol[$numeric]" + + override val index: Int + get() = OID(oid).removeLast() + + override val parent: Snmp4jOid + get() { + val parentOid = OID(oid) + parentOid.removeLast() + return Snmp4jOid(parentOid) + } + + override fun toString(): String = numeric +} + +fun OID.toOid(): Snmp4jOid = Snmp4jOid(this) + +fun Oid.toSnmp4j(): OID = Snmp4jOid(numeric) diff --git a/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/SnmpError.kt b/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/SnmpError.kt new file mode 100644 index 00000000..0e555e59 --- /dev/null +++ b/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/SnmpError.kt @@ -0,0 +1,5 @@ +package com.mussonindustrial.embr.snmp.model + +import org.snmp4j.smi.Null + +val SnmpCommunicationError = Null() diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/context/ConcurrentOidModel.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/context/ConcurrentOidModel.kt new file mode 100644 index 00000000..c875fae9 --- /dev/null +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/context/ConcurrentOidModel.kt @@ -0,0 +1,68 @@ +package com.mussonindustrial.embr.snmp.agents.context + +import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice +import com.mussonindustrial.embr.snmp.model.BasicOidValue +import com.mussonindustrial.embr.snmp.model.Oid +import com.mussonindustrial.embr.snmp.model.OidValue +import com.mussonindustrial.embr.snmp.typing.snmpType +import java.util.concurrent.ConcurrentHashMap +import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue +import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode +import org.snmp4j.smi.Variable + +class ConcurrentOidModel(val device: SnmpAgentDevice) : OidModel { + + private val descriptors = ConcurrentHashMap() + private val knownValues = ConcurrentHashMap>() + + override val oids: List + get() = descriptors.map { it.key } + + override fun read(reads: List): List> { + return device.read(reads).map { coerceCache(it) } + } + + override fun write(writes: List>): List> { + return device.write( + writes.map { (oid, value) -> oid to toSnmpValue(BasicOidValue(oid, value)).value } + ) + } + + override fun walk(roots: List): List> { + return device.walk(roots).map { coerceCache(it) } + } + + override fun readTable( + columns: List, + lowerBoundIndex: Oid?, + upperBoundIndex: Oid?, + ): List>> { + return device.readTable(columns, lowerBoundIndex, upperBoundIndex).map { + it.map { result -> coerceCache(result) } + } + } + + fun coerceCache(value: OidValue): OidValue { + val descriptor = OidModel.ValueDescriptor(value.oid, value.value.snmpType) + val result = toOpcUaValue(value) + + descriptors[result.oid] = descriptor + knownValues[result.oid] = result + + return result + } + + override fun getDescriptors(oids: List): List { + val results = oids.map { descriptors.getOrDefault(it, OidModel.UnknownDescriptor(it)) } + + val missingDescriptors = + oids + .zip(results) + .filter { (_, descriptor) -> descriptor is OidModel.UnknownDescriptor } + .map { it.first } + if (missingDescriptors.isEmpty()) return results + + read(missingDescriptors) + return oids.map { descriptors.getOrDefault(it, OidModel.InvalidDescriptor(it)) } + } +} diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/context/OidModel.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/context/OidModel.kt new file mode 100644 index 00000000..dcb39147 --- /dev/null +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/context/OidModel.kt @@ -0,0 +1,87 @@ +package com.mussonindustrial.embr.snmp.agents.context + +import com.mussonindustrial.embr.snmp.model.BasicOidValue +import com.mussonindustrial.embr.snmp.model.Oid +import com.mussonindustrial.embr.snmp.model.OidValue +import com.mussonindustrial.embr.snmp.model.SnmpCommunicationError +import com.mussonindustrial.embr.snmp.typing.SnmpType +import org.eclipse.milo.opcua.stack.core.StatusCodes +import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue +import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode +import org.eclipse.milo.opcua.stack.core.types.builtin.Variant +import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.ULong +import org.snmp4j.smi.Counter64 +import org.snmp4j.smi.Integer32 +import org.snmp4j.smi.Null +import org.snmp4j.smi.OID +import org.snmp4j.smi.OctetString +import org.snmp4j.smi.SMIAddress +import org.snmp4j.smi.UnsignedInteger32 +import org.snmp4j.smi.Variable + +interface OidModel { + + val oids: List + + fun read(reads: List): List> + + fun write(writes: List>): List> + + fun walk(roots: List): List> + + fun readTable( + columns: List, + lowerBoundIndex: Oid?, + upperBoundIndex: Oid?, + ): List>> + + fun getDescriptors(oids: List): List + + fun toSnmpValue(value: OidValue): OidValue { + val descriptor = getDescriptors(listOf(value.oid)).first() + + val snmpValue = + when (descriptor) { + is ValueDescriptor -> descriptor.snmpType.variableOfType(value.value) + is InvalidDescriptor -> Null.instance + is TableColumnDescriptor -> Null.instance + is TableDescriptor -> Null.instance + is UnknownDescriptor -> Null.instance + } + return BasicOidValue(value.oid, snmpValue) + } + + fun toOpcUaValue(value: OidValue): OidValue { + if (value.value == SnmpCommunicationError) { + return BasicOidValue(value.oid, DataValue(StatusCodes.Bad_CommunicationError)) + } + + val opcUaValue = + when (value.value) { + Null.endOfMibView -> DataValue(StatusCodes.Bad_NotFound) + Null.noSuchObject -> DataValue(StatusCodes.Bad_NotFound) + Null.noSuchInstance -> DataValue(StatusCodes.Bad_NotFound) + Null.instance -> DataValue(Variant.NULL_VALUE) + is Integer32 -> DataValue(Variant.ofInt32(value.value.toInt())) + is UnsignedInteger32 -> DataValue(Variant.ofInt32(value.value.toInt())) + is Counter64 -> DataValue(Variant.ofUInt64(ULong.valueOf(value.value.toLong()))) + is SMIAddress -> DataValue(Variant.ofString(value.value.toString())) + is OctetString -> DataValue(Variant.ofString(value.value.toString())) + is OID -> DataValue(Variant.ofString(value.value.toString())) + else -> DataValue(Variant.ofString(value.value.toString())) + } + return BasicOidValue(value.oid, opcUaValue) + } + + sealed class Descriptor(val oid: Oid) + + class InvalidDescriptor(oid: Oid) : Descriptor(oid) + + class UnknownDescriptor(oid: Oid) : Descriptor(oid) + + open class ValueDescriptor(oid: Oid, val snmpType: SnmpType) : Descriptor(oid) + + class TableColumnDescriptor(oid: Oid, val snmpType: SnmpType) : Descriptor(oid) + + class TableDescriptor(oid: Oid, val columns: List) : Descriptor(oid) +} diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDevice.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDevice.kt index a0713daa..5ebb9729 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDevice.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDevice.kt @@ -1,30 +1,32 @@ package com.mussonindustrial.embr.snmp.agents.devices import com.inductiveautomation.ignition.gateway.opcua.server.api.Device +import com.mussonindustrial.embr.snmp.agents.context.OidModel import com.mussonindustrial.embr.snmp.agents.context.SnmpAgentContext -import com.mussonindustrial.embr.snmp.requests.OidReadResult -import com.mussonindustrial.embr.snmp.requests.OidWriteResult +import com.mussonindustrial.embr.snmp.model.Oid +import com.mussonindustrial.embr.snmp.model.OidValue import org.eclipse.milo.opcua.sdk.server.AddressSpaceFragment import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId -import org.snmp4j.smi.OID -import org.snmp4j.smi.VariableBinding +import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode +import org.snmp4j.smi.Variable interface SnmpAgentDevice : AddressSpaceFragment, Device { val context: SnmpAgentContext<*> val status: Status + val model: OidModel - fun read(reads: List): List + fun read(reads: List): List> - fun write(writes: List): List + fun write(writes: List>): List> - fun walk(roots: List): List + fun walk(roots: List): List> fun readTable( - columns: List, - lowerBoundIndex: OID?, - upperBoundIndex: OID?, - ): List> + columns: List, + lowerBoundIndex: Oid?, + upperBoundIndex: Oid?, + ): List>> fun stripDeviceName(nodeId: NodeId): String { val id = nodeId.identifier.toString() diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDeviceImpl.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDeviceImpl.kt index 7826dfb8..23674861 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDeviceImpl.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDeviceImpl.kt @@ -3,24 +3,28 @@ package com.mussonindustrial.embr.snmp.agents.devices import com.inductiveautomation.ignition.common.util.LoggerEx import com.mussonindustrial.embr.snmp.SnmpGatewayContext import com.mussonindustrial.embr.snmp.agents.configuration.SnmpAgentConfig +import com.mussonindustrial.embr.snmp.agents.context.ConcurrentOidModel import com.mussonindustrial.embr.snmp.agents.context.SnmpAgentContext +import com.mussonindustrial.embr.snmp.agents.opc.BrowsableOidModelAddressSpace import com.mussonindustrial.embr.snmp.agents.opc.DiagnosticAddressSpace import com.mussonindustrial.embr.snmp.agents.opc.OidAddressSpace +import com.mussonindustrial.embr.snmp.model.BasicOidValue +import com.mussonindustrial.embr.snmp.model.Oid +import com.mussonindustrial.embr.snmp.model.OidValue +import com.mussonindustrial.embr.snmp.model.Snmp4jOid +import com.mussonindustrial.embr.snmp.model.SnmpCommunicationError +import com.mussonindustrial.embr.snmp.model.toOid +import com.mussonindustrial.embr.snmp.model.toSnmp4j import com.mussonindustrial.embr.snmp.opc.DeviceAddressSpace -import com.mussonindustrial.embr.snmp.requests.OidReadResult -import com.mussonindustrial.embr.snmp.requests.OidWriteResult -import com.mussonindustrial.embr.snmp.requests.toOidReadResult -import com.mussonindustrial.embr.snmp.requests.toOidWriteResult import com.mussonindustrial.embr.snmp.utils.createSizeBoundedPDUs import java.util.concurrent.TimeUnit import org.eclipse.milo.opcua.sdk.server.AddressSpaceComposite import org.eclipse.milo.opcua.sdk.server.Lifecycle import org.eclipse.milo.opcua.sdk.server.LifecycleManager import org.eclipse.milo.opcua.stack.core.StatusCodes -import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode import org.snmp4j.PDU -import org.snmp4j.smi.OID +import org.snmp4j.smi.Variable import org.snmp4j.smi.VariableBinding import org.snmp4j.util.TableUtils import org.snmp4j.util.TreeUtils @@ -35,11 +39,14 @@ class SnmpAgentDeviceImpl(override val context: SnmpAgentCo override var status: SnmpAgentDevice.Status = SnmpAgentDevice.Status.DISCONNECTED private set + override val model = ConcurrentOidModel(this) + val healthcheck = Healthcheck() val deviceAddressSpace = DeviceAddressSpace(context.deviceContext, this) val diagnosticAddressSpace = DiagnosticAddressSpace(this, this) val oidAddressSpace = OidAddressSpace(this, this) + val browsableOidModelAddressSpace = BrowsableOidModelAddressSpace(this, this) init { lifecycleManager.addLifecycle(context) @@ -47,11 +54,13 @@ class SnmpAgentDeviceImpl(override val context: SnmpAgentCo lifecycleManager.addLifecycle(deviceAddressSpace) lifecycleManager.addLifecycle(diagnosticAddressSpace) lifecycleManager.addLifecycle(oidAddressSpace) + lifecycleManager.addLifecycle(browsableOidModelAddressSpace) lifecycleManager.addStartupTask { onDataItemsCreated( context.deviceContext.subscriptionModel.getDataItems(context.deviceContext.name) ) } + lifecycleManager.addStartupTask { model.walk(listOf(Snmp4jOid("1"))) } } val treeUtils = TreeUtils(context.snmp, context.pduFactory) @@ -82,16 +91,16 @@ class SnmpAgentDeviceImpl(override val context: SnmpAgentCo } } - override fun read(reads: List): List { - val results = mutableMapOf() - val remaining = reads.groupBy { it.oid }.toMutableMap() + override fun read(reads: List): List> { + val results = mutableMapOf>() + val remaining = reads.groupBy { it }.toMutableMap() while (remaining.isNotEmpty()) { val pdus = context.readTarget.createSizeBoundedPDUs( context.pduFactory, - remaining.flatMap { it.value }, + remaining.flatMap { it.value.map { oid -> VariableBinding(oid.toSnmp4j()) } }, ) { type = PDU.GET } @@ -101,110 +110,106 @@ class SnmpAgentDeviceImpl(override val context: SnmpAgentCo val response = context.snmp.send(pdu, context.readTarget).response if (response == null) { logger.warn("GET failed: no response.") - return reads.map { - it.oid.toOidReadResult(DataValue(StatusCodes.Bad_CommunicationError)) - } + return reads.map { BasicOidValue(it, SnmpCommunicationError) } } if (response.errorStatus == 0) { logger.trace("GET successful: ${response.variableBindings}") response.variableBindings.forEach { binding -> - remaining[binding.oid]?.forEach { - results[it] = binding.toOidReadResult() + val oid = binding.oid.toOid() + remaining[oid]?.forEach { + results[it] = BasicOidValue(it, binding.variable) } - remaining.remove(binding.oid) + remaining.remove(oid) } } else { val errorIdx = response.errorIndex if (errorIdx in 1..pdu.size()) { - val badOid = pdu.get(errorIdx - 1).oid + val oid = (pdu.get(errorIdx - 1).oid).toOid() logger.debug( - "GET failed at OID: $badOid (index ${errorIdx}), removing and retrying..." + "GET failed at OID: $oid (index ${errorIdx}), removing and retrying..." ) - val failedResults = remaining.remove(badOid) + val failedResults = remaining.remove(oid) failedResults?.forEach { - results[it] = - it.oid.toOidReadResult( - DataValue(StatusCodes.Bad_CommunicationError) - ) + results[it] = BasicOidValue(it, SnmpCommunicationError) } } else { logger.warn( "GET failed with errorStatusText: ${response.errorStatusText}" ) - return reads.map { - it.oid.toOidReadResult( - DataValue(StatusCodes.Bad_CommunicationError) - ) - } + return reads.map { BasicOidValue(it, SnmpCommunicationError) } } } } catch (e: Exception) { logger.warn("GET failed with exception", e) - return reads.map { - it.oid.toOidReadResult(DataValue(StatusCodes.Bad_CommunicationError)) - } + return reads.map { BasicOidValue(it, SnmpCommunicationError) } } } } - return reads.map { results[it] as OidReadResult } + return reads.map { results[it]!! } } - override fun write(writes: List): List { - return writes.map { + override fun write(writes: List>): List> { + return writes.map { (oid, value) -> if (context.writeTarget == null) { - return@map it.oid.toOidWriteResult(StatusCode(StatusCodes.Bad_WriteNotSupported)) + return@map BasicOidValue(oid, StatusCode(StatusCodes.Bad_CommunicationError)) } val pdu = context.pduFactory.createPDU(context.writeTarget).apply { type = PDU.SET - add(it) + add(VariableBinding(oid.toSnmp4j(), value)) } try { val response = context.snmp.send(pdu, context.writeTarget).response if (response == null) { logger.warn("SET failed: no response.") - return@map it.oid.toOidWriteResult( - StatusCode(StatusCodes.Bad_CommunicationError) - ) + return@map BasicOidValue(oid, StatusCode(StatusCodes.Bad_CommunicationError)) } if (response.errorStatus == 0) { - return@map it.oid.toOidWriteResult(StatusCode.GOOD) + return@map BasicOidValue(oid, StatusCode.GOOD) } else { - return@map it.oid.toOidWriteResult(StatusCode.BAD) + logger.warn("SET failed with errorStatusText: ${response.errorStatusText}") + if (response.errorStatusText == "Not writable") { + return@map BasicOidValue(oid, StatusCode(StatusCodes.Bad_NotWritable)) + } + return@map BasicOidValue(oid, StatusCode.BAD) } } catch (e: Exception) { logger.warn("SET failed with exception", e) - return@map it.oid.toOidWriteResult(StatusCode(StatusCodes.Bad_CommunicationError)) + return@map BasicOidValue(oid, StatusCode(StatusCodes.Bad_CommunicationError)) } } } - override fun walk(roots: List): List { - val results = treeUtils.walk(context.readTarget, roots.toTypedArray()) + override fun walk(roots: List): List> { + val results = treeUtils.walk(context.readTarget, roots.map { it.toSnmp4j() }.toTypedArray()) return results.flatMap { - it.variableBindings?.map { binding -> binding.toOidReadResult() } ?: listOf() + it.variableBindings?.map { binding -> + BasicOidValue(binding.oid.toOid(), binding.variable) + } ?: listOf() } } override fun readTable( - columns: List, - lowerBoundIndex: OID?, - upperBoundIndex: OID?, - ): List> { + columns: List, + lowerBoundIndex: Oid?, + upperBoundIndex: Oid?, + ): List>> { val results = tableUtils.getTable( context.readTarget, - columns.toTypedArray(), - lowerBoundIndex, - upperBoundIndex, + columns.map { it.toSnmp4j() }.toTypedArray(), + lowerBoundIndex?.toSnmp4j(), + upperBoundIndex?.toSnmp4j(), ) return results.mapNotNull { - it.columns?.mapNotNull { binding -> binding?.toOidReadResult() } + it.columns?.mapNotNull { binding -> + BasicOidValue(binding.oid.toOid(), binding.variable) + } } } @@ -258,8 +263,8 @@ class SnmpAgentDeviceImpl(override val context: SnmpAgentCo return } - val response = read(listOf(VariableBinding(OID(context.snmpConfig.healthcheck.oid)))) - val isGood = response.first().value.statusCode.isGood + val response = read(listOf(Snmp4jOid(context.snmpConfig.healthcheck.oid!!))).first() + val isGood = response.value != SnmpCommunicationError logger.trace("Health check result: $isGood") status = diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/BrowsableOidModelAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/BrowsableOidModelAddressSpace.kt new file mode 100644 index 00000000..7b85333f --- /dev/null +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/BrowsableOidModelAddressSpace.kt @@ -0,0 +1,113 @@ +package com.mussonindustrial.embr.snmp.agents.opc + +import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice +import com.mussonindustrial.embr.snmp.opc.DeviceContextManagedAddressSpaceFragment +import com.mussonindustrial.embr.snmp.utils.removeAllNodes +import org.eclipse.milo.opcua.sdk.core.Reference +import org.eclipse.milo.opcua.sdk.server.* +import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode +import org.eclipse.milo.opcua.stack.core.NodeIds +import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText +import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId +import org.eclipse.milo.opcua.stack.core.types.structured.ViewDescription + +class BrowsableOidModelAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceComposite) : + DeviceContextManagedAddressSpaceFragment(device.context.deviceContext, composite) { + + private val root = "Objects" + private val model = device.model + + init { + lifecycleManager.addLifecycle( + object : Lifecycle { + override fun startup() { + addNodes() + } + + override fun shutdown() { + nodeManager.removeAllNodes() + } + } + ) + } + + fun addNodes() { + val objectsFolder = + UaFolderNode( + nodeContext, + nodeId(root), + qualifiedName(root), + LocalizedText.english(root), + ) + nodeManager.addNode(objectsFolder) + + objectsFolder.addReference( + Reference( + objectsFolder.nodeId, + NodeIds.Organizes, + deviceNodeId.expanded(), + Reference.Direction.INVERSE, + ) + ) + + addObjectsFolder(objectsFolder, "Numeric") + addObjectsFolder(objectsFolder, "Symbolic") + } + + fun addObjectsFolder(folder: UaFolderNode, name: String) { + val node = + UaFolderNode( + nodeContext, + nodeId("Objects/${name}"), + qualifiedName(name), + LocalizedText.english(name), + ) + nodeManager.addNode(node) + + folder.addReference( + Reference( + node.nodeId, + NodeIds.Organizes, + folder.nodeId.expanded(), + Reference.Direction.INVERSE, + ) + ) + } + + override fun browse( + context: AddressSpace.BrowseContext, + view: ViewDescription, + nodeIds: List, + ): List { + return nodeIds.map { nodeId -> + val references = nodeManager.getReferences(nodeId) + + when (nodeId) { + nodeId("Objects/Numeric") -> + model.getDescriptors(model.oids).forEach { descriptor -> + references.add( + Reference( + nodeId, + NodeIds.Organizes, + nodeId(descriptor.oid.numeric).expanded(), + Reference.Direction.FORWARD, + ) + ) + } + nodeId("Objects/Symbolic") -> + model.getDescriptors(model.oids).forEach { descriptor -> + references.add( + Reference( + nodeId, + NodeIds.Organizes, + nodeId(descriptor.oid.symbolicName).expanded(), + Reference.Direction.FORWARD, + ) + ) + } + } + + AddressSpace.ReferenceResult.of(references) + } + } +} diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt index 95a608a0..7a8860cf 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt @@ -1,15 +1,11 @@ package com.mussonindustrial.embr.snmp.agents.opc +import com.mussonindustrial.embr.snmp.agents.context.OidModel import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice +import com.mussonindustrial.embr.snmp.model.OidValue +import com.mussonindustrial.embr.snmp.model.Snmp4jOid import com.mussonindustrial.embr.snmp.opc.DeviceContextManagedAddressSpaceFragment -import com.mussonindustrial.embr.snmp.requests.OidReadRequest -import com.mussonindustrial.embr.snmp.requests.OidReadResult -import com.mussonindustrial.embr.snmp.requests.OidWriteRequest -import com.mussonindustrial.embr.snmp.requests.OidWriteResult -import com.mussonindustrial.embr.snmp.requests.toOidReadResult -import com.mussonindustrial.embr.snmp.requests.toOidWriteResult import com.mussonindustrial.embr.snmp.utils.isOid -import com.mussonindustrial.embr.snmp.utils.toVariable import kotlin.jvm.optionals.getOrNull import org.eclipse.milo.opcua.sdk.core.AccessLevel import org.eclipse.milo.opcua.sdk.core.ValueRank @@ -17,28 +13,19 @@ import org.eclipse.milo.opcua.sdk.server.AddressSpace import org.eclipse.milo.opcua.sdk.server.AddressSpaceComposite import org.eclipse.milo.opcua.sdk.server.AddressSpaceFilter import org.eclipse.milo.opcua.sdk.server.SimpleAddressSpaceFilter -import org.eclipse.milo.opcua.stack.core.AttributeId -import org.eclipse.milo.opcua.stack.core.OpcUaDataType -import org.eclipse.milo.opcua.stack.core.StatusCodes -import org.eclipse.milo.opcua.stack.core.UaException -import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue -import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText -import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId -import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode -import org.eclipse.milo.opcua.stack.core.types.builtin.Variant +import org.eclipse.milo.opcua.stack.core.* +import org.eclipse.milo.opcua.stack.core.types.builtin.* import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger import org.eclipse.milo.opcua.stack.core.types.enumerated.NodeClass import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn import org.eclipse.milo.opcua.stack.core.types.structured.ReadValueId -import org.eclipse.milo.opcua.stack.core.types.structured.ViewDescription import org.eclipse.milo.opcua.stack.core.types.structured.WriteValue -import org.snmp4j.smi.OID -import org.snmp4j.smi.VariableBinding class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceComposite) : DeviceContextManagedAddressSpaceFragment(device.context.deviceContext, composite) { private val filter = SimpleAddressSpaceFilter.create { it.getPath().isOid() } + private val model = device.model override fun read( context: AddressSpace.ReadContext, @@ -46,73 +33,92 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos timestamps: TimestampsToReturn, readValueIds: List, ): List { - val results = readValueIds.map { ReadRequest(it) } - val toProcess = results.filter { it.result == null } + val requests = readValueIds.map { ReadRequest(it) } val valueReads = - toProcess.filter { + requests.filter { AttributeId.from(it.readValueId.attributeId).get() == AttributeId.Value } - val valueReadResults = device.read(valueReads.map { VariableBinding(it.oid) }) - valueReadResults.zip(valueReads).forEach { (value, result) -> result.result = value } + readValueAttributes(valueReads).zip(valueReads).forEach { (value, result) -> + result.value = value + } val nonValueReads = - toProcess.filter { + requests.filter { AttributeId.from(it.readValueId.attributeId).get() != AttributeId.Value } - val nonValueReadResults = readNonValueAttributes(nonValueReads) - nonValueReadResults.zip(nonValueReads).forEach { (value, result) -> result.result = value } + readNonValueAttributes(nonValueReads).zip(nonValueReads).forEach { (value, result) -> + result.value = value + } - return results.map { it.result!!.value } + return requests.map { it.value ?: DataValue(Variant.of(null)) } } - fun readNonValueAttributes(results: List): List { - return results.map { - val nodeId = it.readValueId.nodeId - val attributeId = AttributeId.from(it.readValueId.attributeId).getOrNull() - - try { - val result = - when (attributeId) { - AttributeId.NodeId -> nodeId - - AttributeId.NodeClass -> NodeClass.Variable - - AttributeId.BrowseName -> - device.context.deviceContext.qualifiedName(nodeId.getPath()) - - AttributeId.DisplayName, - AttributeId.Description -> LocalizedText.english(nodeId.getPath()) - - AttributeId.WriteMask, - AttributeId.UserWriteMask -> UInteger.valueOf(0) - - AttributeId.DataType -> OpcUaDataType.String.nodeId - - AttributeId.ValueRank -> ValueRank.Scalar.value - - AttributeId.ArrayDimensions -> intArrayOf() + fun readValueAttributes(requests: List): List { + return model.read(requests.map { it.oid }).map { it.value } + } - AttributeId.AccessLevel, - AttributeId.UserAccessLevel -> AccessLevel.toValue(AccessLevel.READ_WRITE) + fun readNonValueAttributes(requests: List): List { + val descriptors = model.getDescriptors(requests.map { it.oid }) - AttributeId.Value -> - throw UaException( - StatusCodes.Bad_InternalError, - "attributeId: $attributeId", - ) + return requests.zip(descriptors).map { (request, descriptor) -> + val nodeId = request.readValueId.nodeId + val attributeId = AttributeId.from(request.readValueId.attributeId).getOrNull() - else -> - throw UaException( - StatusCodes.Bad_AttributeIdInvalid, - "attributeId: $attributeId", - ) - }!! + attributeId + .runCatching { resolveAttributeValue(attributeId, nodeId, descriptor) } + .fold( + onSuccess = { DataValue(Variant(it)) }, + onFailure = { DataValue((it as UaException).statusCode) }, + ) + } + } - it.oid.toOidReadResult(DataValue(Variant(result))) - } catch (e: UaException) { - it.oid.toOidReadResult(DataValue(e.statusCode)) - } + @Throws(UaException::class) + fun resolveAttributeValue( + attributeId: AttributeId?, + nodeId: NodeId, + descriptor: OidModel.Descriptor, + ): Any? { + return when (attributeId) { + AttributeId.NodeId -> nodeId + + AttributeId.NodeClass -> NodeClass.Variable + + AttributeId.BrowseName -> device.context.deviceContext.qualifiedName(nodeId.getPath()) + + AttributeId.DisplayName -> LocalizedText.english(descriptor.oid.numeric) + AttributeId.Description -> LocalizedText.english(descriptor.oid.numeric) + + AttributeId.WriteMask, + AttributeId.UserWriteMask -> UInteger.valueOf(0) + + AttributeId.DataType -> + when (descriptor) { + is OidModel.ValueDescriptor -> descriptor.snmpType.uaDataType + else -> OpcUaDataType.String.nodeId + } + AttributeId.ValueRank -> + when (descriptor) { + is OidModel.ValueDescriptor -> ValueRank.Scalar.value + else -> ValueRank.Scalar.value + } + AttributeId.ArrayDimensions -> + when (descriptor) { + is OidModel.ValueDescriptor -> null + else -> null + } + + AttributeId.AccessLevel, + AttributeId.UserAccessLevel -> AccessLevel.toValue(AccessLevel.READ_WRITE) + + AttributeId.Historizing -> false + + AttributeId.Value -> + throw UaException(StatusCodes.Bad_InternalError, "attributeId: $attributeId") + + else -> + throw UaException(StatusCodes.Bad_AttributeIdInvalid, "attributeId: $attributeId") } } @@ -124,38 +130,23 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos results.forEach { if (it.writeValue.attributeId == null) { - it.result = it.oid.toOidWriteResult(StatusCode(StatusCodes.Bad_AttributeIdInvalid)) + it.value = StatusCode(StatusCodes.Bad_AttributeIdInvalid) } if (it.writeValue.indexRange != null && it.writeValue.indexRange.isNotEmpty()) { - it.result = it.oid.toOidWriteResult(StatusCode(StatusCodes.Bad_NotImplemented)) + it.value = StatusCode(StatusCodes.Bad_NotImplemented) } if (AttributeId.from(it.writeValue.attributeId).getOrNull() != AttributeId.Value) { - it.result = it.oid.toOidWriteResult(StatusCode(StatusCodes.Bad_NotImplemented)) + it.value = StatusCode(StatusCodes.Bad_NotImplemented) } } - val valueWrites = results.filter { it.result == null } - val valueWriteResults = - device.write(valueWrites.map { VariableBinding(it.oid, it.value.toVariable()) }) - valueWriteResults.zip(valueWrites).forEach { (value, result) -> result.result = value } + val valueWrites = results.filter { it.value == null } + model + .write(valueWrites.map { it.oid to it.writeValue.value.value.value }) + .zip(valueWrites) + .forEach { (value, result) -> result.value = value.value } - return results.map { it.result?.statusCode } - } - - override fun browse( - context: AddressSpace.BrowseContext, - view: ViewDescription, - nodeIds: List, - ): List { - return emptyList() - } - - override fun gather( - context: AddressSpace.BrowseContext, - view: ViewDescription, - nodeId: NodeId, - ): AddressSpace.ReferenceResult.ReferenceList { - return AddressSpace.ReferenceResult.ReferenceList(emptyList()) + return results.map { it.value } } override fun getFilter(): AddressSpaceFilter { @@ -166,14 +157,13 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos return device.stripDeviceName(this) } - inner class ReadRequest(val readValueId: ReadValueId) : OidReadRequest { - override var result: OidReadResult? = null - override val oid = OID(readValueId.nodeId.getPath()) + inner class ReadRequest(val readValueId: ReadValueId) : OidValue { + override val oid = Snmp4jOid(readValueId.nodeId.getPath()) + override var value: DataValue? = null } - inner class WriteRequest(val writeValue: WriteValue) : OidWriteRequest { - override var result: OidWriteResult? = null - override val value: DataValue = writeValue.value - override val oid = OID(writeValue.nodeId.getPath()) + inner class WriteRequest(val writeValue: WriteValue) : OidValue { + override val oid = Snmp4jOid(writeValue.nodeId.getPath()) + override var value: StatusCode? = null } } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/rpc/SnmpAgentRpcImpl.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/rpc/SnmpAgentRpcImpl.kt index 341a111e..cc7c443d 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/rpc/SnmpAgentRpcImpl.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/rpc/SnmpAgentRpcImpl.kt @@ -6,12 +6,12 @@ import com.inductiveautomation.ignition.gateway.clientcomm.MutabilityMode import com.inductiveautomation.ignition.gateway.rpc.RpcDelegate import com.mussonindustrial.embr.snmp.SnmpGatewayContext import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice +import com.mussonindustrial.embr.snmp.model.Oid import com.mussonindustrial.embr.snmp.model.QualifiedOidValue +import com.mussonindustrial.embr.snmp.model.Snmp4jOid +import com.mussonindustrial.embr.snmp.utils.toQualifiedValue import com.mussonindustrial.embr.snmp.utils.toQualityCode import org.python.core.Py.ValueError -import org.snmp4j.smi.OID -import org.snmp4j.smi.OctetString -import org.snmp4j.smi.VariableBinding @RpcDelegate.RunsOnClient(clientPermissionId = ClientPermissionsConstants.UNRESTRICTED) class SnmpAgentRpcImpl(val context: SnmpGatewayContext) : SnmpAgentRpc { @@ -24,32 +24,26 @@ class SnmpAgentRpcImpl(val context: SnmpGatewayContext) : SnmpAgentRpc { @RpcDelegate.RequiredMutabilityMode(value = MutabilityMode.READ_ONLY) override fun read(agent: String, oids: List): List { val snmpAgent = requireAgent(agent) - return snmpAgent.read(oids.map { oid -> VariableBinding(OID(oid)) }).map { - it.toQualifiedValue() - } + return snmpAgent.model.read(oids.map { Snmp4jOid(it) }).map { it.toQualifiedValue() } } @RpcDelegate.RequiredMutabilityMode(value = MutabilityMode.READ_WRITE) - override fun write(agent: String, oids: List, values: List): List { + override fun write(agent: String, oids: List, values: List): List { val snmpAgent = requireAgent(agent) if (oids.size != values.size) { throw ValueError("Length of values does not match length of OIDs.") } - return snmpAgent - .write( - oids.zip(values).map { (oid, value) -> - VariableBinding(OID(oid), OctetString(value)) - } - ) - .map { it.statusCode.toQualityCode() } + return snmpAgent.model + .write(oids.zip(values).map { (oid, value) -> Snmp4jOid(oid) to value }) + .map { it.value.toQualityCode() } } @RpcDelegate.RequiredMutabilityMode(value = MutabilityMode.READ_ONLY) override fun walk(agent: String, oids: List): List { val snmpAgent = requireAgent(agent) - return snmpAgent.walk(oids.map { OID(it) }).map { it.toQualifiedValue() } + return snmpAgent.model.walk(oids.map { Snmp4jOid(it) }).map { it.toQualifiedValue() } } @RpcDelegate.RequiredMutabilityMode(value = MutabilityMode.READ_ONLY) @@ -60,18 +54,18 @@ class SnmpAgentRpcImpl(val context: SnmpGatewayContext) : SnmpAgentRpc { upperBoundIndex: String?, ): List> { val snmpAgent = requireAgent(agent) - return snmpAgent + return snmpAgent.model .readTable( - columns.map { OID(it) }, + columns.map { Snmp4jOid(it) }, lowerBoundIndex.nullOrOid(), upperBoundIndex.nullOrOid(), ) - .map { it.map { binding -> binding.toQualifiedValue() } } + .map { results -> results.map { it.toQualifiedValue() } } } - private fun String?.nullOrOid(): OID? { + private fun String?.nullOrOid(): Oid? { this?.let { - return OID(it) + return Snmp4jOid(it) } return null } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/requests/OidReadRequest.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/requests/OidReadRequest.kt deleted file mode 100644 index 184729b6..00000000 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/requests/OidReadRequest.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.mussonindustrial.embr.snmp.requests - -import org.snmp4j.smi.OID - -interface OidReadRequest { - val oid: OID - var result: OidReadResult? -} diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/requests/OidReadResult.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/requests/OidReadResult.kt deleted file mode 100644 index 4ff32bd2..00000000 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/requests/OidReadResult.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.mussonindustrial.embr.snmp.requests - -import com.mussonindustrial.embr.snmp.model.BasicQualifiedOidValue -import com.mussonindustrial.embr.snmp.model.Oid -import com.mussonindustrial.embr.snmp.model.QualifiedOidValue -import com.mussonindustrial.embr.snmp.utils.toDataValue -import com.mussonindustrial.embr.snmp.utils.toQualityCode -import java.util.Date -import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue -import org.snmp4j.smi.OID -import org.snmp4j.smi.VariableBinding - -data class OidReadResult(val oid: Oid, val value: DataValue) { - fun toQualifiedValue(): QualifiedOidValue { - return BasicQualifiedOidValue( - this.oid, - this.value.value.value(), - this.value.statusCode.toQualityCode(), - this.value.serverTime?.javaDate ?: Date(), - ) - } -} - -fun OID.toOidReadResult(value: DataValue): OidReadResult { - return OidReadResult(Oid.fromSnmp4j(this), value) -} - -fun VariableBinding.toOidReadResult(): OidReadResult { - return OidReadResult(Oid.fromSnmp4j(this.oid), this.variable.toDataValue()) -} diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/requests/OidWriteRequest.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/requests/OidWriteRequest.kt deleted file mode 100644 index f6010008..00000000 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/requests/OidWriteRequest.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.mussonindustrial.embr.snmp.requests - -import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue -import org.snmp4j.smi.OID - -interface OidWriteRequest { - val oid: OID - val value: DataValue - var result: OidWriteResult? -} diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/requests/OidWriteResult.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/requests/OidWriteResult.kt deleted file mode 100644 index a8eadc03..00000000 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/requests/OidWriteResult.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.mussonindustrial.embr.snmp.requests - -import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode -import org.snmp4j.smi.OID - -data class OidWriteResult(val oid: OID, val statusCode: StatusCode) - -fun OID.toOidWriteResult(statusCode: StatusCode): OidWriteResult { - return OidWriteResult(this, statusCode) -} diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/typing/SnmpType.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/typing/SnmpType.kt new file mode 100644 index 00000000..2fdd2f08 --- /dev/null +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/typing/SnmpType.kt @@ -0,0 +1,40 @@ +package com.mussonindustrial.embr.snmp.typing + +import com.inductiveautomation.ignition.common.TypeUtilities +import org.eclipse.milo.opcua.stack.core.OpcUaDataType +import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId +import org.snmp4j.smi.Address +import org.snmp4j.smi.Counter32 +import org.snmp4j.smi.Counter64 +import org.snmp4j.smi.Gauge32 +import org.snmp4j.smi.Integer32 +import org.snmp4j.smi.IpAddress +import org.snmp4j.smi.OID +import org.snmp4j.smi.OctetString +import org.snmp4j.smi.UnsignedInteger32 +import org.snmp4j.smi.Variable + +enum class SnmpType(val uaDataType: NodeId, val variableOfType: (Any?) -> Variable) { + Int32(OpcUaDataType.Int32.nodeId, { Integer32(TypeUtilities.toInteger(it)) }), + UInt32(OpcUaDataType.UInt32.nodeId, { UnsignedInteger32(TypeUtilities.toLong(it)) }), + Gauge32(OpcUaDataType.UInt32.nodeId, { Gauge32(TypeUtilities.toLong(it)) }), + Counter32(OpcUaDataType.UInt64.nodeId, { Counter32(TypeUtilities.toLong(it)) }), + Counter64(OpcUaDataType.UInt64.nodeId, { Counter64(TypeUtilities.toLong(it)) }), + OctetString(OpcUaDataType.String.nodeId, { OctetString(TypeUtilities.toString(it)) }), + Oid(OpcUaDataType.String.nodeId, { OID(TypeUtilities.toString(it)) }), + IpAddress(OpcUaDataType.String.nodeId, { IpAddress(TypeUtilities.toString(it)) }), +} + +val Variable.snmpType: SnmpType + get() = + when (this) { + is OID -> SnmpType.Oid + is Gauge32 -> SnmpType.Gauge32 + is Counter32 -> SnmpType.Counter32 + is Counter64 -> SnmpType.Counter64 + is Integer32 -> SnmpType.Int32 + is UnsignedInteger32 -> SnmpType.UInt32 + is Address -> SnmpType.IpAddress + is OctetString -> SnmpType.OctetString + else -> SnmpType.OctetString + } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/SnmpUtils.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/SnmpUtils.kt index 1b1b4acc..1af64465 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/SnmpUtils.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/SnmpUtils.kt @@ -1,32 +1,18 @@ package com.mussonindustrial.embr.snmp.utils +import com.mussonindustrial.embr.snmp.model.BasicQualifiedOidValue +import com.mussonindustrial.embr.snmp.model.OidValue +import com.mussonindustrial.embr.snmp.model.QualifiedOidValue import java.text.ParseException -import org.eclipse.milo.opcua.sdk.server.Lifecycle -import org.eclipse.milo.opcua.sdk.server.LifecycleManager -import org.eclipse.milo.opcua.stack.core.StatusCodes +import java.util.Date import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue -import org.eclipse.milo.opcua.stack.core.types.builtin.Variant import org.snmp4j.PDU import org.snmp4j.SNMP4JSettings -import org.snmp4j.Snmp import org.snmp4j.Target import org.snmp4j.smi.Address -import org.snmp4j.smi.Null -import org.snmp4j.smi.OID -import org.snmp4j.smi.OctetString -import org.snmp4j.smi.Variable import org.snmp4j.smi.VariableBinding import org.snmp4j.util.PDUFactory -fun String.toVariableBinding(): VariableBinding { - return VariableBinding(OID(this)) -} - -fun PDU.addOID(oid: String) { - val binding = VariableBinding(OID(oid)) - this.add(binding) -} - fun String.isOid(): Boolean { try { SNMP4JSettings.getOIDTextFormat().parse(this) @@ -36,34 +22,6 @@ fun String.isOid(): Boolean { } } -fun Variable.toDataValue(): DataValue { - return when (this) { - Null.endOfMibView -> DataValue(StatusCodes.Bad_NotFound) - Null.noSuchObject -> DataValue(StatusCodes.Bad_NotFound) - Null.noSuchInstance -> DataValue(StatusCodes.Bad_NotFound) - Null.instance -> DataValue(Variant.NULL_VALUE) - else -> DataValue(Variant(this.toString())) - } -} - -fun DataValue.toVariable(): Variable { - return OctetString(this.value.value?.toString()) -} - -fun LifecycleManager.addLifecycle(snmp: Snmp) { - this.addLifecycle( - object : Lifecycle { - override fun startup() { - snmp.listen() - } - - override fun shutdown() { - snmp.close() - } - } - ) -} - fun Target.createSizeBoundedPDUs( pduFactory: PDUFactory, bindings: List, @@ -72,14 +30,18 @@ fun Target.createSizeBoundedPDUs( val pdus = mutableListOf() var pdu = pduFactory.createPDU(this).apply { configure(this) } + var count = 0 bindings.forEach { binding -> pdu.add(binding) + count++ - if (pdu.berLength > maxSizeRequestPDU) { + if (pdu.berLength > maxSizeRequestPDU || count > 100) { pdu.trim() pdus.add(pdu) pdu = pduFactory.createPDU(this).apply { configure(this) } + pdu.add(binding) + count = 1 } } if (pdu.size() > 0) { @@ -88,3 +50,11 @@ fun Target.createSizeBoundedPDUs( return pdus } + +fun OidValue.toQualifiedValue(): QualifiedOidValue = + BasicQualifiedOidValue( + oid, + this.value.value.value, + this.value.statusCode.toQualityCode(), + this.value.serverTime?.javaDate ?: Date(), + ) From 18732be9c8c979a4db5d920067d5a74a246f4cc4 Mon Sep 17 00:00:00 2001 From: Ben Musson Date: Mon, 2 Feb 2026 13:41:36 -0500 Subject: [PATCH 2/9] rename and add diagnostics --- .../snmp/agents/devices/SnmpAgentDevice.kt | 4 +- .../agents/devices/SnmpAgentDeviceImpl.kt | 43 ++++++++++++++++--- .../ConcurrentObjectModel.kt} | 16 +++---- .../OidModel.kt => model/ObjectModel.kt} | 4 +- ...kt => BrowsableObjectModelAddressSpace.kt} | 6 ++- .../snmp/agents/opc/DiagnosticAddressSpace.kt | 40 ++++++++++++++++- .../embr/snmp/agents/opc/OidAddressSpace.kt | 31 ++++++++++--- .../embr/snmp/opc/DeviceAddressSpace.kt | 8 +++- 8 files changed, 123 insertions(+), 29 deletions(-) rename modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/{context/ConcurrentOidModel.kt => model/ConcurrentObjectModel.kt} (74%) rename modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/{context/OidModel.kt => model/ObjectModel.kt} (97%) rename modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/{BrowsableOidModelAddressSpace.kt => BrowsableObjectModelAddressSpace.kt} (95%) diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDevice.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDevice.kt index 5ebb9729..4e27e923 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDevice.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDevice.kt @@ -1,8 +1,8 @@ package com.mussonindustrial.embr.snmp.agents.devices import com.inductiveautomation.ignition.gateway.opcua.server.api.Device -import com.mussonindustrial.embr.snmp.agents.context.OidModel import com.mussonindustrial.embr.snmp.agents.context.SnmpAgentContext +import com.mussonindustrial.embr.snmp.agents.model.ObjectModel import com.mussonindustrial.embr.snmp.model.Oid import com.mussonindustrial.embr.snmp.model.OidValue import org.eclipse.milo.opcua.sdk.server.AddressSpaceFragment @@ -14,7 +14,7 @@ interface SnmpAgentDevice : AddressSpaceFragment, Device { val context: SnmpAgentContext<*> val status: Status - val model: OidModel + val model: ObjectModel fun read(reads: List): List> diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDeviceImpl.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDeviceImpl.kt index 23674861..6ecc88fd 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDeviceImpl.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDeviceImpl.kt @@ -3,9 +3,9 @@ package com.mussonindustrial.embr.snmp.agents.devices import com.inductiveautomation.ignition.common.util.LoggerEx import com.mussonindustrial.embr.snmp.SnmpGatewayContext import com.mussonindustrial.embr.snmp.agents.configuration.SnmpAgentConfig -import com.mussonindustrial.embr.snmp.agents.context.ConcurrentOidModel import com.mussonindustrial.embr.snmp.agents.context.SnmpAgentContext -import com.mussonindustrial.embr.snmp.agents.opc.BrowsableOidModelAddressSpace +import com.mussonindustrial.embr.snmp.agents.model.ConcurrentObjectModel +import com.mussonindustrial.embr.snmp.agents.opc.BrowsableObjectModelAddressSpace import com.mussonindustrial.embr.snmp.agents.opc.DiagnosticAddressSpace import com.mussonindustrial.embr.snmp.agents.opc.OidAddressSpace import com.mussonindustrial.embr.snmp.model.BasicOidValue @@ -27,6 +27,8 @@ import org.snmp4j.PDU import org.snmp4j.smi.Variable import org.snmp4j.smi.VariableBinding import org.snmp4j.util.TableUtils +import org.snmp4j.util.TreeEvent +import org.snmp4j.util.TreeListener import org.snmp4j.util.TreeUtils class SnmpAgentDeviceImpl(override val context: SnmpAgentContext) : @@ -39,14 +41,14 @@ class SnmpAgentDeviceImpl(override val context: SnmpAgentCo override var status: SnmpAgentDevice.Status = SnmpAgentDevice.Status.DISCONNECTED private set - override val model = ConcurrentOidModel(this) + override val model = ConcurrentObjectModel(this) val healthcheck = Healthcheck() val deviceAddressSpace = DeviceAddressSpace(context.deviceContext, this) val diagnosticAddressSpace = DiagnosticAddressSpace(this, this) val oidAddressSpace = OidAddressSpace(this, this) - val browsableOidModelAddressSpace = BrowsableOidModelAddressSpace(this, this) + val browsableObjectModelAddressSpace = BrowsableObjectModelAddressSpace(this, this) init { lifecycleManager.addLifecycle(context) @@ -54,13 +56,13 @@ class SnmpAgentDeviceImpl(override val context: SnmpAgentCo lifecycleManager.addLifecycle(deviceAddressSpace) lifecycleManager.addLifecycle(diagnosticAddressSpace) lifecycleManager.addLifecycle(oidAddressSpace) - lifecycleManager.addLifecycle(browsableOidModelAddressSpace) + lifecycleManager.addLifecycle(browsableObjectModelAddressSpace) lifecycleManager.addStartupTask { onDataItemsCreated( context.deviceContext.subscriptionModel.getDataItems(context.deviceContext.name) ) } - lifecycleManager.addStartupTask { model.walk(listOf(Snmp4jOid("1"))) } + lifecycleManager.addStartupTask { buildModel() } } val treeUtils = TreeUtils(context.snmp, context.pduFactory) @@ -194,6 +196,31 @@ class SnmpAgentDeviceImpl(override val context: SnmpAgentCo } } + fun walkAsync(roots: List, listener: WalkListener) { + treeUtils.walk( + context.readTarget, + roots.map { it.toSnmp4j() }.toTypedArray(), + context, + object : TreeListener { + + override fun next(event: TreeEvent): Boolean { + event.variableBindings?.forEach { binding -> + listener.receiveEvent(BasicOidValue(binding.oid.toOid(), binding.variable)) + } + return true + } + + override fun isFinished(): Boolean = false + + override fun finished(event: TreeEvent) {} + }, + ) + } + + fun buildModel() { + walkAsync(listOf(Snmp4jOid("1"))) { model.coerceCache(it) } + } + override fun readTable( columns: List, lowerBoundIndex: Oid?, @@ -213,6 +240,10 @@ class SnmpAgentDeviceImpl(override val context: SnmpAgentCo } } + fun interface WalkListener { + fun receiveEvent(variable: OidValue) + } + inner class Healthcheck : Lifecycle { private val taskOwner = "healthcheck" diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/context/ConcurrentOidModel.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/model/ConcurrentObjectModel.kt similarity index 74% rename from modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/context/ConcurrentOidModel.kt rename to modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/model/ConcurrentObjectModel.kt index c875fae9..57d69185 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/context/ConcurrentOidModel.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/model/ConcurrentObjectModel.kt @@ -1,4 +1,4 @@ -package com.mussonindustrial.embr.snmp.agents.context +package com.mussonindustrial.embr.snmp.agents.model import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice import com.mussonindustrial.embr.snmp.model.BasicOidValue @@ -10,9 +10,9 @@ import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode import org.snmp4j.smi.Variable -class ConcurrentOidModel(val device: SnmpAgentDevice) : OidModel { +class ConcurrentObjectModel(val device: SnmpAgentDevice) : ObjectModel { - private val descriptors = ConcurrentHashMap() + private val descriptors = ConcurrentHashMap() private val knownValues = ConcurrentHashMap>() override val oids: List @@ -43,7 +43,7 @@ class ConcurrentOidModel(val device: SnmpAgentDevice) : OidModel { } fun coerceCache(value: OidValue): OidValue { - val descriptor = OidModel.ValueDescriptor(value.oid, value.value.snmpType) + val descriptor = ObjectModel.ValueDescriptor(value.oid, value.value.snmpType) val result = toOpcUaValue(value) descriptors[result.oid] = descriptor @@ -52,17 +52,17 @@ class ConcurrentOidModel(val device: SnmpAgentDevice) : OidModel { return result } - override fun getDescriptors(oids: List): List { - val results = oids.map { descriptors.getOrDefault(it, OidModel.UnknownDescriptor(it)) } + override fun getDescriptors(oids: List): List { + val results = oids.map { descriptors.getOrDefault(it, ObjectModel.UnknownDescriptor(it)) } val missingDescriptors = oids .zip(results) - .filter { (_, descriptor) -> descriptor is OidModel.UnknownDescriptor } + .filter { (_, descriptor) -> descriptor is ObjectModel.UnknownDescriptor } .map { it.first } if (missingDescriptors.isEmpty()) return results read(missingDescriptors) - return oids.map { descriptors.getOrDefault(it, OidModel.InvalidDescriptor(it)) } + return oids.map { descriptors.getOrDefault(it, ObjectModel.InvalidDescriptor(it)) } } } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/context/OidModel.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/model/ObjectModel.kt similarity index 97% rename from modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/context/OidModel.kt rename to modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/model/ObjectModel.kt index dcb39147..8c42ee77 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/context/OidModel.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/model/ObjectModel.kt @@ -1,4 +1,4 @@ -package com.mussonindustrial.embr.snmp.agents.context +package com.mussonindustrial.embr.snmp.agents.model import com.mussonindustrial.embr.snmp.model.BasicOidValue import com.mussonindustrial.embr.snmp.model.Oid @@ -19,7 +19,7 @@ import org.snmp4j.smi.SMIAddress import org.snmp4j.smi.UnsignedInteger32 import org.snmp4j.smi.Variable -interface OidModel { +interface ObjectModel { val oids: List diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/BrowsableOidModelAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/BrowsableObjectModelAddressSpace.kt similarity index 95% rename from modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/BrowsableOidModelAddressSpace.kt rename to modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/BrowsableObjectModelAddressSpace.kt index 7b85333f..5560ced0 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/BrowsableOidModelAddressSpace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/BrowsableObjectModelAddressSpace.kt @@ -11,8 +11,10 @@ import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId import org.eclipse.milo.opcua.stack.core.types.structured.ViewDescription -class BrowsableOidModelAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceComposite) : - DeviceContextManagedAddressSpaceFragment(device.context.deviceContext, composite) { +class BrowsableObjectModelAddressSpace( + val device: SnmpAgentDevice, + composite: AddressSpaceComposite, +) : DeviceContextManagedAddressSpaceFragment(device.context.deviceContext, composite) { private val root = "Objects" private val model = device.model diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DiagnosticAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DiagnosticAddressSpace.kt index 23a46192..5bf7d396 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DiagnosticAddressSpace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DiagnosticAddressSpace.kt @@ -19,7 +19,7 @@ import org.eclipse.milo.opcua.stack.core.types.builtin.Variant class DiagnosticAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceComposite) : DeviceContextManagedAddressSpaceFragment(device.context.deviceContext, composite) { - private val root = "[Diagnostics]" + private val root = "Diagnostics" init { lifecycleManager.addLifecycle( @@ -76,6 +76,42 @@ class DiagnosticAddressSpace(val device: SnmpAgentDevice, composite: AddressSpac DataValue(Variant(device.status == SnmpAgentDevice.Status.CONNECTED)) }, ) + addDiagnosticNode( + diagnosticsFolder, + "PendingAsyncRequests", + NodeIds.UInt32, + AttributeFilters.getValue { + DataValue(Variant(device.context.snmp.pendingAsyncRequestCount)) + }, + ) + addDiagnosticNode( + diagnosticsFolder, + "PendingSyncRequests", + NodeIds.UInt32, + AttributeFilters.getValue { + DataValue(Variant(device.context.snmp.pendingSyncRequestCount)) + }, + ) + addDiagnosticNode( + diagnosticsFolder, + "MaxRequestPduSize", + NodeIds.UInt32, + AttributeFilters.getValue { + DataValue(Variant(device.context.readTarget.maxSizeRequestPDU)) + }, + ) + addDiagnosticNode( + diagnosticsFolder, + "RetryCount", + NodeIds.UInt32, + AttributeFilters.getValue { DataValue(Variant(device.context.readTarget.retries)) }, + ) + addDiagnosticNode( + diagnosticsFolder, + "ObjectModeSize", + NodeIds.UInt32, + AttributeFilters.getValue { DataValue(Variant(device.model.oids.size)) }, + ) } fun addDiagnosticNode( @@ -85,7 +121,7 @@ class DiagnosticAddressSpace(val device: SnmpAgentDevice, composite: AddressSpac attributeFilter: AttributeFilter, ) { UaVariableNode.UaVariableNodeBuilder(nodeContext).run { - setNodeId(nodeId("${root}${name}")) + setNodeId(nodeId("${root}/${name}")) setBrowseName(qualifiedName(name)) setDisplayName(LocalizedText.english(name)) setDataType(dataType) diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt index 7a8860cf..0d5730e7 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt @@ -1,13 +1,14 @@ package com.mussonindustrial.embr.snmp.agents.opc -import com.mussonindustrial.embr.snmp.agents.context.OidModel import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice +import com.mussonindustrial.embr.snmp.agents.model.ObjectModel import com.mussonindustrial.embr.snmp.model.OidValue import com.mussonindustrial.embr.snmp.model.Snmp4jOid import com.mussonindustrial.embr.snmp.opc.DeviceContextManagedAddressSpaceFragment import com.mussonindustrial.embr.snmp.utils.isOid import kotlin.jvm.optionals.getOrNull import org.eclipse.milo.opcua.sdk.core.AccessLevel +import org.eclipse.milo.opcua.sdk.core.Reference import org.eclipse.milo.opcua.sdk.core.ValueRank import org.eclipse.milo.opcua.sdk.server.AddressSpace import org.eclipse.milo.opcua.sdk.server.AddressSpaceComposite @@ -19,6 +20,7 @@ import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger import org.eclipse.milo.opcua.stack.core.types.enumerated.NodeClass import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn import org.eclipse.milo.opcua.stack.core.types.structured.ReadValueId +import org.eclipse.milo.opcua.stack.core.types.structured.ViewDescription import org.eclipse.milo.opcua.stack.core.types.structured.WriteValue class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceComposite) : @@ -78,7 +80,7 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos fun resolveAttributeValue( attributeId: AttributeId?, nodeId: NodeId, - descriptor: OidModel.Descriptor, + descriptor: ObjectModel.Descriptor, ): Any? { return when (attributeId) { AttributeId.NodeId -> nodeId @@ -95,17 +97,17 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos AttributeId.DataType -> when (descriptor) { - is OidModel.ValueDescriptor -> descriptor.snmpType.uaDataType + is ObjectModel.ValueDescriptor -> descriptor.snmpType.uaDataType else -> OpcUaDataType.String.nodeId } AttributeId.ValueRank -> when (descriptor) { - is OidModel.ValueDescriptor -> ValueRank.Scalar.value + is ObjectModel.ValueDescriptor -> ValueRank.Scalar.value else -> ValueRank.Scalar.value } AttributeId.ArrayDimensions -> when (descriptor) { - is OidModel.ValueDescriptor -> null + is ObjectModel.ValueDescriptor -> null else -> null } @@ -149,6 +151,25 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos return results.map { it.value } } + override fun browse( + context: AddressSpace.BrowseContext, + view: ViewDescription, + nodeIds: List, + ): List { + return nodeIds.map { nodeId -> + AddressSpace.ReferenceResult.of( + listOf( + Reference( + nodeId, + NodeIds.Organizes, + nodeId("Objects/Numeric").expanded(), + Reference.Direction.INVERSE, + ) + ) + ) + } + } + override fun getFilter(): AddressSpaceFilter { return filter } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/DeviceAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/DeviceAddressSpace.kt index 9e88ed69..378ad0dc 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/DeviceAddressSpace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/DeviceAddressSpace.kt @@ -5,19 +5,23 @@ import com.mussonindustrial.embr.snmp.utils.removeAllNodes import org.eclipse.milo.opcua.sdk.core.Reference import org.eclipse.milo.opcua.sdk.server.AddressSpaceComposite import org.eclipse.milo.opcua.sdk.server.Lifecycle -import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode +import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectNode import org.eclipse.milo.opcua.stack.core.NodeIds import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText +import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger class DeviceAddressSpace(deviceContext: DeviceContext, composite: AddressSpaceComposite) : DeviceContextManagedAddressSpaceFragment(deviceContext, composite) { private val deviceFolderNode = - UaFolderNode( + UaObjectNode( nodeContext, deviceNodeId, qualifiedName("[${name}]"), LocalizedText("[${name}]"), + LocalizedText("SNMP Agent Device"), + UInteger.MIN, + UInteger.MIN, ) init { From e11ec01acefa68cebe1cfe914e2a366e7b41fe69 Mon Sep 17 00:00:00 2001 From: Ben Musson Date: Tue, 3 Feb 2026 02:52:00 -0500 Subject: [PATCH 3/9] merge model + device, add methods namespace, add property support --- .../embr/snmp/SnmpGatewayContext.kt | 10 ++ .../snmp/agents/devices/SnmpAgentDevice.kt | 10 +- .../agents/devices/SnmpAgentDeviceImpl.kt | 58 ++++-- .../agents/model/ConcurrentObjectModel.kt | 44 +---- .../embr/snmp/agents/model/ObjectModel.kt | 21 +-- .../snmp/agents/opc/DiagnosticAddressSpace.kt | 26 +-- .../snmp/agents/opc/MethodAddressSpace.kt | 136 +++++++++++++++ ...essSpace.kt => ObjectModelAddressSpace.kt} | 28 +-- .../embr/snmp/agents/opc/OidAddressSpace.kt | 145 ++++++++++++--- .../snmp/agents/opc/types/OidValueType.kt | 165 ++++++++++++++++++ .../embr/snmp/agents/rpc/SnmpAgentRpcImpl.kt | 8 +- .../embr/snmp/opc/SnmpNamespace.kt | 52 ++++++ .../typing/{SnmpType.kt => SnmpDataType.kt} | 22 +-- .../embr/snmp/utils/OidPath.kt | 10 ++ .../embr/snmp/utils/OidSuffix.kt | 46 +++++ .../embr/snmp/utils/SnmpUtils.kt | 2 +- 16 files changed, 634 insertions(+), 149 deletions(-) create mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/MethodAddressSpace.kt rename modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/{BrowsableObjectModelAddressSpace.kt => ObjectModelAddressSpace.kt} (75%) create mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/types/OidValueType.kt create mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNamespace.kt rename modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/typing/{SnmpType.kt => SnmpDataType.kt} (69%) create mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/OidPath.kt create mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/OidSuffix.kt diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/SnmpGatewayContext.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/SnmpGatewayContext.kt index 4663a1a6..bca1dbc3 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/SnmpGatewayContext.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/SnmpGatewayContext.kt @@ -10,8 +10,10 @@ import com.mussonindustrial.embr.gateway.EmbrGatewayContext import com.mussonindustrial.embr.gateway.EmbrGatewayContextImpl import com.mussonindustrial.embr.snmp.agents.SnmpAgentRegistry import com.mussonindustrial.embr.snmp.agents.rpc.SnmpAgentRpcImpl +import com.mussonindustrial.embr.snmp.opc.SnmpNamespace import java.util.concurrent.ThreadFactory import java.util.concurrent.atomic.AtomicInteger +import org.eclipse.milo.opcua.sdk.server.OpcUaServer import org.snmp4j.SNMP4JSettings import org.snmp4j.mp.MPv3 import org.snmp4j.security.SecurityModels @@ -32,6 +34,7 @@ class SnmpGatewayContext(private val context: GatewayContext) : val logger = this.getLoggerEx() val agentRegistry = SnmpAgentRegistry() val agentRpc = SnmpAgentRpcImpl(this) + lateinit var opcUaServer: OpcUaServer init { instance = this @@ -62,6 +65,13 @@ class SnmpGatewayContext(private val context: GatewayContext) : }, ) + fun initOpcUaServer(server: OpcUaServer) { + if (!::opcUaServer.isInitialized) { + opcUaServer = server + SnmpNamespace(server).startup() + } + } + override fun getHealthCheckRegistry(): HealthCheckRegistry? { return super.getHealthCheckRegistry() } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDevice.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDevice.kt index 4e27e923..bcd6312c 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDevice.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDevice.kt @@ -6,9 +6,9 @@ import com.mussonindustrial.embr.snmp.agents.model.ObjectModel import com.mussonindustrial.embr.snmp.model.Oid import com.mussonindustrial.embr.snmp.model.OidValue import org.eclipse.milo.opcua.sdk.server.AddressSpaceFragment +import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode -import org.snmp4j.smi.Variable interface SnmpAgentDevice : AddressSpaceFragment, Device { @@ -16,17 +16,17 @@ interface SnmpAgentDevice : AddressSpaceFragment, Device { val status: Status val model: ObjectModel - fun read(reads: List): List> + fun read(reads: List): List> - fun write(writes: List>): List> + fun write(writes: List>): List> - fun walk(roots: List): List> + fun walk(roots: List): List> fun readTable( columns: List, lowerBoundIndex: Oid?, upperBoundIndex: Oid?, - ): List>> + ): List>> fun stripDeviceName(nodeId: NodeId): String { val id = nodeId.identifier.toString() diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDeviceImpl.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDeviceImpl.kt index 6ecc88fd..bf645997 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDeviceImpl.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDeviceImpl.kt @@ -5,8 +5,9 @@ import com.mussonindustrial.embr.snmp.SnmpGatewayContext import com.mussonindustrial.embr.snmp.agents.configuration.SnmpAgentConfig import com.mussonindustrial.embr.snmp.agents.context.SnmpAgentContext import com.mussonindustrial.embr.snmp.agents.model.ConcurrentObjectModel -import com.mussonindustrial.embr.snmp.agents.opc.BrowsableObjectModelAddressSpace import com.mussonindustrial.embr.snmp.agents.opc.DiagnosticAddressSpace +import com.mussonindustrial.embr.snmp.agents.opc.MethodAddressSpace +import com.mussonindustrial.embr.snmp.agents.opc.ObjectModelAddressSpace import com.mussonindustrial.embr.snmp.agents.opc.OidAddressSpace import com.mussonindustrial.embr.snmp.model.BasicOidValue import com.mussonindustrial.embr.snmp.model.Oid @@ -18,10 +19,12 @@ import com.mussonindustrial.embr.snmp.model.toSnmp4j import com.mussonindustrial.embr.snmp.opc.DeviceAddressSpace import com.mussonindustrial.embr.snmp.utils.createSizeBoundedPDUs import java.util.concurrent.TimeUnit +import kotlin.collections.map import org.eclipse.milo.opcua.sdk.server.AddressSpaceComposite import org.eclipse.milo.opcua.sdk.server.Lifecycle import org.eclipse.milo.opcua.sdk.server.LifecycleManager import org.eclipse.milo.opcua.stack.core.StatusCodes +import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode import org.snmp4j.PDU import org.snmp4j.smi.Variable @@ -48,21 +51,24 @@ class SnmpAgentDeviceImpl(override val context: SnmpAgentCo val deviceAddressSpace = DeviceAddressSpace(context.deviceContext, this) val diagnosticAddressSpace = DiagnosticAddressSpace(this, this) val oidAddressSpace = OidAddressSpace(this, this) - val browsableObjectModelAddressSpace = BrowsableObjectModelAddressSpace(this, this) + val objectModelAddressSpace = ObjectModelAddressSpace(this, this) + val methodAddressSpace = MethodAddressSpace(this, this) init { + lifecycleManager.addStartupTask { SnmpGatewayContext.instance.initOpcUaServer(server) } lifecycleManager.addLifecycle(context) - lifecycleManager.addLifecycle(healthcheck) lifecycleManager.addLifecycle(deviceAddressSpace) lifecycleManager.addLifecycle(diagnosticAddressSpace) + lifecycleManager.addLifecycle(methodAddressSpace) + lifecycleManager.addLifecycle(objectModelAddressSpace) lifecycleManager.addLifecycle(oidAddressSpace) - lifecycleManager.addLifecycle(browsableObjectModelAddressSpace) + lifecycleManager.addLifecycle(healthcheck) lifecycleManager.addStartupTask { + learnObjectModel() onDataItemsCreated( context.deviceContext.subscriptionModel.getDataItems(context.deviceContext.name) ) } - lifecycleManager.addStartupTask { buildModel() } } val treeUtils = TreeUtils(context.snmp, context.pduFactory) @@ -93,7 +99,31 @@ class SnmpAgentDeviceImpl(override val context: SnmpAgentCo } } - override fun read(reads: List): List> { + override fun read(reads: List): List> { + return readRaw(reads).map { model.observe(it) } + } + + override fun write(writes: List>): List> { + return writeRaw( + writes.map { (oid, value) -> oid to model.toSnmpValue(BasicOidValue(oid, value)).value } + ) + } + + override fun walk(roots: List): List> { + return walkRaw(roots).map { model.observe(it) } + } + + override fun readTable( + columns: List, + lowerBoundIndex: Oid?, + upperBoundIndex: Oid?, + ): List>> { + return readTableRaw(columns, lowerBoundIndex, upperBoundIndex).map { + it.map { result -> model.observe(result) } + } + } + + fun readRaw(reads: List): List> { val results = mutableMapOf>() val remaining = reads.groupBy { it }.toMutableMap() @@ -152,7 +182,7 @@ class SnmpAgentDeviceImpl(override val context: SnmpAgentCo return reads.map { results[it]!! } } - override fun write(writes: List>): List> { + fun writeRaw(writes: List>): List> { return writes.map { (oid, value) -> if (context.writeTarget == null) { return@map BasicOidValue(oid, StatusCode(StatusCodes.Bad_CommunicationError)) @@ -187,7 +217,7 @@ class SnmpAgentDeviceImpl(override val context: SnmpAgentCo } } - override fun walk(roots: List): List> { + fun walkRaw(roots: List): List> { val results = treeUtils.walk(context.readTarget, roots.map { it.toSnmp4j() }.toTypedArray()) return results.flatMap { it.variableBindings?.map { binding -> @@ -217,11 +247,7 @@ class SnmpAgentDeviceImpl(override val context: SnmpAgentCo ) } - fun buildModel() { - walkAsync(listOf(Snmp4jOid("1"))) { model.coerceCache(it) } - } - - override fun readTable( + fun readTableRaw( columns: List, lowerBoundIndex: Oid?, upperBoundIndex: Oid?, @@ -244,6 +270,10 @@ class SnmpAgentDeviceImpl(override val context: SnmpAgentCo fun receiveEvent(variable: OidValue) } + fun learnObjectModel() { + walkAsync(listOf(Snmp4jOid("1"))) { model.observe(it) } + } + inner class Healthcheck : Lifecycle { private val taskOwner = "healthcheck" @@ -295,7 +325,7 @@ class SnmpAgentDeviceImpl(override val context: SnmpAgentCo } val response = read(listOf(Snmp4jOid(context.snmpConfig.healthcheck.oid!!))).first() - val isGood = response.value != SnmpCommunicationError + val isGood = response.value.statusCode.isGood logger.trace("Health check result: $isGood") status = diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/model/ConcurrentObjectModel.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/model/ConcurrentObjectModel.kt index 57d69185..499f6d29 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/model/ConcurrentObjectModel.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/model/ConcurrentObjectModel.kt @@ -1,13 +1,11 @@ package com.mussonindustrial.embr.snmp.agents.model import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice -import com.mussonindustrial.embr.snmp.model.BasicOidValue import com.mussonindustrial.embr.snmp.model.Oid import com.mussonindustrial.embr.snmp.model.OidValue -import com.mussonindustrial.embr.snmp.typing.snmpType +import com.mussonindustrial.embr.snmp.typing.snmpDataType import java.util.concurrent.ConcurrentHashMap import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue -import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode import org.snmp4j.smi.Variable class ConcurrentObjectModel(val device: SnmpAgentDevice) : ObjectModel { @@ -18,32 +16,8 @@ class ConcurrentObjectModel(val device: SnmpAgentDevice) : ObjectModel { override val oids: List get() = descriptors.map { it.key } - override fun read(reads: List): List> { - return device.read(reads).map { coerceCache(it) } - } - - override fun write(writes: List>): List> { - return device.write( - writes.map { (oid, value) -> oid to toSnmpValue(BasicOidValue(oid, value)).value } - ) - } - - override fun walk(roots: List): List> { - return device.walk(roots).map { coerceCache(it) } - } - - override fun readTable( - columns: List, - lowerBoundIndex: Oid?, - upperBoundIndex: Oid?, - ): List>> { - return device.readTable(columns, lowerBoundIndex, upperBoundIndex).map { - it.map { result -> coerceCache(result) } - } - } - - fun coerceCache(value: OidValue): OidValue { - val descriptor = ObjectModel.ValueDescriptor(value.oid, value.value.snmpType) + override fun observe(value: OidValue): OidValue { + val descriptor = ObjectModel.ValueDescriptor(value.oid, value.value.snmpDataType) val result = toOpcUaValue(value) descriptors[result.oid] = descriptor @@ -53,16 +27,6 @@ class ConcurrentObjectModel(val device: SnmpAgentDevice) : ObjectModel { } override fun getDescriptors(oids: List): List { - val results = oids.map { descriptors.getOrDefault(it, ObjectModel.UnknownDescriptor(it)) } - - val missingDescriptors = - oids - .zip(results) - .filter { (_, descriptor) -> descriptor is ObjectModel.UnknownDescriptor } - .map { it.first } - if (missingDescriptors.isEmpty()) return results - - read(missingDescriptors) - return oids.map { descriptors.getOrDefault(it, ObjectModel.InvalidDescriptor(it)) } + return oids.map { descriptors.getOrDefault(it, ObjectModel.UnknownDescriptor(it)) } } } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/model/ObjectModel.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/model/ObjectModel.kt index 8c42ee77..c21518e2 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/model/ObjectModel.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/model/ObjectModel.kt @@ -4,10 +4,9 @@ import com.mussonindustrial.embr.snmp.model.BasicOidValue import com.mussonindustrial.embr.snmp.model.Oid import com.mussonindustrial.embr.snmp.model.OidValue import com.mussonindustrial.embr.snmp.model.SnmpCommunicationError -import com.mussonindustrial.embr.snmp.typing.SnmpType +import com.mussonindustrial.embr.snmp.typing.SnmpDataType import org.eclipse.milo.opcua.stack.core.StatusCodes import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue -import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode import org.eclipse.milo.opcua.stack.core.types.builtin.Variant import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.ULong import org.snmp4j.smi.Counter64 @@ -23,17 +22,7 @@ interface ObjectModel { val oids: List - fun read(reads: List): List> - - fun write(writes: List>): List> - - fun walk(roots: List): List> - - fun readTable( - columns: List, - lowerBoundIndex: Oid?, - upperBoundIndex: Oid?, - ): List>> + fun observe(value: OidValue): OidValue fun getDescriptors(oids: List): List @@ -42,7 +31,7 @@ interface ObjectModel { val snmpValue = when (descriptor) { - is ValueDescriptor -> descriptor.snmpType.variableOfType(value.value) + is ValueDescriptor -> descriptor.snmpDataType.variableOfType(value.value) is InvalidDescriptor -> Null.instance is TableColumnDescriptor -> Null.instance is TableDescriptor -> Null.instance @@ -79,9 +68,9 @@ interface ObjectModel { class UnknownDescriptor(oid: Oid) : Descriptor(oid) - open class ValueDescriptor(oid: Oid, val snmpType: SnmpType) : Descriptor(oid) + open class ValueDescriptor(oid: Oid, val snmpDataType: SnmpDataType) : Descriptor(oid) - class TableColumnDescriptor(oid: Oid, val snmpType: SnmpType) : Descriptor(oid) + class TableColumnDescriptor(oid: Oid, val snmpDataType: SnmpDataType) : Descriptor(oid) class TableDescriptor(oid: Oid, val columns: List) : Descriptor(oid) } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DiagnosticAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DiagnosticAddressSpace.kt index 5bf7d396..17e9294a 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DiagnosticAddressSpace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DiagnosticAddressSpace.kt @@ -36,18 +36,18 @@ class DiagnosticAddressSpace(val device: SnmpAgentDevice, composite: AddressSpac } fun addNodes() { - val diagnosticsFolder = + val folder = UaFolderNode( nodeContext, nodeId(root), qualifiedName(root), LocalizedText.english(root), ) - nodeManager.addNode(diagnosticsFolder) + nodeManager.addNode(folder) - diagnosticsFolder.addReference( + folder.addReference( Reference( - diagnosticsFolder.nodeId, + folder.nodeId, NodeIds.Organizes, deviceNodeId.expanded(), Reference.Direction.INVERSE, @@ -55,7 +55,7 @@ class DiagnosticAddressSpace(val device: SnmpAgentDevice, composite: AddressSpac ) addDiagnosticNode( - diagnosticsFolder, + folder, "Address", NodeIds.String, AttributeFilters.getValue { @@ -63,13 +63,13 @@ class DiagnosticAddressSpace(val device: SnmpAgentDevice, composite: AddressSpac }, ) addDiagnosticNode( - diagnosticsFolder, + folder, "Status", NodeIds.String, AttributeFilters.getValue { DataValue(Variant(device.status.toString())) }, ) addDiagnosticNode( - diagnosticsFolder, + folder, "Connected", NodeIds.Boolean, AttributeFilters.getValue { @@ -77,7 +77,7 @@ class DiagnosticAddressSpace(val device: SnmpAgentDevice, composite: AddressSpac }, ) addDiagnosticNode( - diagnosticsFolder, + folder, "PendingAsyncRequests", NodeIds.UInt32, AttributeFilters.getValue { @@ -85,7 +85,7 @@ class DiagnosticAddressSpace(val device: SnmpAgentDevice, composite: AddressSpac }, ) addDiagnosticNode( - diagnosticsFolder, + folder, "PendingSyncRequests", NodeIds.UInt32, AttributeFilters.getValue { @@ -93,7 +93,7 @@ class DiagnosticAddressSpace(val device: SnmpAgentDevice, composite: AddressSpac }, ) addDiagnosticNode( - diagnosticsFolder, + folder, "MaxRequestPduSize", NodeIds.UInt32, AttributeFilters.getValue { @@ -101,14 +101,14 @@ class DiagnosticAddressSpace(val device: SnmpAgentDevice, composite: AddressSpac }, ) addDiagnosticNode( - diagnosticsFolder, + folder, "RetryCount", NodeIds.UInt32, AttributeFilters.getValue { DataValue(Variant(device.context.readTarget.retries)) }, ) addDiagnosticNode( - diagnosticsFolder, - "ObjectModeSize", + folder, + "ObjectModelSize", NodeIds.UInt32, AttributeFilters.getValue { DataValue(Variant(device.model.oids.size)) }, ) diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/MethodAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/MethodAddressSpace.kt new file mode 100644 index 00000000..09aca0d0 --- /dev/null +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/MethodAddressSpace.kt @@ -0,0 +1,136 @@ +package com.mussonindustrial.embr.snmp.agents.opc + +import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice +import com.mussonindustrial.embr.snmp.agents.opc.types.OidValueType +import com.mussonindustrial.embr.snmp.model.Snmp4jOid +import com.mussonindustrial.embr.snmp.opc.DeviceContextManagedAddressSpaceFragment +import com.mussonindustrial.embr.snmp.utils.removeAllNodes +import org.eclipse.milo.opcua.sdk.core.Reference +import org.eclipse.milo.opcua.sdk.core.ValueRank +import org.eclipse.milo.opcua.sdk.server.AddressSpaceComposite +import org.eclipse.milo.opcua.sdk.server.Lifecycle +import org.eclipse.milo.opcua.sdk.server.methods.MethodInvocationHandler +import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode +import org.eclipse.milo.opcua.sdk.server.nodes.UaMethodNode +import org.eclipse.milo.opcua.stack.core.NodeIds +import org.eclipse.milo.opcua.stack.core.types.builtin.* +import org.eclipse.milo.opcua.stack.core.types.structured.Argument +import org.eclipse.milo.opcua.stack.core.types.structured.CallMethodResult + +class MethodAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceComposite) : + DeviceContextManagedAddressSpaceFragment(device.context.deviceContext, composite) { + + val root = "Methods" + + init { + lifecycleManager.addLifecycle( + object : Lifecycle { + override fun startup() { + addNodes() + } + + override fun shutdown() { + nodeManager.removeAllNodes() + } + } + ) + } + + fun addNodes() { + val folder = + UaFolderNode( + nodeContext, + nodeId(root), + qualifiedName(root), + LocalizedText.english(root), + ) + nodeManager.addNode(folder) + + folder.addReference( + Reference( + folder.nodeId, + NodeIds.Organizes, + deviceNodeId.expanded(), + Reference.Direction.INVERSE, + ) + ) + + addMethodNode( + folder.nodeId, + "Walk", + arrayOf( + Argument( + "Root", + NodeIds.String, + ValueRank.ScalarOrOneDimension.value, + null, + LocalizedText.english("Root OID to walk."), + ) + ), + arrayOf( + Argument( + "Result", + OidValueType.TYPE_ID.toNodeId(server.namespaceTable).get(), + ValueRank.ScalarOrOneDimension.value, + null, + LocalizedText.english("Result string will go here."), + ) + ), + ) { _, request -> + val root = + request.inputArguments?.first()?.value as String? + ?: return@addMethodNode CallMethodResult( + StatusCode.BAD, + arrayOfNulls(0), + arrayOfNulls(0), + arrayOf(), + ) + + val results = + device.walk(listOf(Snmp4jOid(root))).map { + ExtensionObject.encode( + server.staticEncodingContext, + OidValueType(it.oid.numeric, it.value.value.value), + ) + } + + val output = Variant(results.toTypedArray()) + + CallMethodResult( + StatusCode.GOOD, + arrayOfNulls(0), + arrayOfNulls(0), + arrayOf(output), + ) + } + } + + fun addMethodNode( + parent: NodeId, + name: String, + inputs: Array, + outputs: Array, + handler: MethodInvocationHandler, + ) { + UaMethodNode.UaMethodNodeBuilder(nodeContext) + .run { + setNodeId(nodeId(name)) + setBrowseName(qualifiedName(name)) + setDisplayName(LocalizedText.english(name)) + addReference( + Reference( + nodeId, + NodeIds.HasComponent, + parent.expanded(), + Reference.Direction.INVERSE, + ) + ) + buildAndAdd() + } + .run { + inputArguments = inputs + outputArguments = outputs + invocationHandler = handler + } + } +} diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/BrowsableObjectModelAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/ObjectModelAddressSpace.kt similarity index 75% rename from modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/BrowsableObjectModelAddressSpace.kt rename to modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/ObjectModelAddressSpace.kt index 5560ced0..49257248 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/BrowsableObjectModelAddressSpace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/ObjectModelAddressSpace.kt @@ -11,10 +11,8 @@ import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId import org.eclipse.milo.opcua.stack.core.types.structured.ViewDescription -class BrowsableObjectModelAddressSpace( - val device: SnmpAgentDevice, - composite: AddressSpaceComposite, -) : DeviceContextManagedAddressSpaceFragment(device.context.deviceContext, composite) { +class ObjectModelAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceComposite) : + DeviceContextManagedAddressSpaceFragment(device.context.deviceContext, composite) { private val root = "Objects" private val model = device.model @@ -34,26 +32,25 @@ class BrowsableObjectModelAddressSpace( } fun addNodes() { - val objectsFolder = + val folder = UaFolderNode( nodeContext, nodeId(root), qualifiedName(root), LocalizedText.english(root), ) - nodeManager.addNode(objectsFolder) + nodeManager.addNode(folder) - objectsFolder.addReference( + folder.addReference( Reference( - objectsFolder.nodeId, + folder.nodeId, NodeIds.Organizes, deviceNodeId.expanded(), Reference.Direction.INVERSE, ) ) - addObjectsFolder(objectsFolder, "Numeric") - addObjectsFolder(objectsFolder, "Symbolic") + addObjectsFolder(folder, "Numeric") } fun addObjectsFolder(folder: UaFolderNode, name: String) { @@ -96,17 +93,6 @@ class BrowsableObjectModelAddressSpace( ) ) } - nodeId("Objects/Symbolic") -> - model.getDescriptors(model.oids).forEach { descriptor -> - references.add( - Reference( - nodeId, - NodeIds.Organizes, - nodeId(descriptor.oid.symbolicName).expanded(), - Reference.Direction.FORWARD, - ) - ) - } } AddressSpace.ReferenceResult.of(references) diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt index 0d5730e7..739c6401 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt @@ -5,7 +5,9 @@ import com.mussonindustrial.embr.snmp.agents.model.ObjectModel import com.mussonindustrial.embr.snmp.model.OidValue import com.mussonindustrial.embr.snmp.model.Snmp4jOid import com.mussonindustrial.embr.snmp.opc.DeviceContextManagedAddressSpaceFragment -import com.mussonindustrial.embr.snmp.utils.isOid +import com.mussonindustrial.embr.snmp.utils.OidPath +import com.mussonindustrial.embr.snmp.utils.OidSuffix +import com.mussonindustrial.embr.snmp.utils.parseOidPath import kotlin.jvm.optionals.getOrNull import org.eclipse.milo.opcua.sdk.core.AccessLevel import org.eclipse.milo.opcua.sdk.core.Reference @@ -26,8 +28,7 @@ import org.eclipse.milo.opcua.stack.core.types.structured.WriteValue class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceComposite) : DeviceContextManagedAddressSpaceFragment(device.context.deviceContext, composite) { - private val filter = SimpleAddressSpaceFilter.create { it.getPath().isOid() } - private val model = device.model + private val filter = SimpleAddressSpaceFilter.create { it.getPath().parseOidPath() != null } override fun read( context: AddressSpace.ReadContext, @@ -37,8 +38,9 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos ): List { val requests = readValueIds.map { ReadRequest(it) } + val directReads = requests.filter { it.path.suffix == null } val valueReads = - requests.filter { + directReads.filter { AttributeId.from(it.readValueId.attributeId).get() == AttributeId.Value } readValueAttributes(valueReads).zip(valueReads).forEach { (value, result) -> @@ -46,23 +48,27 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos } val nonValueReads = - requests.filter { + directReads.filter { AttributeId.from(it.readValueId.attributeId).get() != AttributeId.Value } readNonValueAttributes(nonValueReads).zip(nonValueReads).forEach { (value, result) -> result.value = value } + val suffixReads = requests.filter { it.path.suffix != null } + readSuffixAttributes(suffixReads).zip(suffixReads).forEach { (value, result) -> + result.value = value + } + return requests.map { it.value ?: DataValue(Variant.of(null)) } } fun readValueAttributes(requests: List): List { - return model.read(requests.map { it.oid }).map { it.value } + return device.read(requests.map { it.oid }).map { it.value } } fun readNonValueAttributes(requests: List): List { - val descriptors = model.getDescriptors(requests.map { it.oid }) - + val descriptors = device.model.getDescriptors(requests.map { it.oid }) return requests.zip(descriptors).map { (request, descriptor) -> val nodeId = request.readValueId.nodeId val attributeId = AttributeId.from(request.readValueId.attributeId).getOrNull() @@ -84,20 +90,20 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos ): Any? { return when (attributeId) { AttributeId.NodeId -> nodeId - AttributeId.NodeClass -> NodeClass.Variable - AttributeId.BrowseName -> device.context.deviceContext.qualifiedName(nodeId.getPath()) - + AttributeId.BrowseName -> qualifiedName(descriptor.oid.numeric) AttributeId.DisplayName -> LocalizedText.english(descriptor.oid.numeric) - AttributeId.Description -> LocalizedText.english(descriptor.oid.numeric) + AttributeId.Description -> LocalizedText.english("") AttributeId.WriteMask, AttributeId.UserWriteMask -> UInteger.valueOf(0) + AttributeId.EventNotifier -> null + AttributeId.DataType -> when (descriptor) { - is ObjectModel.ValueDescriptor -> descriptor.snmpType.uaDataType + is ObjectModel.ValueDescriptor -> descriptor.snmpDataType.uaDataType else -> OpcUaDataType.String.nodeId } AttributeId.ValueRank -> @@ -124,6 +130,57 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos } } + fun readSuffixAttributes(requests: List): List { + val descriptors = device.model.getDescriptors(requests.map { it.oid }) + return requests.zip(descriptors).map { (request, descriptor) -> + val nodeId = request.readValueId.nodeId + val attributeId = AttributeId.from(request.readValueId.attributeId).getOrNull() + val path = request.path + + attributeId + .runCatching { resolveSuffixAttribute(attributeId, nodeId, descriptor, path) } + .fold( + onSuccess = { DataValue(Variant(it)) }, + onFailure = { DataValue((it as UaException).statusCode) }, + ) + } + } + + fun resolveSuffixAttribute( + attributeId: AttributeId?, + nodeId: NodeId, + descriptor: ObjectModel.Descriptor, + path: OidPath, + ): Any? { + val suffix = + OidSuffix.from(path.suffix!!) ?: throw UaException(StatusCodes.Bad_NodeIdUnknown) + val context = OidSuffix.Context(device, descriptor) + + return when (attributeId) { + AttributeId.NodeId -> nodeId + AttributeId.NodeClass -> NodeClass.Variable + AttributeId.BrowseName -> suffix.browseName(context) + AttributeId.DisplayName -> suffix.displayName(context) + AttributeId.Description -> suffix.description(context) + + AttributeId.DataType -> suffix.dataType(context) + AttributeId.ValueRank -> suffix.valueRank(context) + AttributeId.ArrayDimensions -> null + + AttributeId.AccessLevel, + AttributeId.UserAccessLevel -> suffix.accessLevel(context) + + AttributeId.Historizing -> false + AttributeId.Value -> suffix.value(context) + + AttributeId.WriteMask, + AttributeId.UserWriteMask -> UInteger.valueOf(0) + + else -> + throw UaException(StatusCodes.Bad_AttributeIdInvalid, "attributeId: $attributeId") + } + } + override fun write( context: AddressSpace.WriteContext, writeValues: List, @@ -143,7 +200,7 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos } val valueWrites = results.filter { it.value == null } - model + device .write(valueWrites.map { it.oid to it.writeValue.value.value.value }) .zip(valueWrites) .forEach { (value, result) -> result.value = value.value } @@ -157,17 +214,56 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos nodeIds: List, ): List { return nodeIds.map { nodeId -> - AddressSpace.ReferenceResult.of( - listOf( - Reference( - nodeId, - NodeIds.Organizes, - nodeId("Objects/Numeric").expanded(), - Reference.Direction.INVERSE, - ) - ) + val references = mutableListOf() + val oidPath = nodeId.getPath().parseOidPath()!! + references += + if (oidPath.suffix == null) { + browseDirect(oidPath, nodeId) + } else { + browseSuffix(oidPath, nodeId) + } + AddressSpace.ReferenceResult.of(references) + } + } + + fun browseDirect(oidPath: OidPath, nodeId: NodeId): List { + val references = mutableListOf() + references += + Reference( + nodeId, + NodeIds.Organizes, + nodeId("Objects/Numeric").expanded(), + Reference.Direction.INVERSE, ) + OidSuffix.ALL.forEach { suffix -> + references += + Reference( + nodeId, + NodeIds.HasProperty, + nodeId("${oidPath.oid}::${suffix.name}").expanded(), + Reference.Direction.FORWARD, + ) } + return references + } + + fun browseSuffix(oidPath: OidPath, nodeId: NodeId): List { + val references = mutableListOf() + references += + Reference( + nodeId, + NodeIds.HasTypeDefinition, + NodeIds.PropertyType.expanded(), + Reference.Direction.FORWARD, + ) + references += + Reference( + nodeId, + NodeIds.HasProperty, + nodeId(oidPath.oid).expanded(), + Reference.Direction.INVERSE, + ) + return references } override fun getFilter(): AddressSpaceFilter { @@ -179,7 +275,8 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos } inner class ReadRequest(val readValueId: ReadValueId) : OidValue { - override val oid = Snmp4jOid(readValueId.nodeId.getPath()) + val path = readValueId.nodeId.getPath().parseOidPath()!! + override val oid = Snmp4jOid(path.oid) override var value: DataValue? = null } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/types/OidValueType.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/types/OidValueType.kt new file mode 100644 index 00000000..4e250f88 --- /dev/null +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/types/OidValueType.kt @@ -0,0 +1,165 @@ +package com.mussonindustrial.embr.snmp.agents.opc.types + +import com.google.common.base.MoreObjects +import com.google.common.base.Objects +import com.mussonindustrial.embr.snmp.opc.SnmpNamespace +import org.eclipse.milo.opcua.sdk.core.Reference +import org.eclipse.milo.opcua.sdk.core.ValueRanks +import org.eclipse.milo.opcua.sdk.server.ManagedNamespace +import org.eclipse.milo.opcua.sdk.server.nodes.UaDataTypeNode +import org.eclipse.milo.opcua.stack.core.NodeIds +import org.eclipse.milo.opcua.stack.core.UaSerializationException +import org.eclipse.milo.opcua.stack.core.encoding.EncodingContext +import org.eclipse.milo.opcua.stack.core.encoding.GenericDataTypeCodec +import org.eclipse.milo.opcua.stack.core.encoding.UaDecoder +import org.eclipse.milo.opcua.stack.core.encoding.UaEncoder +import org.eclipse.milo.opcua.stack.core.types.UaStructuredType +import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId +import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText +import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId +import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName +import org.eclipse.milo.opcua.stack.core.types.builtin.Variant +import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned +import org.eclipse.milo.opcua.stack.core.types.enumerated.StructureType +import org.eclipse.milo.opcua.stack.core.types.structured.StructureDefinition +import org.eclipse.milo.opcua.stack.core.types.structured.StructureField + +class OidValueType(val oid: String, val value: Variant) : UaStructuredType { + + constructor(oid: String, value: Any?) : this(oid, Variant(value)) + + override fun getTypeId(): ExpandedNodeId { + return TYPE_ID + } + + override fun getBinaryEncodingId(): ExpandedNodeId { + return BINARY_ENCODING_ID + } + + override fun getXmlEncodingId(): ExpandedNodeId { + return ExpandedNodeId.NULL_VALUE + } + + override fun getJsonEncodingId(): ExpandedNodeId { + return ExpandedNodeId.NULL_VALUE + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + val that = other as OidValueType + return Objects.equal(oid, that.oid) && Objects.equal(value, that.value) + } + + override fun hashCode(): Int { + return Objects.hashCode(oid, value) + } + + override fun toString(): String { + return MoreObjects.toStringHelper(this).add("oid", oid).add("value", value).toString() + } + + class Codec : GenericDataTypeCodec() { + override fun getType(): Class { + return OidValueType::class.java + } + + @Throws(UaSerializationException::class) + override fun decodeType(context: EncodingContext, decoder: UaDecoder): OidValueType { + val oid = decoder.decodeString("Oid") + val value = decoder.decodeVariant("Value") + + return OidValueType(oid, value) + } + + @Throws(UaSerializationException::class) + override fun encodeType(context: EncodingContext, encoder: UaEncoder, value: OidValueType) { + encoder.encodeString("Oid", value.oid) + encoder.encodeVariant("Value", value.value) + } + } + + companion object { + val TYPE_ID: ExpandedNodeId = + ExpandedNodeId.parse( + String.format( + "nsu=%s;s=%s", + SnmpNamespace.NAMESPACE_URI, + "DataType.OidValueStructType", + ) + ) + + val BINARY_ENCODING_ID: ExpandedNodeId = + ExpandedNodeId.parse( + String.format( + "nsu=%s;s=%s", + SnmpNamespace.NAMESPACE_URI, + "DataType.OidValueStructType.BinaryEncoding", + ) + ) + + fun register(namespace: ManagedNamespace) { + val dataTypeId: NodeId = TYPE_ID.toNodeIdOrThrow(namespace.nodeContext.namespaceTable) + val binaryEncodingId: NodeId = + BINARY_ENCODING_ID.toNodeIdOrThrow(namespace.nodeContext.namespaceTable) + + val dataTypeNode = + UaDataTypeNode( + namespace.nodeContext, + dataTypeId, + QualifiedName(namespace.namespaceIndex, "OidValueStructType"), + LocalizedText.english("OidValueStructType"), + LocalizedText.NULL_VALUE, + Unsigned.uint(0), + Unsigned.uint(0), + false, + ) + .apply { + addReference( + Reference( + dataTypeId, + NodeIds.HasSubtype, + NodeIds.Structure.expanded(), + Reference.Direction.INVERSE, + ) + ) + dataTypeDefinition = + StructureDefinition( + binaryEncodingId, + NodeIds.Structure, + StructureType.Structure, + arrayOf( + StructureField( + "oid", + LocalizedText.NULL_VALUE, + NodeIds.String, + ValueRanks.Scalar, + null, + namespace.nodeContext.server.config.limits.maxStringLength, + false, + ), + StructureField( + "value", + LocalizedText.NULL_VALUE, + NodeIds.BaseDataType, + ValueRanks.Scalar, + null, + Unsigned.uint(0), + false, + ), + ), + ) + } + + namespace.nodeManager.addNode(dataTypeNode) + + namespace.nodeContext.server.staticDataTypeManager.registerType( + dataTypeId, + Codec(), + binaryEncodingId, + null, + null, + ) + } + } +} diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/rpc/SnmpAgentRpcImpl.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/rpc/SnmpAgentRpcImpl.kt index cc7c443d..cc29d640 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/rpc/SnmpAgentRpcImpl.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/rpc/SnmpAgentRpcImpl.kt @@ -24,7 +24,7 @@ class SnmpAgentRpcImpl(val context: SnmpGatewayContext) : SnmpAgentRpc { @RpcDelegate.RequiredMutabilityMode(value = MutabilityMode.READ_ONLY) override fun read(agent: String, oids: List): List { val snmpAgent = requireAgent(agent) - return snmpAgent.model.read(oids.map { Snmp4jOid(it) }).map { it.toQualifiedValue() } + return snmpAgent.read(oids.map { Snmp4jOid(it) }).map { it.toQualifiedValue() } } @RpcDelegate.RequiredMutabilityMode(value = MutabilityMode.READ_WRITE) @@ -35,7 +35,7 @@ class SnmpAgentRpcImpl(val context: SnmpGatewayContext) : SnmpAgentRpc { throw ValueError("Length of values does not match length of OIDs.") } - return snmpAgent.model + return snmpAgent .write(oids.zip(values).map { (oid, value) -> Snmp4jOid(oid) to value }) .map { it.value.toQualityCode() } } @@ -43,7 +43,7 @@ class SnmpAgentRpcImpl(val context: SnmpGatewayContext) : SnmpAgentRpc { @RpcDelegate.RequiredMutabilityMode(value = MutabilityMode.READ_ONLY) override fun walk(agent: String, oids: List): List { val snmpAgent = requireAgent(agent) - return snmpAgent.model.walk(oids.map { Snmp4jOid(it) }).map { it.toQualifiedValue() } + return snmpAgent.walk(oids.map { Snmp4jOid(it) }).map { it.toQualifiedValue() } } @RpcDelegate.RequiredMutabilityMode(value = MutabilityMode.READ_ONLY) @@ -54,7 +54,7 @@ class SnmpAgentRpcImpl(val context: SnmpGatewayContext) : SnmpAgentRpc { upperBoundIndex: String?, ): List> { val snmpAgent = requireAgent(agent) - return snmpAgent.model + return snmpAgent .readTable( columns.map { Snmp4jOid(it) }, lowerBoundIndex.nullOrOid(), diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNamespace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNamespace.kt new file mode 100644 index 00000000..60a7a919 --- /dev/null +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNamespace.kt @@ -0,0 +1,52 @@ +package com.mussonindustrial.embr.snmp.opc + +import com.mussonindustrial.embr.snmp.agents.opc.types.OidValueType +import org.eclipse.milo.opcua.sdk.server.ManagedNamespaceWithLifecycle +import org.eclipse.milo.opcua.sdk.server.OpcUaServer +import org.eclipse.milo.opcua.sdk.server.items.DataItem +import org.eclipse.milo.opcua.sdk.server.items.MonitoredItem +import org.eclipse.milo.opcua.sdk.server.util.SubscriptionModel +import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId +import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId + +class SnmpNamespace(server: OpcUaServer) : ManagedNamespaceWithLifecycle(server, NAMESPACE_URI) { + + companion object { + const val NAMESPACE_URI = "urn:mussonindustrial:embr:snmp" + lateinit var instance: SnmpNamespace + } + + private val subscriptionModel = SubscriptionModel(server, this) + + init { + instance = this + lifecycleManager.addLifecycle(subscriptionModel) + lifecycleManager.addStartupTask { OidValueType.register(this) } + } + + fun nodeId(id: Any): NodeId { + return ExpandedNodeId( + ExpandedNodeId.ServerReference.ServerIndex.LOCAL, + ExpandedNodeId.NamespaceReference.NamespaceUri(NAMESPACE_URI), + id, + ) + .toNodeId(server.namespaceTable) + .get() + } + + override fun onDataItemsCreated(items: List) { + subscriptionModel.onDataItemsCreated(items) + } + + override fun onDataItemsModified(items: List) { + subscriptionModel.onDataItemsModified(items) + } + + override fun onDataItemsDeleted(items: List) { + subscriptionModel.onDataItemsDeleted(items) + } + + override fun onMonitoringModeChanged(items: List) { + subscriptionModel.onMonitoringModeChanged(items) + } +} diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/typing/SnmpType.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/typing/SnmpDataType.kt similarity index 69% rename from modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/typing/SnmpType.kt rename to modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/typing/SnmpDataType.kt index 2fdd2f08..5f86be2d 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/typing/SnmpType.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/typing/SnmpDataType.kt @@ -14,7 +14,7 @@ import org.snmp4j.smi.OctetString import org.snmp4j.smi.UnsignedInteger32 import org.snmp4j.smi.Variable -enum class SnmpType(val uaDataType: NodeId, val variableOfType: (Any?) -> Variable) { +enum class SnmpDataType(val uaDataType: NodeId, val variableOfType: (Any?) -> Variable) { Int32(OpcUaDataType.Int32.nodeId, { Integer32(TypeUtilities.toInteger(it)) }), UInt32(OpcUaDataType.UInt32.nodeId, { UnsignedInteger32(TypeUtilities.toLong(it)) }), Gauge32(OpcUaDataType.UInt32.nodeId, { Gauge32(TypeUtilities.toLong(it)) }), @@ -25,16 +25,16 @@ enum class SnmpType(val uaDataType: NodeId, val variableOfType: (Any?) -> Variab IpAddress(OpcUaDataType.String.nodeId, { IpAddress(TypeUtilities.toString(it)) }), } -val Variable.snmpType: SnmpType +val Variable.snmpDataType: SnmpDataType get() = when (this) { - is OID -> SnmpType.Oid - is Gauge32 -> SnmpType.Gauge32 - is Counter32 -> SnmpType.Counter32 - is Counter64 -> SnmpType.Counter64 - is Integer32 -> SnmpType.Int32 - is UnsignedInteger32 -> SnmpType.UInt32 - is Address -> SnmpType.IpAddress - is OctetString -> SnmpType.OctetString - else -> SnmpType.OctetString + is OID -> SnmpDataType.Oid + is Gauge32 -> SnmpDataType.Gauge32 + is Counter32 -> SnmpDataType.Counter32 + is Counter64 -> SnmpDataType.Counter64 + is Integer32 -> SnmpDataType.Int32 + is UnsignedInteger32 -> SnmpDataType.UInt32 + is Address -> SnmpDataType.IpAddress + is OctetString -> SnmpDataType.OctetString + else -> SnmpDataType.OctetString } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/OidPath.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/OidPath.kt new file mode 100644 index 00000000..6423139a --- /dev/null +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/OidPath.kt @@ -0,0 +1,10 @@ +package com.mussonindustrial.embr.snmp.utils + +data class OidPath(val oid: String, val suffix: String? = null) + +fun String.parseOidPath(): OidPath? { + val parts = this.split("::", limit = 2) + val oid = parts[0] + val suffix = parts.getOrNull(1)?.takeIf { it.isNotBlank() } + return OidPath(oid, suffix) +} diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/OidSuffix.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/OidSuffix.kt new file mode 100644 index 00000000..42abcef6 --- /dev/null +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/OidSuffix.kt @@ -0,0 +1,46 @@ +package com.mussonindustrial.embr.snmp.utils + +import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice +import com.mussonindustrial.embr.snmp.agents.model.ObjectModel +import org.eclipse.milo.opcua.sdk.core.AccessLevel +import org.eclipse.milo.opcua.sdk.core.ValueRank +import org.eclipse.milo.opcua.stack.core.OpcUaDataType +import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText +import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId +import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName +import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UByte + +sealed class OidSuffix(val name: String) { + + data class Context(val device: SnmpAgentDevice, val descriptor: ObjectModel.Descriptor) + + open fun browseName(context: Context): QualifiedName = QualifiedName(0, name) + + open fun displayName(context: Context): LocalizedText = LocalizedText.english(name) + + open fun description(context: Context): LocalizedText = LocalizedText.english("") + + open fun accessLevel(context: Context): UByte? = AccessLevel.toValue(AccessLevel.READ_ONLY) + + abstract fun dataType(context: Context): NodeId + + abstract fun valueRank(context: Context): Int + + abstract fun value(context: Context): Any? + + object DataType : OidSuffix("DataType") { + override fun dataType(context: Context): NodeId = OpcUaDataType.String.nodeId + + override fun valueRank(context: Context) = ValueRank.Scalar.value + + override fun value(context: Context) = + (context.descriptor as? ObjectModel.ValueDescriptor)?.snmpDataType?.name + } + + companion object { + val ALL = listOf(DataType) + private val byName = ALL.associateBy { it.name } + + fun from(name: String): OidSuffix? = byName[name] + } +} diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/SnmpUtils.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/SnmpUtils.kt index 1af64465..2abe0062 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/SnmpUtils.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/SnmpUtils.kt @@ -36,7 +36,7 @@ fun Target.createSizeBoundedPDUs( pdu.add(binding) count++ - if (pdu.berLength > maxSizeRequestPDU || count > 100) { + if (pdu.berLength > maxSizeRequestPDU || count > 50) { pdu.trim() pdus.add(pdu) pdu = pduFactory.createPDU(this).apply { configure(this) } From 9414f623fb7763b65a156781f05cc57bc8590988 Mon Sep 17 00:00:00 2001 From: Ben Musson Date: Tue, 3 Feb 2026 17:58:20 -0500 Subject: [PATCH 4/9] opc ua namespace rework --- .../embr/snmp/model/ExtendedOid.kt | 37 +++++ .../embr/snmp/model/Snmp4jExtendedOid.kt | 7 + .../embr/snmp/model/Snmp4jOid.kt | 2 +- .../snmp/agents/devices/SnmpAgentDevice.kt | 2 +- .../agents/devices/SnmpAgentDeviceImpl.kt | 4 +- .../snmp/agents/opc/DeviceAddressSpace.kt | 53 +++++++ .../snmp/agents/opc/DiagnosticAddressSpace.kt | 34 ++--- .../snmp/agents/opc/MethodAddressSpace.kt | 137 +++--------------- .../agents/opc/ObjectModelAddressSpace.kt | 27 +--- .../embr/snmp/agents/opc/OidAddressSpace.kt | 60 ++++---- .../opc/nodes/DynamicObjectSuffixNode.kt} | 10 +- .../agents/opc/nodes/ReadTableMethodNode.kt | 120 +++++++++++++++ .../snmp/agents/opc/nodes/WalkMethodNode.kt | 83 +++++++++++ .../embr/snmp/agents/rpc/SnmpAgentRpcImpl.kt | 13 +- .../model/ConcurrentObjectModel.kt | 4 +- .../snmp/{agents => }/model/ObjectModel.kt | 6 +- .../embr/snmp/opc/DeviceAddressSpace.kt | 48 ------ .../embr/snmp/opc/SnmpNamespace.kt | 8 +- .../{agents => }/opc/types/OidValueType.kt | 10 +- .../snmp/opc/types/SnmpAgentDeviceType.kt | 42 ++++++ .../embr/snmp/utils/OidPath.kt | 10 -- .../embr/snmp/utils/SnmpUtils.kt | 11 -- 22 files changed, 441 insertions(+), 287 deletions(-) create mode 100644 modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ExtendedOid.kt create mode 100644 modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/Snmp4jExtendedOid.kt create mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DeviceAddressSpace.kt rename modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/{utils/OidSuffix.kt => agents/opc/nodes/DynamicObjectSuffixNode.kt} (83%) create mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/ReadTableMethodNode.kt create mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/WalkMethodNode.kt rename modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/{agents => }/model/ConcurrentObjectModel.kt (87%) rename modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/{agents => }/model/ObjectModel.kt (91%) delete mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/DeviceAddressSpace.kt rename modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/{agents => }/opc/types/OidValueType.kt (96%) create mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/SnmpAgentDeviceType.kt delete mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/OidPath.kt diff --git a/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ExtendedOid.kt b/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ExtendedOid.kt new file mode 100644 index 00000000..a38511bc --- /dev/null +++ b/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ExtendedOid.kt @@ -0,0 +1,37 @@ +package com.mussonindustrial.embr.snmp.model + +import java.text.ParseException +import org.snmp4j.SNMP4JSettings +import org.snmp4j.smi.OID + +interface ExtendedOid : Oid { + val suffix: String? + + val hasSuffix: Boolean + get() = suffix != null +} + +fun String.isOid(): Boolean { + return try { + this.asExtendedOid() + true + } catch (_: ParseException) { + false + } +} + +fun String.asExtendedOid(): ExtendedOid { + val parts = this.split("::", limit = 2) + val maybeOid = parts[0] + val suffix = parts.getOrNull(1)?.takeIf { it.isNotBlank() } + + val oid = SNMP4JSettings.getOIDTextFormat().parse(maybeOid) + return Snmp4jExtendedOid(OID(oid), suffix) +} + +fun String?.nullOrExtendedOid(): ExtendedOid? { + this?.let { + return this.asExtendedOid() + } + return null +} diff --git a/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/Snmp4jExtendedOid.kt b/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/Snmp4jExtendedOid.kt new file mode 100644 index 00000000..fae90fbe --- /dev/null +++ b/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/Snmp4jExtendedOid.kt @@ -0,0 +1,7 @@ +package com.mussonindustrial.embr.snmp.model + +import org.snmp4j.smi.OID + +class Snmp4jExtendedOid(oid: OID, override val suffix: String?) : Snmp4jOid(oid), ExtendedOid { + constructor(numeric: String, suffix: String) : this(OID(numeric), suffix) +} diff --git a/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/Snmp4jOid.kt b/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/Snmp4jOid.kt index 31232751..996ba149 100644 --- a/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/Snmp4jOid.kt +++ b/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/Snmp4jOid.kt @@ -2,7 +2,7 @@ package com.mussonindustrial.embr.snmp.model import org.snmp4j.smi.OID -class Snmp4jOid(val oid: OID) : OID(oid), Oid { +open class Snmp4jOid(val oid: OID) : OID(oid), Oid { constructor(numeric: String) : this(OID(numeric)) diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDevice.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDevice.kt index bcd6312c..45d2416b 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDevice.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDevice.kt @@ -2,7 +2,7 @@ package com.mussonindustrial.embr.snmp.agents.devices import com.inductiveautomation.ignition.gateway.opcua.server.api.Device import com.mussonindustrial.embr.snmp.agents.context.SnmpAgentContext -import com.mussonindustrial.embr.snmp.agents.model.ObjectModel +import com.mussonindustrial.embr.snmp.model.ObjectModel import com.mussonindustrial.embr.snmp.model.Oid import com.mussonindustrial.embr.snmp.model.OidValue import org.eclipse.milo.opcua.sdk.server.AddressSpaceFragment diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDeviceImpl.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDeviceImpl.kt index bf645997..122a701e 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDeviceImpl.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDeviceImpl.kt @@ -4,19 +4,19 @@ import com.inductiveautomation.ignition.common.util.LoggerEx import com.mussonindustrial.embr.snmp.SnmpGatewayContext import com.mussonindustrial.embr.snmp.agents.configuration.SnmpAgentConfig import com.mussonindustrial.embr.snmp.agents.context.SnmpAgentContext -import com.mussonindustrial.embr.snmp.agents.model.ConcurrentObjectModel +import com.mussonindustrial.embr.snmp.agents.opc.DeviceAddressSpace import com.mussonindustrial.embr.snmp.agents.opc.DiagnosticAddressSpace import com.mussonindustrial.embr.snmp.agents.opc.MethodAddressSpace import com.mussonindustrial.embr.snmp.agents.opc.ObjectModelAddressSpace import com.mussonindustrial.embr.snmp.agents.opc.OidAddressSpace import com.mussonindustrial.embr.snmp.model.BasicOidValue +import com.mussonindustrial.embr.snmp.model.ConcurrentObjectModel import com.mussonindustrial.embr.snmp.model.Oid import com.mussonindustrial.embr.snmp.model.OidValue import com.mussonindustrial.embr.snmp.model.Snmp4jOid import com.mussonindustrial.embr.snmp.model.SnmpCommunicationError import com.mussonindustrial.embr.snmp.model.toOid import com.mussonindustrial.embr.snmp.model.toSnmp4j -import com.mussonindustrial.embr.snmp.opc.DeviceAddressSpace import com.mussonindustrial.embr.snmp.utils.createSizeBoundedPDUs import java.util.concurrent.TimeUnit import kotlin.collections.map diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DeviceAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DeviceAddressSpace.kt new file mode 100644 index 00000000..042e4871 --- /dev/null +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DeviceAddressSpace.kt @@ -0,0 +1,53 @@ +package com.mussonindustrial.embr.snmp.agents.opc + +import com.inductiveautomation.ignition.gateway.opcua.server.api.DeviceContext +import com.mussonindustrial.embr.snmp.opc.DeviceContextManagedAddressSpaceFragment +import com.mussonindustrial.embr.snmp.opc.SnmpNamespace +import com.mussonindustrial.embr.snmp.opc.types.SnmpAgentDeviceType +import com.mussonindustrial.embr.snmp.utils.removeAllNodes +import org.eclipse.milo.opcua.sdk.core.Reference +import org.eclipse.milo.opcua.sdk.server.AddressSpaceComposite +import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectNode +import org.eclipse.milo.opcua.stack.core.NodeIds +import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText +import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger + +class DeviceAddressSpace(deviceContext: DeviceContext, composite: AddressSpaceComposite) : + DeviceContextManagedAddressSpaceFragment(deviceContext, composite) { + + init { + lifecycleManager.addStartupTask { addNodes() } + lifecycleManager.addShutdownTask { nodeManager.removeAllNodes() } + } + + fun addNodes() { + UaObjectNode( + nodeContext, + deviceNodeId, + qualifiedName("[${name}]"), + LocalizedText("[${name}]"), + LocalizedText("SNMP Agent Device"), + UInteger.MIN, + UInteger.MIN, + ) + .apply { + nodeManager.addNode(this) + addReference( + Reference( + this.nodeId, + NodeIds.HasTypeDefinition, + SnmpAgentDeviceType.nodeId(SnmpNamespace.instance).expanded(), + Reference.Direction.FORWARD, + ) + ) + addReference( + Reference( + this.nodeId, + NodeIds.Organizes, + rootNodeId.expanded(), + Reference.Direction.INVERSE, + ) + ) + } + } +} diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DiagnosticAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DiagnosticAddressSpace.kt index 17e9294a..a1874638 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DiagnosticAddressSpace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DiagnosticAddressSpace.kt @@ -5,8 +5,8 @@ import com.mussonindustrial.embr.snmp.opc.DeviceContextManagedAddressSpaceFragme import com.mussonindustrial.embr.snmp.utils.removeAllNodes import org.eclipse.milo.opcua.sdk.core.Reference import org.eclipse.milo.opcua.sdk.server.AddressSpaceComposite -import org.eclipse.milo.opcua.sdk.server.Lifecycle -import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode +import org.eclipse.milo.opcua.sdk.server.nodes.UaNode +import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectNode import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilter import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilters @@ -15,6 +15,7 @@ import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId import org.eclipse.milo.opcua.stack.core.types.builtin.Variant +import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger class DiagnosticAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceComposite) : DeviceContextManagedAddressSpaceFragment(device.context.deviceContext, composite) { @@ -22,33 +23,27 @@ class DiagnosticAddressSpace(val device: SnmpAgentDevice, composite: AddressSpac private val root = "Diagnostics" init { - lifecycleManager.addLifecycle( - object : Lifecycle { - override fun startup() { - addNodes() - } - - override fun shutdown() { - nodeManager.removeAllNodes() - } - } - ) + lifecycleManager.addStartupTask { addNodes() } + lifecycleManager.addShutdownTask { nodeManager.removeAllNodes() } } fun addNodes() { val folder = - UaFolderNode( + UaObjectNode( nodeContext, nodeId(root), qualifiedName(root), LocalizedText.english(root), + LocalizedText.NULL_VALUE, + UInteger.MIN, + UInteger.MIN, ) - nodeManager.addNode(folder) + nodeManager.addNode(folder) folder.addReference( Reference( folder.nodeId, - NodeIds.Organizes, + NodeIds.HasComponent, deviceNodeId.expanded(), Reference.Direction.INVERSE, ) @@ -115,7 +110,7 @@ class DiagnosticAddressSpace(val device: SnmpAgentDevice, composite: AddressSpac } fun addDiagnosticNode( - folder: UaFolderNode, + parent: UaNode, name: String, dataType: NodeId, attributeFilter: AttributeFilter, @@ -125,11 +120,12 @@ class DiagnosticAddressSpace(val device: SnmpAgentDevice, composite: AddressSpac setBrowseName(qualifiedName(name)) setDisplayName(LocalizedText.english(name)) setDataType(dataType) + setTypeDefinition(NodeIds.PropertyType) addReference( Reference( nodeId, - NodeIds.HasComponent, - folder.nodeId.expanded(), + NodeIds.HasProperty, + parent.nodeId.expanded(), Reference.Direction.INVERSE, ) ) diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/MethodAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/MethodAddressSpace.kt index 09aca0d0..c03527f4 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/MethodAddressSpace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/MethodAddressSpace.kt @@ -1,136 +1,45 @@ package com.mussonindustrial.embr.snmp.agents.opc import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice -import com.mussonindustrial.embr.snmp.agents.opc.types.OidValueType -import com.mussonindustrial.embr.snmp.model.Snmp4jOid +import com.mussonindustrial.embr.snmp.agents.opc.nodes.ReadTableMethodNode +import com.mussonindustrial.embr.snmp.agents.opc.nodes.WalkMethodNode import com.mussonindustrial.embr.snmp.opc.DeviceContextManagedAddressSpaceFragment import com.mussonindustrial.embr.snmp.utils.removeAllNodes import org.eclipse.milo.opcua.sdk.core.Reference -import org.eclipse.milo.opcua.sdk.core.ValueRank import org.eclipse.milo.opcua.sdk.server.AddressSpaceComposite import org.eclipse.milo.opcua.sdk.server.Lifecycle -import org.eclipse.milo.opcua.sdk.server.methods.MethodInvocationHandler -import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode -import org.eclipse.milo.opcua.sdk.server.nodes.UaMethodNode import org.eclipse.milo.opcua.stack.core.NodeIds -import org.eclipse.milo.opcua.stack.core.types.builtin.* -import org.eclipse.milo.opcua.stack.core.types.structured.Argument -import org.eclipse.milo.opcua.stack.core.types.structured.CallMethodResult class MethodAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceComposite) : - DeviceContextManagedAddressSpaceFragment(device.context.deviceContext, composite) { - - val root = "Methods" + DeviceContextManagedAddressSpaceFragment(device.context.deviceContext, composite), Lifecycle { init { - lifecycleManager.addLifecycle( - object : Lifecycle { - override fun startup() { - addNodes() - } - - override fun shutdown() { - nodeManager.removeAllNodes() - } - } - ) + lifecycleManager.addStartupTask { addNodes() } + lifecycleManager.addShutdownTask { nodeManager.removeAllNodes() } } fun addNodes() { - val folder = - UaFolderNode( - nodeContext, - nodeId(root), - qualifiedName(root), - LocalizedText.english(root), - ) - nodeManager.addNode(folder) - - folder.addReference( - Reference( - folder.nodeId, - NodeIds.Organizes, - deviceNodeId.expanded(), - Reference.Direction.INVERSE, - ) - ) - - addMethodNode( - folder.nodeId, - "Walk", - arrayOf( - Argument( - "Root", - NodeIds.String, - ValueRank.ScalarOrOneDimension.value, - null, - LocalizedText.english("Root OID to walk."), - ) - ), - arrayOf( - Argument( - "Result", - OidValueType.TYPE_ID.toNodeId(server.namespaceTable).get(), - ValueRank.ScalarOrOneDimension.value, - null, - LocalizedText.english("Result string will go here."), + ReadTableMethodNode(nodeContext, nodeId("ReadTable"), device).apply { + nodeManager.addNode(this) + this.addReference( + Reference( + this.nodeId, + NodeIds.HasComponent, + deviceNodeId.expanded(), + Reference.Direction.INVERSE, ) - ), - ) { _, request -> - val root = - request.inputArguments?.first()?.value as String? - ?: return@addMethodNode CallMethodResult( - StatusCode.BAD, - arrayOfNulls(0), - arrayOfNulls(0), - arrayOf(), - ) - - val results = - device.walk(listOf(Snmp4jOid(root))).map { - ExtensionObject.encode( - server.staticEncodingContext, - OidValueType(it.oid.numeric, it.value.value.value), - ) - } - - val output = Variant(results.toTypedArray()) - - CallMethodResult( - StatusCode.GOOD, - arrayOfNulls(0), - arrayOfNulls(0), - arrayOf(output), ) } - } - - fun addMethodNode( - parent: NodeId, - name: String, - inputs: Array, - outputs: Array, - handler: MethodInvocationHandler, - ) { - UaMethodNode.UaMethodNodeBuilder(nodeContext) - .run { - setNodeId(nodeId(name)) - setBrowseName(qualifiedName(name)) - setDisplayName(LocalizedText.english(name)) - addReference( - Reference( - nodeId, - NodeIds.HasComponent, - parent.expanded(), - Reference.Direction.INVERSE, - ) + WalkMethodNode(nodeContext, nodeId("Walk"), device).apply { + nodeManager.addNode(this) + this.addReference( + Reference( + this.nodeId, + NodeIds.HasComponent, + deviceNodeId.expanded(), + Reference.Direction.INVERSE, ) - buildAndAdd() - } - .run { - inputArguments = inputs - outputArguments = outputs - invocationHandler = handler - } + ) + } } } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/ObjectModelAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/ObjectModelAddressSpace.kt index 49257248..41a64b6c 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/ObjectModelAddressSpace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/ObjectModelAddressSpace.kt @@ -18,17 +18,8 @@ class ObjectModelAddressSpace(val device: SnmpAgentDevice, composite: AddressSpa private val model = device.model init { - lifecycleManager.addLifecycle( - object : Lifecycle { - override fun startup() { - addNodes() - } - - override fun shutdown() { - nodeManager.removeAllNodes() - } - } - ) + lifecycleManager.addStartupTask { addNodes() } + lifecycleManager.addShutdownTask { nodeManager.removeAllNodes() } } fun addNodes() { @@ -44,7 +35,7 @@ class ObjectModelAddressSpace(val device: SnmpAgentDevice, composite: AddressSpa folder.addReference( Reference( folder.nodeId, - NodeIds.Organizes, + NodeIds.HasComponent, deviceNodeId.expanded(), Reference.Direction.INVERSE, ) @@ -57,20 +48,12 @@ class ObjectModelAddressSpace(val device: SnmpAgentDevice, composite: AddressSpa val node = UaFolderNode( nodeContext, - nodeId("Objects/${name}"), + nodeId("${root}/${name}"), qualifiedName(name), LocalizedText.english(name), ) nodeManager.addNode(node) - - folder.addReference( - Reference( - node.nodeId, - NodeIds.Organizes, - folder.nodeId.expanded(), - Reference.Direction.INVERSE, - ) - ) + folder.addOrganizes(node) } override fun browse( diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt index 739c6401..4cbbc46b 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt @@ -1,13 +1,13 @@ package com.mussonindustrial.embr.snmp.agents.opc import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice -import com.mussonindustrial.embr.snmp.agents.model.ObjectModel +import com.mussonindustrial.embr.snmp.agents.opc.nodes.DynamicObjectSuffixNode +import com.mussonindustrial.embr.snmp.model.ExtendedOid +import com.mussonindustrial.embr.snmp.model.ObjectModel import com.mussonindustrial.embr.snmp.model.OidValue -import com.mussonindustrial.embr.snmp.model.Snmp4jOid +import com.mussonindustrial.embr.snmp.model.asExtendedOid +import com.mussonindustrial.embr.snmp.model.isOid import com.mussonindustrial.embr.snmp.opc.DeviceContextManagedAddressSpaceFragment -import com.mussonindustrial.embr.snmp.utils.OidPath -import com.mussonindustrial.embr.snmp.utils.OidSuffix -import com.mussonindustrial.embr.snmp.utils.parseOidPath import kotlin.jvm.optionals.getOrNull import org.eclipse.milo.opcua.sdk.core.AccessLevel import org.eclipse.milo.opcua.sdk.core.Reference @@ -28,7 +28,7 @@ import org.eclipse.milo.opcua.stack.core.types.structured.WriteValue class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceComposite) : DeviceContextManagedAddressSpaceFragment(device.context.deviceContext, composite) { - private val filter = SimpleAddressSpaceFilter.create { it.getPath().parseOidPath() != null } + private val filter = SimpleAddressSpaceFilter.create { it.getPath().isOid() } override fun read( context: AddressSpace.ReadContext, @@ -38,7 +38,7 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos ): List { val requests = readValueIds.map { ReadRequest(it) } - val directReads = requests.filter { it.path.suffix == null } + val directReads = requests.filter { !it.oid.hasSuffix } val valueReads = directReads.filter { AttributeId.from(it.readValueId.attributeId).get() == AttributeId.Value @@ -55,7 +55,7 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos result.value = value } - val suffixReads = requests.filter { it.path.suffix != null } + val suffixReads = requests.filter { it.oid.hasSuffix } readSuffixAttributes(suffixReads).zip(suffixReads).forEach { (value, result) -> result.value = value } @@ -135,10 +135,11 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos return requests.zip(descriptors).map { (request, descriptor) -> val nodeId = request.readValueId.nodeId val attributeId = AttributeId.from(request.readValueId.attributeId).getOrNull() - val path = request.path attributeId - .runCatching { resolveSuffixAttribute(attributeId, nodeId, descriptor, path) } + .runCatching { + resolveSuffixAttribute(attributeId, nodeId, descriptor, request.oid.suffix!!) + } .fold( onSuccess = { DataValue(Variant(it)) }, onFailure = { DataValue((it as UaException).statusCode) }, @@ -150,28 +151,28 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos attributeId: AttributeId?, nodeId: NodeId, descriptor: ObjectModel.Descriptor, - path: OidPath, + suffix: String, ): Any? { - val suffix = - OidSuffix.from(path.suffix!!) ?: throw UaException(StatusCodes.Bad_NodeIdUnknown) - val context = OidSuffix.Context(device, descriptor) + val suffixNode = + DynamicObjectSuffixNode.from(suffix) ?: throw UaException(StatusCodes.Bad_NodeIdUnknown) + val context = DynamicObjectSuffixNode.Context(device, descriptor) return when (attributeId) { AttributeId.NodeId -> nodeId AttributeId.NodeClass -> NodeClass.Variable - AttributeId.BrowseName -> suffix.browseName(context) - AttributeId.DisplayName -> suffix.displayName(context) - AttributeId.Description -> suffix.description(context) + AttributeId.BrowseName -> suffixNode.browseName(context) + AttributeId.DisplayName -> suffixNode.displayName(context) + AttributeId.Description -> suffixNode.description(context) - AttributeId.DataType -> suffix.dataType(context) - AttributeId.ValueRank -> suffix.valueRank(context) + AttributeId.DataType -> suffixNode.dataType(context) + AttributeId.ValueRank -> suffixNode.valueRank(context) AttributeId.ArrayDimensions -> null AttributeId.AccessLevel, - AttributeId.UserAccessLevel -> suffix.accessLevel(context) + AttributeId.UserAccessLevel -> suffixNode.accessLevel(context) AttributeId.Historizing -> false - AttributeId.Value -> suffix.value(context) + AttributeId.Value -> suffixNode.value(context) AttributeId.WriteMask, AttributeId.UserWriteMask -> UInteger.valueOf(0) @@ -215,7 +216,7 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos ): List { return nodeIds.map { nodeId -> val references = mutableListOf() - val oidPath = nodeId.getPath().parseOidPath()!! + val oidPath = nodeId.getPath().asExtendedOid() references += if (oidPath.suffix == null) { browseDirect(oidPath, nodeId) @@ -226,7 +227,7 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos } } - fun browseDirect(oidPath: OidPath, nodeId: NodeId): List { + fun browseDirect(oid: ExtendedOid, nodeId: NodeId): List { val references = mutableListOf() references += Reference( @@ -235,19 +236,19 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos nodeId("Objects/Numeric").expanded(), Reference.Direction.INVERSE, ) - OidSuffix.ALL.forEach { suffix -> + DynamicObjectSuffixNode.ALL.forEach { suffix -> references += Reference( nodeId, NodeIds.HasProperty, - nodeId("${oidPath.oid}::${suffix.name}").expanded(), + nodeId("${oid.numeric}::${suffix.name}").expanded(), Reference.Direction.FORWARD, ) } return references } - fun browseSuffix(oidPath: OidPath, nodeId: NodeId): List { + fun browseSuffix(oid: ExtendedOid, nodeId: NodeId): List { val references = mutableListOf() references += Reference( @@ -260,7 +261,7 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos Reference( nodeId, NodeIds.HasProperty, - nodeId(oidPath.oid).expanded(), + nodeId(oid.numeric).expanded(), Reference.Direction.INVERSE, ) return references @@ -275,13 +276,12 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos } inner class ReadRequest(val readValueId: ReadValueId) : OidValue { - val path = readValueId.nodeId.getPath().parseOidPath()!! - override val oid = Snmp4jOid(path.oid) + override val oid = readValueId.nodeId.getPath().asExtendedOid() override var value: DataValue? = null } inner class WriteRequest(val writeValue: WriteValue) : OidValue { - override val oid = Snmp4jOid(writeValue.nodeId.getPath()) + override val oid = writeValue.nodeId.getPath().asExtendedOid() override var value: StatusCode? = null } } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/OidSuffix.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/DynamicObjectSuffixNode.kt similarity index 83% rename from modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/OidSuffix.kt rename to modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/DynamicObjectSuffixNode.kt index 42abcef6..a0d9a9f6 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/OidSuffix.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/DynamicObjectSuffixNode.kt @@ -1,7 +1,7 @@ -package com.mussonindustrial.embr.snmp.utils +package com.mussonindustrial.embr.snmp.agents.opc.nodes import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice -import com.mussonindustrial.embr.snmp.agents.model.ObjectModel +import com.mussonindustrial.embr.snmp.model.ObjectModel import org.eclipse.milo.opcua.sdk.core.AccessLevel import org.eclipse.milo.opcua.sdk.core.ValueRank import org.eclipse.milo.opcua.stack.core.OpcUaDataType @@ -10,7 +10,7 @@ import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UByte -sealed class OidSuffix(val name: String) { +sealed class DynamicObjectSuffixNode(val name: String) { data class Context(val device: SnmpAgentDevice, val descriptor: ObjectModel.Descriptor) @@ -28,7 +28,7 @@ sealed class OidSuffix(val name: String) { abstract fun value(context: Context): Any? - object DataType : OidSuffix("DataType") { + object DataType : DynamicObjectSuffixNode("DataType") { override fun dataType(context: Context): NodeId = OpcUaDataType.String.nodeId override fun valueRank(context: Context) = ValueRank.Scalar.value @@ -41,6 +41,6 @@ sealed class OidSuffix(val name: String) { val ALL = listOf(DataType) private val byName = ALL.associateBy { it.name } - fun from(name: String): OidSuffix? = byName[name] + fun from(name: String): DynamicObjectSuffixNode? = byName[name] } } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/ReadTableMethodNode.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/ReadTableMethodNode.kt new file mode 100644 index 00000000..cf2b4810 --- /dev/null +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/ReadTableMethodNode.kt @@ -0,0 +1,120 @@ +package com.mussonindustrial.embr.snmp.agents.opc.nodes + +import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice +import com.mussonindustrial.embr.snmp.model.asExtendedOid +import com.mussonindustrial.embr.snmp.model.nullOrExtendedOid +import com.mussonindustrial.embr.snmp.opc.types.OidValueType +import kotlin.collections.toTypedArray +import org.eclipse.milo.opcua.sdk.core.ValueRank +import org.eclipse.milo.opcua.sdk.server.methods.MethodInvocationHandler +import org.eclipse.milo.opcua.sdk.server.nodes.UaMethodNode +import org.eclipse.milo.opcua.sdk.server.nodes.UaNodeContext +import org.eclipse.milo.opcua.stack.core.NodeIds +import org.eclipse.milo.opcua.stack.core.types.builtin.DiagnosticInfo +import org.eclipse.milo.opcua.stack.core.types.builtin.ExtensionObject +import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText +import org.eclipse.milo.opcua.stack.core.types.builtin.Matrix +import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId +import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName +import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode +import org.eclipse.milo.opcua.stack.core.types.builtin.Variant +import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger +import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint +import org.eclipse.milo.opcua.stack.core.types.structured.Argument +import org.eclipse.milo.opcua.stack.core.types.structured.CallMethodResult + +class ReadTableMethodNode(context: UaNodeContext, nodeId: NodeId, val device: SnmpAgentDevice) : + UaMethodNode( + context, + nodeId, + QualifiedName.parse("ReadTable"), + LocalizedText.english("ReadTable"), + LocalizedText.english("Read from an SNMP table."), + uint(0), + uint(0), + true, + true, + ) { + + init { + inputArguments = + arrayOf( + Argument( + "Columns", + NodeIds.String, + ValueRank.OneDimension.value, + arrayOf(UInteger.valueOf(0)), + LocalizedText.english("A list of column OIDs that define the table structure."), + ), + Argument( + "LowerBoundIndex", + NodeIds.String, + ValueRank.Scalar.value, + null, + LocalizedText.english("The inclusive lower index bound for the table read."), + ), + Argument( + "UpperBoundIndex", + NodeIds.String, + ValueRank.Scalar.value, + null, + LocalizedText.english("The inclusive upper index bound for the table read."), + ), + ) + + outputArguments = + arrayOf( + Argument( + "Result", + OidValueType.TYPE_ID.toNodeId(nodeContext.namespaceTable).get(), + 2, + arrayOf(UInteger.valueOf(0), UInteger.valueOf(0)), + LocalizedText.english( + "An array containing the table rows and columns derived from the SNMP response." + ), + ) + ) + + invocationHandler = MethodInvocationHandler { _, request -> + val columns = + request.inputArguments?.first()?.value as Array<*>? + ?: return@MethodInvocationHandler CallMethodResult( + StatusCode.BAD, + arrayOfNulls(0), + arrayOfNulls(0), + arrayOf(), + ) + + val lowerBoundIndex = request.inputArguments?.get(1)?.value as String + val upperBoundIndex = request.inputArguments?.get(2)?.value as String + + val results = + device + .readTable( + columns.map { (it as String).asExtendedOid() }, + lowerBoundIndex.ifEmpty { null }.nullOrExtendedOid(), + upperBoundIndex.ifEmpty { null }.nullOrExtendedOid(), + ) + .map { column -> + column + .map { + ExtensionObject.encode( + context.server.staticEncodingContext, + OidValueType(it.oid.numeric, it.value.value.value), + ) + } + .toTypedArray() + } + .toTypedArray() + + val output = Variant(Matrix(results)) + + CallMethodResult( + StatusCode.GOOD, + arrayOfNulls(0), + arrayOfNulls(0), + arrayOf(output), + ) + } + } +} diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/WalkMethodNode.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/WalkMethodNode.kt new file mode 100644 index 00000000..f6729d8c --- /dev/null +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/WalkMethodNode.kt @@ -0,0 +1,83 @@ +package com.mussonindustrial.embr.snmp.agents.opc.nodes + +import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice +import com.mussonindustrial.embr.snmp.model.asExtendedOid +import com.mussonindustrial.embr.snmp.opc.types.OidValueType +import kotlin.collections.toTypedArray +import org.eclipse.milo.opcua.sdk.server.methods.MethodInvocationHandler +import org.eclipse.milo.opcua.sdk.server.nodes.UaMethodNode +import org.eclipse.milo.opcua.sdk.server.nodes.UaNodeContext +import org.eclipse.milo.opcua.stack.core.NodeIds +import org.eclipse.milo.opcua.stack.core.types.builtin.DiagnosticInfo +import org.eclipse.milo.opcua.stack.core.types.builtin.ExtensionObject +import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText +import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId +import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName +import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode +import org.eclipse.milo.opcua.stack.core.types.builtin.Variant +import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger +import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint +import org.eclipse.milo.opcua.stack.core.types.structured.Argument +import org.eclipse.milo.opcua.stack.core.types.structured.CallMethodResult + +class WalkMethodNode(context: UaNodeContext, nodeId: NodeId, val device: SnmpAgentDevice) : + UaMethodNode( + context, + nodeId, + QualifiedName.parse("Walk"), + LocalizedText.english("Walk"), + LocalizedText.english("Walk the SNMP tree starting at the given OID."), + uint(0), + uint(0), + true, + true, + ) { + + init { + inputArguments = + arrayOf( + Argument( + "Roots", + NodeIds.String, + 1, + arrayOf(UInteger.valueOf(0)), + LocalizedText.english("A list of OIDs to walk."), + ) + ) + + outputArguments = + arrayOf( + Argument( + "Result", + OidValueType.TYPE_ID.toNodeId(nodeContext.namespaceTable).get(), + 1, + arrayOf(UInteger.valueOf(0)), + LocalizedText.english("A list of values discovered during the walk operation."), + ) + ) + + invocationHandler = MethodInvocationHandler { _, request -> + val roots = request.inputArguments?.first()?.value as Array<*> + + val results = + device + .walk(roots.map { (it as String).asExtendedOid() }) + .map { + ExtensionObject.encode( + context.server.staticEncodingContext, + OidValueType(it.oid.numeric, it.value.value.value), + ) + } + .toTypedArray() + + val output = Variant(results) + + CallMethodResult( + StatusCode.GOOD, + arrayOfNulls(0), + arrayOfNulls(0), + arrayOf(output), + ) + } + } +} diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/rpc/SnmpAgentRpcImpl.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/rpc/SnmpAgentRpcImpl.kt index cc29d640..209da631 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/rpc/SnmpAgentRpcImpl.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/rpc/SnmpAgentRpcImpl.kt @@ -6,9 +6,9 @@ import com.inductiveautomation.ignition.gateway.clientcomm.MutabilityMode import com.inductiveautomation.ignition.gateway.rpc.RpcDelegate import com.mussonindustrial.embr.snmp.SnmpGatewayContext import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice -import com.mussonindustrial.embr.snmp.model.Oid import com.mussonindustrial.embr.snmp.model.QualifiedOidValue import com.mussonindustrial.embr.snmp.model.Snmp4jOid +import com.mussonindustrial.embr.snmp.model.nullOrExtendedOid import com.mussonindustrial.embr.snmp.utils.toQualifiedValue import com.mussonindustrial.embr.snmp.utils.toQualityCode import org.python.core.Py.ValueError @@ -57,16 +57,9 @@ class SnmpAgentRpcImpl(val context: SnmpGatewayContext) : SnmpAgentRpc { return snmpAgent .readTable( columns.map { Snmp4jOid(it) }, - lowerBoundIndex.nullOrOid(), - upperBoundIndex.nullOrOid(), + lowerBoundIndex.nullOrExtendedOid(), + upperBoundIndex.nullOrExtendedOid(), ) .map { results -> results.map { it.toQualifiedValue() } } } - - private fun String?.nullOrOid(): Oid? { - this?.let { - return Snmp4jOid(it) - } - return null - } } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/model/ConcurrentObjectModel.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ConcurrentObjectModel.kt similarity index 87% rename from modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/model/ConcurrentObjectModel.kt rename to modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ConcurrentObjectModel.kt index 499f6d29..dc60f63b 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/model/ConcurrentObjectModel.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ConcurrentObjectModel.kt @@ -1,8 +1,6 @@ -package com.mussonindustrial.embr.snmp.agents.model +package com.mussonindustrial.embr.snmp.model import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice -import com.mussonindustrial.embr.snmp.model.Oid -import com.mussonindustrial.embr.snmp.model.OidValue import com.mussonindustrial.embr.snmp.typing.snmpDataType import java.util.concurrent.ConcurrentHashMap import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/model/ObjectModel.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ObjectModel.kt similarity index 91% rename from modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/model/ObjectModel.kt rename to modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ObjectModel.kt index c21518e2..0f8cfcb7 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/model/ObjectModel.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ObjectModel.kt @@ -1,9 +1,5 @@ -package com.mussonindustrial.embr.snmp.agents.model +package com.mussonindustrial.embr.snmp.model -import com.mussonindustrial.embr.snmp.model.BasicOidValue -import com.mussonindustrial.embr.snmp.model.Oid -import com.mussonindustrial.embr.snmp.model.OidValue -import com.mussonindustrial.embr.snmp.model.SnmpCommunicationError import com.mussonindustrial.embr.snmp.typing.SnmpDataType import org.eclipse.milo.opcua.stack.core.StatusCodes import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/DeviceAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/DeviceAddressSpace.kt deleted file mode 100644 index 378ad0dc..00000000 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/DeviceAddressSpace.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.mussonindustrial.embr.snmp.opc - -import com.inductiveautomation.ignition.gateway.opcua.server.api.DeviceContext -import com.mussonindustrial.embr.snmp.utils.removeAllNodes -import org.eclipse.milo.opcua.sdk.core.Reference -import org.eclipse.milo.opcua.sdk.server.AddressSpaceComposite -import org.eclipse.milo.opcua.sdk.server.Lifecycle -import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectNode -import org.eclipse.milo.opcua.stack.core.NodeIds -import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText -import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger - -class DeviceAddressSpace(deviceContext: DeviceContext, composite: AddressSpaceComposite) : - DeviceContextManagedAddressSpaceFragment(deviceContext, composite) { - - private val deviceFolderNode = - UaObjectNode( - nodeContext, - deviceNodeId, - qualifiedName("[${name}]"), - LocalizedText("[${name}]"), - LocalizedText("SNMP Agent Device"), - UInteger.MIN, - UInteger.MIN, - ) - - init { - lifecycleManager.addLifecycle( - object : Lifecycle { - override fun startup() { - nodeManager.addNode(deviceFolderNode) - deviceFolderNode.addReference( - Reference( - deviceFolderNode.nodeId, - NodeIds.Organizes, - getRootNodeId().expanded(), - Reference.Direction.INVERSE, - ) - ) - } - - override fun shutdown() { - nodeManager.removeAllNodes() - } - } - ) - } -} diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNamespace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNamespace.kt index 60a7a919..7b8a9c72 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNamespace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNamespace.kt @@ -1,6 +1,7 @@ package com.mussonindustrial.embr.snmp.opc -import com.mussonindustrial.embr.snmp.agents.opc.types.OidValueType +import com.mussonindustrial.embr.snmp.opc.types.OidValueType +import com.mussonindustrial.embr.snmp.opc.types.SnmpAgentDeviceType import org.eclipse.milo.opcua.sdk.server.ManagedNamespaceWithLifecycle import org.eclipse.milo.opcua.sdk.server.OpcUaServer import org.eclipse.milo.opcua.sdk.server.items.DataItem @@ -21,7 +22,10 @@ class SnmpNamespace(server: OpcUaServer) : ManagedNamespaceWithLifecycle(server, init { instance = this lifecycleManager.addLifecycle(subscriptionModel) - lifecycleManager.addStartupTask { OidValueType.register(this) } + lifecycleManager.addStartupTask { + SnmpAgentDeviceType.register(this) + OidValueType.register(this) + } } fun nodeId(id: Any): NodeId { diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/types/OidValueType.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/OidValueType.kt similarity index 96% rename from modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/types/OidValueType.kt rename to modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/OidValueType.kt index 4e250f88..c5433a06 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/types/OidValueType.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/OidValueType.kt @@ -1,7 +1,8 @@ -package com.mussonindustrial.embr.snmp.agents.opc.types +package com.mussonindustrial.embr.snmp.opc.types import com.google.common.base.MoreObjects import com.google.common.base.Objects +import com.mussonindustrial.embr.snmp.model.OidValue import com.mussonindustrial.embr.snmp.opc.SnmpNamespace import org.eclipse.milo.opcua.sdk.core.Reference import org.eclipse.milo.opcua.sdk.core.ValueRanks @@ -28,6 +29,8 @@ class OidValueType(val oid: String, val value: Variant) : UaStructuredType { constructor(oid: String, value: Any?) : this(oid, Variant(value)) + constructor(oidValue: OidValue<*>) : this(oidValue.oid.numeric, oidValue.value) + override fun getTypeId(): ExpandedNodeId { return TYPE_ID } @@ -68,7 +71,6 @@ class OidValueType(val oid: String, val value: Variant) : UaStructuredType { override fun decodeType(context: EncodingContext, decoder: UaDecoder): OidValueType { val oid = decoder.decodeString("Oid") val value = decoder.decodeVariant("Value") - return OidValueType(oid, value) } @@ -107,8 +109,8 @@ class OidValueType(val oid: String, val value: Variant) : UaStructuredType { UaDataTypeNode( namespace.nodeContext, dataTypeId, - QualifiedName(namespace.namespaceIndex, "OidValueStructType"), - LocalizedText.english("OidValueStructType"), + QualifiedName(namespace.namespaceIndex, "OidValueType"), + LocalizedText.english("OidValueType"), LocalizedText.NULL_VALUE, Unsigned.uint(0), Unsigned.uint(0), diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/SnmpAgentDeviceType.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/SnmpAgentDeviceType.kt new file mode 100644 index 00000000..29f4041a --- /dev/null +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/SnmpAgentDeviceType.kt @@ -0,0 +1,42 @@ +package com.mussonindustrial.embr.snmp.opc.types + +import org.eclipse.milo.opcua.sdk.core.Reference +import org.eclipse.milo.opcua.sdk.server.ManagedNamespace +import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectTypeNode +import org.eclipse.milo.opcua.stack.core.NodeIds +import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText +import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId +import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName +import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint + +object SnmpAgentDeviceType { + + const val BROWSE_NAME = "SnmpAgentDeviceType" + + fun nodeId(namespace: ManagedNamespace): NodeId = + NodeId(namespace.namespaceIndex, "ObjectType.$BROWSE_NAME") + + fun register(namespace: ManagedNamespace) { + UaObjectTypeNode( + namespace.nodeContext, + nodeId(namespace), + QualifiedName(namespace.namespaceIndex, BROWSE_NAME), + LocalizedText.english(BROWSE_NAME), + LocalizedText.english(""), + uint(0), + uint(0), + false, + ) + .apply { + namespace.nodeManager.addNode(this) + addReference( + Reference( + nodeId(namespace), + NodeIds.HasSubtype, + NodeIds.BaseObjectType.expanded(), + Reference.Direction.INVERSE, + ) + ) + } + } +} diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/OidPath.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/OidPath.kt deleted file mode 100644 index 6423139a..00000000 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/OidPath.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.mussonindustrial.embr.snmp.utils - -data class OidPath(val oid: String, val suffix: String? = null) - -fun String.parseOidPath(): OidPath? { - val parts = this.split("::", limit = 2) - val oid = parts[0] - val suffix = parts.getOrNull(1)?.takeIf { it.isNotBlank() } - return OidPath(oid, suffix) -} diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/SnmpUtils.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/SnmpUtils.kt index 2abe0062..f7dc07e4 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/SnmpUtils.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/SnmpUtils.kt @@ -3,25 +3,14 @@ package com.mussonindustrial.embr.snmp.utils import com.mussonindustrial.embr.snmp.model.BasicQualifiedOidValue import com.mussonindustrial.embr.snmp.model.OidValue import com.mussonindustrial.embr.snmp.model.QualifiedOidValue -import java.text.ParseException import java.util.Date import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue import org.snmp4j.PDU -import org.snmp4j.SNMP4JSettings import org.snmp4j.Target import org.snmp4j.smi.Address import org.snmp4j.smi.VariableBinding import org.snmp4j.util.PDUFactory -fun String.isOid(): Boolean { - try { - SNMP4JSettings.getOIDTextFormat().parse(this) - return true - } catch (_: ParseException) { - return false - } -} - fun Target.createSizeBoundedPDUs( pduFactory: PDUFactory, bindings: List, From 569ff23ddc55b9e56d612824545f3151bff9bde6 Mon Sep 17 00:00:00 2001 From: Ben Musson Date: Wed, 4 Feb 2026 18:18:24 -0500 Subject: [PATCH 5/9] centralize node ids --- .../snmp/agents/opc/DeviceAddressSpace.kt | 4 +- .../embr/snmp/agents/opc/OidAddressSpace.kt | 2 +- .../agents/opc/nodes/ReadTableMethodNode.kt | 3 +- .../snmp/agents/opc/nodes/WalkMethodNode.kt | 3 +- .../agents/opc/types/SnmpAgentDeviceType.kt | 82 +++++++++ .../embr/snmp/model/ConcurrentObjectModel.kt | 8 +- .../embr/snmp/model/ObjectModel.kt | 5 +- .../embr/snmp/opc/SnmpNamespace.kt | 46 +++-- .../embr/snmp/opc/SnmpNamespaceNodeIds.kt | 26 +++ .../embr/snmp/opc/types/OidValueType.kt | 172 ++++++++++-------- .../snmp/opc/types/SnmpAgentDeviceType.kt | 42 ----- .../embr/snmp/opc/types/SnmpDataType.kt | 129 +++++++++++++ .../embr/snmp/typing/SnmpDataType.kt | 40 ---- 13 files changed, 377 insertions(+), 185 deletions(-) create mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/types/SnmpAgentDeviceType.kt create mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNamespaceNodeIds.kt delete mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/SnmpAgentDeviceType.kt create mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/SnmpDataType.kt delete mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/typing/SnmpDataType.kt diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DeviceAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DeviceAddressSpace.kt index 042e4871..1a134bb9 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DeviceAddressSpace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DeviceAddressSpace.kt @@ -1,9 +1,9 @@ package com.mussonindustrial.embr.snmp.agents.opc import com.inductiveautomation.ignition.gateway.opcua.server.api.DeviceContext +import com.mussonindustrial.embr.snmp.agents.opc.types.SnmpAgentDeviceType import com.mussonindustrial.embr.snmp.opc.DeviceContextManagedAddressSpaceFragment import com.mussonindustrial.embr.snmp.opc.SnmpNamespace -import com.mussonindustrial.embr.snmp.opc.types.SnmpAgentDeviceType import com.mussonindustrial.embr.snmp.utils.removeAllNodes import org.eclipse.milo.opcua.sdk.core.Reference import org.eclipse.milo.opcua.sdk.server.AddressSpaceComposite @@ -36,7 +36,7 @@ class DeviceAddressSpace(deviceContext: DeviceContext, composite: AddressSpaceCo Reference( this.nodeId, NodeIds.HasTypeDefinition, - SnmpAgentDeviceType.nodeId(SnmpNamespace.instance).expanded(), + SnmpNamespace.NodesIds.SnmpAgentDeviceType.expanded(), Reference.Direction.FORWARD, ) ) diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt index 4cbbc46b..077d6cbe 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt @@ -103,7 +103,7 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos AttributeId.DataType -> when (descriptor) { - is ObjectModel.ValueDescriptor -> descriptor.snmpDataType.uaDataType + is ObjectModel.ValueDescriptor -> descriptor.snmpDataType.nodeId else -> OpcUaDataType.String.nodeId } AttributeId.ValueRank -> diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/ReadTableMethodNode.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/ReadTableMethodNode.kt index cf2b4810..48eea529 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/ReadTableMethodNode.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/ReadTableMethodNode.kt @@ -3,6 +3,7 @@ package com.mussonindustrial.embr.snmp.agents.opc.nodes import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice import com.mussonindustrial.embr.snmp.model.asExtendedOid import com.mussonindustrial.embr.snmp.model.nullOrExtendedOid +import com.mussonindustrial.embr.snmp.opc.SnmpNamespace import com.mussonindustrial.embr.snmp.opc.types.OidValueType import kotlin.collections.toTypedArray import org.eclipse.milo.opcua.sdk.core.ValueRank @@ -66,7 +67,7 @@ class ReadTableMethodNode(context: UaNodeContext, nodeId: NodeId, val device: Sn arrayOf( Argument( "Result", - OidValueType.TYPE_ID.toNodeId(nodeContext.namespaceTable).get(), + SnmpNamespace.NodesIds.OidValue, 2, arrayOf(UInteger.valueOf(0), UInteger.valueOf(0)), LocalizedText.english( diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/WalkMethodNode.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/WalkMethodNode.kt index f6729d8c..66c880c6 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/WalkMethodNode.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/WalkMethodNode.kt @@ -2,6 +2,7 @@ package com.mussonindustrial.embr.snmp.agents.opc.nodes import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice import com.mussonindustrial.embr.snmp.model.asExtendedOid +import com.mussonindustrial.embr.snmp.opc.SnmpNamespace import com.mussonindustrial.embr.snmp.opc.types.OidValueType import kotlin.collections.toTypedArray import org.eclipse.milo.opcua.sdk.server.methods.MethodInvocationHandler @@ -49,7 +50,7 @@ class WalkMethodNode(context: UaNodeContext, nodeId: NodeId, val device: SnmpAge arrayOf( Argument( "Result", - OidValueType.TYPE_ID.toNodeId(nodeContext.namespaceTable).get(), + SnmpNamespace.NodesIds.OidValue, 1, arrayOf(UInteger.valueOf(0)), LocalizedText.english("A list of values discovered during the walk operation."), diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/types/SnmpAgentDeviceType.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/types/SnmpAgentDeviceType.kt new file mode 100644 index 00000000..382f164f --- /dev/null +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/types/SnmpAgentDeviceType.kt @@ -0,0 +1,82 @@ +package com.mussonindustrial.embr.snmp.agents.opc.types + +import com.mussonindustrial.embr.snmp.opc.SnmpNamespace +import org.eclipse.milo.opcua.sdk.core.Reference +import org.eclipse.milo.opcua.sdk.server.nodes.UaMethodNode +import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectTypeNode +import org.eclipse.milo.opcua.stack.core.NodeIds +import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText +import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId +import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned +import org.eclipse.milo.opcua.stack.core.types.structured.AccessRestrictionType + +object SnmpAgentDeviceType { + + const val BROWSE_NAME = "SnmpAgentDeviceType" + + private fun method(nodeId: NodeId, browseName: String, parent: NodeId) = + UaMethodNode( + SnmpNamespace.Companion.nodeContext, + nodeId, + SnmpNamespace.Companion.qualifiedName(browseName), + LocalizedText.english(browseName), + LocalizedText.english(""), + Unsigned.uint(0), + Unsigned.uint(0), + true, + true, + ) + .apply { + accessRestrictions = AccessRestrictionType.of() + SnmpNamespace.Companion.nodeManager.addNode(this) + addReference( + Reference( + nodeId, + NodeIds.HasComponent, + parent.expanded(), + Reference.Direction.INVERSE, + ) + ) + addReference( + Reference( + nodeId, + NodeIds.HasModellingRule, + NodeIds.ModellingRule_Mandatory.expanded(), + Reference.Direction.FORWARD, + ) + ) + } + + fun register() { + val type = + UaObjectTypeNode( + SnmpNamespace.Companion.nodeContext, + SnmpNamespace.Companion.NodesIds.SnmpAgentDeviceType, + SnmpNamespace.Companion.qualifiedName(BROWSE_NAME), + LocalizedText.english(BROWSE_NAME), + LocalizedText.english(""), + Unsigned.uint(0), + Unsigned.uint(0), + false, + ) + .apply { + accessRestrictions = AccessRestrictionType.of() + SnmpNamespace.Companion.nodeManager.addNode(this) + addReference( + Reference( + nodeId, + NodeIds.HasSubtype, + NodeIds.BaseObjectType.expanded(), + Reference.Direction.INVERSE, + ) + ) + } + + method(SnmpNamespace.Companion.NodesIds.SnmpAgentDeviceType_Walk, "Walk", type.nodeId) + method( + SnmpNamespace.Companion.NodesIds.SnmpAgentDeviceType_ReadTable, + "ReadTable", + type.nodeId, + ) + } +} diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ConcurrentObjectModel.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ConcurrentObjectModel.kt index dc60f63b..41b600eb 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ConcurrentObjectModel.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ConcurrentObjectModel.kt @@ -1,7 +1,7 @@ package com.mussonindustrial.embr.snmp.model import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice -import com.mussonindustrial.embr.snmp.typing.snmpDataType +import com.mussonindustrial.embr.snmp.opc.types.SnmpDataType import java.util.concurrent.ConcurrentHashMap import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue import org.snmp4j.smi.Variable @@ -15,12 +15,12 @@ class ConcurrentObjectModel(val device: SnmpAgentDevice) : ObjectModel { get() = descriptors.map { it.key } override fun observe(value: OidValue): OidValue { - val descriptor = ObjectModel.ValueDescriptor(value.oid, value.value.snmpDataType) val result = toOpcUaValue(value) - - descriptors[result.oid] = descriptor knownValues[result.oid] = result + descriptors.getOrPut(result.oid) { + ObjectModel.ValueDescriptor(value.oid, SnmpDataType.of(value.value)) + } return result } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ObjectModel.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ObjectModel.kt index 0f8cfcb7..52a145b7 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ObjectModel.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ObjectModel.kt @@ -1,6 +1,6 @@ package com.mussonindustrial.embr.snmp.model -import com.mussonindustrial.embr.snmp.typing.SnmpDataType +import com.mussonindustrial.embr.snmp.opc.types.SnmpDataType import org.eclipse.milo.opcua.stack.core.StatusCodes import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue import org.eclipse.milo.opcua.stack.core.types.builtin.Variant @@ -27,7 +27,8 @@ interface ObjectModel { val snmpValue = when (descriptor) { - is ValueDescriptor -> descriptor.snmpDataType.variableOfType(value.value) + is ValueDescriptor -> + SnmpDataType.variableOfType(descriptor.snmpDataType, value.value) is InvalidDescriptor -> Null.instance is TableColumnDescriptor -> Null.instance is TableDescriptor -> Null.instance diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNamespace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNamespace.kt index 7b8a9c72..769ec93e 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNamespace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNamespace.kt @@ -1,20 +1,47 @@ package com.mussonindustrial.embr.snmp.opc +import com.mussonindustrial.embr.snmp.agents.opc.types.SnmpAgentDeviceType import com.mussonindustrial.embr.snmp.opc.types.OidValueType -import com.mussonindustrial.embr.snmp.opc.types.SnmpAgentDeviceType +import com.mussonindustrial.embr.snmp.opc.types.SnmpDataType import org.eclipse.milo.opcua.sdk.server.ManagedNamespaceWithLifecycle import org.eclipse.milo.opcua.sdk.server.OpcUaServer +import org.eclipse.milo.opcua.sdk.server.UaNodeManager import org.eclipse.milo.opcua.sdk.server.items.DataItem import org.eclipse.milo.opcua.sdk.server.items.MonitoredItem +import org.eclipse.milo.opcua.sdk.server.nodes.UaNodeContext import org.eclipse.milo.opcua.sdk.server.util.SubscriptionModel import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId +import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName +import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UShort class SnmpNamespace(server: OpcUaServer) : ManagedNamespaceWithLifecycle(server, NAMESPACE_URI) { companion object { const val NAMESPACE_URI = "urn:mussonindustrial:embr:snmp" lateinit var instance: SnmpNamespace + + fun nodeId(id: Long): NodeId = + ExpandedNodeId.of(NAMESPACE_URI, id) + .toNodeIdOrThrow(instance.nodeContext.namespaceTable) + + fun nodeId(id: String): NodeId = + ExpandedNodeId.of(NAMESPACE_URI, id) + .toNodeIdOrThrow(instance.nodeContext.namespaceTable) + + fun qualifiedName(name: String): QualifiedName = + QualifiedName(instance.namespaceIndex, name) + + val nodeContext: UaNodeContext + get() = instance.nodeContext + + val nodeManager: UaNodeManager + get() = instance.nodeManager + + val namespaceIndex: UShort + get() = instance.namespaceIndex + + val NodesIds by lazy { SnmpNamespaceNodeIds() } } private val subscriptionModel = SubscriptionModel(server, this) @@ -22,20 +49,13 @@ class SnmpNamespace(server: OpcUaServer) : ManagedNamespaceWithLifecycle(server, init { instance = this lifecycleManager.addLifecycle(subscriptionModel) - lifecycleManager.addStartupTask { - SnmpAgentDeviceType.register(this) - OidValueType.register(this) - } + lifecycleManager.addStartupTask { registerTypes() } } - fun nodeId(id: Any): NodeId { - return ExpandedNodeId( - ExpandedNodeId.ServerReference.ServerIndex.LOCAL, - ExpandedNodeId.NamespaceReference.NamespaceUri(NAMESPACE_URI), - id, - ) - .toNodeId(server.namespaceTable) - .get() + fun registerTypes() { + SnmpDataType.registerAll() + SnmpAgentDeviceType.register() + OidValueType.register() } override fun onDataItemsCreated(items: List) { diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNamespaceNodeIds.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNamespaceNodeIds.kt new file mode 100644 index 00000000..d4849892 --- /dev/null +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNamespaceNodeIds.kt @@ -0,0 +1,26 @@ +package com.mussonindustrial.embr.snmp.opc + +class SnmpNamespaceNodeIds() { + + val Null = SnmpNamespace.nodeId(0) + val Int32 = SnmpNamespace.nodeId(1) + val UInt32 = SnmpNamespace.nodeId(2) + val OctetString = SnmpNamespace.nodeId(3) + val Oid = SnmpNamespace.nodeId(4) + val IpAddress = SnmpNamespace.nodeId(5) + val Counter32 = SnmpNamespace.nodeId(6) + val Counter64 = SnmpNamespace.nodeId(7) + val Gauge32 = SnmpNamespace.nodeId(8) + val TimeTicks = SnmpNamespace.nodeId(9) + + val SnmpAgentDeviceType = SnmpNamespace.nodeId(1000) + val SnmpAgentDeviceType_ReadTable = SnmpNamespace.nodeId(1010) + val SnmpAgentDeviceType_ReadTable_InputArguments = SnmpNamespace.nodeId(1011) + val SnmpAgentDeviceType_ReadTable_OutputArguments = SnmpNamespace.nodeId(1012) + val SnmpAgentDeviceType_Walk = SnmpNamespace.nodeId(1020) + val SnmpAgentDeviceType_Walk_InputArguments = SnmpNamespace.nodeId(1021) + val SnmpAgentDeviceType_Walk_OutputArguments = SnmpNamespace.nodeId(1022) + + val OidValue = SnmpNamespace.nodeId(2000) + val OidValue_Encoding_DefaultBinary = SnmpNamespace.nodeId(2001) +} diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/OidValueType.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/OidValueType.kt index c5433a06..7e470c13 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/OidValueType.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/OidValueType.kt @@ -6,8 +6,8 @@ import com.mussonindustrial.embr.snmp.model.OidValue import com.mussonindustrial.embr.snmp.opc.SnmpNamespace import org.eclipse.milo.opcua.sdk.core.Reference import org.eclipse.milo.opcua.sdk.core.ValueRanks -import org.eclipse.milo.opcua.sdk.server.ManagedNamespace import org.eclipse.milo.opcua.sdk.server.nodes.UaDataTypeNode +import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectNode import org.eclipse.milo.opcua.stack.core.NodeIds import org.eclipse.milo.opcua.stack.core.UaSerializationException import org.eclipse.milo.opcua.stack.core.encoding.EncodingContext @@ -18,10 +18,10 @@ import org.eclipse.milo.opcua.stack.core.types.UaStructuredType import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId -import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName import org.eclipse.milo.opcua.stack.core.types.builtin.Variant import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned import org.eclipse.milo.opcua.stack.core.types.enumerated.StructureType +import org.eclipse.milo.opcua.stack.core.types.structured.AccessRestrictionType import org.eclipse.milo.opcua.stack.core.types.structured.StructureDefinition import org.eclipse.milo.opcua.stack.core.types.structured.StructureField @@ -32,11 +32,11 @@ class OidValueType(val oid: String, val value: Variant) : UaStructuredType { constructor(oidValue: OidValue<*>) : this(oidValue.oid.numeric, oidValue.value) override fun getTypeId(): ExpandedNodeId { - return TYPE_ID + return typeNodeId.expanded() } override fun getBinaryEncodingId(): ExpandedNodeId { - return BINARY_ENCODING_ID + return binaryEncodingNodeId.expanded() } override fun getXmlEncodingId(): ExpandedNodeId { @@ -82,86 +82,100 @@ class OidValueType(val oid: String, val value: Variant) : UaStructuredType { } companion object { - val TYPE_ID: ExpandedNodeId = - ExpandedNodeId.parse( - String.format( - "nsu=%s;s=%s", - SnmpNamespace.NAMESPACE_URI, - "DataType.OidValueStructType", + const val BROWSE_NAME = "OidValue" + + private val typeNodeId: NodeId by lazy { SnmpNamespace.NodesIds.OidValue } + private val binaryEncodingNodeId: NodeId by lazy { + SnmpNamespace.NodesIds.OidValue_Encoding_DefaultBinary + } + + fun register() { + UaObjectNode( + SnmpNamespace.nodeContext, + binaryEncodingNodeId, + SnmpNamespace.qualifiedName("Default Binary"), + LocalizedText.english("Default Binary"), + LocalizedText.NULL_VALUE, + Unsigned.uint(0), + Unsigned.uint(0), ) - ) - - val BINARY_ENCODING_ID: ExpandedNodeId = - ExpandedNodeId.parse( - String.format( - "nsu=%s;s=%s", - SnmpNamespace.NAMESPACE_URI, - "DataType.OidValueStructType.BinaryEncoding", + .apply { + addReference( + Reference( + nodeId, + NodeIds.HasTypeDefinition, + NodeIds.DataTypeEncodingType.expanded(), + Reference.Direction.FORWARD, + ) + ) + accessRestrictions = AccessRestrictionType.of() + SnmpNamespace.nodeManager.addNode(this) + } + + UaDataTypeNode( + SnmpNamespace.nodeContext, + typeNodeId, + SnmpNamespace.qualifiedName(BROWSE_NAME), + LocalizedText.english(BROWSE_NAME), + LocalizedText.NULL_VALUE, + Unsigned.uint(0), + Unsigned.uint(0), + false, ) - ) - - fun register(namespace: ManagedNamespace) { - val dataTypeId: NodeId = TYPE_ID.toNodeIdOrThrow(namespace.nodeContext.namespaceTable) - val binaryEncodingId: NodeId = - BINARY_ENCODING_ID.toNodeIdOrThrow(namespace.nodeContext.namespaceTable) - - val dataTypeNode = - UaDataTypeNode( - namespace.nodeContext, - dataTypeId, - QualifiedName(namespace.namespaceIndex, "OidValueType"), - LocalizedText.english("OidValueType"), - LocalizedText.NULL_VALUE, - Unsigned.uint(0), - Unsigned.uint(0), - false, + .apply { + addReference( + Reference( + nodeId, + NodeIds.HasSubtype, + NodeIds.Structure.expanded(), + Reference.Direction.INVERSE, + ) ) - .apply { - addReference( - Reference( - dataTypeId, - NodeIds.HasSubtype, - NodeIds.Structure.expanded(), - Reference.Direction.INVERSE, - ) + addReference( + Reference( + nodeId, + NodeIds.HasEncoding, + binaryEncodingNodeId.expanded(), + Reference.Direction.FORWARD, ) - dataTypeDefinition = - StructureDefinition( - binaryEncodingId, - NodeIds.Structure, - StructureType.Structure, - arrayOf( - StructureField( - "oid", - LocalizedText.NULL_VALUE, - NodeIds.String, - ValueRanks.Scalar, - null, - namespace.nodeContext.server.config.limits.maxStringLength, - false, - ), - StructureField( - "value", - LocalizedText.NULL_VALUE, - NodeIds.BaseDataType, - ValueRanks.Scalar, - null, - Unsigned.uint(0), - false, - ), + ) + accessRestrictions = AccessRestrictionType.of() + dataTypeDefinition = + StructureDefinition( + binaryEncodingNodeId, + NodeIds.Structure, + StructureType.Structure, + arrayOf( + StructureField( + "oid", + LocalizedText.NULL_VALUE, + NodeIds.String, + ValueRanks.Scalar, + null, + SnmpNamespace.nodeContext.server.config.limits.maxStringLength, + false, + ), + StructureField( + "value", + LocalizedText.NULL_VALUE, + NodeIds.BaseDataType, + ValueRanks.Scalar, + null, + Unsigned.uint(0), + false, ), - ) - } - - namespace.nodeManager.addNode(dataTypeNode) - - namespace.nodeContext.server.staticDataTypeManager.registerType( - dataTypeId, - Codec(), - binaryEncodingId, - null, - null, - ) + ), + ) + + SnmpNamespace.nodeManager.addNode(this) + SnmpNamespace.nodeContext.server.staticDataTypeManager.registerType( + nodeId, + Codec(), + binaryEncodingNodeId, + null, + null, + ) + } } } } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/SnmpAgentDeviceType.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/SnmpAgentDeviceType.kt deleted file mode 100644 index 29f4041a..00000000 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/SnmpAgentDeviceType.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.mussonindustrial.embr.snmp.opc.types - -import org.eclipse.milo.opcua.sdk.core.Reference -import org.eclipse.milo.opcua.sdk.server.ManagedNamespace -import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectTypeNode -import org.eclipse.milo.opcua.stack.core.NodeIds -import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText -import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId -import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName -import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint - -object SnmpAgentDeviceType { - - const val BROWSE_NAME = "SnmpAgentDeviceType" - - fun nodeId(namespace: ManagedNamespace): NodeId = - NodeId(namespace.namespaceIndex, "ObjectType.$BROWSE_NAME") - - fun register(namespace: ManagedNamespace) { - UaObjectTypeNode( - namespace.nodeContext, - nodeId(namespace), - QualifiedName(namespace.namespaceIndex, BROWSE_NAME), - LocalizedText.english(BROWSE_NAME), - LocalizedText.english(""), - uint(0), - uint(0), - false, - ) - .apply { - namespace.nodeManager.addNode(this) - addReference( - Reference( - nodeId(namespace), - NodeIds.HasSubtype, - NodeIds.BaseObjectType.expanded(), - Reference.Direction.INVERSE, - ) - ) - } - } -} diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/SnmpDataType.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/SnmpDataType.kt new file mode 100644 index 00000000..95228aa4 --- /dev/null +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/SnmpDataType.kt @@ -0,0 +1,129 @@ +package com.mussonindustrial.embr.snmp.opc.types + +import com.inductiveautomation.ignition.common.TypeUtilities +import com.mussonindustrial.embr.snmp.opc.SnmpNamespace +import org.eclipse.milo.opcua.sdk.core.Reference +import org.eclipse.milo.opcua.sdk.server.nodes.UaDataTypeNode +import org.eclipse.milo.opcua.stack.core.NodeIds +import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText +import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId +import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName +import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint +import org.eclipse.milo.opcua.stack.core.types.structured.AccessRestrictionType +import org.snmp4j.smi.Variable + +enum class SnmpDataType( + val nodeId: NodeId, + val parentType: NodeId, + val backingVariableClass: Class, + private val factory: (Any?) -> Variable, +) { + Null( + SnmpNamespace.NodesIds.Null, + NodeIds.BaseDataType, + org.snmp4j.smi.Null::class.java, + { org.snmp4j.smi.Null() }, + ), + Int32( + SnmpNamespace.NodesIds.Int32, + NodeIds.Int32, + org.snmp4j.smi.Integer32::class.java, + { org.snmp4j.smi.Integer32(TypeUtilities.toInteger(it)) }, + ), + UInt32( + SnmpNamespace.NodesIds.UInt32, + NodeIds.UInt32, + org.snmp4j.smi.UnsignedInteger32::class.java, + { org.snmp4j.smi.UnsignedInteger32(TypeUtilities.toInteger(it)) }, + ), + OctetString( + SnmpNamespace.NodesIds.OctetString, + NodeIds.String, + org.snmp4j.smi.OctetString::class.java, + { org.snmp4j.smi.OID(TypeUtilities.toString(it)) }, + ), + Oid( + SnmpNamespace.NodesIds.Oid, + NodeIds.String, + org.snmp4j.smi.OID::class.java, + { org.snmp4j.smi.OID(TypeUtilities.toString(it)) }, + ), + IpAddress( + SnmpNamespace.NodesIds.IpAddress, + NodeIds.String, + org.snmp4j.smi.IpAddress::class.java, + { org.snmp4j.smi.IpAddress(TypeUtilities.toString(it)) }, + ), + Counter32( + SnmpNamespace.NodesIds.Counter32, + NodeIds.UInt32, + org.snmp4j.smi.Counter32::class.java, + { org.snmp4j.smi.Counter32(TypeUtilities.toLong(it)) }, + ), + Counter64( + SnmpNamespace.NodesIds.Counter64, + NodeIds.UInt64, + org.snmp4j.smi.Counter64::class.java, + { org.snmp4j.smi.Counter64(TypeUtilities.toLong(it)) }, + ), + Gauge32( + SnmpNamespace.NodesIds.Gauge32, + NodeIds.UInt64, + org.snmp4j.smi.Gauge32::class.java, + { org.snmp4j.smi.Gauge32(TypeUtilities.toLong(it)) }, + ), + TimeTicks( + SnmpNamespace.NodesIds.TimeTicks, + NodeIds.UInt64, + org.snmp4j.smi.TimeTicks::class.java, + { org.snmp4j.smi.TimeTicks(TypeUtilities.toLong(it)) }, + ); + + companion object { + private val byClass: Map, SnmpDataType> = + entries.associateBy { it.backingVariableClass } + + fun registerAll() { + entries.forEach { it.register() } + } + + fun of(variable: Variable): SnmpDataType { + byClass[variable.javaClass]?.let { + return it + } + return entries.firstOrNull { + it.backingVariableClass.isAssignableFrom(variable.javaClass) + } + ?: throw IllegalArgumentException( + "No SNMP type for variable [${variable.javaClass}] $variable" + ) + } + + fun variableOfType(dataType: SnmpDataType, value: Any?): Variable = dataType.factory(value) + } + + private fun register() { + UaDataTypeNode( + SnmpNamespace.nodeContext, + nodeId, + QualifiedName(SnmpNamespace.namespaceIndex, name), + LocalizedText.english(name), + LocalizedText.english(""), + uint(0), + uint(0), + false, + ) + .apply { + accessRestrictions = AccessRestrictionType.of() + SnmpNamespace.nodeManager.addNode(this) + addReference( + Reference( + nodeId, + NodeIds.HasSubtype, + parentType.expanded(), + Reference.Direction.INVERSE, + ) + ) + } + } +} diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/typing/SnmpDataType.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/typing/SnmpDataType.kt deleted file mode 100644 index 5f86be2d..00000000 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/typing/SnmpDataType.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.mussonindustrial.embr.snmp.typing - -import com.inductiveautomation.ignition.common.TypeUtilities -import org.eclipse.milo.opcua.stack.core.OpcUaDataType -import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId -import org.snmp4j.smi.Address -import org.snmp4j.smi.Counter32 -import org.snmp4j.smi.Counter64 -import org.snmp4j.smi.Gauge32 -import org.snmp4j.smi.Integer32 -import org.snmp4j.smi.IpAddress -import org.snmp4j.smi.OID -import org.snmp4j.smi.OctetString -import org.snmp4j.smi.UnsignedInteger32 -import org.snmp4j.smi.Variable - -enum class SnmpDataType(val uaDataType: NodeId, val variableOfType: (Any?) -> Variable) { - Int32(OpcUaDataType.Int32.nodeId, { Integer32(TypeUtilities.toInteger(it)) }), - UInt32(OpcUaDataType.UInt32.nodeId, { UnsignedInteger32(TypeUtilities.toLong(it)) }), - Gauge32(OpcUaDataType.UInt32.nodeId, { Gauge32(TypeUtilities.toLong(it)) }), - Counter32(OpcUaDataType.UInt64.nodeId, { Counter32(TypeUtilities.toLong(it)) }), - Counter64(OpcUaDataType.UInt64.nodeId, { Counter64(TypeUtilities.toLong(it)) }), - OctetString(OpcUaDataType.String.nodeId, { OctetString(TypeUtilities.toString(it)) }), - Oid(OpcUaDataType.String.nodeId, { OID(TypeUtilities.toString(it)) }), - IpAddress(OpcUaDataType.String.nodeId, { IpAddress(TypeUtilities.toString(it)) }), -} - -val Variable.snmpDataType: SnmpDataType - get() = - when (this) { - is OID -> SnmpDataType.Oid - is Gauge32 -> SnmpDataType.Gauge32 - is Counter32 -> SnmpDataType.Counter32 - is Counter64 -> SnmpDataType.Counter64 - is Integer32 -> SnmpDataType.Int32 - is UnsignedInteger32 -> SnmpDataType.UInt32 - is Address -> SnmpDataType.IpAddress - is OctetString -> SnmpDataType.OctetString - else -> SnmpDataType.OctetString - } From c6b4b25cbe344d73bed9eaee22a4e9f50442266b Mon Sep 17 00:00:00 2001 From: Ben Musson Date: Thu, 5 Feb 2026 00:15:11 -0500 Subject: [PATCH 6/9] add opc ua helpers --- .../embr/snmp/model/ExtendedOid.kt | 7 +- .../snmp/agents/opc/DeviceAddressSpace.kt | 26 ++---- .../snmp/agents/opc/DiagnosticAddressSpace.kt | 63 ++++++------- .../snmp/agents/opc/MethodAddressSpace.kt | 26 ++---- .../agents/opc/ObjectModelAddressSpace.kt | 49 ++++------ .../embr/snmp/agents/opc/OidAddressSpace.kt | 39 +++----- .../agents/opc/types/SnmpAgentDeviceType.kt | 63 +++++++------ .../embr/snmp/opc/types/OidValueType.kt | 35 ++------ .../embr/snmp/opc/types/SnmpDataType.kt | 17 ++-- .../embr/snmp/utils/OpcUaUtils.kt | 89 +++++++++++++++++++ 10 files changed, 208 insertions(+), 206 deletions(-) diff --git a/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ExtendedOid.kt b/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ExtendedOid.kt index a38511bc..ab833306 100644 --- a/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ExtendedOid.kt +++ b/modules/snmp/common/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ExtendedOid.kt @@ -9,6 +9,11 @@ interface ExtendedOid : Oid { val hasSuffix: Boolean get() = suffix != null + + fun toIdentifier(): String { + if (!hasSuffix) return numeric + return "${numeric}/${suffix}" + } } fun String.isOid(): Boolean { @@ -21,7 +26,7 @@ fun String.isOid(): Boolean { } fun String.asExtendedOid(): ExtendedOid { - val parts = this.split("::", limit = 2) + val parts = this.split("/", limit = 2) val maybeOid = parts[0] val suffix = parts.getOrNull(1)?.takeIf { it.isNotBlank() } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DeviceAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DeviceAddressSpace.kt index 1a134bb9..9b716c59 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DeviceAddressSpace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DeviceAddressSpace.kt @@ -1,14 +1,14 @@ package com.mussonindustrial.embr.snmp.agents.opc import com.inductiveautomation.ignition.gateway.opcua.server.api.DeviceContext -import com.mussonindustrial.embr.snmp.agents.opc.types.SnmpAgentDeviceType import com.mussonindustrial.embr.snmp.opc.DeviceContextManagedAddressSpaceFragment import com.mussonindustrial.embr.snmp.opc.SnmpNamespace +import com.mussonindustrial.embr.snmp.utils.addHasTypeDefinition +import com.mussonindustrial.embr.snmp.utils.addNode +import com.mussonindustrial.embr.snmp.utils.addOrganizedBy import com.mussonindustrial.embr.snmp.utils.removeAllNodes -import org.eclipse.milo.opcua.sdk.core.Reference import org.eclipse.milo.opcua.sdk.server.AddressSpaceComposite import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectNode -import org.eclipse.milo.opcua.stack.core.NodeIds import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger @@ -31,23 +31,9 @@ class DeviceAddressSpace(deviceContext: DeviceContext, composite: AddressSpaceCo UInteger.MIN, ) .apply { - nodeManager.addNode(this) - addReference( - Reference( - this.nodeId, - NodeIds.HasTypeDefinition, - SnmpNamespace.NodesIds.SnmpAgentDeviceType.expanded(), - Reference.Direction.FORWARD, - ) - ) - addReference( - Reference( - this.nodeId, - NodeIds.Organizes, - rootNodeId.expanded(), - Reference.Direction.INVERSE, - ) - ) + addNode(nodeManager) + addHasTypeDefinition(SnmpNamespace.NodesIds.SnmpAgentDeviceType.expanded()) + addOrganizedBy(rootNodeId.expanded()) } } } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DiagnosticAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DiagnosticAddressSpace.kt index a1874638..17a01cc4 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DiagnosticAddressSpace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DiagnosticAddressSpace.kt @@ -2,8 +2,10 @@ package com.mussonindustrial.embr.snmp.agents.opc import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice import com.mussonindustrial.embr.snmp.opc.DeviceContextManagedAddressSpaceFragment +import com.mussonindustrial.embr.snmp.utils.addComponentOf +import com.mussonindustrial.embr.snmp.utils.addNode +import com.mussonindustrial.embr.snmp.utils.addPropertyOf import com.mussonindustrial.embr.snmp.utils.removeAllNodes -import org.eclipse.milo.opcua.sdk.core.Reference import org.eclipse.milo.opcua.sdk.server.AddressSpaceComposite import org.eclipse.milo.opcua.sdk.server.nodes.UaNode import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectNode @@ -30,24 +32,18 @@ class DiagnosticAddressSpace(val device: SnmpAgentDevice, composite: AddressSpac fun addNodes() { val folder = UaObjectNode( - nodeContext, - nodeId(root), - qualifiedName(root), - LocalizedText.english(root), - LocalizedText.NULL_VALUE, - UInteger.MIN, - UInteger.MIN, - ) - - nodeManager.addNode(folder) - folder.addReference( - Reference( - folder.nodeId, - NodeIds.HasComponent, - deviceNodeId.expanded(), - Reference.Direction.INVERSE, - ) - ) + nodeContext, + nodeId(root), + qualifiedName(root), + LocalizedText.english(root), + LocalizedText.NULL_VALUE, + UInteger.MIN, + UInteger.MIN, + ) + .apply { + addNode(nodeManager) + addComponentOf(deviceNodeId.expanded()) + } addDiagnosticNode( folder, @@ -115,22 +111,19 @@ class DiagnosticAddressSpace(val device: SnmpAgentDevice, composite: AddressSpac dataType: NodeId, attributeFilter: AttributeFilter, ) { - UaVariableNode.UaVariableNodeBuilder(nodeContext).run { - setNodeId(nodeId("${root}/${name}")) - setBrowseName(qualifiedName(name)) - setDisplayName(LocalizedText.english(name)) - setDataType(dataType) - setTypeDefinition(NodeIds.PropertyType) - addReference( - Reference( - nodeId, - NodeIds.HasProperty, - parent.nodeId.expanded(), - Reference.Direction.INVERSE, - ) + UaVariableNode( + nodeContext, + nodeId("${root}/${name}"), + qualifiedName(name), + LocalizedText.english(name), + LocalizedText.english(name), + UInteger.MIN, + UInteger.MIN, ) - addAttributeFilter(attributeFilter) - buildAndAdd() - } + .apply { + setDataType(dataType) + addPropertyOf(parent.nodeId.expanded()) + filterChain.addLast(attributeFilter) + } } } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/MethodAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/MethodAddressSpace.kt index c03527f4..8fb4d05e 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/MethodAddressSpace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/MethodAddressSpace.kt @@ -4,11 +4,11 @@ import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice import com.mussonindustrial.embr.snmp.agents.opc.nodes.ReadTableMethodNode import com.mussonindustrial.embr.snmp.agents.opc.nodes.WalkMethodNode import com.mussonindustrial.embr.snmp.opc.DeviceContextManagedAddressSpaceFragment +import com.mussonindustrial.embr.snmp.utils.addComponentOf +import com.mussonindustrial.embr.snmp.utils.addNode import com.mussonindustrial.embr.snmp.utils.removeAllNodes -import org.eclipse.milo.opcua.sdk.core.Reference import org.eclipse.milo.opcua.sdk.server.AddressSpaceComposite import org.eclipse.milo.opcua.sdk.server.Lifecycle -import org.eclipse.milo.opcua.stack.core.NodeIds class MethodAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceComposite) : DeviceContextManagedAddressSpaceFragment(device.context.deviceContext, composite), Lifecycle { @@ -20,26 +20,12 @@ class MethodAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCom fun addNodes() { ReadTableMethodNode(nodeContext, nodeId("ReadTable"), device).apply { - nodeManager.addNode(this) - this.addReference( - Reference( - this.nodeId, - NodeIds.HasComponent, - deviceNodeId.expanded(), - Reference.Direction.INVERSE, - ) - ) + addNode(nodeManager) + addComponentOf(deviceNodeId.expanded()) } WalkMethodNode(nodeContext, nodeId("Walk"), device).apply { - nodeManager.addNode(this) - this.addReference( - Reference( - this.nodeId, - NodeIds.HasComponent, - deviceNodeId.expanded(), - Reference.Direction.INVERSE, - ) - ) + addNode(nodeManager) + addComponentOf(deviceNodeId.expanded()) } } } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/ObjectModelAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/ObjectModelAddressSpace.kt index 41a64b6c..1f6f97b6 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/ObjectModelAddressSpace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/ObjectModelAddressSpace.kt @@ -2,11 +2,13 @@ package com.mussonindustrial.embr.snmp.agents.opc import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice import com.mussonindustrial.embr.snmp.opc.DeviceContextManagedAddressSpaceFragment +import com.mussonindustrial.embr.snmp.utils.addComponentOf +import com.mussonindustrial.embr.snmp.utils.addNode +import com.mussonindustrial.embr.snmp.utils.addOrganizedBy +import com.mussonindustrial.embr.snmp.utils.organizes import com.mussonindustrial.embr.snmp.utils.removeAllNodes -import org.eclipse.milo.opcua.sdk.core.Reference import org.eclipse.milo.opcua.sdk.server.* import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode -import org.eclipse.milo.opcua.stack.core.NodeIds import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId import org.eclipse.milo.opcua.stack.core.types.structured.ViewDescription @@ -23,37 +25,25 @@ class ObjectModelAddressSpace(val device: SnmpAgentDevice, composite: AddressSpa } fun addNodes() { - val folder = - UaFolderNode( - nodeContext, - nodeId(root), - qualifiedName(root), - LocalizedText.english(root), - ) - nodeManager.addNode(folder) - - folder.addReference( - Reference( - folder.nodeId, - NodeIds.HasComponent, - deviceNodeId.expanded(), - Reference.Direction.INVERSE, - ) - ) - - addObjectsFolder(folder, "Numeric") + UaFolderNode(nodeContext, nodeId(root), qualifiedName(root), LocalizedText.english(root)) + .apply { + addNode(nodeManager) + addComponentOf(deviceNodeId.expanded()) + addObjectsFolder(this, "Numeric") + } } fun addObjectsFolder(folder: UaFolderNode, name: String) { - val node = - UaFolderNode( + UaFolderNode( nodeContext, nodeId("${root}/${name}"), qualifiedName(name), LocalizedText.english(name), ) - nodeManager.addNode(node) - folder.addOrganizes(node) + .apply { + addNode(nodeManager) + addOrganizedBy(folder.nodeId.expanded()) + } } override fun browse( @@ -67,14 +57,7 @@ class ObjectModelAddressSpace(val device: SnmpAgentDevice, composite: AddressSpa when (nodeId) { nodeId("Objects/Numeric") -> model.getDescriptors(model.oids).forEach { descriptor -> - references.add( - Reference( - nodeId, - NodeIds.Organizes, - nodeId(descriptor.oid.numeric).expanded(), - Reference.Direction.FORWARD, - ) - ) + references += nodeId.organizes(nodeId(descriptor.oid.numeric).expanded()) } } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt index 077d6cbe..4f074c91 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt @@ -5,9 +5,14 @@ import com.mussonindustrial.embr.snmp.agents.opc.nodes.DynamicObjectSuffixNode import com.mussonindustrial.embr.snmp.model.ExtendedOid import com.mussonindustrial.embr.snmp.model.ObjectModel import com.mussonindustrial.embr.snmp.model.OidValue +import com.mussonindustrial.embr.snmp.model.Snmp4jExtendedOid import com.mussonindustrial.embr.snmp.model.asExtendedOid import com.mussonindustrial.embr.snmp.model.isOid import com.mussonindustrial.embr.snmp.opc.DeviceContextManagedAddressSpaceFragment +import com.mussonindustrial.embr.snmp.utils.hasProperty +import com.mussonindustrial.embr.snmp.utils.hasTypeDefinition +import com.mussonindustrial.embr.snmp.utils.organizedBy +import com.mussonindustrial.embr.snmp.utils.propertyOf import kotlin.jvm.optionals.getOrNull import org.eclipse.milo.opcua.sdk.core.AccessLevel import org.eclipse.milo.opcua.sdk.core.Reference @@ -229,41 +234,19 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos fun browseDirect(oid: ExtendedOid, nodeId: NodeId): List { val references = mutableListOf() - references += - Reference( - nodeId, - NodeIds.Organizes, - nodeId("Objects/Numeric").expanded(), - Reference.Direction.INVERSE, - ) + references += nodeId.organizedBy(nodeId("Objects/Numeric").expanded()) + DynamicObjectSuffixNode.ALL.forEach { suffix -> - references += - Reference( - nodeId, - NodeIds.HasProperty, - nodeId("${oid.numeric}::${suffix.name}").expanded(), - Reference.Direction.FORWARD, - ) + val extendedOid = Snmp4jExtendedOid(oid.numeric, suffix.name) + references += nodeId.hasProperty(nodeId(extendedOid.toIdentifier()).expanded()) } return references } fun browseSuffix(oid: ExtendedOid, nodeId: NodeId): List { val references = mutableListOf() - references += - Reference( - nodeId, - NodeIds.HasTypeDefinition, - NodeIds.PropertyType.expanded(), - Reference.Direction.FORWARD, - ) - references += - Reference( - nodeId, - NodeIds.HasProperty, - nodeId(oid.numeric).expanded(), - Reference.Direction.INVERSE, - ) + references += nodeId.hasTypeDefinition(NodeIds.PropertyType.expanded()) + references += nodeId.propertyOf(nodeId(oid.numeric).expanded()) return references } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/types/SnmpAgentDeviceType.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/types/SnmpAgentDeviceType.kt index 382f164f..93b0dddc 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/types/SnmpAgentDeviceType.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/types/SnmpAgentDeviceType.kt @@ -1,7 +1,12 @@ package com.mussonindustrial.embr.snmp.agents.opc.types import com.mussonindustrial.embr.snmp.opc.SnmpNamespace -import org.eclipse.milo.opcua.sdk.core.Reference +import com.mussonindustrial.embr.snmp.utils.addComponentOf +import com.mussonindustrial.embr.snmp.utils.addModellingRule +import com.mussonindustrial.embr.snmp.utils.addNode +import com.mussonindustrial.embr.snmp.utils.addOrganizedBy +import com.mussonindustrial.embr.snmp.utils.addSubtypeOf +import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode import org.eclipse.milo.opcua.sdk.server.nodes.UaMethodNode import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectTypeNode import org.eclipse.milo.opcua.stack.core.NodeIds @@ -16,9 +21,9 @@ object SnmpAgentDeviceType { private fun method(nodeId: NodeId, browseName: String, parent: NodeId) = UaMethodNode( - SnmpNamespace.Companion.nodeContext, + SnmpNamespace.nodeContext, nodeId, - SnmpNamespace.Companion.qualifiedName(browseName), + SnmpNamespace.qualifiedName(browseName), LocalizedText.english(browseName), LocalizedText.english(""), Unsigned.uint(0), @@ -27,32 +32,19 @@ object SnmpAgentDeviceType { true, ) .apply { + addNode(SnmpNamespace.nodeManager) + addComponentOf(parent.expanded()) + addModellingRule(NodeIds.ModellingRule_Mandatory.expanded()) + accessRestrictions = AccessRestrictionType.of() - SnmpNamespace.Companion.nodeManager.addNode(this) - addReference( - Reference( - nodeId, - NodeIds.HasComponent, - parent.expanded(), - Reference.Direction.INVERSE, - ) - ) - addReference( - Reference( - nodeId, - NodeIds.HasModellingRule, - NodeIds.ModellingRule_Mandatory.expanded(), - Reference.Direction.FORWARD, - ) - ) } fun register() { val type = UaObjectTypeNode( - SnmpNamespace.Companion.nodeContext, - SnmpNamespace.Companion.NodesIds.SnmpAgentDeviceType, - SnmpNamespace.Companion.qualifiedName(BROWSE_NAME), + SnmpNamespace.nodeContext, + SnmpNamespace.NodesIds.SnmpAgentDeviceType, + SnmpNamespace.qualifiedName(BROWSE_NAME), LocalizedText.english(BROWSE_NAME), LocalizedText.english(""), Unsigned.uint(0), @@ -60,18 +52,25 @@ object SnmpAgentDeviceType { false, ) .apply { + addNode(SnmpNamespace.nodeManager) + addSubtypeOf(NodeIds.BaseObjectType.expanded()) + accessRestrictions = AccessRestrictionType.of() - SnmpNamespace.Companion.nodeManager.addNode(this) - addReference( - Reference( - nodeId, - NodeIds.HasSubtype, - NodeIds.BaseObjectType.expanded(), - Reference.Direction.INVERSE, - ) - ) } + UaFolderNode( + SnmpNamespace.nodeContext, + SnmpNamespace.nodeId("${BROWSE_NAME}.Objects"), + SnmpNamespace.qualifiedName("Objects"), + LocalizedText.english(""), + ) + .apply { + addNode(SnmpNamespace.nodeManager) + addOrganizedBy(type.nodeId.expanded()) + + accessRestrictions = AccessRestrictionType.of() + } + method(SnmpNamespace.Companion.NodesIds.SnmpAgentDeviceType_Walk, "Walk", type.nodeId) method( SnmpNamespace.Companion.NodesIds.SnmpAgentDeviceType_ReadTable, diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/OidValueType.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/OidValueType.kt index 7e470c13..ef8d7426 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/OidValueType.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/OidValueType.kt @@ -4,7 +4,10 @@ import com.google.common.base.MoreObjects import com.google.common.base.Objects import com.mussonindustrial.embr.snmp.model.OidValue import com.mussonindustrial.embr.snmp.opc.SnmpNamespace -import org.eclipse.milo.opcua.sdk.core.Reference +import com.mussonindustrial.embr.snmp.utils.addHasEncoding +import com.mussonindustrial.embr.snmp.utils.addHasTypeDefinition +import com.mussonindustrial.embr.snmp.utils.addNode +import com.mussonindustrial.embr.snmp.utils.addSubtypeOf import org.eclipse.milo.opcua.sdk.core.ValueRanks import org.eclipse.milo.opcua.sdk.server.nodes.UaDataTypeNode import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectNode @@ -100,14 +103,7 @@ class OidValueType(val oid: String, val value: Variant) : UaStructuredType { Unsigned.uint(0), ) .apply { - addReference( - Reference( - nodeId, - NodeIds.HasTypeDefinition, - NodeIds.DataTypeEncodingType.expanded(), - Reference.Direction.FORWARD, - ) - ) + addHasTypeDefinition(NodeIds.DataTypeEncodingType.expanded()) accessRestrictions = AccessRestrictionType.of() SnmpNamespace.nodeManager.addNode(this) } @@ -123,22 +119,10 @@ class OidValueType(val oid: String, val value: Variant) : UaStructuredType { false, ) .apply { - addReference( - Reference( - nodeId, - NodeIds.HasSubtype, - NodeIds.Structure.expanded(), - Reference.Direction.INVERSE, - ) - ) - addReference( - Reference( - nodeId, - NodeIds.HasEncoding, - binaryEncodingNodeId.expanded(), - Reference.Direction.FORWARD, - ) - ) + addNode(SnmpNamespace.nodeManager) + addSubtypeOf(NodeIds.Structure.expanded()) + addHasEncoding(binaryEncodingNodeId.expanded()) + accessRestrictions = AccessRestrictionType.of() dataTypeDefinition = StructureDefinition( @@ -167,7 +151,6 @@ class OidValueType(val oid: String, val value: Variant) : UaStructuredType { ), ) - SnmpNamespace.nodeManager.addNode(this) SnmpNamespace.nodeContext.server.staticDataTypeManager.registerType( nodeId, Codec(), diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/SnmpDataType.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/SnmpDataType.kt index 95228aa4..f087b76f 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/SnmpDataType.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/SnmpDataType.kt @@ -2,7 +2,8 @@ package com.mussonindustrial.embr.snmp.opc.types import com.inductiveautomation.ignition.common.TypeUtilities import com.mussonindustrial.embr.snmp.opc.SnmpNamespace -import org.eclipse.milo.opcua.sdk.core.Reference +import com.mussonindustrial.embr.snmp.utils.addNode +import com.mussonindustrial.embr.snmp.utils.addSubtypeOf import org.eclipse.milo.opcua.sdk.server.nodes.UaDataTypeNode import org.eclipse.milo.opcua.stack.core.NodeIds import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText @@ -40,7 +41,7 @@ enum class SnmpDataType( SnmpNamespace.NodesIds.OctetString, NodeIds.String, org.snmp4j.smi.OctetString::class.java, - { org.snmp4j.smi.OID(TypeUtilities.toString(it)) }, + { org.snmp4j.smi.OctetString(TypeUtilities.toString(it)) }, ), Oid( SnmpNamespace.NodesIds.Oid, @@ -114,16 +115,10 @@ enum class SnmpDataType( false, ) .apply { + addNode(SnmpNamespace.nodeManager) + addSubtypeOf(parentType.expanded()) + accessRestrictions = AccessRestrictionType.of() - SnmpNamespace.nodeManager.addNode(this) - addReference( - Reference( - nodeId, - NodeIds.HasSubtype, - parentType.expanded(), - Reference.Direction.INVERSE, - ) - ) } } } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/OpcUaUtils.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/OpcUaUtils.kt index 402227fe..f7763a13 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/OpcUaUtils.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/OpcUaUtils.kt @@ -1,7 +1,13 @@ package com.mussonindustrial.embr.snmp.utils import com.inductiveautomation.ignition.common.model.values.QualityCode +import org.eclipse.milo.opcua.sdk.core.Reference +import org.eclipse.milo.opcua.sdk.server.NodeManager import org.eclipse.milo.opcua.sdk.server.UaNodeManager +import org.eclipse.milo.opcua.sdk.server.nodes.UaNode +import org.eclipse.milo.opcua.stack.core.NodeIds +import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId +import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode fun UaNodeManager.removeAllNodes() { @@ -15,3 +21,86 @@ fun StatusCode.toQualityCode(): QualityCode { QualityCode.Bad } } + +fun UaNode.addReference( + sourceNodeId: NodeId, + referenceTypeId: NodeId, + targetNodeId: ExpandedNodeId, + direction: Reference.Direction, +) { + addReference(Reference(sourceNodeId, referenceTypeId, targetNodeId, direction)) +} + +fun UaNode.addNode(nodeManager: NodeManager) { + nodeManager.addNode(this) +} + +fun NodeId.componentOf(targetNodeId: ExpandedNodeId) = + Reference(this, NodeIds.HasComponent, targetNodeId, Reference.Direction.INVERSE) + +fun UaNode.addComponentOf(targetNodeId: ExpandedNodeId) { + addReference(nodeId.componentOf(targetNodeId)) +} + +fun NodeId.hasEncoding(targetNodeId: ExpandedNodeId) = + Reference(this, NodeIds.HasEncoding, targetNodeId, Reference.Direction.FORWARD) + +fun UaNode.addHasEncoding(targetNodeId: ExpandedNodeId) { + addReference(nodeId.hasEncoding(targetNodeId)) +} + +fun NodeId.modellingRule(targetNodeId: ExpandedNodeId) = + Reference(this, NodeIds.HasModellingRule, targetNodeId, Reference.Direction.FORWARD) + +fun UaNode.addModellingRule(targetNodeId: ExpandedNodeId) { + addReference(nodeId.modellingRule(targetNodeId)) +} + +fun NodeId.organizedBy(targetNodeId: ExpandedNodeId) = + Reference(this, NodeIds.Organizes, targetNodeId, Reference.Direction.INVERSE) + +fun UaNode.addOrganizedBy(targetNodeId: ExpandedNodeId) { + addReference(nodeId.organizedBy(targetNodeId)) +} + +fun NodeId.organizes(targetNodeId: ExpandedNodeId) = + Reference(this, NodeIds.Organizes, targetNodeId, Reference.Direction.FORWARD) + +fun UaNode.addOrganizes(targetNodeId: ExpandedNodeId) { + addReference(nodeId.organizes(targetNodeId)) +} + +fun NodeId.hasProperty(targetNodeId: ExpandedNodeId) = + Reference(this, NodeIds.HasProperty, targetNodeId, Reference.Direction.FORWARD) + +fun UaNode.addHasProperty(targetNodeId: ExpandedNodeId) { + addReference(nodeId.hasProperty(targetNodeId)) +} + +fun NodeId.propertyOf(targetNodeId: ExpandedNodeId) = + Reference(this, NodeIds.HasProperty, targetNodeId, Reference.Direction.INVERSE) + +fun UaNode.addPropertyOf(targetNodeId: ExpandedNodeId) { + addReference(nodeId.propertyOf(targetNodeId)) +} + +fun NodeId.hasSubtype(targetNodeId: ExpandedNodeId) = + Reference(this, NodeIds.HasSubtype, targetNodeId, Reference.Direction.FORWARD) + +fun UaNode.addHasSubtype(targetNodeId: ExpandedNodeId) { + addReference(nodeId.hasSubtype(targetNodeId)) +} + +fun NodeId.subTypeOf(targetNodeId: ExpandedNodeId) = + Reference(this, NodeIds.HasSubtype, targetNodeId, Reference.Direction.INVERSE) + +fun UaNode.addSubtypeOf(targetNodeId: ExpandedNodeId) { + addReference(nodeId.subTypeOf(targetNodeId)) +} + +fun NodeId.hasTypeDefinition(targetNodeId: ExpandedNodeId) = + Reference(this, NodeIds.HasTypeDefinition, targetNodeId, Reference.Direction.FORWARD) + +fun UaNode.addHasTypeDefinition(targetNodeId: ExpandedNodeId) { + addReference(nodeId.hasTypeDefinition(targetNodeId)) +} From c080cdf62ff805a19f955c29fe5c8474c54305b1 Mon Sep 17 00:00:00 2001 From: Ben Musson Date: Thu, 5 Feb 2026 01:47:56 -0500 Subject: [PATCH 7/9] basic agent profiling --- .../snmp/agents/devices/SnmpAgentDevice.kt | 1 + .../agents/devices/SnmpAgentDeviceImpl.kt | 98 +++++++++++++++++-- .../snmp/agents/devices/SnmpAgentProfile.kt | 6 ++ .../snmp/agents/opc/DiagnosticAddressSpace.kt | 32 +++--- .../embr/snmp/agents/opc/OidAddressSpace.kt | 8 +- ...tSuffixNode.kt => DescriptorSuffixNode.kt} | 17 +++- .../embr/snmp/model/ConcurrentObjectModel.kt | 20 +++- .../embr/snmp/model/ObjectModel.kt | 6 +- .../embr/snmp/utils/SnmpUtils.kt | 34 ------- 9 files changed, 156 insertions(+), 66 deletions(-) create mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentProfile.kt rename modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/{DynamicObjectSuffixNode.kt => DescriptorSuffixNode.kt} (73%) diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDevice.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDevice.kt index 45d2416b..c519eca3 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDevice.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDevice.kt @@ -15,6 +15,7 @@ interface SnmpAgentDevice : AddressSpaceFragment, Device { val context: SnmpAgentContext<*> val status: Status val model: ObjectModel + val profile: SnmpAgentProfile fun read(reads: List): List> diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDeviceImpl.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDeviceImpl.kt index 122a701e..208ed420 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDeviceImpl.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDeviceImpl.kt @@ -11,13 +11,13 @@ import com.mussonindustrial.embr.snmp.agents.opc.ObjectModelAddressSpace import com.mussonindustrial.embr.snmp.agents.opc.OidAddressSpace import com.mussonindustrial.embr.snmp.model.BasicOidValue import com.mussonindustrial.embr.snmp.model.ConcurrentObjectModel +import com.mussonindustrial.embr.snmp.model.ObjectModel import com.mussonindustrial.embr.snmp.model.Oid import com.mussonindustrial.embr.snmp.model.OidValue import com.mussonindustrial.embr.snmp.model.Snmp4jOid import com.mussonindustrial.embr.snmp.model.SnmpCommunicationError import com.mussonindustrial.embr.snmp.model.toOid import com.mussonindustrial.embr.snmp.model.toSnmp4j -import com.mussonindustrial.embr.snmp.utils.createSizeBoundedPDUs import java.util.concurrent.TimeUnit import kotlin.collections.map import org.eclipse.milo.opcua.sdk.server.AddressSpaceComposite @@ -27,8 +27,10 @@ import org.eclipse.milo.opcua.stack.core.StatusCodes import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode import org.snmp4j.PDU +import org.snmp4j.mp.SnmpConstants import org.snmp4j.smi.Variable import org.snmp4j.smi.VariableBinding +import org.snmp4j.util.PDUFactory import org.snmp4j.util.TableUtils import org.snmp4j.util.TreeEvent import org.snmp4j.util.TreeListener @@ -45,6 +47,7 @@ class SnmpAgentDeviceImpl(override val context: SnmpAgentCo private set override val model = ConcurrentObjectModel(this) + override val profile = SnmpAgentProfile() val healthcheck = Healthcheck() @@ -68,6 +71,10 @@ class SnmpAgentDeviceImpl(override val context: SnmpAgentCo onDataItemsCreated( context.deviceContext.subscriptionModel.getDataItems(context.deviceContext.name) ) + discoverMaxPduSize().apply { + profile.maxResponsePduSize = this + profile.maxRequestPduSize = this + } } } @@ -130,10 +137,7 @@ class SnmpAgentDeviceImpl(override val context: SnmpAgentCo while (remaining.isNotEmpty()) { val pdus = - context.readTarget.createSizeBoundedPDUs( - context.pduFactory, - remaining.flatMap { it.value.map { oid -> VariableBinding(oid.toSnmp4j()) } }, - ) { + createSizeBoundedPDUs(context.pduFactory, remaining.keys.toList()) { type = PDU.GET } @@ -173,7 +177,7 @@ class SnmpAgentDeviceImpl(override val context: SnmpAgentCo } } } catch (e: Exception) { - logger.warn("GET failed with exception", e) + logger.warn("GET failed with exception: ${e.message}", e) return reads.map { BasicOidValue(it, SnmpCommunicationError) } } } @@ -336,4 +340,86 @@ class SnmpAgentDeviceImpl(override val context: SnmpAgentCo } } } + + private fun sendTestPdu(targetSize: Int): PDU { + val pdu = PDU().apply { type = PDU.GET } + + while (pdu.berLength < targetSize) { + pdu.add(VariableBinding(SnmpConstants.sysObjectID)) + } + + pdu.trim() + + return context.snmp.send(pdu, context.readTarget).response + } + + private fun discoverMaxPduSize(min: Int = 256, max: Int = 65535): Int { + var low = min + var high = max + var best = min + + while (low <= high) { + val candidate = (low + high) / 2 + + val response = + try { + sendTestPdu(candidate) + } catch (_: Exception) { + high = candidate - 1 + continue + } + + if (response.errorStatus == PDU.tooBig) { + high = candidate - 1 + continue + } + + best = candidate + low = candidate + 1 + } + + return best + } + + private fun createSizeBoundedPDUs( + pduFactory: PDUFactory, + reads: List, + configure: PDU.() -> Unit = {}, + ): List { + + val descriptors = model.getDescriptors(reads) + val pdus = mutableListOf() + + var pdu = pduFactory.createPDU(context.readTarget).apply(configure) + var expectedResponseSize = 0 + + fun startNewPdu() { + pdu.trim() + pdus += pdu + pdu = pduFactory.createPDU(context.readTarget).apply(configure) + expectedResponseSize = 0 + } + + reads.zip(descriptors).forEach { (oid, descriptor) -> + val binding = VariableBinding(oid.toSnmp4j()) + val valueSize = (descriptor as? ObjectModel.ValueDescriptor)?.expectedSize ?: 0 + + val wouldOverflow = + pdu.berLength > profile.maxRequestPduSize || + expectedResponseSize + valueSize > profile.maxResponsePduSize + + if (wouldOverflow && pdu.size() > 0) { + startNewPdu() + } + + pdu.add(binding) + expectedResponseSize += valueSize + } + + if (pdu.size() > 0) { + pdus += pdu + } + + return pdus + } } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentProfile.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentProfile.kt new file mode 100644 index 00000000..19ce60c0 --- /dev/null +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentProfile.kt @@ -0,0 +1,6 @@ +package com.mussonindustrial.embr.snmp.agents.devices + +class SnmpAgentProfile { + var maxRequestPduSize: Int = 65535 + var maxResponsePduSize: Int = 65535 +} diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DiagnosticAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DiagnosticAddressSpace.kt index 17a01cc4..f70c0509 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DiagnosticAddressSpace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DiagnosticAddressSpace.kt @@ -7,7 +7,6 @@ import com.mussonindustrial.embr.snmp.utils.addNode import com.mussonindustrial.embr.snmp.utils.addPropertyOf import com.mussonindustrial.embr.snmp.utils.removeAllNodes import org.eclipse.milo.opcua.sdk.server.AddressSpaceComposite -import org.eclipse.milo.opcua.sdk.server.nodes.UaNode import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectNode import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilter @@ -46,7 +45,7 @@ class DiagnosticAddressSpace(val device: SnmpAgentDevice, composite: AddressSpac } addDiagnosticNode( - folder, + folder.nodeId, "Address", NodeIds.String, AttributeFilters.getValue { @@ -54,13 +53,13 @@ class DiagnosticAddressSpace(val device: SnmpAgentDevice, composite: AddressSpac }, ) addDiagnosticNode( - folder, + folder.nodeId, "Status", NodeIds.String, AttributeFilters.getValue { DataValue(Variant(device.status.toString())) }, ) addDiagnosticNode( - folder, + folder.nodeId, "Connected", NodeIds.Boolean, AttributeFilters.getValue { @@ -68,7 +67,7 @@ class DiagnosticAddressSpace(val device: SnmpAgentDevice, composite: AddressSpac }, ) addDiagnosticNode( - folder, + folder.nodeId, "PendingAsyncRequests", NodeIds.UInt32, AttributeFilters.getValue { @@ -76,7 +75,7 @@ class DiagnosticAddressSpace(val device: SnmpAgentDevice, composite: AddressSpac }, ) addDiagnosticNode( - folder, + folder.nodeId, "PendingSyncRequests", NodeIds.UInt32, AttributeFilters.getValue { @@ -84,21 +83,25 @@ class DiagnosticAddressSpace(val device: SnmpAgentDevice, composite: AddressSpac }, ) addDiagnosticNode( - folder, + folder.nodeId, + "MaxResponsePduSize", + NodeIds.UInt32, + AttributeFilters.getValue { DataValue(Variant(device.profile.maxResponsePduSize)) }, + ) + addDiagnosticNode( + folder.nodeId, "MaxRequestPduSize", NodeIds.UInt32, - AttributeFilters.getValue { - DataValue(Variant(device.context.readTarget.maxSizeRequestPDU)) - }, + AttributeFilters.getValue { DataValue(Variant(device.profile.maxRequestPduSize)) }, ) addDiagnosticNode( - folder, + folder.nodeId, "RetryCount", NodeIds.UInt32, AttributeFilters.getValue { DataValue(Variant(device.context.readTarget.retries)) }, ) addDiagnosticNode( - folder, + folder.nodeId, "ObjectModelSize", NodeIds.UInt32, AttributeFilters.getValue { DataValue(Variant(device.model.oids.size)) }, @@ -106,7 +109,7 @@ class DiagnosticAddressSpace(val device: SnmpAgentDevice, composite: AddressSpac } fun addDiagnosticNode( - parent: UaNode, + parent: NodeId, name: String, dataType: NodeId, attributeFilter: AttributeFilter, @@ -121,8 +124,9 @@ class DiagnosticAddressSpace(val device: SnmpAgentDevice, composite: AddressSpac UInteger.MIN, ) .apply { + addNode(nodeManager) setDataType(dataType) - addPropertyOf(parent.nodeId.expanded()) + addPropertyOf(parent.expanded()) filterChain.addLast(attributeFilter) } } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt index 4f074c91..dfa9aad9 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt @@ -1,7 +1,7 @@ package com.mussonindustrial.embr.snmp.agents.opc import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice -import com.mussonindustrial.embr.snmp.agents.opc.nodes.DynamicObjectSuffixNode +import com.mussonindustrial.embr.snmp.agents.opc.nodes.DescriptorSuffixNode import com.mussonindustrial.embr.snmp.model.ExtendedOid import com.mussonindustrial.embr.snmp.model.ObjectModel import com.mussonindustrial.embr.snmp.model.OidValue @@ -159,8 +159,8 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos suffix: String, ): Any? { val suffixNode = - DynamicObjectSuffixNode.from(suffix) ?: throw UaException(StatusCodes.Bad_NodeIdUnknown) - val context = DynamicObjectSuffixNode.Context(device, descriptor) + DescriptorSuffixNode.from(suffix) ?: throw UaException(StatusCodes.Bad_NodeIdUnknown) + val context = DescriptorSuffixNode.Context(device, descriptor) return when (attributeId) { AttributeId.NodeId -> nodeId @@ -236,7 +236,7 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos val references = mutableListOf() references += nodeId.organizedBy(nodeId("Objects/Numeric").expanded()) - DynamicObjectSuffixNode.ALL.forEach { suffix -> + DescriptorSuffixNode.ALL.forEach { suffix -> val extendedOid = Snmp4jExtendedOid(oid.numeric, suffix.name) references += nodeId.hasProperty(nodeId(extendedOid.toIdentifier()).expanded()) } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/DynamicObjectSuffixNode.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/DescriptorSuffixNode.kt similarity index 73% rename from modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/DynamicObjectSuffixNode.kt rename to modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/DescriptorSuffixNode.kt index a0d9a9f6..9b831e94 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/DynamicObjectSuffixNode.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/DescriptorSuffixNode.kt @@ -10,7 +10,7 @@ import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UByte -sealed class DynamicObjectSuffixNode(val name: String) { +sealed class DescriptorSuffixNode(val name: String) { data class Context(val device: SnmpAgentDevice, val descriptor: ObjectModel.Descriptor) @@ -28,7 +28,7 @@ sealed class DynamicObjectSuffixNode(val name: String) { abstract fun value(context: Context): Any? - object DataType : DynamicObjectSuffixNode("DataType") { + object DataType : DescriptorSuffixNode("DataType") { override fun dataType(context: Context): NodeId = OpcUaDataType.String.nodeId override fun valueRank(context: Context) = ValueRank.Scalar.value @@ -37,10 +37,19 @@ sealed class DynamicObjectSuffixNode(val name: String) { (context.descriptor as? ObjectModel.ValueDescriptor)?.snmpDataType?.name } + object ExpectedSize : DescriptorSuffixNode("ExpectedSize") { + override fun dataType(context: Context): NodeId = OpcUaDataType.UInt32.nodeId + + override fun valueRank(context: Context) = ValueRank.Scalar.value + + override fun value(context: Context) = + (context.descriptor as? ObjectModel.ValueDescriptor)?.expectedSize + } + companion object { - val ALL = listOf(DataType) + val ALL = listOf(DataType, ExpectedSize) private val byName = ALL.associateBy { it.name } - fun from(name: String): DynamicObjectSuffixNode? = byName[name] + fun from(name: String): DescriptorSuffixNode? = byName[name] } } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ConcurrentObjectModel.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ConcurrentObjectModel.kt index 41b600eb..1b3b4a74 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ConcurrentObjectModel.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ConcurrentObjectModel.kt @@ -18,9 +18,23 @@ class ConcurrentObjectModel(val device: SnmpAgentDevice) : ObjectModel { val result = toOpcUaValue(value) knownValues[result.oid] = result - descriptors.getOrPut(result.oid) { - ObjectModel.ValueDescriptor(value.oid, SnmpDataType.of(value.value)) - } + val variable = value.value + descriptors + .getOrPut(result.oid) { + ObjectModel.ValueDescriptor( + value.oid, + SnmpDataType.of(variable), + variable.berLength, + ) + } + .apply { + when (this) { + is ObjectModel.ValueDescriptor -> { + if (variable.berLength > expectedSize) expectedSize *= 2 + } + else -> {} + } + } return result } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ObjectModel.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ObjectModel.kt index 52a145b7..0110e604 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ObjectModel.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ObjectModel.kt @@ -65,7 +65,11 @@ interface ObjectModel { class UnknownDescriptor(oid: Oid) : Descriptor(oid) - open class ValueDescriptor(oid: Oid, val snmpDataType: SnmpDataType) : Descriptor(oid) + open class ValueDescriptor( + oid: Oid, + val snmpDataType: SnmpDataType, + var expectedSize: Int = 0, + ) : Descriptor(oid) class TableColumnDescriptor(oid: Oid, val snmpDataType: SnmpDataType) : Descriptor(oid) diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/SnmpUtils.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/SnmpUtils.kt index f7dc07e4..066ac346 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/SnmpUtils.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/SnmpUtils.kt @@ -5,40 +5,6 @@ import com.mussonindustrial.embr.snmp.model.OidValue import com.mussonindustrial.embr.snmp.model.QualifiedOidValue import java.util.Date import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue -import org.snmp4j.PDU -import org.snmp4j.Target -import org.snmp4j.smi.Address -import org.snmp4j.smi.VariableBinding -import org.snmp4j.util.PDUFactory - -fun Target.createSizeBoundedPDUs( - pduFactory: PDUFactory, - bindings: List, - configure: PDU.() -> Unit = {}, -): List { - - val pdus = mutableListOf() - var pdu = pduFactory.createPDU(this).apply { configure(this) } - var count = 0 - - bindings.forEach { binding -> - pdu.add(binding) - count++ - - if (pdu.berLength > maxSizeRequestPDU || count > 50) { - pdu.trim() - pdus.add(pdu) - pdu = pduFactory.createPDU(this).apply { configure(this) } - pdu.add(binding) - count = 1 - } - } - if (pdu.size() > 0) { - pdus.add(pdu) - } - - return pdus -} fun OidValue.toQualifiedValue(): QualifiedOidValue = BasicQualifiedOidValue( From cefb3627b60e0ca96c813997033739278bd3d1bd Mon Sep 17 00:00:00 2001 From: Ben Musson Date: Thu, 5 Feb 2026 02:32:40 -0500 Subject: [PATCH 8/9] Update OidValueType.kt --- .../com/mussonindustrial/embr/snmp/opc/types/OidValueType.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/OidValueType.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/OidValueType.kt index ef8d7426..8d6fbf90 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/OidValueType.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/OidValueType.kt @@ -133,7 +133,7 @@ class OidValueType(val oid: String, val value: Variant) : UaStructuredType { StructureField( "oid", LocalizedText.NULL_VALUE, - NodeIds.String, + SnmpNamespace.NodesIds.Oid, ValueRanks.Scalar, null, SnmpNamespace.nodeContext.server.config.limits.maxStringLength, From 8f691401f2f9ca5e13a312dd641bdc3aab012102 Mon Sep 17 00:00:00 2001 From: Ben Musson Date: Mon, 2 Mar 2026 21:42:34 -0500 Subject: [PATCH 9/9] reorganization --- .../agents/devices/SnmpAgentDeviceImpl.kt | 16 ++--- .../snmp/agents/devices/SnmpAgentProfile.kt | 4 +- .../snmp/agents/opc/DeviceAddressSpace.kt | 8 +-- .../snmp/agents/opc/DiagnosticAddressSpace.kt | 8 +-- .../snmp/agents/opc/MethodAddressSpace.kt | 6 +- .../agents/opc/ObjectModelAddressSpace.kt | 10 +-- .../embr/snmp/agents/opc/OidAddressSpace.kt | 35 ++--------- .../agents/opc/nodes/DescriptorSuffixNode.kt | 63 ++++++++++++++----- .../agents/opc/types/SnmpAgentDeviceType.kt | 21 ++++--- .../embr/snmp/agents/rpc/SnmpAgentRpcImpl.kt | 4 +- .../embr/snmp/model/ConcurrentObjectModel.kt | 33 +++++----- .../SnmpUtils.kt => model/OidValueUtils.kt} | 6 +- .../embr/snmp/opc/OpcUaUtils.kt | 17 +++++ .../OpcUaUtils.kt => opc/ReferenceHelpers.kt} | 17 +---- .../embr/snmp/opc/SnmpNamespace.kt | 25 ++++---- .../embr/snmp/opc/SnmpNamespaceNodeIds.kt | 26 -------- .../embr/snmp/opc/SnmpNodeIds.kt | 28 +++++++++ .../embr/snmp/opc/types/OidValueType.kt | 23 +++---- .../embr/snmp/opc/types/SnmpDataType.kt | 18 +++--- 19 files changed, 197 insertions(+), 171 deletions(-) rename modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/{utils/SnmpUtils.kt => model/OidValueUtils.kt} (59%) create mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/OpcUaUtils.kt rename modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/{utils/OpcUaUtils.kt => opc/ReferenceHelpers.kt} (87%) delete mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNamespaceNodeIds.kt create mode 100644 modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNodeIds.kt diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDeviceImpl.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDeviceImpl.kt index 208ed420..72325df8 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDeviceImpl.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentDeviceImpl.kt @@ -67,13 +67,15 @@ class SnmpAgentDeviceImpl(override val context: SnmpAgentCo lifecycleManager.addLifecycle(oidAddressSpace) lifecycleManager.addLifecycle(healthcheck) lifecycleManager.addStartupTask { - learnObjectModel() - onDataItemsCreated( - context.deviceContext.subscriptionModel.getDataItems(context.deviceContext.name) - ) - discoverMaxPduSize().apply { - profile.maxResponsePduSize = this - profile.maxRequestPduSize = this + context.deviceContext.gatewayContext.executionManager.executeOnce { + discoverMaxPduSize().apply { + profile.maxResponsePduSize = this + profile.maxRequestPduSize = this + } + learnObjectModel() + onDataItemsCreated( + context.deviceContext.subscriptionModel.getDataItems(context.deviceContext.name) + ) } } } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentProfile.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentProfile.kt index 19ce60c0..e98b2bf0 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentProfile.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/devices/SnmpAgentProfile.kt @@ -1,6 +1,6 @@ package com.mussonindustrial.embr.snmp.agents.devices class SnmpAgentProfile { - var maxRequestPduSize: Int = 65535 - var maxResponsePduSize: Int = 65535 + var maxRequestPduSize: Int = 1400 + var maxResponsePduSize: Int = 1400 } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DeviceAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DeviceAddressSpace.kt index 9b716c59..7ce7b79c 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DeviceAddressSpace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DeviceAddressSpace.kt @@ -3,10 +3,10 @@ package com.mussonindustrial.embr.snmp.agents.opc import com.inductiveautomation.ignition.gateway.opcua.server.api.DeviceContext import com.mussonindustrial.embr.snmp.opc.DeviceContextManagedAddressSpaceFragment import com.mussonindustrial.embr.snmp.opc.SnmpNamespace -import com.mussonindustrial.embr.snmp.utils.addHasTypeDefinition -import com.mussonindustrial.embr.snmp.utils.addNode -import com.mussonindustrial.embr.snmp.utils.addOrganizedBy -import com.mussonindustrial.embr.snmp.utils.removeAllNodes +import com.mussonindustrial.embr.snmp.opc.addHasTypeDefinition +import com.mussonindustrial.embr.snmp.opc.addNode +import com.mussonindustrial.embr.snmp.opc.addOrganizedBy +import com.mussonindustrial.embr.snmp.opc.removeAllNodes import org.eclipse.milo.opcua.sdk.server.AddressSpaceComposite import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectNode import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DiagnosticAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DiagnosticAddressSpace.kt index f70c0509..42379b11 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DiagnosticAddressSpace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/DiagnosticAddressSpace.kt @@ -2,10 +2,10 @@ package com.mussonindustrial.embr.snmp.agents.opc import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice import com.mussonindustrial.embr.snmp.opc.DeviceContextManagedAddressSpaceFragment -import com.mussonindustrial.embr.snmp.utils.addComponentOf -import com.mussonindustrial.embr.snmp.utils.addNode -import com.mussonindustrial.embr.snmp.utils.addPropertyOf -import com.mussonindustrial.embr.snmp.utils.removeAllNodes +import com.mussonindustrial.embr.snmp.opc.addComponentOf +import com.mussonindustrial.embr.snmp.opc.addNode +import com.mussonindustrial.embr.snmp.opc.addPropertyOf +import com.mussonindustrial.embr.snmp.opc.removeAllNodes import org.eclipse.milo.opcua.sdk.server.AddressSpaceComposite import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectNode import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/MethodAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/MethodAddressSpace.kt index 8fb4d05e..c864d809 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/MethodAddressSpace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/MethodAddressSpace.kt @@ -4,9 +4,9 @@ import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice import com.mussonindustrial.embr.snmp.agents.opc.nodes.ReadTableMethodNode import com.mussonindustrial.embr.snmp.agents.opc.nodes.WalkMethodNode import com.mussonindustrial.embr.snmp.opc.DeviceContextManagedAddressSpaceFragment -import com.mussonindustrial.embr.snmp.utils.addComponentOf -import com.mussonindustrial.embr.snmp.utils.addNode -import com.mussonindustrial.embr.snmp.utils.removeAllNodes +import com.mussonindustrial.embr.snmp.opc.addComponentOf +import com.mussonindustrial.embr.snmp.opc.addNode +import com.mussonindustrial.embr.snmp.opc.removeAllNodes import org.eclipse.milo.opcua.sdk.server.AddressSpaceComposite import org.eclipse.milo.opcua.sdk.server.Lifecycle diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/ObjectModelAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/ObjectModelAddressSpace.kt index 1f6f97b6..1614959e 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/ObjectModelAddressSpace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/ObjectModelAddressSpace.kt @@ -2,11 +2,11 @@ package com.mussonindustrial.embr.snmp.agents.opc import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice import com.mussonindustrial.embr.snmp.opc.DeviceContextManagedAddressSpaceFragment -import com.mussonindustrial.embr.snmp.utils.addComponentOf -import com.mussonindustrial.embr.snmp.utils.addNode -import com.mussonindustrial.embr.snmp.utils.addOrganizedBy -import com.mussonindustrial.embr.snmp.utils.organizes -import com.mussonindustrial.embr.snmp.utils.removeAllNodes +import com.mussonindustrial.embr.snmp.opc.addComponentOf +import com.mussonindustrial.embr.snmp.opc.addNode +import com.mussonindustrial.embr.snmp.opc.addOrganizedBy +import com.mussonindustrial.embr.snmp.opc.organizes +import com.mussonindustrial.embr.snmp.opc.removeAllNodes import org.eclipse.milo.opcua.sdk.server.* import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt index dfa9aad9..60245670 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/OidAddressSpace.kt @@ -9,10 +9,10 @@ import com.mussonindustrial.embr.snmp.model.Snmp4jExtendedOid import com.mussonindustrial.embr.snmp.model.asExtendedOid import com.mussonindustrial.embr.snmp.model.isOid import com.mussonindustrial.embr.snmp.opc.DeviceContextManagedAddressSpaceFragment -import com.mussonindustrial.embr.snmp.utils.hasProperty -import com.mussonindustrial.embr.snmp.utils.hasTypeDefinition -import com.mussonindustrial.embr.snmp.utils.organizedBy -import com.mussonindustrial.embr.snmp.utils.propertyOf +import com.mussonindustrial.embr.snmp.opc.hasProperty +import com.mussonindustrial.embr.snmp.opc.hasTypeDefinition +import com.mussonindustrial.embr.snmp.opc.organizedBy +import com.mussonindustrial.embr.snmp.opc.propertyOf import kotlin.jvm.optionals.getOrNull import org.eclipse.milo.opcua.sdk.core.AccessLevel import org.eclipse.milo.opcua.sdk.core.Reference @@ -160,31 +160,8 @@ class OidAddressSpace(val device: SnmpAgentDevice, composite: AddressSpaceCompos ): Any? { val suffixNode = DescriptorSuffixNode.from(suffix) ?: throw UaException(StatusCodes.Bad_NodeIdUnknown) - val context = DescriptorSuffixNode.Context(device, descriptor) - - return when (attributeId) { - AttributeId.NodeId -> nodeId - AttributeId.NodeClass -> NodeClass.Variable - AttributeId.BrowseName -> suffixNode.browseName(context) - AttributeId.DisplayName -> suffixNode.displayName(context) - AttributeId.Description -> suffixNode.description(context) - - AttributeId.DataType -> suffixNode.dataType(context) - AttributeId.ValueRank -> suffixNode.valueRank(context) - AttributeId.ArrayDimensions -> null - - AttributeId.AccessLevel, - AttributeId.UserAccessLevel -> suffixNode.accessLevel(context) - - AttributeId.Historizing -> false - AttributeId.Value -> suffixNode.value(context) - - AttributeId.WriteMask, - AttributeId.UserWriteMask -> UInteger.valueOf(0) - - else -> - throw UaException(StatusCodes.Bad_AttributeIdInvalid, "attributeId: $attributeId") - } + val context = DescriptorSuffixNode.Context(nodeId, device, descriptor) + return suffixNode.readAttribute(context, attributeId) } override fun write( diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/DescriptorSuffixNode.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/DescriptorSuffixNode.kt index 9b831e94..e32abf46 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/DescriptorSuffixNode.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/nodes/DescriptorSuffixNode.kt @@ -4,45 +4,80 @@ import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice import com.mussonindustrial.embr.snmp.model.ObjectModel import org.eclipse.milo.opcua.sdk.core.AccessLevel import org.eclipse.milo.opcua.sdk.core.ValueRank +import org.eclipse.milo.opcua.stack.core.AttributeId import org.eclipse.milo.opcua.stack.core.OpcUaDataType +import org.eclipse.milo.opcua.stack.core.StatusCodes +import org.eclipse.milo.opcua.stack.core.UaException import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UByte +import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger +import org.eclipse.milo.opcua.stack.core.types.enumerated.NodeClass sealed class DescriptorSuffixNode(val name: String) { - data class Context(val device: SnmpAgentDevice, val descriptor: ObjectModel.Descriptor) + data class Context( + val nodeId: NodeId, + val device: SnmpAgentDevice, + val descriptor: ObjectModel.Descriptor, + ) - open fun browseName(context: Context): QualifiedName = QualifiedName(0, name) + open fun getBrowseName(context: Context): QualifiedName = QualifiedName(0, name) - open fun displayName(context: Context): LocalizedText = LocalizedText.english(name) + open fun getDisplayName(context: Context): LocalizedText = LocalizedText.english(name) - open fun description(context: Context): LocalizedText = LocalizedText.english("") + open fun getDescription(context: Context): LocalizedText = LocalizedText.english("") - open fun accessLevel(context: Context): UByte? = AccessLevel.toValue(AccessLevel.READ_ONLY) + open fun getAccessLevel(context: Context): UByte? = AccessLevel.toValue(AccessLevel.READ_ONLY) - abstract fun dataType(context: Context): NodeId + abstract fun getDataType(context: Context): NodeId - abstract fun valueRank(context: Context): Int + abstract fun getValueRank(context: Context): Int - abstract fun value(context: Context): Any? + abstract fun getValue(context: Context): Any? + + open fun readAttribute(context: Context, attributeId: AttributeId?): Any? { + return when (attributeId) { + AttributeId.NodeId -> context.nodeId + AttributeId.NodeClass -> NodeClass.Variable + AttributeId.BrowseName -> getBrowseName(context) + AttributeId.DisplayName -> getDisplayName(context) + AttributeId.Description -> getDescription(context) + + AttributeId.DataType -> getDataType(context) + AttributeId.ValueRank -> getValueRank(context) + AttributeId.ArrayDimensions -> null + + AttributeId.AccessLevel, + AttributeId.UserAccessLevel -> getAccessLevel(context) + + AttributeId.Historizing -> false + AttributeId.Value -> getValue(context) + + AttributeId.WriteMask, + AttributeId.UserWriteMask -> UInteger.valueOf(0) + + else -> + throw UaException(StatusCodes.Bad_AttributeIdInvalid, "attributeId: $attributeId") + } + } object DataType : DescriptorSuffixNode("DataType") { - override fun dataType(context: Context): NodeId = OpcUaDataType.String.nodeId + override fun getDataType(context: Context): NodeId = OpcUaDataType.String.nodeId - override fun valueRank(context: Context) = ValueRank.Scalar.value + override fun getValueRank(context: Context) = ValueRank.Scalar.value - override fun value(context: Context) = + override fun getValue(context: Context) = (context.descriptor as? ObjectModel.ValueDescriptor)?.snmpDataType?.name } object ExpectedSize : DescriptorSuffixNode("ExpectedSize") { - override fun dataType(context: Context): NodeId = OpcUaDataType.UInt32.nodeId + override fun getDataType(context: Context): NodeId = OpcUaDataType.UInt32.nodeId - override fun valueRank(context: Context) = ValueRank.Scalar.value + override fun getValueRank(context: Context) = ValueRank.Scalar.value - override fun value(context: Context) = + override fun getValue(context: Context) = (context.descriptor as? ObjectModel.ValueDescriptor)?.expectedSize } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/types/SnmpAgentDeviceType.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/types/SnmpAgentDeviceType.kt index 93b0dddc..c259d20e 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/types/SnmpAgentDeviceType.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/opc/types/SnmpAgentDeviceType.kt @@ -1,13 +1,14 @@ package com.mussonindustrial.embr.snmp.agents.opc.types import com.mussonindustrial.embr.snmp.opc.SnmpNamespace -import com.mussonindustrial.embr.snmp.utils.addComponentOf -import com.mussonindustrial.embr.snmp.utils.addModellingRule -import com.mussonindustrial.embr.snmp.utils.addNode -import com.mussonindustrial.embr.snmp.utils.addOrganizedBy -import com.mussonindustrial.embr.snmp.utils.addSubtypeOf +import com.mussonindustrial.embr.snmp.opc.addComponentOf +import com.mussonindustrial.embr.snmp.opc.addModellingRule +import com.mussonindustrial.embr.snmp.opc.addNode +import com.mussonindustrial.embr.snmp.opc.addOrganizedBy +import com.mussonindustrial.embr.snmp.opc.addSubtypeOf import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode import org.eclipse.milo.opcua.sdk.server.nodes.UaMethodNode +import org.eclipse.milo.opcua.sdk.server.nodes.UaNodeContext import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectTypeNode import org.eclipse.milo.opcua.stack.core.NodeIds import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText @@ -39,10 +40,10 @@ object SnmpAgentDeviceType { accessRestrictions = AccessRestrictionType.of() } - fun register() { + fun register(nodeContext: UaNodeContext) { val type = UaObjectTypeNode( - SnmpNamespace.nodeContext, + nodeContext, SnmpNamespace.NodesIds.SnmpAgentDeviceType, SnmpNamespace.qualifiedName(BROWSE_NAME), LocalizedText.english(BROWSE_NAME), @@ -52,20 +53,20 @@ object SnmpAgentDeviceType { false, ) .apply { - addNode(SnmpNamespace.nodeManager) + addNode(nodeContext.nodeManager) addSubtypeOf(NodeIds.BaseObjectType.expanded()) accessRestrictions = AccessRestrictionType.of() } UaFolderNode( - SnmpNamespace.nodeContext, + nodeContext, SnmpNamespace.nodeId("${BROWSE_NAME}.Objects"), SnmpNamespace.qualifiedName("Objects"), LocalizedText.english(""), ) .apply { - addNode(SnmpNamespace.nodeManager) + addNode(nodeContext.nodeManager) addOrganizedBy(type.nodeId.expanded()) accessRestrictions = AccessRestrictionType.of() diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/rpc/SnmpAgentRpcImpl.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/rpc/SnmpAgentRpcImpl.kt index 209da631..b5b1ea0a 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/rpc/SnmpAgentRpcImpl.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/agents/rpc/SnmpAgentRpcImpl.kt @@ -9,8 +9,8 @@ import com.mussonindustrial.embr.snmp.agents.devices.SnmpAgentDevice import com.mussonindustrial.embr.snmp.model.QualifiedOidValue import com.mussonindustrial.embr.snmp.model.Snmp4jOid import com.mussonindustrial.embr.snmp.model.nullOrExtendedOid -import com.mussonindustrial.embr.snmp.utils.toQualifiedValue -import com.mussonindustrial.embr.snmp.utils.toQualityCode +import com.mussonindustrial.embr.snmp.model.toQualifiedValue +import com.mussonindustrial.embr.snmp.opc.toQualityCode import org.python.core.Py.ValueError @RpcDelegate.RunsOnClient(clientPermissionId = ClientPermissionsConstants.UNRESTRICTED) diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ConcurrentObjectModel.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ConcurrentObjectModel.kt index 1b3b4a74..7b138b8a 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ConcurrentObjectModel.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/ConcurrentObjectModel.kt @@ -19,22 +19,27 @@ class ConcurrentObjectModel(val device: SnmpAgentDevice) : ObjectModel { knownValues[result.oid] = result val variable = value.value - descriptors - .getOrPut(result.oid) { - ObjectModel.ValueDescriptor( - value.oid, - SnmpDataType.of(variable), - variable.berLength, - ) - } - .apply { - when (this) { - is ObjectModel.ValueDescriptor -> { - if (variable.berLength > expectedSize) expectedSize *= 2 + if (variable == SnmpCommunicationError) { + descriptors.getOrPut(result.oid) { ObjectModel.InvalidDescriptor(result.oid) } + } else { + descriptors + .getOrPut(result.oid) { + ObjectModel.ValueDescriptor( + value.oid, + SnmpDataType.of(variable), + variable.berLength, + ) + } + .apply { + when (this) { + is ObjectModel.ValueDescriptor -> { + if (variable.berLength > expectedSize) expectedSize *= 2 + } + else -> {} } - else -> {} } - } + } + return result } diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/SnmpUtils.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/OidValueUtils.kt similarity index 59% rename from modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/SnmpUtils.kt rename to modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/OidValueUtils.kt index 066ac346..bd02c4cb 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/SnmpUtils.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/model/OidValueUtils.kt @@ -1,8 +1,6 @@ -package com.mussonindustrial.embr.snmp.utils +package com.mussonindustrial.embr.snmp.model -import com.mussonindustrial.embr.snmp.model.BasicQualifiedOidValue -import com.mussonindustrial.embr.snmp.model.OidValue -import com.mussonindustrial.embr.snmp.model.QualifiedOidValue +import com.mussonindustrial.embr.snmp.opc.toQualityCode import java.util.Date import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/OpcUaUtils.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/OpcUaUtils.kt new file mode 100644 index 00000000..53706617 --- /dev/null +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/OpcUaUtils.kt @@ -0,0 +1,17 @@ +package com.mussonindustrial.embr.snmp.opc + +import com.inductiveautomation.ignition.common.model.values.QualityCode +import org.eclipse.milo.opcua.sdk.server.UaNodeManager +import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode + +fun UaNodeManager.removeAllNodes() { + this.nodes.forEach { removeNode(it) } +} + +fun StatusCode.toQualityCode(): QualityCode { + return if (this.isGood) { + QualityCode.Good + } else { + QualityCode.Bad + } +} diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/OpcUaUtils.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/ReferenceHelpers.kt similarity index 87% rename from modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/OpcUaUtils.kt rename to modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/ReferenceHelpers.kt index f7763a13..080c9e89 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/utils/OpcUaUtils.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/ReferenceHelpers.kt @@ -1,26 +1,11 @@ -package com.mussonindustrial.embr.snmp.utils +package com.mussonindustrial.embr.snmp.opc -import com.inductiveautomation.ignition.common.model.values.QualityCode import org.eclipse.milo.opcua.sdk.core.Reference import org.eclipse.milo.opcua.sdk.server.NodeManager -import org.eclipse.milo.opcua.sdk.server.UaNodeManager import org.eclipse.milo.opcua.sdk.server.nodes.UaNode import org.eclipse.milo.opcua.stack.core.NodeIds import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId -import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode - -fun UaNodeManager.removeAllNodes() { - this.nodes.forEach { removeNode(it) } -} - -fun StatusCode.toQualityCode(): QualityCode { - return if (this.isGood) { - QualityCode.Good - } else { - QualityCode.Bad - } -} fun UaNode.addReference( sourceNodeId: NodeId, diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNamespace.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNamespace.kt index 769ec93e..f9475524 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNamespace.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNamespace.kt @@ -10,7 +10,6 @@ import org.eclipse.milo.opcua.sdk.server.items.DataItem import org.eclipse.milo.opcua.sdk.server.items.MonitoredItem import org.eclipse.milo.opcua.sdk.server.nodes.UaNodeContext import org.eclipse.milo.opcua.sdk.server.util.SubscriptionModel -import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UShort @@ -21,13 +20,9 @@ class SnmpNamespace(server: OpcUaServer) : ManagedNamespaceWithLifecycle(server, const val NAMESPACE_URI = "urn:mussonindustrial:embr:snmp" lateinit var instance: SnmpNamespace - fun nodeId(id: Long): NodeId = - ExpandedNodeId.of(NAMESPACE_URI, id) - .toNodeIdOrThrow(instance.nodeContext.namespaceTable) + fun nodeId(id: Int) = NodeId(namespaceIndex, id) - fun nodeId(id: String): NodeId = - ExpandedNodeId.of(NAMESPACE_URI, id) - .toNodeIdOrThrow(instance.nodeContext.namespaceTable) + fun nodeId(id: String) = NodeId(namespaceIndex, id) fun qualifiedName(name: String): QualifiedName = QualifiedName(instance.namespaceIndex, name) @@ -41,7 +36,15 @@ class SnmpNamespace(server: OpcUaServer) : ManagedNamespaceWithLifecycle(server, val namespaceIndex: UShort get() = instance.namespaceIndex - val NodesIds by lazy { SnmpNamespaceNodeIds() } + val NodesIds by lazy { + SnmpNodeIds { + when (it) { + is Number -> nodeId(it.toInt()) + is String -> nodeId(it) + else -> throw IllegalArgumentException("Invalid node ID: $it") + } + } + } } private val subscriptionModel = SubscriptionModel(server, this) @@ -53,9 +56,9 @@ class SnmpNamespace(server: OpcUaServer) : ManagedNamespaceWithLifecycle(server, } fun registerTypes() { - SnmpDataType.registerAll() - SnmpAgentDeviceType.register() - OidValueType.register() + SnmpDataType.registerAll(nodeContext) + SnmpAgentDeviceType.register(nodeContext) + OidValueType.register(nodeContext) } override fun onDataItemsCreated(items: List) { diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNamespaceNodeIds.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNamespaceNodeIds.kt deleted file mode 100644 index d4849892..00000000 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNamespaceNodeIds.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.mussonindustrial.embr.snmp.opc - -class SnmpNamespaceNodeIds() { - - val Null = SnmpNamespace.nodeId(0) - val Int32 = SnmpNamespace.nodeId(1) - val UInt32 = SnmpNamespace.nodeId(2) - val OctetString = SnmpNamespace.nodeId(3) - val Oid = SnmpNamespace.nodeId(4) - val IpAddress = SnmpNamespace.nodeId(5) - val Counter32 = SnmpNamespace.nodeId(6) - val Counter64 = SnmpNamespace.nodeId(7) - val Gauge32 = SnmpNamespace.nodeId(8) - val TimeTicks = SnmpNamespace.nodeId(9) - - val SnmpAgentDeviceType = SnmpNamespace.nodeId(1000) - val SnmpAgentDeviceType_ReadTable = SnmpNamespace.nodeId(1010) - val SnmpAgentDeviceType_ReadTable_InputArguments = SnmpNamespace.nodeId(1011) - val SnmpAgentDeviceType_ReadTable_OutputArguments = SnmpNamespace.nodeId(1012) - val SnmpAgentDeviceType_Walk = SnmpNamespace.nodeId(1020) - val SnmpAgentDeviceType_Walk_InputArguments = SnmpNamespace.nodeId(1021) - val SnmpAgentDeviceType_Walk_OutputArguments = SnmpNamespace.nodeId(1022) - - val OidValue = SnmpNamespace.nodeId(2000) - val OidValue_Encoding_DefaultBinary = SnmpNamespace.nodeId(2001) -} diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNodeIds.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNodeIds.kt new file mode 100644 index 00000000..48bf2ab2 --- /dev/null +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/SnmpNodeIds.kt @@ -0,0 +1,28 @@ +package com.mussonindustrial.embr.snmp.opc + +import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId + +class SnmpNodeIds(nodeIdFactory: (Any) -> NodeId) { + + val Null = nodeIdFactory(0) + val Int32 = nodeIdFactory(1) + val UInt32 = nodeIdFactory(2) + val OctetString = nodeIdFactory(3) + val Oid = nodeIdFactory(4) + val IpAddress = nodeIdFactory(5) + val Counter32 = nodeIdFactory(6) + val Counter64 = nodeIdFactory(7) + val Gauge32 = nodeIdFactory(8) + val TimeTicks = nodeIdFactory(9) + + val SnmpAgentDeviceType = nodeIdFactory(1000) + val SnmpAgentDeviceType_ReadTable = nodeIdFactory(1010) + val SnmpAgentDeviceType_ReadTable_InputArguments = nodeIdFactory(1011) + val SnmpAgentDeviceType_ReadTable_OutputArguments = nodeIdFactory(1012) + val SnmpAgentDeviceType_Walk = nodeIdFactory(1020) + val SnmpAgentDeviceType_Walk_InputArguments = nodeIdFactory(1021) + val SnmpAgentDeviceType_Walk_OutputArguments = nodeIdFactory(1022) + + val OidValue = nodeIdFactory(2000) + val OidValue_Encoding_DefaultBinary = nodeIdFactory(2001) +} diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/OidValueType.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/OidValueType.kt index 8d6fbf90..e027cf3f 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/OidValueType.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/OidValueType.kt @@ -4,12 +4,13 @@ import com.google.common.base.MoreObjects import com.google.common.base.Objects import com.mussonindustrial.embr.snmp.model.OidValue import com.mussonindustrial.embr.snmp.opc.SnmpNamespace -import com.mussonindustrial.embr.snmp.utils.addHasEncoding -import com.mussonindustrial.embr.snmp.utils.addHasTypeDefinition -import com.mussonindustrial.embr.snmp.utils.addNode -import com.mussonindustrial.embr.snmp.utils.addSubtypeOf +import com.mussonindustrial.embr.snmp.opc.addHasEncoding +import com.mussonindustrial.embr.snmp.opc.addHasTypeDefinition +import com.mussonindustrial.embr.snmp.opc.addNode +import com.mussonindustrial.embr.snmp.opc.addSubtypeOf import org.eclipse.milo.opcua.sdk.core.ValueRanks import org.eclipse.milo.opcua.sdk.server.nodes.UaDataTypeNode +import org.eclipse.milo.opcua.sdk.server.nodes.UaNodeContext import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectNode import org.eclipse.milo.opcua.stack.core.NodeIds import org.eclipse.milo.opcua.stack.core.UaSerializationException @@ -92,9 +93,9 @@ class OidValueType(val oid: String, val value: Variant) : UaStructuredType { SnmpNamespace.NodesIds.OidValue_Encoding_DefaultBinary } - fun register() { + fun register(nodeContext: UaNodeContext) { UaObjectNode( - SnmpNamespace.nodeContext, + nodeContext, binaryEncodingNodeId, SnmpNamespace.qualifiedName("Default Binary"), LocalizedText.english("Default Binary"), @@ -105,11 +106,11 @@ class OidValueType(val oid: String, val value: Variant) : UaStructuredType { .apply { addHasTypeDefinition(NodeIds.DataTypeEncodingType.expanded()) accessRestrictions = AccessRestrictionType.of() - SnmpNamespace.nodeManager.addNode(this) + nodeContext.nodeManager.addNode(this) } UaDataTypeNode( - SnmpNamespace.nodeContext, + nodeContext, typeNodeId, SnmpNamespace.qualifiedName(BROWSE_NAME), LocalizedText.english(BROWSE_NAME), @@ -119,7 +120,7 @@ class OidValueType(val oid: String, val value: Variant) : UaStructuredType { false, ) .apply { - addNode(SnmpNamespace.nodeManager) + addNode(nodeManager) addSubtypeOf(NodeIds.Structure.expanded()) addHasEncoding(binaryEncodingNodeId.expanded()) @@ -136,7 +137,7 @@ class OidValueType(val oid: String, val value: Variant) : UaStructuredType { SnmpNamespace.NodesIds.Oid, ValueRanks.Scalar, null, - SnmpNamespace.nodeContext.server.config.limits.maxStringLength, + nodeContext.server.config.limits.maxStringLength, false, ), StructureField( @@ -151,7 +152,7 @@ class OidValueType(val oid: String, val value: Variant) : UaStructuredType { ), ) - SnmpNamespace.nodeContext.server.staticDataTypeManager.registerType( + nodeContext.server.staticDataTypeManager.registerType( nodeId, Codec(), binaryEncodingNodeId, diff --git a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/SnmpDataType.kt b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/SnmpDataType.kt index f087b76f..46575e58 100644 --- a/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/SnmpDataType.kt +++ b/modules/snmp/gateway/src/main/kotlin/com/mussonindustrial/embr/snmp/opc/types/SnmpDataType.kt @@ -2,13 +2,13 @@ package com.mussonindustrial.embr.snmp.opc.types import com.inductiveautomation.ignition.common.TypeUtilities import com.mussonindustrial.embr.snmp.opc.SnmpNamespace -import com.mussonindustrial.embr.snmp.utils.addNode -import com.mussonindustrial.embr.snmp.utils.addSubtypeOf +import com.mussonindustrial.embr.snmp.opc.addNode +import com.mussonindustrial.embr.snmp.opc.addSubtypeOf import org.eclipse.milo.opcua.sdk.server.nodes.UaDataTypeNode +import org.eclipse.milo.opcua.sdk.server.nodes.UaNodeContext import org.eclipse.milo.opcua.stack.core.NodeIds import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId -import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint import org.eclipse.milo.opcua.stack.core.types.structured.AccessRestrictionType import org.snmp4j.smi.Variable @@ -84,8 +84,8 @@ enum class SnmpDataType( private val byClass: Map, SnmpDataType> = entries.associateBy { it.backingVariableClass } - fun registerAll() { - entries.forEach { it.register() } + fun registerAll(nodeContext: UaNodeContext) { + entries.forEach { it.register(nodeContext) } } fun of(variable: Variable): SnmpDataType { @@ -103,11 +103,11 @@ enum class SnmpDataType( fun variableOfType(dataType: SnmpDataType, value: Any?): Variable = dataType.factory(value) } - private fun register() { + private fun register(nodeContext: UaNodeContext) { UaDataTypeNode( - SnmpNamespace.nodeContext, + nodeContext, nodeId, - QualifiedName(SnmpNamespace.namespaceIndex, name), + SnmpNamespace.qualifiedName(name), LocalizedText.english(name), LocalizedText.english(""), uint(0), @@ -115,7 +115,7 @@ enum class SnmpDataType( false, ) .apply { - addNode(SnmpNamespace.nodeManager) + addNode(nodeContext.nodeManager) addSubtypeOf(parentType.expanded()) accessRestrictions = AccessRestrictionType.of()