I have written the following utility, as my first non-tutorial program in Go.
The purpose of the utility is
- to connect to a torque/force sensor (aka load-cell) via UDP;
- to send an initialization command; and
- to record the resulting stream of measurements.
Since I am planning further functionality (setting duty cycle of a brushless motor controller via serial), which will be logged to the same file, I have separated the file writing from the UDP packet logging via a channel.
Since this is a utility that serves a single purpose, error handling is minimal.
I am hoping for feedback regarding the general style and idiomaticy of the code and the folder structure (separating loadcell-related files into a separate sub-package, in the loadcell sub-directory).
Specifically, I would appreciate your thoughts on:
- A possible bug in
go version go1.4.2 darwin/amd64
, in loadcell.go (see comment before the finalfor
loop) - The
FromNetworkBytes
function in loadCellPacket.go. I really wanted to make a class method, e.g.packet := loadCellPacket.FromNetworkBytes(b)
, but as far as I found, the Go options are either:packet := loadCellPacketFromNetworkBytes(b)
(a simple function), orvar packet loadCellBytes; packet.FromNetworkBytes(b)
. I opted for the later, since I can reuse the packet variable, but I would appreciate feedback.
The code is hosted here, and for longevity of this question, I repeat the files below. I have left out import statements for brevity.
./main.go
package main
import (
...
"github.com/mikehamer/ati-torque-force-logger/loadcell"
)
func main() {
// Parse flags
loadCellAddress := flag.String("address", "192.168.1.200:49152", "The address of the loadcell")
logFileName := flag.String("logfile", fmt.Sprintf("%s_loadcell.log", time.Now().Format("2006-01-02_15-04-05")), "The name of the logfile")
flag.Parse()
//open CSV log
logfile, err := os.Create(*logFileName)
if err != nil {
log.Fatal(err)
}
defer logfile.Close()
// Setup communication channels
receivedMeasurements := make(chan loadcell.Measurement)
//connect and stream from loadcell
go loadcell.ReceiveLoadCellStream(*loadCellAddress, receivedMeasurements)
//loop and write logs
fmt.Println("Saving output to", logfile.Name())
logfile.WriteString("t, Fx, Fy, Fz, Tx, Ty, Tz\n")
for {
select {
case measurement := <-receivedMeasurements:
logfile.WriteString(measurement.String())
}
}
}
./loadcell/loadcell.go
package loadcell
// NETWORK CONSTANTS
var loadCellStartStreamCommand = loadCellCommand{0x1234, 0x0002, 0} // the command to send to enable realtime stream
// ReceiveLoadCellStream opens a network stream to the loadcell, sends a configuration packet and then relays received measurements back through the supplied channel
func ReceiveLoadCellStream(loadCellAddress string, receivedPackets chan<- Measurement) error {
// calculate loadcell address
remoteAddr, err := net.ResolveUDPAddr("udp", loadCellAddress)
if err != nil {
log.Fatal(err)
}
//open connection to loadcell
conn, err := net.DialUDP("udp", nil, remoteAddr)
if err != nil {
log.Fatal(err)
}
fmt.Println("UDP Server: Local", conn.LocalAddr(), "-> Remote", conn.RemoteAddr())
defer conn.Close()
// send the command instructing the loadcell to begin a realtime data stream
conn.Write(loadCellStartStreamCommand.NetworkBytes())
// begin receiving packets from the network connection and sending them on the outgoing channel
startTime := time.Now()
buf := make([]byte, 36) //BUG? This causes ReadFromUDP to block = GOOD
//var buf []byte // While this causes ReadFromUDP to continuously return 0,nil,nil
for {
var packet loadCellPacket
n, remoteAddr, err := conn.ReadFromUDP(buf)
switch {
//packet of the correct size is received
case uintptr(n) == unsafe.Sizeof(packet):
if err := packet.FromNetworkBytes(buf); err != nil {
log.Fatal(err)
} //decode it from network stream
receivedPackets <- packet.ParseMeasurement()
//packet is received but with incorrect size
case n != 0:
log.Print("From", remoteAddr, "got unexpected bytes", buf[:n])
//an error occurs
case err != nil:
log.Fatal(err)
}
}
}
./loadcell/loadCellPacket.go
package loadcell
// loadCellPacket is the packet as received over the network
type loadCellPacket struct {
RdtSequence uint32 // RDT sequence number of this packet.
FtSequence uint32 // The record’s internal sequence number
Status uint32 // System status code
// Force and torque readings use counts values
Fx int32 // X-axis force
Fy int32 // Y-axis force
Fz int32 // Z-axis force
Tx int32 // X-axis torque
Ty int32 // Y-axis torque
Tz int32 // Z-axis torque
}
// FromNetworkBytes parses a loadCellPacket from a network (BigEndian) bytestream
func (s *loadCellPacket) FromNetworkBytes(b []byte) error {
var packet loadCellPacket
buf := bytes.NewReader(b)
if err := binary.Read(buf, binary.BigEndian, &packet); err != nil {
return err
}
*s = packet
return nil
}
// ParseMeasurement creates a Measurement from the loadCellPacket
func (s *loadCellPacket) ParseMeasurement(rxTime float64) Measurement {
return Measurement{
rxTime,
float32(s.Fx) / 1e6,
float32(s.Fy) / 1e6,
float32(s.Fz) / 1e6,
float32(s.Tx) / 1e6,
float32(s.Ty) / 1e6,
float32(s.Tz) / 1e6}
}
./loadcell/LoadCellMeasurement.go
package loadcell
// Measurement is a loadCellPacket that has been converted into a useable form
type Measurement struct {
RxTime float64 // the receive time, since the beginning of the program
Fx float32 // x-force in Newtons
Fy float32 // y-force in Newtons
Fz float32 // z-force in Newtons
Tx float32 // x-torque in Newton-meters
Ty float32 // y-torque in Newton-meters
Tz float32 // z-torque in Newton-meters
}
// Bytes returns the Measurement as a LittleEndian-encoded byte slice, ready for serialization
func (s *Measurement) Bytes() []byte {
buf := new(bytes.Buffer)
if err := binary.Write(buf, binary.LittleEndian, s); err != nil {
log.Fatal(err)
}
return buf.Bytes()
}
// String returns the Measurement as a comma-separated string, ready for logging
func (s *Measurement) String() string {
return fmt.Sprintf("%.6f, %v, %v, %v, %v, %v, %v\n", s.RxTime, s.Fx, s.Fy, s.Fz, s.Tx, s.Ty, s.Tz)
}
./loadcell/loadCellCommand.go
package loadcell
// loadCellCommand is a command packet sent to the loadcell
type loadCellCommand struct {
header uint16 // = 0x1234 Required
command uint16 // Command to execute
sampleCount uint32 // Samples to output (0 = infinite)
}
// NetworkBytes returns the loadCellCommand as a BigEndian-encoded byte slice ready for network transmission
func (s *loadCellCommand) NetworkBytes() []byte {
buf := new(bytes.Buffer)
if err := binary.Write(buf, binary.BigEndian, s); err != nil {
log.Fatal(err)
}
return buf.Bytes()
}