ESP8266 kết nối Internet - Phần 3: Arduino gặp Smartphone, hai đứa nói chuyện bằng JSON thông qua Socket

Để hiểu và sử dụng được code trong bài này, các bạn vui lòng đọc 2 phần trước:

Ở bài trước, chúng ta đã biết được cách Arduino gửi dữ liệu và bị điều khiển bởi Socket Server. Hôm nay, chúng ta sẽ viết chương trình điều khiển trên Smartphone để điều khiển Arduino.

VIII. Làm thế nào để điện thoại kết nối được đến Socket Server

1. Lựa chọn ngôn ngữ lập trình cho thiết bị di động

Theo như mô hình mà chúng ta đã thống nhất với nhau từ các bài trước, thì ESP8266 nằm ở tầng 2 cùng tầng với điện thoại thông minh.

Nhưng khác với ESP8266, chúng ta không thể dùng C++ để lập trình cho Android (Java) hay IOS (Object-C hoặc Swift). Vì vậy, không thể chơi lầy áp dụng những code ở phần ESP8266 vào điện thoại được. 

Như vậy, nếu muốn viết một chương trình điều khiển Arduino từ điện thoại thông minh, thì theo cách thông thường, chúng ta phải học Java để lập trình Socket Client cho Android, học Object-C hoặc Swift để lập trình Socket Client trên iOS. Thật NẢN phải không nào? Nếu là mình thì mình sẽ không làm thế. Vì chẳng khác nào phải viết lại những dòng code đơn giản cho những việc đơn giản! 

Như vậy, chúng ta cần tìm một giải pháp chung cho vấn đề này. Biết rằng Socket Client có thể chạy trên trình duyệt (Sử dụng javascript giống với Socket Server). Cả 2 thiết bị iPhone hay Android đều có trình duyệt để mà chạy. Không những thế các thiết bị khác như máy tính, Windows Phone, Blackberry Phone,... cũng có thể sử dụng đươc vì nó đều có điện thoại. Vậy, chúng ta sẽ lập trình một trang web thân thiện với điện thoại để thực hiện điều này.

Đến đây, một số bạn nếu đã biết lập trình Android, iOS sẽ cảm thấy mình hơi ngu, vì bảo rằng, trình duyệt Web thì làm sao "smooth" (chạy mượt) như các chương trình native được. Mình cũng xin được trả lời, thay vì suy nghĩ việc một web app thì phải "ít mượt" hơn native app, thì bạn hãy thử trải nghiệm phần mềm iNút (iPhone Android) của mình. Đây là một chương trình dùng để điều khiển thiết bị điện trong nhà, do mình sử dụng công nghệ hybrid app, bản chất là web app! Hãy thử trên nhiều thiết bị và đánh giá hiệu năng nhé.

Việc lập trình web app cũng không hề dễ dàng, nhưng cũng không quá khó. Vì sao mình lại nói thế? Vì web app muốn chạy ổn định với một bộ giao diện đồ sộ như một trang web bình thường sẽ rất khó. Nhưng nếu bạn theo dõi công nghệ thường xuyên thì sẽ biết rằng hiện nay, với sự ra đời của HTML5 và CSS3 đã tạo điều kiện thuận lợi để các công nghệ thiết kế web mới ra đời: responsive, flat template. Vơi những công nghệ này, hiệu năng của web app sẽ tăng đáng kể vì không phải tốn thời gian render các hiệu ứng (CSS3 hỗ trợ hiệu ứng nhiều lắm luôn), không cần flash để chơi video hay nghe nhạc (HTML5 đã cân việc này),... và còn rất nhiều điều hay nữa.

Ngoài ra, khi đã rãnh với webapp, bạn sẽ dễ dàng lên được hybrid app. Bản chất hybrid app là một dạng webapp + native plugin. Nghĩa là bạn lập trình webapp thì sẽ bị hạn chế truy cập vào các thiết bị phần cứng của điện thoại như la bàn, gps, camera,... (bản chất webapp sẽ vẫn chơi được nhưng sẽ tùy duyên - tùy thiết bị). Nhưng khi bạn lên level hybrid app thì các plugin native sẽ giúp bạn đi sâu hơn mà bạn vẫn lập trình bình thường như việc bạn đang làm bên webapp. 

Nghe có vẻ tuyệt vời phải không nào? Vậy thì tại sao, bạn không thử một lần làm webapp trong dự án này cùng với mình nhỉ? Mình là một người đi trước trong phần hybrid app và cảm nhận được sức mạnh của nó. Và trước đó, cũng đã thử webapp trước khi đi lên hybrid app. Và lời khuyên dành cho bạn, đó là nếu bạn muốn đi nhanh thì hãy học webapp thật tốt vào, tư duy lập trình và khả năng lập trình trên thiết bị di động của bạn sẽ lên rất nhanh.

2. Cách lập trình webapp

Bản chất webapp không phải là một phần mềm native trên điện thoại của bạn. Nó thực chất là một trang web được thiết kế để phù hợp với thiết bị di động của bạn. Và bạn sẽ tương tác với app của bạn thông qua trình duyệt web trên thiết bị điện thoại

Vì webapp bản chất là một trang web, nên bạn cần một nơi lưu trữ nội dung trang web và các tài nguyên (resource) cần thiết. Có hai cách để lưu trữ một webapp:

  • Đóng gói thành file cài đặt native, trong đó sẽ có một webview dùng để truy cập đến webapp được lưu trữ trong file cài đặt này. Cái này, tiện ở chỗ, chương trình được lưu trên máy nên có thể sử dụng offline. Nhưng mình nghĩ trong dự án này đã nhắm đến internet nên thôi, để dành lên hybrid app làm luôn. Vì hybrid app nó dùng phương pháp này.
  • Lưu trữ trên một Server, người dùng dùng điện thoại thông minh để truy cập webapp từ trình duyệt web của họ. Chúng ta sẽ sử dụng phương pháp này, vì nó dễ cho mình heart khi hướng dẫn các bạn, và cũng dễ cho các bạn khi các bạn mới học heart. Đôi bên cùng vui vẻ làm việc cool.

