USB ऐप्लिकेशन को वेब पर पोर्ट किया जा रहा है. भाग 1: li एक्सटेंशन

जानें कि बाहरी डिवाइसों के साथ इंटरैक्ट करने वाले कोड को WebAssembly और Fugu API की मदद से, वेब पर कैसे पोर्ट किया जा सकता है.

एक पिछली पोस्ट में, मैंने File System Access API, WebAssembly, और Asyncify की मदद से, फ़ाइल सिस्टम एपीआई का इस्तेमाल करके ऐप्लिकेशन को वेब पर पोर्ट करने का तरीका बताया था. अब मैं WebAssembly के साथ Fugu API को इंटिग्रेट करने और अहम सुविधाओं को खोए बिना ऐप्लिकेशन को वेब पर पोर्ट करने के विषय पर बात करना चाहता हूं.

मैं आपको दिखाऊंगा कि यूएसबी डिवाइसों के साथ कम्यूनिकेट करने वाले ऐप्लिकेशन को वेब पर कैसे पोर्ट किया जा सकता है. इसके लिए, libusb को WebAssembly में पोर्ट किया जाएगा. libusb, C में लिखी गई एक लोकप्रिय यूएसबी लाइब्रेरी है. इसे Emscripten, Asyncify, और WebUSB की मदद से पोर्ट किया जाएगा.

सबसे पहले: एक डेमो

किसी लाइब्रेरी को पोर्ट करते समय सबसे ज़रूरी काम है, सही डेमो चुनना. इससे पोर्ट की गई लाइब्रेरी की क्षमताओं को दिखाया जा सकता है. साथ ही, इसे कई तरीकों से टेस्ट किया जा सकता है और यह देखने में भी आकर्षक लगती है.

मैंने डीएसएलआर रिमोट कंट्रोल का आइडिया चुना. खास तौर पर, ओपन सोर्स प्रोजेक्ट gPhoto2 इस क्षेत्र में काफ़ी समय से काम कर रहा है. यह कई तरह के डिजिटल कैमरों के लिए, रिवर्स-इंजीनियरिंग और सहायता उपलब्ध कराता है. यह कई प्रोटोकॉल के साथ काम करता है. हालांकि, मुझे यूएसबी के साथ काम करने वाले प्रोटोकॉल में सबसे ज़्यादा दिलचस्पी थी. यह libusb के ज़रिए काम करता है.

मैं इस डेमो को बनाने के चरणों को दो हिस्सों में बताऊंगा. इस ब्लॉग पोस्ट में, मैं libusb को पोर्ट करने का तरीका बताऊंगा. साथ ही, यह भी बताऊंगा कि अन्य लोकप्रिय लाइब्रेरी को Fugu API पर पोर्ट करने के लिए, कौनसी तरकीबें ज़रूरी हो सकती हैं. दूसरी पोस्ट में, मैं gPhoto2 को पोर्ट और इंटिग्रेट करने के बारे में ज़्यादा जानकारी दूंगा.

आखिर में, मुझे एक ऐसा वेब ऐप्लिकेशन मिला जो डीएसएलआर से लाइव फ़ीड की झलक दिखाता है और यूएसबी की मदद से उसकी सेटिंग को कंट्रोल कर सकता है. तकनीकी जानकारी पढ़ने से पहले, लाइव या पहले से रिकॉर्ड किया गया डेमो देखें:

डेमो Sony कैमरे से कनेक्ट किए गए लैपटॉप पर चल रहा है.

कैमरे से जुड़ी खास सुविधाओं के बारे में जानकारी

आपने देखा होगा कि वीडियो में सेटिंग बदलने में कुछ समय लगता है. आपको दिखने वाली ज़्यादातर अन्य समस्याओं की तरह, यह समस्या WebAssembly या WebUSB की परफ़ॉर्मेंस की वजह से नहीं होती. इसके बजाय, यह समस्या gPhoto2 के, डेमो के लिए चुने गए कैमरे के साथ इंटरैक्ट करने के तरीके की वजह से होती है.

Sony a6600, आईएसओ, अपर्चर या शटर स्पीड जैसी वैल्यू को सीधे तौर पर सेट करने के लिए कोई एपीआई उपलब्ध नहीं कराता. इसके बजाय, यह सिर्फ़ तय किए गए चरणों की संख्या के हिसाब से, उन्हें बढ़ाने या घटाने के लिए निर्देश देता है. मामले को और मुश्किल बनाने के लिए, यह उन इस्तेमाल की जा सकने वाली वैल्यू की सूची भी नहीं दिखाता जो असल में काम करती हैं. ऐसा लगता है कि Sony के कई कैमरा मॉडल में, दिखाई गई सूची को हार्डकोड किया गया है.

इनमें से कोई एक वैल्यू सेट करते समय, gPhoto2 के पास ये काम करने के अलावा कोई और विकल्प नहीं होता:

  1. चुनी गई वैल्यू के हिसाब से, एक या कुछ कदम आगे बढ़ाएं.
  2. कैमरे की सेटिंग अपडेट होने में थोड़ा समय लग सकता है.
  3. कैमरे ने जिस वैल्यू को कैप्चर किया है उसे पढ़कर सुनाएं.
  4. देखें कि आखिरी चरण में, आपकी पसंद की वैल्यू को छोड़ा न गया हो. साथ ही, सूची के आखिर या शुरुआत में रैप न किया गया हो.
  5. दोहराएं.

इसमें कुछ समय लग सकता है. हालांकि, अगर कैमरा उस वैल्यू के साथ काम करता है, तो वह उस वैल्यू पर सेट हो जाएगा. अगर ऐसा नहीं है, तो वह उस वैल्यू पर सेट हो जाएगा जो उसके सबसे करीब होगी.

