Tworzenie połączonego z internetem urządzenia IoT z wykorzystaniem procesora Intel Edison

Kenneth Christiansen
Kenneth Christiansen

Internet Rzeczy jest obecnie na ustach wszystkich i budzi duże zainteresowanie wśród majsterkowiczów i programistów takich jak ja. Nic nie jest fajniejsze niż ożywienie własnych wynalazków i możliwość z nimi rozmawiania.

Urządzenia IoT, które instalują aplikacje, których rzadko używasz, mogą być uciążliwe, dlatego korzystamy z przyszłych technologii internetowych, takich jak PhysicalWeb i Web Bluetooth, aby urządzenia IoT były bardziej intuicyjne i mniej inwazyjne.

Aplikacja kliencka

Internet i IoT – dopasowanie, które warto rozważyć

Przed Internetem Rzeczy jeszcze wiele przeszkód do pokonania, zanim odniesie ono ogromny sukces. Jednym z nich są firmy i produkty, które wymagają instalowania aplikacji na każdym zakupionym urządzeniu, przez co telefony użytkowników są zaśmiecone mnóstwem aplikacji, których rzadko używają.

Dlatego bardzo cieszy nas projekt Physical Web, który umożliwia urządzeniom przesyłanie adresu URL do witryny internetowej w nieinwazyjny sposób. W połączeniu z nowymi technologiami internetowymi, takimi jak Web Bluetooth, Web USBWeb NFC, strony mogą łączyć się bezpośrednio z urządzeniem lub przynajmniej wyjaśniać, jak to zrobić.

W tym artykule skupiamy się głównie na protokole Web Bluetooth, ale niektóre przypadki użycia mogą lepiej pasować do Web NFC lub Web USB. Na przykład Web USB jest preferowany, jeśli ze względów bezpieczeństwa wymagane jest połączenie fizyczne.

Witryna może też pełnić funkcję progresywnej aplikacji internetowej (PWA). Zachęcamy do zapoznania się z wyjaśnieniem Google na temat PWA. PWA to witryny, które zapewniają responsywne wrażenia użytkownika podobne do aplikacji, mogą działać offline i można je dodać do ekranu głównego urządzenia.

W ramach testowania koncepcji zbudowałem małe urządzenie z użyciem płytki rozszerzającej Arduino Intel® Edison. Urządzenie zawiera czujnik temperatury (TMP36) oraz siłownik (kolorowa dioda LED). Schematy tego urządzenia znajdziesz na końcu tego artykułu.

Płytka prototypowa.

Intel Edison to interesujący produkt, ponieważ może uruchamiać pełną dystrybucję systemu Linux*. Dlatego mogę łatwo zaprogramować go za pomocą Node.js. Instalator umożliwia zainstalowanie pakietu Intel* XDK, który ułatwia rozpoczęcie pracy, ale programowanie i przesyłanie na urządzenie możesz też wykonać ręcznie.

W przypadku mojej aplikacji Node.js potrzebowałam 3 modułów Node.js oraz ich zależności:

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

Pierwsza z nich automatycznie instaluje noble, czyli moduł węzła, którego używam do komunikacji przez Bluetooth Low Energy.

.

Plik package.json projektu wygląda tak:

{
    "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"
    }
}

Ogłoszenie witryny

Od wersji 49 Chrome na Androida obsługuje sieć fizyczną, która umożliwia Chrome wyświetlanie adresów URL przesyłanych przez urządzenia w pobliżu. Deweloperzy muszą spełnić kilka wymagań, np. zapewnić publiczny dostęp do witryn i użyć protokołu HTTPS.

Protokół Eddystone ma limit rozmiaru 18 bajtów na adresy URL. Aby adres URL mojej aplikacji demonstracyjnej (https://webbt-sensor-hub.appspot.com/) działał, muszę użyć skróconego adresu URL.

Przesyłanie adresu URL jest bardzo proste. Wystarczy, że zaimportujesz wymagane biblioteki i wywołasz kilka funkcji. Jednym ze sposobów jest wywołanie funkcji advertiseUrl, gdy włączony jest moduł BLE:

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'});
    }   
}

To naprawdę proste. Na poniższym obrazku widać, że Chrome bez problemu znajduje urządzenie.

Chrome informuje o bliskich sygnalizatorach Physical Web.
Adres URL aplikacji internetowej jest widoczny na liście.

Komunikacja z czujnikiem/aktywatorem

Używamy Johnny-Five* do komunikacji z naszą tablicą. Johnny-Five ma przyjemną abstrakcję do komunikacji z czujnikiem TMP36.

Poniżej znajdziesz prosty kod, który powiadamia o zmianach temperatury, a także ustawia początkowy kolor diody LED.

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);
});

Na razie możesz zignorować powyższe zmienne *Characteristic. Zostaną one zdefiniowane w późniejszej sekcji dotyczącej interfejsu Bluetooth.

Jak widać w instancji obiektu Thermometer, komunikuję się z TMP36 przez port analogowy A0. Odnogi napięcia na katodzie kolorowej diody LED są połączone z pinami cyfrowymi 3, 5 i 6, które są pinami modulacji szerokości impulsu (PWM) na płytce rozszerzeń Edison Arduino.

Edison board

Rozmowa przez Bluetooth

Rozmowy z Bluetooth nie mogłyby być łatwiejsze niż z noble.

W tym przykładzie tworzymy 2 charakterystyki Bluetooth Low Energy: jedną dla diody LED i jedną dla czujnika temperatury. Pierwsza pozwala odczytać aktualny kolor diody LED i ustawić nowy. To ostatnie pozwala nam subskrybować zdarzenia zmiany temperatury.

