236 lines
6.2 KiB
Go
236 lines
6.2 KiB
Go
|
package xbee
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"encoding/binary"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
)
|
||
|
|
||
|
// the frames have an outer shell - we will make a function that takes
|
||
|
// an inner frame element and wraps it in the appropriate headers.
|
||
|
|
||
|
// first, we should make it take the frame directly, so we make an interface
|
||
|
// that represents "framable" things. note that bytes.Buffer also fulfils this.
|
||
|
|
||
|
type Frameable interface {
|
||
|
// returns the API identifier for this frame.
|
||
|
GetId() byte
|
||
|
// encodes this frame correctly.
|
||
|
Bytes() ([]byte, error)
|
||
|
}
|
||
|
|
||
|
// now we can describe our function that takes a framable and contains it + calculates checksums.
|
||
|
func calculateChecksum(data []byte) byte {
|
||
|
var sum byte
|
||
|
for _, v := range data {
|
||
|
sum += v
|
||
|
}
|
||
|
return 0xFF - sum
|
||
|
}
|
||
|
|
||
|
func WriteFrame(w io.Writer, cmd Frameable) (n int, err error) {
|
||
|
frame_data, err := cmd.Bytes()
|
||
|
|
||
|
if err != nil {
|
||
|
return
|
||
|
}
|
||
|
frame := make([]byte, len(frame_data)+4)
|
||
|
frame[0] = 0x7E
|
||
|
|
||
|
binary.BigEndian.PutUint16(frame[1:], uint16(len(frame_data)))
|
||
|
|
||
|
copy(frame[3:], frame_data)
|
||
|
|
||
|
chk := calculateChecksum(frame_data)
|
||
|
|
||
|
frame[len(frame)-1] = chk
|
||
|
return w.Write(frame)
|
||
|
}
|
||
|
|
||
|
func makeXbeeApiFrame(cmd Frameable) ([]byte, error) {
|
||
|
dataBuf, _ := cmd.Bytes()
|
||
|
frameBuf := make([]byte, len(dataBuf)+4)
|
||
|
|
||
|
// move data and construct the frame
|
||
|
|
||
|
frameBuf[0] = 0x7E // start delimiter
|
||
|
|
||
|
// length
|
||
|
// todo: check endiannes (0x7e, msb lsb)
|
||
|
binary.BigEndian.PutUint16(frameBuf[1:3], uint16(len(dataBuf)))
|
||
|
|
||
|
copy(frameBuf[3:], dataBuf)
|
||
|
|
||
|
chksum := calculateChecksum(dataBuf)
|
||
|
|
||
|
frameBuf[len(frameBuf)-1] = chksum
|
||
|
|
||
|
return frameBuf, nil
|
||
|
}
|
||
|
|
||
|
// now we can describe frames in other files that implement Frameable.
|
||
|
// the remaining challenge is reception and actual API frames.
|
||
|
// xbee uses the first byte of the "frame data" as the API identifier or command.
|
||
|
|
||
|
//go:generate stringer -output=api_frame_cmd.go -type xbeeCmd
|
||
|
type XBeeCmd byte
|
||
|
|
||
|
const (
|
||
|
// commands sent to the xbee s3b
|
||
|
|
||
|
ATCmd XBeeCmd = 0x08 // AT Command
|
||
|
ATCmdQueue XBeeCmd = 0x09 // AT Command - Queue Parameter Value
|
||
|
TxReq XBeeCmd = 0x10 // TX Request
|
||
|
TxReqExpl XBeeCmd = 0x11 // Explicit TX Request
|
||
|
RemoteCmdReq XBeeCmd = 0x17 // Remote Command Request
|
||
|
// commands recieved from the xbee
|
||
|
|
||
|
ATCmdResponse XBeeCmd = 0x88 // AT Command Response
|
||
|
ModemStatus XBeeCmd = 0x8A // Modem Status
|
||
|
TxStatus XBeeCmd = 0x8B // Transmit Status
|
||
|
RouteInfoPkt XBeeCmd = 0x8D // Route information packet
|
||
|
AddrUpdate XBeeCmd = 0x8E // Aggregate Addressing Update
|
||
|
RxPkt XBeeCmd = 0x90 // RX Indicator (AO=0)
|
||
|
RxPktExpl XBeeCmd = 0x91 // Explicit RX Indicator (AO=1)
|
||
|
IOSample XBeeCmd = 0x92 // Data Sample RX Indicator
|
||
|
NodeId XBeeCmd = 0x95 // Note Identification Indicator
|
||
|
RemoteCmdResp XBeeCmd = 0x97 // Remote Command Response
|
||
|
)
|
||
|
|
||
|
// AT commands are hard, so let's write out all the major ones here
|
||
|
|
||
|
type ATCmdFrame struct {
|
||
|
Id byte
|
||
|
Cmd string
|
||
|
Param []byte
|
||
|
Queued bool
|
||
|
}
|
||
|
|
||
|
// implement the frame stuff for us.
|
||
|
func (atFrame *ATCmdFrame) Bytes() ([]byte, error) {
|
||
|
buf := new(bytes.Buffer)
|
||
|
|
||
|
if atFrame.Queued {
|
||
|
// queued (batched) at comamnds have different Frame type
|
||
|
buf.WriteByte(byte(ATCmdQueue))
|
||
|
|
||
|
} else {
|
||
|
// normal frame type
|
||
|
buf.WriteByte(byte(ATCmd))
|
||
|
|
||
|
}
|
||
|
|
||
|
buf.WriteByte(atFrame.Id)
|
||
|
|
||
|
// write cmd, if it's the right length.
|
||
|
if cmdLen := len(atFrame.Cmd); cmdLen != 2 {
|
||
|
return nil, fmt.Errorf("AT command incorrect length: %d", cmdLen)
|
||
|
}
|
||
|
buf.Write([]byte(atFrame.Cmd))
|
||
|
|
||
|
// write param.
|
||
|
buf.Write(atFrame.Param)
|
||
|
return buf.Bytes(), nil
|
||
|
}
|
||
|
|
||
|
// transmissions to this address are instead broadcast
|
||
|
const BroadcastAddr = 0xFFFF
|
||
|
|
||
|
type TxFrame struct {
|
||
|
Id byte
|
||
|
Destination uint64
|
||
|
BCastRadius uint8
|
||
|
Options uint8
|
||
|
Payload []byte
|
||
|
}
|
||
|
|
||
|
func (txFrame *TxFrame) Bytes() ([]byte, error) {
|
||
|
buf := new(bytes.Buffer)
|
||
|
|
||
|
buf.WriteByte(byte(TxReq))
|
||
|
|
||
|
buf.WriteByte(txFrame.Id)
|
||
|
|
||
|
a := make([]byte, 8)
|
||
|
binary.LittleEndian.PutUint64(a, txFrame.Destination)
|
||
|
buf.Write(a)
|
||
|
|
||
|
// write the reserved part.
|
||
|
buf.Write([]byte{0xFF, 0xFE})
|
||
|
|
||
|
// write the radius
|
||
|
buf.WriteByte(txFrame.BCastRadius)
|
||
|
|
||
|
buf.WriteByte(txFrame.Options)
|
||
|
|
||
|
buf.Write(txFrame.Payload)
|
||
|
|
||
|
return buf.Bytes(), nil
|
||
|
}
|
||
|
|
||
|
type RemoteATCmdReq struct {
|
||
|
ATCmdFrame
|
||
|
Destination uint64
|
||
|
Options uint8
|
||
|
}
|
||
|
|
||
|
func (remoteAT *RemoteATCmdReq) Bytes() ([]byte, error) {
|
||
|
buf := new(bytes.Buffer)
|
||
|
buf.WriteByte(byte(RemoteCmdReq))
|
||
|
|
||
|
buf.WriteByte(remoteAT.Id)
|
||
|
|
||
|
a := make([]byte, 8)
|
||
|
binary.LittleEndian.PutUint64(a, remoteAT.Destination)
|
||
|
buf.Write(a)
|
||
|
|
||
|
// write the reserved part.
|
||
|
buf.Write([]byte{0xFF, 0xFE})
|
||
|
// write options
|
||
|
buf.WriteByte(remoteAT.Options)
|
||
|
|
||
|
// now, write the AT command and the data.
|
||
|
buf.Write([]byte(remoteAT.Cmd))
|
||
|
|
||
|
buf.Write(remoteAT.Param)
|
||
|
|
||
|
return buf.Bytes(), nil
|
||
|
|
||
|
}
|
||
|
|
||
|
// Now we will implement receiving packets from a character stream.
|
||
|
// we first need to make a thing that produces frames from a stream using a scanner.
|
||
|
|
||
|
// this is a split function for bufio.scanner. It makes it easier to handle the FSM
|
||
|
// for extracting data from a stream. For the Xbee, this means that we must
|
||
|
// find the magic start character, (check that it's escaped), read the length,
|
||
|
// and then ensure we have enough length to finish the token, requesting more data
|
||
|
// if we do not.
|
||
|
//
|
||
|
// see https://pkg.go.dev/bufio#SplitFunc for more info
|
||
|
// https://medium.com/golangspec/in-depth-introduction-to-bufio-scanner-in-golang-55483bb689b4
|
||
|
func xbeeFrameSplit(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||
|
if atEOF && len(data) == 0 {
|
||
|
// there's no data, request more.
|
||
|
return 0, nil, nil
|
||
|
}
|
||
|
|
||
|
if startIdx := bytes.IndexByte(data, 0x7E); startIdx >= 0 {
|
||
|
// we have a start character. get the length.
|
||
|
// we add 4 since start delimiter (1) + length (2) + checksum (1).
|
||
|
// the length inside the packet represents the frame data only.
|
||
|
var frameLen = binary.BigEndian.Uint16(data[startIdx+1:startIdx+3]) + 4
|
||
|
if len(data[startIdx:]) < int(frameLen) {
|
||
|
// we got the length, but there's not enough data for the frame. we can trim the
|
||
|
// data that came before the start, but not return a token.
|
||
|
return startIdx, nil, nil
|
||
|
}
|
||
|
// there is enough data to pull a frame.
|
||
|
// todo: check checksum here? we can return an error.
|
||
|
return startIdx + int(frameLen), data[startIdx : startIdx+int(frameLen)], nil
|
||
|
}
|
||
|
// we didn't find a start character in our data, so request more. trash everythign given to us
|
||
|
return len(data), nil, nil
|
||
|
}
|