Connecting microcontrollers to the cloud using Raspberry Pi and I2C
I have been building with microcontrollers since the 8 bit Motorola HC11 EVB I used for Computer Engineering. As I complete various projects, I always considered how to get that data in the cloud. After learning about IoT and the AWS platform, I was able to configure a Raspberry Pi to work as an IoT device, and I came up with the idea to create a cloud based Digital Signal Processor (DSP), using the RPi as my Arduino's "IoT Device Gateway". I used the Arduino compatible Teensy 3.6 as my audio processor, and it is connected to the RPi using I2C. The Teensy devices are powerful microcontrollers, and have special features designed for audio projects. The audio function for this project will be to generate a test tone that I will use to measure the impedance of a loudspeaker.
I have tinkered around with the different components of the project individually, and my main task was to integrate them all together. The only new technology for me was using the I2C protocol between two chips. As I mentioned, I had a RPi configured with Amazon's IoT platform, so I was able to re-purpose most of that work. I then set up the I2C communication between the RPi and Teensy. Once all the hardware communication was stable (also known as "make sure to remember pull up resistors"), I began writing code to perform audio processing, as well as to process requests from the IoT platform. Finally, I wrote a web application using the React framework that would control my IoT device. Below is a wiring model for the device.
As you can see, there is not much wiring to do (which is one of the points of I2C). There are also many options available for how to send audio output from Teensy to a loudspeaker. I have chosen a very simple model here, but have experimented with using the Sparkfun THAT 1646 outsmarts breakout, which provides a nice balanced output. I use that with PWM Audio Amplifiers.
Connect the RPi to AWS IoT Platform
Getting the RPi to communicate with the AWS platform is fairly straightforward once you log into the console. When I first learned how to do this, there were eleven steps that needed to be run on the RPi to register and create the certificates and policies necessary to connect a RPi to the IoT Platform. Since then, it has become easier to do this work through the web interface. The interface walks you through the process, and creates all the necessary certificates using a browser on your RPi. There is a tutorial in the links section ("Connecting you Raspberry Pi") that demonstrates how to connect your RPi to the AWS IoT platform using the web console. The instructions assume you are working through the RPi's GUI, but, I prefer to have mine "headless", and connect as a network drive to transfer files and develop, and have a ssh terminal open to the device to run my programs. If you are still interested in performing these tasks through the CLI, they are referenced at the bottom of this article.
Install the SDK
Once all of the steps above are taken, you can download the appropriate SDK from AWS. For a RPi, you can choose between either the Embedded C, Python or JavaScript SDK. I chose the JavaScript SDK, and followed the "Using the AWS IoT Device SDK for JavaScript" link below that guides you through the process of setting up your run-time and making sure you can communicate with the AWS IoT platform.
The JavaScript code necessary to connect my RPi to the AWS is straightforward. Each of the items in the configuration objects are the outputs when creating a Thing. I register the callback "onConnect" to the Thing Shadow, which will fire after the device connects to the platform. I then register the device, and pass in a few variables: presistentSubscribe set to true so that the device is always listening for MQTT topics, and ignoreDeltas to make sure that the device listens for all changes.
var awsIot = require('aws-iot-device-sdk');
var awsConfiguration = {
poolId: 'YourCognitoIdentityPoolId',
host: 'YourAWSIoTEndpoint', e.g. 'prefix.iot.us-east-1.amazonaws.com',
region: 'YourAwsRegion', e.g. 'us-east-1',
clientid: '<your arn>',
thingname: '<your thing>',
clientToken: '<your token>'
};
var thingShadows = awsIot.thingShadow({
keyPath: 'credentials/private.key',
certPath: 'credentials/servicecertificate.pem',
caPath: 'credentials/root-ca.pem',
clientId: awsConfiguration.clientid,
host: awsConfiguration.host
});
thingShadows.on('connect',onConnect );
function onConnect() {
thingShadows.register( awsConfiguration.thingname, {
persistentSubscribe: true,
ignoreDeltas: false
}, onRegister);
}
function onRegister() {
if(debugLevel >= 1){
console.log("On Register")
}
}
thingShadows.subscribe('topic/audio/frequency');
I2C between a RPi and Arduino
It's a pretty straightforward command line process to set up I2C on the RPi. The first time I tested I2C, I used my Sparkfun MPU-9250 Sensor, which uses the I2C protocol to communicate readings from the 3 Axis that it measures. I also followed a lesson on Adafruit that shows how to configure I2C on a raspberry pi. The wiring is simple, two wires for power, two wires for I2C. The RPi uses pin 3 for SDA and pin 5 for SCL. I also used pin 1 for 3.3V power and pin 6 for Ground.
using the nifty i2cdetect command line tool:
sudo apt-get install -y i2c-tools
i2cdetect -y 1
you should see the address #68 for the 9250:
Connecting up to the Teensy requires moving the wires from the 9250 to the Teensy, which uses pin 19 for SCL, and ping 18 for SDA. Although the RPi has pull up resistors, the Teensy does not, and I added the recommended 4.7K resistors to the 3.3V line. Since I will be debugging through the Arduino serial monitor interface, I don't have to worry about powering the Arduino from the RPi.
Arduino Implementation
The first step was to write a small enough app that, when downloaded, would at least respond with the address of the device when I use the i2cdetect utility.
The Arduino code below is what I used to complete the test. It has a simple to test I2C on the Teensy. It is listening for both receive and request events from the RPi. It listens for commands using the wire library. In this case, setting the frequency of the sine wave being generated. I used the Teensy Audio System Design Tool to help me figure out the Teensy audio functions. The interface is drag and drop, it has great information on the API, and it also generates code for you!
#include <Wire.h>
#include <ADC.h>
#include <Audio.h>
AudioSynthWaveformSine sine1;
AudioOutputAnalog dac1;
AudioConnection patchCord1(sine1, dac1);
//Amplitude
const int readPin2 = A14; // ADC1
// Function prototypes
void receiveEvent(int count);
// some constants
#define SLAVE_ADDRESS 0x08 //slave address, any number from 0x01 to 0x7F
#define REG_MAP_SIZE 14
#define MAX_SENT_BYTES 2
byte registerMap[REG_MAP_SIZE];
byte receivedCommands[MAX_SENT_BYTES];
char char_variable;
int databuf[] = {};
int i = 0;
ADC *adc = new ADC(); // adc object;
int freq = 800;
int impedanceReading;
float sine_amplitude;
void setup() {
AudioMemory(12);
//this sets the output voltage to 3.3 V
dac1.analogReference(EXTERNAL);
sine1.frequency(freq);
Serial.begin(115200);
Serial.println("on");
Wire.begin(SLAVE_ADDRESS); // join i2c bus with address #8
Wire.onReceive(receiveEvent);
}
/*********
*the receiveEvent corresponds to the I2C writeWord function on the Raspberry Pi
*receivedCommands[0] = COMMAND (CMD_SET_FREQUENCY)
*receivedCommands[1] = High Byte of Frequency
*receivedCommands[2] = Low Byte of Frequency
*/
void receiveEvent(int bytesReceived)
{
Serial.print("receiveEvent bytes Received: ");
Serial.println(bytesReceived);
//
for (int a = 0; a < bytesReceived; a++){
if ( a < MAX_SENT_BYTES){
receivedCommands[a] = Wire.receive();
} else {
Wire.receive();
}
Serial.println(receivedCommands[a]);
}
switch(receivedCommands[0]){
case 173: //#AD (CMD_SET_FREQUENCY)
freq = BitShiftCombine(receivedCommands[2],receivedCommands[1]);
Serial.println(freq);
sine1.frequency(freq);
break;
}
}
//helper function to combine two bytes into an int
int BitShiftCombine( unsigned char x_high, unsigned char x_low){
int combined;
combined = x_high; //send x_high to rightmost 8 bits
combined = combined<<8; //shift x_high over to leftmost 8 bits
combined |= x_low; //logical OR keeps x_high intact in combined and fills in rightmost 8 bits
return combined;
}
// if you want your code to stop working, put a bunch of stuff in the loop function.
// especially a delay.
void loop() {
}
Raspberry Pi Implementation
The RPi code requires the use of a module called i2c-bus. This handles the i2c communication much like the wire library does for Arduino. This code starts up and registers itself with the IoT platform, subscribes to topics, and processes the events by packaging the data from the topic into an I2C request, and sending that to the Teensy to process, as shown above. When the frequency is set, the RPi responds to the ThingShadow and updates itself with the new reported value.
var awsIot = require('aws-iot-device-sdk');
var i2c = require('i2c-bus');
var debugLevel = 1;
var awsConfiguration = {
poolId: 'region:YourCognitoIdentityPoolId', // 'YourCognitoIdentityPoolId'
host: 'host.iot.region.amazonaws.com', // 'YourAWSIoTEndpoint', e.g. 'prefix.iot.us-east-1.amazonaws.com'
region: 'region', // 'YourAwsRegion', e.g. 'us-east-1'
clientid: 'arn:aws:iot:region:clientid:thing/pi-Thing',
thingname: 'pi-Thing',
clientToken: 'pi-Thing-device'
};
//initiate the I2C protocol
i2c1 = i2c.openSync(1);
//create a set of constants that will act as I2C Command Bytes and Addresses
var SB_ADDR = 0x08,
CMD_ACCESS_CONFIG = 0xac, //172
CMD_SET_FREQUENCY = 0xad, //173
CMD_READ_IMPEDANCE = 0xaa, //170
CMD_START_IMPEDANCE_READ = 0xee; //238
//create a thing shadow
var thingShadows = awsIot.thingShadow({
keyPath: 'credentials/private.key',
certPath: 'credentials/servicecertificate.pem',
caPath: 'credentials/root-ca.pem',
clientId: awsConfiguration.clientid,
host: awsConfiguration.host
});
// Client token value returned from thingShadows.update() operation
var clientTokenUpdate;
//thingshadow reported JSON
var speakerBrain = {"state": {"reported": { "audio": {"frequency":400,"amplitude":1.0}}},"clientToken":awsConfiguration.clientToken};
var params = { thingName: awsConfiguration.thingname /* required */ };
//subscribe to Topics related to the ThingShadow, and attach a function to them
thingShadows.on('connect',onConnect );
function onConnect() {
thingShadows.register( awsConfiguration.thingname, {
persistentSubscribe: true,
ignoreDeltas: false
}, onRegister);
}
function onRegister() {
if(debugLevel >= 1){
console.log("On Register")
}
}
function onAccepted(err, granted,payload) {
if(debugLevel >= 1){
console.log('on Accepted '); //JSON.stringify(granted)
}
}
thingShadows.subscribe('/shadow/update/accepted',onAccepted);
//use a custom topic to transmit frequency changes
thingShadows.subscribe('topic/audio/frequency');
function setVariables (err, data) {
if (err && debugLevel >= 2) {
console.log(err, err.stack); // an error occurred
} else if(debugLevel >= 1){
console.log("set variables"); // successful response
}
}
thingShadows.on('message', function(topic, payload) {
if(debugLevel >= 1){
console.log('message', topic);
if (topic === 'topic/audio/frequency') {
speakerBrain.state.reported.audio.frequency = parseInt(JSON.parse(payload).frequency);
i2c1.writeWord(SB_ADDR, CMD_SET_FREQUENCY,parseInt(speakerBrain.state.reported.audio.frequency),setFrequency);
}
}
});
thingShadows.on('timeout',
function(thingName, clientToken) {
if(debugLevel >= 1){
console.log('received timeout on '+thingName+ ' with token: '+ clientToken);
}
});
//these functions are for frequency
function setFrequency(err){
// set frequency: it either worked or it didn'tif (err) {if(debugLevel >= 1){console.log(err);
}
} else {
if(debugLevel >= 1){
console.log('set frequency success');
}
updateThingShadow();
}
}
function updateThingShadow(){
delete speakerBrain.version;
delete speakerBrain.timestamp;
clientTokenUpdate = thingShadows.update(awsConfiguration.thingname,speakerBrain);
if (clientTokenUpdate === null){
if(debugLevel >= 1){
console.log('Change Impedance: update shadow failed, operation still in progress');
}
}
else {
if(debugLevel >= 1){
console.log("Client Token: " + clientTokenUpdate);
if(clientTokenUpdate != awsConfiguration.clientToken){
console.log("mismatched client update token!");
}
}
}
}
Changing the Frequency
At this point, I was able to start testing the system without having to hook up the DAC to an amplifier, and then a speaker. Instead, I wired my oscilloscope to measure the frequency from the Teensy's DAC pin. To test the interactions with the Thing, use the test interface included in AWS: after logging into the web console, navigate to the "test" section in the IoT platform, towards the bottom is a section where you can publish to a topic. This is the fastest, most reliable way to test the system.
You can also log into your thing shadow, choose the "monitor" section, and view the modifications to it in real time:
Web Application Implementation
It's easy to include the AWS SDK into react, and I threw together a simple app "hello world" app that would change the frequency. I am also working on a feature to read the impedance of a loudspeaker, and thought I'd try out some better graphics than just a number. I created three components in React: A "Frequency" component to control the frequency used by the micro controller, an "Impedance" component to view the loudspeaker impedance returned by the micro controller, and an "IoT" component that encapsulates and controls all communication with the IoT Platform, and has a small viewer to show the current state.
The most interesting aspect is how to set up the communication with the IoT platform using a Web Application: we still need to create a secure connection to the IoT platform, however, we cannot store the keys and certificates because of the security compromise in doing so. Just for fun, I wrote the code connecting to the platform using both a device object and a thing shadow object available in the SDK. The thing shadow implementation is much more involved, and requires more care in how updates are performed, the biggest issue being revision errors. The device SDK allows me to easily build MQTT topics that can be subscribed to by both the web application and the device itself. This particular feature is being used by many developers writing serverless applications who wish to implement real time chat, or other pub/sub models, even though they may not technically be IoT applications.
In regards to Cognito security, I have two functions in my IoT class: getIoTIdentity and updateIoTCredentials. The function getIoTIdentity is used to retrieve the identity and credentials associated with a Cognito Pool Id. The function updateIoTCredentials applies the access key, secret key and session tokens required for communication for both the device and Thing Shadow.
import React from 'react';
import '../../index.css';
//include the necessary SDKs
var AWS = require('aws-sdk');
var AWSIoTData = require('aws-iot-device-sdk');
//I create a file to store configuration details
var AWSConfiguration = require('../../configuration.js');
AWS.config.region = AWSConfiguration.region;
//get the public credentials allowing access to device
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: AWSConfiguration.poolId
});
class IoT extends React.Component {
constructor(props) {
super(props);
this.getIoTIdentity = this.getIoTIdentity.bind(this);
this.updateIoTCredentials = this.updateIoTCredentials.bind(this);
this.onConnect = this.onConnect.bind(this);
this.onRegistered = this.onRegistered.bind(this);
this.onStatus = this.onStatus.bind(this);
this.onMessage = this.onMessage.bind(this);
this.registered = false;
this.params = {
thingName: AWSConfiguration.thingname /* required */
};
this.iotdata = new AWS.IotData({endpoint: AWSConfiguration.host});
this.thingShadows = AWSIoTData.thingShadow({
region: AWSConfiguration.region,
clientId: AWSConfiguration.clientid,
host: AWSConfiguration.host,
protocol: 'wss',
maximumReconnectTimeMs: 8000,
debug: false,
accessKeyId: '',
secretKey: '',
sessionToken: ''
});
this.device = AWSIoTData.device({
region: AWSConfiguration.region,
host: AWSConfiguration.host,
protocol: 'wss',
debug: false,
accessKeyId: '',
secretKey: '',
sessionToken: ''
});
//Create and Update Cognito Identity
this.cognitoIdentity = new AWS.CognitoIdentity();
AWS.config.credentials.get(this.getIoTIdentity);
this.device.subscribe('$aws/things/' + AWSConfiguration.thingname + '/shadow/update/accepted');
this.device.subscribe('topic/loudspeaker/impedance');
this.device.on('message', this.onMessage);
this.thingShadows.on('connect', this.onConnect);
this.state = {
"connected": false,
"registered": false,
"frequency": 0
};
} // end constructor
componentWillReceiveProps(nextProps) {
if (nextProps.frequency !== this.props.frequency) {
console.log("sending data")
speakerBrain = {
"state": {
"desired": {
"audio": {
"frequency": nextProps.frequency,
"amplitude": 1.0
}
}
},
"clientToken":AWSConfiguration.clientToken
};
this.device.publish('topic/audio/frequency','{"frequency":'+nextProps.frequency+'}');
}
}
//gets the Credentials.
getIoTIdentity(err, data) {
if (!err) {
if (debugLevel >= 1) {
console.log('retrieved identity: ' + AWS.config.credentials.identityId);
}
var params = {
IdentityId: AWS.config.credentials.identityId
};
this.cognitoIdentity.getCredentialsForIdentity(params, this.updateIoTCredentials);
} else {
if (debugLevel >= 1) {
console.log('error retrieving identity:' + err);
}
}
}
updateIoTCredentials(err, data) {
if (!err) {
//// Update our latest AWS credentials; the MQTT client will use these// during its next reconnect attempt.//this.device.updateWebSocketCredentials(data.Credentials.AccessKeyId,
data.Credentials.SecretKey,
data.Credentials.SessionToken);
this.thingShadows.updateWebSocketCredentials(data.Credentials.AccessKeyId,
data.Credentials.SecretKey,
data.Credentials.SessionToken);
} else {
if (debugLevel >= 1) {
console.log('error retrieving credentials: ' + err);
}
}
}
onMessage(topic, payload) {
console.log('got message', topic, payload.toString());
if (topic === 'topic/loudspeaker/impedance') {
this.props.onIoTChange(payload.toString());
}
}
onConnect() {
console.log("connected");
this.setState({
"connected": true
});
this.thingShadows.subscribe('/shadow/update/accepted', this.onAccepted);
this.thingShadows.subscribe('/shadow/update/delta', this.onDelta);
this.thingShadows.subscribe('topic/loudspeaker/impedance', this.handleImpedance);
if (!this.state.registered) {
this.thingShadows.register(AWSConfiguration.thingname, {}, this.onRegistered);
}
console.log("subscribed");
}
onRegistered() {
this.setState({
"registered": true
});
}
onStatus(thingName, stat, clientToken, stateObject) {
console.log('received ' + stat + ' on ' + thingName + ', using : ' + clientToken);
switch (stat) {
case 'accepted':
if (stateObject.state.reported !== undefined) {
console.log(JSON.stringify(stateObject.state.reported.audio));
}
break;
default:
break;
}
}
render() {
return (
<div className = "IoTdata" >
Device: {
AWSConfiguration.thingname
} < br / >
Connected: {
this.state.connected ? 'yes' : 'no'
} < br / >
Registered: {
this.state.registered ? 'yes' : 'no'
} < br / ></div>
);
}
}
The front end is far less interesting looking than what is going on behind the scenes!
What's the point?
It is getting easier to connect ANYTHING to the cloud using an IoT platform as the bi-directional communication platform. While the main purpose may still be to attach sensors to the cloud for real time data visualization, machine learning, and data analytics, the SDKs available make it possible to integrate with more than just traditional sensors. Because of this, the uses of the IoT platform are growing far beyond industrial applications, and into more serverless, web, and mobile applications.
At concept3D, we use the AWS IoT Platform as a core component of our 3D data visualization toolset, allowing us to stream in real time sensor data from the field and onto our mapping platform, using our maps, renderings, photospheres and tours to create rich user experiences in remote data visualizations. Whether using thing shadows for two way communication, or the Message Broker for more custom Pub/Sub models, it allows us a scalable solution for streaming the thousands of sensors being visualized on our platform.
Personally, I love experimenting with microcontrollers, and the sensors that connect up to them. My interests have been around using the Arduino Pro Mini's for my LED light shows, Lilypad's for my wearable LED tuxedo, and Teensy's for my digital audio projects. There is usually a "social" aspect to these projects, and I often think about connecting them to the cloud, but most of the controllers don't have a way to connect, and I wouldn't want to waste the resources doing that. However, combining the resources of the RPi with the specialized tasks of a microcontroller, gives me the best of both worlds and offers far greater processing options for me. For this project, I was interested in using the cloud to have loudspeakers capable of designing and building themselves. I am continuing to add sensors so that I can create impedance curves and measure the other Thiele/Small parameters, so that, one day, a 3D printer will build the perfect cabinet and crossovers for the loudspeakers it was given.
Helpful Links
- Connecting Your Raspberry Pi
- Using the AWS IoT Device SDK for JavaScript
- Configuring I2C on Raspberry Pi
- Combine 2 bits into int on an Arduino
- AWS SDK for Javascript
Command Line
Step 1: aws iot create-thing --thing-name pi-Thing
{
"thingArn": "arn:aws:iot:us-west-2:311186652569:thing/pi-Thing",
"thingName": "pi-Thing",
"thingId": "8c0d511c-fb0c-4dac-bd0d-29fbcb22028a"
}
Step 2: aws iot create-keys-and-certificate --set-as-active --certificate-pem-outfile servicecertificate.pem --public-key-outfile pi-thing-public.key --private-key-outfile pi-thing-private.key
**rename pi-thing to whatever you want
Sample Output:
{
"certificateArn": "arn:aws:iot:<ARN>",
"certificatePem": "<CERTIFICATE>",
"keyPair": {
"PublicKey": "<PUBLIC KEY>",
"PrivateKey": "<RSA PRIVATE KEY>"
},
"certificateId": "<CERTIFICATE ID>"
}
Step 3: aws iot create-policy --policy-name pi-thing-Policy --policy-document '{"Version": "2012-10-17", "Statement": [ { "Effect": "Allow","Action": "iot:*", "Resource": "*" } ]}'
Sample Output:
{
"policyName": "pi-thing-Policy",
"policyArn": "arn:aws:iot:<ARN>
9:policy/pi-thing-Policy",
"policyDocument": "{\"Version\": \"2012-10-17\", \"Statement\": [ { \"Effect\": \"Allow\",\"Action\": \"iot:*\", \"Resource\": \"*\" } ]}",
"policyVersionId": "1"
}
** you should be able to log into AWS console and see your certificates and policies
** Steps to retrieve the data you just created if you did not save it:
Step 4: aws iot list-things (to see your thing)
Step 5: aws iot list-certificates
Step 6: aws iot list-policies
Step 7: wget -O root-ca.pem https://www.symantec.com/content/en/us/enterprise/verisign/roots/VeriSign-Class%203-Public-Primary-Certification-Authority-G5.pem
Step 8: aws iot attach-principal-policy --policy-name pi-thing-Policy --principal arn:aws:iot:<ARN FROM ABOVE>
Step 9: aws iot attach-thing-principal --thing-name pi-Thing --principal arn:aws:iot:<CERT FROM ABOVE>
Validate:
Step 10: aws iot list-thing-principals --thing-name pi-Thing
Step 11: aws iot list-principal-policies –principal arn:aws:iot:<ARN FROM ABOVE>