अन्य कैमरों में अलग-अलग सेटिंग, एपीआई, और गड़बड़ियां हो सकती हैं. ध्यान रखें कि gPhoto2 एक ओपन-सोर्स प्रोजेक्ट है. इसलिए, सभी कैमरा मॉडल की ऑटोमेटेड या मैन्युअल टेस्टिंग करना मुमकिन नहीं है. इसलिए, समस्या की पूरी जानकारी देने वाली रिपोर्ट और पीआर का हमेशा स्वागत है. हालांकि, पहले आधिकारिक gPhoto2 क्लाइंट के साथ समस्याओं को फिर से जनरेट करना न भूलें.

क्रॉस-प्लैटफ़ॉर्म के साथ काम करने की सुविधा के बारे में ज़रूरी जानकारी

माफ़ करें, Windows पर डीएसएलआर कैमरों जैसे "जाने-माने" डिवाइसों को एक सिस्टम ड्राइवर असाइन किया जाता है. यह ड्राइवर, WebUSB के साथ काम नहीं करता. अगर आपको Windows पर डेमो आज़माना है, तो आपको Zadig जैसे टूल का इस्तेमाल करना होगा. इससे, कनेक्ट किए गए डीएसएलआर के ड्राइवर को WinUSB या libusb में बदला जा सकता है. यह तरीका मेरे और कई अन्य उपयोगकर्ताओं के लिए ठीक से काम करता है. हालांकि, आपको इसे अपने जोखिम पर इस्तेमाल करना चाहिए.

Linux पर, आपको WebUSB के ज़रिए अपने डीएसएलआर को ऐक्सेस करने की अनुमति देने के लिए, कस्टम अनुमतियां सेट करनी पड़ सकती हैं. हालांकि, यह आपके डिस्ट्रिब्यूशन पर निर्भर करता है.

macOS और Android पर, डेमो को बिना किसी अतिरिक्त सेटअप के काम करना चाहिए. अगर इसे Android फ़ोन पर आज़माया जा रहा है, तो लैंडस्केप मोड पर स्विच करना न भूलें. ऐसा इसलिए, क्योंकि मैंने इसे रिस्पॉन्सिव बनाने पर ज़्यादा ध्यान नहीं दिया है. हालांकि, पुल रिक्वेस्ट (पीआर) का स्वागत है!:

यूएसबी-सी केबल से Canon कैमरे से कनेक्ट किया गया Android फ़ोन.
Android फ़ोन पर चल रहा वही डेमो. Surma की खींची गई फ़ोटो.

WebUSB को क्रॉस-प्लैटफ़ॉर्म पर इस्तेमाल करने के बारे में ज़्यादा जानने के लिए, "WebUSB के लिए डिवाइस बनाना" लेख का "प्लैटफ़ॉर्म के हिसाब से ध्यान रखने वाली बातें" सेक्शन देखें.

libusb में नया बैकएंड जोड़ना

अब तकनीकी जानकारी पर आते हैं. libusb जैसा शिम एपीआई उपलब्ध कराना और अन्य ऐप्लिकेशन को इससे लिंक करना मुमकिन है. ऐसा पहले भी किया जा चुका है. हालांकि, इस तरीके में गड़बड़ी होने की संभावना ज़्यादा होती है. साथ ही, इससे आगे के एक्सटेंशन या रखरखाव में ज़्यादा मुश्किल होती है. मैं चीज़ों को सही तरीके से करना चाहता था, ताकि आने वाले समय में उन्हें अपस्ट्रीम में वापस भेजा जा सके और libusb में मर्ज किया जा सके.

खुशकिस्मती से, libusb README में यह बताया गया है:

“libusb को अंदरूनी तौर पर इस तरह से ऐब्स्ट्रैक्ट किया गया है कि इसे अन्य ऑपरेटिंग सिस्टम पर पोर्ट किया जा सकता है. ज़्यादा जानकारी के लिए, कृपया PORTING फ़ाइल देखें.”

libusb को इस तरह से स्ट्रक्चर किया गया है कि सार्वजनिक एपीआई, "बैकएंड" से अलग होता है. ये बैकएंड, ऑपरेटिंग सिस्टम के लो-लेवल एपीआई के ज़रिए डिवाइसों को लिस्ट करने, खोलने, बंद करने, और उनसे कम्यूनिकेट करने के लिए ज़िम्मेदार होते हैं. इस तरह, libusb पहले से ही Linux, macOS, Windows, Android, OpenBSD/NetBSD, Haiku, और Solaris के बीच के अंतर को अलग करता है. साथ ही, इन सभी प्लैटफ़ॉर्म पर काम करता है.

मुझे Emscripten+WebUSB "ऑपरेटिंग सिस्टम" के लिए, एक और बैकएंड जोड़ना पड़ा. उन बैकएंड के लिए लागू किए गए कोड, libusb/os फ़ोल्डर में मौजूद होते हैं:

~/w/d/libusb $ ls libusb/os
darwin_usb.c           haiku_usb_raw.h  threads_posix.lo
darwin_usb.h           linux_netlink.c  threads_posix.o
events_posix.c         linux_udev.c     threads_windows.c
events_posix.h         linux_usbfs.c    threads_windows.h
events_posix.lo        linux_usbfs.h    windows_common.c
events_posix.o         netbsd_usb.c     windows_common.h
events_windows.c       null_usb.c       windows_usbdk.c
events_windows.h       openbsd_usb.c    windows_usbdk.h
haiku_pollfs.cpp       sunos_usb.c      windows_winusb.c
haiku_usb_backend.cpp  sunos_usb.h      windows_winusb.h
haiku_usb.h            threads_posix.c
haiku_usb_raw.cpp      threads_posix.h