Dzięki noble utworzenie cechy jest bardzo proste. Wystarczy, że zdefiniujesz sposób komunikacji tej właściwości i UUID. Opcje komunikacji to odczyt, zapis, powiadomienie lub ich dowolna kombinacja. Najłatwiej jest utworzyć nowy obiekt i odziedziczyć go z poziomu klasy bleno.Characteristic.

Wynikowy obiekt cechy wygląda tak:

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);

Aktualną wartość temperatury przechowujemy w zmiennej this._lastValue. Aby metoda „read” działała, musimy dodać metodę onReadRequest i zakodować wartość.

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

W przypadku „notify” musimy dodać metodę obsługi subskrypcji i anulowania subskrypcji. Zasadniczo po prostu przechowujemy wywołanie zwrotne. Gdy mamy nowy powód temperatury, który chcemy wysłać, wywołujemy funkcję z nową wartością (zakodowaną tak jak powyżej).

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;
};

Ponieważ wartości mogą się nieco wahać, musimy wygładzić wartości uzyskane z czujnika TMP36. Postanowiłem po prostu obliczać średnią z 100 próbek i wysyłać aktualizacje tylko wtedy, gdy temperatura zmienia się o co najmniej 1 stopień.

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);
    }
};

To był czujnik temperatury. Diody LED w kolorze są prostsze. Obiekt i metoda „read” zostały pokazane poniżej. Właściwość jest skonfigurowana tak, aby zezwalać na operacje „read” i „write” oraz ma inny identyfikator UUID niż właściwość temperatury.

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);
};

Aby sterować diodą LED z obiektu, dodaję element this._led, który służy do przechowywania obiektu Johnny-Five LED. Ustawiłem też kolor diody LED na domyślny (biały, czyli #ffffff).

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

Metoda „write” otrzymuje ciąg znaków (podobnie jak „read” przesyła ciąg znaków), który może zawierać kod koloru CSS (np. nazwy CSS, takie jak rebeccapurple lub szesnastkowe kody, takie jak #ff00bb). Używam modułu node o nazwie parse-color, aby zawsze otrzymywać wartość szesnastkową, której oczekuje Johnny-Five.

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);
};

Wszystkie te rozwiązania nie zadziałają, jeśli nie uwzględnimy modułu bleno. eddystone-beacon nie będzie działać z bleno, chyba że użyjesz wersji noble dołączonej do tego pakietu. Na szczęście jest to bardzo proste:

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

Teraz wystarczy, aby reklamował ono nasze urządzenie (UUID) i jego cechy (inne UUID).

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

Tworzenie aplikacji internetowej klienta

Nie wchodząc w szczegóły działania części aplikacji klienckiej, która nie korzysta z Bluetooth, możemy na przykład zademonstrować responsywny interfejs użytkownika utworzony w Polymerze*. Wynikiem jest aplikacja widoczna poniżej:

Aplikacja kliencka na telefonie.
komunikat o błędzie.

Po prawej stronie widać wcześniejszą wersję, która zawiera prosty dziennik błędów dodany w celu ułatwienia procesu tworzenia.

Web Bluetooth ułatwia komunikację z urządzeniami Bluetooth Low Energy, więc przyjrzyjmy się uproszczonej wersji mojego kodu połączenia. Jeśli nie wiesz, jak działają obietnice, przeczytaj ten materiał, zanim przejdziesz dalej.

Łączenie z urządzeniem Bluetooth wymaga łańcucha obietnic. Najpierw filtrujemy według urządzenia (UUID: FC00, nazwa: Edison). Wyświetla się okno, w którym użytkownik może wybrać urządzenie na podstawie filtra. Następnie łączymy się z usługą GATT i pobieramy główną usługę oraz powiązane z nią cechy. Następnie odczytujemy wartości i konfigurujemy wywołania zwrotne powiadomień.

Uproszczona wersja kodu poniżej działa tylko z najnowszym interfejsem API Bluetooth na potrzeby sieci. Wymaga więc Chrome Dev (M49) na Androidzie.

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.
});

Odczytywanie i zapisywanie ciągu znaków z interfejsu DataView / ArrayBuffer (którego używa interfejs WebBluetooth API) jest tak samo proste jak użycie Buffer po stronie Node.js. Wystarczy, że użyjesz 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);
},

Obsługa zdarzenia characteristicvaluechanged w przypadku czujnika temperatury jest też dość prosta:

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);
},

Podsumowanie

To wszystko. Jak widać, komunikacja z Bluetooth Low Energy przy użyciu Web Bluetooth po stronie klienta i Node.js na Edisonie jest dość łatwa i bardzo wydajna.

Korzystając z fizycznego internetu i internetowego Bluetootha, Chrome wyszukuje urządzenie i umożliwia użytkownikowi łatwe połączenie z nim bez konieczności instalowania rzadko używanych aplikacji, których użytkownik może nie chcieć instalować i które mogą być aktualizowane od czasu do czasu.

Prezentacja

Możesz wypróbować klienta, aby zainspirować się, jak tworzyć własne aplikacje internetowe do łączenia się z niestandardowymi urządzeniami IoT.

Kod źródłowy

Kod źródłowy jest dostępny tutaj. Zgłoś problemy lub prześlij poprawki.

Szkic

Jeśli masz ochotę na prawdziwą przygodę i chcesz odtworzyć to, co ja zrobiłem, zapoznaj się z rysunkiem Edisona i schematem montażowym poniżej:

Szkic