diff --git a/src-go/arke/celaeno.go b/src-go/arke/celaeno.go index 924a7b3..4bd28a0 100644 --- a/src-go/arke/celaeno.go +++ b/src-go/arke/celaeno.go @@ -1,150 +1,153 @@ package arke import ( "encoding/binary" "fmt" "time" ) type CelaenoSetPoint struct { Power uint8 } 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 } func (c *CelaenoSetPoint) String() string { return fmt.Sprintf("Celaeno.SetPoint{Power: %d}", c.Power) } type WaterLevelStatus uint8 const ( CelaenoWaterNominal WaterLevelStatus = 0x00 CelaenoWaterWarning WaterLevelStatus = 0x01 CelaenoWaterCritical WaterLevelStatus = 0x02 CelaenoWaterReadError WaterLevelStatus = 0x04 ) type CelaenoStatus struct { WaterLevel WaterLevelStatus Fan FanStatusAndRPM } func (m *CelaenoStatus) MessageClassID() MessageClass { return CelaenoStatusMessage } func (c *CelaenoStatus) Unmarshall(buf []byte) error { if err := checkSize(buf, 3); err != nil { return err } if buf[0]&0x04 != 0 { c.WaterLevel = CelaenoWaterReadError } else if buf[0]&0x02 != 0 { c.WaterLevel = CelaenoWaterCritical } else { c.WaterLevel = WaterLevelStatus(buf[0]) } c.Fan = FanStatusAndRPM(binary.LittleEndian.Uint16(buf[1:])) return nil } func (s WaterLevelStatus) String() string { if s == CelaenoWaterReadError { return "readout-error" } prefix := "" if s&CelaenoWaterReadError != 0 { prefix = "readout-error|" } if s&CelaenoWaterCritical != 0 { return prefix + "critical" } if s&CelaenoWaterWarning != 0 { return prefix + "warning" } return prefix + "nominal" } func (c *CelaenoStatus) String() string { return fmt.Sprintf("Celaeno.Status{WaterLevel: %s, Fan:%s}", c.WaterLevel, c.Fan) } type CelaenoConfig struct { RampUpTime time.Duration RampDownTime time.Duration MinimumOnTime time.Duration DebounceTime time.Duration } func (m *CelaenoConfig) MessageClassID() MessageClass { return CelaenoConfigMessage } 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 (c *CelaenoConfig) String() string { return fmt.Sprintf("Celaeno.Config{RampUp: %s, RampDown: %s, MinimumOn: %s, Debounce: %s}", c.RampUpTime, c.RampDownTime, c.MinimumOnTime, c.DebounceTime) } func init() { messageFactory[CelaenoSetPointMessage] = func() ReceivableMessage { return &CelaenoSetPoint{} } + messagesName[CelaenoSetPointMessage] = "Celaeno.SetPoint" messageFactory[CelaenoStatusMessage] = func() ReceivableMessage { return &CelaenoStatus{} } + messagesName[CelaenoStatusMessage] = "Celaeno.Status" messageFactory[CelaenoConfigMessage] = func() ReceivableMessage { return &CelaenoConfig{} } + messagesName[CelaenoConfigMessage] = "Celaeno.Config" } diff --git a/src-go/arke/format_test.go b/src-go/arke/format_test.go index 671a626..a6c7f92 100644 --- a/src-go/arke/format_test.go +++ b/src-go/arke/format_test.go @@ -1,184 +1,198 @@ package arke import ( "time" . "gopkg.in/check.v1" ) type FormatSuite struct{} var _ = Suite(&FormatSuite{}) func (s *FormatSuite) TestFormatting(c *C) { testdata := []struct { M ReceivableMessage E string }{ { &ZeusSetPoint{51.455, 20.001, 127}, "Zeus.SetPoint{Humidity: 51.46%, Temperature: 20.00°C, Wind: 127}", }, { &ZeusStatus{ Fans: [3]FanStatusAndRPM{ FanStatusAndRPM(uint16(FanOK)<<12 | uint16(1234)), FanStatusAndRPM(uint16(FanStalled) << 14), FanStatusAndRPM(uint16(FanAging)<<14 | uint16(100)), }, }, "Zeus.Status{General: idle, WindFan: {Status: OK, RPM: 1234}, LeftFan: {Status: Aging, RPM: 100}, RightFan: {Status: Stalled, RPM: 0}}", }, { &ZeusReport{12.009, [4]float32{12.001, 13.005, 14, 15}}, "Zeus.Report{Humidity: 12.01%, Ant: 12.00°C, Aux1: 13.01°C, Aux2: 14.00°C, Aux3: 15.00°C}", }, { &ZeusConfig{ PDConfig{ ProportionnalMultiplier: 10, DerivativeMultiplier: 2, IntegralMultiplier: 100, DividerPower: 4, DividerPowerIntegral: 14, }, PDConfig{ ProportionnalMultiplier: 12, DerivativeMultiplier: 3, IntegralMultiplier: 103, DividerPower: 5, DividerPowerIntegral: 11, }, }, "Zeus.Config{Humidity:PIDConfig{Proportional:10/16, Derivative: 2/16, Integral: 100/16384}, Temperature:PIDConfig{Proportional:12/32, Derivative: 3/32, Integral: 103/2048}}", }, { &ZeusControlPoint{1245, -5469}, "Zeus.ControlPoint{Humidity: 1245, Temperature: -5469}", }, { &ZeusDeltaTemperature{[4]float32{-1.25, -0.0625, 0, 0.125}}, "Zeus.DeltaTemperature{Ants: -1.2500°C, Aux1: -0.0625°C, Aux2: 0.0000°C, Aux3: 0.1250°C}", }, { &CelaenoSetPoint{Power: 156}, "Celaeno.SetPoint{Power: 156}", }, { &CelaenoConfig{ RampUpTime: 500 * time.Millisecond, RampDownTime: 3500 * time.Millisecond, DebounceTime: 1000 * time.Millisecond, MinimumOnTime: 4 * time.Second, }, "Celaeno.Config{RampUp: 500ms, RampDown: 3.5s, MinimumOn: 4s, Debounce: 1s}", }, { &CelaenoStatus{}, "Celaeno.Status{WaterLevel: nominal, Fan:{Status: OK, RPM: 0}}"}, { &HeliosSetPoint{}, "Helios.SetPoint{Visible: 0, UV: 0}", }, { &HeartBeatData{ Class: HeliosClass, ID: 3, }, "arke.HeartBeat{Class: Helios, ID: 3}", }, { &HeartBeatData{ Class: CelaenoClass, ID: 3, MajorVersion: 1, MinorVersion: 4, PatchVersion: 0, TweakVersion: 1, }, "arke.HeartBeat{Class: Celaeno, ID: 3, Version: 1.4.0.1}", }, { &HeartBeatRequestData{ Class: 0, Period: 0, }, "arke.HeartBeatRequest{Class: Broadcast, Node: All, Period: SinglePing}", }, { &HeartBeatRequestData{ Class: ZeusClass, Period: 100 * time.Millisecond, }, "arke.HeartBeatRequest{Class: Zeus, Node: All, Period: 100ms}", }, { &ResetRequestData{ Class: 0, ID: 0, }, "arke.ResetRequest{Class: Broadcast, Node: All}", }, { &ResetRequestData{ Class: HeliosClass, ID: 2, }, "arke.ResetRequest{Class: Helios, Node: 2}", }, { &IDChangeRequestData{ Class: CelaenoClass, Old: 1, New: 2, }, "arke.IDChangeRequest{Class: Celaeno, OldID: 1, NewID: 2}", }, { &ErrorReportData{ Class: ZeusClass, ID: 1, ErrorCode: 0x42, }, "arke.ErrorReport{Class: Zeus, ID: 1, ErrorCode: 0x0042}", }, + { + &MessageRequestData{ + Class: ZeusSetPointMessage, + ID: 2, + }, + "arke.MessageRequest{Message:Zeus.SetPoint, Node: 2}", + }, + { + &MessageRequestData{ + Class: ZeusControlPointMessage, + ID: 0, + }, + "arke.MessageRequest{Message:Zeus.ControlPoint, Node: all}", + }, } c.Assert(len(testdata) >= len(messageFactory), Equals, true) for _, d := range testdata { c.Check(d.M.String(), Equals, d.E) } zeusStatus := []struct { V ZeusStatusValue E string }{ {ZeusIdle, "idle"}, {ZeusTemperatureUnreachable | ZeusActive, "temperature-unreachable|active"}, {ZeusHumidityUnreachable | ZeusClimateNotControlledWatchDog, "humidity-unreachable|climate-uncontrolled|idle"}, {ZeusClimateNotControlledWatchDog | ZeusActive, "sensor-issue"}, } for _, d := range zeusStatus { c.Check(d.V.String(), Equals, d.E) } celaenoStatus := []struct { V WaterLevelStatus E string }{ {CelaenoWaterNominal, "nominal"}, {CelaenoWaterReadError | CelaenoWaterNominal, "readout-error"}, {CelaenoWaterReadError | CelaenoWaterWarning, "readout-error|warning"}, {CelaenoWaterReadError | CelaenoWaterCritical | CelaenoWaterWarning, "readout-error|critical"}, {CelaenoWaterCritical | CelaenoWaterWarning, "critical"}, } for _, d := range celaenoStatus { c.Check(d.V.String(), Equals, d.E) } } diff --git a/src-go/arke/helios.go b/src-go/arke/helios.go index eec7701..422e9c8 100644 --- a/src-go/arke/helios.go +++ b/src-go/arke/helios.go @@ -1,38 +1,40 @@ package arke import "fmt" 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() MessageClass { return HeliosSetPointMessage } func (c *HeliosSetPoint) String() string { return fmt.Sprintf("Helios.SetPoint{Visible: %d, UV: %d}", c.Visible, c.UV) } func init() { messageFactory[HeliosSetPointMessage] = func() ReceivableMessage { return &HeliosSetPoint{} } + messagesName[HeliosSetPointMessage] = "Helios.SetPoint" + messagesName[HeliosPulseModeMessage] = "Helios.PulseMode" } diff --git a/src-go/arke/messages.go b/src-go/arke/messages.go index f86946e..d20a788 100644 --- a/src-go/arke/messages.go +++ b/src-go/arke/messages.go @@ -1,187 +1,239 @@ package arke import ( "fmt" socketcan "github.com/atuleu/golang-socketcan" ) type MessageType uint16 type MessageClass uint16 type NodeClass uint16 type NodeID uint8 const ( NetworkControlCommand MessageType = 0x00 HighPriorityMessage MessageType = 0x01 StandardMessage MessageType = 0x02 HeartBeat MessageType = 0x03 MessageTypeMask uint16 = 0x03 << 9 BroadcastClass NodeClass = 0x0 ZeusClass NodeClass = 0x38 HeliosClass NodeClass = 0x34 CelaenoClass NodeClass = 0x30 NodeClassMask uint16 = 0x3f << 3 ResetRequest MessageClass = 0x00 SynchronisationRequest MessageClass = 0x01 IDChangeRequest MessageClass = 0x02 ErrorReport MessageClass = 0x03 HeartBeatRequest MessageClass = 0x07 IDMask uint16 = 0x07 BroadcastID NodeID = 0x00 + RTRRequestMessage MessageClass = MessageClass(1 << 10) HeartBeatMessage MessageClass = MessageClass(HeartBeat << 9) ResetRequestMessage MessageClass = MessageClass(0x7f8 | ResetRequest) SynchronisationRequestMessage MessageClass = MessageClass(0x7f8 | SynchronisationRequest) IDChangeRequestMessage MessageClass = MessageClass(0x7f8 | IDChangeRequest) ErrorReportMessage MessageClass = MessageClass(0x7f8 | ErrorReport) HeartBeatRequestMessage MessageClass = MessageClass(0x7f8 | HeartBeatRequest) ZeusSetPointMessage MessageClass = 0x38 ZeusReportMessage MessageClass = 0x39 ZeusVibrationReportMessage MessageClass = 0x3a ZeusConfigMessage MessageClass = 0x3b ZeusStatusMessage MessageClass = 0x3c ZeusControlPointMessage MessageClass = 0x3d ZeusDeltaTemperatureMessage MessageClass = 0x3e HeliosSetPointMessage MessageClass = 0x34 HeliosPulseModeMessage MessageClass = 0x35 CelaenoSetPointMessage MessageClass = 0x30 CelaenoStatusMessage MessageClass = 0x31 CelaenoConfigMessage MessageClass = 0x32 ) 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() MessageClass } type SendableMessage interface { Marshaller identifiable } type ReceivableMessage interface { Unmarshaller identifiable String() string } type Message interface { Marshaller Unmarshaller identifiable } 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 NodeID) error { if err := checkID(ID); err != nil { return err } mType := StandardMessage if highPriority == true { mType = HighPriorityMessage } f := socketcan.CanFrame{ 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 NodeID) error { if err := checkID(ID); err != nil { return err } return itf.Send(socketcan.CanFrame{ ID: makeCANIDT(StandardMessage, m.MessageClassID(), ID), Extended: false, RTR: true, Data: make([]byte, 0), Dlc: 0, }) } type messageCreator func() ReceivableMessage var messageFactory = make(map[MessageClass]messageCreator) type networkCommandParser func(c MessageClass, buffer []byte) (ReceivableMessage, NodeID, error) var networkCommandFactory = make(map[NodeID]networkCommandParser) +type MessageRequestData struct { + Class MessageClass + ID NodeID +} + +func (d *MessageRequestData) MessageClassID() MessageClass { + return RTRRequestMessage +} + +var messagesName = make(map[MessageClass]string) + +func (c MessageClass) String() string { + if n, ok := messagesName[c]; ok == true { + return n + } + return "" +} + +func (d *MessageRequestData) String() string { + if d.ID == NodeID(0) { + return fmt.Sprintf("arke.MessageRequest{Message:%s, Node: all}", d.Class) + } + return fmt.Sprintf("arke.MessageRequest{Message:%s, Node: %d}", d.Class, d.ID) +} + +func (d *MessageRequestData) Unmarshall(buf []byte) error { + return nil +} + +func parseRTR(f *socketcan.CanFrame) (ReceivableMessage, NodeID, error) { + mType, mClass, mID := extractCANIDT(f.ID) + + if f.Dlc > 0 { + return nil, 0, fmt.Errorf("RTR frame with a payload") + } + if mType != StandardMessage && mType != HighPriorityMessage { + return nil, 0, fmt.Errorf("Unauthorized network command RTR frame") + } + + _, ok := messageFactory[mClass] + if ok == false { + return nil, mID, fmt.Errorf("Unknown message type 0x%02x", int(mClass)) + } + + return &MessageRequestData{ + Class: mClass, + ID: mID, + }, mID, nil + +} + func ParseMessage(f *socketcan.CanFrame) (ReceivableMessage, NodeID, error) { if f.Extended == true { return nil, 0, fmt.Errorf("Arke does not support extended IDT") } if f.RTR == true { - return nil, 0, fmt.Errorf("RTR frame") + return parseRTR(f) } mType, mClass, mID := extractCANIDT(f.ID) if mType == NetworkControlCommand { parser, ok := networkCommandFactory[mID] if ok == false { return nil, 0, fmt.Errorf("Unknown network command 0x%02x", mID) } return parser(mClass, f.Data[0:f.Dlc]) } if mType == HeartBeat { res := &HeartBeatData{} if err := res.Unmarshall(f.Data[0:f.Dlc]); err != nil { return nil, mID, err } res.Class = NodeClass(mClass) res.ID = mID return res, mID, nil } creator, ok := messageFactory[mClass] if ok == false { - return nil, mID, fmt.Errorf("Unknown message type 0x%02x", mClass) + return nil, mID, fmt.Errorf("Unknown message type 0x%02x", int(mClass)) } m := creator() err := m.Unmarshall(f.Data[0:f.Dlc]) 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 index 4ca8cab..8810a03 100644 --- a/src-go/arke/messages_test.go +++ b/src-go/arke/messages_test.go @@ -1,191 +1,234 @@ package arke import ( "time" socketcan "github.com/atuleu/golang-socketcan" . "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) } } func (s *MessageSuite) TestMessageParsing(c *C) { testdata := []struct { F socketcan.CanFrame ID NodeID M ReceivableMessage }{ { socketcan.CanFrame{ID: makeCANIDT(HeartBeat, MessageClass(ZeusClass), 2)}, 2, &HeartBeatData{ZeusClass, 2, 0, 0, 0, 0}, }, { socketcan.CanFrame{ID: makeCANIDT(HeartBeat, MessageClass(CelaenoClass), 4), Dlc: 4, Data: []byte{1, 2, 3, 4}}, 4, &HeartBeatData{CelaenoClass, 4, 1, 2, 3, 4}, }, { socketcan.CanFrame{ID: makeCANIDT(NetworkControlCommand, MessageClass(ZeusClass), NodeID(HeartBeatRequest))}, 0, &HeartBeatRequestData{ZeusClass, 0}, }, { socketcan.CanFrame{ID: makeCANIDT(NetworkControlCommand, MessageClass(ZeusClass), NodeID(HeartBeatRequest)), Dlc: 2, Data: []byte{0xe8, 0x03}}, 0, &HeartBeatRequestData{ZeusClass, 1 * time.Second}, }, { socketcan.CanFrame{ID: makeCANIDT(NetworkControlCommand, MessageClass(0), NodeID(ResetRequest)), Dlc: 1, Data: []byte{0x00}}, 0, &ResetRequestData{BroadcastClass, BroadcastID}, }, { socketcan.CanFrame{ID: makeCANIDT(NetworkControlCommand, MessageClass(HeliosClass), NodeID(IDChangeRequest)), Dlc: 2, Data: []byte{0x01, 0x02}}, 1, &IDChangeRequestData{HeliosClass, 1, 2}, }, { socketcan.CanFrame{ID: makeCANIDT(NetworkControlCommand, MessageClass(0), NodeID(ErrorReport)), Dlc: 4, Data: []byte{byte(ZeusClass), 3, 0x42, 0}}, 3, &ErrorReportData{ZeusClass, 3, 0x0042}, }, { socketcan.CanFrame{ID: makeCANIDT(StandardMessage, ZeusSetPointMessage, 2), Dlc: 5, Data: []byte{0, 0, 0, 0, 0}}, 2, &ZeusSetPoint{0, -40, 0}, }, { socketcan.CanFrame{ID: makeCANIDT(StandardMessage, ZeusReportMessage, 3), Dlc: 8, Data: []byte{0, 0, 0, 0, 0, 0, 0, 0}}, 3, &ZeusReport{0, [4]float32{-40, 0, 0, 0}}, }, { socketcan.CanFrame{ID: makeCANIDT(StandardMessage, ZeusConfigMessage, 4), Dlc: 8, Data: []byte{0, 0, 0, 0, 0, 0, 0, 0}}, 4, &ZeusConfig{PDConfig{0, 0, 0, 0, 0}, PDConfig{0, 0, 0, 0, 0}}, }, { socketcan.CanFrame{ID: makeCANIDT(StandardMessage, ZeusStatusMessage, 5), Dlc: 7, Data: []byte{0, 0, 0, 0, 0, 0, 0}}, 5, &ZeusStatus{0, [3]FanStatusAndRPM{0, 0, 0}}, }, { socketcan.CanFrame{ID: makeCANIDT(StandardMessage, ZeusControlPointMessage, 6), Dlc: 4, Data: []byte{2, 0, 3, 0}}, 6, &ZeusControlPoint{2, 3}, }, { socketcan.CanFrame{ID: makeCANIDT(StandardMessage, ZeusDeltaTemperatureMessage, 7), Dlc: 8, Data: []byte{0, 0, 0, 0, 0, 0, 0, 0}}, 7, &ZeusDeltaTemperature{[4]float32{0, 0, 0, 0}}, }, { socketcan.CanFrame{ID: makeCANIDT(StandardMessage, HeliosSetPointMessage, 1), Dlc: 2, Data: []byte{0x7f, 0xff}}, 1, &HeliosSetPoint{127, 255}, }, { socketcan.CanFrame{ID: makeCANIDT(StandardMessage, CelaenoSetPointMessage, 1), Dlc: 1, Data: []byte{0x7f}}, 1, &CelaenoSetPoint{127}, }, { socketcan.CanFrame{ID: makeCANIDT(StandardMessage, CelaenoStatusMessage, 2), Dlc: 3, Data: []byte{0x06, 0x00, 0x00}}, 2, &CelaenoStatus{WaterLevel: CelaenoWaterReadError, Fan: 0}, }, { socketcan.CanFrame{ID: makeCANIDT(StandardMessage, CelaenoStatusMessage, 3), Dlc: 3, Data: []byte{0x02, 0x00, 0x00}}, 3, &CelaenoStatus{WaterLevel: CelaenoWaterCritical, Fan: 0}, }, { socketcan.CanFrame{ID: makeCANIDT(StandardMessage, CelaenoConfigMessage, 4), Dlc: 8, Data: []byte{0xe8, 0x03, 0xe8, 0x03, 0xe8, 0x03, 0xe8, 0x03}}, 4, &CelaenoConfig{time.Second, time.Second, time.Second, time.Second}, }, + { + socketcan.CanFrame{ID: makeCANIDT(StandardMessage, CelaenoConfigMessage, 5), Dlc: 0, Data: []byte{}, RTR: true}, + 5, + &MessageRequestData{Class: CelaenoConfigMessage, ID: 5}, + }, } for _, d := range testdata { m, ID, err := ParseMessage(&d.F) if c.Check(err, IsNil) == false { continue } c.Check(ID, Equals, d.ID) c.Check(m, DeepEquals, d.M) c.Check(m.MessageClassID(), Equals, d.M.MessageClassID()) } errorData := []struct { F socketcan.CanFrame E string }{ { socketcan.CanFrame{Extended: true}, "Arke does not support extended IDT", }, { - socketcan.CanFrame{RTR: true}, - "RTR frame", + socketcan.CanFrame{RTR: true, Dlc: 1}, + "RTR frame with a payload", + }, + { + socketcan.CanFrame{RTR: true, Dlc: 0, ID: makeCANIDT(NetworkControlCommand, 0, 0)}, + "Unauthorized network command RTR frame", }, + { socketcan.CanFrame{ID: makeCANIDT(NetworkControlCommand, 0, 6)}, "Unknown network command 0x06", }, { socketcan.CanFrame{ID: makeCANIDT(HeartBeat, MessageClass(ZeusClass), 1), Dlc: 1, Data: []byte{0}}, "Invalid buffer size 1 .*", }, { socketcan.CanFrame{ID: makeCANIDT(StandardMessage, 0, 1), Dlc: 1, Data: []byte{0}}, "Unknown message type 0x00", }, + { + socketcan.CanFrame{ID: makeCANIDT(StandardMessage, 0, 1), Dlc: 0, Data: []byte{}, RTR: true}, + "Unknown message type 0x00", + }, { socketcan.CanFrame{ID: makeCANIDT(StandardMessage, ZeusReportMessage, 1), Dlc: 1, Data: []byte{0}}, "Could not parse message data: .*", }, } for _, d := range errorData { _, _, err := ParseMessage(&d.F) c.Check(err, ErrorMatches, d.E) } + + // does nothing + c.Check((&MessageRequestData{}).Unmarshall(nil), Equals, nil) +} + +func (s *MessageSuite) TestMessagesName(c *C) { + testdata := []struct { + C MessageClass + E string + }{ + {ZeusSetPointMessage, "Zeus.SetPoint"}, + {ZeusReportMessage, "Zeus.Report"}, + {ZeusVibrationReportMessage, "Zeus.VibrationReport"}, + {ZeusConfigMessage, "Zeus.Config"}, + {ZeusStatusMessage, "Zeus.Status"}, + {ZeusControlPointMessage, "Zeus.ControlPoint"}, + {ZeusDeltaTemperatureMessage, "Zeus.DeltaTemperature"}, + {HeliosSetPointMessage, "Helios.SetPoint"}, + {HeliosPulseModeMessage, "Helios.PulseMode"}, + {CelaenoSetPointMessage, "Celaeno.SetPoint"}, + {CelaenoStatusMessage, "Celaeno.Status"}, + {CelaenoConfigMessage, "Celaeno.Config"}, + {0, ""}, + } + + for _, d := range testdata { + c.Check(d.C.String(), Equals, d.E) + } + } diff --git a/src-go/arke/zeus.go b/src-go/arke/zeus.go index d31e77a..e489622 100644 --- a/src-go/arke/zeus.go +++ b/src-go/arke/zeus.go @@ -1,261 +1,268 @@ package arke import ( "encoding/binary" "fmt" "math" ) type ZeusSetPoint struct { Humidity float32 Temperature float32 Wind uint8 } 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 } func (sp *ZeusSetPoint) String() string { return fmt.Sprintf("Zeus.SetPoint{Humidity: %.2f%%, Temperature: %.2f°C, Wind: %d}", sp.Humidity, sp.Temperature, sp.Wind) } type ZeusReport struct { Humidity float32 Temperature [4]float32 } 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 } func (sp *ZeusReport) String() string { return fmt.Sprintf("Zeus.Report{Humidity: %.2f%%, Ant: %.2f°C, Aux1: %.2f°C, Aux2: %.2f°C, Aux3: %.2f°C}", sp.Humidity, sp.Temperature[0], sp.Temperature[1], sp.Temperature[2], sp.Temperature[3]) } type ZeusConfig struct { Humidity PDConfig Temperature PDConfig } func (c *ZeusConfig) String() string { return fmt.Sprintf("Zeus.Config{Humidity:%s, Temperature:%s}", c.Humidity, c.Temperature) } 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 } type ZeusStatusValue uint8 const ( ZeusIdle ZeusStatusValue = 0x00 ZeusActive ZeusStatusValue = 1 << 0 ZeusClimateNotControlledWatchDog ZeusStatusValue = 1 << 1 ZeusHumidityUnreachable ZeusStatusValue = 1 << 2 ZeusTemperatureUnreachable ZeusStatusValue = 1 << 3 ) type ZeusStatus struct { Status ZeusStatusValue Fans [3]FanStatusAndRPM } func (s ZeusStatusValue) String() string { prefix := "" if s&ZeusTemperatureUnreachable != 0 { prefix += "temperature-unreachable|" } if s&ZeusHumidityUnreachable != 0 { prefix += "humidity-unreachable|" } if s&ZeusClimateNotControlledWatchDog != 0 { if s&ZeusActive != 0 { return prefix + "sensor-issue" } prefix += "climate-uncontrolled|" } if s&ZeusActive != 0 { return prefix + "active" } return prefix + "idle" } func (s *ZeusStatus) String() string { return fmt.Sprintf("Zeus.Status{General: %s, WindFan: %s, LeftFan: %s, RightFan: %s}", s.Status, s.Fans[0], s.Fans[2], s.Fans[1], ) } func (m *ZeusStatus) MessageClassID() MessageClass { return ZeusStatusMessage } func (m *ZeusStatus) Unmarshall(buf []byte) error { if err := checkSize(buf, 7); err != nil { return err } m.Status = ZeusStatusValue(buf[0]) m.Fans[0] = FanStatusAndRPM(binary.LittleEndian.Uint16(buf[1:])) m.Fans[1] = FanStatusAndRPM(binary.LittleEndian.Uint16(buf[3:])) m.Fans[2] = FanStatusAndRPM(binary.LittleEndian.Uint16(buf[5:])) return nil } type ZeusControlPoint struct { Humidity int16 Temperature int16 } func (cp *ZeusControlPoint) String() string { return fmt.Sprintf("Zeus.ControlPoint{Humidity: %d, Temperature: %d}", cp.Humidity, cp.Temperature, ) } 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 } type ZeusDeltaTemperature struct { Delta [4]float32 } func (d *ZeusDeltaTemperature) String() string { return fmt.Sprintf("Zeus.DeltaTemperature{Ants: %.4f°C, Aux1: %.4f°C, Aux2: %.4f°C, Aux3: %.4f°C}", d.Delta[0], d.Delta[1], d.Delta[2], d.Delta[3], ) } func (m *ZeusDeltaTemperature) MessageClassID() MessageClass { return ZeusDeltaTemperatureMessage } func (m *ZeusDeltaTemperature) Marshall(buf []byte) (int, error) { binary.LittleEndian.PutUint16(buf[0:], uint16(int16(m.Delta[0]*float32(hih6030Max)/165.0))) for i := 1; i < 4; i++ { binary.LittleEndian.PutUint16(buf[(2*i):], uint16(int16(m.Delta[i]/0.0625))) } return 8, nil } func (m *ZeusDeltaTemperature) Unmarshall(buf []byte) error { if err := checkSize(buf, 8); err != nil { return err } m.Delta[0] = float32(int16(binary.LittleEndian.Uint16(buf[0:]))) * 165.0 / float32(hih6030Max) for i := 1; i < 4; i++ { m.Delta[i] = float32(int16(binary.LittleEndian.Uint16(buf[(2*i):]))) * 0.0625 } return nil } func init() { messageFactory[ZeusSetPointMessage] = func() ReceivableMessage { return &ZeusSetPoint{} } + messagesName[ZeusSetPointMessage] = "Zeus.SetPoint" messageFactory[ZeusReportMessage] = func() ReceivableMessage { return &ZeusReport{} } + messagesName[ZeusReportMessage] = "Zeus.Report" messageFactory[ZeusConfigMessage] = func() ReceivableMessage { return &ZeusConfig{} } + messagesName[ZeusConfigMessage] = "Zeus.Config" messageFactory[ZeusStatusMessage] = func() ReceivableMessage { return &ZeusStatus{} } + messagesName[ZeusStatusMessage] = "Zeus.Status" messageFactory[ZeusControlPointMessage] = func() ReceivableMessage { return &ZeusControlPoint{} } + messagesName[ZeusControlPointMessage] = "Zeus.ControlPoint" messageFactory[ZeusDeltaTemperatureMessage] = func() ReceivableMessage { return &ZeusDeltaTemperature{} } + messagesName[ZeusDeltaTemperatureMessage] = "Zeus.DeltaTemperature" + messagesName[ZeusVibrationReportMessage] = "Zeus.VibrationReport" }