Serial Communications in Ignition with Arduino

Today we’re going to be taking a look at using serial communication devices inside of Ignition. We will be using an Arduino microcontroller as our serial device, and using the Serial Support Client Module (Download) to gain access to the serial scripting functions we need to establish communication between the devices. We will be using an Ultrasonic Rangefinder to detect distance and when the distance falls below a setpoint we will rotate a Servo. The framework we will be building is intended to be built upon to suit your project needs, and you are encouraged to modify the programs and do try this at home.

Materials:

  • Arduino Uno
  • Parallax Standard Servo (4-6 VDC, Torque: 38oz-in @ 6V)
  • HC-SR04 Ultrasonic Distance Sensor
  • Breadboard
  • M2M Jumper Wires (x12)
  • M2F Jumper Wire (x4)

Setting Up The Arduino


The first thing we will do is set up our Arduino and the components we will use with it. Below is the wiring diagram for this project:

For the purposes of this article I’m not going to go into too much detail about Arduino programming, however I have included the arduino sketch source. If you are interested in learning more about Arduino you can visit the Arduino website.

#include <Servo.h>

int triggerPin = 2;
int echoPin = 3;
int servoPin = 10;
Servo servo;

long distance;

void setup() {
  // Initialize pin modes
  pinMode(triggerPin, INPUT);
  pinMode(echoPin, OUTPUT);
  
  servo.attach(servoPin); // attach the servo to the pin 
  servo.write(120); // Set the servo to mid point 
  Serial.begin(9600); // Begin serial communications with a 9600 baud rate
}

void loop() {
  poll(); //update distance and print to the stream

  //Handle any incoming data
  if(Serial.available()){
    serialInterpreter();
  }
  
  delay(250); //delay next cycle
}

//wait for outgoing serial data to complete transmission and end the connection
void terminate() {
  Serial.flush();
  Serial.end();
}

/* set the servo to the specified position while keeping the value within 
 * the supported range*/
void setServoPos(int pos){
  if (pos < 0){
    pos = 0;
  } else if (pos > 180){
    pos = 180;
  }
  servo.write(pos);
}

//Returns the time it took to detect an echo off of on object in front of the sensor
long ping(int echoPin, int triggerPin)
{
  unsigned long ping;
  
  digitalWrite(echoPin, LOW);
  delayMicroseconds(2);
  digitalWrite(echoPin, HIGH);
  delayMicroseconds(5);
  digitalWrite(echoPin, LOW);
  
  ping = pulseIn(triggerPin, HIGH);
  return ping;
}

//update distance and print to the stream
void poll(){
  Serial.flush();
  distance=ping(echoPin, triggerPin);
  Serial.println(distance);
}

//function to encapsulate handling of incoming transmissions
void serialInterpreter(){
    String line = readLine();
    if (line.startsWith("function: ")) {
      line.replace("function: ", "");
      parseSerialFunction(line);
    }
}

//reads 200 bytes of incoming data up until the new line character is detected
String readLine(){
  char stream[200];
  String line;
  if(Serial.available()){
    Serial.readBytesUntil("\n", stream, 200);
  }
  line = (String)stream;
  line.trim();
  return line;
}

//interprets incoming data intended to call a function stored on the Arduino
void parseSerialFunction(String line){
  int paramStartIndex = line.indexOf('(');
  int paramEndIndex = line.indexOf(')');
  String function = line;
  function.remove(paramStartIndex);
  String paramString = line.substring(paramStartIndex+1,paramEndIndex);

  /* Supported functions logic */
  if(function == "setServoPos") {
    setServoPos(paramString.toInt());
  }

  if(function == "poll") {
    poll();
  }

  if(function == "terminate") {
    terminate();
  }
}

Setting Up The Serial Module Scripts


The functions that are provided by the Serial Support Client Module are:

An important thing to note about serial communications is that attempting to read or write to a serial port that is not open or available will throw IOError exceptions. For this reason we are going to encapsulate the built in serial methods into our own functions with try/except statements to handle most of the errors, and print their contents to the console rather than interrupting our program execution. To do this we are going to create a new script in the project script library. In the Project Browser, right-click on “Script Library [project]” and select “New Script” to create a script and name it “serial”.

Open it up to get started. We are going to incorporate our own method of tracking which ports or open, so that we can build on this foundation in the future and support the management of multiple serial connections. We will start by declaring a python dictionary to store boolean values representing which ports are open that we can look up by name.

isOpen = {}

Next we are going to need to be able to open a port for communication. To do this we will check if the port is already open, close it if it is, configure the serial port, open it and register it as open.

def openSerialPort(portName, baudRate):
	try:
		if portName in isOpen and isOpen[portName]:
			closeSerialPort(portName)
		system.serial.configureSerialPort(portName, baudRate)
		system.serial.openSerialPort(portName)
		isOpen[portName] = True
	except IOError, e:
		print str(e)

In order to prevent memory leaks we want to make sure we have a way to close serial ports when we are done with them, otherwise they may persist.  We also need to be able to update our dictionary to reflect the ports closing.

