package xbee import ( "bytes" "encoding/binary" "fmt" ) // This code is handled slightly differently. Rather than use structs to represent // the frame data, we instead use an interface that represents an AT command, and // standalone functions that convert the AT command to frame data. // this way, we can add command support without needing to compose structs. // The downside is that it's more difficult to deal with. // // AT command responses are handled with a struct to get the response status code. // // an ATCmd is anything that has a Payload function that returns the given payload // pre-formatted to be send over the wire, and a Cmd command that returns the 2-character // ATcommand itself. It must also have a parse response function that takes the response struct. type ATCmd interface { Payload() []byte // converts the AT command options to the binary argument format Cmd() [2]rune // returns the AT command string. Parse(*ATCmdResponse) error // takes a command response and parses the data into itself. } // The AT command, in its raw format. we don't handle parameters since it's dealt with by // the interface. type RawATCmd []byte // implements frameable. this is kinda stupid but makes it more safe. func (b RawATCmd) Bytes() []byte { return b } // EncodeATCommand takes an AT command and encodes it in the payload format. // it takes the frame index (which can be zero) as well as if it should be queued or // not. It encodes the AT command to be framed and sent over the wire and returns the packet func encodeATCommand(at ATCmd, idx uint8, queued bool) RawATCmd { // we encode a new byte slice that contains the cmd + payload concatenated correclty. // this is then used to make the command frame, which contains ID/Type/Queued or not. // the ATCmdFrame can be converted to bytes to be sent over the wire once framed. buf := new(bytes.Buffer) // we change the frame type based on if it's queued. if queued { buf.WriteByte(byte(ATCmdQueueType)) } else { buf.WriteByte(byte(ATCmdType)) } // next is the provided frame identifier, used for callbacks. buf.WriteByte(idx) cmd := at.Cmd() buf.WriteByte(byte(cmd[0])) buf.WriteByte(byte(cmd[1])) // the payload can be empty. This would make it a query. p := at.Payload() buf.Write(p) return buf.Bytes() } type ATCmdResponse struct { Cmd string Status ATCmdStatus Data []byte } func ParseATCmdResponse(p []byte) (*ATCmdResponse, error) { if p[0] != 0x88 { return nil, fmt.Errorf("invalid frame type 0x%x", p[0]) } resp := &ATCmdResponse{ Cmd: string(p[2:4]), Status: ATCmdStatus(p[4]), // TODO: check if this overflows when there's no payload. Data: p[5:], } return resp, nil } //go:generate stringer -output=at_cmd_status.go -type ATCmdStatus type ATCmdStatus uint8 const ( ATCmdStatusOK ATCmdStatus = 0 ATCmdStatusErr ATCmdStatus = 1 ATCmdStatusInvalidCmd ATCmdStatus = 2 ATCmdStatusInvalidParam ATCmdStatus = 3 ) type RemoteATCmdReq struct { Destination uint64 Options uint8 } func encodeRemoteATCommand(at ATCmd, idx uint8, queued bool, destination uint64) RawATCmd { // sizing take from buf := new(bytes.Buffer) buf.WriteByte(byte(RemoteCmdReqType)) buf.WriteByte(idx) binary.Write(buf, binary.BigEndian, destination) binary.Write(buf, binary.BigEndian, uint16(0xFFFE)) // set remote command options. if idx = 0, we set bit zero (disable ack) // if queued is true, we clear bit one (if false we set it) var options uint8 = 0 if idx == 0 { options = options | 0x1 } if !queued { options = options | 0x2 } // write AT command cmd := at.Cmd() buf.WriteByte(byte(cmd[0])) buf.WriteByte(byte(cmd[1])) // write payload. buf.Write(at.Payload()) return buf.Bytes() } // let's actually define some AT commands now.