blob: 46302ef700fe58736c34115b7f1379f4b9ef1a92 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package knxnetip
import (
"context"
"math"
"runtime/debug"
"strconv"
"time"
"github.com/pkg/errors"
"github.com/apache/plc4x/plc4go/pkg/api/values"
driverModel "github.com/apache/plc4x/plc4go/protocols/knxnetip/readwrite/model"
"github.com/apache/plc4x/plc4go/spi/options"
"github.com/apache/plc4x/plc4go/spi/utils"
spiValues "github.com/apache/plc4x/plc4go/spi/values"
)
///////////////////////////////////////////////////////////////////////////////////////////////////////
// KNX Specific Operations used by the driver internally
//
// These functions all provide access to some of the internal KNX operations
// They provide this functionality to other parts of the KNX driver, which are
// not part of the PLC4Go API.
//
// Remarks about these functions:
// They expect the called private functions to handle timeouts, so these will not.
///////////////////////////////////////////////////////////////////////////////////////////////////////
func (m *Connection) ReadGroupAddress(ctx context.Context, groupAddress []byte, datapointType *driverModel.KnxDatapointType) <-chan KnxReadResult {
result := make(chan KnxReadResult, 1)
sendResponse := func(value values.PlcValue, numItems uint8, err error) {
timeout := time.NewTimer(10 * time.Millisecond)
select {
case result <- KnxReadResult{
value: value,
numItems: numItems,
err: err,
}:
if !timeout.Stop() {
<-timeout.C
}
case <-timeout.C:
timeout.Stop()
}
}
go func() {
defer func() {
if err := recover(); err != nil {
m.log.Error().
Str("stack", string(debug.Stack())).
Interface("err", err).
Msg("panic-ed")
}
}()
groupAddressReadResponse, err := m.sendGroupAddressReadRequest(ctx, groupAddress)
if err != nil {
sendResponse(nil, 0, errors.Wrap(err, "error reading group address"))
return
}
var payload []byte
// TODO: maybe groupAddressReadResponse.DataFirstByte can be written as uint 6 so the we wouldn't need to cast
payload = append(payload, byte(groupAddressReadResponse.GetDataFirstByte()))
payload = append(payload, groupAddressReadResponse.GetData()...)
// Parse the response data.
rb := utils.NewReadBufferByteBased(payload)
// If the size of the tag is greater than 6, we have to skip the first byte
if datapointType.DatapointMainType().SizeInBits() > 6 {
_, _ = rb.ReadUint8("datapointType", 8)
}
// Set a default datatype if none is provided
if *datapointType == driverModel.KnxDatapointType_DPT_UNKNOWN {
defaultDatapointType := driverModel.KnxDatapointType_USINT
datapointType = &defaultDatapointType
}
// Parse the value
plcValue, err := driverModel.KnxDatapointParseWithBuffer(context.Background(), rb, *datapointType)
if err != nil {
sendResponse(nil, 0, errors.Wrap(err, "error parsing group address response"))
return
}
// Return the value
sendResponse(plcValue, 1, nil)
}()
return result
}
func (m *Connection) DeviceConnect(ctx context.Context, targetAddress driverModel.KnxAddress) <-chan KnxDeviceConnectResult {
result := make(chan KnxDeviceConnectResult, 1)
sendResponse := func(connection *KnxDeviceConnection, err error) {
timeout := time.NewTimer(10 * time.Millisecond)
select {
case result <- KnxDeviceConnectResult{
connection: connection,
err: err,
}:
if !timeout.Stop() {
<-timeout.C
}
case <-timeout.C:
timeout.Stop()
}
}
go func() {
defer func() {
if err := recover(); err != nil {
m.log.Error().
Str("stack", string(debug.Stack())).
Interface("err", err).
Msg("panic-ed")
}
}()
// If we're already connected, use that connection instead.
if connection, ok := m.DeviceConnections[targetAddress]; ok {
sendResponse(connection, nil)
return
}
// First send a connection request
controlConnectResponse, err := m.sendDeviceConnectionRequest(ctx, targetAddress)
if err != nil {
sendResponse(nil, errors.Wrap(err, "error creating device connection"))
return
}
if controlConnectResponse == nil {
sendResponse(nil, errors.New("error creating device connection"))
return
}
// Create the new connection object.
connection := &KnxDeviceConnection{
counter: 0,
// I was told this value on the knx-forum.
// Seems the max payload is 3 bytes less ...
maxApdu: 0, // This is the default max APDU Size
}
m.DeviceConnections[targetAddress] = connection
// If the connection request was successful, try to read the device-descriptor
deviceDescriptorResponse, err := m.sendDeviceDeviceDescriptorReadRequest(ctx, targetAddress)
if err != nil {
sendResponse(nil, errors.New(
"error reading device descriptor: "+err.Error()))
return
}
// Save the device-descriptor value
deviceDescriptor := uint16(deviceDescriptorResponse.GetData()[0])<<8 | (uint16(deviceDescriptorResponse.GetData()[1]) & 0xFF)
connection.deviceDescriptor = deviceDescriptor
// Last, not least, read the max APDU size
// If we were able to read the max APDU size, then use the minimum of
// the connection APDU size and the device APDU size, otherwise use the
// default APDU Size of 15
// Defined in: 03_05_01 Resources v01.09.03 AS Page 40
deviceApduSize := uint16(15)
propertyValueResponse, err := m.sendDevicePropertyReadRequest(ctx, targetAddress, 0, 56, 1, 1)
if err == nil {
// If the count is 0, then this property doesn't exist or the user has no permission to read it.
// In all other cases we expect the response to contain the value.
if propertyValueResponse.GetCount() > 0 {
dataLength := uint8(len(propertyValueResponse.GetData()))
data := propertyValueResponse.GetData()
ctxForModel := options.GetLoggerContextForModel(ctx, m.log, options.WithPassLoggerToModel(m.passLogToModel))
plcValue, err := driverModel.KnxPropertyParse(ctxForModel, data,
driverModel.KnxInterfaceObjectProperty_PID_DEVICE_MAX_APDULENGTH.PropertyDataType(), dataLength)
// Return the result
if err == nil {
deviceApduSize = plcValue.GetUint16()
} else {
m.log.Debug().Err(err).Msg("Error parsing knx property")
}
}
}
// Set the max apdu size for this connection.
connection.maxApdu = uint16(math.Min(float64(deviceApduSize), 240))
sendResponse(connection, nil)
}()
return result
}
func (m *Connection) DeviceDisconnect(ctx context.Context, targetAddress driverModel.KnxAddress) <-chan KnxDeviceDisconnectResult {
result := make(chan KnxDeviceDisconnectResult, 1)
sendResponse := func(connection *KnxDeviceConnection, err error) {
timeout := time.NewTimer(10 * time.Millisecond)
select {
case result <- KnxDeviceDisconnectResult{
connection: connection,
err: err,
}:
if !timeout.Stop() {
<-timeout.C
}
case <-timeout.C:
timeout.Stop()
}
}
go func() {
defer func() {
if err := recover(); err != nil {
m.log.Error().
Str("stack", string(debug.Stack())).
Interface("err", err).
Msg("panic-ed")
}
}()
if connection, ok := m.DeviceConnections[targetAddress]; ok {
_, err := m.sendDeviceDisconnectionRequest(ctx, targetAddress)
// Remove the connection from the list.
delete(m.DeviceConnections, targetAddress)
sendResponse(connection, err)
} else {
sendResponse(connection, nil)
}
}()
return result
}
func (m *Connection) DeviceAuthenticate(ctx context.Context, targetAddress driverModel.KnxAddress, buildingKey []byte) <-chan KnxDeviceAuthenticateResult {
result := make(chan KnxDeviceAuthenticateResult, 1)
sendResponse := func(err error) {
timeout := time.NewTimer(10 * time.Millisecond)
select {
case result <- KnxDeviceAuthenticateResult{
err: err,
}:
if !timeout.Stop() {
<-timeout.C
}
case <-timeout.C:
timeout.Stop()
}
}
go func() {
defer func() {
if err := recover(); err != nil {
m.log.Error().
Str("stack", string(debug.Stack())).
Interface("err", err).
Msg("panic-ed")
}
}()
// Check if there is already a connection available,
// if not, create a new one.
connection, ok := m.DeviceConnections[targetAddress]
if !ok {
connections := m.DeviceConnect(ctx, targetAddress)
deviceConnectionResult := <-connections
// If we didn't get a connect, abort
if deviceConnectionResult.err != nil {
sendResponse(errors.Wrapf(deviceConnectionResult.err, "error connecting to device at: %s", KnxAddressToString(targetAddress)))
}
}
// If we successfully got a connection, read the property
if connection == nil {
sendResponse(errors.New("unable to connect to device"))
return
}
authenticationLevel := uint8(0)
authenticationResponse, err := m.sendDeviceAuthentication(ctx, targetAddress, authenticationLevel, buildingKey)
if err == nil {
if authenticationResponse.GetLevel() == authenticationLevel {
sendResponse(nil)
} else {
// We authenticated correctly but not to the level requested.
sendResponse(errors.Errorf("got error authenticating at device %s",
KnxAddressToString(targetAddress)))
}
} else {
sendResponse(errors.Errorf("got error authenticating at device %s", KnxAddressToString(targetAddress)))
}
}()
return result
}
func (m *Connection) DeviceReadProperty(ctx context.Context, targetAddress driverModel.KnxAddress, objectId uint8, propertyId uint8, propertyIndex uint16, numElements uint8) <-chan KnxReadResult {
result := make(chan KnxReadResult, 1)
sendResponse := func(value values.PlcValue, numItems uint8, err error) {
timeout := time.NewTimer(10 * time.Millisecond)
select {
case result <- KnxReadResult{
value: value,
numItems: numItems,
err: err,
}:
if !timeout.Stop() {
<-timeout.C
}
case <-timeout.C:
timeout.Stop()
}
}
go func() {
defer func() {
if err := recover(); err != nil {
m.log.Error().
Str("stack", string(debug.Stack())).
Interface("err", err).
Msg("panic-ed")
}
}()
// Check if there is already a connection available,
// if not, create a new one.
connection, ok := m.DeviceConnections[targetAddress]
if !ok {
connections := m.DeviceConnect(ctx, targetAddress)
deviceConnectionResult := <-connections
// If we didn't get a connect, abort
if deviceConnectionResult.err != nil {
sendResponse(nil,
0,
errors.Wrapf(deviceConnectionResult.err, "error connecting to device at: %s", KnxAddressToString(targetAddress)),
)
}
}
// If we successfully got a connection, read the property
if connection == nil {
sendResponse(nil, 0, errors.New("unable to connect to device"))
return
}
propertyValueResponse, err := m.sendDevicePropertyReadRequest(ctx, targetAddress, objectId, propertyId, propertyIndex, numElements)
if err != nil {
sendResponse(nil, 0, err)
return
}
// Find out the type of the property
var objectType *driverModel.KnxInterfaceObjectType
for curObjectType := driverModel.KnxInterfaceObjectType_OT_UNKNOWN; curObjectType <= driverModel.KnxInterfaceObjectType_OT_SUNBLIND_SENSOR_BASIC; curObjectType++ {
if curObjectType.Code() == strconv.Itoa(int(objectId)) {
objectType = &curObjectType
break
}
}
property := driverModel.KnxInterfaceObjectProperty_PID_UNKNOWN
if objectType != nil {
for curProperty := driverModel.KnxInterfaceObjectProperty_PID_UNKNOWN; curProperty <= driverModel.KnxInterfaceObjectProperty_PID_SUNBLIND_SENSOR_BASIC_ENABLE_TOGGLE_MODE; curProperty++ {
if curProperty.PropertyId() == propertyId &&
(curProperty.ObjectType() == driverModel.KnxInterfaceObjectType_OT_GENERAL ||
curProperty.ObjectType() == *objectType) {
property = curProperty
break
}
}
}
dataLength := uint8(len(propertyValueResponse.GetData()))
data := propertyValueResponse.GetData()
ctxForModel := options.GetLoggerContextForModel(ctx, m.log, options.WithPassLoggerToModel(m.passLogToModel))
plcValue, err := driverModel.KnxPropertyParse(ctxForModel, data, property.PropertyDataType(), dataLength)
if err != nil {
sendResponse(nil, 0, err)
} else {
sendResponse(plcValue, 1, err)
}
}()
return result
}
func (m *Connection) DeviceReadPropertyDescriptor(ctx context.Context, targetAddress driverModel.KnxAddress, objectId uint8, propertyId uint8) <-chan KnxReadResult {
result := make(chan KnxReadResult, 1)
sendResponse := func(value values.PlcValue, numItems uint8, err error) {
timeout := time.NewTimer(10 * time.Millisecond)
select {
case result <- KnxReadResult{
value: value,
numItems: numItems,
err: err,
}:
if !timeout.Stop() {
<-timeout.C
}
case <-timeout.C:
timeout.Stop()
}
}
go func() {
defer func() {
if err := recover(); err != nil {
m.log.Error().
Str("stack", string(debug.Stack())).
Interface("err", err).
Msg("panic-ed")
}
}()
// Check if there is already a connection available,
// if not, create a new one.
connection, ok := m.DeviceConnections[targetAddress]
if !ok {
connections := m.DeviceConnect(ctx, targetAddress)
deviceConnectionResult := <-connections
// If we didn't get a connect, abort
if deviceConnectionResult.err != nil {
sendResponse(
nil,
0,
errors.Wrapf(deviceConnectionResult.err, "error connecting to device at: %s", KnxAddressToString(targetAddress)),
)
}
}
if connection == nil {
sendResponse(nil, 0, errors.New("unable to connect to device"))
return
}
// If we successfully got a connection, read the property
propertyDescriptionResponse, err := m.sendDevicePropertyDescriptionReadRequest(ctx, targetAddress, objectId, propertyId)
if err != nil {
sendResponse(nil, 0, err)
return
}
val := map[string]values.PlcValue{}
val["writable"] = spiValues.NewPlcBOOL(propertyDescriptionResponse.GetWriteEnabled())
val["dataType"] = spiValues.NewPlcSTRING(propertyDescriptionResponse.GetPropertyDataType().Name())
val["maxElements"] = spiValues.NewPlcUINT(propertyDescriptionResponse.GetMaxNrOfElements())
val["readLevel"] = spiValues.NewPlcSTRING(propertyDescriptionResponse.GetReadLevel().String())
val["writeLevel"] = spiValues.NewPlcSTRING(propertyDescriptionResponse.GetWriteLevel().String())
str := spiValues.NewPlcStruct(val)
sendResponse(&str, 1, nil)
}()
return result
}
func (m *Connection) DeviceReadMemory(ctx context.Context, targetAddress driverModel.KnxAddress, address uint16, numElements uint8, datapointType *driverModel.KnxDatapointType) <-chan KnxReadResult {
result := make(chan KnxReadResult, 1)
sendResponse := func(value values.PlcValue, numItems uint8, err error) {
timeout := time.NewTimer(10 * time.Millisecond)
select {
case result <- KnxReadResult{
value: value,
numItems: numItems,
err: err,
}:
if !timeout.Stop() {
<-timeout.C
}
case <-timeout.C:
timeout.Stop()
}
}
go func() {
defer func() {
if err := recover(); err != nil {
m.log.Error().
Str("stack", string(debug.Stack())).
Interface("err", err).
Msg("panic-ed")
}
}()
// Set a default datatype, if none is specified
if datapointType == nil {
dpt := driverModel.KnxDatapointType_USINT
datapointType = &dpt
}
// Check if there is already a connection available,
// if not, create a new one.
connection, ok := m.DeviceConnections[targetAddress]
if !ok {
connections := m.DeviceConnect(ctx, targetAddress)
deviceConnectionResult := <-connections
// If we didn't get a connect, abort
if deviceConnectionResult.err != nil {
sendResponse(
nil,
0,
errors.Wrapf(deviceConnectionResult.err, "error connecting to device at: %s", KnxAddressToString(targetAddress)),
)
}
}
if connection == nil {
// TODO: do we need to send a response here
return
}
// If we successfully got a connection, read the property
// Depending on the gateway Max APDU and the device Max APDU, split this up into multiple requests.
// An APDU starts with the last 6 bits of the first data byte containing the count
// followed by the 16-bit address, so these are already used.
elementSize := datapointType.DatapointMainType().SizeInBits() / 8
remainingRequestElements := numElements
curStartingAddress := address
var results []values.PlcValue
for remainingRequestElements > 0 {
// As the maxApdu can change, we have to do this in the loop.
maxNumBytes := uint8(math.Min(float64(connection.maxApdu-3), float64(63)))
maxNumElementsPerRequest := uint8(math.Floor(float64(maxNumBytes / elementSize)))
numElements := uint8(math.Min(float64(remainingRequestElements), float64(maxNumElementsPerRequest)))
numBytes := numElements * uint8(math.Max(float64(1), float64(datapointType.DatapointMainType().SizeInBits()/8)))
memoryReadResponse, err := m.sendDeviceMemoryReadRequest(ctx, targetAddress, curStartingAddress, numBytes)
if err != nil {
// TODO: do we need to send a response here
return
}
// If the number of bytes read is less than expected,
// Update the connection.maxApdu value. This is required
// as some devices seem to be sending back less than the
// number of bytes specified than the maxApdu.
if uint8(len(memoryReadResponse.GetData())) < numBytes {
connection.maxApdu = uint16(len(memoryReadResponse.GetData()) + 3)
}
// Parse the data according to the property type information
rb := utils.NewReadBufferByteBased(memoryReadResponse.GetData())
for rb.HasMore(datapointType.DatapointMainType().SizeInBits()) {
plcValue, err := driverModel.KnxDatapointParseWithBuffer(context.Background(), rb, *datapointType)
// Return the result
if err != nil {
sendResponse(nil, 0, err)
return
}
results = append(results, plcValue)
// Update the counters and addresses.
remainingRequestElements--
curStartingAddress = curStartingAddress + uint16(elementSize)
}
// If there are still remaining bytes, keep them for the next time.
}
if len(results) > 1 {
var plcList values.PlcValue
plcList = spiValues.NewPlcList(results)
sendResponse(plcList, 1, nil)
} else if len(results) == 1 {
sendResponse(results[0], 1, nil)
}
}()
return result
}