Mathematica and Arduino

Ian Johnson, Wolfram Research

Join the Conversation #WolframTechConf

What is an Arduino?

Arduino is a small hobbyist microcontroller platform that makes it easy to interface with the world of hardware electronics without being an Electrical Engineer.

Mathematica has built in tools and utilities to interface with Arduino boards. With the DeviceFramework Arduino driver built into Mathematica 10.1+, live data can be imported into the Wolfram Language to be live analyzed with the most power of the Wolfram Language.

Additionally, Mathematica can upload to the Arduino and directly control physical hardware like motors, lights, and any number of the things from the giant Arduino open source community.

Materials used in this talk

Mathematica 10.2+

Arduino software 1.6.0, 1.6.1, 1.6.3, or 1.6.4 (note can be automatically installed from within Mathematica)

Arduino Uno (x2 for the Morse code encode and decode part)

LEDs

Pushbutton

220 Ω resistors for LEDs

10 kΩ resistor for pushbutton

Assorted wiring supplies

Photoresistor

First steps

Opening a connection

To interface with the device, you need to open a connection to it with the DeviceFramework function DeviceOpen.

DeviceOpen["Arduino","/dev/tty.usbmodem1451"]

After opening a connection, we need to upload the custom sketch that the Arduino runs as it’s main program with DeviceConfigure

DeviceConfigure["Arduino","Upload"]

Writing and Reading Pins

It is easy to read and write from the pins on the Arduino with DeviceRead and DeviceWrite

Reading from a digital pin returns the digital state of that pin, as either a 1 or 0.

DeviceRead["Arduino",4]

Reading from an analog pin by default returns a Quantity of the voltage on that pin.

DeviceRead["Arduino","A0"]

By using the option “ReadMode”, we can read the digital state of the pin

DeviceRead["Arduino",{"A0","ReadMode"->"Digital"}]

If we want the raw 10-bit numeric value from the ADC, we can use the Identity function with the “ReturnFunction” option

DeviceRead["Arduino",{"A0","ReturnFunction"->Identity}]

Of course we can use any kind of anonymous function with this, as well as using it with the digital pins.

