38
\$\begingroup\$

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:

  1. A possible bug in go version go1.4.2 darwin/amd64, in loadcell.go (see comment before the final for loop)
  2. 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), or var 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()
}
\$\endgroup\$
1
\$\begingroup\$

Regarding the first part of it:

From reading the golang docs, it appears

buf := make([]byte, 36) //BUG? This causes ReadFromUDP to block = GOOD

may work because a specific allocation and initialization of 36 []byte type is occuring, at least enough to hold something if a UDP packet arrives to be read

see Documentation: builtin

while

var buf []byte        //     While this causes ReadFromUDP to continuously return 0,nil,nil

in this case you've got buf as a pointer to byte but neither a new nor make has been called on it

One caveat, you are using 1.4.x and the docs seem to be up to current 1.9.x. I didn't see an easy way to check the specs on the net package or Connection/UDP usage, as to whether anything has changed...

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.