Để lập trình một webapp, chúng ta cần biết được các từ khóa chính và phụ, để giúp cho các bạn khi bí có thể tìm tài liệu bên ngoài đọc thêm nhằm nâng cao kiến thức, ahihi.

Từ khóa chính

  • HTML5: Bản chất là HTML cộng thêm một số thẻ mới như thẻ audio, video,... để giúp bạn xây dựng cấu trúc trang web. Cấu trúc trang web giống như sườn của một tòa nhà. 
  • CSS3: Bản chất là CSS cộng thêm một số thuộc tính mới nhằm làm trang web đẹp hơn mà không sử dụng đến hình ảnh hoặc javascript như bo góc, hiệu ứng fadeout, fadein,... nhiều hiệu ứng lắm, nói vài cái cho anh em vui thôi.
  • JAVASCRIPT: Giống như ngôn ngữ mà chúng ta sử dụng trong socket server

Nhưng nói là nói vậy thôi, anh em mà bắt đầu nghiên cứu từ 3 từ khóa đó là chết ngay.

Từ khóa phụ

Để đi nhanh, chúng ta cần một chiếc xe Ferrari. Và những chiếc xe như thế được gọi là framework (thư viện). Và mình xin đề xuất một bộ thư viện cân hết cả 3 từ khóa chính trên. Đó là Mobile Angularjs. Bản chất thư viện này là sự kế thừa của:

  • AngularJS thư viện giúp chúng ta làm webapp theo mô hình MVC. Ở đây, mình đang nói đến AngularJS 1.x nhé, các bạn mà chơi AngularJS 2.x mà tẩu hòa nhập ma với bài của mình, thì mình không chịu trách nhiệm nha.
  • Mobile Template, cái này ý nói đến template của thư viện Mobile Angularjs. Nó sẽ giúp bạn dễ dàng thiết kế một app thân thiện với điện thoại. Xem demo tại đây.

Tản mạn xíu

Vậy, bạn sẽ đọc các mục dưới kia như thế nào cho hiệu quả?

  • Nếu bạn đã biết đến lập trình Web
    • Sẽ dễ dàng hơn cho bạn, vì bạn đã hiểu HTML5, CSS3 và Javascript nó hoạt động như thế nào rồi. Công việc của bạn chỉ là việc xem qua các tài liệu về framework mà chúng ta sẽ sử dụng. Trong đó, quan trọng nhất là AngularJS
  • Nếu bạn chưa biết gì về lập trình web
    • Thì thật khó khăn. Nếu bạn thích đi từ từ để hiểu hết mọi khía cạnh thì sẽ rất tốn thời gian, nhưng nó sẽ tốt hơn cho bạn, bạn hãy đọc 6, 7 chương đầu trong mục Getting started của Angularjs. Hoặc chỉ cần nắm được controller, service của Angularjs hoạt động như thế nào là được.
    • Còn nếu bạn thích đi nhanh, làm một phát được ngay, nhưng có thể sẽ có rất nhiều khoảng trống trong kiến thức. Nhưng được cái là có cái để cầm nắm ngay thì hãy đọc theo các phần mình viết ở dưới.
      • Mình sẽ giúp các bạn:
        • Điều khiển bật tắt đèn.
        • Đọc cảm biến (mưa cho nhanh).
        • Từ dưới Arduino nhấn nút thì gửi giá trị lên Server.
        • Ghi thông tin lên màn hình I2C LCD.
      • Nhiệm vụ của các bạn là vận dụng sự sáng tạo của bạn thân để sáng tạo và tôn trọng quyền tác giả của mình trong những đoạn code này.

Chúc các bạn, đọc bài thành công!

3. Khởi tạo project Webapp đầu tiên

Như đã nói ở trên, để webapp hoạt động, chúng ta cần một nơi để host (lưu trữ) web. Bạn có thể tự tạo một server (khác vớ socket server) để làm chỗ lưu trữ web cho project Webapp này. Còn theo mình, hãy tận dụng luôn project socket server hiện tại làm nơi host luôn cho nó tập trung.

Các bạn hãy clone socketServer8 của mình về nhé.

git clone https://github.com/ngohuynhngockhanh/socketServer8

Sau đó, bạn chạy lệnh 

npm install

để cài đặt project

Cấu trúc thư mục của socketServer8 như sau:

  • node_modules/ #thư mục chứa các module cần dùng
  • index.js #file thực thi tạo socket server
  • webapp/ #Thư mục chứa code của webapp
    • index.html #file html cài đặt CSS3, JAVASCRPT
    • webapp.js #file thực thi tạo socket client và cấu trúc 
    • home.html #giao diện chương trình điều khiển

Những file bôi đỏ là những file mà bạn sẽ quan tâm.

  • File index.js chính là file socket server như những bài trước.
  • File webapp.js chính là file webapp socket client, dùng ngôn ngữ javascript để cài đặt chương trình trên client. Mình sẽ nói rõ hơn trong ví dụ này.
  • File home.html chính là file giao diện của webapp socket client.

Nhiệm vụ của các file thì đã rõ, vậy vì sao chúng lại có nhiệm vụ như thế?

Hãy cùng nhắc lại mô hình. 

Theo mô hình này, và những gì chúng ta đã học được. Nhiệm vụ trong phần này chính là xây dựng module Webapp (chạy trên trình duyệt web) ở tầng 2. Module này nằm ở tầng 2, vì vậy để giao tiếp với Arduino. Nó phải đi qua Socket Server (tầng 1), rồi ESP8266 (tầng 2) và Arduino (tầng 3). Và ngược lại, để từ Arduino, giao tiếp tới Webapp, ta cần đi qua ESP8266 (tầng 2), rồi lên Socket Server (tầng 1) và cuối cùng là Webapp (tầng 2).

