En aquest cas el microcontrolador funciona com a client i es comunica amb dues taules de Google. En una de les taules es van guardant periòdicament els valors reals de temperatura i humitat. En l'altra s'hi guarda la temperatura de consigna i el mode de funcionament triat. El programa del microcontrolador és el següent:
// PROJECTE I Termòstat IOT // El termòstat té 4 modalitats de funcionament: // I / i - estem a casa (IN): temp. cosigna val 21 °C // O / o - no estem a casa (OUT): temp. consigna val 15 °C // H / h - estem de vacances (HOLIDAY): temp. de consigna val 5 °C // 5<T<30 - podem escollir una temperatura T qualsevol sempre i quan estigui compresa entre 5 °C i 30°C // // la modalitat desitjada s'escull a través de google calendar, on existeix la possibilitat de crear una rutina // // la informació sobre com està funcionant la caldera es pot consultar a través de la aplicació que hem dissenyat // #include <Adafruit_NeoPixel.h> #include <SPI.h> // Carreguem la biblioteca SPI #include <WiFiNINA.h> // Carreguem la biblioteca WiFiNINA #include "DHT.h" #define DHTPIN 2 #define DHTTYPE DHT22 #define server_len 50 #define pag_len 400 #define mis_len 2 // Nombre màxim de línies de la resposta
//definim les variables que utilitzarem float consTemp = 15.0; int estat = 0; //variables connexió internet const char idXarxa[] = "xarxa-wifi"; // Nom del punt d'accés const char contrasenya[] = "contrasenya-wifi"; // Contrasenya de connexió //variables de connexió a servidors //google calendar char server[server_len]; char pagina[pag_len]; unsigned long darreraConnexio = 0; const unsigned long periodeConnexio = 10000UL; bool pendent, completa, redir; bool ara = false; bool ini_msg = false; int comp_lin = 0; String peticio = ""; // Aquí guardarem una línia de la petició del client String peticioAux = ""; // la petició anterior (també ho farem servir de reserva) String missatge[mis_len]; // Aquí hi guardarem el text rebut String server1, pagina1; int status = WL_IDLE_STATUS; String modalitat1; //ens indicarà quin mode de funcionament desitja el client (indicat al calendar) int modalitat; String horaInici1; //ens indicarà l'hora d'inici de la modalitat seleccionada float horaInici; String horaFinal1; //ens indicarà l'hora de finalització de la modalitat seleccionada float horaFinal; String horaActual1; //ens indica l'hora actual float horaActual; //Variables per escriure al full de cálcul (app) const char formulari[] = "1zF9RrmMr-u7375oNP-ipsACeXpnFr96WaAQvlmVgMBU"; const char adrePost[] = "/forms/u/1/d/e/1FAIpQLSfNU0DqezlDe5sZJ0LTfLQSvkylKfmrS2itDQlUA-1Q_Z1RtA/formResponse"; String camp1 = "entry.1004986425"; String camp2 = "entry.1676120579"; String camp3 = "entry.1320931023"; String camp4 = "entry.1218586113"; char serverApp[] = "docs.google.com"; float hum_App, temp_App; String data; String estat_App = "OFF"; //canvi: ful de càlcul(app) (false) / calendar(true) bool canvi = false; Adafruit_NeoPixel cadena = Adafruit_NeoPixel(3, 1, NEO_GRB + NEO_KHZ800); DHT dht(DHTPIN, DHTTYPE); WiFiSSLClient client;
//funció per tractar un missatge
void processa(String missat){
String Acte, Inici, Final, Actual; // Aquí guardarem les dades
Acte = missat.substring(0, missat.indexOf(","));
missat = missat.substring(missat.indexOf(",") +1);
Inici = missat.substring(0, missat.indexOf(","));
missat = missat.substring(missat.indexOf(",") +1);
Final = missat.substring(0, missat.indexOf(","));
Actual = missat.substring(missat.indexOf(",") +1);
Serial.print("Modalitat: ");
Serial.println(Acte);
Serial.print("Inici: ");
Serial.println(Inici);
Serial.print("Final: ");
Serial.println(Final);
Serial.print("Actual: ");
Serial.println(Actual);
}
void setup() {
cadena.begin(); // Inicialitza els NeoPixels
cadena.show();
dht.begin();
Serial.begin(9600);
cadena.setPixelColor(0, 10, 0, 0); // pixel vermell encès - calefacció aturada
cadena.show(); // Actualitza
estat = 0; // inicialitzem el sistema amb la calefacció en estat aturat
if (WiFi.status() == WL_NO_MODULE) {
Serial.println(F("No s'ha trobat el dispositiu Wi-Fi"));
// La funció F obliga al compilador a guardar el text a la memòria de programa
while (true); // Bloquegem el programa
}
String versio = WiFi.firmwareVersion();
if (versio < "1.0.0") {
Serial.println(F("Convindria actualitzar el firmware"));
}
while (status != WL_CONNECTED) {
Serial.print(F("Connectant a la xarxa "));
Serial.println(idXarxa);
status = WiFi.begin(idXarxa, contrasenya);
delay(10000); // Ho tornarem a intentar passats 10 s
}
Serial.print(F("Connectat a "));
Serial.println(WiFi.SSID());
Serial.print(F("Estat de la connexió: "));
Serial.println(WiFi.status());
Serial.print(F("Adreça IP del dispositiu: "));
Serial.println(WiFi.localIP());
Serial.print(F("Intensitat del senyal: "));
Serial.print(WiFi.RSSI());
Serial.println(F(" dBm"));
Serial.println();
Serial.println(F("Anem a connectar al servidor"));
Serial.println();
redir = false;
}
void loop() {
//part que escriu les dades que volem mostrar a l'usuari en un full de càlcul (app)
if (canvi==false){
canvi=true;
if (estat==1){
estat_App = "ON";
}
if (estat==0){
estat_App = "OFF";
}
hum_App = dht.readHumidity();
temp_App = dht.readTemperature();
if (status != WiFi.status()) { // Mirem si ha canviat l'estat de la connexió
status = WiFi.status();
if (status == WL_AP_CONNECTED) {
Serial.println("Dispositiu connectat al punt d'accés");
} else {
Serial.println("El dispositiu s'ha desconnectat del punt d'accés");
}
}
data = "";
data += camp2;
data += "=";
data += hum_App;
data += "&";
data += camp1;
data += "=";
data += temp_App;
data += "&";
data += camp3;
data += "=";
data += estat_App;
data += "&";
data += camp4;
data += "=";
data += consTemp;
data += "&submit=Submit";
if (client.connect(serverApp, 443)) {
Serial.println("Connectat");
client.print("POST ");
client.print(adrePost);
client.print("?formkey=");
client.print(formulari);
client.println("&ifq HTTP/1.1");
client.print("Host: ");
client.println(serverApp);
client.println("Content-Type: application/x-www-form-urlencoded");
client.println("Connection: close");
client.print("Content-Length: ");
client.println(data.length());
client.println();
client.print(data);
client.println();
Serial.print("Enviat T = ");
Serial.print(temp_App);
Serial.print(" H = ");
Serial.println(hum_App);
Serial.println(estat_App);
client.stop();
}
delay(1000);
if (!client.connected()) {
Serial.println();
Serial.println("Desconnectant");
client.stop();
}
}
//part que s'encarrega de llegir i interpretar la informació que l'usuari proporciona a través de Google Calendar
if (canvi==true){
float hum = dht.readHumidity();
float temp = dht.readTemperature();
//control de l'activació de la calefacció segons el valor de la temperatura de consigna
if (estat == 1 && temp > consTemp+1){
cadena.setPixelColor(0, 10, 0, 0); // pixel vermell encès - calefacció aturada
cadena.show(); // Actualitza
estat = 0;
} else if (estat == 0 && temp < consTemp-1){
cadena.setPixelColor(0, 0, 10, 0); // pixel verd encès - calefacció en marxa
cadena.show(); // Actualitza
estat = 1;
}
// El bucle principal té tres parts:
// 1. Gestió dels caràcters que arriben
// 2. Tractament de les dades rebudes
// 3. Nova petició quan ha passat el temps
while (client.available()) {
// Gestió dels caràcters que arriben
// Aquest bucle va guardant els caràcters rebuts
// i espera al moment en que arriba un salt de línia
char c = client.read(); // Rebem caràcters del servidor
if (c == '\n') { // Mirem si és un salt de línia
peticioAux = peticio; // Guardem la petició anterior
peticio = ""; // Ens preparem per a la línia següent
completa = true; // Preparat per tractar-ho
} else {
peticio += c; // Afegim el caràcter rebut
}
// Quan ha arribat un salt de línia, hem de mirar què ha arribat
if (completa){ // Ha arribat una línia completa
// El nostre script envia un salt de línia al final i, per tant,
// totes les dades les podrem anar agafant de peticioAux
// Si la darrera tasca no tingués salt de línia no ens arribaria
// aquí però estaria guardada a peticio
if ((ini_msg) && (comp_lin < mis_len) && (peticioAux.length() > 1)){
// Si la línia ja és la resposta la guardem
missatge[comp_lin++] = peticioAux;
}
if (peticioAux.startsWith(F("HTTP/1.1 200"))){ // Resposta bona
pendent = true;
redir = false;
}
if (peticioAux.startsWith(F("HTTP/1.1 302"))){ // Redireccionament
redir = true;
}
if (redir && (peticioAux.startsWith(F("Location:")))){
// Si hi ha redireccionament, hem de buscar l'adreça
// i extreure'n el servidor i la pàgina
String adre = peticioAux.substring(peticioAux.indexOf("//") +2);
server1 = adre.substring(0, adre.indexOf(".com") +4);
pagina1 = adre.substring(adre.indexOf(".com") +4);
server1.toCharArray(server, server_len);
pagina1.toCharArray(pagina, pag_len);
ara = true;
}
if (pendent && peticioAux.startsWith(F("Connection: close"))){
ini_msg = true;
}
completa = false;
}
}
// Hi ha una resposta per processar
if (pendent) {
pendent = false;
if (comp_lin > 0){
if (missatge[0].length() > 1){ // Si no està buit
//Escrivim també les dades captades pel sensor
Serial.print("Temperatura consigna: ");
Serial.print(consTemp);
Serial.print(" Estat: ");
Serial.println(estat);
Serial.print("Temperatura: ");
Serial.print(temp);
Serial.print(" °C");
Serial.print(" Humitat: ");
Serial.print(hum);
Serial.println(" %");
Serial.println(" ");
//Escrivim informació sobre la tasca actual, o la més propera si no en tenim cap d'actual
Serial.print(F("--- Tasca "));
Serial.print(0+1);
Serial.println(F(" ---"));
processa(missatge[0]);
Serial.println();
//variables que utilitzarem per imposar les condicions de canvi de consigna
modalitat1 = missatge[0].substring(0, missatge[0].indexOf(","));
//modalitat = modalitat1.toInt();
Serial.println(modalitat1);
missatge[0] = missatge[0].substring(missatge[0].indexOf(",") +1);
horaInici1 = missatge[0].substring(0, missatge[0].indexOf(","));
horaInici = horaInici1.toFloat();
missatge[0] = missatge[0].substring(missatge[0].indexOf(",") +1);
horaFinal1 = missatge[0].substring(0, missatge[0].indexOf(","));
horaFinal = horaFinal1.toFloat();
horaActual1 = missatge[0].substring(missatge[0].indexOf(",") +1);
horaActual = horaActual1.toFloat();
//condicions de canvi de consigna
if (modalitat1 == "I" || modalitat1 == "i"){
if (horaInici<=horaActual && horaActual<=horaFinal){
consTemp = 21;
}
}
if (modalitat1 == "O" || modalitat1 == "o"){
if (horaInici<=horaActual && horaActual<=horaFinal){
consTemp = 15;
}
}
if (modalitat1 == "H" || modalitat1 == "h"){
if (horaInici<=horaActual && horaActual<=horaFinal){
consTemp = 5;
}
}
modalitat = modalitat1.toInt();
if (modalitat>5 && modalitat<30 && horaInici<=horaActual && horaActual<=horaFinal){
consTemp = modalitat;
}
if (horaActual<horaInici) {
consTemp = 15;
}
Serial.print("Nova temperatura consigna: ");
Serial.println(consTemp);
Serial.println();
}
} else {
Serial.println(F("No queden tasques per al dia d'avui"));
Serial.println();
//Escrivim també les dades captades pel sensor
Serial.print("Temperatura consigna: ");
Serial.print(consTemp);
Serial.print(" Estat: ");
Serial.println(estat);
Serial.print("Temperatura: ");
Serial.print(temp);
Serial.print(" °C");
Serial.print(" Humitat: ");
Serial.print(hum);
Serial.println(" %");
Serial.println(" ");
consTemp = 15;
}
canvi=false; // ara que ja hem interpretat totes les dades, permetem que
// es faci el canvi a escriure les dades al full de cálcul
}
// Quan toca, tornem a fer una petició
if (ara || ((millis() - darreraConnexio > periodeConnexio))) {
ini_msg = false;
comp_lin = 0;
for (byte k = 0; k < mis_len; k++){
missatge[k] = "";
}
if (!redir){
server1 = "script.google.com";
pagina1 = "/macros/s/AKfycbx4lM1DGpnSH0yJ7G5Dgwn2x6nvSD8dyCRTnMEvt7eoAgKLRmY/exec";
server1.toCharArray(server, server_len);
pagina1.toCharArray(pagina, pag_len);
}
ara = false;
client.stop();
if (client.connect(server, 443)) {
Serial.println(F("S'ha fet la connexió al servidor"));
Serial.println();
client.print(F("GET "));
client.print(pagina);
client.println(F(" HTTP/1.1"));
client.print(F("Host: "));
client.println(server);
client.println(F("Connection: close"));
client.println();
// Guardem quan hem fet la connexió
darreraConnexio = millis();
} else {
Serial.println(F("connection failed"));
Serial.println();
}
}
}
}
També hi ha un script al Google Drive que permetia interactuar amb el calendari en el que es guarda la informació dels modes de funcionament de la caldera.
var idCal = "f2bjf3idp4ihdakjpkj9k5rghc@group.calendar.google.com";
function doGet(e) {
var salt = "\n";
var cal = CalendarApp.getCalendarById(idCal);
if (!cal) { // Si el calendari no existeix o no tenim permís
resultat = "Calendari no trobat!";
return ContentService.createTextOutput(resultat);
}
var ara = new Date(); // La data i l'hora del moment d'executar l'script
var final = new Date();
final.setHours(23); // Li canviem l'hora a les 23.59
final.setMinutes(59);
// Agafem tots els esdeveniments des d'ara (inclosos els ja iniciats) fins les 23.59 h
var esdev = cal.getEvents(ara, final);
var numEsdev = esdev.length; // Quants n'hi ha?
var resultat = "";
if (numEsdev > 0){ // Hi ha, com a mínim, un esdeveniment
resultat = resultat + dades(esdev[0]) + salt; // Primer esdeveniment
}
if (numEsdev > 1){ // Hi ha, com a mínim, un segon esdeveniment
resultat = resultat + salt + dades(esdev[1]) + salt; // Segon esdeveniment
}
return ContentService.createTextOutput(resultat); // Enviem la resposta
}
function dades(esdAct) { // Organitza les dades d'un esdeveniment
var descrip = esdAct.getTitle(); // Títol de l'esdeveniment
var dataIni = esdAct.getStartTime(); // Data i hora d'inici
var dataFi = esdAct.getEndTime(); // Data i hora d'acabament
// Ens interessen només les hores d'inici i acabament
// i les volem en el format habitual en català
var ini = dataIni.getHours() + "." + dataIni.getMinutes();
var fi = dataFi.getHours() + "." + dataFi.getMinutes();
var ara2 = new Date();
var Hora = ara2.getHours();
var Minut = ara2.getMinutes();
var actual = Hora + "," + Minut;
var resul = descrip + "," + ini + "," + fi + "," + actual;
return resul;
}
Aquest grup va fer una aplicació amb App Inventor que permetia interaccionar amb el termòstat. La disposició d'elements a la pantalla és com es mostra a les imatges següents:

El programa d'aquesta pantalla és el següent:




Aquesta obra d'Oriol Boix està llicenciada sota una llicència no importada Reconeixement-NoComercial-SenseObraDerivada 3.0.