יצירת מכשיר IoT מותאם לאינטרנט באמצעות Intel Edison

Kenneth Christiansen
Kenneth Christiansen

כיום, האינטרנט של הדברים נמצא על שפתיהם של כולם, והוא מתרגש מאוד לחובבי בינה מלאכותית (Tinkerer) ומתכנתים כמוני. אין דבר מגניב יותר מלעורר לחיים את ההמצאות שלכם ולדבר איתן!

אבל מכשירי IoT שמתקינים אפליקציות שאתם לא משתמשים בהן לעיתים רחוקות יכולים להטריד, לכן אנחנו מנצלים את הטכנולוגיות של האינטרנט, כמו ה-Bluetooth והאינטרנט הפיזי, כדי שמכשירי IoT יהיו יותר אינטואיטיביים ופחות פולשניים.

אפליקציית לקוח

אינטרנט ו-IoT, שילוב מנצח

יש עדיין הרבה מכשולים שצריך להתגבר עליהם כדי שהאינטרנט של הדברים יהיה הצלחה עצומה. אחד מהחסרונות הוא חברות ומוצרים שמחייבים אנשים להתקין אפליקציות בכל מכשיר שהם רוכשים, וממלאים את הטלפונים של המשתמשים במגוון אפליקציות שהם מעטים להשתמש בהן.

לכן אנחנו שמחים מאוד על הפרויקט Physical Web, שמאפשר למכשירים לשדר כתובת URL לאתר באינטרנט, בדרך לא פולשנית. בשילוב עם טכנולוגיות אינטרנט מתפתחות כמו Web Bluetooth,‏ Web USB ו-Web NFC, האתרים יכולים להתחבר ישירות למכשיר או לפחות להסביר איך לעשות זאת.

במאמר הזה אנחנו מתמקדים בעיקר ב-Bluetooth באינטרנט, אבל חלק מהתרחישים יתאימו יותר ל-Web NFC או Web USB. לדוגמה, כדאי להשתמש ב-Web USB אם אתם צריכים חיבור פיזי מסיבות אבטחה.

האתר יכול לשמש גם בתור Progressive Web App (PWA). אנחנו ממליצים לקוראים לעיין בהסבר של Google על אפליקציות PWA. אפליקציות PWA הן אתרים עם חוויית משתמש רספונסיבית ודומה לאפליקציה, שיכולים לפעול במצב אופליין ולהתווסף למסך הבית של המכשיר.

בתור הוכחת רעיון, פיתחתי מכשיר קטן באמצעות Intel® Edison Arduino. המכשיר מכיל חיישן טמפרטורה (TMP36) וכן מפעיל (קטודה של נורית LED צבעונית). שרטוטי המכשיר מופיעים בסוף המאמר.

ברדבורד.

Intel Edison הוא מוצר מעניין כי אפשר להריץ בו הפצה מלאה של Linux*. לכן אפשר לתכנת אותו בקלות באמצעות Node.js. התוכנה להתקנה מאפשרת להתקין את Intel* XDK, וכך להתחיל בקלות. אפשר גם לתכנת ולהעלות את האפליקציה למכשיר באופן ידני.

באפליקציה שלי ב-Node.js נדרשו לי שלושה מודולים של Node, וגם יחסי התלות שלהם:

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

הקוד הראשון מתקין באופן אוטומטי את noble, שהוא מודול הצומת שבו משתמשים כדי לדבר דרך Bluetooth עם צריכת אנרגיה נמוכה.

הקובץ package.json של הפרויקט נראה כך:

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

הכרזה על האתר

החל מגרסה 49, Chrome ב-Android תומך באינטרנט פיזי, שמאפשר ל-Chrome לראות כתובות URL שמשודרות על ידי מכשירים בסביבתו. יש כמה דרישות שהמפתחים צריכים להיות מודעים אליהן, כמו הצורך שהאתרים יהיו נגישים לכולם ושהפרוטוקול שלהם יהיה HTTPS.

לפרוטוקול Eddystone יש מגבלת גודל של 18 בייטים בכתובות URL. לכן, כדי שכתובת ה-URL של אפליקציית הדגמה שלי תפעל (https://webbt-sensor-hub.appspot.com/), צריך להשתמש בשירות לקצרת כתובות URL.

שידור כתובת ה-URL הוא פשוט למדי. כל מה שצריך לעשות הוא לייבא את הספריות הנדרשות ולקרוא לכמה פונקציות. אחת הדרכים לעשות זאת היא להפעיל את הפונקציה advertiseUrl כשצ'יפ ה-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'});
    }   
}

זה פשוט מאוד. בתמונה הבאה אפשר לראות ש-Chrome מוצא את המכשיר בצורה תקינה.

ב-Chrome מתבצעת הודעה על סמנים פיזיים של אינטרנט בקרבת מקום.
כתובת ה-URL של אפליקציית האינטרנט מופיעה ברשימה.

תקשורת עם החיישן/האקטור

אנחנו משתמשים ב-Johnny-Five* כדי לדבר עם השיפורים של הלוח. ל-Johnny-Five יש הפשטה יפה לדיבור עם חיישן TMP36.

בהמשך מופיע הקוד הפשוט לקבלת התראות על שינויים בטמפרטורה, וגם להגדרת צבע ה-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);
});

בשלב הזה אפשר להתעלם מהמשתנים *Characteristic שלמעלה. הם יוגדרו בהמשך, בקטע בנושא ממשק עם Bluetooth.

כפי שאפשר לראות ביצירת האובייקט Thermometer, אני משוחח עם TMP36 דרך היציאה האנלוגית A0. רגלי המתח בקטודה של ה-LED הצבעוני מחוברות לפינים הדיגיטליים 3, 5 ו-6, שהם גם הפינים של PWM (מודולציה של רוחב הפולס) בלוח ה-breakout של Edison Arduino.

לוח Edison

שיחה עם Bluetooth

הדיבור אל Bluetooth לא יכול להיות קל יותר מאשר עם noble.

בדוגמה הבאה אנחנו יוצרים שני מאפיינים של Bluetooth עם צריכת אנרגיה נמוכה: אחד לנורית ה-LED והשני לחיישן הטמפרטורה. הקוד הראשון מאפשר לנו לקרוא את צבע ה-LED הנוכחי ולהגדיר צבע חדש. האפשרות השנייה מאפשרת לנו להירשם לאירועים של שינויים בטמפרטורה.

בעזרת noble, קל ליצור מאפיין. כל מה שצריך לעשות הוא להגדיר את אופן התקשורת של המאפיין ולהגדיר מזהה UUID. אפשרויות התקשורת הן קריאה, כתיבה, התראה או כל שילוב שלהן. הדרך הקלה ביותר לעשות זאת היא ליצור אובייקט חדש ולקבל בירושה מ-bleno.Characteristic.

אובייקט המאפיין שנוצר נראה כך:

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

אנחנו מאחסנים את ערך הטמפרטורה הנוכחי במשתנה this._lastValue. כדי שאפשר יהיה לבצע קריאה, צריך להוסיף שיטה onReadRequest ולקודד את הערך.

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

עבור 'notify', צריך להוסיף שיטה לטיפול במינויים ובביטול מינויים. בעיקרון, אנחנו פשוט שומרים קריאה חוזרת. כשיש לנו סיבה חדשה לשינוי הטמפרטורה שאנחנו רוצים לשלוח, אנחנו קוראים ל-callback עם הערך החדש (מקודד כמו למעלה).

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

מכיוון שהערכים עשויים להשתנות מעט, אנחנו צריכים להחליק את הערכים שאנחנו מקבלים מהחיישן TMP36. בחרתי פשוט לחשב את הממוצע של 100 דגימות ולשלוח עדכונים רק כשהטמפרטורה משתנה ב-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);
    }
};

זה היה חיישן הטמפרטורה. נורת ה-LED הצבעונית פשוטה יותר. האובייקט והשיטה read מופיעים בהמשך. המאפיין מוגדר לאפשר פעולות 'קריאה' ו'כתיבה', ויש לו UUID שונה ממאפיין הטמפרטורה.

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