Mục đích ban đầu của chúng ta là từ Webapp (máy tính và điện thoại) điều khiển Arduino. Để đạt được mục đích này, chúng ta đã trải qua 2 phần và kết quả đạt được ở phần 2 đó là: Điều khiển Arduno từ Socket Server. Trong mô hình đó, chúng ta xem ESP8266 làm trung gian vận chuyển thông tin từ Socket Server. ESP8266 nhận được gì từ Socket Server (thông qua Socket) thì lại gửi xuống hết Arduino (Serial command). Arduino xử lý xong, lại gửi trả kết quả về cho ESP8266 (Serial command), ESP8266 nhận được tin từ Arduino thì đẩy toàn bộ đến Socket server (thông qua Socket). Như vậy, đó là một chu trình khép kín toàn phần. Còn chu kỳ khép kín bán phần là từ Arduino sau mỗi chu kỳ bao nhiêu mili giây gì đó thì gửi kết quả cảm biến hay giá trị nút nhấn về cho Socket Server.

Vậy, trong bài toán nêu ra ở vấn đề 1: Webapp điều khiển Arduino (trong mạng Wifi). Chúng ta đã giải được một nửa. Bây giờ, chúng ta sẽ giải bài toán biến Socket Server trở thành nơi trung gian trao đổi hàng hóa (dữ liệu) giữa ESP8266 và Trình duyệt Web. Vì khi giải bài toán này, kết hợp với bài toán đã giải ở phần 2 (Điều khiển Arduno từ Socket Server), chúng ta sẽ giải quyết được bài toán Webapp điều khiển Arduino (trong mạng Wifi). Vì, trong mô hình đó, ESP8266 và Socket Server sẽ trở thành nơi trung gian vận chuyển dữ liệu giữa WebappArduino.

Và hiển nhiên, bài toán biến Socket Server trở thành nơi trung gian trao đổi hàng hóa (dữ liệu) giữa ESP8266 và Trình duyệt Web cũng không có gì là khó khăn cả.

Để giải bài toán trên, chúng ta sẽ dùng khái niệm namespace của thư viện socket.io (cái thư viện mà chúng ta dùng để xây dựng phương thức giữa tầng 1 và tầng 2 đấy ahihi). Vậy namespace là gì?

Hãy tưởng tượng, ESP8266 là một người đi xin việc đến trung tâm giới thiệu việc làm (Socket Server) để xin việc. Anh ta được trung tâm xếp vào nhóm người xin việc. Còn anh Webapp là đại diện một công ty cần tuyển nhân viên, anh ta cũng đến trung tâm việc làm để đăng tin tuyển dụng. Như vậy, trung tâm (Socket Server) sẽ xếp anh ta vào nhóm người tuyển nhân viên. Và namespace chính là những gì được bôi đen. Nó được dùng để phân biệt giữa những nhóm sự vật (socket) có điểm chung cùng thuộc một loại nhiệm vụ nào đó do người lập trình tự đặt. Bản thân Websocket không có khái niệm namespace! Mà chính thư viện socket.io đề ra để người lập trình dễ dàng phân loại chúng (tương tự mô hình MQTT).

Cũng từ ví dụ trên, bản thân nó cũng đã nêu lên được bản chất của vấn đề chúng ta cần giải quyết. Chúng ta cần lập trình cho Socket server đứng giữa làm trung gian kết nối nhóm người xin việcnhóm người tuyển nhân viên

Gọi:

  • ESP8266 (nhóm người xin việc) thuộc namespace esp8266.
  • Webapp (nhóm người tuyển nhân viên) thuộc namespace webapp.

Định nghĩa công việc của socket server:

  • Khi webapp gửi (emit) dữ liệu (lệnh + JSON) đến socket server, thì socket server sẽ gửi toàn bộ dữ liệu (lệnh + JSON) đến esp8266.
  • Khi esp8266 gửi (emit) dữ liệu (lệnh + JSON) đến socket server, thì socket server sẽ gửi toàn bộ dữ liệu (lệnh + JSON) đến esp8266.

Ngoài ra, socket server còn làm nhiệm vụ host các file tĩnh của webapp (cái này không liên quan socket nhé), nên chúng ta sẽ làm một vài dòng code nhỏ để webapp chạy ngon lành, các bạn có thể không cần hiểu cũng được, vì từ đây, chúng ta sẽ không code gì trên socket server nữa, vì nó ổn định rồi!

Ok, cùng bắt đầu ví dụ 1 thôi nào!

4. Ví dụ 1: Bật tắt đèn LED từ webapp + đọc cảm biến mưa từ webapp

Trước khi đọc code, hiểu diễn giải, mời các bạn, xem video!

Code Socket Server

Và đây chính là code hoàn thiện của socket server.

const PORT = 3484;									//Đặt địa chỉ Port được mở ra để tạo ra chương trình mạng Socket Server

var http = require('http');
var express = require('express');							//#include thư viện express - dùng để tạo server http nhanh hơn thư viện http cũ
var socketio = require('socket.io')				//#include thư viện socketio

var ip = require('ip');
var app = express();									//#Khởi tạo một chương trình mạng (app)
var server = http.Server(app)

var io = socketio(server);								//#Phải khởi tạo io sau khi tạo app

var webapp_nsp = io.of('/webapp')				//namespace của webapp
var esp8266_nsp = io.of('/esp8266')				//namespace của esp8266

var middleware = require('socketio-wildcard')();		//Để có thể bắt toàn bộ lệnh!
esp8266_nsp.use(middleware);									//Khi esp8266 emit bất kỳ lệnh gì lên thì sẽ bị bắt
webapp_nsp.use(middleware);									//Khi webapp emit bất kỳ lệnh gì lên thì sẽ bị bắt

server.listen(PORT);										// Cho socket server (chương trình mạng) lắng nghe ở port 3484
console.log("Server nodejs chay tai dia chi: " + ip.address() + ":" + PORT)

//Cài đặt webapp các fie dữ liệu tĩnh
app.use(express.static("node_modules/mobile-angular-ui")) 			// Có thể truy cập các file trong node_modules/mobile-angular-ui từ xa
app.use(express.static("node_modules/angular")) 							// Có thể truy cập các file trong node_modules/angular từ xa
app.use(express.static("node_modules/angular-route")) 				// Có thể truy cập các file trong node_modules/angular-route từ xa
app.use(express.static("node_modules/socket.io-client")) 				// Có thể truy cập các file trong node_modules/socket.io-client từ xa
app.use(express.static("node_modules/angular-socket-io"))			// Có thể truy cập các file trong node_modules/angular-socket-io từ xa
app.use(express.static("webapp")) 													// Dùng để lưu trữ webapp


