A Golang program to dump serial data into CSV file

A unique situation in which I wanted to dump the memory of a program running on a microcontroller – the program can send data through serial port, but for it to make sense for the programmer it has to be dumped in a readable format. And another challenge was that the controller was programmable only from MS Windows. So this dumping program must be able to run on Win64.

I chose Golang for this purpose as I don’t know the serial port reading API of Win32/64 but I can build for Win64 using Golang on Linux and a nice Serial library was available for Golang as well.

package main

import (
	"fmt"
	"github.com/tarm/serial"
	"bufio"
	"os"
	"strconv"
	"time"
	"encoding/hex"
	"bytes"
	"encoding/binary"
	"sync"
)

type mt_var struct {
	u32 uint32
	i32 int32
	f32 float32
	choice int64
}

func (v *mt_var) Parse(varvalue_b []byte) {
	reader := bytes.NewReader(varvalue_b)

	switch v.choice {
	case 1:
		if err := binary.Read(reader, binary.BigEndian, &v.u32); err != nil {
			fmt.Println("Error occurred while parsing bytes to uint32: ", err)
		}
	case 2:
		if err := binary.Read(reader, binary.BigEndian, &v.i32); err != nil {
			fmt.Println("Error occurred while parsing bytes to int32: ", err)
		}
	case 3:
		if err := binary.Read(reader, binary.BigEndian, &v.f32); err != nil {
			fmt.Println("Error occurred while parsing bytes to float: ", err)
		}
	}
}

func (v mt_var) String() (ret string) {
	switch v.choice {
	case 1:
		ret = fmt.Sprintf("%v", v.u32)
	case 2:
		ret = fmt.Sprintf("%v", v.i32)
	case 3:
		ret = fmt.Sprintf("%v", v.f32)
	}
	return
}

func main() {
	scanner := bufio.NewScanner(os.Stdin)

	defer func() {
		fmt.Println("Press any key to exit")
		scanner.Scan()
	}()

	fmt.Print("Enter serial port name: ")
	scanner.Scan()
	portname := scanner.Text()

	fmt.Print("Enter baud rate: ")
	scanner.Scan()
	baud_s := scanner.Text()
	baud, err := strconv.ParseInt(baud_s, 10, 64)

	if err != nil {
		fmt.Println("Unable to parse baud rate: ", err)
		return
	}

	fmt.Print("Enter serial port timeout in seconds: ")
	scanner.Scan()
	timeout_s := scanner.Text()
	timeout, err := strconv.ParseInt(timeout_s, 10, 64)

	if err != nil {
		fmt.Println("Unable to parse timeout value: ", err)
		return
	}

	fmt.Print("Select data type of variables (1 - uint32, 2 - int32, 3 - float32): ")
	scanner.Scan()
	dt_var_s := scanner.Text()
	dt_var, err := strconv.ParseInt(dt_var_s, 10, 64)

	if err != nil {
		fmt.Println("Unable to parse choice value for data type: ", err)
		return
	} else if dt_var < 1 || dt_var > 3 {
		fmt.Println("Invalid choice ", dt_var, " for data type selection")
		return
	}

	fmt.Print("Enter number of variables to be read per timestamp: ")
	scanner.Scan()
	num_vars_s := scanner.Text()
	num_vars, err := strconv.ParseInt(num_vars_s, 10, 64)

	if err != nil {
		fmt.Println("Unable to parse num vars value: ", err)
		return
	}

	fmt.Print("Enter number of bytes per variable: ")
	scanner.Scan()
	num_bytes_per_var_s := scanner.Text()
	num_bytes_per_var, err := strconv.ParseInt(num_bytes_per_var_s, 10, 64)

	if err != nil {
		fmt.Println("Unable to parse num bytes per var value: ", err)
		return
	}

	fmt.Print("Enter total number of timestamps: ")
	scanner.Scan()
	num_timestamps_s := scanner.Text()
	num_timestamps, err := strconv.ParseInt(num_timestamps_s, 10, 64)

	if err != nil{
		fmt.Println("Unable to parse num timestamps value: ", err)
		return
	}

	fmt.Print("Enter DAQ sample time: ")
	scanner.Scan()
	sampletime_s := scanner.Text()
	sampletime, err := strconv.ParseInt(sampletime_s, 10, 64)

	if err != nil {
		fmt.Println("Unable to parse sample time value: ", err)
		return
	}

	c := &serial.Config{Name: portname, Baud: int(baud), ReadTimeout: time.Second * time.Duration(timeout)}
	port, err := serial.OpenPort(c)

	if err != nil {
		fmt.Println("Unable to open serial port: ", err)
		return
	}

	defer func() {
		fmt.Println("Closing serial port")
		port.Close()
	}()

	var wg sync.WaitGroup
	var stop_serial_writer = make(chan bool)

	fmt.Println("Press any key to start reading serial port")
	scanner.Scan()

	wg.Add(1)
	go serial_writer(port, stop_serial_writer, &wg)

	fmt.Println("Starting to read port")

	writer_channel := make(chan []byte, 1000)

	wg.Add(1)
	go writer(writer_channel, num_vars, &wg, sampletime, num_bytes_per_var, dt_var)

	bytes_per_timestamp := num_vars * num_bytes_per_var
	var bytecount int

outer:
	for i := int64(1); i <= num_timestamps; i++ {
		buf := make([]byte, bytes_per_timestamp)

		for j := int64(0); j < bytes_per_timestamp; j++ {
			if n, err := port.Read(buf[j:j+1]); err != nil {
				fmt.Println("Error occurred ", err)
				break outer
			} else {
				bytecount += n
				fmt.Printf("Read %d bytes\r", bytecount)
			}
		}

		writer_channel <- buf
	}

	fmt.Println()

	close(writer_channel)
	stop_serial_writer <- true

	fmt.Println("waiting for all goroutines to exit")
	wg.Wait()
}

