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  }