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

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

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

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

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

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

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

मैं इस डेमो को बनाने का तरीका दो हिस्सों में बताऊंगा. इस ब्लॉग पोस्ट में, मैं बताऊंगा कि मैंने खुद को liBsb दूसरी पोस्ट में, मुझे gPhoto2 को पोर्ट करने और इंटिग्रेट करने के बारे में जानकारी मिलेगी.

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

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

कैमरे की खास सुविधाओं के बारे में नोट

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

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

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

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

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

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

क्रॉस-प्लैटफ़ॉर्म के साथ काम करने से जुड़ी ज़रूरी जानकारी

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

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

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

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

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

लिबसब में नया बैकएंड जोड़ना

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

अच्छी बात यह है कि लिबसब README कहता है:

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

libasb को इस तरह से तैयार किया गया है कि जिसमें सार्वजनिक एपीआई, "बैकएंड" से अलग हो. ये बैकएंड, ऑपरेटिंग सिस्टम के लो-लेवल एपीआई की मदद से डिवाइसों की सूची बनाने, उन्हें खोलने, बंद करने, और उनसे असल में कनेक्ट करने के लिए ज़िम्मेदार होते हैं. इस तरह libasb से, Linux, macOS, Windows, Android, OpenBSD/NetBSD, हाइकू, और 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 ऑब्जेक्ट को डिवाइस के हैंडल के तौर पर सेव करना

libasb, निजी डेटा के लिए तय किए गए हिस्से के लिए इस्तेमाल के लिए तैयार पॉइंटर देता है. उन पॉइंटर के साथ 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 की गड़बड़ियों को सही तरीके से हैंडल करना और उन्हें लिबस गड़बड़ी कोड में बदलना था. हालांकि, 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() तरीके को कॉल करना, उसके नतीजे का इंतज़ार करना, और लिबब स्टेटस कोड के रूप में गड़बड़ी का कोड दिखाना इस तरह दिखता है:

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

डिवाइस की गिनती

निश्चित तौर पर, किसी भी डिवाइस को खोलने से पहले, liBb को उपलब्ध डिवाइसों की सूची फिर से हासिल करनी होगी. बैकएंड को यह कार्रवाई, get_device_list हैंडलर की मदद से लागू करनी होगी.

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

पहले मैंने get_device_list हैंडलर को लागू करने के दौरान, सीधे तौर पर requestDevice() का इस्तेमाल करने की कोशिश की. हालांकि, कनेक्ट किए गए डिवाइसों की सूची के साथ अनुमति के अनुरोध को दिखाने को संवेदनशील कार्रवाई माना जाता है. इसे, उपयोगकर्ता के इंटरैक्शन (जैसे, पेज पर किसी बटन पर क्लिक करना) से ट्रिगर होना चाहिए. ऐसा न होने पर, हमेशा अस्वीकार किया गया प्रॉमिस दिखता है. हो सकता है कि libb ऐप्लिकेशन, ऐप्लिकेशन शुरू होने पर कनेक्ट किए गए डिवाइस की सूची बनाना चाहें. इसलिए, 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 पर कोड और टिप्पणियों को देखना न भूलें.

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

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

वेब पर इस तरीके में कई समस्याएं हैं.

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

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

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

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

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

बाद में, मैंने ब्राउज़र इवेंट सिस्टम की मदद से इसे बेहतर बनाने का फ़ैसला लिया. इस लागू करने के कई तरीके को और बेहतर बनाया जा सकता है. हालांकि, फ़िलहाल मैंने कस्टम इवेंट को किसी खास लिबब डेटा स्ट्रक्चर से जोड़े बिना, सीधे ग्लोबल ऑब्जेक्ट पर निकलने का विकल्प चुना है. मैंने नीचे दिए गए इंतज़ार और 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);
  }
});

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

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() वाले हिस्से का इस्तेमाल "जागने" के लिए किया जाता है 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)
  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 आपके अनुरोध किए गए एक्सटेंशन के आधार पर अलग-अलग आउटपुट देता है. मैं एक्ज़ीक्यूटेबल एक्सटेंशन को .html में बदलने के लिए AC_SUBST(EXEEXT, …) का उपयोग कर रहा/रही हूं, ताकि पैकेज—टेस्ट और उदाहरण—में जो भी एक्ज़ीक्यूटेबल है, वह Emscripten के डिफ़ॉल्ट शेल वाला एचटीएमएल बन जाए और JavaScript और WebAssembly को लोड और इंस्टैंशिएट करने का काम करे.

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

अंत में, जैसा कि पहले बताया गया है, WebUSB के लिए उपयोगकर्ता के जेस्चर (हाव-भाव) के ज़रिए डिवाइस की गिनती करना ज़रूरी है. li बसb के उदाहरणों और टेस्ट का मानना है कि स्टार्ट-अप होने पर, डिवाइसों की गिनती की जा सकती है और वे बिना किसी बदलाव के गड़बड़ी से फ़ेल हो जाते हैं. इसके बजाय, मुझे ऑटोमैटिक एक्ज़ीक्यूशन (-s INVOKE_RUN=0) को बंद करना पड़ा और मैन्युअल callMain() तरीके (-s EXPORTED_RUNTIME_METHODS=...) को सार्वजनिक करना पड़ा.

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

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

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

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

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

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

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

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

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

नतीजा

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

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

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