From d5713b5e438fa3f133a89d61053259c890f7e7c6 Mon Sep 17 00:00:00 2001 From: Gregor Riepl Date: Sun, 31 Jan 2021 16:56:33 +0100 Subject: [PATCH] Added control server --- .gitignore | 2 + uncanny/can.go | 115 ++++++++++++++ uncanny/cmd/uncanny/main.go | 27 ++++ uncanny/go.mod | 5 + uncanny/go.sum | 4 + uncanny/message.go | 305 ++++++++++++++++++++++++++++++++++++ uncanny/server.go | 119 ++++++++++++++ 7 files changed, 577 insertions(+) create mode 100644 uncanny/can.go create mode 100644 uncanny/cmd/uncanny/main.go create mode 100644 uncanny/go.mod create mode 100644 uncanny/go.sum create mode 100644 uncanny/message.go create mode 100644 uncanny/server.go diff --git a/.gitignore b/.gitignore index 0dd1311..0821e0a 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ fp-info-cache *.lst *.map *.srec + +uncanny/uncanny diff --git a/uncanny/can.go b/uncanny/can.go new file mode 100644 index 0000000..6322360 --- /dev/null +++ b/uncanny/can.go @@ -0,0 +1,115 @@ +package uncanny + +import ( + "errors" + "fmt" + "log" + "github.com/brutella/can" +) + +type Can struct { + bus *can.Bus + swstate FeedbackMessage + dispstate int +} + +func NewCan(intf string) (*Can, error) { + bus, err := can.NewBusForInterfaceWithName(intf) + if err != nil { + return nil, errors.New(fmt.Sprintf("Can't open %s: %v", intf, err)) + } + can := &Can{ + bus: bus, + dispstate: -1, + } + bus.Subscribe(can) + return can, nil +} + +func (c *Can) Start() { + go func() { + // send the init sequence + c.initialize() + // ConnectAndPublish will block, so we'll just leave the Printf hanging here + log.Printf("CAN bus shut down: %v", c.bus.ConnectAndPublish()) + }() +} + +func (c *Can) Stop() { + c.bus.Disconnect() +} + +func (c *Can) Handle(frame can.Frame) { + typ, obj, err := DecodeMessage(frame) + if err != nil { + if err == UnsupportedMessageType { + log.Printf("Unsupported message type: 0x%08x", frame.ID) + } else { + log.Printf("Cannot decode message: %v", err) + } + } else { + switch typ { + case MessageTypeFeedback: + c.swstate = obj.(FeedbackMessage) + case MessageTypeFeedbackRequest: + // ignore + case MessageTypePower: + // ignore + case MessageTypePowerRequest: + // ignore + case MessageTypeDispense: + c.dispstate = obj.(DispenseMessage).Slot + case MessageTypeAuto: + // ignore + } + } +} + +func (c *Can) IsEmpty(slot int) bool { + switch (slot) { + case 0: + return c.swstate.EmptyD + case 1: + return c.swstate.EmptyE + case 2: + return c.swstate.EmptyF + case 3: + return c.swstate.EmptyG + case 4: + return c.swstate.EmptyH + } + return false +} + +func (c *Can) IsDispensing(slot int) bool { + switch (slot) { + case 0: + return c.swstate.EndD + case 1: + return c.swstate.EndE + case 2: + return c.swstate.EndF + case 3: + return c.swstate.EndG + case 4: + return c.swstate.EndH + } + return false +} + +func (c *Can) ActiveDispenser() int { + return c.dispstate +} + +func (c *Can) Dispense(slot int) error { + return c.bus.Publish(DispenseMessage{slot}.Encode()) +} + +func (c *Can) Cancel() error { + return c.bus.Publish(DispenseMessage{DispenseSlotOff}.Encode()) +} + +func (c *Can) initialize() error { + // enable automatic status updates + return c.bus.Publish(AutoMessage{true}.Encode()) +} diff --git a/uncanny/cmd/uncanny/main.go b/uncanny/cmd/uncanny/main.go new file mode 100644 index 0000000..bdb88b5 --- /dev/null +++ b/uncanny/cmd/uncanny/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "flag" + "git.kabelsalat.ch/seto/matemat/uncanny" + "log" +) + +const ( + CanInterface = "can0" + ListenAddress = "localhost:8000" +) + +func main() { + var intf string + var addr string + flag.StringVar(&intf, "interface", CanInterface, "CAN interface name") + flag.StringVar(&addr, "listen", ListenAddress, "HTTP listen address") + flag.Parse() + can, err := uncanny.NewCan(intf) + if err != nil { + log.Fatal(err) + } + server := uncanny.NewServer(can) + can.Start() + log.Fatal(server.ListenAndServe(addr)) +} diff --git a/uncanny/go.mod b/uncanny/go.mod new file mode 100644 index 0000000..cfb7e69 --- /dev/null +++ b/uncanny/go.mod @@ -0,0 +1,5 @@ +module git.kabelsalat.ch/seto/matemat/uncanny + +go 1.15 + +require github.com/brutella/can v0.0.1 diff --git a/uncanny/go.sum b/uncanny/go.sum new file mode 100644 index 0000000..9cde019 --- /dev/null +++ b/uncanny/go.sum @@ -0,0 +1,4 @@ +github.com/brutella/can v0.0.1 h1:Rz+2Zuje3NT79daon8wPN9+VphH3/kl1DP8Dhf/k1NI= +github.com/brutella/can v0.0.1/go.mod h1:NYDxbQito3w4+4DcjWs/fpQ3xyaFdpXw/KYqtZFU98k= +golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06 h1:0oC8rFnE+74kEmuHZ46F6KHsMr5Gx2gUQPuNz28iQZM= +golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/uncanny/message.go b/uncanny/message.go new file mode 100644 index 0000000..23cf336 --- /dev/null +++ b/uncanny/message.go @@ -0,0 +1,305 @@ +package uncanny + +/* +// include our message format definitions +#cgo CPPFLAGS: -I${SRCDIR}/.. +#include + +// since we can't use macros in Go directly, let's add some wrappers +// to avoid conflicts, we'll simply lowercase the symbol names + +const uint16_t can_msg_feedback_status = CAN_MSG_FEEDBACK_STATUS; +const size_t can_msg_feedback_status_field_reset_sw = CAN_MSG_FEEDBACK_STATUS_FIELD_RESET_SW; +const uint8_t can_msg_feedback_status_bit_reset_sw = CAN_MSG_FEEDBACK_STATUS_BIT_RESET_SW; +const size_t can_msg_feedback_status_field_empty_h = CAN_MSG_FEEDBACK_STATUS_FIELD_EMPTY_H; +const uint8_t can_msg_feedback_status_bit_empty_h = CAN_MSG_FEEDBACK_STATUS_BIT_EMPTY_H; +const size_t can_msg_feedback_status_field_empty_g = CAN_MSG_FEEDBACK_STATUS_FIELD_EMPTY_G; +const uint8_t can_msg_feedback_status_bit_empty_g = CAN_MSG_FEEDBACK_STATUS_BIT_EMPTY_G; +const size_t can_msg_feedback_status_field_empty_f = CAN_MSG_FEEDBACK_STATUS_FIELD_EMPTY_F; +const uint8_t can_msg_feedback_status_bit_empty_f = CAN_MSG_FEEDBACK_STATUS_BIT_EMPTY_F; +const size_t can_msg_feedback_status_field_empty_e = CAN_MSG_FEEDBACK_STATUS_FIELD_EMPTY_E; +const uint8_t can_msg_feedback_status_bit_empty_e = CAN_MSG_FEEDBACK_STATUS_BIT_EMPTY_E; +const size_t can_msg_feedback_status_field_empty_d = CAN_MSG_FEEDBACK_STATUS_FIELD_EMPTY_D; +const uint8_t can_msg_feedback_status_bit_empty_d = CAN_MSG_FEEDBACK_STATUS_BIT_EMPTY_D; +const size_t can_msg_feedback_status_field_end_h = CAN_MSG_FEEDBACK_STATUS_FIELD_END_H; +const uint8_t can_msg_feedback_status_bit_end_h = CAN_MSG_FEEDBACK_STATUS_BIT_END_H; +const size_t can_msg_feedback_status_field_end_g = CAN_MSG_FEEDBACK_STATUS_FIELD_END_G; +const uint8_t can_msg_feedback_status_bit_end_g = CAN_MSG_FEEDBACK_STATUS_BIT_END_G; +const size_t can_msg_feedback_status_field_end_f = CAN_MSG_FEEDBACK_STATUS_FIELD_END_F; +const uint8_t can_msg_feedback_status_bit_end_f = CAN_MSG_FEEDBACK_STATUS_BIT_END_F; +const size_t can_msg_feedback_status_field_end_e = CAN_MSG_FEEDBACK_STATUS_FIELD_END_E; +const uint8_t can_msg_feedback_status_bit_end_e = CAN_MSG_FEEDBACK_STATUS_BIT_END_E; +const size_t can_msg_feedback_status_field_end_d = CAN_MSG_FEEDBACK_STATUS_FIELD_END_D; +const uint8_t can_msg_feedback_status_bit_end_d = CAN_MSG_FEEDBACK_STATUS_BIT_END_D; + +const uint16_t can_msg_power_status = CAN_MSG_POWER_STATUS; + +const uint16_t can_msg_power_dispense = CAN_MSG_POWER_DISPENSE; +const uint8_t can_msg_power_dispense_off = CAN_MSG_POWER_DISPENSE_OFF; +const uint8_t can_msg_power_dispense_slot1 = CAN_MSG_POWER_DISPENSE_SLOT1; +const uint8_t can_msg_power_dispense_slot2 = CAN_MSG_POWER_DISPENSE_SLOT2; +const uint8_t can_msg_power_dispense_slot3 = CAN_MSG_POWER_DISPENSE_SLOT3; +const uint8_t can_msg_power_dispense_slot4 = CAN_MSG_POWER_DISPENSE_SLOT4; +const uint8_t can_msg_power_dispense_slot5 = CAN_MSG_POWER_DISPENSE_SLOT5; + +const uint16_t can_msg_auto_status = CAN_MSG_AUTO_STATUS; +const uint8_t can_msg_auto_status_disable = CAN_MSG_AUTO_STATUS_DISABLE; +const uint8_t can_msg_auto_status_enable = CAN_MSG_AUTO_STATUS_ENABLE; +*/ +import "C" + +import ( + "errors" + "github.com/brutella/can" +) + +var ( + InvalidMessageID = errors.New("Invalid message ID, cannot decode") + InvalidMessageLength = errors.New("Invalid message length, cannot decode") + UnsupportedMessageType = errors.New("Unsupported message type, cannot decode") +) + +type MessageType int +const ( + MessageTypeInvalid MessageType = iota + MessageTypeFeedback + MessageTypeFeedbackRequest + MessageTypePower + MessageTypePowerRequest + MessageTypeDispense + MessageTypeAuto +) + +// DecodeMessage decodes a CAN frame and returns an appropriate message +// object and the message type. +// +// For request messages, only the type is returned (object and error are nil) +// +// When the message cannot be decoded, a nil object and MessageTypeInvalid is +// returned, plus an appropriate error. If the message ID is unknown, error +// will be UnsupportedMessageType. +func DecodeMessage(frame can.Frame) (MessageType, interface{}, error) { + switch frame.ID { + case uint32(C.can_msg_feedback_status): + msg, err := DecodeFeedbackMessage(frame) + return MessageTypeFeedback, msg, err + case uint32(C.can_msg_feedback_status) | can.MaskRtr: + return MessageTypeFeedbackRequest, nil, nil + case uint32(C.can_msg_power_status): + msg, err := DecodePowerMessage(frame) + return MessageTypePower, msg, err + case uint32(C.can_msg_power_status) | can.MaskRtr: + return MessageTypePowerRequest, nil, nil + case uint32(C.can_msg_power_dispense): + msg, err := DecodeDispenseMessage(frame) + return MessageTypeDispense, msg, err + case uint32(C.can_msg_auto_status): + msg, err := DecodeAutoMessage(frame) + return MessageTypeAuto, msg, err + } + return MessageTypeInvalid, nil, UnsupportedMessageType +} + +type FeedbackMessage struct { + EndD bool + EndE bool + EndF bool + EndG bool + EndH bool + EmptyD bool + EmptyE bool + EmptyF bool + EmptyG bool + EmptyH bool + ResetSw bool +} + +func RequestFeedbackMessage() can.Frame { + return can.Frame{ + ID: uint32(C.can_msg_feedback_status) | can.MaskRtr, + } +} + +func DecodeFeedbackMessage(frame can.Frame) (FeedbackMessage, error) { + var ret FeedbackMessage + if frame.ID != uint32(C.can_msg_feedback_status) { + return ret, InvalidMessageID + } + if frame.Length != 2 { + return ret, InvalidMessageLength + } + ret.EndD = frame.Data[C.can_msg_feedback_status_field_end_d] & C.can_msg_feedback_status_bit_end_d != 0 + ret.EndE = frame.Data[C.can_msg_feedback_status_field_end_e] & C.can_msg_feedback_status_bit_end_e != 0 + ret.EndF = frame.Data[C.can_msg_feedback_status_field_end_f] & C.can_msg_feedback_status_bit_end_f != 0 + ret.EndG = frame.Data[C.can_msg_feedback_status_field_end_g] & C.can_msg_feedback_status_bit_end_g != 0 + ret.EndH = frame.Data[C.can_msg_feedback_status_field_end_h] & C.can_msg_feedback_status_bit_end_h != 0 + ret.EmptyD = frame.Data[C.can_msg_feedback_status_field_empty_d] & C.can_msg_feedback_status_bit_empty_d != 0 + ret.EmptyE = frame.Data[C.can_msg_feedback_status_field_empty_e] & C.can_msg_feedback_status_bit_empty_e != 0 + ret.EmptyF = frame.Data[C.can_msg_feedback_status_field_empty_f] & C.can_msg_feedback_status_bit_empty_f != 0 + ret.EmptyG = frame.Data[C.can_msg_feedback_status_field_empty_g] & C.can_msg_feedback_status_bit_empty_g != 0 + ret.EmptyH = frame.Data[C.can_msg_feedback_status_field_empty_h] & C.can_msg_feedback_status_bit_empty_h != 0 + ret.ResetSw = frame.Data[C.can_msg_feedback_status_field_reset_sw] & C.can_msg_feedback_status_bit_reset_sw != 0 + return ret, nil +} + +func (f FeedbackMessage) Encode() can.Frame { + data := [8]uint8{} + if f.EndD { + data[C.can_msg_feedback_status_field_end_d] |= C.can_msg_feedback_status_bit_end_d + } + if f.EndE { + data[C.can_msg_feedback_status_field_end_e] |= C.can_msg_feedback_status_bit_end_e + } + if f.EndF { + data[C.can_msg_feedback_status_field_end_f] |= C.can_msg_feedback_status_bit_end_f + } + if f.EndG { + data[C.can_msg_feedback_status_field_end_g] |= C.can_msg_feedback_status_bit_end_g + } + if f.EndH { + data[C.can_msg_feedback_status_field_end_h] |= C.can_msg_feedback_status_bit_end_h + } + if f.EmptyD { + data[C.can_msg_feedback_status_field_empty_d] |= C.can_msg_feedback_status_bit_empty_d + } + if f.EmptyE { + data[C.can_msg_feedback_status_field_empty_e] |= C.can_msg_feedback_status_bit_empty_e + } + if f.EmptyF { + data[C.can_msg_feedback_status_field_empty_f] |= C.can_msg_feedback_status_bit_empty_f + } + if f.EmptyG { + data[C.can_msg_feedback_status_field_empty_g] |= C.can_msg_feedback_status_bit_empty_g + } + if f.EmptyH { + data[C.can_msg_feedback_status_field_empty_h] |= C.can_msg_feedback_status_bit_empty_h + } + if f.ResetSw { + data[C.can_msg_feedback_status_field_reset_sw] |= C.can_msg_feedback_status_bit_reset_sw + } + return can.Frame{ + ID: uint32(C.can_msg_feedback_status), + Length: 2, + Data: data, + } +} + +type PowerMessage struct { + Bits int +} + +func RequestPowerMessage() can.Frame { + return can.Frame{ + ID: uint32(C.can_msg_power_status) | can.MaskRtr, + } +} + +func DecodePowerMessage(frame can.Frame) (PowerMessage, error) { + var ret PowerMessage + if frame.ID != uint32(C.can_msg_power_status) { + return ret, InvalidMessageID + } + if frame.Length != 2 { + return ret, InvalidMessageLength + } + ret.Bits = (int(frame.Data[0]) << 8) | int(frame.Data[1]) + return ret, nil +} + +func (f PowerMessage) Encode() can.Frame { + data := [8]uint8{} + data[0] = uint8(f.Bits >> 8) + data[1] = uint8(f.Bits) + return can.Frame{ + ID: uint32(C.can_msg_power_status), + Length: 2, + Data: data, + } +} + +const DispenseSlotOff int = -1 + +type DispenseMessage struct { + Slot int +} + +func DecodeDispenseMessage(frame can.Frame) (DispenseMessage, error) { + var ret DispenseMessage + if frame.ID != uint32(C.can_msg_power_dispense) { + return ret, InvalidMessageID + } + if frame.Length != 1 { + return ret, InvalidMessageLength + } + switch frame.Data[0] { + case C.can_msg_power_dispense_off: + ret.Slot = DispenseSlotOff + case C.can_msg_power_dispense_slot1: + ret.Slot = 0 + case C.can_msg_power_dispense_slot2: + ret.Slot = 1 + case C.can_msg_power_dispense_slot3: + ret.Slot = 2 + case C.can_msg_power_dispense_slot4: + ret.Slot = 3 + case C.can_msg_power_dispense_slot5: + ret.Slot = 4 + } + return ret, nil +} + +func (f DispenseMessage) Encode() can.Frame { + data := [8]uint8{} + switch f.Slot { + case DispenseSlotOff: + data[0] = C.can_msg_power_dispense_off + case 0: + data[0] = C.can_msg_power_dispense_slot1 + case 1: + data[0] = C.can_msg_power_dispense_slot2 + case 2: + data[0] = C.can_msg_power_dispense_slot3 + case 3: + data[0] = C.can_msg_power_dispense_slot4 + case 4: + data[0] = C.can_msg_power_dispense_slot5 + } + return can.Frame{ + ID: uint32(C.can_msg_power_dispense), + Length: 1, + Data: data, + } +} + +type AutoMessage struct { + AutoUpdate bool +} + +func DecodeAutoMessage(frame can.Frame) (AutoMessage, error) { + var ret AutoMessage + if frame.ID != uint32(C.can_msg_auto_status) { + return ret, InvalidMessageID + } + if frame.Length != 1 { + return ret, InvalidMessageLength + } + // we accept all non-null values as "enable" + if frame.Data[0] != C.can_msg_auto_status_disable { + ret.AutoUpdate = true + } + return ret, nil +} + +func (f AutoMessage) Encode() can.Frame { + data := [8]uint8{} + if f.AutoUpdate { + data[0] = C.can_msg_auto_status_enable + } else { + data[0] = C.can_msg_auto_status_disable + } + return can.Frame{ + ID: uint32(C.can_msg_auto_status), + Length: 1, + Data: data, + } +} diff --git a/uncanny/server.go b/uncanny/server.go new file mode 100644 index 0000000..db0f8bf --- /dev/null +++ b/uncanny/server.go @@ -0,0 +1,119 @@ +package uncanny + +import ( + "log" + "fmt" + "net/http" + "path" + "strconv" +) + +const ( + // highest slot number (range: 0..maxSlot) + maxSlot = 4 +) + +type Server struct { + *http.ServeMux + can *Can +} + +func NewServer(can *Can) *Server { + ret := &Server{ + ServeMux: http.NewServeMux(), + can: can, + } + ret.registerHandlers() + return ret +} + +func (s *Server) registerHandlers() { + s.HandleFunc("/", http.NotFound) + s.HandleFunc("/dispense/", func(rw http.ResponseWriter, r *http.Request) { + if matched, _ := path.Match("/dispense/[0-9]?[0-9]*", r.URL.Path); matched { + slot, err := strconv.ParseUint(path.Base(r.URL.Path), 10, 32) + if err != nil { + log.Printf("Error decoding slot number: %v", err) + http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + } else if slot < 0 || slot > maxSlot { + log.Printf("Invalid slot number: %u", slot) + http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + } else { + err = s.can.Dispense(int(slot)) + if err != nil { + log.Printf("Error sending dispense command: %v", err) + rw.Header().Set("Content-Type", "text/plain") + rw.WriteHeader(http.StatusServiceUnavailable) + fmt.Fprintf(rw, "%u error", slot) + } else { + rw.Header().Set("Content-Type", "text/plain") + rw.WriteHeader(http.StatusOK) + fmt.Fprintf(rw, "%u dispense", slot) + } + } + } else { + http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + } + }) + s.HandleFunc("/stop", func(rw http.ResponseWriter, r *http.Request) { + err := s.can.Cancel() + if err != nil { + log.Printf("Error sending stop command: %v", err) + rw.Header().Set("Content-Type", "text/plain") + rw.WriteHeader(http.StatusServiceUnavailable) + fmt.Fprintf(rw, "error") + } else { + rw.Header().Set("Content-Type", "text/plain") + rw.WriteHeader(http.StatusOK) + fmt.Fprintf(rw, "stopped") + } + }) + s.HandleFunc("/level/", func(rw http.ResponseWriter, r *http.Request) { + if matched, _ := path.Match("/level/[0-9]?[0-9]*", r.URL.Path); matched { + slot, err := strconv.ParseUint(path.Base(r.URL.Path), 10, 32) + if err != nil { + log.Printf("Error decoding slot number: %v", err) + http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + } else if slot < 0 || slot > maxSlot { + log.Printf("Invalid slot number: %u", slot) + http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + } else { + rw.Header().Set("Content-Type", "text/plain") + rw.WriteHeader(http.StatusOK) + if s.can.IsEmpty(int(slot)) { + fmt.Fprintf(rw, "%u empty", slot) + } else { + fmt.Fprintf(rw, "%u full", slot) + } + } + } else { + http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + } + }) + s.HandleFunc("/active/", func(rw http.ResponseWriter, r *http.Request) { + if matched, _ := path.Match("/active/[0-9]?[0-9]*", r.URL.Path); matched { + slot, err := strconv.ParseUint(path.Base(r.URL.Path), 10, 32) + if err != nil { + log.Printf("Error decoding slot number: %v", err) + http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + } else if slot < 0 || slot > maxSlot { + log.Printf("Invalid slot number: %u", slot) + http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + } else { + rw.Header().Set("Content-Type", "text/plain") + rw.WriteHeader(http.StatusOK) + if s.can.IsDispensing(int(slot)) { + fmt.Fprintf(rw, "%u dispensing", slot) + } else { + fmt.Fprintf(rw, "%u off", slot) + } + } + } else { + http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + } + }) +} + +func (s *Server) ListenAndServe(addr string) error { + return http.ListenAndServe(addr, s) +}