diff --git a/src-go/arke/celaeno.go b/src-go/arke/celaeno.go index 9bd4fa0..4bc1dea 100644 --- a/src-go/arke/celaeno.go +++ b/src-go/arke/celaeno.go @@ -1,101 +1,101 @@ package arke import ( "encoding/binary" "fmt" "time" ) type CelaenoSetPoint struct { Power uint8 } -func (m *CelaenoSetPoint) MessageClassID() uint16 { +func (m *CelaenoSetPoint) MessageClassID() MessageClass { return CelaenoSetPointMessage } func (c CelaenoSetPoint) Marshall(buf []byte) (int, error) { if err := checkSize(buf, 1); err != nil { return 0, err } buf[0] = c.Power return 1, nil } func (c *CelaenoSetPoint) Unmarshall(buf []byte) error { if err := checkSize(buf, 1); err != nil { return err } c.Power = buf[0] return nil } type CelaenoStatus struct { WaterLevel uint8 Fan FanStatusAndRPM } -func (m *CelaenoStatus) MessageClassID() uint16 { +func (m *CelaenoStatus) MessageClassID() MessageClass { return CelaenoStatusMessage } func (c *CelaenoStatus) Unmarshall(buf []byte) error { if err := checkSize(buf, 3); err != nil { return err } c.WaterLevel = buf[0] c.Fan = FanStatusAndRPM(binary.LittleEndian.Uint16(buf[1:])) return nil } type CelaenoConfig struct { RampUpTime time.Duration RampDownTime time.Duration MinimumOnTime time.Duration DebounceTime time.Duration } -func (m *CelaenoConfig) MessageClassID() uint16 { +func (m *CelaenoConfig) MessageClassID() MessageClass { return CelaenoConfigNessage } const MaxUint16 = ^uint16(0) func castDuration(t time.Duration) (uint16, error) { res := t.Nanoseconds() / 1000000 if res > int64(MaxUint16) { return 0xffff, fmt.Errorf("Time constant overflow") } return uint16(res), nil } func (c CelaenoConfig) Marshall(buf []byte) (int, error) { if err := checkSize(buf, 8); err != nil { return 0, err } for i, t := range []time.Duration{c.RampUpTime, c.RampDownTime, c.MinimumOnTime, c.DebounceTime} { data, err := castDuration(t) if err != nil { return 2 * i, err } binary.LittleEndian.PutUint16(buf[(2*i):], data) } return 8, nil } func (c *CelaenoConfig) Unmarshall(buf []byte) error { if err := checkSize(buf, 8); err != nil { return err } c.RampUpTime = time.Duration(binary.LittleEndian.Uint16(buf[0:])) * time.Millisecond c.RampDownTime = time.Duration(binary.LittleEndian.Uint16(buf[2:])) * time.Millisecond c.MinimumOnTime = time.Duration(binary.LittleEndian.Uint16(buf[4:])) * time.Millisecond c.DebounceTime = time.Duration(binary.LittleEndian.Uint16(buf[6:])) * time.Millisecond return nil } func init() { messageFactory[CelaenoSetPointMessage] = func() ReceivableMessage { return &CelaenoSetPoint{} } messageFactory[CelaenoStatusMessage] = func() ReceivableMessage { return &CelaenoStatus{} } messageFactory[CelaenoConfigNessage] = func() ReceivableMessage { return &CelaenoConfig{} } } diff --git a/src-go/arke/helios.go b/src-go/arke/helios.go index 69ae06b..b1622f1 100644 --- a/src-go/arke/helios.go +++ b/src-go/arke/helios.go @@ -1,32 +1,32 @@ package arke type HeliosSetPoint struct { Visible uint8 UV uint8 } func (c HeliosSetPoint) Marshall(buf []byte) (int, error) { if err := checkSize(buf, 2); err != nil { return 0, err } buf[0] = c.Visible buf[1] = c.UV return 2, nil } func (c *HeliosSetPoint) Unmarshall(buf []byte) error { if err := checkSize(buf, 2); err != nil { return err } c.Visible = buf[0] c.UV = buf[1] return nil } -func (m *HeliosSetPoint) MessageClassID() uint16 { +func (m *HeliosSetPoint) MessageClassID() MessageClass { return HeliosSetPointMessage } func init() { messageFactory[HeliosSetPointMessage] = func() ReceivableMessage { return &HeliosSetPoint{} } } diff --git a/src-go/arke/messages.go b/src-go/arke/messages.go index 0db4093..1c600cc 100644 --- a/src-go/arke/messages.go +++ b/src-go/arke/messages.go @@ -1,151 +1,165 @@ package arke // #include "../../include/arke.h" import "C" import ( "fmt" socketcan "github.com/atuleu/golang-socketcan" ) +type MessageType uint16 +type MessageClass uint16 +type NodeClass uint16 +type NodeID uint8 + const ( - NetworkControlCommand uint16 = C.ARKE_NETWORK_CONTROL_COMMAND - HighPriorityMessage uint16 = C.ARKE_HIGH_PRIORITY_MESSAGE - StandardMessage uint16 = C.ARKE_MESSAGE - HeartBeat uint16 = C.ARKE_HEARTBEAT - MessageTypeMask uint16 = C.ARKE_MESSAGE_TYPE_MASK - - BroadcastClass uint16 = C.ARKE_BROADCAST - ZeusClass uint16 = C.ARKE_ZEUS - HeliosClass uint16 = C.ARKE_HELIOS - CelaenoClass uint16 = C.ARKE_CELAENO - NodeClassMask uint16 = C.ARKE_NODE_CLASS_MASK - - ResetRequest uint16 = C.ARKE_RESET_REQUEST - SynchronisationRequest uint16 = C.ARKE_SYNCHRONISATION - HeartBeatRequest uint16 = C.ARKE_HEARTBEAT_REQUEST - IDMask uint16 = C.ARKE_SUBID_MASK - - ZeusSetPointMessage uint16 = C.ARKE_ZEUS_SET_POINT - ZeusReportMessage uint16 = C.ARKE_ZEUS_REPORT - ZeusVibrationReportMessage uint16 = C.ARKE_ZEUS_VIBRATION_REPORT - ZeusConfigMessage uint16 = C.ARKE_ZEUS_CONFIG - ZeusStatusMessage uint16 = C.ARKE_ZEUS_STATUS - ZeusControlPointMessage uint16 = C.ARKE_ZEUS_CONTROL_POINT - HeliosSetPointMessage uint16 = C.ARKE_HELIOS_SET_POINT - HeliosPulseModeMessage uint16 = C.ARKE_HELIOS_PULSE_MODE - CelaenoSetPointMessage uint16 = C.ARKE_CELAENO_SET_POINT - CelaenoStatusMessage uint16 = C.ARKE_CELAENO_STATUS - CelaenoConfigNessage uint16 = C.ARKE_CELAENO_CONFIG + NetworkControlCommand MessageType = C.ARKE_NETWORK_CONTROL_COMMAND + HighPriorityMessage MessageType = C.ARKE_HIGH_PRIORITY_MESSAGE + StandardMessage MessageType = C.ARKE_MESSAGE + HeartBeat MessageType = C.ARKE_HEARTBEAT + MessageTypeMask uint16 = C.ARKE_MESSAGE_TYPE_MASK + + BroadcastClass NodeClass = C.ARKE_BROADCAST + ZeusClass NodeClass = C.ARKE_ZEUS + HeliosClass NodeClass = C.ARKE_HELIOS + CelaenoClass NodeClass = C.ARKE_CELAENO + NodeClassMask uint16 = C.ARKE_NODE_CLASS_MASK + + ResetRequest MessageClass = C.ARKE_RESET_REQUEST + SynchronisationRequest MessageClass = C.ARKE_SYNCHRONISATION + HeartBeatRequest MessageClass = C.ARKE_HEARTBEAT_REQUEST + IDMask uint16 = C.ARKE_SUBID_MASK + + BroadcastID NodeID = 0x00 + + ZeusSetPointMessage MessageClass = C.ARKE_ZEUS_SET_POINT + ZeusReportMessage MessageClass = C.ARKE_ZEUS_REPORT + ZeusVibrationReportMessage MessageClass = C.ARKE_ZEUS_VIBRATION_REPORT + ZeusConfigMessage MessageClass = C.ARKE_ZEUS_CONFIG + ZeusStatusMessage MessageClass = C.ARKE_ZEUS_STATUS + ZeusControlPointMessage MessageClass = C.ARKE_ZEUS_CONTROL_POINT + HeliosSetPointMessage MessageClass = C.ARKE_HELIOS_SET_POINT + HeliosPulseModeMessage MessageClass = C.ARKE_HELIOS_PULSE_MODE + CelaenoSetPointMessage MessageClass = C.ARKE_CELAENO_SET_POINT + CelaenoStatusMessage MessageClass = C.ARKE_CELAENO_STATUS + CelaenoConfigNessage MessageClass = C.ARKE_CELAENO_CONFIG ) +func makeCANIDT(t MessageType, c MessageClass, n NodeID) uint32 { + return uint32((uint32(t) << 9) | (uint32(c) << 3) | uint32(n)) +} + +func extractCANIDT(idt uint32) (t MessageType, c MessageClass, n NodeID) { + n = NodeID(idt & 0x7) + c = MessageClass((idt & 0x1f8) >> 3) + t = MessageType((idt & 0x600) >> 9) + return +} + type Marshaller interface { Marshall([]byte) (int, error) } type Unmarshaller interface { Unmarshall([]byte) error } type identifiable interface { - MessageClassID() uint16 + MessageClassID() MessageClass } type SendableMessage interface { Marshaller identifiable } type ReceivableMessage interface { Unmarshaller identifiable } type Message interface { Marshaller Unmarshaller identifiable } -func checkID(ID uint8) error { +func checkID(ID NodeID) error { if ID > 7 { return fmt.Errorf("Invalid device ID %d (max is 7)", ID) } return nil } -func SendMessage(itf *socketcan.RawInterface, m SendableMessage, highPriority bool, ID uint8) error { +func SendMessage(itf *socketcan.RawInterface, m SendableMessage, highPriority bool, ID NodeID) error { if err := checkID(ID); err != nil { return err } - canID := (m.MessageClassID() << 3) | uint16(ID) + mType := StandardMessage if highPriority == true { - canID |= HighPriorityMessage << 9 - } else { - canID |= StandardMessage << 9 + mType = HighPriorityMessage } f := socketcan.CanFrame{ - ID: uint32(canID), + ID: makeCANIDT(mType, m.MessageClassID(), ID), Extended: false, RTR: false, Data: make([]byte, 8), } dlc, err := m.Marshall(f.Data) if err != nil { return fmt.Errorf("Could not marshall %v: %s", m, err) } f.Dlc = uint8(dlc) return itf.Send(f) } -func RequestMessage(itf *socketcan.RawInterface, m ReceivableMessage, ID uint8) error { +func RequestMessage(itf *socketcan.RawInterface, m ReceivableMessage, ID NodeID) error { if err := checkID(ID); err != nil { return err } return itf.Send(socketcan.CanFrame{ - ID: uint32((StandardMessage << 9) | (m.MessageClassID() << 3) | uint16(ID)), + ID: makeCANIDT(StandardMessage, m.MessageClassID(), ID), Extended: false, RTR: false, Data: make([]byte, 0), Dlc: 0, }) } type messageCreator func() ReceivableMessage -var messageFactory = make(map[uint16]messageCreator) +var messageFactory = make(map[MessageClass]messageCreator) -func ParseMessage(f *socketcan.CanFrame) (ReceivableMessage, uint8, error) { +func ParseMessage(f *socketcan.CanFrame) (ReceivableMessage, NodeID, error) { if f.Extended == true { return nil, 0, fmt.Errorf("Arke does not support extended can ID") } if f.RTR == true { return nil, 0, fmt.Errorf("RTR frame") } - mType := uint16((f.ID & 0x7ff) >> 9) + mType, mClass, mID := extractCANIDT(f.ID) if mType == NetworkControlCommand { return nil, 0, fmt.Errorf("Network Command") } if mType == HeartBeat { return nil, 0, fmt.Errorf("Heartbeating not implemented yet") } - mID := uint8(f.ID & 0x7) - mClass := uint16((f.ID & 0x1f8) >> 3) creator, ok := messageFactory[mClass] if ok == false { return nil, mID, fmt.Errorf("Unknown message type 0x%05x", mClass) } m := creator() err := m.Unmarshall(f.Data) if err != nil { err = fmt.Errorf("Could not parse message data: %s", err) } return m, mID, err } diff --git a/src-go/arke/messages_test.go b/src-go/arke/messages_test.go new file mode 100644 index 0000000..d05f725 --- /dev/null +++ b/src-go/arke/messages_test.go @@ -0,0 +1,43 @@ +package arke + +import ( + . "gopkg.in/check.v1" +) + +type MessageSuite struct{} + +var _ = Suite(&MessageSuite{}) + +func (s *MessageSuite) TestCANIDTIO(c *C) { + testdata := []struct { + IDT uint32 + Type MessageType + Class MessageClass + ID NodeID + }{ + { + 0x00, + NetworkControlCommand, + MessageClass(BroadcastClass), + NodeID(ResetRequest), + }, + { + 0x781, + HeartBeat, + MessageClass(CelaenoClass), + 1, + }, + } + + for _, d := range testdata { + resType, resClass, resID := extractCANIDT(d.IDT) + c.Check(resType, Equals, d.Type) + c.Check(resClass, Equals, d.Class) + c.Check(resID, Equals, d.ID) + } + + for _, d := range testdata { + res := makeCANIDT(d.Type, d.Class, d.ID) + c.Check(res, Equals, d.IDT) + } +} diff --git a/src-go/arke/zeus.go b/src-go/arke/zeus.go index b4d55df..810d25d 100644 --- a/src-go/arke/zeus.go +++ b/src-go/arke/zeus.go @@ -1,169 +1,169 @@ package arke import ( "encoding/binary" "fmt" "math" ) // #include "../../include/arke.h" import "C" type ZeusSetPoint struct { Humidity float32 Temperature float32 Wind uint8 } -func (m *ZeusSetPoint) MessageClassID() uint16 { +func (m *ZeusSetPoint) MessageClassID() MessageClass { return ZeusSetPointMessage } func checkSize(buf []byte, expected int) error { if len(buf) < expected { return fmt.Errorf("Invalid buffer size %d, required %d", len(buf), expected) } return nil } func (m ZeusSetPoint) Marshall(buf []byte) (int, error) { if err := checkSize(buf, 5); err != nil { return 0, err } binary.LittleEndian.PutUint16(buf[0:], humidityFloatToBinary(m.Humidity)) binary.LittleEndian.PutUint16(buf[2:], hih6030TemperatureFloatToBinary(m.Temperature)) buf[4] = m.Wind return 5, nil } func (m *ZeusSetPoint) Unmarshall(buf []byte) error { if err := checkSize(buf, 5); err != nil { return err } m.Humidity = humidityBinaryToFloat(binary.LittleEndian.Uint16(buf[0:])) if math.IsNaN(float64(m.Humidity)) == true { return fmt.Errorf("Invalid humidity value") } m.Temperature = hih6030TemperatureBinaryToFloat(binary.LittleEndian.Uint16(buf[2:])) if math.IsNaN(float64(m.Temperature)) == true { return fmt.Errorf("Invalid temperature value") } m.Wind = buf[4] return nil } type ZeusReport struct { Humidity float32 Temperature [4]float32 } -func (m *ZeusReport) MessageClassID() uint16 { +func (m *ZeusReport) MessageClassID() MessageClass { return ZeusReportMessage } func (m *ZeusReport) Unmarshall(buf []byte) error { if err := checkSize(buf, 8); err != nil { return err } packed := []uint16{ binary.LittleEndian.Uint16(buf[0:]), binary.LittleEndian.Uint16(buf[2:]), binary.LittleEndian.Uint16(buf[4:]), binary.LittleEndian.Uint16(buf[6:]), } m.Humidity = humidityBinaryToFloat(packed[0] & 0x3fff) if math.IsNaN(float64(m.Humidity)) == true { return fmt.Errorf("Invalid humidity value") } m.Temperature[0] = hih6030TemperatureBinaryToFloat((packed[0] >> 14) | (packed[1]&0x0fff)<<2) if math.IsNaN(float64(m.Temperature[0])) == true { return fmt.Errorf("Invalid temperature value") } m.Temperature[1] = tmp1075BinaryToFloat((packed[1] >> 12) | (packed[2]&0x00ff)<<4) m.Temperature[2] = tmp1075BinaryToFloat((packed[2] >> 8) | (packed[3]&0x000f)<<8) m.Temperature[3] = tmp1075BinaryToFloat((packed[3] & 0xfff0) >> 4) return nil } type ZeusConfig struct { Humidity PDConfig Temperature PDConfig } -func (m *ZeusConfig) MessageClassID() uint16 { +func (m *ZeusConfig) MessageClassID() MessageClass { return ZeusConfigMessage } func (m ZeusConfig) Marshall(buf []byte) (int, error) { if err := checkSize(buf, 8); err != nil { return 0, err } if err := m.Humidity.marshall(buf[0:]); err != nil { return 0, err } if err := m.Temperature.marshall(buf[4:]); err != nil { return 4, err } return 8, nil } func (m *ZeusConfig) Unmarshall(buf []byte) error { if err := checkSize(buf, 8); err != nil { return err } m.Humidity.unmarshall(buf[0:]) m.Temperature.unmarshall(buf[4:]) return nil } const ( ZeusIdle uint8 = C.ARKE_ZEUS_IDLE ZeusActive uint8 = C.ARKE_ZEUS_ACTIVE ZeusClimateNotControlledWatchDog uint8 = C.ARKE_ZEUS_CLIMATE_UNCONTROLLED_WD ) type ZeusStatus struct { Status uint8 Fans [2]FanStatusAndRPM } -func (m *ZeusStatus) MessageClassID() uint16 { +func (m *ZeusStatus) MessageClassID() MessageClass { return ZeusStatusMessage } func (m *ZeusStatus) Unmarshall(buf []byte) error { if err := checkSize(buf, 5); err != nil { return err } m.Status = buf[0] m.Fans[0] = FanStatusAndRPM(binary.LittleEndian.Uint16(buf[1:])) m.Fans[1] = FanStatusAndRPM(binary.LittleEndian.Uint16(buf[3:])) return nil } type ZeusControlPoint struct { Humidity int16 Temperature int16 } -func (m *ZeusControlPoint) MessageClassID() uint16 { +func (m *ZeusControlPoint) MessageClassID() MessageClass { return ZeusControlPointMessage } func (m *ZeusControlPoint) Unmarshall(buf []byte) error { if err := checkSize(buf, 4); err != nil { return err } m.Humidity = int16(binary.LittleEndian.Uint16(buf[0:])) m.Temperature = int16(binary.LittleEndian.Uint16(buf[2:])) return nil } func init() { messageFactory[ZeusSetPointMessage] = func() ReceivableMessage { return &ZeusSetPoint{} } messageFactory[ZeusReportMessage] = func() ReceivableMessage { return &ZeusReport{} } messageFactory[ZeusConfigMessage] = func() ReceivableMessage { return &ZeusConfig{} } messageFactory[ZeusStatusMessage] = func() ReceivableMessage { return &ZeusStatus{} } messageFactory[ZeusControlPointMessage] = func() ReceivableMessage { return &ZeusControlPoint{} } }