If you have read Haruki Murakamiβs book: What I talk about when I talk about Running, youβll probably be expecting some wise saying just about now π. Remaining true to the book, I guess weβll have to get creative, right? So, thereβs this wise saying that goes like this, Very few things indeed are really impaRsible. With the emphasis on the R of course, I guess you are getting the gist of our *wise* saying π.
Anyhow, letβs get back to the serious stuff. What could we actually talk about when we talk about R or Arduino, or more interestingly R and Arduino? Frankly speaking, the name Arduino has a nice ring to it (selfishly because it starts with an R sound), and both have a thing for color blue π€.
But thatβs beside the point. Putting it very simply, Arduino is an open-source electronics platform based on easy-to-use hardware (Arduino Board) and software (Arduino IDE). One can tell the board what to do if one has the correct form of data and a set of instructions for processing the data and performing subsequent operations. The Arduinoβs microcontroller is responsible for holding all your compiled code and executing the commands you specify. The Arduino Software on the other hand is the boardβs IDE where one writes the set of instructions governing the board. The getting started guide would be a good place to start learning about the Arduino ecosystem.
Switching over to R, we couldnβt have found better words to summarize what R is than with these words found in the book Advanced R by Hadley Wickham: Despite its sometimes frustrating quirks, R is, at its heart, an elegant and beautiful language, well tailored for data science. π
With all this said, a fine convergence can be struck between the two: data. Consider this very simple example. We want the Arduino board to turn an LED (Light Emitting Diode) ON once it receives a 1 and OFF once it receives a 0. If one can get a way of sending some data (1 or 0) to the boardβs microcontroller, then, the set objective will be achieved sooner or later. This will serve as the basis of our post: Exploring the interoperability between R and Arduino by establishing a flow of data between the two and having instructions on the boardβs microcontroller that execute commands, based on the data received. In this context, R will be handling data and all the hustle associated with it and sending it to Arduino. Arduino on the other hand will be actuating peripherals based on the data it has received.
How will we achieve this? π₯ π₯ β¦ using Arduinoβs capability to be programmed directly via a serial port (more on this later).
Below is a quick overview of what weβll use to demonstrate the interoperability of R and Arduino:
First, weβll send a series of data defining the brightness (in the range 0% - 100%) of 3 LEDs from the Rstudio IDE to the Arduinoβs serial port.
An Arduino script waits until serial data is available, extracts the brightness values for the 3 LEDs, maps them to analog values (0 - 255), instructs the boardβs microcontroller to write these values to the LEDs and then sends the mapped values (0 - 255) to the Rstudio-Arduino serial interface.
Rstudio will read the values sent from Arduino to the serial. Weβll then use these values (in the range 0 - 255) to create a data set for rotating a servo motor and pass these values to the serial interface.
Once the Arduino detects there is serial data again, it reads each value (0 - 255) on the serial interface, maps it to an angle rotation value (in the range 0Β° - 180Β°), rotates the servo and sends the angle back to the serial interface.
Finally, weβll read the motor angles and wrap it off with some ggplot π in Rstudio.
When all is said and done, this is what we want to achieve using R and Arduino, working in tandem:
To follow along, youβll need an Arduino IDE and Board (we used the Arduino UNO board at the time of writing this), Red, Green and Blue LEDs, a Servo Motor (SG90) and Jumper wires, all wrapped up in enthusiasm tinged with some spunk! The hardware components will be connected as shown below:
For the RStudio part, weβll be requiring libraries in the Tidyverse, the Magrittr package, Plotly package and the Serial package. The tidyverse is a collection of R packages designed for data science tasks such as data wrangling and visualization. The serial package enables reading and writing binary and ASCII data to RS232/RS422/RS485 or any other virtual serial interface of the computer. The plotly package creates interactive web graphics from βggplot2β graphs. You can have them installed as follows:
install.packages(c("tidyverse", "serial", "plotly", "magrittr"))
Time to fire up Rstudio. Letβs begin by loading the libraries weβve just installed.
suppressPackageStartupMessages({
library(tidyverse)
library(serial)
library(plotly)
library(magrittr)
})To obtain a list of the installed serial interfaces in your computer, simply use the serial::listPorts function.
## [1] "COM11" "COM12" "COM2" "COM6" "COM7" "COM9"
Great! Seems we have about six serial interfaces at our disposal. More than enough!
Next, weβll create a serial port object called arduino, which represents a serial client for communication with the USB serial port where our board is connected. Among the 6, where is our Arduino UNO connected π€?
This vital information can be obtained by firing up the Arduino IDE navigating to Tools β’ Serial Port and then selecting the appropriate port as shown in the snippet below:
Snippet of Arduinoβs IDE showing the the Arduino boardβs serial port.
In our case, the USB serial port was COM9. With this info, we should be well on our way to creating a serial interface connection with the board. This is achieved using the serial::serialConnection function. The interface parameters are such that the baud rate (specifies the number of bits being transferred per second) is set to 9600, which is the same value in the Arduino script. Also, we have specified that the transmission ends with a new line and that the transmission is complete if the end of line symbol is the carriage return cr. Now, letβs R this up!
arduino <- serialConnection(
port = "COM9",
mode = "9600,n,8,1" ,
buffering = "none",
newline = TRUE,
eof = "",
translation = "cr",
handshake = "none",
buffersize = 4096
)
Now that the serial interface is in place, the next step is initialising the interface and keeping it open for later usage such as writing and reading data from it. Once serial::isOpen initialises the interface, the Arduino board blinks. This is because the board resets once a serial port is opened to allow the bootloader to receive a new sketch.
serial::isOpen tests whether the connection is open or not.
## [1] TRUE
At this point, we are all set to write some data to the serial interface. The values weβll be sending to the serial interface are in the range of 0 - 100, expressing the desired percentage of LED brightness. Also, weβll append letter characters R G B to help Arduino distinguish what value is written to what LED. Weβll see this in just a moment.
In the meantime, letβs just whip up some R script that creates a data set with 3 columns r g b and which appends a letter to their brightness values.
n <- 60
arduino_input <- tibble(
r = (sample(1:100, size = n, replace = T) %>%
paste('R', sep = '')),
g = (sample(1:100, size = n, replace = T) %>%
paste('G', sep = '')),
b = (sample(1:100, size = n, replace = T) %>%
paste('B', sep = ''))
)
# get a glimpse of the arduino_input
glimpse(arduino_input)## Rows: 60
## Columns: 3
## $ r <chr> "73R", "33R", "12R", "16R", "83R", "6R", "74R", "42R", "33R", "99...
## $ g <chr> "68G", "95G", "93G", "78G", "4G", "88G", "37G", "89G", "17G", "34...
## $ b <chr> "95B", "93B", "36B", "56B", "61B", "71B", "73B", "80B", "36B", "1...
With that brief glance tibble::glimpse() has accorded us, we are able to observe that the LED values that will be written to the serial interface are of type character. We can blame/thank paste() for this, but in retrospect, this is the desired data type for serial communications.
So this is it, in the case of serial communication, the ASCII character set is used to represent all the letters, numbers, symbols, and special commands that you might want to send.
The chunk below uses serial::write.serialConnection() to write the LED values to the serial port row by row.
Now, letβs SHIP IT!
# good practice to close then open the connection again
close(arduino)
open(arduino)
# gives enough time for the board to reset once a serial interface
# is initiated
Sys.sleep(2)
for (r in seq_len(n)){
Sys.sleep(0.1)
write.serialConnection(arduino, paste(arduino_input[r,], collapse = ''))
}You are probably wondering, How will character values light up the LEDs?. Our Arduino script will handle this as shown in the snippet below:
Snippet of Arduinoβs IDE showing how Arduino will make decisions based on incoming data.
The main Arduino program loop waits until serial data is available (if(Serial.available())), stores the data on the interface as a character vector (mychar) and then runs it through a series of switch statements. In particular, a switch statement compares the value of a variable to the values specified in case statements. When a case statement is found whose value matches that of the variable, the code in that case statement is run.
Letβs take an example. For instance, say the character vector sent from RStudio is 94R44G22B. The first match case is case '0'...'9': which is converted to an integer by subtracting the zero-valued character.
\(t\), which represents LED brightness in the range 0 - 100% is first initialised to \(0\). The first value to be read will be \(9\). Consequently, the value of \(t\) becomes:
\(t\,=\,0\,\times\,10\,+\,('9'\,-\,'0')\)
\(\therefore\,t\,=\,9\)
The second value is a \(4\) and which matches the first case. The new value of \(t\) becomes:
\(t\,=\,9\,\times\,10\,+\,('4'\,-\,'0')\)
\(\therefore\,t\,=\,94\)
The next value that is read is an \('R'\) which matches case 'R'. In this case, the value of \(t\,=\,94\) is remapped to an analog value in the range \(0-255\) that can be used with analogWrite() functions.
For folks wondering why there is need for remapping values that go to the analogWrite() function, itβs coming right at you. So, if we just wanted to blink and LED ON and OFF, we would simply send a digital HIGH (5v) or a digital LOW (0v). But what if we want to output a voltage other than 0v or 5v, such as varying the brightness of the LED? Well, we canβt- unless we are using a digital-to-analog converter (DAC) integrated circuit.
However, one can get pretty close to generating analog output values by using a trick called pulse-width modulation (PWM). Select pins on each Arduino can use the analogWrite() command to generate PWM signals that can emulate a pure analog signal when used with certain peripherals. These pins are marked with a ~ on the board. On the Arduino Uno, pins 3, 5, 6, 9, 10, and 11 are PWM pins.
The PWM output is an 8-bit value. In other words, you can write values from \(0\) to \(2^8 - 1\), or \(0\) to \(255\). In the case of our LED circuit, mapping the output to 255 will result in full brightness, and 0 will result in the LED turning off, with the brightness varying between these two values.
Okay, now back to case 'R'! Once an analog value is written to the LED, one interesting instruction follows Serial.println(rval). As you might have guessed, this is Arduinoβs way of saying: Write that value to the serial port!. After this is done, the value of t is set back to 0 and the next input characters are run through the subsequent cases.
Now, letβs read the mapped values sent to the serial port connection by Arduino. read.serialConnection() is put to the test π€.
# reading mapped data sent from Arduino
data_frm_arduino <- tibble(
capture.output(cat(read.serialConnection(arduino,n=0)))
)
# select the first 9 rows
data_frm_arduino %>% slice_head(n = 9)Wow! Yeah! Weβve got our re-mapped data back home ποΈ! Something interesting to note is that read.serialConnection() reads the whole buffer at once. Also, the data is in a long format since reading takes place per line. This can probably be corrected by playing around with end-of-line characters specified at the translation option in serial::serialConnection but weβll leave it at that, for now.
Would we be staying true to R and the principles of Tidy data if we left the data_frm_arduino data set as it is? Nope! Well then, letβs get our wrangling on!
data_frm_arduino %<>% tibble(
# assigning values to their approriate LED
led_names = rep_along(seq_len(nrow(data_frm_arduino)), c('mapped_r','mapped_g','mapped_b'))) %>%
# renaming the first column
rename("led_val" = 1) %>%
group_by(led_names) %>%
# adding identifiers as required by pivot_wider
mutate(row = row_number()) %>%
# creating new columns using 'led_val' values
pivot_wider(names_from = led_names, values_from = led_val) %>%
# dropping the 'row' column
select(-row) %>%
# converting all columns to data type integer
mutate_all(as.integer)
data_frm_arduino %>% slice_head(n = 10)A data set showing the initial LED values sent from RStudio to Arduinoβs serial port and the mapped values sent back would communicate things better. Letβs get right at it.
combined_data <- as_tibble(
# merge the two data sets
cbind(arduino_input, data_frm_arduino)) %>%
# drop non numeric characters eg R, G, B
mutate(across(where(is.character), ~parse_number(.x)), across(where(is.double), as.integer)) %>%
# reorder columns.. dplyr::relocate can do the trick too
select(c(1, 4, 2, 5, 3, 6))
combined_data %>% slice_head(n = 10)At this point, weβve already sent data from RStudio to Arduino, remapped it, lighted up some LEDs, and sent the remapped data back to the RStudio IDE. Thatβs been an incredible voyage by all means. Letβs wrap it with one final adventure: driving a servo motor.
So, this is it. From the RStudio IDEβs end, weβll create a new dataset from the received remapped values (0-255), append a terminating character, and write these values to the Arduinoβs serial port.
# creating a new dataset that selects values in the order:
# maxmimum of received LED values then minimum of the LED values
# then maximum then minimum and on and on we go ...
row_min <- tibble(min_input = data_frm_arduino %>% apply(1,min)) %>%
# select even rows
filter(row_number() %% 2 == 0)
servo_input <- tibble(servo_in = data_frm_arduino %>% apply(1,max))
# replacing the even rows with a minimum value
servo_input[c(1:n)[c(F,T)],] <- row_min
# appending a terminating character
servo_input %<>% mutate(servo_in = servo_in %>% paste('S', sep = ''))And off we write the values to the serial interface.
close(arduino)
open(arduino)
Sys.sleep(2)
for (r in seq_len(n)){
Sys.sleep(1)
write.serialConnection(arduino, paste(servo_input[r,], collapse = ''))
}The main Arduino program loop waits until serial data is available, extracts the integer value, remaps the value from the range \(0 - 255\) to a servo angle \(0 - 179\), and the writes this value to the servo. Our stalwart board then prints the mapped angle value to the serial interface. An Arduino snippet where this magic happens is as shown:
Now, letβs get what Arduino echoed back at us and do some data wrangling while at it.
angl_frm_ard <- tibble(
# reading mapped angles sent from Arduino
capture.output(cat(read.serialConnection(arduino,n=0)))) %>%
# renaming first column
rename("mapped_servo_angles" = 1) %>%
mutate_all(as.integer)
# select the first 10 rows
angl_frm_ard %>% slice_head(n = 10)############### what we sent vs what we received ##############
combined_angles <- as_tibble(
# merge the two data sets
cbind(servo_input, angl_frm_ard)) %>%
# drop non numeric character S
mutate(across(where(is.character), ~parse_number(.x)),
across(where(is.double), as.integer))
combined_angles %>%
slice_head(n = 10)Much better! Column servo_in shows the data we sent to Arduino, while mapped_servo_angles represents what Arduino wrote to the servo and echoed back at us. Such friendship π€!
Now, as they say, allβs well that ends with an informative visualization (to be taken with a grain of salt π).
Letβs see the sweep made by the servo at each instance of writing angle data.
theme_set(theme_light())
plt <- angl_frm_ard %>%
ggplot(mapping = aes(x = 1:nrow(angl_frm_ard),
y = mapped_servo_angles)) +
geom_line() +
# smooth line fitted to the data
geom_smooth(se = F) +
labs(x = "Count",
y = "Servo angle",
title = "Variation of servo angle at each count instance")+
theme(plot.title = element_text(hjust = 0.5))
ggplotly(plt)## `geom_smooth()` using method = 'loess' and formula 'y ~ x'
That must have been quite an erratic trajectory! But yeah, we just wanted to illustrate that a servo could be rotated. Weβll get to real-life and more practical use-cases real soon.
Itβs time we wrapped things up.
In this post, we really tried to show the bi-directional flow of data between RStudio IDE and the Arduino. At each instance, the data we sent to the Arduino got transormed, actuated a peripheral and then echoed back. The data we got back then went through some tidying and wrangling to put it in the right format that would be executed by the Arduino in subsequent operations.
We hope this got you up to speed with both Arduino and R, and ignited a genuine interest to explore the amazing things one can do with these two beauties!
We really look forward to exploring, learning and Ring more on this topic β¦ soon.
Thanks for reading!
Be sure to check out great blogs, tutorials and other formats of R resources coming out every day at RWeekly.org!
Till then,
Happy Learning π©π½βπ» π¨βπ» π¨πΎβπ» π©βπ» ,
Eric (R_ic) (Gold Microsoft Learn Student Ambassador), Ian (Co-organizer DekutR Data Science Community) and Sam (Co-organizer DekutR Data Science Community).
H. Wickham and G. Grolemund, R for Data Science: Visualize, Model, Transform, Tidy, and Import Data. 2017.
J. Blum, Exploring ARDUINO tools and techniques for engineering wizardry, 2nd Edition. 2019.
Here is the Arduino script that was uploaded to the board. It is responsible for instructing the board what to do based on the data it receives from RStudio IDE.
# include <Servo.h>
// for storing readings for RGB leds and servo
int rval = 0;
int gval = 0;
int bval = 0;
int sval = 0;
int RED = 6; // Red LED on pin 6
int GREEN = 5;
int BLUE = 3;
int SERVO = 9; // Servo on Pin 9
Servo myServo;
void setup() {
// put your setup code here, to run once:
Serial.begin(9600); // serial port at 9600 baud
// setting pins as output
pinMode(RED, OUTPUT);
pinMode(GREEN, OUTPUT);
pinMode(BLUE, OUTPUT);
// Attaching the Servo object
myServo.attach(SERVO);
}
void loop() {
if (Serial.available()){
// creates variables visible to only 1 function. They persist
// beyond the function call and preserve their value
static int t = 0;
char mychar = Serial.read();
switch(mychar){
//mychar: a variable whose value to compare with various cases.
case '0'...'9':
t = t * 10 + mychar - '0';
break;
case 'R':
{
rval = map(t, 0, 100, 0, 255);
analogWrite(RED, rval);
Serial.println(rval);
}
t = 0;
break;
case 'G':
{
gval = map(t, 0, 100, 0, 255);
analogWrite(GREEN, gval);
Serial.println(gval);
}
t = 0;
break;
case 'B':
{
bval = map(t, 0, 100, 0, 255);
analogWrite(BLUE, bval);
Serial.println(bval);
}
t = 0;
break;
case 'S':
{
// map analogue LED value to an angle between 0 and 180 degrees
sval = map(t, 0, 255, 0, 179);
Serial.println(sval);
delay(5);
myServo.write (sval);
delay(150);
}
t = 0;
break;
}
}
}