//giải nén chuỗi JSON thành các OBJECT
function ParseJson(jsondata) {
    try {
        return JSON.parse(jsondata);
    } catch (error) {
        return null;
    }
}


//Bắt các sự kiện khi esp8266 kết nối
esp8266_nsp.on('connection', function(socket) {
	console.log('esp8266 connected')
	
	socket.on('disconnect', function() {
		console.log("Disconnect socket esp8266")
	})
	
	//nhận được bất cứ lệnh nào
	socket.on("*", function(packet) {
		console.log("esp8266 rev and send to webapp packet: ", packet.data) //in ra để debug
		var eventName = packet.data[0]
		var eventJson = packet.data[1] || {} //nếu gửi thêm json thì lấy json từ lệnh gửi, không thì gửi chuỗi json rỗng, {}
		webapp_nsp.emit(eventName, eventJson) //gửi toàn bộ lệnh + json đến webapp
	})
})

//Bắt các sự kiện khi webapp kết nối

webapp_nsp.on('connection', function(socket) {
	
	console.log('webapp connected')
	
	//Khi webapp socket bị mất kết nối
	socket.on('disconnect', function() {
		console.log("Disconnect socket webapp")
	})
	
	socket.on('*', function(packet) {
		console.log("webapp rev and send to esp8266 packet: ", packet.data) //in ra để debug
		var eventName = packet.data[0]
		var eventJson = packet.data[1] || {} //nếu gửi thêm json thì lấy json từ lệnh gửi, không thì gửi chuỗi json rỗng, {}
		esp8266_nsp.emit(eventName, eventJson) //gửi toàn bộ lệnh + json đến esp8266
	});
})

Để cho nó hoạt động, các bạn đừng quên chạy lệnh dưới ở thư mục socketServer8 nhé.

node index.js

Code ESP8266

Của chúng ta cũng đã hoàn thiện và sẽ tái sử dụng lại code đó. Chúng ta chỉ việc thêm biến char namespace_esp8266[] vào và đặt tên nó là esp8266 nhé. Các bạn cung nhớ đổi IP của socket server, ssid và psk mạng wifi nha. Nếu không biết cách lấy IP của socket server thì xem ở bài viết này

#include <SoftwareSerial.h>
#include <ESP8266WiFi.h>
#include <SocketIOClient.h>
#include <SerialCommand.h>  

//include thư viện để kiểm tra free RAM trên con esp8266
extern "C" {
  #include "user_interface.h"
}


const byte RX = D1;
const byte TX = D2;

SoftwareSerial mySerial(RX, TX, false, 256); 
SerialCommand sCmd(mySerial); // Khai báo biến sử dụng thư viện Serial Command

SocketIOClient client;
const char* ssid = "MACHTUDONG";          //Tên mạng Wifi mà Socket server của bạn đang kết nối
const char* password = "smarthome12345";  //Pass mạng wifi ahihi, anh em rãnh thì share pass cho mình với.

char host[] = "192.168.200.46";  //Địa chỉ IP dịch vụ, hãy thay đổi nó theo địa chỉ IP Socket server của bạn.
int port = 3484;                  //Cổng dịch vụ socket server do chúng ta tạo!
char namespace_esp8266[] = "esp8266";   //Thêm Arduino!

//từ khóa extern: dùng để #include các biến toàn cục ở một số thư viện khác. Trong thư viện SocketIOClient có hai biến toàn cục
// mà chúng ta cần quan tâm đó là
// RID: Tên hàm (tên sự kiện
// Rfull: Danh sách biến (được đóng gói lại là chuối JSON)
extern String RID;
extern String Rfull;


void setup()
{
    //Bật baudrate ở mức 57600 để giao tiếp với máy tính qua Serial
    Serial.begin(57600);
    mySerial.begin(57600); //Bật software serial để giao tiếp với Arduino, nhớ để baudrate trùng với software serial trên mạch arduino
    delay(10);

    //Việc đầu tiên cần làm là kết nối vào mạng Wifi
    Serial.print("Ket noi vao mang ");
    Serial.println(ssid);

    //Kết nối vào mạng Wifi
    WiFi.begin(ssid, password);

    //Chờ đến khi đã được kết nối
    while (WiFi.status() != WL_CONNECTED) { //Thoát ra khỏi vòng 
        delay(500);
        Serial.print('.');
    }

    Serial.println();
    Serial.println(F("Da ket noi WiFi"));
    Serial.println(F("Di chi IP cua ESP8266 (Socket Client ESP8266): "));
    Serial.println(WiFi.localIP());

    if (!client.connect(host, port, namespace_esp8266)) {
        Serial.println(F("Ket noi den socket server that bai!"));
        return;
    }

    sCmd.addDefaultHandler(defaultCommand); //Lệnh nào đi qua nó cũng bắt hết, rồi chuyển xuống hàm defaultCommand!
    Serial.println("Da san sang nhan lenh");
    
}

void loop()
{

    //Khi bắt được bất kỳ sự kiện nào thì chúng ta có hai tham số:
    //  +RID: Tên sự kiện
    //  +RFull: Danh sách tham số được nén thành chuỗi JSON!
    if (client.monitor()) {

        //in ra serial cho Arduino
        mySerial.print(RID);
        mySerial.print('\r');
        mySerial.print(Rfull);
        mySerial.print('\r');

        //in ra serial monitor
        Serial.print(RID);
        Serial.print(' ');
        Serial.println(Rfull);
        
        //Kiểm tra xem còn dư bao nhiêu RAM, để debug
        uint32_t free = system_get_free_heap_size();
        Serial.println(free);
    }

    //Kết nối lại!
    if (!client.connected()) {
      client.reconnect(host, port, namespace_esp8266);
    }

    sCmd.readSerial();
}

