DeviceExtensionPoint::getSamplingMetrics - Help with SamplingGroupStats

I'm trying to implement DeviceExtensionPoint::getSamplingMetrics for the first time.

I think I understand the usage of com.codahale.metrics.Timer and how to calculate the AggregateStats portion of the sampling metrics, but I'm lost on how to track the SamplingGroupStats.

I have an AddressSpaceFragment + SubscriptionModel that issues reads and writes to the device implementation:

class OidAddressSpace(val device: SnmpDeviceImpl<*>) : AddressSpaceFragment, Lifecycle {

    private val filter = SimpleAddressSpaceFilter.create { it.getPath().isOid() }
    private val subscriptionModel = SubscriptionModel(device.context.deviceContext.server, this)

    override fun startup() {
        subscriptionModel.startup()
        device.register(this)
    }

    override fun shutdown() {
        subscriptionModel.shutdown()
        device.unregister(this)
    }

    override fun read(
        context: AddressSpace.ReadContext,
        maxAge: Double,
        timestamps: TimestampsToReturn,
        readValueIds: List<ReadValueId>,
    ): List<DataValue?> {
        val results = readValueIds.map { ReadRequest(it) }
        val toProcess = results.filter { it.result == null }

        val valueReads =
            toProcess.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 }

        val nonValueReads =
            toProcess.filter {
                AttributeId.from(it.readValueId.attributeId).get() != AttributeId.Value
            }
        val nonValueReadResults = readNonValueAttributes(nonValueReads)
        nonValueReadResults.zip(nonValueReads).forEach { (value, result) -> result.result = value }

        return results.map { it.result?.value }
    }

    fun readNonValueAttributes(results: List<ReadRequest>): List<OidReadResult> {
        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()

                        AttributeId.AccessLevel,
                        AttributeId.UserAccessLevel -> AccessLevel.toValue(AccessLevel.READ_WRITE)

                        AttributeId.Value ->
                            throw UaException(
                                StatusCodes.Bad_InternalError,
                                "attributeId: $attributeId",
                            )

                        else ->
                            throw UaException(
                                StatusCodes.Bad_AttributeIdInvalid,
                                "attributeId: $attributeId",
                            )
                    }!!

                OidReadResult(DataValue(Variant(result)))
            } catch (e: UaException) {
                OidReadResult(DataValue(e.statusCode))
            }
        }
    }

    override fun write(
        context: AddressSpace.WriteContext,
        writeValues: List<WriteValue>,
    ): List<StatusCode?> {
        val results = writeValues.map { WriteRequest(it) }

        results.forEach {
            if (it.writeValue.attributeId == null) {
                it.result = StatusCode(StatusCodes.Bad_AttributeIdInvalid).toOidWriteResult()
            }
            if (it.writeValue.indexRange != null && it.writeValue.indexRange.isNotEmpty()) {
                it.result = StatusCode(StatusCodes.Bad_NotImplemented).toOidWriteResult()
            }
            if (AttributeId.from(it.writeValue.attributeId).getOrNull() != AttributeId.Value) {
                it.result = StatusCode(StatusCodes.Bad_WriteNotSupported).toOidWriteResult()
            }
        }

        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 }

        return results.map { it.result?.statusCode }
    }

    override fun browse(
        context: AddressSpace.BrowseContext,
        view: ViewDescription,
        nodeIds: List<NodeId>,
    ): List<AddressSpace.ReferenceResult> {
        return emptyList()
    }

    override fun gather(
        context: AddressSpace.BrowseContext,
        view: ViewDescription,
        nodeId: NodeId,
    ): AddressSpace.ReferenceResult.ReferenceList {
        return AddressSpace.ReferenceResult.ReferenceList(emptyList())
    }

    override fun onDataItemsCreated(items: List<DataItem>) {
        subscriptionModel.onDataItemsCreated(items)
    }

    override fun onDataItemsModified(items: List<DataItem>) {
        subscriptionModel.onDataItemsModified(items)
    }

    override fun onDataItemsDeleted(items: List<DataItem>) {
        subscriptionModel.onDataItemsDeleted(items)
    }

    override fun onMonitoringModeChanged(items: List<MonitoredItem>) {
        subscriptionModel.onMonitoringModeChanged(items)
    }

    override fun getFilter(): AddressSpaceFilter {
        return filter
    }

    fun NodeId.getPath(): String {
        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 WriteRequest(val writeValue: WriteValue) : OidWriteRequest {
        override var result: OidWriteResult? = null
        override val value: DataValue = writeValue.value
        override val oid = OID(writeValue.nodeId.getPath())
    }
}

Tracking the AggregateStats at the device level makes sense, but how do I know what samplingInterval I am issuing requests for? Are there any hints contained within the AddressSpace.ReadContext?

No, you pretty much can't calculate per sampling group stats if you use the naive SubscriptionModel from Milo instead of handle MonitoredItems yourself.

All the drivers with stats implement onDataItemCreated etc... and have custom sampling implementations.

2 Likes

The device details / overload calculations are very much built around how we implement our drivers. It arguably does not belong in the API at all, but it was there historically and I couldn't convince myself to remove it for 8.3. I did shove it off into the extension point instead of being present on the Device interface, though.

Cool, I might play around with a custom SubscriptionModel-type-thing that can expose what I’d need.

Or might just leave this alone and go the easy route of just implementing the Aggregate statistics…