Tạo thiết bị IoT hỗ trợ web nhờ Intel Edison

Kenneth Christiansen
Kenneth Christiansen

Ngày nay, Internet của vạn vật có mặt trên môi trường của mọi người và nó khiến những người mày mò và lập trình viên như tôi rất hào hứng. Không gì tuyệt vời hơn việc hiện thực hoá các phát minh của riêng bạn và trò chuyện với chúng!

Tuy nhiên, việc các thiết bị IoT cài đặt những ứng dụng mà bạn hiếm khi sử dụng có thể gây phiền toái. Vì vậy, chúng tôi tận dụng các công nghệ web sắp ra mắt như Web thực và Web Bluetooth để giúp thiết bị IoT trực quan và ít gây phiền toái hơn.

Ứng dụng khách

Web và IoT, một sự kết hợp hoàn hảo

Vẫn còn nhiều rào cản cần vượt qua trước khi Internet của vạn vật có thể gặt hái được thành công lớn. Một trở ngại là các công ty và sản phẩm yêu cầu mọi người cài đặt ứng dụng cho từng thiết bị họ mua, làm điện thoại của người dùng trở nên lộn xộn với vô số ứng dụng mà họ hiếm khi sử dụng.

Vì lý do này, chúng tôi rất hào hứng với dự án Physical Web (Mạng thực tế). Dự án này cho phép các thiết bị truyền phát một URL đến một trang web trực tuyến theo cách không gây phiền toái. Khi kết hợp với các công nghệ web mới nổi như Bluetooth web, USB webNFC web, các trang web có thể kết nối trực tiếp với thiết bị hoặc ít nhất là giải thích cách thức phù hợp để thực hiện việc này.

Mặc dù chúng tôi chủ yếu tập trung vào Web Bluetooth trong bài viết này, nhưng một số trường hợp sử dụng có thể phù hợp hơn với Web NFC hoặc Web USB. Ví dụ: bạn nên dùng USB trên web nếu cần kết nối thực tế vì lý do bảo mật.

Trang web cũng có thể đóng vai trò là một Ứng dụng web tiến bộ (PWA). Bạn nên tham khảo nội dung giải thích của Google về Ứng dụng web tiến bộ (PWA). PWA là các trang web có trải nghiệm người dùng thích ứng, giống như ứng dụng, có thể hoạt động khi không có mạng và có thể được thêm vào màn hình chính của thiết bị.

Để minh hoạ về khái niệm, tôi đã và đang tạo ra một thiết bị nhỏ bằng cách sử dụng bo mạch đột phá Intel® Edison Arduino. Thiết bị này chứa một cảm biến nhiệt độ (TMP36) cũng như một bộ truyền động (catốt LED có màu). Bạn có thể xem sơ đồ của thiết bị này ở cuối bài viết.

Bảng mạch khung.

Intel Edison là một sản phẩm thú vị vì có thể chạy một bản phân phối Linux* đầy đủ. Do đó, tôi có thể dễ dàng lập trình nút này bằng Node.js. Trình cài đặt cho phép bạn cài đặt Intel* XDK để dễ dàng bắt đầu, mặc dù bạn cũng có thể lập trình và tải lên thiết bị theo cách thủ công.

Đối với ứng dụng Node.js, tôi cần có 3 mô-đun nút cũng như các phần phụ thuộc của chúng:

  • eddystone-beacon
  • parse-color
  • johnny-five

Trình bổ trợ trước tự động cài đặt noble, đây là mô-đun nút mà tôi sử dụng để trò chuyện qua Bluetooth năng lượng thấp.

Tệp package.json cho dự án có dạng như sau:

{
    "name": "edison-webbluetooth-demo-server",
    "version": "1.0.0",
    "main": "main.js",
    "engines": {
    "node": ">=0.10.0"
    },
    "dependencies": {
    "eddystone-beacon": "^1.0.5",
    "johnny-five": "^0.9.30",
    "parse-color": "^1.0.0"
    }
}

Công bố trang web

Kể từ phiên bản 49, Chrome trên Android hỗ trợ Physical Web, cho phép Chrome xem các URL được các thiết bị xung quanh truyền phát. Có một số yêu cầu mà nhà phát triển phải nắm được, chẳng hạn như yêu cầu đối với các trang web cần phải có thể truy cập công khai và sử dụng HTTPS.

