goraspberry-piembedded-linuxsysfsiio

Why is my Go app not reading from sysfs like the busybox `cat` command?


Go 1.12 on Linux 4.19.93 armv6l. Hardware is a raspberypi zero w (BCM2835) running a yocto linux image.

I've got a gpio driven SRF04 proximity sensor driven by the srf04 linux driver.

It works great over sysfs and the busybox shell.

# cat /sys/bus/iio/devices/iio:device0/in_distance_raw
1646

I've used Go before with IIO devices that support triggers and buffered output at high sample rates on this hardware platform. However for this application the srf04 driver doesn't implement those IIO features. Drat. I don't really feel like adding buffer / trigger support to the driver myself (at this time) since I do not have a need for a 'high' sample rate. A handful of pings per second should suffice for my purpose. I figure I'll calculate mean & std. dev. for a rolling window of data points and 'divine' the signal out of the noise.

So with that - I'd be perfectly happy to Read the bytes from the published sysfs file with Go.

Which brings me to the point of this post. When I open the file for reading, and try to Read() any number of bytes, I always get a generic -EIO error.

func (s *Srf04) Read() (int, error) {
    samp := make([]byte, 16)

    f, err := os.OpenFile(s.readPath, OS.O_RDONLY, os.ModeDevice)
    if err != nil {
        return 0, err
    }
    defer f.Close()

    n, err := f.Read(samp)
    if err != nil {
        // This block is always executed.
        // The error is never a timeout, and always 'input/output error' (-EIO aka -5)
        log.Fatal(err)
    }
    ...
}

This seems like strange behavior to me. So I decided to mess with using io.ReadFull. This yielded unreliable results.

func (s *Srf04) Read() (int, error) {
    samp := make([]byte, 16)

    f, err := os.OpenFile(s.readPath, OS.O_RDONLY, os.ModeDevice)
    if err != nil {
        return 0, err
    }
    defer f.Close()

    for {
        n, err := io.ReadFull(readFile, samp)
        log.Println("ReadFull ", n, " bytes.")
        if err == io.EOF {
            break
        }
        if err != nil {
            log.Println(err)
        }
    }

    ...

}

I ended up adding it to a loop, as I found behavior changes from 'one-off' reads to multiple read calls subsequent to one another. I have it exiting if it gets an EOF, and repeatedly trying to read otherwise.

The results are straight-up crazy unreliable, seemingly returning random results. Sometimes I get the -5, other times I read between 2 - 5 bytes from the device. Sometimes I get bytes without an eof file before the EOF. The bytes appear to represent character data for numbers (each rune is a rune between [0-9]) -- which I'd expect.

Aside: I expect this is related to file polling and the go blocking IO implementation, but I have no way to really tell.

As a temporary workaround, I decided try using os.exec, and now I get results I'd expect to see.

func (s *Srf04)Read() (int, error) {
    out, err := exec.Command("cat", s.readPath).Output()
    if err != nil  {
       return 0, err
    }
    return strconv.Atoi(string(out))
}

But Yick. os.exec. Yuck.


Solution

  • I'd try to run that cat whatever encantation under strace and then peer at what read(2) calls cat actually manages to do (including the number of bytes actually read), and then I'd try to re-create that behaviour in Go.

    My own sheer guess at the problem's cause is that the driver (or the sysfs layer) is not too well prepared to deal with certain access patterns.

    For a start, consider that GNU cat is not a simple-minded byte shoveler but is rather a reasonably tricky piece of software, which, among other things, considers optimal I/O block sizes for both input and output devices (if available), calls fadvise(2) etc. It's not that any of that gets actually used when you run it on your sysfs-exported file, but it may influence how the full stack (starting with the sysfs layer) performs in the case of using cat and with your code, respectively.

    Hence my advice: start with strace-ing the cat and then try to re-create its usage pattern in your Go code; then try to come up with a minimal subset of that, which works; then profoundly comment your code ;-)