reprage

The code running on Raspberry PI’s within the gasworks project (An art installation that loosely mimics brain cells as clusters of lights) is all written in Golang. While, the hardware architecture for each of the neurones has a Raspberry PI sending commands to an Arduino over serial. This communication link was one of the first things I prototyped for the project.

image

The veritable Dave Cheney maintains unofficial ARM builds of Go which are compatible with the Raspberry PI. So the first step is to grab that, and follow the along with the Golang installation instructions.

For serial communication I used the huin fork of the goserial library, mainly because the code had a far more idiomatic Go style than the others I looked.

Opening up a connection to the Arduino is a case of hunting around for that USB device that is most likely the Arduino:

	package main

	import (
		"github.com/huin/goserial"
		"io/ioutil"
		"strings"
	)

	// findArduino looks for the file that represents the Arduino
	// serial connection. Returns the fully qualified path to the
	// device if we are able to find a likely candidate for an
	// Arduino, otherwise an empty string if unable to find
	// something that 'looks' like an Arduino device.
	func findArduino() string {
		contents, _ := ioutil.ReadDir("/dev")

		// Look for what is mostly likely the Arduino device
		for _, f := range contents {
			if strings.Contains(f.Name(), "tty.usbserial") ||
				strings.Contains(f.Name(), "ttyUSB") {
				return "/dev/" + f.Name()
			}
		}

		// Have not been able to find a USB device that 'looks'
		// like an Arduino.
		return ""
	}

	func main() {
		// Find the device that represents the arduino serial
		// connection.
		c := &goserial.Config{Name: findArduino(), Baud: 9600}
		s, _ := goserial.OpenPort(c)
	}

The thing that tripped me up when prototyping the communication code was that I wasn’t able to immediately pump data down the serial connection to the Arduino, unless I had the Arduino serial monitor open.

When making a serial connection to an Arduino it automatically (unless it is a new Arduino Leonardo) resets it (similar to what happens when you press the reset button). It then takes about a second for the bootloader on the Arduino to do it’s thing and get into a state where it is able to accept data over the serial port.

I worked around this in Golang a little inelegantly by sleeping for a second, however it is possible to disable the Arduino reset on serial connection with a simple hardware hack.

	func main() {
		// Find the device that represents the Arduino serial connection.
		c := &goserial.Config{Name: findArduino(), Baud: 9600}
		s, _ := goserial.OpenPort(c)

		// When connecting to an older revision Arduino, you need to wait
		// a little while it resets.
		time.Sleep(1 * time.Second)
	}

The communication protocol I used between the Raspberry PI and Arduino was very simple. Each command is five bytes, the first byte being the command identifier, with the four remaining bytes reserved for a single mandatory float argument (that could be ignored if necessary on the Arduino).

Packaging up commands and sending them over the wire was pretty easy with the binary encoding package bundled into the Golang standard library. It was a case of encoding the argument into a byte buffer, then looping over the bytes in both the command and argument byte buffer, writing them to the serial port:

	// sendArduinoCommand transmits a new command over the nominated serial
	// port to the arduino. Returns an error on failure. Each command is
	// identified by a single byte and may take one argument (a float).
	func sendArduinoCommand(
	command byte, argument float32, serialPort io.ReadWriteCloser) error {
		if serialPort == nil {
			return nil
		}

		// Package argument for transmission
		bufOut := new(bytes.Buffer)
		err := binary.Write(bufOut, binary.LittleEndian, argument)
		if err != nil {
			return err
		}

		// Transmit command and argument down the pipe.
		for _, v := range [][]byte{[]byte{command}, bufOut.Bytes()} {
			_, err = serialPort.Write(v)
			if err != nil {
				return err
			}
		}

		return nil
	}

Putting it all together within the main function becomes:

	func main() {
		// Find the device that represents the arduino serial connection.
		c := &goserial.Config{Name: findArduino(), Baud: 9600}
		s, _ := goserial.OpenPort(c)

		// When connecting to an older revision Arduino, you need to wait
		// a little while it resets.
		time.Sleep(1 * time.Second)
		sendArduinoCommand('a', 1.0, s)
	}

Picking this data up on the Arduino side of the serial connection is done by reading the the first command byte and then using a union to decode the four argument bytes back into a float:

	typedef struct {
	  char instruction; // The instruction that arrived by serial connection.
	  float argument;   // The argument that came with the instruction.
	} Command;

	/**
	 * ReadCommand sucks down the lastest command from the serial port,
	 * returns {'*', 0.0} if no new command is available.
	 */
	Command ReadCommand() {
	  // Not enough bytes for a command, return an empty command.
	  if (Serial.available() < 5) {
    	return (Command) {'*', 0.0};
	  }

	  union {
    	char b[4];
	    float f;
	  } ufloat;

	  // Read the command identifier and argument from the serial port.
	  char c = Serial.read();
	  Serial.readBytes(ufloat.b, 4);

	  return (Command) {c, ufloat.f};
	}

Now, just make sure you set the same baud rate on the Arduino side of the connection, and start reading off commands from the serial connection:

	/**
	 * Arduino initalisation.
	 */
	void setup() {
	  Serial.begin(9600);
	}

	/**
	 * Main Arduino loop.
	 */
	void loop() {
	  Command c = ReadCommand();

	  // Do something awesome with the command. Like represent the state of a
	  // simulated neurone as a lighting sequence.
	}

For a full example of how this all works, you can checkout the RaspberryPI code and Arduino code for the Gasworks project on github. Enjoy!

Comments:

You can join the conversation on Twitter or Instagram

Become a Patreon to get early and behind-the-scenes access along with email notifications for each new post.