blob: cc778627885da09c3a0e6d282b7c81a334e22ea6 [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 bacgopes
import (
"fmt"
"github.com/pkg/errors"
"github.com/rs/zerolog"
readWriteModel "github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model"
)
type ApplicationRequirements interface {
ApplicationServiceElementRequirements
}
type Application struct {
ApplicationServiceElementContract
Collector
objectName map[string]*LocalDeviceObject
objectIdentifier map[string]*LocalDeviceObject
localDevice *LocalDeviceObject
deviceInfoCache *DeviceInfoCache
controllers map[string]any
helpers map[string]func(pdu PDU) error
_startupDisabled bool
// pass through args
argAseID *int
log zerolog.Logger
}
func NewApplication(localLog zerolog.Logger, localDevice *LocalDeviceObject, opts ...func(*Application)) (*Application, error) {
a := &Application{
log: localLog,
}
for _, opt := range opts {
opt(a)
}
localLog.Debug().
Interface("localDevice", localDevice).
Interface("deviceInfoCache", a.deviceInfoCache).
Interface("aseID", a.argAseID).
Msg("NewApplication")
var err error
a.ApplicationServiceElementContract, err = NewApplicationServiceElement(localLog, func(element *applicationServiceElement) {
element.elementID = a.argAseID
})
if err != nil {
return nil, err
}
// local objects by ID and name
a.objectName = map[string]*LocalDeviceObject{}
a.objectIdentifier = map[string]*LocalDeviceObject{}
// keep track of the local device
if localDevice != nil {
a.localDevice = localDevice
// bind the device object to this application
localDevice.App = a
// local objects by ID and name
a.objectName[localDevice.ObjectName] = localDevice
a.objectName[localDevice.ObjectIdentifier] = localDevice
}
// use the provided cache or make a default one
if a.deviceInfoCache == nil {
a.deviceInfoCache = NewDeviceInfoCache(localLog)
}
// controllers for managing confirmed requests as a client
a.controllers = map[string]any{}
// now set up the rest of the capabilities
a.Collector = Collector{}
// if starting up is enabled, find all the startup functions
if !a._startupDisabled {
for _, fn := range a.CapabilityFunctions("startup") {
localLog.Debug().Interface("fn", fn).Msg("startup fn")
Deferred(fn, NoArgs, NoKWArgs)
}
}
return a, nil
}
func WithApplicationAseID(aseID int) func(*Application) {
return func(a *Application) {
a.argAseID = &aseID
}
}
func WithApplicationDeviceInfoCache(deviceInfoCache *DeviceInfoCache) func(*Application) {
return func(a *Application) {
a.deviceInfoCache = deviceInfoCache
}
}
func (a *Application) GetDeviceInfoCache() *DeviceInfoCache {
return a.deviceInfoCache
}
func (a *Application) String() string {
return fmt.Sprintf("Application(TBD...)") // TODO: fill some info here
}
// AddObject adds an object to the local collection
func (a *Application) AddObject(obj *LocalDeviceObject) error {
a.log.Debug().Stringer("obj", obj).Msg("AddObject")
// extract the object name and identifier
objectName := obj.ObjectName
if objectName == "" {
return errors.New("object name required")
}
objectIdentifier := obj.ObjectIdentifier
if objectIdentifier == "" {
return errors.New("object identifier required")
}
// make sure it hasn't already been defined
if _, ok := a.objectName[objectName]; ok {
return errors.Errorf("already an object with name %s", objectName)
}
if _, ok := a.objectIdentifier[objectIdentifier]; ok {
return errors.Errorf("already an object with identifier %s", objectIdentifier)
}
// now put it in local dictionaries
a.objectName[objectName] = obj
a.objectIdentifier[objectIdentifier] = obj
// append the new object's identifier to the local device's object list if there is one and has an object list property
if a.localDevice != nil {
a.localDevice.ObjectList = append(a.localDevice.ObjectList, objectIdentifier)
}
// let the object know which application stack it belongs to
obj.App = a
return nil
}
// DeleteObject deletes an object from the local collection
func (a *Application) DeleteObject(obj *LocalDeviceObject) error {
a.log.Debug().Stringer("obj", obj).Msg("DeleteObject")
// extract the object name and identifier
objectName := obj.ObjectName
objectIdentifier := obj.ObjectIdentifier
// delete it from the application
delete(a.objectName, objectName)
delete(a.objectIdentifier, objectIdentifier)
// remove the object's identifier from the device's object list if there is one and has an object list property
if a.localDevice != nil {
foundIndex := -1
for i, s := range a.localDevice.ObjectList {
if s == objectIdentifier {
foundIndex = i
}
}
if foundIndex >= 0 {
a.localDevice.ObjectList = append(a.localDevice.ObjectList[0:foundIndex], a.localDevice.ObjectList[foundIndex+1:]...)
}
}
// make sure the object knows it's detached from an application
obj.App = nil
return nil
}
// GetObjectId returns a local object or None.
func (a *Application) GetObjectId(objectId string) *LocalDeviceObject {
return a.objectIdentifier[objectId]
}
// GetObjectName returns a local object or None.
func (a *Application) GetObjectName(objectName string) *LocalDeviceObject {
return a.objectName[objectName]
}
// IterObjects iterates over the objects
func (a *Application) IterObjects() []*LocalDeviceObject {
localDeviceObjects := make([]*LocalDeviceObject, 0, len(a.objectIdentifier))
for _, object := range a.objectIdentifier {
localDeviceObjects = append(localDeviceObjects, object)
}
return localDeviceObjects
}
// GetServicesSupported returns a ServicesSupported bit string based in introspection, look for helper methods that
//
// match confirmed and unconfirmed services.
//
// TODO: match that with readWriteModel.BACnetServicesSupported
func (a *Application) GetServicesSupported() []string {
servicesSupported := make([]string, 0, len(a.helpers))
for key := range a.helpers {
servicesSupported = append(servicesSupported, key)
}
return servicesSupported
}
func (a *Application) Request(args Args, kwargs KWArgs) error {
a.log.Debug().Stringer("Args", args).Stringer("KWArgs", kwargs).Msg("Request")
apdu := args.Get0PDU()
// double-check the input is the right kind of APDU
switch apdu.GetRootMessage().(type) {
case readWriteModel.APDUUnconfirmedRequest, readWriteModel.APDUConfirmedRequest:
default:
return errors.New("APDU expected")
}
return a.ApplicationServiceElementContract.Request(args, kwargs)
}
func (a *Application) Indication(args Args, kwargs KWArgs) error {
a.log.Debug().Stringer("Args", args).Stringer("KWArgs", kwargs).Msg("Indication")
apdu := args.Get0PDU()
// get a helper function
helperName := fmt.Sprintf("Do_%T", apdu)
helperFn := a.helpers[helperName]
a.log.Debug().
Str("helperName", helperName).
Interface("helperFn", helperFn).
Msg("working with helper")
// send back a reject for unrecognized services
if helperFn == nil {
if _, ok := apdu.(readWriteModel.APDUConfirmedRequest); ok {
return errors.Errorf("no function %s", helperName)
}
return nil
}
if err := helperFn(apdu); err != nil {
a.log.Debug().Err(err).Msg("err result")
// TODO: do proper mapping
if err := a.Response(NewArgs(NewPDU(readWriteModel.NewAPDUError(0, readWriteModel.BACnetConfirmedServiceChoice_CREATE_OBJECT, nil, 0))), kwargs); err != nil {
return err
}
}
return nil
}