|
| 1 | +package serial |
| 2 | + |
| 3 | +import ( |
| 4 | + "bytes" |
| 5 | + "encoding/binary" |
| 6 | + "errors" |
| 7 | + "fmt" |
| 8 | + "time" |
| 9 | + |
| 10 | + "github.com/howeyc/crc16" |
| 11 | + "go.bug.st/serial" |
| 12 | +) |
| 13 | + |
| 14 | +// Serial is a wrapper of serial port interface that |
| 15 | +// features specific functions to send provisioning |
| 16 | +// commands through the serial port to an arduino device. |
| 17 | +type Serial struct { |
| 18 | + port serial.Port |
| 19 | +} |
| 20 | + |
| 21 | +// NewSerial instantiate and returns a Serial instance. |
| 22 | +// The Serial Connect method should be called before using |
| 23 | +// its send/receive functions. |
| 24 | +func NewSerial() *Serial { |
| 25 | + s := &Serial{} |
| 26 | + return s |
| 27 | +} |
| 28 | + |
| 29 | +// Connect tries to connect Serial to a specific serial port. |
| 30 | +func (s *Serial) Connect(address string) error { |
| 31 | + mode := &serial.Mode{ |
| 32 | + BaudRate: 57600, |
| 33 | + } |
| 34 | + port, err := serial.Open(address, mode) |
| 35 | + if err != nil { |
| 36 | + err = fmt.Errorf("%s: %w", "connecting to serial port", err) |
| 37 | + return err |
| 38 | + } |
| 39 | + s.port = port |
| 40 | + |
| 41 | + s.port.SetReadTimeout(time.Millisecond * 2000) |
| 42 | + return nil |
| 43 | +} |
| 44 | + |
| 45 | +// Send allows to send a provisioning command to a connected arduino device. |
| 46 | +func (s *Serial) Send(cmd Command, payload []byte) error { |
| 47 | + payload = append([]byte{byte(cmd)}, payload...) |
| 48 | + msg := encode(Cmd, payload) |
| 49 | + |
| 50 | + _, err := s.port.Write(msg) |
| 51 | + if err != nil { |
| 52 | + err = fmt.Errorf("%s: %w", "sending message through serial", err) |
| 53 | + return err |
| 54 | + } |
| 55 | + |
| 56 | + return nil |
| 57 | +} |
| 58 | + |
| 59 | +// SendReceive allows to send a provisioning command to a connected arduino device. |
| 60 | +// Then, it waits for a response from the device and, if any, returns it. |
| 61 | +// If no response is received after 2 seconds, an error is returned. |
| 62 | +func (s *Serial) SendReceive(cmd Command, payload []byte) ([]byte, error) { |
| 63 | + err := s.Send(cmd, payload) |
| 64 | + if err != nil { |
| 65 | + return nil, err |
| 66 | + } |
| 67 | + return s.receive() |
| 68 | +} |
| 69 | + |
| 70 | +// Close should be used when the Serial connection isn't used anymore. |
| 71 | +// After that, Serial could Connect again to any port. |
| 72 | +func (s *Serial) Close() error { |
| 73 | + return s.port.Close() |
| 74 | +} |
| 75 | + |
| 76 | +// receive allows to wait for a response from an arduino device under provisioning. |
| 77 | +// Its timeout is set to 2 seconds. It returns an error if the response is not valid |
| 78 | +// or if the timeout expires. |
| 79 | +// TODO: consider refactoring using a more explicit procedure: |
| 80 | +// start := s.Read(buff, MsgStartLength) |
| 81 | +// payloadLen := s.Read(buff, payloadFieldLen) |
| 82 | +func (s *Serial) receive() ([]byte, error) { |
| 83 | + buff := make([]byte, 1000) |
| 84 | + var resp []byte |
| 85 | + |
| 86 | + received := 0 |
| 87 | + payloadLen := 0 |
| 88 | + // Wait to receive the entire packet that is long as the preamble (from msgStart to payload length field) |
| 89 | + // plus the actual payload length plus the length of the ending sequence. |
| 90 | + for received < (payloadLenField+payloadLenFieldLen)+payloadLen+len(msgEnd) { |
| 91 | + n, err := s.port.Read(buff) |
| 92 | + if err != nil { |
| 93 | + err = fmt.Errorf("%s: %w", "receiving from serial", err) |
| 94 | + return nil, err |
| 95 | + } |
| 96 | + if n == 0 { |
| 97 | + break |
| 98 | + } |
| 99 | + received += n |
| 100 | + resp = append(resp, buff[:n]...) |
| 101 | + |
| 102 | + // Update the payload length as soon as it is received. |
| 103 | + if payloadLen == 0 && received >= (payloadLenField+payloadLenFieldLen) { |
| 104 | + payloadLen = int(binary.BigEndian.Uint16(resp[payloadLenField:(payloadLenField + payloadLenFieldLen)])) |
| 105 | + // TODO: return error if payloadLen is too large. |
| 106 | + } |
| 107 | + } |
| 108 | + |
| 109 | + if received == 0 { |
| 110 | + err := errors.New("receiving from serial: timeout, nothing received") |
| 111 | + return nil, err |
| 112 | + } |
| 113 | + |
| 114 | + // TODO: check if msgStart is present |
| 115 | + |
| 116 | + if !bytes.Equal(resp[received-len(msgEnd):], msgEnd[:]) { |
| 117 | + err := errors.New("receiving from serial: end of message (0xAA, 0x55) not found") |
| 118 | + return nil, err |
| 119 | + } |
| 120 | + |
| 121 | + payload := resp[payloadField : payloadField+payloadLen-crcFieldLen] |
| 122 | + ch := crc16.Checksum(payload, crc16.CCITTTable) |
| 123 | + // crc is contained in the last bytes of the payload |
| 124 | + cp := binary.BigEndian.Uint16(resp[payloadField+payloadLen-crcFieldLen : payloadField+payloadLen]) |
| 125 | + if ch != cp { |
| 126 | + err := errors.New("receiving from serial: signature of received message is not valid") |
| 127 | + return nil, err |
| 128 | + } |
| 129 | + |
| 130 | + return payload, nil |
| 131 | +} |
| 132 | + |
| 133 | +// encode is internally used to create a valid provisioning packet |
| 134 | +func encode(mType MsgType, msg []byte) []byte { |
| 135 | + // Insert the preamble sequence followed by the message type |
| 136 | + packet := append(msgStart[:], byte(mType)) |
| 137 | + |
| 138 | + // Append the packet length |
| 139 | + bLen := make([]byte, payloadLenFieldLen) |
| 140 | + binary.BigEndian.PutUint16(bLen, (uint16(len(msg) + crcFieldLen))) |
| 141 | + packet = append(packet, bLen...) |
| 142 | + |
| 143 | + // Append the message payload |
| 144 | + packet = append(packet, msg...) |
| 145 | + |
| 146 | + // Calculate and append the message signature |
| 147 | + ch := crc16.Checksum(msg, crc16.CCITTTable) |
| 148 | + checksum := make([]byte, crcFieldLen) |
| 149 | + binary.BigEndian.PutUint16(checksum, ch) |
| 150 | + packet = append(packet, checksum...) |
| 151 | + |
| 152 | + // Append final byte sequence |
| 153 | + packet = append(packet, msgEnd[:]...) |
| 154 | + return packet |
| 155 | +} |
0 commit comments