OneInOneOutASCII Arduino Sketch

This sketch is an Arduino program which acts as an simplified hardware I/O server using a simple readable message protocol. The intent is to provide an easily modified and extended real-time embedded hardware controller which can interface easily with a non-real-time client running on a laptop or Raspberry Pi.

The following documentation was extracted from the OneInOneOutASCII sample sketch and highlights particular functions, variables, and classes within the code.

Note that if your only objective is basic hardware access the Firmata firmware is more efficient and complete. It however is significantly harder to extend, and the binary protocol is harder to debug.

Top-Level Functions

void setup(void)

Standard Arduino initialization function to configure the system.

void loop(void)

Standard Arduino polling function to handle all I/O and periodic processing. This loop should never be allowed to stall or block so that all tasks can be constantly serviced.

ASCII Messaging Protocol

static void parse_input_message(int argc, char *argv[])

Process an input message. Unrecognized commands are silently ignored. The input is provided an array argv of argc pointers to strings, one per token.

static void hardware_input_poll(void)

Polling function to read and send specific input values at periodic intervals.

static void serial_input_poll(void)

Polling function to process messages arriving over the serial port. Each iteration through this polling function processes at most one character. It records the input message line into a buffer while simultaneously dividing it into ‘tokens’ delimited by whitespace. Each token is a string of non-whitespace characters, and might represent either a symbol or an integer. Once a message is complete, parse_input_message() is called.

static void send_debug_message(const char *str)

Send a single debugging string to the console.

static void send_debug_message(int i)

Send a single debugging integer to the console.

static void send_message(const char *command, long value)

Send a single-argument message back to the host.

static void send_message(const char *command, long value1, long value2)

Send a two-argument message back to the host.

static void user_message_0(char *command)

Convenience function provided to help with extending the messaging protocol; this function receives zero-argument messages which just contain a token as a string, e.g. “stop”. The protocol can also be extended by modifying parse_input_message().

static void user_message_1(char *command, int value)

Similar to user_message_0; process one-argument messages with a single value. E.g. “speed 33”.

static void user_message_2(char *command, int value1, int value2)

Similar to user_message_0; process two-argument messages. E.g. “pantilt 0 33”.

Full Source Code

