github.com/arduino/arduino-cloud-cli@v0.0.0-20240517070944-e7a449561083/internal/serial/serial.go (about) 1 // This file is part of arduino-cloud-cli. 2 // 3 // Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published 7 // by the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <https://www.gnu.org/licenses/>. 17 18 package serial 19 20 import ( 21 "bytes" 22 "context" 23 "encoding/binary" 24 "errors" 25 "fmt" 26 "time" 27 28 "github.com/howeyc/crc16" 29 "go.bug.st/serial" 30 ) 31 32 // Serial is a wrapper of serial port interface that 33 // features specific functions to send provisioning 34 // commands through the serial port to an arduino device. 35 type Serial struct { 36 port serial.Port 37 } 38 39 // NewSerial instantiate and returns a Serial instance. 40 // The Serial Connect method should be called before using 41 // its send/receive functions. 42 func NewSerial() *Serial { 43 s := &Serial{} 44 return s 45 } 46 47 // Connect tries to connect Serial to a specific serial port. 48 func (s *Serial) Connect(address string) error { 49 mode := &serial.Mode{ 50 BaudRate: 57600, 51 } 52 port, err := serial.Open(address, mode) 53 if err != nil { 54 err = fmt.Errorf("%s: %w", "connecting to serial port", err) 55 return err 56 } 57 s.port = port 58 59 s.port.SetReadTimeout(time.Millisecond * 2500) 60 return nil 61 } 62 63 // Send allows to send a provisioning command to a connected arduino device. 64 func (s *Serial) Send(ctx context.Context, cmd Command, payload []byte) error { 65 if err := ctx.Err(); err != nil { 66 return err 67 } 68 69 payload = append([]byte{byte(cmd)}, payload...) 70 msg := encode(Cmd, payload) 71 72 _, err := s.port.Write(msg) 73 if err != nil { 74 err = fmt.Errorf("%s: %w", "sending message through serial", err) 75 return err 76 } 77 78 return nil 79 } 80 81 // SendReceive allows to send a provisioning command to a connected arduino device. 82 // Then, it waits for a response from the device and, if any, returns it. 83 // If no response is received after 2 seconds, an error is returned. 84 func (s *Serial) SendReceive(ctx context.Context, cmd Command, payload []byte) ([]byte, error) { 85 if err := s.Send(ctx, cmd, payload); err != nil { 86 return nil, err 87 } 88 return s.receive(ctx) 89 } 90 91 // Close should be used when the Serial connection isn't used anymore. 92 // After that, Serial could Connect again to any port. 93 func (s *Serial) Close() error { 94 return s.port.Close() 95 } 96 97 // receive allows to wait for a response from an arduino device under provisioning. 98 // Its timeout is set to 2 seconds. It returns an error if the response is not valid 99 // or if the timeout expires. 100 // TODO: consider refactoring using a more explicit procedure: 101 // start := s.Read(buff, MsgStartLength) 102 // payloadLen := s.Read(buff, payloadFieldLen) 103 func (s *Serial) receive(ctx context.Context) ([]byte, error) { 104 buff := make([]byte, 1000) 105 var resp []byte 106 107 received := 0 108 payloadLen := 0 109 // Wait to receive the entire packet that is long as the preamble (from msgStart to payload length field) 110 // plus the actual payload length plus the length of the ending sequence. 111 for received < (payloadLenField+payloadLenFieldLen)+payloadLen+len(msgEnd) { 112 if err := ctx.Err(); err != nil { 113 return nil, err 114 } 115 116 n, err := s.port.Read(buff) 117 if err != nil { 118 err = fmt.Errorf("%s: %w", "receiving from serial", err) 119 return nil, err 120 } 121 if n == 0 { 122 break 123 } 124 received += n 125 resp = append(resp, buff[:n]...) 126 127 // Update the payload length as soon as it is received. 128 if payloadLen == 0 && received >= (payloadLenField+payloadLenFieldLen) { 129 payloadLen = int(binary.BigEndian.Uint16(resp[payloadLenField:(payloadLenField + payloadLenFieldLen)])) 130 // TODO: return error if payloadLen is too large. 131 } 132 } 133 134 if received == 0 { 135 err := errors.New("receiving from serial: timeout, nothing received") 136 return nil, err 137 } 138 139 // TODO: check if msgStart is present 140 141 if !bytes.Equal(resp[received-len(msgEnd):], msgEnd[:]) { 142 err := errors.New("receiving from serial: end of message (0xAA, 0x55) not found") 143 return nil, err 144 } 145 146 payload := resp[payloadField : payloadField+payloadLen-crcFieldLen] 147 ch := crc16.Checksum(payload, crc16.CCITTTable) 148 // crc is contained in the last bytes of the payload 149 cp := binary.BigEndian.Uint16(resp[payloadField+payloadLen-crcFieldLen : payloadField+payloadLen]) 150 if ch != cp { 151 err := errors.New("receiving from serial: signature of received message is not valid") 152 return nil, err 153 } 154 155 return payload, nil 156 } 157 158 // encode is internally used to create a valid provisioning packet. 159 func encode(mType MsgType, msg []byte) []byte { 160 // Insert the preamble sequence followed by the message type 161 packet := append(msgStart[:], byte(mType)) 162 163 // Append the packet length 164 bLen := make([]byte, payloadLenFieldLen) 165 binary.BigEndian.PutUint16(bLen, (uint16(len(msg) + crcFieldLen))) 166 packet = append(packet, bLen...) 167 168 // Append the message payload 169 packet = append(packet, msg...) 170 171 // Calculate and append the message signature 172 ch := crc16.Checksum(msg, crc16.CCITTTable) 173 checksum := make([]byte, crcFieldLen) 174 binary.BigEndian.PutUint16(checksum, ch) 175 packet = append(packet, checksum...) 176 177 // Append final byte sequence 178 packet = append(packet, msgEnd[:]...) 179 return packet 180 }