DeviceRead["Arduino",{4,"ReturnFunction"->(If[#===1,Graphics[{Green,Disk[]}],Graphics[{Red,Disk[]}]]&)}]

We can make it dynamic too

Dynamic[DeviceRead["Arduino",{4,"ReturnFunction"->(If[#===1,4->Graphics[{Green,Disk[]},ImageSize->10],4->Graphics[{Red,Disk[]},ImageSize->10]]&)}]]

While this simple example runs fine, more complicated expressions may hang the kernel as Dynamic and Serial Ports don’t mix well.

A better method to get dynamic updating is with RunScheduledTask. Setup a Dynamic of just a variable and use a ScheduledTask to update that variable.

message="it's not on";
RunScheduledTask[If[DeviceRead["Arduino",4]===1,message="it's on",message="it's not on"]];
Dynamic[message]

RemoveScheduledTask[ScheduledTasks[]]

We can also very easily show how to use Mathematica and Arduino to build an extremely simple and intuitive logic box.

With[{vars={a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r},arduinoPins={2,3,4,5,6,7,8,9,10,11,12,13,"A0","A1","A2","A3","A4","A5"}},
Manipulate[
Column@Normal@DeviceWrite["Arduino",AssociationThread[arduinoPins,vars]],
Evaluate[Sequence@@(Table[{{vars[[index]],0,"Pin "<>ToString[arduinoPins[[index]]]},{0,1}},{index,18}])],
SynchronousUpdating->False,
ControlType->Setter
]
]

It is also easy to hard control the direction of pins with DeviceConfigure

DeviceConfigure["Arduino","PinConfigurations"-><|4->"Output","A3"->"Input"|>]

Trying to read from an output pin and write to an input pin will fail.

DeviceRead["Arduino",4]

DeviceWrite["Arduino","A3"->1]

You can use the device object for the Arduino to query the currently configured states of the pins.

FindDevices["Arduino"]

arduinoObject=First[%]

The PinConfigurations property has a Dataset of the Arduino’s last updated state.

arduinoObject["PinConfigurations"]

DeviceWrite[arduinoObject,13->1]

arduinoObject["PinConfigurations"]["D13"]

arduinoObject["PinConfigurations"]["A0"]

arduinoObject["PinConfigurations"]["A0"]["LastReadValue"]

Deploying Custom Functions to the Arduino

We have just scratched the surface of the interactivity with the Arduino. With the builtin driver inside Mathematica, we can upload custom C/C++ functions to the Arduino and execute them asynchronously from Mathematica.

DeviceConfigure allows “Upload” to be a rule going to a list of configuration options. Functions allows us to specify what ArduinoCode functions we want to upload. Here we will use SymbolicC for the function.

Needs["SymbolicC`"]

This simple function takes two integer arguments and adds them together

DeviceConfigure["Arduino","Upload"->{"Functions"-><|"SymbolicCAdd"->CFunction["long","add2",{{"long", "i"},{"long","j"}}, CReturn[ COperator[ Plus, {"i","j"}]]]|>}]

After this is done uploading, we can execute this function with DeviceExecute

DeviceExecute["Arduino","SymbolicCAdd",{1,2}]

DeviceExecute["Arduino","SymbolicCAdd",{1000,20000}]

Attempting to run this function with Real number arguments fails, as the C function is statically typed for 32-bit long integer arguments. In the documentation there is information on the mapping between types in Mathematica and types on the Arduino.

DeviceExecute["Arduino","SymbolicCAdd",{1.1,2.2}]

We can also define functions just by using a string and the ArduinoCode symbolic wrapper. This function will add two Real numbers together (implemented as 32-bit floating point numbers on the Arduino).

DeviceConfigure["Arduino","Upload"->{"Functions"-><|"NormalAdd"->ArduinoCode[<|"ArgumentTypes"->{Real,Real},"ReturnType"->Real,
"Code"->"
float add2(float i, float j)
{
	return i + j;
}
"|>]|>}]

We can execute this function as above with the function name “NormalAdd”

DeviceExecute["Arduino","NormalAdd",{3.2,4.2}]

An In-depth example with Morse Code

There are more options to configure the sketch that is uploaded to the Arduino, including Initialization, Libraries, and a few others mentioned in the Documentation Center.

With the “Libraries” option, we can include other C/C++ source code libraries into the sketch. Here, Morse is an Arduino library I wrote for this talk that simply flashes an LED in Morse code corresponding to the string passed in to it. That library can be found alongside the talk notebook on pathable.

DeviceConfigure["Arduino","Upload"->{"Libraries"->"~/Downloads/Morse.zip",Initialization->"Morse pin7Morse(7,200);","Functions"-><|"MorseCodeBlink"->ArduinoCode[<|"ArgumentTypes"->String,"Code"->"void blinkMorse(char * strToBlink) { pin7Morse.morseCoder(strToBlink);}"|>]|>}]

DeviceExecute["Arduino","MorseCodeBlink","SOS"]

DeviceExecute["Arduino","MorseCodeBlink","hello world"]

With another Arduino and a photoresistor, we can read the flashes of light, and decode what was sent. To do this, we will first need to send the word and then capture the raw data from the second Arduino

blinkingArduino=First[FindDevices["Arduino"]]

readingArduino=DeviceOpen["Arduino","/dev/tty.usbmodem1411"]

If we turn the LED on blinkingArduino on, and read from the photoresistor equipped arduino, we will see that the voltage is about 4.5-5 volts.

DeviceWrite[blinkingArduino,7->1]

DeviceRead[readingArduino,"A0"]

If we turn the LED off, and read again, there is a noticeable decrease.

DeviceWrite[blinkingArduino,7->0]

DeviceRead[readingArduino,"A0"]

We can use this discretization to measure whether the LED is on, and then we will be able to see for how long the LED is on.

Here, we will blink the string SOS, and then using the function DeviceReadTimeSeries, we will get a TimeSeries object of the values of the analog value of the photoresistor’s voltage.

(for the length, here I vastly over estimate the amount of time, each time unit is .2 seconds long, and the maximum number of time units for any character is for the literal ‘0’, which has 5 dashes(each with 4 time unit waits), and there is a delay of 2 time units between each character)

DeviceExecute[blinkingArduino,"MorseCodeBlink","new kind of science"];lightTimeSeries=DeviceReadTimeSeries[readingArduino,{12*.200*StringLength["new kind of science"],.02},"A0"]

We can easily visualize this data with DateListPlot.

DateListPlot[lightTimeSeries,PlotRange->{0,5}]

We notice there’s a lot of time after the signal where it doesn’t change at all. This won’t affect our analysis much, but to see the signal better, we can take the first half of the time series and plot just that.

ListLinePlot[Take[Values[lightTimeSeries],Round@((3*Length@Values@lightTimeSeries)/4)],PlotRange->{0,5}]

Now, let’s binarize this into values that are either on (1) or off (0). We do this by comparing each value to the middle value. First, let’s get the middle value with the function MinMax

middleValue=With[{minMax=MinMax[lightTimeSeries]},((Last[minMax]-First[minMax])/2)+First[minMax]]

Now, we can determine if all of the points are above or below the middle point and binarize the list

binarizedList=(#≥middleValue)&/@Values[lightTimeSeries]/.{True->1,False->0}

ListLinePlot[binarizedList]

Now we need to split the list into continued sequences of 1’s and 0’s.

sequences=Split[binarizedList]

Then we get the length and value of each of the sequences.

sequenceLengthValues={Length[#],First[#]}&/@sequences

sequenceLengthValues[[All,1]]

Now, we need to filter the similarly lengthed values into the same groups. That is, 28, 29, and 30 all correspond to the same time unit length, so we need to group those together, just as 8, 9, and 10 all correspond to the same information. We should also discard the values at the end that don’t contain any information

specificLengths=Select[#<100&&#>4&]@Union[sequenceLengthValues[[All,1]]]

splitLengths=With[{minMax=MinMax@specificLengths},BinLists[specificLengths,{1+Append[Table[i,{i,0,Last[minMax],Round[((Subtract@@(Reverse@minMax))/(If[Last[minMax]>50,3,2]))]}],Last[minMax]]}]/.{}->Nothing]

Now we construct a replacement association for the lengths to replace the arbitrary numbers with specific time lengths

lengthReplacements=Join[
AssociationThread[First[splitLengths],Table["short",{Length[First[splitLengths]]}]],
AssociationThread[splitLengths[[2]],Table["long",Evaluate@{Length[splitLengths[[2]]]}]],
If[Length[splitLengths]===3,AssociationThread[splitLengths[[3]],Table["space",Evaluate@{Length[splitLengths[[3]]]}]],<||>]
]

Now replacing the original lengths list gives us a completely unarbitrary representation of the information contained in the signal.

newSequences=sequenceLengthValues/.lengthReplacements

These are some simple replacement rules for the signals and their mapping to letters and numbers

dashes=<|{"short",1}->".",{"short",0}->" ",{"long",0}->"\n",{"long",1}->"-",{"space",0}->"*"|>;
letters=\[LeftAssociation]". -"->"a","- . . ."->"b","- . - ."->"c","- . ."->"d","."->"e",". . - ."->"f","- - ."->"g",". . . ."->"h",". ."->"i",". - - -"->"j","- . -"->"k",". - . ."->"l","- -"->"m","- ."->"n","- - -"->"o",". - - ."->"p","- - . -"->"q",". - ."->"r",". . ."->"s","-"->"t",". . -"->"u",". . . -"->"v",". - -"->"w","- . - -"->"z",". - - - -"->"1",". . - - -"->"2",". . . - -"->"3",". . . . -"->"4",". . . . ."->"5","- . . . ."->"6","- - . . ."->"7","- - - . ."->"8","- - - - ."->"9","- - - - -"->"0","*"->" ","\""->" "\[RightAssociation];

This drops any beginning and ending signals that don’t map to anything and replaces the sequences with morse codes

morseCodes=Cases[_String]@(newSequences/.dashes)

We need to split the words by “*”, as that was the character chosen to represent the verbatim “ “ character. This delimits different words in the string

morseCodeWords=StringSplit[#,"\n"]&/@StringSplit[StringJoin[morseCodes],"*"]

Replacing the codes with letters we can see words form.

words=morseCodeWords/.letters

Joining the words together with spaces riffled in between gives us the decoded string

StringJoin[Riffle[words," "]]

We can put this all into a single function that takes the time series data and computes the original string. Note that this algorithm is independent of the time unit used, and also of the magnitude of the peaks and troughs of the signal. (Note that there are significant optimizations that could be made here, but I have replicated the algorithm demonstrated above for clarity)

decode[timeSeries_TemporalData]:=Module[
{
dashes=<|{"short",1}->".",{"short",0}->" ",{"long",0}->"\n",{"long",1}->"-",{"space",0}->"*"|>,
letters=\[LeftAssociation]". -"->"a","- . . ."->"b","- . - ."->"c","- . ."->"d","."->"e",". . - ."->"f","- - ."->"g",". . . ."->"h",". ."->"i",". - - -"->"j","- . -"->"k",". - . ."->"l","- -"->"m","- ."->"n","- - -"->"o",". - - ."->"p","- - . -"->"q",". - ."->"r",". . ."->"s","-"->"t",". . -"->"u",". . . -"->"v",". - -"->"w","- . - -"->"z",". - - - -"->"1",". . - - -"->"2",". . . - -"->"3",". . . . -"->"4",". . . . ."->"5","- . . . ."->"6","- - . . ."->"7","- - - . ."->"8","- - - - ."->"9","- - - - -"->"0","*"->" ","\""->" "\[RightAssociation],
middle,
oneZeroSequence,
sequenceLengths,
splitLengths,
lengthReplacements,
newSequences,
morseCodes
},
(*find the middle value of the time series*)
middle=With[{minMax=MinMax[Select[NumericQ]@(QuantityMagnitude/@Normal@Values[timeSeries])]},((Last[minMax]-First[minMax])/2)+First[minMax]];
(*binarize the values into 1 if above the midpoint, 0 if below*)
oneZeroSequence=Split[(#≥middle)&/@(Select[NumericQ]@(QuantityMagnitude/@Normal@Values[timeSeries]))/.{True->1,False->0}];
(*turn this into a list of the length of the sequence of the same value*)
sequenceLengths={Length[#],First[#]}&/@oneZeroSequence;
(*now we divide the lengths into either 2 or 3 lists depending on the presence of any spaces (which are over 60 time units long)*)
(*the first group is the short timings, the second is the long timings, and the third (if it exists) is the *)
splitLengths=Module[{specificLengths=Select[#<100&&#>4&]@Union[sequenceLengths[[All,1]]],minMax},minMax=MinMax@specificLengths;BinLists[specificLengths,{1+Append[Table[i,{i,0,Last[minMax],Round[((Subtract@@(Reverse@minMax))/(If[Last[minMax]>50,3,2]))]}],Last[minMax]]}]/.{}->Nothing];
(*then we make a replacement table for the lengths to map them to the absolute timing units instead of the approximate lengths*)
lengthReplacements=Join[
AssociationThread[First[splitLengths],Table["short",{Length[First[splitLengths]]}]],
AssociationThread[splitLengths[[2]],Table["long",Evaluate@{Length[splitLengths[[2]]]}]],
If[Length[splitLengths]===3,AssociationThread[splitLengths[[3]],Table["space",Evaluate@{Length[splitLengths[[3]]]}]],<||>]
];
(*penultimately, we make the replacements for the lengths, then for the lengths to the dashes and dots, drop the non-strings, and string join everything together*)
morseCodes=StringJoin[Cases[_String]@(sequenceLengths/.lengthReplacements/.dashes)];
(*finally, we split the words up (as noted by the asterisks), and split each word into letters by splitting by newlines, then replace all the letters and riffle the words with spaces and joining everything together*)
Return[StringJoin[Riffle[(StringSplit[#,"\n"]&/@StringSplit[morseCodes,"*"])/.letters," "]]]
];

DeviceExecute[blinkingArduino,"MorseCodeBlink","arduino"];
ts=DeviceReadTimeSeries[readingArduino,{14*.200*StringLength["arduino"],.03},"A0"]

DateListPlot[ts]

decode[ts]

Let’s put this all together into a little guessing game function  that chooses a random word with the new Mathematica 10.3 function RandomWord and then gives us the definition of the word

guessingGame[]:=Module[{randWord=RandomWord[],blinkTimeSeries,decodedWord},
privateWord=randWord;
DeviceExecute[blinkingArduino,"MorseCodeBlink",randWord];
blinkTimeSeries=DeviceReadTimeSeries[readingArduino,{14*.200*StringLength[randWord],.03},"A0"];
decodedWord=decode[blinkTimeSeries];
guessedWord="";
Column[
{
Row[{WordDefinition[decodedWord],Show[Quiet[WordCloud[DeleteStopwords@StringJoin[Riffle[Union[Flatten[Apply[List,(Normal/@WikipediaSearch[decodedWord]),4]]]," "]]]],ImageSize->{500,500}]}],
InputField[Dynamic[guessedWord],String],
Dynamic[Refresh[ToLowerCase@guessedWord===ToLowerCase@privateWord,UpdateInterval->1]]
}
]
]

guessingGame[]

Scheduling on the Arduino

The Sketch or program that the Arduino runs to interface with Mathematica has a functioning scheduler built into it, allowing the Arduino to schedule things independently of Mathematica.

The Arduino can schedule any arbitrary custom function uploaded to it, so let’s define a simple blink function that just inverts the value of the LED I have attached.

DeviceConfigure[blinkingArduino,"Upload"->"Functions"-><|"SingleBlink"->ArduinoCode[<|"Code"->"void blinkFunction() { pinMode(7,OUTPUT); digitalWrite(7,!digitalRead(7)); }"|>]|>]

To run it once, we just use DeviceExecute as normal

DeviceExecute[blinkingArduino,"SingleBlink"]

Running it again blinks the LED off.

DeviceExecute[blinkingArduino,"SingleBlink"]

With the Scheduling option, we can set it to run this function on a schedule. { dt , count } runs every dt seconds for a maximum of count times.

DeviceExecute[blinkingArduino,"SingleBlink","Scheduling"->{1,6}]

You will notice this returns immediately, and doesn’t wait for computation to finish.

Furthermore, while this is being schedule on the Arduino, in between running the code, the Arduino can still respond to reading values and writing values. Here, we will set it to blink indefinitely.

DeviceExecute[blinkingArduino,"SingleBlink","Scheduling"->1]

We can still run DeviceRead on the Arduino while it is running this though

DeviceRead[blinkingArduino,"A0"]

DeviceRead[blinkingArduino,"A5"]

DeviceConfigure[blinkingArduino,"PinConfigurations"-><|2->"Output",4->"Input"|>]

We can even use DeviceReadTimeSeries, but note that it will probably complain to us about some evaluations taking too long, because in between reading the states of the pins, it needs to run the scheduled function.

DeviceReadTimeSeries[blinkingArduino,{3,.03},"A0"]

DateListPlot[%]

When we are done with the Scheduled function, we can end it prematurely with DeviceExecute as well.

DeviceExecute[blinkingArduino,"DeleteTask","SingleBlink"]

Note that this does not interrupt the code that is run inside the function, this will simply prevent the function from being run again. This then allows other functions to be run or scheduled.

Some last implementation notes

The scheduling on the Arduino is only capable of millisecond precision at the moment, so the highest frequency you can schedule things is 1 kHz (actually a little less due to the calling overhead).

The scheduler can only handle one function at a time, so you cannot have more than one function scheduled at a time.

The specified time interval between functions is the time to wait after execution of the function has finished before running it again. For example, if you have a function that takes 5 seconds to run and you schedule it to be run every 1 second, it will wait for 1 second after each evaluation before entering the function again.

A current limitation is that these tasks must be sent to the Arduino by Mathematica while the Arduino is on. If the Arduino resets (via power loss or otherwise), then the scheduled function will no longer be scheduled, and the device will need to be reconnected to Mathematica to reschedule the task.

You can upload as many functions to the Arduino as you like, but note that the Arduino has limited program space, and that with the simple blink function we used above, the remaining amount of program space was about 12 kilobytes. Using lots of large libraries may make the sketch too large to upload, in which case the upload will fail.