The full code is all in one file OneInOneOutASCII.ino.

  1/// \file OneInOneOutASCII.ino
  2/// \brief Arduino program to act as an simplified hardware I/O server using a simple message protocol.
  3
  4/// \copyright Copyright (c) 2014, Garth Zeglin.  All rights reserved. Licensed
  5///            under the terms of the BSD 3-clause license as included in
  6///            LICENSE.
  7
  8/// \details This example is intended as a starting point for adding low-latency
  9///          hardware-level computing on an Arduino coupled to dynamic code
 10///          (e.g. Pure Data or Python) on a laptop or Raspberry Pi.  The
 11///          communications between the Arduino and the host uses a simple
 12///          message protocol based on lines of ASCII text.
 13
 14/****************************************************************/
 15/**** ASCII messaging scheme ************************************/
 16/****************************************************************/
 17
 18// The message protocol is based on commands encoded as a sequence of string
 19// tokens and integers in a line of text.  One line is one message.  All the
 20// input message formats begin with a string naming a specific command or
 21// destination followed by one or two argument integers.  The output formats are
 22// similar but include more general debugging output with a variable number of
 23// tokens.
 24
 25// The following message formats are recognized by this program.  Note that not
 26// all pins or channels are supported, e.g., servo output is only supported on a
 27// particular pin.
 28
 29// Command	Arguments		Meaning
 30// led		<value>			controls the built-in LED, value is 0 or non-zero
 31// poll         <value>                 set the input polling rate, value is milliseconds 
 32// pwm		<pin> <value>		PWM control on given pin
 33// dig		<pin> <value>		digital output on given pin, value is 0 or non-zero
 34// svo		<pin> <value>	 	hobby servo PWM control signal on given pin, value is angle in degrees
 35
 36// Additional messages can be added by inserting code in the user_message_#() functions below.
 37
 38// This program generates the following messages:
 39
 40// Command	Arguments		Meaning
 41// dbg		<value-or-token>+	debugging message to print for user
 42// clk		<microseconds>		Arduino clock time in microseconds
 43// led		<value>			reply with current LED state
 44// ana		<channel> <value>	analog input value on given channel, value is 0 to 1023
 45// dig		<pin> <value>		digital input value on PIN8, value is 0 or 1
 46
 47/****************************************************************/
 48/**** Library imports *******************************************/
 49/****************************************************************/
 50
 51// Use the Servo library for generating control signals for hobby servomotors.
 52// Hobby servos require a specific form of pulse-width modulated control signal,
 53// usually with positive-going pulses between 1 and 2 milliseconds repeated at
 54// 50 Hz.  Note that this is a significantly different signal than the PWM
 55// usually required for powering a motor at variable torque.
 56#include <Servo.h>
 57
 58/****************************************************************/
 59/**** Global variables and constants ****************************/
 60/****************************************************************/
 61
 62// The baud rate is the number of bits per second transmitted over the serial port.
 63#define BAUD_RATE 115200
 64
 65// Interval in milliseconds between input samples.
 66static unsigned int hardware_polling_interval = 50; // 20 Hz samples to start
 67
 68// Create a hobby servo control signal generator.
 69static Servo servo_output;
 70static const int servo_output_pin = 4;
 71
 72// The maximum message line length.
 73#define MAX_LINE_LENGTH 80
 74
 75// The maximum number of tokens in a single message.
 76#define MAX_TOKENS 10
 77
 78// Some version of the Arduino IDE don't correctly define this symbol for an
 79// Arduino Uno.
 80#ifndef LED_BUILTIN
 81#define LED_BUILTIN 13
 82#endif
 83
 84/****************************************************************/
 85/**** Utility functions *****************************************/
 86/****************************************************************/
 87
 88/// Send a single debugging string to the console.
 89static void send_debug_message( const char *str )
 90{
 91  Serial.print("dbg ");
 92  Serial.println( str );
 93}
 94
 95/****************************************************************/
 96/// Send a single debugging integer to the console.
 97static void send_debug_message( int i )
 98{
 99  Serial.print("dbg ");
100  Serial.println( i );
101}
102
103/****************************************************************/
104/// Send a single-argument message back to the host.
105static void send_message( const char *command, long value )
106{
107  Serial.print( command );
108  Serial.print( " " );
109  Serial.println( value );
110}
111
112/****************************************************************/
113/// Send a two-argument message back to the host.
114static void send_message( const char *command, long value1, long value2 )
115{
116  Serial.print( command );
117  Serial.print( " " );
118  Serial.print( value1 );
119  Serial.print( " " );
120  Serial.println( value2 );
121}
122
123/****************************************************************/
124// Wrapper on strcmp for clarity of code.  Returns true if strings are
125// identical.
126static int string_equal( char *str1, char *str2) 
127{
128  return !strcmp(str1, str2);
129}
130
131/****************************************************************/
132/****************************************************************/
133// Application-specific message processing.  You can customize these functions
134// to add additional message types.
135
136/// Convenience function provided to help with extending the messaging protocol;
137/// this function receives zero-argument messages which just contain a token as
138/// a string, e.g. "stop".  The protocol can also be extended by modifying
139/// parse_input_message().
140static void user_message_0( char *command )
141{
142  if (string_equal(command, "stop")) {
143    // do something to set the stop state here
144
145    send_debug_message("now stopped");
146
147  } else  if (string_equal(command, "start")) {
148    // do something to set the start state here
149
150    send_debug_message("starting");
151  }
152  // ...
153}
154
155/// Similar to user_message_0; process one-argument messages with a single
156/// value. E.g. "speed 33".
157static void user_message_1( char *command, int value )
158{
159  if (string_equal(command, "speed")) {
160    // do something to set the stop state using 'value'
161
162  }
163  // ...
164}
165
166/// Similar to user_message_0; process two-argument messages. E.g. "pantilt 0
167/// 33".
168static void user_message_2( char *command, int value1, int value2 )
169{
170  if (string_equal(command, "pantilt")) {
171    // do something using value1 and value2
172  }
173  // ...
174}
175
176/****************************************************************/
177/// Process an input message.  Unrecognized commands are silently ignored.
178///   \param argc   number of argument tokens
179///   \param argv   array of pointers to strings, one per token
180static void parse_input_message(int argc, char *argv[])
181{
182  // Interpret the first token as a command symbol.
183  char *command = argv[0];
184
185  /* -- process zero-argument commands --------------------------- */
186  if (argc == 1) {
187
188    // just pass it along
189    user_message_0( command );
190  }
191
192  /* -- process one-argument commands --------------------------- */
193  else if (argc == 2) {
194    int value = atoi(argv[1] );
195
196    // Process the 'led' command.
197    if ( string_equal( command, "led" )) {
198#ifdef LED_BUILTIN
199      pinMode( LED_BUILTIN, OUTPUT );
200      // turn on the LED if that value is true, then echo it back as a handshake
201      digitalWrite(LED_BUILTIN, (value != 0) ? HIGH : LOW);
202#endif
203      send_message( "led", value );
204    }
205    else if ( string_equal( command, "poll" )) {
206      if (value > 0)  hardware_polling_interval = value;
207      else send_debug_message("invalid poll value");
208    }
209
210    // else just pass it along
211    else user_message_1( command, value );
212  } 
213
214  /* -- process two-argument commands --------------------------- */
215  else if (argc == 3) {
216    int pin   = atoi(argv[1] );
217    int value = atoi(argv[2] );
218
219
220    // Process the 'pwm' command to generate a variable duty-cycle PWM signal on
221    // any digital pin.  The value must be between 0 and 255.
222    if ( string_equal( command, "pwm" )) {
223      analogWrite( pin, value );
224      return;
225    }
226
227    // Process the 'dig' command to set a pin to output mode and control its level.
228    else if ( string_equal( command, "dig" )) {
229      pinMode( pin, OUTPUT );
230      digitalWrite( pin, value );
231      return;
232
233    }
234
235    // Process the 'svo' command to generate a hobby-servo PWM signal on a particular pin.
236    // The value must be an angle between 0 and 180.
237    else if ( string_equal( command, "svo" )) {
238      if (pin == servo_output_pin) {
239	servo_output.write( value );
240      } else {
241	send_debug_message("unsupported servo pin");
242      }
243      return;
244    }
245
246    // else just pass it along
247    else user_message_2( command, pin, value );
248  }
249}
250
251/****************************************************************/
252/// Polling function to process messages arriving over the serial port.  Each
253/// iteration through this polling function processes at most one character.  It
254/// records the input message line into a buffer while simultaneously dividing it
255/// into 'tokens' delimited by whitespace.  Each token is a string of
256/// non-whitespace characters, and might represent either a symbol or an integer.
257/// Once a message is complete, parse_input_message() is called.
258
259static void serial_input_poll(void)
260{
261  static char input_buffer[ MAX_LINE_LENGTH ];   // buffer for input characters
262  static char *argv[MAX_TOKENS];                 // buffer for pointers to tokens
263  static int chars_in_buffer = 0;  // counter for characters in buffer
264  static int chars_in_token = 0;   // counter for characters in current partially-received token (the 'open' token)
265  static int argc = 0;             // counter for tokens in argv
266  static int error = 0;            // flag for any error condition in the current message
267
268  // Check if at least one byte is available on the serial input.
269  if (Serial.available()) {
270    int input = Serial.read();
271
272    // If the input is a whitespace character, end any currently open token.
273    if ( isspace(input) ) {
274      if ( !error && chars_in_token > 0) {
275	if (chars_in_buffer == MAX_LINE_LENGTH) error = 1;
276	else {
277	  input_buffer[chars_in_buffer++] = 0;  // end the current token
278	  argc++;                               // increase the argument count
279	  chars_in_token = 0;                   // reset the token state
280	}
281      }
282
283      // If the whitespace input is an end-of-line character, then pass the message buffer along for interpretation.
284      if (input == '\r' || input == '\n') {
285
286	// if the message included too many tokens or too many characters, report an error
287	if (error) send_debug_message("excessive input error");
288
289	// else process any complete message
290	else if (argc > 0) parse_input_message( argc, argv ); 
291
292	// reset the full input state
293	error = chars_in_token = chars_in_buffer = argc = 0;                     
294      }
295    }
296
297    // Else the input is a character to store in the buffer at the end of the current token.
298    else {
299      // if beginning a new token
300      if (chars_in_token == 0) {
301
302	// if the token array is full, set an error state
303	if (argc == MAX_TOKENS) error = 1;
304
305	// otherwise save a pointer to the start of the token
306	else argv[ argc ] = &input_buffer[chars_in_buffer];
307      }
308
309      // the save the input and update the counters
310      if (!error) {
311	if (chars_in_buffer == MAX_LINE_LENGTH) error = 1;
312	else {
313	  input_buffer[chars_in_buffer++] = input;
314	  chars_in_token++;
315	}
316      }
317    }
318  }
319}
320
321/****************************************************************/
322/// Polling function to read and send specific input values at periodic
323/// intervals.
324
325// N.B. The timing calculation could be improved to reduce jitter.
326
327static void hardware_input_poll(void)
328{
329  static unsigned long last_time = 0;
330  unsigned long now = millis();
331
332  if ((now - last_time) > hardware_polling_interval) {
333    last_time = now;
334    
335    // send A0 analog state
336    send_message( "ana", 0, analogRead(0) );
337
338    // send PIN8 digital state
339    send_message( "dig", 8, digitalRead(8) );
340
341    // send a time reading
342    long clock = micros();
343    send_message( "clk", clock );
344  }
345}
346
347/****************************************************************/
348/**** Standard entry points for Arduino system ******************/
349/****************************************************************/
350
351/// Standard Arduino initialization function to configure the system.
352void setup()
353{
354  // initialize the Serial port
355  Serial.begin( BAUD_RATE );
356
357  // send a message as a diagnostic
358  send_debug_message("wakeup");
359
360  // set up the hobby servo control output
361  servo_output.attach( servo_output_pin );
362
363  // additional hardware configuration can go here
364}
365
366/****************************************************************/
367/// Standard Arduino polling function to handle all I/O and periodic processing.
368/// This loop should never be allowed to stall or block so that all tasks can be
369/// constantly serviced.
370void loop()
371{
372  serial_input_poll();
373  hardware_input_poll();
374
375  // other polled tasks can go here
376}
377
378/****************************************************************/
379/****************************************************************/