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

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

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

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

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

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

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

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

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

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

कैमरे से जुड़ी समस्याओं के बारे में जानकारी

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

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

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

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

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

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

अलग-अलग प्लैटफ़ॉर्म के साथ काम करने के बारे में अहम जानकारी

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

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

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

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

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

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

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

हालांकि, libusb README में बताया गया है कि:

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

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++ के साथ काम करता है. इसलिए, यह विकल्प मेरे लिए चुना गया था. मैंने ज़रूरी स्ट्रक्चर के साथ 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 API को मैनेज किया जा सके, जहां libusb को सिंक्रोनस ऑपरेशन की उम्मीद होती है. इसके लिए, Asyncify या खास तौर पर, val::await() के ज़रिए Embind इंटिग्रेशन का इस्तेमाल किया जा सकता है.

मुझे WebUSB से जुड़ी गड़बड़ियों को सही तरीके से मैनेज करना था और उन्हें libusb गड़बड़ी कोड में बदलना था. हालांकि, फ़िलहाल Embind में, C++ साइड से JavaScript अपवाद या 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() का इस्तेमाल करता है. इसका इस्तेमाल, मुख्य ब्राउज़र इवेंट लूप को फिर से कंट्रोल करने के लिए किया जाता है. इससे ब्राउज़र, WebUSB के साथ-साथ उपयोगकर्ता के किसी भी इंटरैक्शन और I/O इवेंट को मैनेज कर सकता है.
  3. देखें कि तय की गई टाइम आउट की समयसीमा खत्म हो गई है या नहीं. अगर नहीं, तो लूप जारी रखें.

जैसा कि टिप्पणी में बताया गया है, यह तरीका सबसे सही नहीं था. ऐसा इसलिए, क्योंकि Asyncify की मदद से पूरे कॉल स्टैक को सेव और वापस लाने की प्रोसेस तब भी चलती रहती थी, जब कोई यूएसबी इवेंट नहीं होता था. ऐसा ज़्यादातर समय होता है. साथ ही, आधुनिक ब्राउज़र में setTimeout() की प्रोसेस पूरी होने में कम से कम 4 मिलीसेकंड लगते हैं. इसके बावजूद, यह सुविधा इतनी अच्छी थी कि इसकी मदद से, डीएसएलआर से 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([&#39;-v&#39;])` का आकलन किया. इस एक्सप्रेशन ने `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 में क्रॉस-प्लैटफ़ॉर्म ट्रांसफ़र रद्द करने की सुविधा न होने की वजह से है.
  • आइसोक्रोनस ट्रांसफ़र की सुविधा उपलब्ध नहीं है. उदाहरण के तौर पर, ट्रांसफ़र के मौजूदा मोड को लागू करके, इसे जोड़ना मुश्किल नहीं होना चाहिए. हालांकि, यह एक ऐसा मोड है जो बहुत कम इस्तेमाल किया जाता है. साथ ही, मेरे पास इसका टेस्ट करने के लिए कोई डिवाइस नहीं है. इसलिए, फ़िलहाल मैंने इसे 'काम नहीं करता' के तौर पर सेट किया है. अगर आपके पास ऐसे डिवाइस हैं और आपको लाइब्रेरी में योगदान देना है, तो हमें PR भेजें!
  • पहले बताई गई, अलग-अलग प्लैटफ़ॉर्म पर काम करने से जुड़ी सीमाएं. ये पाबंदियां ऑपरेटिंग सिस्टम लगाते हैं. इसलिए, हम यहां कुछ नहीं कर सकते. हालांकि, हम उपयोगकर्ताओं से ड्राइवर या अनुमतियों को बदलने के लिए कह सकते हैं. हालांकि, अगर एचआईडी या सीरियल डिवाइसों को पोर्ट किया जा रहा है, तो libusb के उदाहरण का पालन करें और किसी अन्य लाइब्रेरी को किसी दूसरे Fugu API पर पोर्ट करें. उदाहरण के लिए, hidapi को WebHID में पोर्ट किया जा सकता है. इससे, यूएसबी के लो-लेवल ऐक्सेस से जुड़ी समस्याओं से पूरी तरह से बचा जा सकता है.

नतीजा

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

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

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