def closeSerialPort(portName):
	try:
		if portName in isOpen:
			if isOpen[portName]:
				system.serial.closeSerialPort(portName)
				isOpen[portName]=False
	except IOError, e:
		print str(e)

We will also need to provide a way for registering incoming transmissions from the serial port to a tag. This function has a little built in protection that will attempt to open a new port in the event the port is closed when the function is called.

def updateSerialTag(tagPath, portName, baudRate):
	try:
		if portName not in isOpen or isOpen[portName] == False:
			openSerialPort(portName, baudRate)
		line = system.serial.readLine(portName)
		system.tag.write(tagPath, line)
	except IOError, e:
		print str(e)

As you may have noticed in the list of function there is no built in way to write a single line ending in a new line character without explicity ending every call to system.serial.write() with ‘\n’. To make it easier we’re just gonna add it automatically inside of our next function:

def writeLine(portName, msg):
	try:
		if isOpen[portName]:
			system.serial.write(portName, msg+'\n')
	except IOError, e:
		print str(e)

And finally, as a forethought for future applications we’ll add a simple function to close all active ports:

def closeAllPorts():
	for key, value in isOpen:
		closeSerialPort(key)

The Project


The first thing we are going to do is create two new tags, one to store the setpoint to trigger the servo movement, and another to store the data obtained from the serial port. Right-click the “Tags” folder in the Tag Browser, and create a new folder called “Serial”, then right-click and create two new memory tags called DistanceRaw and MinDistSP, both of which will be Integers.

Next create a new window and add two buttons, one to start the serial communication, and one to end it. Select your start button and press “Ctrl+J” to bring up the scripting dialogue, select action->actionPerformed and select the script tab. Then add the following line of code:

project.serial.openSerialPort("COM3", 9600)

Then, on the close button add:

project.serial.closeSerialPort("COM3")

Notice we are using “COM3” as our port name, however, yours may vary. On a windows machine you can check the port name in the Device Manager under Ports(COM & LPT)

Great! Now we have a way to start and stop the connection. Let’s create a way to update our tags value. Add a Timer to the Window, and configure it as shown below:

Next, press “Ctrl+J” to bring up the scripting dialogue. and add the following under the script editor tab as a propertyChange script:

if event.propertyName == "value":
	if event.source.value == 1:
		project.serial.updateSerialTag("Serial/DistanceRaw", "COM3", 9600)

At this point, if our arduino is connected, putting the Designer in preview mode will continuously update the tags value with our distance sensor’s reading. Let’s display this value on our page by binding the tags value to the value property of a Numeric Display. With the Numeric Display selected, click on the Binding Icon  next to the Value property in the Property Editor, select “Tag” as the binding type and browse the treeview for our DistanceRaw tag:

Now we are going to add some controls to our project. Drag a Numeric Text Field and 2 Spinners to the window. Select the Numeric Text Field, and bind the Value(Integer) property to the MinDistSP tag using the same method as above. This time however, we need to make sure that the “Bidirectional” checkbox is checked. This will make sure any input to this field will automatically update the tag value, and the display will always show the tags current value.

Configure the two Spinner’s Numeric Maximum properties to be 180, and name them “defaultServo” and “rotationServo” to be easily identified. These will be used to set the default resting angle of the servo and the angle to rotate it to when the distance falls below the MinDistSP value.

Select the Numeric Display and click on the Customizer icon   to add a custom boolean property to the component called “activated”. In the Property Editor select the Binding icon  next to the “activated” property, select the “Expression” binding type and add the following code as the expression:

if ({Serial/DistanceRaw} < {Serial/MinDistSP}, true, false)

This expression will set the value of the activated flag to true if the distance is less than the setpoint, and false if it is greater.

Finally, we only need to call our servo control functions on the arduino and pass in the values we supply when the flag value is changed. With the Numeric Display still selected Press “Ctrl+J” to bring up the scripting dialogue, and add the following code to the script editor tab as a propertyChange script:

if event.propertyName == "activated":
	if event.source.activated:
		project.serial.writeLine("COM3","function: setServoPos("+str(event.source.parent.getComponent('rotationServo').intValue)+")")
	else:
		project.serial.writeLine("COM3","function: setServoPos("+str(event.source.parent.getComponent('defaultServo').intValue)+")")

Thats it! Take it for a test drive. As I said before you are encouraged to modify the program, for instance, adding a way to manually set the servo position via a third spinner, or storing the input data history in a database. Thanks for reading, hope you enjoyed this. If you have any questions please don’t hesitate to ask. Subscribe to our blog for more cool Ignition tips, tricks, projects and more.

One thought on “Serial Communications in Ignition with Arduino

  1. Is the module not free? At one time serial capability was free…..
    Have you took a look at the OPC sketch for the arduino?
    Very cool project but super disappointing the module is not free.
    Such a basic module yet its not free?????

Leave a Reply

Your email address will not be published. Required fields are marked *