void defaultCommand(String command) {
  char *json = sCmd.next();
  client.send(command, (String) json);//gửi dữ liệu về cho Socket Server

  //In ra serial monitor để debug
  Serial.print(command);
  Serial.print(' ');
  Serial.println(json);
}

Code Arduino

Chúng ta sẽ tái sử dụng Ví dụ 3: Ví dụ 2 + Đọc cảm biến mưa 

Bạn hãy nối mạch như sau nhé:

Linh kiện cần chuẩn bị:

Bạn lắp mạch này thêm vào phần mạch ở trên nhé. Nếu bạn không có cảm biến mưa thì cũng không sao cả, kệ nó, giá trị sẽ được tự tạo random. Sẽ thú vị hơn nhiều khi không có module cảm biến mưa!

#include <ArduinoJson.h>
#include <SoftwareSerial.h>
#include <SerialCommand.h>  // Thêm vào sketch thư viện Serial Command
const byte RX = 3;          // Chân 3 được dùng làm chân RX
const byte TX = 2;          // Chân 2 được dùng làm chân TX
 
SoftwareSerial mySerial = SoftwareSerial(RX, TX); 
 
 
SerialCommand sCmd(mySerial); // Khai báo biến sử dụng thư viện Serial Command
 
int red = 4, blue = 5; // led đỏ đối vô digital 4, led xanh đối vô digital 5
int rainSensor = 6; // Chân tín hiệu cảm biến mưa ở chân digital 6 (arduino)
 
const unsigned long CHU_KY_1_LA_BAO_NHIEU = 5000UL; //Cứ sau 5000ms = 5s thì chu kỳ lặp lại
 
void setup() {
  //Khởi tạo Serial ở baudrate 57600 để debug ở serial monitor
  Serial.begin(57600);
 
  //Khởi tạo Serial ở baudrate 57600 cho cổng Serial thứ hai, dùng cho việc kết nối với ESP8266
  mySerial.begin(57600);
  
  //pinMode 2 đèn LED là OUTPUT
  pinMode(red,OUTPUT);
  pinMode(blue,OUTPUT);
 
  pinMode(rainSensor,INPUT);// Đặt chân cảm biến mưa là INPUT, vì tín hiệu sẽ được truyền đến cho Arduino
  
  
  // Một số hàm trong thư viện Serial Command
  sCmd.addCommand("LED",   led); //Khi có lệnh LED thì sẽ thực thi hàm led  
  sCmd.addCommand("RAIN",  rain_detect);//Khi có lệnh RAIN thì sẽ thực thi hàm rain để kiểm tra trị cảm biến mưa
  Serial.println("Da san sang nhan lenh");
}
 
 
unsigned long chuky1 = 0;
void loop() {
  //Khởi tạo một chu kỳ lệnh, chu kỳ là 5000ms
  if (millis() - chuky1 > CHU_KY_1_LA_BAO_NHIEU) {
    chuky1 = millis();
    rain_detect();
  }
  sCmd.readSerial();
  //Bạn không cần phải thêm bất kỳ dòng code nào trong hàm loop này cả
}
 
// hàm led_red sẽ được thực thi khi gửi hàm LED_RED
void led() {
  Serial.println("LED");
  char *json = sCmd.next(); //Chỉ cần một dòng này để đọc tham số nhận đươc
  Serial.println(json);
  StaticJsonBuffer<200> jsonBuffer; //tạo Buffer json có khả năng chứa tối đa 200 ký tự
  JsonObject& root = jsonBuffer.parseObject(json);//đặt một biến root mang kiểu json
 
  int redStatus = root["led"][0];//json -> tham số root --> phần tử thứ 0. Đừng lo lắng nếu bạn không có phần tử này, không có bị lỗi đâu!
  int blueStatus = root["led"][1];//json -> tham số root --> phần tử thứ 0. Đừng lo lắng nếu bạn không có phần tử này, không có bị lỗi đâu!
 
  //kiểm thử giá trị
  Serial.print(F("redStatus "));
  Serial.println(redStatus);
  Serial.print(F("blueStatus "));
  Serial.println(blueStatus);
 
  StaticJsonBuffer<200> jsonBuffer2;
  JsonObject& root2 = jsonBuffer2.createObject();
  root2["redStatus"] = redStatus;
  root2["blueStatus"] = blueStatus;
 
  //Tạo một mảng trong JSON
  JsonArray& data = root2.createNestedArray("data");
  data.add(redStatus); 
  data.add(blueStatus);
  
  
  //in ra cổng software serial để ESP8266 nhận
  mySerial.print("LED_STATUS");   //gửi tên lệnh
  mySerial.print('\r');           // gửi \r
  root2.printTo(mySerial);        //gửi chuỗi JSON
  mySerial.print('\r');           // gửi \r
 
  //in ra Serial để debug
  root2.printTo(Serial); //Xuống dòng
  
  //xuất ra màn hình
  digitalWrite(red, redStatus);
  digitalWrite(blue, blueStatus);
}

void rain_detect() {
  StaticJsonBuffer<200> jsonBuffer;
  JsonObject& root = jsonBuffer.createObject();
  //đọc giá trị cảm biến rồi in ra root
  root["digital"] = digitalRead(rainSensor);
  root["message"] = digitalRead(rainSensor) ? "Khong mua" : "Co mua ne ahihi";


  //Gửi đi hoy!
  //in ra cổng software serial để ESP8266 nhận
  mySerial.print("RAIN");   //gửi tên lệnh
  mySerial.print('\r');           // gửi \r
  root.printTo(mySerial);        //gửi chuỗi JSON
  mySerial.print('\r'); 
}

Nhiệm vụ của đoạn code trên cũng không khác mấy so vớ ví dụ 3 của phần 2.

  • Lắng nghe 2 lệnh:
    • LED: điều khiển 2 đèn LED
    • RAIN: yêu cầu đọc cảm biến mưa
  • Tạo một chu kỳ 5 giây một lần gửi dữ liệu lên Socket server.

Code Webapp (Javascript)

