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 – połączenie

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, co zaśmieca telefony użytkowników 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. Tryb Web USB jest preferowany, jeśli na przykład ze względów bezpieczeństwa wymagasz fizycznego połączenia.

Witryna może też pełnić funkcję progresywnej aplikacji internetowej (PWA). Zachęcamy do zapoznania się z wyjaśnieniem Google na temat PWA. Aplikacje PWA są elastyczne, przypominające aplikacje, działają w trybie offline i można je dodać do ekranu głównego urządzenia.

Na dowód koncepcji stworzyłem małe urządzenie, używając płytki zasilającej Intel® Edison Arduino. Urządzenie jest wyposażone w czujnik temperatury (TMP36) oraz urządzenie uruchamiające (katodę LED). Schematy tego urządzenia znajdziesz na końcu tego artykułu.

Płytka prototypowa.

Intel Edison to interesujący produkt, ponieważ umożliwia uruchamianie pełnej dystrybucji systemu Linux*. Dlatego mogę łatwo zaprogramować go za pomocą Node.js. Instalator pozwala zainstalować procesor Intel* XDK, co ułatwia rozpoczęcie pracy, ale możesz też zaprogramować i przesłać na urządzenie 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. Można to zrobić na przykład przez wywołanie metody advertiseUrl, gdy układ BLE jest włączony:

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ę nie może być prostsze. Jak widać na grafice poniżej, Chrome bez problemu znajduje urządzenie.

Chrome informuje o bliskich sygnalizatorach Physical Web.
Adres URL aplikacji internetowej jest podany.

Komunikacja z czujnikiem/aktywatorem

Johnny-Five* pomaga nam wprowadzać ulepszenia. Johnny-Five ma niezłą abstrakcję, jeśli chodzi o rozmowę 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);
});

Możesz na razie zignorować powyższe zmienne *Characteristic. Zdefiniujemy je w dalszej sekcji poświęconej obsłudze Bluetootha.

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 poniższym przykładzie tworzymy 2 cechy Bluetooth Low Energy: jedną dla diody LED i czujnika temperatury. Pierwsza pozwala odczytać aktualny kolor diody LED i ustawić nowy. Dzięki temu możemy 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” przedstawiono 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);
};

Żadne z powyższych rozwiązań nie zadziała, 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 całkiem 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

Bez wyłapywania zbyt wielu błędów dotyczących działania elementów aplikacji klienckiej niekorzystających z Bluetootha możemy zademonstrować na przykładzie elastyczny interfejs użytkownika utworzony w Polymer*. 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 uzyskujemy usługę podstawową i powiązane dane, a następnie odczytujemy wartości i konfigurujemy wywołania zwrotne powiadomień.

Uproszczona wersja naszego kodu poniżej działa tylko z najnowszym interfejsem Web Bluetooth API i dlatego wymaga Chrome w wersji M49 na Androida.

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 (wykorzystywanego przez interfejs WebBluetooth API) jest tak samo łatwe jak używanie Buffer po stronie Node.js. Potrzebujemy tylko tych elementów TextEncoder i TextDecoder:

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.

Za pomocą internetu rzeczy i internetowego Bluetootha Chrome wyszukuje urządzenie i umożliwia użytkownikowi łatwe połączenie z nim bez instalowania rzadko używanych aplikacji, które mogą być od czasu do czasu aktualizowane.

Prezentacja

Wypróbuj klienta, aby znaleźć inspirację i dowiedzieć się, jak tworzyć własne aplikacje internetowe do łączenia się z własnymi urządzeniami z Internetem rzeczy.

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