हर बैकएंड में, सामान्य टाइप और हेल्पर के साथ libusbi.h हेडर शामिल होता है. साथ ही, इसमें usbi_os_backend टाइप का usbi_backend वैरिएबल दिखाना ज़रूरी होता है. उदाहरण के लिए, Windows बैकएंड कुछ ऐसा दिखता है:

const struct usbi_os_backend usbi_backend = {
  "Windows",
  USBI_CAP_HAS_HID_ACCESS,
  windows_init,
  windows_exit,
  windows_set_option,
  windows_get_device_list,
  NULL,   /* hotplug_poll */
  NULL,   /* wrap_sys_device */
  windows_open,
  windows_close,
  windows_get_active_config_descriptor,
  windows_get_config_descriptor,
  windows_get_config_descriptor_by_value,
  windows_get_configuration,
  windows_set_configuration,
  windows_claim_interface,
  windows_release_interface,
  windows_set_interface_altsetting,
  windows_clear_halt,
  windows_reset_device,
  NULL,   /* alloc_streams */
  NULL,   /* free_streams */
  NULL,   /* dev_mem_alloc */
  NULL,   /* dev_mem_free */
  NULL,   /* kernel_driver_active */
  NULL,   /* detach_kernel_driver */
  NULL,   /* attach_kernel_driver */
  windows_destroy_device,
  windows_submit_transfer,
  windows_cancel_transfer,
  NULL,   /* clear_transfer_priv */
  NULL,   /* handle_events */
  windows_handle_transfer_completion,
  sizeof(struct windows_context_priv),
  sizeof(union windows_device_priv),
  sizeof(struct windows_device_handle_priv),
  sizeof(struct windows_transfer_priv),
};

प्रॉपर्टी को देखने पर, हमें पता चलता है कि स्ट्रक्चर में बैकएंड का नाम, उसकी क्षमताओं का सेट, फ़ंक्शन पॉइंटर के तौर पर अलग-अलग लो-लेवल यूएसबी ऑपरेशन के लिए हैंडलर, और आखिर में, निजी डिवाइस-/कॉन्टेक्स्ट-/ट्रांसफ़र-लेवल डेटा को सेव करने के लिए तय किए गए साइज़ शामिल होते हैं.

निजी डेटा फ़ील्ड, कम से कम उन सभी चीज़ों के लिए ओएस हैंडल को सेव करने के लिए काम के होते हैं. ऐसा इसलिए, क्योंकि हैंडल के बिना हमें यह नहीं पता होता कि कोई कार्रवाई किस आइटम पर लागू होती है. वेब पर लागू करने के लिए, ओएस हैंडल, WebUSB JavaScript ऑब्जेक्ट होंगे. Emscripten में उन्हें दिखाने और सेव करने का सामान्य तरीका, emscripten::val क्लास का इस्तेमाल करना है. यह क्लास, Embind (Emscripten का बाइंडिंग सिस्टम) के हिस्से के तौर पर उपलब्ध कराई जाती है.

फ़ोल्डर में मौजूद ज़्यादातर बैकएंड, C में लागू किए गए हैं. हालांकि, कुछ बैकएंड C++ में लागू किए गए हैं. Embind सिर्फ़ C++ के साथ काम करता है. इसलिए, मैंने C++ को चुना और ज़रूरी स्ट्रक्चर के साथ libusb/libusb/os/emscripten_webusb.cpp को जोड़ा. साथ ही, निजी डेटा फ़ील्ड के लिए sizeof(val) को जोड़ा:

#include <emscripten.h>
#include <emscripten/val.h>

#include "libusbi.h"

using namespace emscripten;

// …function implementations

const usbi_os_backend usbi_backend = {
  .name = "Emscripten + WebUSB backend",
  .caps = LIBUSB_CAP_HAS_CAPABILITY,
  // …handlers—function pointers to implementations above
  .device_priv_size = sizeof(val),
  .transfer_priv_size = sizeof(val),
};

WebUSB ऑब्जेक्ट को डिवाइस हैंडल के तौर पर सेव करना

libusb, निजी डेटा के लिए तय की गई जगह के लिए, इस्तेमाल के लिए तैयार पॉइंटर उपलब्ध कराता है. उन पॉइंटर को val इंस्टेंस के तौर पर इस्तेमाल करने के लिए, मैंने छोटे हेल्पर जोड़े हैं. ये हेल्पर, पॉइंटर को इन-प्लेस बनाते हैं, उन्हें रेफ़रंस के तौर पर वापस लाते हैं, और वैल्यू को बाहर ले जाते हैं:

// We store an Embind handle to WebUSB USBDevice in "priv" metadata of
// libusb device, this helper returns a pointer to it.
struct ValPtr {
 public:
  void init_to(val &&value) { new (ptr) val(std::move(value)); }

  val &get() { return *ptr; }
  val take() { return std::move(get()); }

 protected:
  ValPtr(val *ptr) : ptr(ptr) {}

 private:
  val *ptr;
};

struct WebUsbDevicePtr : ValPtr {
 public:
  WebUsbDevicePtr(libusb_device *dev)
      : ValPtr(static_cast<val *>(usbi_get_device_priv(dev))) {}
};

val &get_web_usb_device(libusb_device *dev) {
  return WebUsbDevicePtr(dev).get();
}

struct WebUsbTransferPtr : ValPtr {
 public:
  WebUsbTransferPtr(usbi_transfer *itransfer)
      : ValPtr(static_cast<val *>(usbi_get_transfer_priv(itransfer))) {}
};

सिंक्रोनस C कॉन्टेक्स्ट में एसिंक्रोनस वेब एपीआई

अब एसिंक्रोनस WebUSB एपीआई को मैनेज करने का तरीका चाहिए, जहां libusb सिंक्रोनस ऑपरेशन की उम्मीद करता है. इसके लिए, Asyncify का इस्तेमाल किया जा सकता है. इसके अलावा, val::await() के ज़रिए इसके Embind इंटिग्रेशन का इस्तेमाल किया जा सकता है.

मुझे WebUSB की गड़बड़ियों को सही तरीके से मैनेज करना था और उन्हें libusb के गड़बड़ी कोड में बदलना था. हालांकि, Embind में फ़िलहाल JavaScript के अपवादों या C++ की ओर से Promise अस्वीकार किए जाने वाले अनुरोधों को मैनेज करने का कोई तरीका नहीं है. इस समस्या को हल करने के लिए, JavaScript साइड पर रिजेक्शन को कैच करें और नतीजे को { error, value } ऑब्जेक्ट में बदलें. इसे अब C++ साइड से सुरक्षित तरीके से पार्स किया जा सकता है. मैंने EM_JS मैक्रो और Emval.to{Handle, Value} एपीआई के कॉम्बिनेशन का इस्तेमाल करके ऐसा किया:

EM_JS(EM_VAL, em_promise_catch_impl, (EM_VAL handle), {
  let promise = Emval.toValue(handle);
  promise = promise.then(
    value => ({error : 0, value}),
    error => {
      const ERROR_CODES = {
        // LIBUSB_ERROR_IO
        NetworkError : -1,
        // LIBUSB_ERROR_INVALID_PARAM
        DataError : -2,
        TypeMismatchError : -2,
        IndexSizeError : -2,
        // LIBUSB_ERROR_ACCESS
        SecurityError : -3,
        …
      };
      console.error(error);
      let errorCode = -99; // LIBUSB_ERROR_OTHER
      if (error instanceof DOMException)
      {
        errorCode = ERROR_CODES[error.name] ?? errorCode;
      }
      else if (error instanceof RangeError || error instanceof TypeError)
      {
        errorCode = -2; // LIBUSB_ERROR_INVALID_PARAM
      }
      return {error: errorCode, value: undefined};
    }
  );
  return Emval.toHandle(promise);
});

val em_promise_catch(val &&promise) {
  EM_VAL handle = promise.as_handle();
  handle = em_promise_catch_impl(handle);
  return val::take_ownership(handle);
}

// C++ struct representation for {value, error} object from above
// (performs conversion in the constructor).
struct promise_result {
  libusb_error error;
  val value;

  promise_result(val &&result)
      : error(static_cast<libusb_error>(result["error"].as<int>())),
        value(result["value"]) {}

  // C++ counterpart of the promise helper above that takes a promise, catches
  // its error, converts to a libusb status and returns the whole thing as
  // `promise_result` struct for easier handling.
  static promise_result await(val &&promise) {
    promise = em_promise_catch(std::move(promise));
    return {promise.await()};
  }
};

अब मैं WebUSB ऑपरेशन से मिले किसी भी Promise पर promise_result::await() का इस्तेमाल कर सकता हूं. साथ ही, इसके error और value फ़ील्ड की अलग-अलग जांच कर सकता हूं.

उदाहरण के लिए, libusb_device_handle से USBDevice को दिखाने वाले val को वापस पाना, उसके open() तरीके को कॉल करना, उसके नतीजे का इंतज़ार करना, और गड़बड़ी के कोड को libusb स्टेटस कोड के तौर पर दिखाना कुछ ऐसा दिखता है:

int em_open(libusb_device_handle *handle) {
  auto web_usb_device = get_web_usb_device(handle->dev);
  return promise_result::await(web_usb_device.call<val>("open")).error;
}

डिवाइस की सूची

किसी भी डिवाइस को खोलने से पहले, libusb को उपलब्ध डिवाइसों की सूची हासिल करनी होगी. बैकएंड को get_device_list हैंडलर के ज़रिए इस कार्रवाई को लागू करना होगा.

दिक्कत यह है कि अन्य प्लैटफ़ॉर्म के उलट, सुरक्षा की वजहों से वेब पर कनेक्ट किए गए सभी यूएसबी डिवाइसों की गिनती करने का कोई तरीका नहीं है. इसके बजाय, फ़्लो को दो हिस्सों में बांटा गया है. सबसे पहले, वेब ऐप्लिकेशन navigator.usb.requestDevice() के ज़रिए, खास प्रॉपर्टी वाले डिवाइसों का अनुरोध करता है. इसके बाद, उपयोगकर्ता मैन्युअल तरीके से चुनता है कि उसे कौनसा डिवाइस दिखाना है या अनुमति के अनुरोध को अस्वीकार करना है. इसके बाद, ऐप्लिकेशन navigator.usb.getDevices() के ज़रिए, पहले से मंज़ूरी पा चुके और कनेक्ट किए गए डिवाइसों की सूची दिखाता है.

सबसे पहले, मैंने get_device_list हैंडलर को लागू करने के लिए, सीधे तौर पर requestDevice() का इस्तेमाल करने की कोशिश की. हालांकि, कनेक्ट किए गए डिवाइसों की सूची के साथ अनुमति का प्रॉम्प्ट दिखाना एक संवेदनशील कार्रवाई माना जाता है. साथ ही, इसे उपयोगकर्ता के इंटरैक्शन (जैसे, पेज पर किसी बटन पर क्लिक करना) से ट्रिगर किया जाना चाहिए. ऐसा न करने पर, यह हमेशा अस्वीकार किया गया प्रॉमिस दिखाता है. libusb ऐप्लिकेशन अक्सर ऐप्लिकेशन के शुरू होने पर, कनेक्ट किए गए डिवाइसों की सूची दिखाना चाहते हैं. इसलिए, requestDevice() का इस्तेमाल करना कोई विकल्प नहीं था.

इसके बजाय, मुझे navigator.usb.requestDevice() को शुरू करने की ज़िम्मेदारी असली डेवलपर पर छोड़नी पड़ी. साथ ही, navigator.usb.getDevices() से पहले से मंज़ूरी पा चुके डिवाइसों को ही एक्सपोज़ करना पड़ा:

// Store the global `navigator.usb` once upon initialisation.
thread_local const val web_usb = val::global("navigator")["usb"];

int em_get_device_list(libusb_context *ctx, discovered_devs **devs) {
  // C++ equivalent of `await navigator.usb.getDevices()`.
  // Note: at this point we must already have some devices exposed -
  // caller must have called `await navigator.usb.requestDevice(...)`
  // in response to user interaction before going to LibUSB.
  // Otherwise this list will be empty.
  auto result = promise_result::await(web_usb.call<val>("getDevices"));
  if (result.error) {
    return result.error;
  }
  auto &web_usb_devices = result.value;
  // Iterate over the exposed devices.
  uint8_t devices_num = web_usb_devices["length"].as<uint8_t>();
  for (uint8_t i = 0; i < devices_num; i++) {
    auto web_usb_device = web_usb_devices[i];
    // …
    *devs = discovered_devs_append(*devs, dev);
  }
  return LIBUSB_SUCCESS;
}

ज़्यादातर बैकएंड कोड, val और promise_result का इस्तेमाल उसी तरह करते हैं जैसा ऊपर दिखाया गया है. डेटा ट्रांसफ़र को मैनेज करने वाले कोड में, कुछ और दिलचस्प हैक मौजूद हैं. हालांकि, इस लेख के मकसद के लिए, उन्हें लागू करने की जानकारी कम अहम है. अगर आपकी दिलचस्पी है, तो GitHub पर कोड और टिप्पणियां ज़रूर देखें.

इवेंट लूप को वेब पर पोर्ट करना

libusb पोर्ट के एक और हिस्से के बारे में मुझे बात करनी है. वह है इवेंट हैंडलिंग. पिछले लेख में बताया गया है कि C जैसी सिस्टम लैंग्वेज में ज़्यादातर एपीआई सिंक्रोनस होते हैं. साथ ही, इवेंट हैंडलिंग भी सिंक्रोनस होती है. आम तौर पर, इसे एक इनफ़ाइनाइट लूप के ज़रिए लागू किया जाता है. यह लूप, बाहरी I/O सोर्स के सेट से "पोल" करता है. इसका मतलब है कि यह डेटा को पढ़ने की कोशिश करता है या डेटा उपलब्ध होने तक प्रोग्राम चलाना को ब्लॉक करता है. जब इनमें से कम से कम एक सोर्स जवाब देता है, तो यह उसे संबंधित हैंडलर को एक इवेंट के तौर पर पास करता है. हैंडलर का काम पूरा होने के बाद, कंट्रोल फिर से लूप पर वापस आ जाता है और अगले पोल के लिए रुक जाता है.

वेब पर इस तरीके का इस्तेमाल करने में कुछ समस्याएं आती हैं.

पहला, WebUSB, डिवाइसों के रॉ हैंडल को एक्सपोज़ नहीं करता और न ही कर सकता है. इसलिए, उन्हें सीधे तौर पर पोल नहीं किया जा सकता. दूसरा, libusb अन्य इवेंट के लिए eventfd और pipe एपीआई का इस्तेमाल करता है. साथ ही, यह बिना रॉ डिवाइस हैंडल वाले ऑपरेटिंग सिस्टम पर ट्रांसफ़र को मैनेज करने के लिए भी इनका इस्तेमाल करता है. हालांकि, Emscripten में फ़िलहाल eventfd काम नहीं करता. इसके अलावा, pipe काम करता है, लेकिन फ़िलहाल यह स्पेसिफ़िकेशन के मुताबिक नहीं है और इवेंट के लिए इंतज़ार नहीं कर सकता.

आखिर में, सबसे बड़ी समस्या यह है कि वेब का अपना इवेंट लूप होता है. इस ग्लोबल इवेंट लूप का इस्तेमाल, किसी भी बाहरी I/O ऑपरेशन के लिए किया जाता है. इनमें fetch(), टाइमर या इस मामले में WebUSB शामिल हैं. साथ ही, जब भी संबंधित ऑपरेशन पूरे होते हैं, तो यह इवेंट या Promise हैंडलर को ट्रिगर करता है. किसी दूसरे, नेस्ट किए गए, इनफ़ाइनाइट इवेंट लूप को लागू करने से, ब्राउज़र का इवेंट लूप आगे नहीं बढ़ पाएगा. इसका मतलब है कि यूज़र इंटरफ़ेस (यूआई) काम करना बंद कर देगा. साथ ही, कोड को उन I/O इवेंट के लिए कभी सूचनाएं नहीं मिलेंगी जिनका वह इंतज़ार कर रहा है. आम तौर पर, इससे डेडलॉक की स्थिति पैदा हो जाती है. ऐसा तब हुआ, जब मैंने डेमो में libusb का इस्तेमाल करने की कोशिश की. पेज फ़्रीज़ हो गया.

ब्लॉक करने वाले अन्य I/O की तरह, ऐसे इवेंट लूप को वेब पर पोर्ट करने के लिए, डेवलपर को मुख्य थ्रेड को ब्लॉक किए बिना उन लूप को चलाने का तरीका ढूंढना होगा. एक तरीका यह है कि ऐप्लिकेशन को रीफ़ैक्टर करके, I/O इवेंट को अलग थ्रेड में मैनेज किया जाए और नतीजों को मुख्य थ्रेड में वापस भेजा जाए. दूसरा तरीका है, Asyncify का इस्तेमाल करके लूप को रोकना और इवेंट के लिए इंतज़ार करना. इससे इवेंट को ब्लॉक नहीं किया जाता.

मुझे libusb या gPhoto2 में कोई अहम बदलाव नहीं करना था. साथ ही, मैंने Promise इंटिग्रेशन के लिए पहले से ही Asyncify का इस्तेमाल किया था. इसलिए, मैंने यही तरीका चुना. poll() के ब्लॉक करने वाले वैरिएंट को सिम्युलेट करने के लिए, मैंने शुरुआती सिद्धांत के प्रमाण के तौर पर एक लूप का इस्तेमाल किया है. इसे यहां दिखाया गया है:

#ifdef __EMSCRIPTEN__
  // TODO: optimize this. Right now it will keep unwinding-rewinding the stack
  // on each short sleep until an event comes or the timeout expires.
  // We should probably create an actual separate thread that does signaling
  // or come up with a custom event mechanism to report events from
  // `usbi_signal_event` and process them here.
  double until_time = emscripten_get_now() + timeout_ms;
  do {
    // Emscripten `poll` ignores timeout param, but pass 0 explicitly just
    // in case.
    num_ready = poll(fds, nfds, 0);
    if (num_ready != 0) break;
    // Yield to the browser event loop to handle events.
    emscripten_sleep(0);
  } while (emscripten_get_now() < until_time);
#else
  num_ready = poll(fds, nfds, timeout_ms);
#endif

यह क्या करता है:

  1. यह poll() को कॉल करता है, ताकि यह पता लगाया जा सके कि बैकएंड ने अब तक कोई इवेंट रिपोर्ट किया है या नहीं. अगर कुछ गड़बड़ियां हैं, तो लूप रुक जाता है. ऐसा न होने पर, Emscripten का poll() लागू करने का तरीका, तुरंत 0 के साथ वापस आ जाएगा.
  2. emscripten_sleep(0) को कॉल करें. यह फ़ंक्शन, बैकग्राउंड में Asyncify और setTimeout() का इस्तेमाल करता है. इसका इस्तेमाल यहां मुख्य ब्राउज़र इवेंट लूप को कंट्रोल वापस देने के लिए किया जाता है. इससे ब्राउज़र, उपयोगकर्ता के किसी भी इंटरैक्शन और I/O इवेंट को मैनेज कर सकता है. इसमें WebUSB भी शामिल है.
  3. देखें कि तय किया गया टाइम आउट खत्म हो गया है या नहीं. अगर नहीं, तो लूप जारी रखें.

जैसा कि टिप्पणी में बताया गया है, यह तरीका सही नहीं था. इसकी वजह यह थी कि यह Asyncify के साथ पूरे कॉल स्टैक को सेव और वापस लाता रहता था. भले ही, हैंडल करने के लिए कोई यूएसबी इवेंट न हो (जो कि ज़्यादातर समय होता है). साथ ही, इसकी वजह यह भी थी कि मॉडर्न ब्राउज़र में setTimeout() की कम से कम अवधि चार मिलीसेकंड होती है. इसके बावजूद, सिद्धांत के प्रमाण के तौर पर, डीएसएलआर से 13 से 14 एफ़पीएस की लाइव स्ट्रीम करने के लिए यह काफ़ी कारगर साबित हुआ.

बाद में, मैंने ब्राउज़र इवेंट सिस्टम का इस्तेमाल करके इसे बेहतर बनाने का फ़ैसला किया. इस तरीके को बेहतर बनाने के कई तरीके हैं. हालांकि, फ़िलहाल मैंने कस्टम इवेंट को सीधे ग्लोबल ऑब्जेक्ट पर भेजने का विकल्प चुना है. इसके लिए, उन्हें किसी खास libusb डेटा स्ट्रक्चर से नहीं जोड़ा गया है. मैंने EM_ASYNC_JS मैक्रो के आधार पर, इंतज़ार करने और सूचना देने के तरीके का इस्तेमाल करके ऐसा किया है:

EM_JS(void, em_libusb_notify, (void), {
  dispatchEvent(new Event("em-libusb"));
});

EM_ASYNC_JS(int, em_libusb_wait, (int timeout), {
  let onEvent, timeoutId;

  try {
    return await new Promise(resolve => {
      onEvent = () => resolve(0);
      addEventListener('em-libusb', onEvent);

      timeoutId = setTimeout(resolve, timeout, -1);
    });
  } finally {
    removeEventListener('em-libusb', onEvent);
    clearTimeout(timeoutId);
  }
});

em_libusb_notify() फ़ंक्शन का इस्तेमाल तब किया जाता है, जब libusb किसी इवेंट की रिपोर्ट करने की कोशिश करता है. जैसे, डेटा ट्रांसफ़र पूरा होना:

void usbi_signal_event(usbi_event_t *event)
{
  uint64_t dummy = 1;
  ssize_t r;

  r = write(EVENT_WRITE_FD(event), &dummy, sizeof(dummy));
  if (r != sizeof(dummy))
    usbi_warn(NULL, "event write failed");
#ifdef __EMSCRIPTEN__
  em_libusb_notify();
#endif
}

इस बीच, em_libusb_wait() वाले हिस्से का इस्तेमाल, Asyncify के स्लीप मोड से "बाहर निकलने" के लिए किया जाता है. ऐसा तब होता है, जब em-libusb इवेंट मिलता है या टाइम आउट खत्म हो जाता है:

double until_time = emscripten_get_now() + timeout_ms;
for (;;) {
  // Emscripten `poll` ignores timeout param, but pass 0 explicitly just
  // in case.
  num_ready = poll(fds, nfds, 0);
  if (num_ready != 0) break;
  int timeout = until_time - emscripten_get_now();
  if (timeout <= 0) break;
  int result = em_libusb_wait(timeout);
  if (result != 0) break;
}

स्लीप और वेक-अप की संख्या में काफ़ी कमी आने की वजह से, इस तरीके से emscripten_sleep() पर आधारित पिछले तरीके की परफ़ॉर्मेंस से जुड़ी समस्याएं ठीक हो गईं. साथ ही, डीएसएलआर डेमो का थ्रूपुट 13-14 FPS से बढ़कर 30+ FPS हो गया. यह स्मूद लाइव फ़ीड के लिए काफ़ी है.

सिस्टम बनाना और पहला टेस्ट

बैकएंड का काम पूरा होने के बाद, मुझे इसे Makefile.am और configure.ac में जोड़ना पड़ा. यहां सिर्फ़ Emscripten के लिए फ़्लैग में बदलाव करना दिलचस्प है:

emscripten)
  AC_SUBST(EXEEXT, [.html])
  # Note: LT_LDFLAGS is not enough here because we need link flags for executable.
  AM_LDFLAGS="${AM_LDFLAGS} --bind -s ASYNCIFY -s ASSERTIONS -s ALLOW_MEMORY_GROWTH -s INVOKE_RUN=0 -s EXPORTED_RUNTIME_METHODS=['callMain']"
  ;;

पहली बात, Unix प्लैटफ़ॉर्म पर मौजूद एक्ज़ीक्यूटेबल फ़ाइलों में आम तौर पर फ़ाइल एक्सटेंशन नहीं होते. हालांकि, Emscripten आपके अनुरोध किए गए एक्सटेंशन के आधार पर अलग-अलग आउटपुट देता है. मैं AC_SUBST(EXEEXT, …) का इस्तेमाल करके, एक्ज़ीक्यूटेबल एक्सटेंशन को .html में बदल रहा हूं. इससे पैकेज में मौजूद कोई भी एक्ज़ीक्यूटेबल—टेस्ट और उदाहरण—Emscripten के डिफ़ॉल्ट शेल के साथ एचटीएमएल बन जाता है. यह शेल, JavaScript और WebAssembly को लोड करने और इंस्टैंशिएट करने का काम करता है.

दूसरा, Embind और Asyncify का इस्तेमाल करने की वजह से, मुझे उन सुविधाओं (--bind -s ASYNCIFY) को चालू करना होगा. साथ ही, लिंकर पैरामीटर की मदद से डाइनैमिक मेमोरी ग्रोथ (-s ALLOW_MEMORY_GROWTH) की अनुमति देनी होगी. माफ़ करें, लाइब्रेरी के पास लिंकर को उन फ़्लैग की जानकारी देने का कोई तरीका नहीं है. इसलिए, इस libusb पोर्ट का इस्तेमाल करने वाले हर ऐप्लिकेशन को अपने बिल्ड कॉन्फ़िगरेशन में भी वही लिंकर फ़्लैग जोड़ने होंगे.

आखिर में, जैसा कि पहले बताया गया है, WebUSB को उपयोगकर्ता के जेस्चर की मदद से डिवाइस की पहचान करने की ज़रूरत होती है. libusb के उदाहरणों और टेस्ट में यह माना जाता है कि वे स्टार्ट-अप के समय डिवाइसों की पहचान कर सकते हैं. साथ ही, बिना किसी बदलाव के गड़बड़ी की वजह से काम नहीं करते. इसके बजाय, मुझे अपने-आप लागू होने की सुविधा (-s INVOKE_RUN=0) बंद करनी पड़ी और मैन्युअल तरीके से callMain() लागू करने का तरीका (-s EXPORTED_RUNTIME_METHODS=...) दिखाना पड़ा.

यह सब करने के बाद, जनरेट की गई फ़ाइलों को स्टैटिक वेब सर्वर से दिखाया जा सकता था. साथ ही, WebUSB को शुरू किया जा सकता था और DevTools की मदद से, एचटीएमएल की उन एक्ज़ीक्यूटेबल फ़ाइलों को मैन्युअल तरीके से चलाया जा सकता था.

स्क्रीनशॉट में Chrome विंडो दिख रही है. इसमें, स्थानीय तौर पर उपलब्ध `testlibusb` पेज पर DevTools खुला हुआ है. DevTools कंसोल, `navigator.usb.requestDevice({ filters: [] })` का आकलन कर रहा है. इससे अनुमति का प्रॉम्प्ट ट्रिगर हुआ है. फ़िलहाल, यह उपयोगकर्ता से ऐसा यूएसबी डिवाइस चुनने के लिए कह रहा है जिसे पेज के साथ शेयर किया जाना चाहिए. फ़िलहाल, ILCE-6600 (Sony का कैमरा) चुना गया है.

DevTools खुले होने पर, अगले चरण का स्क्रीनशॉट. डिवाइस चुनने के बाद, Console ने एक नए एक्सप्रेशन `Module.callMain(['-v'])` का आकलन किया. इससे `testlibusb` ऐप्लिकेशन, ज़्यादा शब्दों में जानकारी देने वाले मोड में चालू हो गया. आउटपुट में, पहले कनेक्ट किए गए यूएसबी कैमरे के बारे में कई तरह की जानकारी दिखती है: मैन्युफ़ैक्चरर Sony, प्रॉडक्ट ILCE-6600, सीरियल नंबर, कॉन्फ़िगरेशन वगैरह.

यह बहुत ज़्यादा नहीं लगता, लेकिन किसी नए प्लैटफ़ॉर्म पर लाइब्रेरी पोर्ट करते समय, पहली बार मान्य आउटपुट मिलने की स्टेज तक पहुंचना काफ़ी रोमांचक होता है!

पोर्ट का इस्तेमाल करना

जैसा कि ऊपर बताया गया है, पोर्ट Emscripten की कुछ सुविधाओं पर निर्भर करता है. फ़िलहाल, इन सुविधाओं को ऐप्लिकेशन के लिंकिंग स्टेज पर चालू करना ज़रूरी है. अगर आपको अपने ऐप्लिकेशन में इस libusb पोर्ट का इस्तेमाल करना है, तो आपको यह तरीका अपनाना होगा:

  1. अपने बिल्ड के हिस्से के तौर पर, libusb का नया वर्शन डाउनलोड करें या इसे अपने प्रोजेक्ट में git सबमॉड्यूल के तौर पर जोड़ें.
  2. libusb फ़ोल्डर में autoreconf -fiv चलाएं.
  3. प्रोजेक्ट को क्रॉस-कंपाइलेशन के लिए शुरू करने और बनाए गए आर्टफ़ैक्ट को सेव करने के लिए पाथ सेट करने के लिए, emconfigure ./configure –host=wasm32 –prefix=/some/installation/path चलाएं.
  4. emmake make install चलाएं.
  5. अपने ऐप्लिकेशन या हायर-लेवल लाइब्रेरी को, पहले से चुने गए पाथ में libusb खोजने के लिए कहें.
  6. अपने ऐप्लिकेशन के लिंक आर्ग्युमेंट में ये फ़्लैग जोड़ें: --bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH.

फ़िलहाल, लाइब्रेरी में कुछ सीमाएं हैं:

  • ट्रांसफ़र रद्द करने की सुविधा उपलब्ध नहीं है. यह WebUSB की एक सीमा है. यह libusb में क्रॉस-प्लैटफ़ॉर्म ट्रांसफ़र को रद्द करने की सुविधा न होने की वजह से है.
  • आइसोक्रोनस ट्रांसफ़र की सुविधा उपलब्ध नहीं है. इसे जोड़ने के लिए, मौजूदा ट्रांसफ़र मोड को लागू करने के तरीके को उदाहरण के तौर पर इस्तेमाल किया जा सकता है. हालांकि, यह मोड बहुत कम इस्तेमाल किया जाता है और मेरे पास इसे टेस्ट करने के लिए कोई डिवाइस नहीं था. इसलिए, फ़िलहाल मैंने इसे 'काम नहीं करता' के तौर पर छोड़ दिया है. अगर आपके पास ऐसे डिवाइस हैं और आपको लाइब्रेरी में योगदान देना है, तो पीआर का स्वागत है!
  • क्रॉस-प्लैटफ़ॉर्म से जुड़ी वे सीमाएं जिनके बारे में पहले बताया गया है. ये पाबंदियां ऑपरेटिंग सिस्टम की वजह से लागू होती हैं. इसलिए, हम यहां ज़्यादा कुछ नहीं कर सकते. हालांकि, हम उपयोगकर्ताओं से ड्राइवर या अनुमतियों को बदलने के लिए कह सकते हैं. हालांकि, अगर आपको एचआईडी या सीरियल डिवाइसों को पोर्ट करना है, तो libusb के उदाहरण का इस्तेमाल करके, किसी अन्य लाइब्रेरी को किसी दूसरे Fugu API पर पोर्ट किया जा सकता है. उदाहरण के लिए, C लाइब्रेरी hidapi को WebHID पर पोर्ट किया जा सकता है. साथ ही, लो-लेवल यूएसबी ऐक्सेस से जुड़ी समस्याओं को पूरी तरह से हल किया जा सकता है.

नतीजा

इस पोस्ट में मैंने दिखाया है कि Emscripten, Asyncify, और Fugu API की मदद से, libusb जैसी लो-लेवल लाइब्रेरी को भी इंटिग्रेशन की कुछ तरकीबों के साथ वेब पर पोर्ट किया जा सकता है.

इस तरह की ज़रूरी और बड़े पैमाने पर इस्तेमाल की जाने वाली लो-लेवल लाइब्रेरी को पोर्ट करना काफ़ी फ़ायदेमंद होता है. ऐसा इसलिए, क्योंकि इससे हाई-लेवल लाइब्रेरी या पूरे ऐप्लिकेशन को भी वेब पर लाया जा सकता है. इससे, पहले एक या दो प्लैटफ़ॉर्म तक सीमित रहने वाले अनुभव, अब सभी तरह के डिवाइसों और ऑपरेटिंग सिस्टम पर उपलब्ध हो जाते हैं. साथ ही, ये अनुभव सिर्फ़ एक लिंक पर क्लिक करके हासिल किए जा सकते हैं.

अगली पोस्ट में, हम वेब gPhoto2 डेमो बनाने के चरणों के बारे में बताएंगे. यह डेमो, न सिर्फ़ डिवाइस की जानकारी हासिल करता है, बल्कि libusb की ट्रांसफ़र सुविधा का भी बड़े पैमाने पर इस्तेमाल करता है. इस बीच, हमें उम्मीद है कि आपको libusb का उदाहरण पसंद आया होगा. साथ ही, आपको डेमो आज़माने, लाइब्रेरी के साथ एक्सपेरिमेंट करने या फिर किसी दूसरी लोकप्रिय लाइब्रेरी को Fugu एपीआई में से किसी एक पर पोर्ट करने की प्रेरणा मिली होगी.