Giao thức Eddystone có giới hạn kích thước 18 byte trên URL. Vì vậy, để URL cho ứng dụng minh hoạ của tôi hoạt động (https://webbt-sensor-hub.appspot.com/), tôi cần sử dụng một công cụ rút ngắn URL.

Việc phát URL khá đơn giản. Tất cả những gì bạn cần làm là nhập các thư viện cần thiết và gọi một vài hàm. Một cách để thực hiện việc này là gọi advertiseUrl khi chip BLE đang bật:

var beacon = require("eddystone-beacon");
var bleno = require('eddystone-beacon/node_modules/bleno');

bleno.on('stateChange', function(state) {    
    if (state === 'poweredOn') {
    beacon.advertiseUrl("https://goo.gl/9FomQC", {name: 'Edison'});
    }   
}

Việc này thật sự không thể dễ dàng hơn. Bạn có thể thấy trong hình ảnh bên dưới rằng Chrome tìm thấy thiết bị một cách dễ dàng.

Chrome thông báo về các beacon Web thực tế lân cận.
URL ứng dụng web được liệt kê.

Giao tiếp với cảm biến/bộ truyền động

Chúng tôi sử dụng Johnny-Five* để trò chuyện với các tính năng nâng cao của bảng điều khiển. Johnny-Five có một tính năng trừu tượng rất hay để giao tiếp với cảm biến TMP36.

Dưới đây là mã đơn giản để nhận thông báo về sự thay đổi nhiệt độ cũng như đặt màu đèn LED ban đầu.

var five = require("johnny-five");
var Edison = require("edison-io");
var board = new five.Board({
    io: new Edison()
});

board.on("ready", function() {
    // Johnny-Five's Led.RGB class can be initialized with
    // an array of pin numbers in R, G, B order.
    // Reference: http://johnny-five.io/api/led.rgb/#parameters
    var led = new five.Led.RGB([ 3, 5, 6 ]);

    // Johnny-Five's Thermometer class provides a built-in
    // controller definition for the TMP36 sensor. The controller
    // handles computing a Celsius (also Fahrenheit & Kelvin) from
    // a raw analog input value.
    // Reference: http://johnny-five.io/api/thermometer/
    var temp = new five.Thermometer({
    controller: "TMP36",
    pin: "A0",
    });

    temp.on("change", function() {
    temperatureCharacteristic.valueChange(this.celsius);
    });

    colorCharacteristic._led = led;
    led.color(colorCharacteristic._value);
    led.intensity(30);
});

Hiện tại, bạn có thể bỏ qua các biến *Characteristic ở trên; các biến này sẽ được xác định trong phần sau về cách giao tiếp với Bluetooth.

Như bạn có thể nhận thấy trong quá trình tạo bản sao của đối tượng Nhiệt kế, tôi giao tiếp với TMP36 thông qua cổng A0 tương tự. Các chân điện áp trên cực âm đèn LED màu được kết nối với các chân kỹ thuật số 3, 5 và 6, chính là chân điều chế độ rộng xung (PWM) trên bảng phân tích Edison Arduino.

Bảng Edison

Trò chuyện với Bluetooth

Nói chuyện với Bluetooth không thể dễ dàng hơn nhiều so với noble.

Trong ví dụ sau, chúng ta tạo hai đặc điểm Bluetooth Low Energy: một cho đèn LED và một cho cảm biến nhiệt độ. Lựa chọn trước cho phép chúng ta đọc màu đèn LED hiện tại và đặt màu mới. Lớp này cho phép chúng ta đăng ký các sự kiện thay đổi nhiệt độ.

Với noble, bạn có thể dễ dàng tạo một đặc điểm. Bạn chỉ cần xác định cách đặc điểm giao tiếp và xác định UUID. Các tuỳ chọn giao tiếp là đọc, ghi, thông báo hoặc bất kỳ tổ hợp nào của các tuỳ chọn đó. Cách dễ nhất để thực hiện việc này là tạo một đối tượng mới và kế thừa từ bleno.Characteristic.

Đối tượng đặc trưng thu được sẽ có dạng như sau:

var TemperatureCharacteristic = function() {
    bleno.Characteristic.call(this, {
    uuid: 'fc0a',
    properties: ['read', 'notify'],
    value: null
    });
    
    this._lastValue = 0;
    this._total = 0;
    this._samples = 0;
    this._onChange = null;
};

util.inherits(TemperatureCharacteristic, bleno.Characteristic);

Chúng ta đang lưu trữ giá trị nhiệt độ hiện tại trong biến this._lastValue. Chúng ta cần thêm một phương thức onReadRequest và mã hoá giá trị để "đọc" hoạt động.

TemperatureCharacteristic.prototype.onReadRequest = function(offset, callback) {
    var data = new Buffer(8);
    data.writeDoubleLE(this._lastValue, 0);
    callback(this.RESULT_SUCCESS, data);
};

Đối với tính năng "thông báo", chúng ta cần thêm một phương thức để xử lý các gói thuê bao và huỷ đăng ký. Về cơ bản, chúng ta chỉ lưu trữ một lệnh gọi lại. Khi muốn gửi lý do mới về nhiệt độ, chúng ta gọi lệnh gọi lại đó bằng giá trị mới (được mã hoá như ở trên).

TemperatureCharacteristic.prototype.onSubscribe = function(maxValueSize, updateValueCallback) {
    console.log("Subscribed to temperature change.");
    this._onChange = updateValueCallback;
    this._lastValue = undefined;
};

TemperatureCharacteristic.prototype.onUnsubscribe = function() {
    console.log("Unsubscribed to temperature change.");
    this._onChange = null;
};

Vì các giá trị có thể biến động một chút, nên chúng ta cần làm mượt các giá trị nhận được từ cảm biến TMP36. Tôi chỉ chọn lấy trung bình của 100 mẫu và chỉ gửi thông tin cập nhật khi nhiệt độ thay đổi ít nhất 1 độ.

TemperatureCharacteristic.prototype.valueChange = function(value) {
    this._total += value;
    this._samples++;
    
    if (this._samples < NO_SAMPLES) {
        return;
    }
        
    var newValue = Math.round(this._total / NO_SAMPLES);
    
    this._total = 0;
    this._samples = 0;
    
    if (this._lastValue && Math.abs(this._lastValue - newValue) < 1) {
        return;
    }
    
    this._lastValue = newValue;
    
    console.log(newValue);
    var data = new Buffer(8);
    data.writeDoubleLE(newValue, 0);
    
    if (this._onChange) {
        this._onChange(data);
    }
};

Đó là cảm biến nhiệt độ. Đèn LED màu đơn giản hơn. Đối tượng cũng như phương thức "read" (đọc) được hiển thị bên dưới. Đặc điểm này được định cấu hình để cho phép các thao tác "đọc" và "ghi" và có UUID khác với đặc điểm nhiệt độ.

var ColorCharacteristic = function() {
    bleno.Characteristic.call(this, {
    uuid: 'fc0b',
    properties: ['read', 'write'],
    value: null
    });
    this._value = 'ffffff';
    this._led = null;
};

util.inherits(ColorCharacteristic, bleno.Characteristic);

ColorCharacteristic.prototype.onReadRequest = function(offset, callback) {
    var data = new Buffer(this._value);
    callback(this.RESULT_SUCCESS, data);
};

Để điều khiển đèn LED từ đối tượng, tôi thêm một thành viên this._led mà tôi dùng để lưu trữ đối tượng đèn LED Johnny-Five. Tôi cũng đặt màu của đèn LED thành giá trị mặc định (màu trắng, còn gọi là #ffffff).

board.on("ready", function() {
    ...
    colorCharacteristic._led = led;
    led.color(colorCharacteristic._value);
    led.intensity(30);
    ...
}

Phương thức "ghi" nhận một chuỗi (giống như "đọc" gửi một chuỗi), chuỗi này có thể bao gồm mã màu CSS (Ví dụ: tên CSS như rebeccapurple hoặc mã hex như #ff00bb). Tôi sử dụng mô-đun nút có tên parse-color để luôn nhận được giá trị hex mà Johnny-Five mong đợi.

ColorCharacteristic.prototype.onWriteRequest = function(data, offset, withoutResponse, callback) {
    var value = parse(data.toString('utf8')).hex;
    if (!value) {
        callback(this.RESULT_SUCCESS);
        return;
    }
    
    this._value = value;
    console.log(value);

    if (this._led) {
        this._led.color(this._value);
    }
    callback(this.RESULT_SUCCESS);
};

Tất cả những điều trên sẽ không hoạt động nếu chúng ta không đưa mô-đun bleno vào. eddystone-beacon sẽ không hoạt động với bleno trừ phi bạn sử dụng phiên bản noble được phân phối cùng với nó. Thật may, việc đó khá đơn giản:

var bleno = require('eddystone-beacon/node_modules/bleno');
var util = require('util');

Bây giờ, tất cả những gì chúng ta cần là để ứng dụng quảng cáo thiết bị của chúng ta (UUID) và các đặc điểm của thiết bị (UUID khác)

bleno.on('advertisingStart', function(error) {
    ...
    bleno.setServices([
        new bleno.PrimaryService({
        uuid: 'fc00',
        characteristics: [
            temperatureCharacteristic, colorCharacteristic
        ]
        })
    ]);
});

Tạo ứng dụng web

Để làm ví dụ, chúng ta có thể minh hoạ giao diện người dùng thích ứng được tạo trong Polymer* mà không gặp phải quá nhiều lần thất bại về cách hoạt động của các phần không có Bluetooth trong ứng dụng. Ứng dụng thu được sẽ có dạng như sau:

Ứng dụng khách trên điện thoại.
Thông báo lỗi.

Ở bên phải là một phiên bản cũ, hiển thị một nhật ký lỗi đơn giản mà tôi đã thêm vào để dễ dàng phát triển.

Web Bluetooth giúp bạn dễ dàng giao tiếp với các thiết bị Bluetooth Năng lượng thấp, vì vậy, hãy xem phiên bản đơn giản của mã kết nối của tôi. Nếu bạn không biết cách hoạt động của lời hứa, hãy xem tài nguyên này trước khi đọc thêm.

Việc kết nối với một thiết bị Bluetooth liên quan đến một chuỗi các lời hứa. Trước tiên, chúng ta lọc thiết bị (UUID: FC00, tên: Edison). Thao tác này sẽ hiển thị một hộp thoại cho phép người dùng chọn thiết bị theo bộ lọc. Sau đó, chúng ta kết nối với dịch vụ GATT và nhận dịch vụ chính cũng như các đặc điểm liên quan, sau đó đọc các giá trị và thiết lập lệnh gọi lại thông báo.

Phiên bản đơn giản của mã dưới đây chỉ hoạt động với API Web Bluetooth mới nhất nên yêu cầu Nhà phát triển Chrome (M49) trên Android.

navigator.bluetooth.requestDevice({
    filters: [{ name: 'Edison' }],
    optionalServices: [0xFC00]
})

.then(device => device.gatt.connect())

.then(server => server.getPrimaryService(0xFC00))

.then(service => {
    let p1 = () => service.getCharacteristic(0xFC0B)
    .then(characteristic => {
    this.colorLedCharacteristic = characteristic;
    return this.readLedColor();
    });

    let p2 = () => service.getCharacteristic(0xFC0A)
    .then(characteristic => {
    characteristic.addEventListener(
        'characteristicvaluechanged', this.onTemperatureChange);
    return characteristic.startNotifications();
    });

    return p1().then(p2);
})

.catch(err => {
    // Catch any error.
})
            
.then(() => {
    // Connection fully established, unless there was an error above.
});

Việc đọc và ghi một chuỗi từ DataView / ArrayBuffer (API WebBluetooth sử dụng) cũng dễ dàng như việc sử dụng Buffer ở phía Node.js. Tất cả những gì chúng ta cần sử dụng là TextEncoderTextDecoder:

readLedColor: function() {
    return this.colorLedCharacteristic.readValue()
    .then(data => {
    // In Chrome 50+, a DataView is returned instead of an ArrayBuffer.
    data = data.buffer ? data : new DataView(data);
    let decoder = new TextDecoder("utf-8");
    let decodedString = decoder.decode(data);
    document.querySelector('#color').value = decodedString;
    });
},

writeLedColor: function() {
    let encoder = new TextEncoder("utf-8");
    let value = document.querySelector('#color').value;
    let encodedString = encoder.encode(value.toLowerCase());

    return this.colorLedCharacteristic.writeValue(encodedString);
},

Việc xử lý sự kiện characteristicvaluechanged cho cảm biến nhiệt độ cũng khá dễ dàng:

onTemperatureChange: function(event) {
    let data = event.target.value;
    // In Chrome 50+, a DataView is returned instead of an ArrayBuffer.
    data = data.buffer ? data : new DataView(data);
    let temperature = data.getFloat64(0, /*littleEndian=*/ true);
    document.querySelector('#temp').innerHTML = temperature.toFixed(0);
},

Tóm tắt

Vậy là xong! Như bạn có thể thấy, việc giao tiếp với Bluetooth Năng lượng thấp bằng cách sử dụng Web Bluetooth ở phía máy khách và Node.js trên Edison khá dễ dàng và rất mạnh mẽ.

Bằng cách sử dụng Web trong cuộc sống và Bluetooth của Web, Chrome sẽ tìm thiết bị và cho phép người dùng dễ dàng kết nối với thiết bị mà không cần cài đặt các ứng dụng ít dùng mà người dùng có thể không muốn sử dụng. Các ứng dụng này có thể cập nhật tuỳ từng thời điểm.

Bản minh hoạ

Bạn có thể thử ứng dụng khách để lấy cảm hứng về cách tạo ứng dụng web của riêng mình nhằm kết nối với các thiết bị Internet của vạn vật tuỳ chỉnh.

Mã nguồn

Bạn có thể xem mã nguồn tại đây. Vui lòng báo cáo vấn đề hoặc gửi bản vá.

Phác thảo

Nếu bạn thực sự thích phiêu lưu và muốn tái hiện những gì tôi đã làm, hãy tham khảo bản phác thảo của Edison và bảng mạch khung dưới đây:

Phác thảo