func writer(channel chan []byte, num_vars int64, wg *sync.WaitGroup, sampletime int64, bytes_per_var int64, dt_var int64) {
	defer func() {
		wg.Done()
	}()

	home_directory, err := os.UserHomeDir()
	if err != nil {
		fmt.Println("Unable to fetch user home directory: ", err)
		return
	}

	time_str := time.Now().Format("2006-01-02-15-04-05")
	filename_raw := fmt.Sprintf("%s%cserial2csv_raw_%s.txt", home_directory, os.PathSeparator, time_str)
	filename_csv := fmt.Sprintf("%s%cserial2csv_%s.csv", home_directory, os.PathSeparator, time_str)

	f_raw, err := os.Create(filename_raw)
	if err != nil {
		fmt.Println("Unable to create file ", filename_raw, ": ", err)
		return
	}
	defer f_raw.Close()

	f_csv, err := os.Create(filename_csv)
	if err != nil {
		fmt.Println("Unable to create file ", filename_csv, ": ", err)
		return
	}
	defer f_csv.Close()

	bytes_written := 0
	var o_sampletime int64


	for data := range channel {
		hexdata := make([]byte, hex.EncodedLen(len(data)))
		hex.Encode(hexdata, data)
		if n, err := f_raw.Write(hexdata); err != nil {
			bytes_written += n
			fmt.Println("Error occurred while writing to file ", filename_raw, ": ", err)
		}

		fmt.Fprintf(f_csv, "%v,", o_sampletime)
		o_sampletime += sampletime

		for varnumber := int64(1); varnumber <= num_vars; varnumber++ {
			var err error
			var n int

			varvalue_b := data[varnumber * bytes_per_var - bytes_per_var:varnumber * bytes_per_var]
			var varvalue = mt_var{choice: dt_var}
			varvalue.Parse(varvalue_b)

			if varnumber == num_vars {
				n, err = fmt.Fprintln(f_csv, varvalue)
			} else {
				n, err = fmt.Fprintf(f_csv, "%v,", varvalue)
			}

			if err != nil {
				fmt.Println("Error while writing to file ", filename_csv, ": ", err)
			} else {
				bytes_written += n
			}
		}
	}

	fmt.Println("Total bytes written to files ", bytes_written)
	fmt.Println("RAW Data: ", filename_raw)
	fmt.Println("CSV Data: ", filename_csv)
}

func serial_writer(port *serial.Port, stop chan bool, wg *sync.WaitGroup) {
	fmt.Println("Starting serial port writer - sending 1 continuously")

	defer wg.Done()

	data := []byte{1}
	zero := []byte{0}

outer_one:
	for {
		select {
		case <-stop:
			fmt.Println("stopping to send 1 on serial")
			break outer_one
		default:
			port.Write(data)
		}
	}

	zero_stop_timer := time.NewTimer(2 * time.Second)

outer_zero:
	for {
		select {
		case <-zero_stop_timer.C:
			fmt.Println("stopping to send 0 on serial")
			break outer_zero
		default:
			port.Write(zero)
		}
	}

	zero_stop_timer.Stop()
}

The same is available on my Github.

Building this program on Linux, for Win64 is pretty easy (my Linux box is amd64):

GOOS=windows go build serial2csv_sync_mdt.go

You may require some changes in the build command if building for 32 bit or from a 32 bit system.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: