التحكّم في شمعة PLAYBULB باستخدام Web Bluetooth

1. ما هي هذه الميزة؟

IMG_19700101_023537~2~2.jpg

في هذا الدرس التطبيقي التفاعلي، ستتعرّف على كيفية التحكّم في شمعة PLAYBULB LED بدون لهب باستخدام JavaScript فقط بفضل واجهة برمجة تطبيقات Web Bluetooth. خلال هذه الرحلة، ستتعرّف أيضًا على ميزات JavaScript ES2015، مثل Classes و Arrow functions و Map و Promises.

ما ستتعلمه

  • كيفية التفاعل مع جهاز يتضمّن بلوتوث مجاور في JavaScript
  • كيفية استخدام فئات ES2015 ودوال الأسهم وMap وPromises

المتطلبات

2. تشغيل أول أغنية

ننصحك بالاطّلاع على الإصدار النهائي من التطبيق الذي ستنشئه على الرابط https://googlecodelabs.github.io/candle-bluetooth وتجربة جهاز PLAYBULB Candle Bluetooth المتوفّر لديك قبل البدء في هذا الدرس التطبيقي حول الترميز.

يمكنك أيضًا مشاهدة فيديو لي وأنا أغيّر الألوان على الرابط https://www.youtube.com/watch?v=fBCPA9gIxlU

3- طريقة الإعداد

تنزيل الرمز النموذجي

يمكنك الحصول على الرمز النموذجي لهذا الرمز من خلال تنزيل ملف ZIP هنا:

أو من خلال استنساخ مستودع git هذا:

git clone https://github.com/googlecodelabs/candle-bluetooth.git

إذا نزّلت المصدر كملف مضغوط، من المفترض أن يؤدي فك ضغطه إلى إنشاء مجلد رئيسي candle-bluetooth-master.

تثبيت خادم الويب وإثبات ملكيته

يمكنك استخدام خادم الويب الخاص بك، ولكن تم تصميم هذا الدرس التطبيقي حول الترميز ليعمل بشكل جيد مع "خادم الويب في Chrome". إذا لم يكن هذا التطبيق مثبَّتًا بعد، يمكنك تثبيته من "سوق Chrome الإلكتروني".

بعد تثبيت تطبيق Web Server for Chrome، انقر على اختصار "التطبيقات" في شريط الإشارات:

Screen Shot 2016-11-16 at 4.10.42 PM.png

في النافذة التالية، انقر على رمز "خادم الويب":

9f3c21b2cf6cbfb5.png

سيظهر مربّع الحوار التالي الذي يتيح لك ضبط إعدادات خادم الويب المحلي:

Screen Shot 2016-11-16 at 3.40.47 PM.png

انقر على الزر اختيار مجلد، واختَر جذر المستودع الذي تم استنساخه (أو إلغاء أرشفته). سيسمح لك ذلك بعرض عملك قيد التنفيذ من خلال عنوان URL المميّز في مربّع حوار خادم الويب (في قسم عناوين URL لخادم الويب).

ضِمن "الخيارات"، ضَع علامة في المربّع بجانب عرض ملف index.html تلقائيًا، كما هو موضّح أدناه:

Screen Shot 2016-11-16 at 3.40.56 PM.png

انتقِل الآن إلى موقعك الإلكتروني في متصفّح الويب (من خلال النقر على عنوان URL لخادم الويب المميّز)، وستظهر لك صفحة على النحو التالي:

Screen Shot 2016-11-16 at 3.20.22 PM.png

إذا أردت معرفة شكل هذا التطبيق على هاتف Android، عليك تفعيل تصحيح الأخطاء عن بُعد على Android وإعداد ميزة "إعادة توجيه المنفذ" (رقم المنفذ التلقائي هو 8887). بعد ذلك، يمكنك ببساطة فتح علامة تبويب جديدة في Chrome للانتقال إلى http://localhost:8887 على هاتف Android.

التالي

في هذه المرحلة، لا يمكن لهذا التطبيق على الويب تنفيذ الكثير من الإجراءات. لنبدأ بإضافة ميزة التوافق مع البلوتوث.

4. اكتشاف الشمعة

سنبدأ بكتابة مكتبة تستخدم فئة JavaScript ES2015 لجهاز PLAYBULB Candle الذي يعمل بتقنية البلوتوث.

حافظ على هدوئك. إنّ بنية الفئة لا تقدّم نموذجًا جديدًا للتوريث المستند إلى الكائنات في JavaScript. فهي توفّر ببساطة بنية أوضح بكثير لإنشاء الكائنات والتعامل مع الوراثة، كما يمكنك الاطّلاع أدناه.

أولاً، لنحدّد فئة PlaybulbCandle في playbulbCandle.js وننشئ مثيلاً playbulbCandle سيكون متاحًا في ملف app.js لاحقًا.

playbulbCandle.js

(function() {
  'use strict';

  class PlaybulbCandle {
    constructor() {
      this.device = null;
    }
  }

  window.playbulbCandle = new PlaybulbCandle();

})();

لطلب الوصول إلى جهاز بلوتوث قريب، علينا استدعاء navigator.bluetooth.requestDevice. بما أنّ جهاز PLAYBULB Candle يعلن باستمرار (إذا لم يكن مقترنًا بعد) عن معرّف فريد لخدمة Bluetooth GATT ثابت معروف في شكله المختصر باسم 0xFF02، يمكننا ببساطة تحديد قيمة ثابتة وإضافتها إلى مَعلمة الخدمات في الفلاتر في طريقة connect عامة جديدة للفئة PlaybulbCandle.

سنتتبّع أيضًا عنصر BluetoothDevice داخليًا حتى نتمكّن من الوصول إليه لاحقًا إذا لزم الأمر. بما أنّ navigator.bluetooth.requestDevice تعرض وعد JavaScript ES2015، سننفّذ ذلك في طريقة then.

playbulbCandle.js

(function() {
  'use strict';

  const CANDLE_SERVICE_UUID = 0xFF02;

  class PlaybulbCandle {
    constructor() {
      this.device = null;
    }
    connect() {
      let options = {filters:[{services:[ CANDLE_SERVICE_UUID ]}]};
      return navigator.bluetooth.requestDevice(options)
      .then(function(device) {
        this.device = device;
      }.bind(this)); 
    }
  }

  window.playbulbCandle = new PlaybulbCandle();

})();

كإجراء أمان، يجب طلب رصد أجهزة البلوتوث القريبة باستخدام navigator.bluetooth.requestDevice من خلال إجراء يتخذه المستخدم، مثل النقر باللمس أو بالماوس. لهذا السبب سنستدعي طريقة connect عندما ينقر المستخدم على الزر "ربط" في الملف app.js:

app.js

document.querySelector('#connect').addEventListener('click', function(event) {
  document.querySelector('#state').classList.add('connecting');
  playbulbCandle.connect()
  .then(function() {
    console.log(playbulbCandle.device);
    document.querySelector('#state').classList.remove('connecting');
    document.querySelector('#state').classList.add('connected');
  })
  .catch(function(error) {
    console.error('Argh!', error);
  });
});

تشغيل التطبيق

في هذه المرحلة، انتقِل إلى موقعك الإلكتروني في متصفّح الويب (من خلال النقر على عنوان URL لخادم الويب المميّز في تطبيق خادم الويب) أو أعِد تحميل الصفحة الحالية ببساطة. انقر على الزر الأخضر "ربط" (Connect)، واختَر الجهاز في أداة الاختيار، وافتح وحدة تحكّم "أدوات المطوّرين" المفضّلة لديك باستخدام اختصار لوحة المفاتيح Ctrl + Shift + J، ولاحظ تسجيل العنصر BluetoothDevice.

Screen Shot 2016-11-16 at 3.27.12 PM.png

قد يظهر لك خطأ إذا كان البلوتوث غير مفعّل و/أو كان جهاز البلوتوث PLAYBULB Candle غير مفعّل. في هذه الحالة، فعِّلهما وتابِع الخطوات مرة أخرى.

المكافأة الإلزامية

لا أعرف رأيك، ولكن أرى الكثير من function() {} في هذا الرمز. لننتقل إلى () => {} JavaScript ES2015 Arrow Functions بدلاً من ذلك. إنّها منقذة للحياة: كلّ مزايا الدوال المجهولة، بدون أيّ مشاكل في الربط.

playbulbCandle.js

(function() {
  'use strict';

  const CANDLE_SERVICE_UUID = 0xFF02;

  class PlaybulbCandle {
    constructor() {
      this.device = null;
    }
    connect() {
      let options = {filters:[{services:[ CANDLE_SERVICE_UUID ]}]};
      return navigator.bluetooth.requestDevice(options)
      .then(device => {
        this.device = device;
      }); 
    }
  }

  window.playbulbCandle = new PlaybulbCandle();

})();

app.js

document.querySelector('#connect').addEventListener('click', event => {
  playbulbCandle.connect()
  .then(() => {
    console.log(playbulbCandle.device);
    document.querySelector('#state').classList.remove('connecting');
    document.querySelector('#state').classList.add('connected');
  })
  .catch(error => {
    console.error('Argh!', error);
  });
});

التالي

- حسنًا... هل يمكنني التحدث إلى هذه الشمعة؟

- بالتأكيد، انتقِل إلى الخطوة التالية

الأسئلة الشائعة

5- قراءة محتوى

ماذا تفعل الآن بعد أن تلقّيت BluetoothDevice من وعد navigator.bluetooth.requestDevice؟ لننتقل إلى خادم GATT لجهاز التحكّم عن بُعد الذي يتضمّن بلوتوث والذي يتضمّن تعريفات خدمة البلوتوث وخصائصه من خلال طلب device.gatt.connect():

playbulbCandle.js

  class PlaybulbCandle {
    constructor() {
      this.device = null;
    }
    connect() {
      let options = {filters:[{services:[ CANDLE_SERVICE_UUID ]}]};
      return navigator.bluetooth.requestDevice(options)
      .then(device => {
        this.device = device;
        return device.gatt.connect();
      });
    }
  }

قراءة اسم الجهاز

في هذه الحالة، نكون متصلين بخادم GATT الخاص بجهاز PLAYBULB Candle الذي يتضمّن بلوتوث. نريد الآن الحصول على خدمة GATT الأساسية (التي تم الإعلان عنها باسم 0xFF02 سابقًا) وقراءة سمة اسم الجهاز (0xFFFF) التي تنتمي إلى هذه الخدمة. يمكن تحقيق ذلك بسهولة عن طريق إضافة طريقة جديدة getDeviceName إلى الفئة PlaybulbCandle واستخدام device.gatt.getPrimaryService وservice.getCharacteristic. ستعرض الطريقة characteristic.readValue في الواقع DataView، وسنعمل ببساطة على فك ترميزها باستخدام TextDecoder.

playbulbCandle.js

  const CANDLE_DEVICE_NAME_UUID = 0xFFFF;

  ...

    getDeviceName() {
      return this.device.gatt.getPrimaryService(CANDLE_SERVICE_UUID)
      .then(service => service.getCharacteristic(CANDLE_DEVICE_NAME_UUID))
      .then(characteristic => characteristic.readValue())
      .then(data => {
        let decoder = new TextDecoder('utf-8');
        return decoder.decode(data);
      });
    }

لنضِف هذا إلى app.js من خلال الاتصال بـ playbulbCandle.getDeviceName بعد الاتصال وعرض اسم الجهاز.

app.js

document.querySelector('#connect').addEventListener('click', event => {
  playbulbCandle.connect()
  .then(() => {
    console.log(playbulbCandle.device);
    document.querySelector('#state').classList.remove('connecting');
    document.querySelector('#state').classList.add('connected');
    return playbulbCandle.getDeviceName().then(handleDeviceName);
  })
  .catch(error => {
    console.error('Argh!', error);
  });
});

function handleDeviceName(deviceName) {
  document.querySelector('#deviceName').value = deviceName;
}

في هذه المرحلة، انتقِل إلى موقعك الإلكتروني في متصفّح الويب (من خلال النقر على عنوان URL لخادم الويب المميّز في تطبيق خادم الويب) أو أعِد تحميل الصفحة الحالية ببساطة. تأكَّد من تشغيل PLAYBULB Candle، ثم انقر على الزر "ربط" في الصفحة، وسيظهر لك اسم الجهاز أسفل أداة اختيار الألوان.

Screen Shot 2016-11-16 at 3.29.21 PM.png

قراءة مستوى البطارية

تتوفّر أيضًا سمة بلوتوث عادية لمستوى البطارية في جهاز PLAYBULB Candle الذي يتضمّن مستوى شحن البطارية. وهذا يعني أنّه يمكننا استخدام أسماء عادية، مثل battery_service لمعرّف الخدمة الفريد العام (UUID) في Bluetooth GATT وbattery_level لمعرّف السمة الفريد العام (UUID) في Bluetooth GATT.

لنضِف طريقة getBatteryLevel جديدة إلى الفئة PlaybulbCandle ونقرأ مستوى البطارية بالنسبة المئوية.

playbulbCandle.js

    getBatteryLevel() {
      return this.device.gatt.getPrimaryService('battery_service')
      .then(service => service.getCharacteristic('battery_level'))
      .then(characteristic => characteristic.readValue())
      .then(data => data.getUint8(0));
    }

علينا أيضًا تعديل عنصر options JavaScript ليشمل خدمة البطارية في المفتاح optionalServices لأنّ جهاز PLAYBULB Candle Bluetooth لا يعلن عنها ولكنها تظل إلزامية للوصول إليها.

playbulbCandle.js

      let options = {filters:[{services:[ CANDLE_SERVICE_UUID ]}],
                     optionalServices: ['battery_service']};
      return navigator.bluetooth.requestDevice(options)

كما في السابق، لنربط هذا الإجراء بالدالة app.js من خلال استدعاء الدالة playbulbCandle.getBatteryLevel بعد الحصول على اسم الجهاز وعرض مستوى البطارية.

app.js

document.querySelector('#connect').addEventListener('click', event => {
  playbulbCandle.connect()
  .then(() => {
    console.log(playbulbCandle.device);
    document.querySelector('#state').classList.remove('connecting');
    document.querySelector('#state').classList.add('connected');
    return playbulbCandle.getDeviceName().then(handleDeviceName)
    .then(() => playbulbCandle.getBatteryLevel().then(handleBatteryLevel));
  })
  .catch(error => {
    console.error('Argh!', error);
  });
});

function handleDeviceName(deviceName) {
  document.querySelector('#deviceName').value = deviceName;
}

function handleBatteryLevel(batteryLevel) {
  document.querySelector('#batteryLevel').textContent = batteryLevel + '%';
}

في هذه المرحلة، انتقِل إلى موقعك الإلكتروني في متصفّح الويب (من خلال النقر على عنوان URL لخادم الويب المميّز في تطبيق خادم الويب) أو أعِد تحميل الصفحة الحالية ببساطة. انقر على الزر "ربط" في الصفحة، وسيظهر لك اسم الجهاز ومستوى البطارية.

Screen Shot 2016-11-16 at 3.29.21 PM.png

التالي

- كيف يمكنني تغيير لون هذه المصباح؟ لهذا السبب أنا هنا!

- لم يتبقَّ الكثير، أعدك بذلك...

الأسئلة الشائعة

6. تغيير اللون

يمكن تغيير اللون بسهولة من خلال كتابة مجموعة معيّنة من الأوامر إلى سمة Bluetooth (0xFFFC) في خدمة GATT الأساسية المُعلن عنها باسم 0xFF02. على سبيل المثال، يؤدي تحويل لون PLAYBULB Candle إلى الأحمر إلى كتابة مجموعة من الأعداد الصحيحة غير الموقّعة ذات 8 بتات تساوي [0x00, 255, 0, 0] حيث 0x00 هو التشبع الأبيض و255, 0, 0 هي قيم الأحمر والأخضر والأزرق على التوالي .

سنستخدم characteristic.writeValue لكتابة بعض البيانات في سمة Bluetooth في الطريقة العامة الجديدة setColor للفئة PlaybulbCandle. وسنعرض أيضًا قيم الأحمر والأخضر والأزرق الفعلية عند تنفيذ الوعد، حتى نتمكّن من استخدامها في app.js لاحقًا:

playbulbCandle.js

  const CANDLE_COLOR_UUID = 0xFFFC;

  ...

    setColor(r, g, b) {
      let data = new Uint8Array([0x00, r, g, b]);
      return this.device.gatt.getPrimaryService(CANDLE_SERVICE_UUID)
      .then(service => service.getCharacteristic(CANDLE_COLOR_UUID))
      .then(characteristic => characteristic.writeValue(data))
      .then(() => [r,g,b]);
    }

لنعدّل الدالة changeColor في app.js لاستدعاء playbulbCandle.setColor عندما يتم وضع علامة في زر الاختيار "بلا تأثير". تكون متغيرات الألوان العامة r, g, b مضبوطة مسبقًا عندما ينقر المستخدم على لوحة اختيار الألوان.

app.js

function changeColor() {
  var effect = document.querySelector('[name="effectSwitch"]:checked').id;
  if (effect === 'noEffect') {
    playbulbCandle.setColor(r, g, b).then(onColorChanged);
  }
}

في هذه المرحلة، انتقِل إلى موقعك الإلكتروني في متصفّح الويب (من خلال النقر على عنوان URL لخادم الويب المميّز في تطبيق خادم الويب) أو أعِد تحميل الصفحة الحالية ببساطة. انقر على الزر "ربط" في الصفحة، ثم انقر على علبة الألوان لتغيير لون PLAYBULB Candle عدة مرات كما تريد.

Screen Shot 2016-11-16 at 3.31.37 PM.png

المزيد من تأثيرات الشموع

إذا سبق لك إضاءة شمعة، ستعرف أنّ الضوء ليس ثابتًا. لحسن الحظ، يتوفّر لدينا سمة أخرى من سمات البلوتوث (0xFFFB) في خدمة GATT الأساسية يتم الإعلان عنها على أنّها 0xFF02، وتتيح للمستخدم ضبط بعض تأثيرات الشمعة.

يمكن مثلاً ضبط "تأثير الشمعة" من خلال كتابة [0x00, r, g, b, 0x04, 0x00, 0x01, 0x00]. ويمكنك أيضًا ضبط "تأثير الوميض" باستخدام [0x00, r, g, b, 0x00, 0x00, 0x1F, 0x00].

لنضِف الطريقتَين setCandleEffectColor وsetFlashingColor إلى الفئة PlaybulbCandle.

playbulbCandle.js

  const CANDLE_EFFECT_UUID = 0xFFFB;

  ...

    setCandleEffectColor(r, g, b) {
      let data = new Uint8Array([0x00, r, g, b, 0x04, 0x00, 0x01, 0x00]);
      return this.device.gatt.getPrimaryService(CANDLE_SERVICE_UUID)
      .then(service => service.getCharacteristic(CANDLE_EFFECT_UUID))
      .then(characteristic => characteristic.writeValue(data))
      .then(() => [r,g,b]);
    }
    setFlashingColor(r, g, b) {
      let data = new Uint8Array([0x00, r, g, b, 0x00, 0x00, 0x1F, 0x00]);
      return this.device.gatt.getPrimaryService(CANDLE_SERVICE_UUID)
      .then(service => service.getCharacteristic(CANDLE_EFFECT_UUID))
      .then(characteristic => characteristic.writeValue(data))
      .then(() => [r,g,b]);
    }

