We are Daniele Davoli, Shany Guetta, Marawan Hassaan and Andrea Littera students of the Master Course of Engineering in Computer Science at "La Sapienza" University of Rome, attending the Internet of Things course.
Our focus is to find a smart and connected way to operate the air conditioners in order to save energy, save the environment and, of course, save money.
The idea is to intelligently control the switching, the temperature and power of the air conditioner, from an application and from a remote location, receive telemetry data from the device in the home.
That's what we have done with Conditionly. You can find below the step by step documentation, the link to the Github repository for all the code that you need and some usefull links about the project.
Usefull links- Authors' LinkedIn profiles: Daniele Davoli, Shany Guetta, Marawan Hassaan, Andrea Littera
- Presentations on Slideshare: User Requirements and Minimum Value Product
- Repository on Github
NUCLEO-F401RE
The STM32 Nucleo board provides an affordable and flexible way for users to try out new ideas and build prototypes with any STM32 microcontroller line, choosing from the various combinations of performance, power consumption and features.
Check user manual here.
X-NUCLEO-IDW01M1
It is a Wi-Fi evaluation board expanding STM32 boards, The CE, IC and FCC certified SPWF01SA module has an embedded STM32 MCU, a low-power Wi-Fi b/g/n SoC with integrated power amplifier and power management and an SMD antenna. The SPWF01SA module is also equipped with1 M-Byte of external FLASH for firmware update over-the-air (FOTA). The firmware features a complete software IP stack to open to 8 TCP/UDP sockets, as well as dynamic web pages with SSI to interact with the module and a REST API (GET & POST) for conveniently transferring files to/from servers in the cloud. The module can simultaneously behave as a socket server and socket client. The firmware supports secure sockets with TLS/SSL encryption, ensuring secure end-to-end communications with the cloud, with or without authentication.
Check user manual here.
X-NUCLEO-IKS01A1
A motion MEMS and environmental sensor evaluation board system. It has inside LSM6DS0 which is a MEMS 3D accelerometer + 3D gyroscope. Also, it has LIS3MDL that is MEMS 3D magnetometer. Additionally, it uses LPS25HB that is a pressure sensor with absolute digital output barometer. Besides, it has HTS221 which is capacitive digital relative humidity and temperature.
Check user manual here.
IR Transmitter
IR LED emits light, in the range of Infrared frequency. IR light is invisible to us as its wavelength (700nm – 1mm) is much higher than the visible light range. IR LED shave light emitting angle of approx. 20-60 degree and range of approx. few centimeters to several feets, it depends upon the type of IR transmitter and the manufacturer. Some transmitters have the range in kilometers. IR LED white or transparent in colour, so it can give out amount of maximum light. It has working voltage of 5V and it is based on the 38KHz IR Transmitter Sensor.
Check IR datasheet.
Mini USB cable
It is cable to connect the board to the computer to allow for the extraction of the code on the internal memory of X-Nucleo-F401RE.
For the NUCLEO-F401RE, X-NUCLEO-IKS01A1 and X-NUCLEO-IDW01M1, each board connects as shield to the other, so they are connected to form 3 layers (as shown in figure).
After preparing the hardware configuration, IR LED is connected to the board, where the Vcc is connected to PA-5(D13) and the ground of the LED to the GND of the board.
Now, your components are ready for the next step.
Step 1 - Control the air conditioner from STM boardThe first important step to be carried out is to find a way to connect the STM board to the air conditioner, in order to be able to send commands and control it.
After an initial analysis of the market, we realized that most of the air conditioners currently on the market use infrared (IR) technology to send a signal.
Furthermore the most recent air conditioners that are sold, do not always have wifi or bluethoot connections, for this reason and for the previous ones we decided to focus on infrared based technology.
For our purpose, in order to perform tests, we have chosen to work with Daikin brand conditioners.
The first task was therefore to build a first instrument with an Arduino board and the IRremote library, that is the most spread to manage IR trasmitters and receivers, developed for the Arduino environment.
At the following link you can find the library and the basic examples, in our case we simply tried to decode the raw signal coming from the remote control of our air conditioner.
After the first test, we realized that the signal sent did not match any of the currently standardized formats on the market (SONY, NEC, RC6, ecc.), in fact every time a key is pressed on the remote control, that sends the entire status of the remote control in a undefined way, almost for now.
From that point on, we found an important analysis of this signal, very useful for our purpose, which you can find at this link (in italian).
Even if in our equipment we didn't have an oscilloscope to figure out the signal sent, we could guess it regarding the market standards. In a first analysis it's possible to see that different bits are sent by modulating impulses with a different time delay between them (PAM - Pulse Amplitude Modulation). At the end of the post it's possible to find a first draft that seeks to analyze the meaning of these bits.
The first sequence is composed of 5 bits
all set to 0
. The sequence A is composed of: 0x11, 0xDA, 0x27, 0x0, 0xC5, 0x10, 0x0, 0xE7
. The sequence B is instead composed of: 0x11, 0xDA, 0x27, 0x0, 0x42, 0x25, 0x14, 0x8D
.
The sequence C is the one that contains the data, it’s very specific and it's the base for the next operations.
After the first part of analysis we decided to concentrate our efforts to edit the existing IRremote library for Mbed Os, so we'll be able to send the array containing the commands.
Based on IRremote, the previous article, and further research, we then came to write the class ir_Daikin.cpp
which allow us to send the right signal extending IRremote.
First we have to define the message that must be sent; that's the reason we ported the existing code for Arduino: to create a new library for Mbed Os with an extremely simple interface.
That library takes care of creating the message and then sending it via IRremote.
The interface consists of the following functions:
void on();
void off();
void setDaikin();
void setPower(uint8_t state);
void setSwing_on();
void setSwing_off();
void setSwing(uint8_t state);
void setSwingLR_on();
void setSwingLR_off();
void setSwingLR(uint8_t state);
void setMode(uint8_t mode); // 0 FAN, 1 COOL, 2 DRY, 3 HEAT, 4 AUTO
void setFan(uint8_t speed); // 0 ~ 4 speed, 5 cars, 6 moon
void setTemp(uint8_t temp); // 23 ~ 33
void sendCommand();
unsigned char*getDaikin();
void description();
void sendFromJson(const char*json);
At that point we need to import the library in order to easly send a command in the correct format:
#include "ConditionlyDaikin/ConditionlyDaikin.h"
Create an istance:
ConditionlyDaikin irdaikin(LED1);
and, after setting the state, like the temperature or the swing, send the command:
irdaikin.setTemp(29);
irdaiki.setSwing_on();
irdaikin.sendCommand();
We also worked to have a version which handle JSON commands in the following way:
json = "{\"power\":true, \"mode\":4, \"fan\":5, \"swing\":true, \"swingLR\":true, \"temp\":19}";
irdaikin.sendFromJson(json);
Step 2 - Connect the board to AWS backendIn order to connect, the Nucleo board to the internet, we have two options. Either you use the ethernet cable or the Wi-Fi. In our case, we used Wi-Fi for its flexibility and cable management.
That is why X-NUCLEO-IDW01M1 is used to allow the Nucleo for contacting the server through the Wi-Fi module.
In order to configure the connection to the Wi-Fi and make it work properly, we needed to set up your credentials in the mbed_app.json
file. You check the config
field which is responsible of the configuration of the Wi-Fi connection.
All we needed to do is to replace the XXXXXXXXX
in "wifi-ssid"
field with the name of your Wi-Fi identifier name, also we needed to put the password in place of the other XXXXXXXXX
in the "wifi-password"
parameter.
Make sure that the encryption type of your Wi-Fi connection is set to WPA2 PSK
.
{
"config": {
"wifi-ssid": {
"parameter": "SSID",
"value": "\"XXXXXXXXXXXX\""
},
"wifi-password": {
"parameter": "Password",
"value": "\"XXXXXXXXXXXX\""
},
}
}
When the main code runs, it calls for the function init_wifi
. This function works as first by instantiating avariable name called wifi
that will be responsible for the interface with the Wi-Fi module.
Then, we needed to call the function connect
that takes as arguments the SSID, Password and encryption type and returns an integer that is equal to zero if the connection is successful.
int init_wifi()
{
wifi = WiFiInterface::get_default_instance();
if (!wifi) {
printf("ERROR: There is no WiFi connection. \n");
return -1;
}
printf("\n Now connecting to %s...\n", MBED_CONF_APP_WIFI_SSID);
int val = wifi->connect(MBED_CONF_APP_WIFI_SSID, MBED_CONF_APP_WIFI_PASSWORD, NSAPI_SECURITY_WPA2);
if (val !=0) {
printf("\n Error in connection %d\n", val);
return -1;
}
printf("Connection is successful\n\n");
return 1;
}
Now our board is connected to internet and can call for requests online.
Now, the board needed to get the commands from the used server. To do that, the board asks the server for an HTTP request (GET) to be able to receive what the user press on the remote application.
Consequently, the function callForServer
is called. It creates a network interface-based on the connection created in the previous step. Next, it makes the HTTP Request instance taking into constructor the network instance, the type of the request (in our case, it is GET) and two important parameters. The first one is Secure Sockets Layer Certification Authority (SSL CA)
, the second is the end point into your AWS gateway API. Below the code:
bool callForServer()
{
NetworkInterface *network = (NetworkInterface *)wifi;
HttpsRequest *req = new HttpsRequest(network, SSL_CA_PEM, HTTP_GET,"https://XXXXXXXXXXXXXXXXXX.execute-api.XXXXX-XXXX-X.amazonaws.com/beta/command");
HttpResponse *response;
response = req->send(NULL, 0);
if(response !=NULL ) {
printf("Received message:\n%s\n",response->get_body_as_string().c_str());
if(strcmp("null",response->get_body_as_string().c_str()) ==0) {
delete req;
return false;
} else {
delete req;
return true;
}
}
delete request;
return false;
}
Two things you have to do here
1. Here we must put your address for the end point generated at the AWS instead of "https://XXXXXXXXXXXXXXXXXX.execute-api.XXXXX-XXXX-X.amazonaws.com/beta/command"
in this function inside the main file. This end point is what the server uses to pass the commands to the board, so it has to provide the right formula of command. Below the code:
static const char SSL_CA_PEM[] = "-----BEGIN CERTIFICATE-----\n"
"MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF"
"ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6"
"b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL"
"MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv"
"b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj"
"ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM"
"9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw"
"IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6"
"VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L"
"93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm"
"jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC"
"AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA"
"A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI"
"U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs"
"N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv"
"o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU"
"5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy"
"rqXRfboQnoZsG4q5WTP468SQvvG5"
"-----END CERTIFICATE-----";
2. We needed to specify the SSL certificate of Amazon CA in the main file in the following position. “The one you see now in the code is the updated one till the moment (June, 2019), formany different reasons, it can change and in this case you need to update it in the code.”
To obtain the newcertificate:
- Open the end point of your application in your web browser;
- Click on the lock icon next to the URL;
- Select the Certificate tab;
- Go to Certificate Path;
- Choose the Upper certificate, the one for the Root Certificate Authority;
- Click on View Certificate;
- Select Details tab;
- Scroll down till you find the PublicKey;
- Copy RSA (2048) andconvert it into ASCII, you will obtain the needed certificate.
Now, you have a response from your server containing the string to be used to send updates to the board. You capture it the variable response.
This repose will be passed to the following step to be parsed and converted into actual commands to control the air conditioner.
Step 3 - AWS backendFirst of all we created a rule in AWS IoT in order to remain in a listening mode in the conditionly/message
topic: every time a message is received on that topic, the body of the message is inserted on a table of the AmazonDynamo DB.
Below we show the rule creation:
In the AWS IoT test console we can see the message received from the app when the user send a command throughtout the specific event function:
and, of course, we can see that a new row is correctly inserted in the Dynamo DB table:
Than we created a lambda function in order to retrieve the last command inserted in the table every time the Nucleo board will ask for it. Below we can see the lambda function code with the appropriate permissions to read the Dynamo DB in python:
import json
import boto3
from boto3.dynamodb.conditions import Key, Attr
from botocore.exceptions import ClientError
def lambda_handler(event, context):
#Accessing DB
dynamodb = boto3.client('dynamodb')
response = dynamodb.scan(TableName='conditionly')
data = response['Items']
maxid = 0
maxtemp = 0
print("Data:", data)
# Scan table to check for the latest command
for item in data:
currentid = item["id"]["N"]
currenttemp = item["temp"]["N"]
if(int(currentid) > maxid):
maxid = int(currentid)
maxtemp = currenttemp
db_response = dynamodb.get_item(TableName='conditionly', Key={'id':{'N': str(maxid)}, 'temp':{'N': str(maxtemp)}})
#Found an item corresponding to the id requested
item = db_response['Item']
temp = item["temp"]["N"]
fan = item["fan"]["N"]
mode = item["mode"]["N"]
power = item["power"]["BOOL"]
swingLR = item["swingLR"]["BOOL"]
swing = item["swing"]["BOOL"]
print("Found cmd:", temp)
#Preparing the response for Nucleo board
response = {
"power":power,
"mode":int(mode),
"fan":int(fan),
"swing":swing,
"swingLR":swingLR,
"temp":int(temp)
}
return response
Finally we created an API in order to trigger our lambda function every time the server receives a GET request from the board.
We have choosen the Google Flutter framework to deploy the mobile app whith the following features:
- Code once in a native way for every device (iOS and Android)
- Use the concise and easy-to-use Dart programming language
- Build beatiful and effective UI to improve the UX
First of all we created a new flutter project using Android Studio as the IDE (using Android Studio doesn't mean you can only build the app for the Android OS, you can easy import the Flutter project on a Mac and build the bin file for iOS).
Since there isn't the official AWS SDK for the Flutter projects with the aim to connect our app to the AWS IoT, we port the existing ASW SDK for Android Project to our Flutter project by rearranging the code form this Github repository.
Once we installed the dart:js library to insert JavaScript code in Dart files, we added the AWS SDK on the Android source files directory and we create a Flutter channel throughtout aws-iot-device-sdk.dart
in order to access to a native part of code in Kotlin, that we can see below:
package com.example.myapp
import com.amazonaws.services.iot.client.AWSIotException
import com.amazonaws.services.iot.client.AWSIotMqttClient
import com.amazonaws.services.iot.client.AWSIotQos
import com.amazonaws.services.iot.client.AWSIotMessage
import android.os.Bundle
import android.util.Log
import io.flutter.plugin.common.MethodChannel
import io.flutter.app.FlutterActivity
import io.flutter.plugins.GeneratedPluginRegistrant
import org.json.JSONObject
class MyMessage(topic: String, qos: AWSIotQos, payload: String) : AWSIotMessage(topic, qos, payload) {
override fun onSuccess() {
// called when message publishing succeeded
}
override fun onFailure() {
// called when message publishing failed
}
override fun onTimeout() {
// called when message publishing timed out
}
}
class MainActivity() : FlutterActivity() {
private val CHANNEL = "samples.flutter.io/iot"
private val TAG = "DEBUG"
private val topic = "conditionly/message"
private var id:Int = 0;
private val client: AWSIotMqttClient by lazy { connectToAws() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GeneratedPluginRegistrant.registerWith(this)
MethodChannel(flutterView, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "publishToDevice") {
/* val payload = call.argument<Map<String, String>>("command");*/
val rootObject= JSONObject()
rootObject.put("id",id as Int)
rootObject.put("power", call?.argument<Boolean>("power") as Boolean)
rootObject.put("mode", call?.argument<Int>("mode") as Int)
rootObject.put("fan", call?.argument<Int>("fan") as Int)
rootObject.put("swing", call?.argument<Boolean>("swing") as Boolean)
rootObject.put("swingLR", call?.argument<Boolean>("swingLR") as Boolean)
rootObject.put("temp", call?.argument<Int>("temp") as Int)
publish(rootObject!!)
result.success(true)
id++
} else {
result.notImplemented()
}
}
}
fun connectToAws(): AWSIotMqttClient {
Log.d(TAG, "connect")
val client = AWSIotMqttClient("a3knmvk82jtze4-ats.iot.us-east-2.amazonaws.com", "123", "AKIAIDSAT653LMNPZ4RQ", "Q2M8urfOPcbmTlSnje+aS3/fRiC1slapZC0EV+9d")
client.connect()
Log.d(TAG, client.getConnectionStatus().toString())
return client
}
fun publish(payload: JSONObject) {
val msg = MyMessage(topic, AWSIotQos.QOS0, payload.toString())
client.publish(msg, 3000)
}
}
At that point, it's easy to send to AWS IoT the message payload we want from every point of our app by invoking the publishToDevice
method.
Understand the what we have to send to send to AWS IoT it was a critical aspect. As we can see in the previous steps the right format of the command message is the JSON standard, below we can see the specifications of the command message (the values that each parameter can assume) and an example.
Command message example:
{
"power": true,
"mode": 4,
"fan": 5,
"swing": false,
"swingLR": false,
"temp": 27
}
Regarding the specifics, we inizialized the variables in the following way:
int _tmp = 22; //temperature
int _mode = 0; //mode
int _fan = 0; //fan
bool _power = false; //power
bool _swing = false; //swing
bool _swingLR = false; //swingLR
Leaving aside the temperature variable that we'll explain later, we implemented the minimal logic operations on those variables in an event-driven way directly into the UI code part, thanks to the power of Dart language. Below we can see a rappresentative example (exactly the same for the other variables):
new Row(children: <Widget>[ //Row UI element
Text("Select", style: TextStyle( //Text element with style
color: Colors.white,
fontSize: 18.0,
fontFamily: "WorkSansSemiBold"),
),
PopupMenuButton<int>( //Button element
icon: Icon(Icons.arrow_drop_down),
onSelected: (int result) //Event-driven function that implements
//our logic operation
{ setState(() { _fan = result; }); },
itemBuilder: (BuildContext context) => <PopupMenuEntry<int>>[
const PopupMenuItem( //Menu Item with value and label assigned
value: 0,
child: Text('FAN'),
),
const PopupMenuItem(
value: 1,
child: Text('COOL'),
),
const PopupMenuItem(
value: 2,
child: Text('DRY'),
),
const PopupMenuItem(
value: 3,
child: Text('HEAT'),
),
const PopupMenuItem(
value: 4,
child: Text('AUTO'),
),
],
)
For the temperature variable we easly implemented two simple functions in order to increment or decrement the _tmp
value regarding the limitations of the specifics:
// Temperature
void _incrementTmp() {
setState(() {
_tmp++;
if (_tmp > 32) {
_tmp = 32;
}
});
}
void _decrementTmp() {
setState(() {
_tmp--;
if (_tmp < 18) {
_tmp = 18;
}
});
}
Implemented the state changes of our air conditioner system, we could send the parameter values in the JSON format thanks to the following code, invoking the publishToDevice
method as we see before:
//Send data to AWS IoT
void _publishToCond() {
platform.invokeMethod('publishToDevice', <String, dynamic>{
'power': _power,
'mode': _mode,
'fan': _fan,
'swing': _swing,
'swingLR': _swingLR,
'temp': _tmp
});
_scaffoldKey.currentState.showSnackBar(snackBar);
}
Sure that the code works, we focused on UI styling of the app by coding the style attributes in the file theme.dart
. Below a clear example of the UI improvments since the first app version:
Comments