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