Nội dung của file webapp.js, các bạn hãy mở lên và xem, hoặc xem thông qua:

angular.module('myApp', [
    'ngRoute',
    'mobile-angular-ui',
	'btford.socket-io'
]).config(function($routeProvider) {
    $routeProvider.when('/', {
        templateUrl: 'home.html',
        controller: 'Home'
    });
}).factory('mySocket', function (socketFactory) {
	var myIoSocket = io.connect('/webapp');	//Tên namespace webapp

	mySocket = socketFactory({
		ioSocket: myIoSocket
	});
	return mySocket;
	
/////////////////////// Những dòng code ở trên phần này là phần cài đặt, các bạn hãy đọc thêm về angularjs để hiểu, cái này không nhảy cóc được nha!
}).controller('Home', function($scope, mySocket) {
	////Khu 1 -- Khu cài đặt tham số 
    //cài đặt một số tham số test chơi
	//dùng để đặt các giá trị mặc định
    $scope.CamBienMua = "Không biết nữa ahihi, chưa thấy có thằng nào cập nhập hết";
    $scope.leds_status = [1, 1]
	
	////Khu 2 -- Cài đặt các sự kiện khi tương tác với người dùng
	//các sự kiện ng-click, nhấn nút
	$scope.updateSensor  = function() {
		mySocket.emit("RAIN")
	}
	
	$scope.changeLED = function() {
		console.log("send LED ", $scope.leds_status)
		
		var json = {
			"led": $scope.leds_status
		}
		mySocket.emit("LED", json)
	}
	
	////Khu 3 -- Nhận dữ liệu từ Arduno gửi lên (thông qua ESP8266 rồi socket server truyền tải!)
	//các sự kiện từ Arduino gửi lên (thông qua esp8266, thông qua server)
	mySocket.on('RAIN', function(json) {
		$scope.CamBienMua = (json.digital == 1) ? "Không mưa" : "Có mưa rồi yeah ahihi"
	})
	//Khi nhận được lệnh LED_STATUS
	mySocket.on('LED_STATUS', function(json) {
		//Nhận được thì in ra thôi hihi.
		console.log("recv LED", json)
		$scope.leds_status = json.data
	})
	
	
	//// Khu 4 -- Những dòng code sẽ được thực thi khi kết nối với Arduino (thông qua socket server)
	mySocket.on('connect', function() {
		console.log("connected")
		mySocket.emit("RAIN") //Cập nhập trạng thái mưa
	})
		
});

Trừ cái phần cài đặt ra, các bạn không cần quan tâm, chỉ quan tâm đến 4 khu mà mình nói thôi nha.

  1. Khu 1 -- Khu cài đặt tham số 
    • Các tham số được đặt tên theo dạng: $scope.<tên biến> = <giá trị>
    • Ví dụ:
      • $scope.CamBienMua = "Không biết nữa ahihi, chưa thấy có thằng nào cập nhập hết";
      • $scope.leds_status = [1, 1]
    • Trong javascript dấu ; không quan trọng!
  2. Khu 2 -- Cài đặt các sự kiện khi tương tác với người dùng
    • Các hàm được đặt tên theo dạng: $scope.<tên hàm> = function(<danh sách tham số>) { <cài đặt> }
    • Các sự kiện khi tương tác với người dùng được cài đặt bên trong file webapp.js được gọi khi thêm vào các tham số bên trong file home.html
    • Ví dụ: hàm changeLED sẽ được gọi khi <input type="checkbox"> bị thay đổi.
    • Hàm updateSensor() sẽ được gọi khi thẻ <button> được click vào!
  3. Khu 3 -- Nhận dữ liệu từ Arduno gửi lên (thông qua ESP8266 rồi socket server truyền tải!)
    • Cái này giống với những kỳ mình đã nòi ở socket server. Chỉ khác là tên biến được thay đổi thành mySocket!
  4. Khu 4 -- Những dòng code sẽ được thực thi khi kết nối với Arduino (thông qua socket server)
    • Tương tự như void setup() của Arduino!

Code Webapp (HTML)

Cái này rất khó giải thích, nhưng nói chung, bạn cứ mở file home.html lên và thử chỉnh sửa file này và đọc tiếp các phần sau là sẽ hiểu ngay. Hãy vận dụng quy tắc copy paste, bạn nhé.

<div class="scrollable">
	<div class="scrollable-content">
		<div class="list-group text-center">
			<!-- cục giới thiệu -->
			<div class="list-group-item list-group-item-home">
				<h1>Socket Client Webapp <small>1.0</small>
				</h1>
			</div>
			
			<!-- Cục cảm biến mưa -->
			<div class="list-group-item list-group-item-home">
				<div>
					<i class="fa fa-gamepad feature-icon text-primary"></i>
				</div>
				<div>
					<h3 class="home-heading">Cảm biến mưa</h3>
					Kết quả: {{CamBienMua}}
				</div>
			</div>
			
			<!-- Cục thay đổi trạng thái LED -->
			<div class="list-group-item list-group-item-home">
				<div>
					<i class="fa fa-tachometer feature-icon  text-primary"></i>
				</div>
				<div>
					<h3 class="home-heading">TRẠNG THÁI ĐÈN LED</h3>
					<div ng-repeat="led_value in leds_status  track by $index">
						Đèn LED thứ {{$index}} đang {{led_value ? "Bật" : "Tắt"}}. <input type="checkbox" ng-true-value="1" ng-false-value="0" ng-change="changeLED()" ng-model="leds_status[$index]" />
					</div>
				</div>
			</div>
			
			<!-- Cục cập nhập giá trị cảm biến -->
			<div class="list-group-item list-group-item-home">
				<div>
					<i class="fa fa-tachometer feature-icon  text-primary"></i>
				</div>
				<div>
					<h3 class="home-heading">Cập nhập cảm biến</h3>
					<button class="button" ng-click="updateSensor()">
						Nhấn vô dể cập nhập giá trị cảm biến
					</div>
				</div>
			</div>
			
			
		</div>
	</div>
</div>

