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/****************************************************************/