لنعدّل الدالة changeColor في app.js لاستدعاء playbulbCandle.setCandleEffectColor عند وضع علامة في زر الاختيار "تأثير الشمعة"، وplaybulbCandle.setFlashingColor عند وضع علامة في زر الاختيار "وميض". هذه المرة، سنستخدم switch إذا لم يكن لديك مانع.

app.js

function changeColor() {
  var effect = document.querySelector('[name="effectSwitch"]:checked').id;
  switch(effect) {
    case 'noEffect':
      playbulbCandle.setColor(r, g, b).then(onColorChanged);
      break;
    case 'candleEffect':
      playbulbCandle.setCandleEffectColor(r, g, b).then(onColorChanged);
      break;
    case 'flashing':
      playbulbCandle.setFlashingColor(r, g, b).then(onColorChanged);
      break;
  }
}

في هذه المرحلة، انتقِل إلى موقعك الإلكتروني في متصفّح الويب (من خلال النقر على عنوان URL لخادم الويب المميّز في تطبيق خادم الويب) أو أعِد تحميل الصفحة الحالية ببساطة. انقر على الزر "ربط" في الصفحة وشغِّل تأثيرَي "الشموع" و"الوميض".

Screen Shot 2016-11-16 at 3.33.23 PM.png

التالي

- هذا كل ما في الأمر؟ ‫3 تأثيرات سيئة للشموع؟ هل هذا هو سبب وجودي هنا؟

- هناك المزيد، ولكن عليك البحث عنها بنفسك هذه المرة.

7. بذل جهد إضافي

وها نحن هنا! قد تعتقد أنّها النهاية تقريبًا، ولكن لم ينتهِ التطبيق بعد. لنرى ما إذا كنت قد فهمت ما نسخته ولصقته خلال هذا الدرس العملي. إليك ما عليك فعله بنفسك الآن لجعل هذا التطبيق يتألّق.

إضافة التأثيرات المفقودة

في ما يلي بيانات التأثيرات الناقصة:

  • النبض: [0x00, r, g, b, 0x01, 0x00, 0x09, 0x00] (قد تحتاج إلى تعديل قيم r, g, b هناك)
  • قوس قزح: [0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00] (ننصح الأشخاص المصابين بالصرع بتجنُّب هذا الرمز)
  • تلاشي ألوان قوس قزح: [0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x26, 0x00]

يعني ذلك بشكل أساسي إضافة طرق setPulseColor وsetRainbow وsetRainbowFade جديدة إلى الفئة PlaybulbCandle واستدعاؤها في changeColor.

حلّ مشكلة "لا يوجد تأثير"

كما لاحظت، لا يؤدي الخيار "بلا تأثير" إلى إعادة ضبط أي تأثير قيد التقدم، وهذا أمر بسيط ولكنّه مهم. لنحلّ هذه المشكلة. في طريقة setColor، عليك أولاً التحقّق مما إذا كان هناك تأثير قيد التقدّم من خلال متغيّر فئة جديد _isEffectSet، وإذا كان true، عليك إيقاف التأثير قبل ضبط لون جديد باستخدام هذه البيانات: [0x00, r, g, b, 0x05, 0x00, 0x01, 0x00].

كتابة اسم الجهاز

هذا السؤال سهل. كتابة اسم جهاز مخصّص بسيطة مثل كتابة اسم جهاز البلوتوث السابق. أنصحك باستخدام طريقة TextEncoder encode للحصول على Uint8Array يتضمّن اسم الجهاز.

بعد ذلك، سأضيف "إدخال" eventListener إلى document.querySelector('#deviceName') وأسمّيه playbulbCandle.setDeviceName لتبسيط الأمر.

لقد سمّيتُ قناتي PLAY💡 CANDLE.

8. هذا كل شيء!

ما تعلّمته

  • كيفية التفاعل مع جهاز يتضمّن بلوتوث مجاور في JavaScript
  • كيفية استخدام فئات ES2015 ودوال الأسهم وMap وPromises

الخطوات التالية