5. Ví dụ 2: Ví dụ 1 + Đọc giá trị từ nút nhấn + điều khiển servo + ghi ra màn hình LCD thông qua mô hình LCD

Anh em có đồ gì thì gắn đồ đó vô, không có thì thôi, không sao cả, ahihi

Anh em nối mạch như trên rồi thêm một số món đồ như sau nữa là được. Trên tinh thần, có món nào, nối món đó, đúng chân cẳng như hình, không có thì thôi bỏ qua, ahihi.

Nối mạch servo

Nối mạch i2c lcd

Anh em bỏ con DHT11 ra nhé!

Nối mạch nút nhấn

Code Socket server và code ESP8266

Không thay đổi so với ví dụ 1.

Code Arduino cập nhập mới

#include <ArduinoJson.h>
#include <Servo.h>
#include <LiquidCrystal_I2C.h>
#include <SoftwareSerial.h>
#include <SerialCommand.h>  // Thêm vào sketch thư viện Serial Command
const byte RX = 3;          // Chân 3 được dùng làm chân RX
const byte TX = 2;          // Chân 2 được dùng làm chân TX
 
SoftwareSerial mySerial = SoftwareSerial(RX, TX); 
 
 
SerialCommand sCmd(mySerial); // Khai báo biến sử dụng thư viện Serial Command
 
byte red = 4, blue = 5; // led đỏ đối vô digital 4, led xanh đối vô digital 5
byte rainSensor = 6; // Chân tín hiệu cảm biến mưa ở chân digital 6 (arduino)
byte buttons[] = {7, 8}; // chơi với mảng cho nó đã!
byte servoPin = 9; //Chân servo

Servo servo;

LiquidCrystal_I2C lcd(0x27,16,2); //Khai báo LCD
 
const unsigned long CHU_KY_1_LA_BAO_NHIEU = 5000UL; //Cứ sau 5000ms = 5s thì chu kỳ lặp lại
 
void setup() {
  //Khởi tạo Serial ở baudrate 57600 để debug ở serial monitor
  Serial.begin(57600);
 
  //Khởi tạo Serial ở baudrate 57600 cho cổng Serial thứ hai, dùng cho việc kết nối với ESP8266
  mySerial.begin(57600);
  
  //pinMode 2 đèn LED là OUTPUT
  pinMode(red,OUTPUT);
  pinMode(blue,OUTPUT);
 
  pinMode(rainSensor,INPUT);// Đặt chân cảm biến mưa là INPUT, vì tín hiệu sẽ được truyền đến cho Arduino

  servo.attach(servoPin); //cài đặt chân servo

  //Cài đặt LCD
  lcd.init();  
  //Bật đèn màn hình LCD
  lcd.backlight();
  //in ra màn hình chào mừng
  lcd.print("Xin chao");

  //Cài đặt mấy cái nút nhấn theo INPUT PULLUP
  for (int i = 0; i < sizeof(buttons); i++) {
    pinMode(buttons[i], INPUT_PULLUP);
  }
  
  // Một số hàm trong thư viện Serial Command
  sCmd.addCommand("LED",   led); //Khi có lệnh LED thì sẽ thực thi hàm led  
  sCmd.addCommand("RAIN",  rain_detect);//Khi có lệnh RAIN thì sẽ thực thi hàm rain để kiểm tra trị cảm biến mưa
  
  sCmd.addCommand("LCD_PRINT",   lcd_print);
  sCmd.addCommand("SERVO", servo_enjoy);
  Serial.println("Da san sang nhan lenh");
}
 
 
unsigned long chuky1 = 0;
byte oldButtonsValue[] = {0, 0};
void loop() {

  //Khởi tạo một chu kỳ lệnh, chu kỳ là 5000ms
  if (millis() - chuky1 > CHU_KY_1_LA_BAO_NHIEU) {
    chuky1 = millis();
    rain_detect();
  }

  bool willSendButtonInformation = false; //Đặt cờ để kiểm tra có gửi thông tin có trạng thái thay đổi nút nhấn hay không?
  for (int i = 0; i < sizeof(buttons); i++) {
    byte value = digitalRead(buttons[i]); //đọc trị nút nhấn
    if (value != oldButtonsValue[i])      //kiểm tra trị nút nhấn với giá trị trước đó!
      willSendButtonInformation = true;
    oldButtonsValue[i] = value;
  }
  if (willSendButtonInformation) {
    sendButtonInformation();
  }
  
  sCmd.readSerial();
  //Bạn không cần phải thêm bất kỳ dòng code nào trong hàm loop này cả
}

void sendButtonInformation() {
  StaticJsonBuffer<100> jsonBuffer;
  JsonObject& root = jsonBuffer.createObject();
  JsonArray& data = root.createNestedArray("data");
  for (int i = 0; i < sizeof(oldButtonsValue); i++) {
    data.add(oldButtonsValue[i]);
  }

  //in ra cổng software serial để ESP8266 nhận
  mySerial.print("BUTTON");   //gửi tên lệnh
  mySerial.print('\r');           // gửi \r
  root.printTo(mySerial);        //gửi chuỗi JSON
  mySerial.print('\r');           // gửi \r
}
 
// hàm led_red sẽ được thực thi khi gửi hàm LED_RED
void led() {
  Serial.println("LED");
  char *json = sCmd.next(); //Chỉ cần một dòng này để đọc tham số nhận đươc
  Serial.println(json);
  StaticJsonBuffer<200> jsonBuffer; //tạo Buffer json có khả năng chứa tối đa 200 ký tự
  JsonObject& root = jsonBuffer.parseObject(json);//đặt một biến root mang kiểu json
 
  int redStatus = root["led"][0];//json -> tham số root --> phần tử thứ 0. Đừng lo lắng nếu bạn không có phần tử này, không có bị lỗi đâu!
  int blueStatus = root["led"][1];//json -> tham số root --> phần tử thứ 0. Đừng lo lắng nếu bạn không có phần tử này, không có bị lỗi đâu!
 
  //kiểm thử giá trị
  Serial.print(F("redStatus "));
  Serial.println(redStatus);
  Serial.print(F("blueStatus "));
  Serial.println(blueStatus);
 
  StaticJsonBuffer<200> jsonBuffer2;
  JsonObject& root2 = jsonBuffer2.createObject();
  root2["redStatus"] = redStatus;
  root2["blueStatus"] = blueStatus;
 
  //Tạo một mảng trong JSON
  JsonArray& data = root2.createNestedArray("data");
  data.add(redStatus); 
  data.add(blueStatus);
  
  
  //in ra cổng software serial để ESP8266 nhận
  mySerial.print("LED_STATUS");   //gửi tên lệnh
  mySerial.print('\r');           // gửi \r
  root2.printTo(mySerial);        //gửi chuỗi JSON
  mySerial.print('\r');           // gửi \r
 
  //in ra Serial để debug
  root2.printTo(Serial); //Xuống dòng
  
  //xuất ra màn hình
  digitalWrite(red, redStatus);
  digitalWrite(blue, blueStatus);
}