כדי לשלוט בנורת ה-LED מהאובייקט, מוסיפים את החבר this._led שבו שומרים את אובייקט ה-LED של Johnny-Five. הגדרתי גם את צבע ה-LED לערך ברירת המחדל שלו (לבן, או #ffffff).

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

השיטה write מקבלת מחרוזת (בדיוק כמו ש-read שולחת מחרוזת), שיכולה להכיל קוד צבע של CSS (לדוגמה: שמות CSS כמו rebeccapurple או קודים הקסדצימליים כמו #ff00bb). אני משתמש במודול node שנקרא parse-color כדי לקבל תמיד את הערך הקסדצימלי, שהוא הערך ש-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);
};

כל הפעולות שלמעלה לא יפעלו אם לא נכלול את המודול bleno. eddystone-beacon לא יפעל עם bleno אלא אם משתמשים בגרסת noble שמופצת עם bleno. למרבה המזל, זה די פשוט:

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

עכשיו כל מה שאנחנו צריכים הוא שהמכשיר יפרסם את המזהה הייחודי האוניברסלי (UUID) ואת המאפיינים שלו (מזהי UUID אחרים).

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

יצירת אפליקציית האינטרנט של הלקוח

בלי להיכנס לפרטים רבים מדי לגבי האופן שבו פועלים החלקים באפליקציית הלקוח שלא קשורים ל-Bluetooth, נציג לדוגמה ממשק משתמש רספונסיבי שנוצר ב-Polymer*. האפליקציה שנוצרה מוצגת בהמשך:

אפליקציית לקוח בטלפון.
הודעת שגיאה.

בצד שמאל מוצגת גרסה קודמת, שבה מוצג יומן שגיאות פשוט שהוספתי כדי להקל על הפיתוח.

באמצעות Web Bluetooth אפשר לתקשר בקלות עם מכשירים עם Bluetooth צריכת אנרגיה נמוכה, אז נסתכל על גרסה פשוטה של קוד החיבור שלי. אם אתם לא יודעים איך הבטחות פועלות, כדאי לעיין במקור המידע הזה לפני שממשיכים לקרוא.

התחברות למכשיר Bluetooth כוללת שרשרת של הבטחות. קודם מסננים את המכשיר (UUID: ‏ FC00, שם: ‏ Edison). תוצג תיבת דו-שיח שמאפשרת למשתמש לבחור את המכשיר לפי המסנן. לאחר מכן אנחנו מתחברים לשירות GATT ומקבלים את השירות הראשי ואת המאפיינים המשויכים, ואז קוראים את הערכים ומגדירים קריאות חזרה (callbacks) להתראות.

הגרסה הפשוטה של הקוד שלנו שבהמשך פועלת רק עם Web Bluetooth API העדכני ביותר, ולכן נדרשת גרסה של Chrome Dev‏ (M49) ל-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.
});

קריאה וכתיבה של מחרוזת מ-DataView / ArrayBuffer (המאפיינים שבהם משתמש WebBluetooth API) הן פשוטות כמו שימוש ב-Buffer בצד Node.js. כל מה שצריך הוא TextEncoder ו-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);
},

גם הטיפול באירוע characteristicvaluechanged של חיישן הטמפרטורה קל למדי:

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

סיכום

זהו, זהו! כמו שאפשר לראות, התקשורת עם Bluetooth עם צריכת אנרגיה נמוכה באמצעות חיבור Bluetooth לאינטרנט בצד הלקוח ו-Node.js ב-Edison היא די קלה וחזקה.

באמצעות Physical Web ו-Web Bluetooth, Chrome מאתר את המכשיר ומאפשר למשתמש להתחבר אליו בקלות בלי להתקין אפליקציות שבהן הוא משתמש לעיתים רחוקות, שאולי הוא לא רוצה, ויכול להיות שהן יתעדכנו מדי פעם.

הדגמה (דמו)

אתם יכולים לנסות את הלקוח כדי לקבל השראה ליצירת אפליקציות אינטרנט משלכם לחיבור למכשירי ה-Internet of Things בהתאמה אישית.

קוד מקור

קוד המקור זמין כאן. אפשר לדווח על בעיות או לשלוח תיקונים.

שרטוט

אם אתם רוצים לנסות לחזור על מה שעשיתי, תוכלו לעיין בתרשים של Edison ובלוח הלחצנים שלמטה:

שרטוט