github.com/simpleiot/simpleiot@v0.18.3/client/serial.go (about)

     1  package client
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/binary"
     6  	"fmt"
     7  	"io"
     8  	"log"
     9  	"path/filepath"
    10  	"strconv"
    11  	"time"
    12  
    13  	"github.com/fsnotify/fsnotify"
    14  	"github.com/kjx98/crc16"
    15  	"github.com/nats-io/nats.go"
    16  	"github.com/simpleiot/simpleiot/data"
    17  	"github.com/simpleiot/simpleiot/test"
    18  	"go.bug.st/serial"
    19  )
    20  
    21  // SerialDev represents a serial (MCU) config
    22  type SerialDev struct {
    23  	ID                string `node:"id"`
    24  	Parent            string `node:"parent"`
    25  	Description       string `point:"description"`
    26  	Port              string `point:"port"`
    27  	Baud              string `point:"baud"`
    28  	MaxMessageLength  int    `point:"maxMessageLength"`
    29  	HRDestNode        string `point:"hrDest"`
    30  	SyncParent        bool   `point:"syncParent"`
    31  	Debug             int    `point:"debug"`
    32  	Disabled          bool   `point:"disabled"`
    33  	Log               string `point:"log"`
    34  	Rx                int    `point:"rx"`
    35  	RxReset           bool   `point:"rxReset"`
    36  	Tx                int    `point:"tx"`
    37  	TxReset           bool   `point:"txReset"`
    38  	HrRx              int64  `point:"hrRx"`
    39  	HrRxReset         bool   `point:"hrRxReset"`
    40  	Uptime            int    `point:"uptime"`
    41  	ErrorCount        int    `point:"errorCount"`
    42  	ErrorCountReset   bool   `point:"errorCountReset"`
    43  	ErrorCountHR      int    `point:"errorCountHR"`
    44  	ErrorCountResetHR bool   `point:"errorCountResetHR"`
    45  	Rate              bool   `point:"rate"`
    46  	RateHR            bool   `point:"rate"`
    47  	Connected         bool   `point:"connected"`
    48  	Download          string `point:"download"`
    49  	Progress          int    `point:"progress"`
    50  	Files             []File `child:"file"`
    51  }
    52  
    53  type sendData struct {
    54  	seq     byte
    55  	ack     bool
    56  	subject string
    57  	points  data.Points
    58  }
    59  
    60  // SerialDevClient is a SIOT client used to manage serial devices
    61  type SerialDevClient struct {
    62  	nc                  *nats.Conn
    63  	config              SerialDev
    64  	stop                chan struct{}
    65  	newPoints           chan NewPoints
    66  	newEdgePoints       chan NewPoints
    67  	wrSeq               byte // convention is we increment this right before sending a packet
    68  	lastSendStats       time.Time
    69  	natsSub             string
    70  	natsSubSerialPoints string
    71  	natsSubHRUp         string
    72  	parentSubscription  *nats.Subscription
    73  	ratePointCount      int
    74  	ratePointCountHR    int
    75  	rateLastSend        time.Time
    76  	portCobsWrapper     *CobsWrapper
    77  	sendPointsCh        chan sendData
    78  }
    79  
    80  // NewSerialDevClient ...
    81  func NewSerialDevClient(nc *nats.Conn, config SerialDev) Client {
    82  	ret := &SerialDevClient{
    83  		nc:            nc,
    84  		config:        config,
    85  		stop:          make(chan struct{}),
    86  		newPoints:     make(chan NewPoints),
    87  		newEdgePoints: make(chan NewPoints),
    88  		natsSub:       SubjectNodePoints(config.ID),
    89  		sendPointsCh:  make(chan sendData),
    90  	}
    91  
    92  	ret.populateNatsSubjects()
    93  
    94  	return ret
    95  }
    96  
    97  func (sd *SerialDevClient) populateNatsSubjects() {
    98  	phrup := fmt.Sprintf("phrup.%v.%v", sd.config.Parent, sd.config.ID)
    99  	if sd.config.HRDestNode != "" {
   100  		phrup = fmt.Sprintf("phrup.%v.%v", sd.config.HRDestNode, sd.config.ID)
   101  	}
   102  	sd.natsSubHRUp = phrup
   103  
   104  	if sd.parentSubscription != nil {
   105  		err := sd.parentSubscription.Unsubscribe()
   106  		if err != nil {
   107  			log.Println("Serial: error unsubscribing from parent sub:", err)
   108  		}
   109  		sd.parentSubscription = nil
   110  	}
   111  
   112  	if sd.config.SyncParent {
   113  		sd.natsSubSerialPoints = SubjectNodePoints(sd.config.Parent)
   114  		var err error
   115  		// Copy some config to avoid race conditions
   116  		serialID := sd.config.ID
   117  		debug := sd.config.Debug
   118  		sd.parentSubscription, err = sd.nc.Subscribe(sd.natsSubSerialPoints, func(msg *nats.Msg) {
   119  			points, err := data.PbDecodePoints(msg.Data)
   120  			if err != nil {
   121  				log.Println("Error decoding points in serial parent:", err)
   122  				return
   123  			}
   124  
   125  			// only send points whose orgin is not the serial node ID as those are just
   126  			// getting echo'd back
   127  			var pointsToSend data.Points
   128  
   129  			for _, p := range points {
   130  				if p.Origin != serialID {
   131  					pointsToSend = append(pointsToSend, p)
   132  				}
   133  			}
   134  
   135  			if len(pointsToSend) > 0 {
   136  				if sd.portCobsWrapper == nil {
   137  					if debug >= 4 {
   138  						log.Printf("Serial port closed; points not sent: %v", pointsToSend)
   139  					}
   140  					return
   141  				}
   142  				sd.sendPointsCh <- sendData{points: pointsToSend}
   143  			}
   144  		})
   145  		if err != nil {
   146  			log.Println("Error subscribing to serial parent:", err)
   147  		}
   148  	} else {
   149  		sd.natsSubSerialPoints = SubjectNodePoints(sd.config.ID)
   150  	}
   151  }
   152  
   153  // if seq == 0, then sd.wrSeq is used
   154  func (sd *SerialDevClient) sendPointsToDevice(seq byte, ack bool, sub string, pts data.Points) error {
   155  	if seq == 0 {
   156  		seq = sd.wrSeq
   157  	}
   158  
   159  	if sub == "" {
   160  		sub = "proto"
   161  	}
   162  
   163  	d, err := SerialEncode(seq, sub, pts)
   164  	if err != nil {
   165  		return fmt.Errorf("error encoding points to send to MCU: %w", err)
   166  	}
   167  
   168  	if sd.config.Debug >= 4 {
   169  		if len(pts) > 0 {
   170  			log.Printf("SER TX (%v) seq:%v sub:%v :\n%v", sd.config.Description, seq, sub, pts)
   171  		} else {
   172  			log.Printf("SER TX (%v) seq:%v sub:%v\n", sd.config.Description, seq, sub)
   173  		}
   174  	}
   175  
   176  	if sd.portCobsWrapper != nil {
   177  		_, err = sd.portCobsWrapper.Write(d)
   178  		if err != nil {
   179  			return fmt.Errorf("error writing data to port: %w", err)
   180  		}
   181  
   182  		sd.config.Tx++
   183  		err = SendPoints(sd.nc, sd.natsSub,
   184  			data.Points{{Type: data.PointTypeTx, Value: float64(sd.config.Tx)}},
   185  			false)
   186  
   187  		if err != nil {
   188  			return fmt.Errorf("Error sending Serial tx stats: %w", err)
   189  		}
   190  	}
   191  
   192  	if !ack {
   193  		_ = ack
   194  		// TODO: we need to check for response and implement retries
   195  		// yet.
   196  	}
   197  
   198  	return nil
   199  }
   200  
   201  type downloadState struct {
   202  	name         string
   203  	fileBuf      *bytes.Buffer
   204  	packet       *bytes.Buffer
   205  	seq          byte
   206  	currentBlock int
   207  }
   208  
   209  // Run the main logic for this client and blocks until stopped
   210  func (sd *SerialDevClient) Run() error {
   211  	log.Println("Starting serial client:", sd.config.Description)
   212  
   213  	// -1 indicates nothing is downloading right now
   214  	dlTimeout := time.NewTicker(10 * time.Second)
   215  	dlTimeout.Stop()
   216  
   217  	if sd.config.Connected {
   218  		sd.config.Connected = false
   219  		err := SendNodePoint(sd.nc, sd.config.ID, data.Point{Type: data.PointTypeConnected, Value: 0}, false)
   220  		if err != nil {
   221  			log.Println("Error sending connected point")
   222  		}
   223  	}
   224  
   225  	watcher, err := fsnotify.NewWatcher()
   226  	if err != nil {
   227  		return fmt.Errorf("Error creating a new fsnotify watcher: %w", err)
   228  
   229  	}
   230  	defer watcher.Close()
   231  
   232  	if sd.config.Port != "" {
   233  		err := watcher.Add(filepath.Dir(sd.config.Port))
   234  		if err != nil {
   235  			log.Println("Error adding watcher for:", sd.config.Port)
   236  		}
   237  	}
   238  
   239  	checkPortDur := time.Second * 10
   240  	timerCheckPort := time.NewTimer(checkPortDur)
   241  
   242  	serialReadData := make(chan []byte)
   243  	listenerClosed := make(chan struct{})
   244  	listenerSerialErr := make(chan struct{})
   245  
   246  	dlState := downloadState{}
   247  
   248  	dlFileStop := func() {
   249  		dlState.name = ""
   250  		_ = SendNodePoint(sd.nc, sd.config.ID, data.Point{Type: data.PointTypeDownload}, true)
   251  	}
   252  
   253  	dlFileSend := func() {
   254  		if dlState.name == "" {
   255  			log.Println("File download error, no file queued")
   256  			return
   257  		}
   258  
   259  		// packet format
   260  		// sequence (1 byte)
   261  		// subject (16 bytes, always file)
   262  		// filename (16 bytes)
   263  		// block index (4 bytes)
   264  		// data
   265  		// crc (2 bytes)
   266  
   267  		// we save off packet in struct so we can resend on retries
   268  		dlState.packet = &bytes.Buffer{}
   269  
   270  		sd.wrSeq++
   271  		dlState.seq = sd.wrSeq
   272  		err := dlState.packet.WriteByte(sd.wrSeq)
   273  		if err != nil {
   274  			log.Println("Error writing seq to file packet: ", err)
   275  			dlFileStop()
   276  			return
   277  		}
   278  
   279  		// subject is always file
   280  		f := make([]byte, 16)
   281  		copy(f, []byte("file"))
   282  		_, err = dlState.packet.Write(f)
   283  		if err != nil {
   284  			log.Println("Error writing file to file packet: ", err)
   285  			dlFileStop()
   286  			return
   287  		}
   288  
   289  		n := make([]byte, 16)
   290  		copy(n, []byte(dlState.name))
   291  		_, err = dlState.packet.Write(n)
   292  		if err != nil {
   293  			log.Println("Error writing filename to file packet: ", err)
   294  			dlFileStop()
   295  			return
   296  		}
   297  
   298  		if dlState.fileBuf.Len() <= 0 {
   299  			dlState.currentBlock = -1
   300  		}
   301  
   302  		// copy block number
   303  		b := make([]byte, 4)
   304  		binary.LittleEndian.PutUint32(b, uint32(dlState.currentBlock))
   305  		_, err = dlState.packet.Write(b)
   306  		if err != nil {
   307  			log.Println("Error writing block numbr to packet: ", err)
   308  			dlFileStop()
   309  			return
   310  		}
   311  
   312  		// copy payload
   313  		_, err = io.CopyN(dlState.packet, dlState.fileBuf, 400)
   314  		if err != nil {
   315  			if err != io.EOF {
   316  				log.Println("Error copying file packet content: ", err)
   317  				dlFileStop()
   318  				return
   319  			}
   320  		}
   321  
   322  		crc := crc16.ChecksumCCITT(dlState.packet.Bytes())
   323  		_ = binary.Write(dlState.packet, binary.LittleEndian, crc)
   324  
   325  		if sd.config.Debug >= 4 {
   326  			log.Printf("SER TX file seq:%v name:%v block:%v len:%v", sd.wrSeq, dlState.name, dlState.currentBlock, dlState.packet.Len())
   327  		}
   328  
   329  		if sd.portCobsWrapper != nil {
   330  			// _, err = io.Copy(sd.portCobsWrapper, dlState.packet)
   331  			_, err = sd.portCobsWrapper.Write(dlState.packet.Bytes())
   332  
   333  			if err != nil {
   334  				log.Println("Error writing file to cobs wrapper: ", err)
   335  				return
   336  			}
   337  		}
   338  	}
   339  
   340  	// dlRetry := func() {
   341  
   342  	// }
   343  
   344  	dlFileStart := func(name string) {
   345  		if dlState.name != "" {
   346  			log.Println("Error, file download already in progress")
   347  			return
   348  		}
   349  
   350  		if len(name) > 16 {
   351  			log.Println("file name is too long: ", name)
   352  			return
   353  		}
   354  
   355  		// see if we can find the file
   356  		for _, f := range sd.config.Files {
   357  			if f.Name == name {
   358  				d, err := f.GetContents()
   359  				if err != nil {
   360  					log.Println("Error decoding file: ", err)
   361  					continue
   362  				}
   363  				dlState.fileBuf = bytes.NewBuffer(d)
   364  				dlState.name = name
   365  				dlState.currentBlock = 0
   366  
   367  				dlFileSend()
   368  				return
   369  			}
   370  		}
   371  
   372  		log.Println("Error could not find file: ", name)
   373  	}
   374  
   375  	closePort := func() {
   376  		if sd.portCobsWrapper != nil {
   377  			log.Println("Closing serial port:", sd.config.Description)
   378  			sd.portCobsWrapper.Close()
   379  		}
   380  		sd.portCobsWrapper = nil
   381  
   382  		sd.config.Connected = false
   383  		err := SendNodePoint(sd.nc, sd.config.ID, data.Point{Type: data.PointTypeConnected, Value: 0}, false)
   384  		if err != nil {
   385  			log.Println("Error sending connected point")
   386  		}
   387  	}
   388  
   389  	if sd.config.Download != "" {
   390  		_ = SendNodePoint(sd.nc, sd.config.ID, data.Point{Type: data.PointTypeDownload}, true)
   391  	}
   392  
   393  	listener := func(port io.ReadWriteCloser, maxMessageLen int) {
   394  		errCount := 0
   395  		for {
   396  			buf := make([]byte, maxMessageLen)
   397  			c, err := port.Read(buf)
   398  			if err != nil {
   399  				if err != io.EOF && err.Error() != "Port has been closed" {
   400  					log.Printf("Error reading port %v: %v\n", sd.config.Description, err)
   401  
   402  					listenerSerialErr <- struct{}{}
   403  
   404  					// we don't want to reset the port on every COBS
   405  					// decode error, so accumulate a few before we do this
   406  					if err == ErrCobsDecodeError ||
   407  						err == ErrCobsTooMuchData {
   408  						errCount++
   409  						if errCount < 10000000 {
   410  							continue
   411  						}
   412  					}
   413  
   414  					listenerClosed <- struct{}{}
   415  					return
   416  				}
   417  			}
   418  			if c <= 0 {
   419  				continue
   420  			}
   421  
   422  			errCount = 0
   423  
   424  			buf = buf[0:c]
   425  			serialReadData <- buf
   426  		}
   427  	}
   428  
   429  	openPort := func() {
   430  		if sd.config.MaxMessageLength <= 0 {
   431  			sd.config.MaxMessageLength = 1024
   432  			err := SendPoints(sd.nc, sd.natsSub,
   433  				data.Points{{Type: data.PointTypeMaxMessageLength, Value: 1024}}, true)
   434  			if err != nil {
   435  				log.Println("Error sending max message len message:", err)
   436  			}
   437  		}
   438  
   439  		// make sure port is closed before we try to (re)open it
   440  		closePort()
   441  
   442  		if sd.config.Disabled {
   443  			closePort()
   444  			timerCheckPort.Stop()
   445  			return
   446  		}
   447  
   448  		var io io.ReadWriteCloser
   449  
   450  		if sd.config.Port == "serialfifo" {
   451  			// we are in test mode and using unix fifos instead of
   452  			// real serial ports. The fifo must already by started
   453  			// by the test harness
   454  			var err error
   455  			io, err = test.NewFifoB(sd.config.Port)
   456  			if err != nil {
   457  				log.Println("SerialDevClient: error opening fifo:", err)
   458  				return
   459  			}
   460  		} else {
   461  			if sd.config.Port == "" || sd.config.Baud == "" {
   462  				log.Printf("Serial port %v not configured\n", sd.config.Description)
   463  				timerCheckPort.Reset(checkPortDur)
   464  				return
   465  			}
   466  
   467  			baud, err := strconv.Atoi(sd.config.Baud)
   468  
   469  			if err != nil {
   470  				log.Printf("Serial port %v invalid baud\n", sd.config.Description)
   471  				timerCheckPort.Reset(checkPortDur)
   472  				return
   473  			}
   474  
   475  			mode := &serial.Mode{
   476  				BaudRate: baud,
   477  			}
   478  
   479  			serialPort, err := serial.Open(sd.config.Port, mode)
   480  			if err != nil {
   481  				log.Printf("Error opening serial port %v: %v", sd.config.Description,
   482  					err)
   483  				timerCheckPort.Reset(checkPortDur)
   484  				return
   485  			}
   486  
   487  			time.Sleep(time.Millisecond)
   488  			err = serialPort.SetDTR(false)
   489  			if err != nil {
   490  				log.Printf("Error clearing serial port DTR: %v\n", err)
   491  			}
   492  
   493  			time.Sleep(time.Millisecond * 100)
   494  			err = serialPort.SetDTR(true)
   495  			if err != nil {
   496  				log.Printf("Error setting serial port DTR: %v\n", err)
   497  			}
   498  
   499  			io = serialPort
   500  		}
   501  
   502  		sd.portCobsWrapper = NewCobsWrapper(io, sd.config.MaxMessageLength)
   503  		sd.portCobsWrapper.SetDebug(sd.config.Debug)
   504  		timerCheckPort.Stop()
   505  
   506  		log.Println("Serial port opened:", sd.config.Description)
   507  
   508  		go listener(sd.portCobsWrapper, sd.config.MaxMessageLength)
   509  
   510  		p := data.Points{{
   511  			Time: time.Now(),
   512  			Type: data.PointTypeTimeSync,
   513  		}}
   514  
   515  		sd.config.Connected = true
   516  		err := SendNodePoint(sd.nc, sd.config.ID, data.Point{Type: data.PointTypeConnected, Value: 1}, false)
   517  		if err != nil {
   518  			log.Println("Error sending connected point")
   519  		}
   520  
   521  		sd.wrSeq++
   522  		err = sd.sendPointsToDevice(sd.wrSeq, false, "", p)
   523  		if err != nil {
   524  			log.Println("Error sending time sync point to device: %w", err)
   525  		}
   526  	}
   527  
   528  	openPort()
   529  
   530  exitSerialClient:
   531  
   532  	for {
   533  		select {
   534  		case <-sd.stop:
   535  			break exitSerialClient
   536  		case <-timerCheckPort.C:
   537  			openPort()
   538  		case <-listenerClosed:
   539  			closePort()
   540  			timerCheckPort.Reset(checkPortDur)
   541  		case <-listenerSerialErr:
   542  			sd.config.ErrorCount++
   543  			points := []data.Point{{Type: data.PointTypeErrorCount, Value: float64(sd.config.ErrorCount)}}
   544  			err := SendPoints(sd.nc, sd.natsSub, points, false)
   545  			if err != nil {
   546  				log.Println("Error sending error points:", err)
   547  			}
   548  		case e, ok := <-watcher.Events:
   549  			if ok {
   550  				if e.Name == sd.config.Port {
   551  					if e.Op == fsnotify.Remove {
   552  						closePort()
   553  					} else if e.Op == fsnotify.Create {
   554  						openPort()
   555  					}
   556  				}
   557  			}
   558  		case rd := <-serialReadData:
   559  			if sd.config.Debug >= 8 {
   560  				log.Println("SER RX RAW:", test.HexDump(rd))
   561  			}
   562  
   563  			// decode serial packet
   564  			seq, subject, payload, err := SerialDecode(rd)
   565  			if err != nil {
   566  				log.Printf("Serial framing error (sub:%v): %v", subject, err)
   567  
   568  				var t string
   569  				var cnt int
   570  
   571  				if subject == "phr" {
   572  					t = data.PointTypeErrorCountHR
   573  					sd.config.ErrorCountHR++
   574  					cnt = sd.config.ErrorCountHR
   575  				} else {
   576  					t = data.PointTypeErrorCount
   577  					sd.config.ErrorCount++
   578  					cnt = sd.config.ErrorCount
   579  				}
   580  
   581  				err := SendPoints(sd.nc, sd.natsSub, []data.Point{{Type: t, Value: float64(cnt)}}, false)
   582  				if err != nil {
   583  					log.Println("Error sending error points:", err)
   584  				}
   585  
   586  				break
   587  			}
   588  
   589  			if subject == "ack" {
   590  				if sd.config.Debug >= 4 {
   591  					log.Printf("SER RX (%v) seq:%v sub:%v", sd.config.Description, seq, subject)
   592  				}
   593  				// TODO we need to handle acks, retries, etc
   594  				if dlState.name != "" {
   595  					if seq == dlState.seq {
   596  						if dlState.currentBlock == -1 {
   597  							log.Println("File download finished: ", dlState.name)
   598  							dlFileStop()
   599  						} else {
   600  							dlState.currentBlock++
   601  							dlFileSend()
   602  						}
   603  					}
   604  				}
   605  				break
   606  			}
   607  
   608  			if subject == "phr" {
   609  				// we have high rate points
   610  				sd.config.HrRx++
   611  				err := sd.nc.Publish(sd.natsSubHRUp, payload)
   612  				if err != nil {
   613  					log.Println("Error publishing HR data:", err)
   614  				}
   615  				sd.ratePointCountHR++
   616  				// we're done
   617  				break
   618  			}
   619  
   620  			if subject == "log" {
   621  				points := data.Points{{Type: data.PointTypeLog, Text: string(payload)}}
   622  
   623  				if sd.config.Debug >= 1 {
   624  					log.Printf("Serial client %v: log: %v\n",
   625  						sd.config.Description, string(payload))
   626  				}
   627  				err := SendPoints(sd.nc, sd.natsSubSerialPoints, points, false)
   628  				if err != nil {
   629  					log.Println("Error sending log point from MCU:", err)
   630  				}
   631  			}
   632  
   633  			// we must have a protobuf payload
   634  			points, errDecode := data.PbDecodeSerialPoints(payload)
   635  			var adminPoints data.Points
   636  
   637  			sd.config.Rx++
   638  
   639  			// make sure time is set on all points
   640  			for i, p := range points {
   641  				if p.Time.Year() <= 1980 {
   642  					points[i].Time = time.Now()
   643  				}
   644  			}
   645  
   646  			sd.ratePointCount += len(points)
   647  
   648  			if errDecode == nil && len(points) > 0 {
   649  				if sd.config.Debug >= 4 {
   650  					log.Printf("SER RX (%v) seq:%v sub:%v\n%v", sd.config.Description, seq, subject, points)
   651  				}
   652  
   653  				// send response
   654  				err := sd.sendPointsToDevice(seq, false, "ack", nil)
   655  				if err != nil {
   656  					log.Println("Error sending ack to device:", err)
   657  				}
   658  
   659  				if !sd.config.SyncParent {
   660  					err = data.MergePoints(sd.config.ID, points, &sd.config)
   661  					if err != nil {
   662  						log.Println("error merging new points:", err)
   663  					}
   664  				}
   665  			} else {
   666  				log.Println("Error decoding serial packet from device:",
   667  					sd.config.Description, errDecode)
   668  				sd.config.ErrorCount++
   669  				adminPoints = append(adminPoints,
   670  					data.Point{Type: data.PointTypeErrorCount, Value: float64(sd.config.ErrorCount)})
   671  			}
   672  
   673  			if time.Since(sd.lastSendStats) > time.Second*5 {
   674  				adminPoints = append(adminPoints,
   675  					data.Points{
   676  						{Time: time.Now(), Type: data.PointTypeRx, Value: float64(sd.config.Rx)},
   677  						{Time: time.Now(), Type: data.PointTypeHrRx, Value: float64(sd.config.HrRx)},
   678  					}...)
   679  				sd.lastSendStats = time.Now()
   680  			}
   681  
   682  			if time.Since(sd.rateLastSend) > time.Second {
   683  				now := time.Now()
   684  				elapsedSec := now.Sub(sd.rateLastSend).Seconds()
   685  				rate := float64(sd.ratePointCount) / elapsedSec
   686  				rateHR := float64(sd.ratePointCountHR) / elapsedSec
   687  				adminPoints = append(adminPoints,
   688  					data.Point{Time: now, Type: data.PointTypeRate,
   689  						Value: rate},
   690  					data.Point{Time: now, Type: data.PointTypeRateHR,
   691  						Value: rateHR},
   692  				)
   693  				sd.rateLastSend = now
   694  				sd.ratePointCount = 0
   695  				sd.ratePointCountHR = 0
   696  			}
   697  
   698  			if sd.config.SyncParent {
   699  				// add serial ID to origin for all points we send to the parent
   700  				for i := range points {
   701  					points[i].Origin = sd.config.ID
   702  				}
   703  			}
   704  
   705  			if len(points) > 0 {
   706  				err := SendPoints(sd.nc, sd.natsSubSerialPoints, points, false)
   707  				if err != nil {
   708  					log.Println("Error sending points received from MCU:", err)
   709  				}
   710  			}
   711  
   712  			if len(adminPoints) > 0 {
   713  				err := SendPoints(sd.nc, sd.natsSub, adminPoints, false)
   714  				if err != nil {
   715  					log.Println("Error sending admin points:", err)
   716  				}
   717  			}
   718  
   719  		case pts := <-sd.newPoints:
   720  			op := false
   721  			updateNatsSubjects := false
   722  			for _, p := range pts.Points {
   723  				// check if any of the config changes should cause us to re-open the port
   724  				if p.Type == data.PointTypePort ||
   725  					p.Type == data.PointTypeBaud ||
   726  					p.Type == data.PointTypeDisabled ||
   727  					p.Type == data.PointTypeMaxMessageLength {
   728  					op = true
   729  				}
   730  
   731  				if p.Type == data.PointTypePort {
   732  					err := watcher.Add(filepath.Dir(p.Text))
   733  					if err != nil {
   734  						log.Println("Error adding watcher on serial port name change:", p.Text)
   735  					}
   736  				}
   737  
   738  				if p.Type == data.PointTypeDisabled {
   739  					if p.Value == 0 {
   740  						closePort()
   741  					} else {
   742  						op = true
   743  					}
   744  				}
   745  
   746  				if p.Type == data.PointTypeHRDest {
   747  					updateNatsSubjects = true
   748  				}
   749  
   750  				if p.Type == data.PointTypeSyncParent {
   751  					updateNatsSubjects = true
   752  				}
   753  
   754  				if p.Type == data.PointTypeDebug {
   755  					sd.portCobsWrapper.SetDebug(int(p.Value))
   756  				}
   757  
   758  				if p.Type == data.PointTypeDownload {
   759  					if p.Text == "" {
   760  						log.Println("Stopping download: ", dlState.name)
   761  						dlFileStop()
   762  					} else {
   763  						dlFileStart(p.Text)
   764  						log.Println("Starting download: ", p.Text)
   765  					}
   766  				}
   767  			}
   768  
   769  			err := data.MergePoints(pts.ID, pts.Points, &sd.config)
   770  			if err != nil {
   771  				log.Println("error merging new points:", err)
   772  			}
   773  
   774  			if updateNatsSubjects {
   775  				sd.populateNatsSubjects()
   776  			}
   777  
   778  			if op {
   779  				openPort()
   780  			}
   781  
   782  			if sd.portCobsWrapper == nil {
   783  				break
   784  			}
   785  
   786  			if sd.config.ErrorCountReset {
   787  				points := data.Points{
   788  					{Type: data.PointTypeErrorCount, Value: 0},
   789  					{Type: data.PointTypeErrorCountReset, Value: 0},
   790  				}
   791  				err = SendPoints(sd.nc, sd.natsSub, points, false)
   792  				if err != nil {
   793  					log.Println("Error resetting MCU error count:", err)
   794  				}
   795  
   796  				sd.config.ErrorCountReset = false
   797  				sd.config.ErrorCount = 0
   798  			}
   799  
   800  			if sd.config.ErrorCountResetHR {
   801  				points := data.Points{
   802  					{Type: data.PointTypeErrorCountHR, Value: 0},
   803  					{Type: data.PointTypeErrorCountResetHR, Value: 0},
   804  				}
   805  				err = SendPoints(sd.nc, sd.natsSub, points, false)
   806  				if err != nil {
   807  					log.Println("Error resetting MCU error count:", err)
   808  				}
   809  
   810  				sd.config.ErrorCountResetHR = false
   811  				sd.config.ErrorCountHR = 0
   812  			}
   813  
   814  			if sd.config.RxReset {
   815  				points := data.Points{
   816  					{Type: data.PointTypeRx, Value: 0},
   817  					{Type: data.PointTypeRxReset, Value: 0},
   818  				}
   819  				err = SendPoints(sd.nc, sd.natsSub, points, false)
   820  				if err != nil {
   821  					log.Println("Error resetting MCU error count:", err)
   822  				}
   823  
   824  				sd.config.RxReset = false
   825  				sd.config.Rx = 0
   826  			}
   827  
   828  			if sd.config.HrRxReset {
   829  				points := data.Points{
   830  					{Type: data.PointTypeHrRx, Value: 0},
   831  					{Type: data.PointTypeHrRxReset, Value: 0},
   832  				}
   833  				err = SendPoints(sd.nc, sd.natsSub, points, false)
   834  				if err != nil {
   835  					log.Println("Error resetting MCU error count:", err)
   836  				}
   837  
   838  				sd.config.HrRxReset = false
   839  				sd.config.HrRx = 0
   840  			}
   841  
   842  			if sd.config.TxReset {
   843  				points := data.Points{
   844  					{Type: data.PointTypeTx, Value: 0},
   845  					{Type: data.PointTypeTxReset, Value: 0},
   846  				}
   847  				err = SendPoints(sd.nc, sd.natsSub, points, false)
   848  				if err != nil {
   849  					log.Println("Error resetting MCU error count:", err)
   850  				}
   851  
   852  				sd.config.TxReset = false
   853  				sd.config.Tx = 0
   854  			}
   855  
   856  			// check if we have any points that need sent to MCU
   857  			if !sd.config.SyncParent {
   858  				toSend := data.Points{}
   859  				for _, p := range pts.Points {
   860  					switch p.Type {
   861  					case data.PointTypePort,
   862  						data.PointTypeBaud,
   863  						data.PointTypeDescription,
   864  						data.PointTypeErrorCount,
   865  						data.PointTypeErrorCountReset,
   866  						data.PointTypeRxReset,
   867  						data.PointTypeTxReset:
   868  						continue
   869  					}
   870  
   871  					// strip off Origin as MCU does not need that
   872  					p.Origin = ""
   873  					toSend = append(toSend, p)
   874  				}
   875  
   876  				if len(toSend) > 0 {
   877  					sd.wrSeq++
   878  					err := sd.sendPointsToDevice(sd.wrSeq, false, "", toSend)
   879  					if err != nil {
   880  						log.Println("Error sending points to serial device:", err)
   881  					}
   882  				}
   883  			}
   884  
   885  		case pts := <-sd.newEdgePoints:
   886  			err := data.MergeEdgePoints(pts.ID, pts.Parent, pts.Points, &sd.config)
   887  			if err != nil {
   888  				log.Println("error merging new points:", err)
   889  			}
   890  
   891  		case sData := <-sd.sendPointsCh:
   892  			err := sd.sendPointsToDevice(sData.seq, sData.ack, sData.subject, sData.points)
   893  			if err != nil {
   894  				log.Println("Error sending data to device: ", err)
   895  			}
   896  
   897  			// TODO need to send edge points to MCU, not implemented yet
   898  		}
   899  	}
   900  
   901  	log.Println("Stopping serial client:", sd.config.Description)
   902  	closePort()
   903  	if sd.parentSubscription != nil {
   904  		err := sd.parentSubscription.Unsubscribe()
   905  		if err != nil {
   906  			log.Println("Error unsubscribing:", err)
   907  		}
   908  	}
   909  
   910  	return nil
   911  
   912  }
   913  
   914  // Stop sends a signal to the Run function to exit
   915  func (sd *SerialDevClient) Stop(_ error) {
   916  	close(sd.stop)
   917  }
   918  
   919  // Points is called by the Manager when new points for this
   920  // node are received.
   921  func (sd *SerialDevClient) Points(nodeID string, points []data.Point) {
   922  	sd.newPoints <- NewPoints{nodeID, "", points}
   923  }
   924  
   925  // EdgePoints is called by the Manager when new edge points for this
   926  // node are received.
   927  func (sd *SerialDevClient) EdgePoints(nodeID, parentID string, points []data.Point) {
   928  	sd.newEdgePoints <- NewPoints{nodeID, parentID, points}
   929  }