void rain_detect() {
  StaticJsonBuffer<200> jsonBuffer;
  JsonObject& root = jsonBuffer.createObject();
  //đọc giá trị cảm biến rồi in ra root
  root["digital"] = digitalRead(rainSensor);
  root["message"] = digitalRead(rainSensor) ? "Khong mua" : "Co mua ne ahihi";


  //Gửi đi hoy!
  //in ra cổng software serial để ESP8266 nhận
  mySerial.print("RAIN");   //gửi tên lệnh
  mySerial.print('\r');           // gửi \r
  root.printTo(mySerial);        //gửi chuỗi JSON
  mySerial.print('\r'); 
}

void lcd_print() {
  Serial.println("LCD_PRINT");
  char *json = sCmd.next(); //Chỉ cần một dòng này để đọc tham số nhận đươc
  Serial.println(json);
  StaticJsonBuffer<100> jsonBuffer; //tạo Buffer json có khả năng chứa tối đa 100 ký tự
  JsonObject& root = jsonBuffer.parseObject(json);//đặt một biến root mang kiểu json
 
  char *line1 = root["line"][0];
  char *line2 = root["line"][1];
 
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(line1);
  lcd.setCursor(0, 1);
  lcd.print(line2);
  Serial.println("ok aihi");
}

void servo_enjoy() {
  Serial.println("SERVO");
  char *json = sCmd.next(); //Chỉ cần một dòng này để đọc tham số nhận đươc
  Serial.println(json);
  StaticJsonBuffer<50> jsonBuffer; //tạo Buffer json có khả năng chứa tối đa 100 ký tự
  JsonObject& root = jsonBuffer.parseObject(json);//đặt một biến root mang kiểu json
 
  int degree = root["degree"];
  const char *message = root["message"];
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(message);
  servo.write(degree);
  Serial.println(degree);
}

Trong code này, mình có thêm 2 sự kiện là LCD_PRINT, SERVO, cấu trúc của hai lệnh này giống hệt như phần 2. Các bạn có thể xem lại phần 2 để rõ nhé.

Ngoài ra ở trong hàm loop(), mình có thêm sự kiện kiểm tra việc nhấn nút nhấn. Các bạn xem code là ra ngay!

Code Socket server và Webapp

Các bạn hãy clone project của mình về nhé

git clone https://github.com/ngohuynhngockhanh/socketServer9

Và đừng quên

npm install

...để cài đặt nhé!

Khởi chạy socket server và host webapp bằng cách chạy lệnh này nhé!

node index.js

Hãy mở file webapp.jshome.html trong thư mục webapp để xem sự thay đổi nhé. Còn với file index.js thì nội dung không hề thay đổi so với ví dụ 1.

6. Tóm lại

Qua cả 3 phần của loạt bài này, chúng ta đã có thể điều khiển thiết bị Arduino từ xa thông qua webapp trên điện thoại trong mạng Wifi (LAN). Việc này rất dễ và không gặp nhiều trở ngại, các công nghệ và thư viện cũng đã đi trước, mình chỉnh sửa lại cho phù hợp với chuẩn IoT.

Qua 3 phần, chúng ta đã làm được nhiều điều vui. Vậy, hãy cùng chờ đợi phần 4 với khả năng điều khiển Arduino từ ngoài Internet nhé.

lên
20 thành viên đã đánh giá bài viết này hữu ích.
Chuyên mục: 
Các dự án được truyền cảm hứng

Bộ điều khiển PID - ứng dụng phần 2 - xe dò line dùng thuật toán PID

Tiép nối bài viết về xe dò line cảm ơn Đỗ Hữu Toàn đã viết hộ mình phần 4. hôm nay mình sẽ làm cho chiếc xe dò line đi mượt và có hồn hơn 

lên
34 thành viên đã đánh giá bài viết này hữu ích.
Các bài viết cùng tác giả

Lấy từng bit trong một thanh ghi của iNut PLC - Lấy từng bit trong một luồng cảm biến của iNut cảm biến

Làm thế nào để lấy từng bit của một thanh ghi? Bạn lưu trạng thái của INPUT/OUTPUT hoặc cảm biến digital vào một bit của thanh ghi và bạn muốn đọc nó để thiết kế giao diện? Đừng lo lắng. Giải pháp cực kỳ đơn giản, nó chỉ là một khối lệnh kéo thả thôi mà! Nào, cùng khám phá!

lên
7 thành viên đã đánh giá bài viết này hữu ích.

CCA - Kết quả Cuộc thi Sáng tạo với Arduino.vn

Như vậy là đã kết thúc mùa CCA đầu tiên, hẳn trong chúng ta đều mong chờ kết quả của Cuộc thi Sáng tạo với Arduino.vn để biết được rằng thế hệ trẻ đàn em có nuôi dưỡng ước mơ tự động hóa như chúng ta đã và đang làm hay không? Vừa qua, BGK gồm các bạn trong BQT và Kiểm tra viên đã chấm online các nhóm dự án thông qua skype. Và bây giờ, trong bài viết này, chúng ta sẽ biết được ngay kết quả.

lên
15 thành viên đã đánh giá bài viết này hữu ích.