Skip to content

Commit

Permalink
feat(plc4go/cbus): introduced sal field
Browse files Browse the repository at this point in the history
  • Loading branch information
sruehl committed Aug 17, 2022
1 parent 5fa2432 commit 2651c29
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 30 deletions.
69 changes: 69 additions & 0 deletions plc4go/internal/cbus/Field.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,22 @@ func NewCALGetstatusField(unitAddress readWriteModel.UnitAddress, parameter read
}
}

// SALField can be used to send SAL commands
type SALField interface {
model.PlcField
GetApplication() readWriteModel.ApplicationIdContainer
GetSALCommand() string
}

func NewSALField(application readWriteModel.ApplicationIdContainer, salCommand string, numElements uint16) SALField {
return &salField{
fieldType: SAL,
application: application,
salCommand: salCommand,
numElements: numElements,
}
}

// SALMonitorField can be used to monitor sal fields
type SALMonitorField interface {
model.PlcField
Expand Down Expand Up @@ -207,6 +223,12 @@ type calGetstatusField struct {
numElements uint16
}

type salField struct {
fieldType FieldType
application readWriteModel.ApplicationIdContainer
salCommand string
numElements uint16
}
type salMonitorField struct {
fieldType FieldType
unitAddress readWriteModel.UnitAddress
Expand Down Expand Up @@ -445,6 +467,53 @@ func (c calGetstatusField) String() string {
return writeBuffer.GetBox().String()
}

func (s salField) GetApplication() readWriteModel.ApplicationIdContainer {
return s.application
}

func (s salField) GetSALCommand() string {
return s.salCommand
}

func (s salField) GetAddressString() string {
return fmt.Sprintf("sal/%s/%s", s.application, s.salCommand)
}

func (s salField) GetTypeName() string {
return s.fieldType.GetName()
}

func (s salField) GetQuantity() uint16 {
return s.numElements
}

func (s salField) Serialize(writeBuffer utils.WriteBuffer) error {
if err := writeBuffer.PushContext(s.fieldType.GetName()); err != nil {
return err
}

if err := s.application.Serialize(writeBuffer); err != nil {
return err
}

if err := writeBuffer.WriteString("salCommand", uint32(len(s.salCommand)*8), "UTF-8", s.salCommand); err != nil {
return err
}

if err := writeBuffer.PopContext(s.fieldType.GetName()); err != nil {
return err
}
return nil
}

func (s salField) String() string {
writeBuffer := utils.NewBoxedWriteBufferWithOptions(true, true)
if err := writeBuffer.WriteSerializable(s); err != nil {
return err.Error()
}
return writeBuffer.GetBox().String()
}

func (s salMonitorField) GetAddressString() string {
// TODO: this is nonsense... fix that
return fmt.Sprintf("%d/%s%s[%d]", s.fieldType, s.unitAddress, s.application, s.numElements)
Expand Down
55 changes: 55 additions & 0 deletions plc4go/internal/cbus/FieldHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package cbus

import (
"encoding/hex"
"fmt"
"github.com/apache/plc4x/plc4go/pkg/api/model"
readWriteModel "github.com/apache/plc4x/plc4go/protocols/cbus/readwrite/model"
"github.com/apache/plc4x/plc4go/spi/utils"
Expand All @@ -38,6 +39,7 @@ const (
CAL_RECALL
CAL_IDENTIFY
CAL_GETSTATUS
SAL
SAL_MONITOR
MMI_STATUS_MONITOR
UNIT_INFO
Expand All @@ -50,6 +52,7 @@ func (i FieldType) GetName() string {
type FieldHandler struct {
statusRequestPattern *regexp.Regexp
calPattern *regexp.Regexp
salPattern *regexp.Regexp
salMonitorPattern *regexp.Regexp
mmiMonitorPattern *regexp.Regexp
unityQuery *regexp.Regexp
Expand All @@ -59,12 +62,47 @@ func NewFieldHandler() FieldHandler {
return FieldHandler{
statusRequestPattern: regexp.MustCompile(`^status/(?P<statusRequestType>(?P<binary>binary)|level=0x(?P<startingGroupAddressLabel>00|20|40|60|80|A0|C0|E0))/(?P<application>.*)`),
calPattern: regexp.MustCompile(`^cal/(?P<unitAddress>.+)/(?P<calType>recall=\[(?P<recallParamNo>\w+), ?(?P<recallCount>\d+)]|identify=(?P<identifyAttribute>\w+)|getstatus=(?P<getstatusParamNo>\w+), ?(?P<getstatusCount>\d+))`),
salPattern: regexp.MustCompile(`^sal/(?P<application>.*)/(?P<salCommand>.*)`),
salMonitorPattern: regexp.MustCompile(`^salmonitor/(?P<unitAddress>.+)/(?P<application>.+)`),
mmiMonitorPattern: regexp.MustCompile(`^mmimonitor/(?P<unitAddress>.+)/(?P<application>.+)`),
unityQuery: regexp.MustCompile(`^info/(?P<unitAddress>.+)/(?P<identifyAttribute>.+)`),
}
}

func ms2s[T fmt.Stringer](t []T) []string {
result := make([]string, len(t))
for i, stringer := range t {
result[i] = stringer.String()
}
return result
}

var PossibleSalCommands = map[readWriteModel.ApplicationId][]string{
readWriteModel.ApplicationId_RESERVED: nil, // TODO: Not yet implemented
readWriteModel.ApplicationId_FREE_USAGE: nil, // TODO: Not yet implemented
readWriteModel.ApplicationId_TEMPERATURE_BROADCAST: ms2s(readWriteModel.TemperatureBroadcastCommandTypeValues),
readWriteModel.ApplicationId_ROOM_CONTROL_SYSTEM: nil, // TODO: Not yet implemented
readWriteModel.ApplicationId_LIGHTING: ms2s(readWriteModel.LightingCommandTypeValues),
readWriteModel.ApplicationId_VENTILATION: ms2s(readWriteModel.LightingCommandTypeValues),
readWriteModel.ApplicationId_IRRIGATION_CONTROL: ms2s(readWriteModel.LightingCommandTypeValues),
readWriteModel.ApplicationId_POOLS_SPAS_PONDS_FOUNTAINS_CONTROL: ms2s(readWriteModel.LightingCommandTypeValues),
readWriteModel.ApplicationId_HEATING: ms2s(readWriteModel.LightingCommandTypeValues),
readWriteModel.ApplicationId_AIR_CONDITIONING: ms2s(readWriteModel.AirConditioningCommandTypeValues),
readWriteModel.ApplicationId_TRIGGER_CONTROL: ms2s(readWriteModel.TriggerControlCommandTypeValues),
readWriteModel.ApplicationId_ENABLE_CONTROL: ms2s(readWriteModel.EnableControlCommandTypeValues),
readWriteModel.ApplicationId_AUDIO_AND_VIDEO: ms2s(readWriteModel.LightingCommandTypeValues),
readWriteModel.ApplicationId_SECURITY: ms2s(readWriteModel.SecurityCommandTypeValues),
readWriteModel.ApplicationId_METERING: ms2s(readWriteModel.MeteringCommandTypeValues),
readWriteModel.ApplicationId_ACCESS_CONTROL: ms2s(readWriteModel.AccessControlCommandTypeValues),
readWriteModel.ApplicationId_CLOCK_AND_TIMEKEEPING: ms2s(readWriteModel.ClockAndTimekeepingCommandTypeValues),
readWriteModel.ApplicationId_TELEPHONY_STATUS_AND_CONTROL: ms2s(readWriteModel.TelephonyCommandTypeValues),
readWriteModel.ApplicationId_MEASUREMENT: ms2s(readWriteModel.MeasurementCommandTypeValues),
readWriteModel.ApplicationId_TESTING: nil, // TODO: Not yet implemented
readWriteModel.ApplicationId_MEDIA_TRANSPORT_CONTROL: ms2s(readWriteModel.MediaTransportControlCommandTypeValues),
readWriteModel.ApplicationId_ERROR_REPORTING: ms2s(readWriteModel.ErrorReportingCommandTypeValues),
readWriteModel.ApplicationId_HVAC_ACTUATOR: ms2s(readWriteModel.LightingCommandTypeValues),
}

func (m FieldHandler) ParseQuery(query string) (model.PlcField, error) {
if match := utils.GetSubgroupMatches(m.statusRequestPattern, query); match != nil {
var startingGroupAddressLabel *byte
Expand Down Expand Up @@ -200,6 +238,23 @@ func (m FieldHandler) ParseQuery(query string) (model.PlcField, error) {
default:
return nil, errors.Errorf("Invalid cal type %s", calTypeArgument)
}
} else if match := utils.GetSubgroupMatches(m.salPattern, query); match != nil {
application, err := applicationIdFromArgument(match["application"])
if err != nil {
return nil, errors.Wrap(err, "Error getting application id from argument")
}
salCommand := match["salCommand"]
if salCommand == "" {
return nil, errors.Wrap(err, "Error getting salCommand from argument")
}
isValid := false
for _, request := range PossibleSalCommands[application.ApplicationId()] {
isValid = isValid || strings.HasPrefix(salCommand, request)
}
if !isValid {
return nil, errors.Errorf("Invalid sal command %s for %s. Allowed requests: %s", salCommand, application, PossibleSalCommands[application.ApplicationId()])
}
panic("Implement me")
} else if match := utils.GetSubgroupMatches(m.salMonitorPattern, query); match != nil {
var unitAddress readWriteModel.UnitAddress
{
Expand Down
113 changes: 88 additions & 25 deletions plc4go/internal/cbus/Subscriber.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ import (
"fmt"
apiModel "github.com/apache/plc4x/plc4go/pkg/api/model"
"github.com/apache/plc4x/plc4go/pkg/api/values"
"github.com/apache/plc4x/plc4go/protocols/cbus/readwrite/model"
readWriteModel "github.com/apache/plc4x/plc4go/protocols/cbus/readwrite/model"
spiModel "github.com/apache/plc4x/plc4go/spi/model"
spiValues "github.com/apache/plc4x/plc4go/spi/values"
"github.com/rs/zerolog/log"
"strings"
"time"
)

Expand Down Expand Up @@ -78,10 +79,10 @@ func (m *Subscriber) Unsubscribe(ctx context.Context, unsubscriptionRequest apiM
return result
}

func (m *Subscriber) handleMonitoredMMI(calReply model.CALReply) bool {
func (m *Subscriber) handleMonitoredMMI(calReply readWriteModel.CALReply) bool {
var unitAddressString string
switch calReply := calReply.(type) {
case model.CALReplyLongExactly:
case readWriteModel.CALReplyLongExactly:
if calReply.GetIsUnitAddress() {
unitAddressString = fmt.Sprintf("u%d", calReply.GetUnitAddress().GetAddress())
} else {
Expand All @@ -96,7 +97,6 @@ func (m *Subscriber) handleMonitoredMMI(calReply model.CALReply) bool {
unitAddressString = "u0" // On short form it should be always unit 0 TODO: double check that
}
calData := calReply.GetCalData()
// TODO: filter
for _, subscriptionRequest := range m.subscriptionRequests {
fields := map[string]apiModel.PlcField{}
types := map[string]spiModel.SubscriptionType{}
Expand All @@ -114,7 +114,11 @@ func (m *Subscriber) handleMonitoredMMI(calReply model.CALReply) bool {
continue
}
if unitAddress := field.GetUnitAddress(); unitAddress != nil {
// TODO: filter in unit address
unitSuffix := fmt.Sprintf("u%d", unitAddress.GetAddress())
if !strings.HasSuffix(unitAddressString, unitSuffix) {
log.Debug().Msgf("Current address string %s has not the suffix %s", unitAddressString, unitSuffix)
continue
}
}
application := field.GetApplication()
// TODO: filter in unit address
Expand All @@ -130,20 +134,70 @@ func (m *Subscriber) handleMonitoredMMI(calReply model.CALReply) bool {

var applicationString string

isLevel := true
blockStart := byte(0x0)
switch calData := calData.(type) {
case model.CALDataStatusExactly:
case readWriteModel.CALDataStatusExactly:
applicationString = calData.GetApplication().ApplicationId().String()
case model.CALDataStatusExtendedExactly:
blockStart = calData.GetBlockStart()

statusBytes := calData.GetStatusBytes()
responseCodes[fieldName] = apiModel.PlcResponseCode_OK
plcListValues := make([]values.PlcValue, len(statusBytes)*4)
for i, statusByte := range statusBytes {
plcListValues[i*4+0] = spiValues.NewPlcSTRING(statusByte.GetGav0().String())
plcListValues[i*4+1] = spiValues.NewPlcSTRING(statusByte.GetGav1().String())
plcListValues[i*4+2] = spiValues.NewPlcSTRING(statusByte.GetGav2().String())
plcListValues[i*4+3] = spiValues.NewPlcSTRING(statusByte.GetGav3().String())
}
plcValues[fieldName] = spiValues.NewPlcList(plcListValues)
case readWriteModel.CALDataStatusExtendedExactly:
applicationString = calData.GetApplication().ApplicationId().String()
isLevel = calData.GetCoding() == readWriteModel.StatusCoding_LEVEL_BY_ELSEWHERE || calData.GetCoding() == readWriteModel.StatusCoding_LEVEL_BY_THIS_SERIAL_INTERFACE
blockStart = calData.GetBlockStart()
coding := calData.GetCoding()
switch coding {
case readWriteModel.StatusCoding_BINARY_BY_THIS_SERIAL_INTERFACE:
fallthrough
case readWriteModel.StatusCoding_BINARY_BY_ELSEWHERE:
statusBytes := calData.GetStatusBytes()
responseCodes[fieldName] = apiModel.PlcResponseCode_OK
plcListValues := make([]values.PlcValue, len(statusBytes)*4)
for i, statusByte := range statusBytes {
plcListValues[i*4+0] = spiValues.NewPlcSTRING(statusByte.GetGav0().String())
plcListValues[i*4+1] = spiValues.NewPlcSTRING(statusByte.GetGav1().String())
plcListValues[i*4+2] = spiValues.NewPlcSTRING(statusByte.GetGav2().String())
plcListValues[i*4+3] = spiValues.NewPlcSTRING(statusByte.GetGav3().String())
}
plcValues[fieldName] = spiValues.NewPlcList(plcListValues)
case readWriteModel.StatusCoding_LEVEL_BY_THIS_SERIAL_INTERFACE:
fallthrough
case readWriteModel.StatusCoding_LEVEL_BY_ELSEWHERE:
levelInformation := calData.GetLevelInformation()
responseCodes[fieldName] = apiModel.PlcResponseCode_OK
plcListValues := make([]values.PlcValue, len(levelInformation))
for i, levelInformation := range levelInformation {
switch levelInformation := levelInformation.(type) {
case readWriteModel.LevelInformationAbsentExactly:
plcListValues[i] = spiValues.NewPlcSTRING("is absent")
case readWriteModel.LevelInformationCorruptedExactly:
plcListValues[i] = spiValues.NewPlcSTRING("corrupted")
case readWriteModel.LevelInformationNormalExactly:
plcListValues[i] = spiValues.NewPlcUSINT(levelInformation.GetActualLevel())
default:
panic("Impossible case")
}
}
plcValues[fieldName] = spiValues.NewPlcList(plcListValues)
}
default:
return false
}
// TODO: we might need to encode more data into the address from sal data
address[fieldName] = fmt.Sprintf("/%s/%s", unitAddressString, applicationString)

// TODO: map values properly
plcValues[fieldName] = spiValues.NewPlcSTRING(fmt.Sprintf("%s", calData))
responseCodes[fieldName] = apiModel.PlcResponseCode_OK
statusType := "binary"
if isLevel {
statusType = fmt.Sprintf("level=0x%X", blockStart)
}
address[fieldName] = fmt.Sprintf("status/%s/%s", statusType, applicationString)

// Assemble a PlcSubscription event
if len(plcValues) > 0 {
Expand All @@ -156,7 +210,7 @@ func (m *Subscriber) handleMonitoredMMI(calReply model.CALReply) bool {
return true
}

func (m *Subscriber) handleMonitoredSal(sal model.MonitoredSAL) bool {
func (m *Subscriber) handleMonitoredSal(sal readWriteModel.MonitoredSAL) bool {
// TODO: filter
for _, subscriptionRequest := range m.subscriptionRequests {
fields := map[string]apiModel.PlcField{}
Expand All @@ -174,12 +228,6 @@ func (m *Subscriber) handleMonitoredSal(sal model.MonitoredSAL) bool {
plcValues[fieldName] = nil
continue
}
if unitAddress := field.GetUnitAddress(); unitAddress != nil {
// TODO: filter in unit address
}
application := field.GetApplication()
// TODO: filter in unit address
_ = application

subscriptionType := subscriptionRequest.GetType(fieldName)
// TODO: handle subscriptionType
Expand All @@ -189,10 +237,10 @@ func (m *Subscriber) handleMonitoredSal(sal model.MonitoredSAL) bool {
types[fieldName] = subscriptionRequest.GetType(fieldName)
intervals[fieldName] = subscriptionRequest.GetInterval(fieldName)

var salData model.SALData
var salData readWriteModel.SALData
var unitAddressString, applicationString string
switch sal := sal.(type) {
case model.MonitoredSALLongFormSmartModeExactly:
case readWriteModel.MonitoredSALLongFormSmartModeExactly:
if sal.GetIsUnitAddress() {
unitAddressString = fmt.Sprintf("u%d", sal.GetUnitAddress().GetAddress())
} else {
Expand All @@ -205,13 +253,28 @@ func (m *Subscriber) handleMonitoredSal(sal model.MonitoredSAL) bool {
}
applicationString = sal.GetApplication().ApplicationId().String()
salData = sal.GetSalData()
case model.MonitoredSALShortFormBasicModeExactly:
case readWriteModel.MonitoredSALShortFormBasicModeExactly:
unitAddressString = "u0" // On short form it should be always unit 0 TODO: double check that
applicationString = sal.GetApplication().ApplicationId().String()
salData = sal.GetSalData()
}
// TODO: we might need to encode more data into the address from sal data
address[fieldName] = fmt.Sprintf("/%s/%s", unitAddressString, applicationString)
if unitAddress := field.GetUnitAddress(); unitAddress != nil {
unitSuffix := fmt.Sprintf("u%d", unitAddress.GetAddress())
if !strings.HasSuffix(unitAddressString, unitSuffix) {
log.Debug().Msgf("Current address string %s has not the suffix %s", unitAddressString, unitSuffix)
continue
}
}

if application := field.GetApplication(); application != readWriteModel.ApplicationIdContainer_RESERVED_FF {
if actualApplicationIdString := application.ApplicationId().String(); applicationString != actualApplicationIdString {
log.Debug().Msgf("Current application id %s doesn't matchactual id %s", unitAddressString, actualApplicationIdString)
continue
}
}

// TODO: we need to map commands e.g. if we get a MeteringDataElectricityConsumption we can map that to MeteringDataMeasureElectricity
address[fieldName] = fmt.Sprintf("sal/%s/%s", applicationString, "TODO")

// TODO: map values properly
plcValues[fieldName] = spiValues.NewPlcSTRING(fmt.Sprintf("%s", salData))
Expand Down
11 changes: 6 additions & 5 deletions plc4go/internal/cbus/fieldtype_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 2651c29

Please sign in to comment.