ওয়েবে USB অ্যাপ্লিকেশন পোর্টিং। পার্ট 1: libusb

বাহ্যিক ডিভাইসের সাথে ইন্টারঅ্যাক্ট করে এমন কোড কিভাবে WebAssembly এবং Fugu API-এর মাধ্যমে ওয়েবে পোর্ট করা যায় তা জানুন।

আগের একটি পোস্টে , আমি দেখিয়েছিলাম কিভাবে ফাইল সিস্টেম অ্যাকসেস API , WebAssembly এবং Asyncify দিয়ে ওয়েবে ফাইল সিস্টেম API ব্যবহার করে অ্যাপ পোর্ট করতে হয়। এখন আমি WebAssembly এর সাথে Fugu API গুলিকে একীভূত করার এবং গুরুত্বপূর্ণ বৈশিষ্ট্যগুলি না হারিয়ে অ্যাপগুলিকে ওয়েবে পোর্ট করার একই বিষয় চালিয়ে যেতে চাই৷

আমি দেখাব কিভাবে ইউএসবি ডিভাইসের সাথে যোগাযোগ করে এমন অ্যাপগুলিকে ওয়েবে পোর্ট করে libusb— সি-তে লেখা একটি জনপ্রিয় ইউএসবি লাইব্রেরি যা WebAssembly ( Emscripten এর মাধ্যমে), Asyncify এবং WebUSB পোর্ট করে ওয়েবে পোর্ট করা যায়।

প্রথম জিনিস প্রথম: একটি ডেমো

একটি লাইব্রেরি পোর্ট করার সময় সবচেয়ে গুরুত্বপূর্ণ কাজটি হল সঠিক ডেমো বেছে নেওয়া—এমন কিছু যা পোর্ট করা লাইব্রেরির ক্ষমতা প্রদর্শন করবে, আপনাকে এটি বিভিন্ন উপায়ে পরীক্ষা করার অনুমতি দেবে এবং একই সময়ে দৃশ্যত বাধ্য করবে৷

আমি যে ধারণাটি বেছে নিয়েছিলাম তা ছিল ডিএসএলআর রিমোট কন্ট্রোল। বিশেষ করে, একটি ওপেন সোর্স প্রজেক্ট gPhoto2 এই জায়গাটিতে অনেকদিন ধরেই রয়েছে রিভার্স-ইঞ্জিনিয়ার এবং বিভিন্ন ধরনের ডিজিটাল ক্যামেরার জন্য সমর্থন বাস্তবায়ন করতে। এটি বেশ কয়েকটি প্রোটোকল সমর্থন করে, তবে আমি যেটিতে সবচেয়ে বেশি আগ্রহী ছিলাম তা হল USB সমর্থন, যা এটি libusb এর মাধ্যমে সম্পাদন করে।

আমি দুটি অংশে এই ডেমো নির্মাণের পদক্ষেপগুলি বর্ণনা করব। এই ব্লগ পোস্টে, আমি বর্ণনা করব কিভাবে আমি libusb নিজেই পোর্ট করেছি, এবং অন্যান্য জনপ্রিয় লাইব্রেরিগুলিকে Fugu API-এ পোর্ট করার জন্য কী কী কৌশল প্রয়োজন হতে পারে। দ্বিতীয় পোস্টে , আমি gPhoto2 নিজেই পোর্টিং এবং ইন্টিগ্রেট করার বিষয়ে বিস্তারিত জানাব।

শেষ পর্যন্ত, আমি একটি কার্যকরী ওয়েব অ্যাপ্লিকেশন পেয়েছি যা একটি DSLR থেকে লাইভ ফিডের পূর্বরূপ দেখায় এবং USB এর মাধ্যমে এর সেটিংস নিয়ন্ত্রণ করতে পারে। প্রযুক্তিগত বিবরণ পড়ার আগে লাইভ বা প্রাক-রেকর্ড করা ডেমোটি নির্দ্বিধায় দেখুন:

একটি Sony ক্যামেরার সাথে সংযুক্ত একটি ল্যাপটপে চলমান ডেমো

ক্যামেরা-নির্দিষ্ট quirks উপর নোট

আপনি হয়তো লক্ষ্য করেছেন যে ভিডিওতে সেটিংস পরিবর্তন করতে কিছু সময় লাগে। আপনি দেখতে পারেন অন্যান্য সমস্যাগুলির মতো, এটি WebAssembly বা WebUSB-এর কার্যকারিতার কারণে নয়, তবে ডেমোর জন্য নির্বাচিত নির্দিষ্ট ক্যামেরার সাথে gPhoto2 কীভাবে ইন্টারঅ্যাক্ট করে।

Sony a6600 সরাসরি ISO, অ্যাপারচার বা শাটার স্পীডের মত মান সেট করার জন্য একটি API প্রকাশ করে না এবং এর পরিবর্তে শুধুমাত্র নির্দিষ্ট সংখ্যক ধাপে সেগুলিকে বাড়ানো বা কমানোর জন্য কমান্ড প্রদান করে। বিষয়গুলিকে আরও জটিল করার জন্য, এটি প্রকৃতপক্ষে সমর্থিত মানগুলির একটি তালিকা ফেরত দেয় না, হয় - প্রত্যাবর্তিত তালিকাটি অনেক সনি ক্যামেরা মডেল জুড়ে হার্ডকোডযুক্ত বলে মনে হয়।

এই মানগুলির একটি সেট করার সময়, gPhoto2 এর অন্য কোন বিকল্প নেই:

  1. নির্বাচিত মানের দিকে একটি পদক্ষেপ (বা কয়েকটি) করুন।
  2. ক্যামেরার সেটিংস আপডেট করার জন্য একটু অপেক্ষা করুন।
  3. ক্যামেরাটি আসলে যে মূল্য দিয়েছিল তা পড়ুন।
  4. পরীক্ষা করুন যে শেষ ধাপটি পছন্দসই মানের উপর লাফ দেয়নি বা তালিকার শেষ বা শুরুতে মোড়ানো হয়নি।
  5. পুনরাবৃত্তি করুন।

এটি কিছু সময় নিতে পারে, তবে মানটি যদি ক্যামেরা দ্বারা সমর্থিত হয় তবে এটি সেখানে পৌঁছে যাবে এবং যদি না হয় তবে এটি নিকটতম সমর্থিত মানটিতে থামবে৷

অন্যান্য ক্যামেরায় সম্ভবত বিভিন্ন সেটের সেটিংস, অন্তর্নিহিত APIs এবং quirks থাকবে। মনে রাখবেন যে gPhoto2 হল একটি ওপেন-সোর্স প্রজেক্ট, এবং সেখানে সমস্ত ক্যামেরা মডেলের স্বয়ংক্রিয় বা ম্যানুয়াল পরীক্ষা করা সহজভাবে সম্ভব নয়, তাই বিস্তারিত ইস্যু রিপোর্ট এবং পিআর সর্বদা স্বাগত জানাই (তবে আধিকারিকদের সাথে সমস্যাগুলি পুনরুত্পাদন করা নিশ্চিত করুন) gPhoto2 ক্লায়েন্ট প্রথমে)।

গুরুত্বপূর্ণ ক্রস-প্ল্যাটফর্ম সামঞ্জস্যপূর্ণ নোট

দুর্ভাগ্যবশত, Windows-এ DSLR ক্যামেরা সহ যেকোনো "পরিচিত" ডিভাইসে একটি সিস্টেম ড্রাইভার বরাদ্দ করা হয়, যা WebUSB-এর সাথে সামঞ্জস্যপূর্ণ নয়। আপনি যদি উইন্ডোজে ডেমোটি চেষ্টা করতে চান, তাহলে আপনাকে WinUSB বা libusb-এ সংযুক্ত DSLR-এর ড্রাইভারকে ওভাররাইড করতে Zadig- এর মতো একটি টুল ব্যবহার করতে হবে। এই পদ্ধতিটি আমার এবং অন্যান্য অনেক ব্যবহারকারীর জন্য ভাল কাজ করে, তবে আপনার নিজের ঝুঁকিতে এটি ব্যবহার করা উচিত।

Linux-এ, WebUSB-এর মাধ্যমে আপনার DSLR-এ অ্যাক্সেসের অনুমতি দেওয়ার জন্য আপনাকে সম্ভবত কাস্টম অনুমতি সেট করতে হবে, যদিও এটি আপনার বিতরণের উপর নির্ভর করে।

MacOS এবং Android এ, ডেমোটি বাক্সের বাইরে কাজ করা উচিত। আপনি যদি এটি একটি অ্যান্ড্রয়েড ফোনে চেষ্টা করে থাকেন তবে ল্যান্ডস্কেপ মোডে স্যুইচ করতে ভুলবেন না কারণ আমি এটিকে প্রতিক্রিয়াশীল করার জন্য খুব বেশি প্রচেষ্টা করিনি (PRs স্বাগত!):

একটি USB-C তারের মাধ্যমে একটি Canon ক্যামেরার সাথে সংযুক্ত Android ফোন।
অ্যান্ড্রয়েড ফোনে একই ডেমো চলছে। সুরমার ছবি।

WebUSB-এর ক্রস-প্ল্যাটফর্ম ব্যবহারের উপর আরও গভীর নির্দেশিকা জন্য, "WebUSB-এর জন্য একটি ডিভাইস তৈরি করা" এর "প্ল্যাটফর্ম-নির্দিষ্ট বিবেচনা" বিভাগটি দেখুন।

libusb-এ একটি নতুন ব্যাকএন্ড যোগ করা হচ্ছে

এখন প্রযুক্তিগত বিবরণ সম্মুখের. যদিও libusb-এর অনুরূপ একটি শিম API প্রদান করা সম্ভব (এটি আগে অন্যরা করেছে) এবং এটির বিরুদ্ধে অন্যান্য অ্যাপ্লিকেশনগুলিকে লিঙ্ক করা, এই পদ্ধতিটি ত্রুটি-প্রবণ এবং আরও কোনো এক্সটেনশন বা রক্ষণাবেক্ষণকে কঠিন করে তোলে। আমি জিনিসগুলি সঠিকভাবে করতে চেয়েছিলাম, এমনভাবে যা সম্ভাব্যভাবে আপস্ট্রিমে অবদান রাখতে পারে এবং ভবিষ্যতে libusb-এ একত্রিত হতে পারে।

ভাগ্যক্রমে, libusb README বলেছেন:

libusb অভ্যন্তরীণভাবে এমনভাবে বিমূর্ত করা হয়েছে যে এটি আশা করা যায় অন্যান্য অপারেটিং সিস্টেমে পোর্ট করা যেতে পারে। আরও তথ্যের জন্য অনুগ্রহ করে পোর্টিং ফাইলটি দেখুন।"

libusb এমনভাবে গঠন করা হয়েছে যেখানে পাবলিক API "ব্যাকএন্ড" থেকে আলাদা। এই ব্যাকএন্ডগুলি অপারেটিং সিস্টেমের নিম্ন-স্তরের API-এর মাধ্যমে ডিভাইসগুলির তালিকা, খোলা, বন্ধ এবং প্রকৃতপক্ষে যোগাযোগের জন্য দায়ী৷ এইভাবে 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 ভেরিয়েবল প্রকাশ করতে হবে। উদাহরণস্বরূপ, এটি উইন্ডোজ ব্যাকএন্ডের মত দেখাচ্ছে:

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

বৈশিষ্ট্যগুলি দেখে, আমরা দেখতে পাচ্ছি যে স্ট্রাকটে ব্যাকএন্ডের নাম, এর ক্ষমতার একটি সেট, ফাংশন পয়েন্টার আকারে বিভিন্ন নিম্ন-স্তরের ইউএসবি অপারেশনের জন্য হ্যান্ডলার এবং অবশেষে, ব্যক্তিগত ডিভাইস-/প্রসঙ্গ সংরক্ষণের জন্য বরাদ্দ করা আকার অন্তর্ভুক্ত রয়েছে। -/স্থানান্তর-স্তরের ডেটা।

ব্যক্তিগত ডেটা ক্ষেত্রগুলি অন্তত সেই সমস্ত জিনিসগুলিতে OS হ্যান্ডলগুলি সংরক্ষণ করার জন্য দরকারী, কারণ হ্যান্ডেলগুলি ছাড়াই আমরা জানি না যে কোন প্রদত্ত অপারেশনটি প্রযোজ্য। ওয়েব বাস্তবায়নে, ওএস হ্যান্ডলগুলি অন্তর্নিহিত ওয়েবইউএসবি জাভাস্ক্রিপ্ট অবজেক্ট হবে। এমস্ক্রিপ্টেনে তাদের উপস্থাপন এবং সংরক্ষণ করার প্রাকৃতিক উপায় হল emscripten::val ক্লাসের মাধ্যমে, যা এম্বিন্ড (এমস্ক্রিপ্টেনের বাইন্ডিং সিস্টেম) এর অংশ হিসাবে সরবরাহ করা হয়।

ফোল্ডারের বেশিরভাগ ব্যাকএন্ড সি এ প্রয়োগ করা হয়, তবে কয়েকটি সি++ এ প্রয়োগ করা হয়। এম্বিন্ড শুধুমাত্র C++ এর সাথে কাজ করে, তাই পছন্দটি আমার জন্য করা হয়েছিল এবং আমি প্রয়োজনীয় কাঠামোর সাথে এবং ব্যক্তিগত ডেটা ক্ষেত্রের জন্য sizeof(val) সহ libusb/libusb/os/emscripten_webusb.cpp যোগ করেছি:

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

সিঙ্ক্রোনাস সি প্রেক্ষাপটে Async ওয়েব API

এখন async WebUSB API-গুলি পরিচালনা করার একটি উপায় প্রয়োজন যেখানে libusb সিঙ্ক্রোনাস অপারেশন আশা করে। এর জন্য, আমি Asyncify ব্যবহার করতে পারি, বা, আরও নির্দিষ্টভাবে, val::await() এর মাধ্যমে এর এম্বিন্ড ইন্টিগ্রেশন।

আমি WebUSB ত্রুটিগুলি সঠিকভাবে পরিচালনা করতে এবং সেগুলিকে libusb ত্রুটি কোডগুলিতে রূপান্তর করতে চেয়েছিলাম, তবে এম্বিন্ডের কাছে বর্তমানে জাভাস্ক্রিপ্ট ব্যতিক্রমগুলি বা C++ পাশ থেকে Promise প্রত্যাখ্যানগুলি পরিচালনা করার কোনও উপায় নেই। এই সমস্যাটি জাভাস্ক্রিপ্টের দিকে একটি প্রত্যাখ্যান ধরার মাধ্যমে এবং ফলাফলটিকে একটি { error, value } অবজেক্টে রূপান্তর করে যা এখন নিরাপদে C++ পাশ থেকে পার্স করা যেতে পারে। আমি EM_JS ম্যাক্রো এবং Emval.to{Handle, Value} API-এর সংমিশ্রণে এটি করেছি:

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 হ্যান্ডলারের মাধ্যমে এই অপারেশনটি বাস্তবায়ন করবে।

অসুবিধা হল, অন্যান্য প্ল্যাটফর্মের বিপরীতে, নিরাপত্তার কারণে ওয়েবে সমস্ত সংযুক্ত USB ডিভাইসগুলি গণনা করার কোনও উপায় নেই৷ পরিবর্তে, প্রবাহ দুটি ভাগে বিভক্ত হয়। প্রথমত, ওয়েব অ্যাপ্লিকেশনটি 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 ব্যবহার করে একইভাবে যেমন উপরে দেখানো হয়েছে। ডেটা ট্রান্সফার হ্যান্ডলিং কোডে আরও কয়েকটি আকর্ষণীয় হ্যাক রয়েছে, তবে এই নিবন্ধটির উদ্দেশ্যে সেই বাস্তবায়নের বিবরণ কম গুরুত্বপূর্ণ। আপনি যদি আগ্রহী হন তবে গিথুবে কোড এবং মন্তব্যগুলি পরীক্ষা করতে ভুলবেন না।

পোর্টিং ইভেন্ট ওয়েবে লুপ

libusb পোর্টের আরও একটি অংশ যা আমি আলোচনা করতে চাই তা হল ইভেন্ট হ্যান্ডলিং। পূর্ববর্তী নিবন্ধে বর্ণিত হিসাবে, C এর মতো সিস্টেম ভাষায় বেশিরভাগ APIগুলি সিঙ্ক্রোনাস, এবং ইভেন্ট পরিচালনাও এর ব্যতিক্রম নয়। এটি সাধারণত একটি অসীম লুপের মাধ্যমে বাস্তবায়িত হয় যা "পোল" (কিছু ডেটা উপলব্ধ না হওয়া পর্যন্ত ডেটা পড়ার চেষ্টা করে বা এক্সিকিউশন ব্লক করে) বহিরাগত I/O উত্সগুলির একটি সেট থেকে, এবং, যখন তাদের মধ্যে অন্তত একটি সাড়া দেয়, এটি একটি ইভেন্ট হিসাবে পাস করে। সংশ্লিষ্ট হ্যান্ডলারের কাছে। হ্যান্ডলার শেষ হয়ে গেলে, নিয়ন্ত্রণ লুপে ফিরে আসে এবং এটি অন্য পোলের জন্য বিরতি দেয়।

ওয়েবে এই পদ্ধতির সাথে কয়েকটি সমস্যা রয়েছে।

প্রথমত, WebUSB অন্তর্নিহিত ডিভাইসগুলির কাঁচা হ্যান্ডেলগুলিকে প্রকাশ করে না এবং করতে পারে না, তাই সেগুলিকে সরাসরি পোলিং করা একটি বিকল্প নয়৷ দ্বিতীয়ত, libusb অন্যান্য ইভেন্টের জন্য eventfd এবং pipe এপিআই ব্যবহার করে সেইসাথে অপারেটিং সিস্টেমে কাচা ডিভাইস হ্যান্ডেল ছাড়াই স্থানান্তর পরিচালনার জন্য, কিন্তু eventfd বর্তমানে Emscripten-এ সমর্থিত নয়, এবং pipe , সমর্থিত থাকাকালীন, বর্তমানে স্পেকের সাথে সঙ্গতিপূর্ণ নয় এবং করতে পারে। ইভেন্টের জন্য অপেক্ষা করবেন না।

অবশেষে, সবচেয়ে বড় সমস্যা হল ওয়েবের নিজস্ব ইভেন্ট লুপ রয়েছে। এই গ্লোবাল ইভেন্ট লুপটি যেকোন বাহ্যিক I/O ক্রিয়াকলাপগুলির জন্য ব্যবহার করা হয় ( fetch() , টাইমার সহ, বা, এই ক্ষেত্রে, WebUSB), এবং যখনই সংশ্লিষ্ট ক্রিয়াকলাপ শেষ হয় তখন এটি ইভেন্ট বা Promise হ্যান্ডলারদের আহ্বান করে৷ অন্য, নেস্টেড, অসীম ইভেন্ট লুপ চালানো ব্রাউজারের ইভেন্ট লুপকে কখনো অগ্রগতি হতে বাধা দেবে, যার মানে হল যে শুধুমাত্র UI অপ্রতিক্রিয়াশীল হয়ে উঠবে না, কিন্তু কোডটি কখনই সেই একই 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 ইভেন্টগুলি পরিচালনা করার অনুমতি দেয়।
  3. নির্দিষ্ট সময়সীমা শেষ হয়ে গেছে কিনা তা পরীক্ষা করুন এবং, যদি না হয়, লুপ চালিয়ে যান।

মন্তব্যটি যেমন উল্লেখ করেছে, এই পদ্ধতিটি সর্বোত্তম ছিল না, কারণ এটি অ্যাসিঙ্কাইফাইয়ের সাথে সম্পূর্ণ কল স্ট্যাক সংরক্ষণ-পুনরুদ্ধার করে রেখেছিল এমনকি যখন এখনও পরিচালনা করার মতো কোনও ইউএসবি ইভেন্ট ছিল না (যা বেশিরভাগ সময়), এবং setTimeout() নিজেই একটি আধুনিক ব্রাউজারে সর্বনিম্ন সময়কাল 4ms। তবুও, এটি ধারণার প্রমাণে DSLR থেকে 13-14 FPS লাইভস্ট্রিম তৈরি করতে যথেষ্ট ভাল কাজ করেছে।

পরে, আমি ব্রাউজার ইভেন্ট সিস্টেমের সুবিধা দিয়ে এটিকে উন্নত করার সিদ্ধান্ত নিয়েছি। এই বাস্তবায়নকে আরও উন্নত করা যেতে পারে এমন বেশ কয়েকটি উপায় রয়েছে, কিন্তু আপাতত আমি একটি নির্দিষ্ট 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() -ভিত্তিক বাস্তবায়নের দক্ষতার সমস্যাগুলিকে সংশোধন করেছে এবং DSLR ডেমো থ্রুপুটকে 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']"
  ;;

প্রথমত, ইউনিক্স প্ল্যাটফর্মের এক্সিকিউটেবলে সাধারণত ফাইল এক্সটেনশন থাকে না। Emscripten, যাইহোক, আপনি কোন এক্সটেনশনের অনুরোধের উপর নির্ভর করে বিভিন্ন আউটপুট তৈরি করে। আমি এক্সিকিউটেবল এক্সটেনশনকে .html এ পরিবর্তন করতে AC_SUBST(EXEEXT, …) ব্যবহার করছি যাতে প্যাকেজের মধ্যে যেকোন এক্সিকিউটেবল-পরীক্ষা এবং উদাহরণ-এমস্ক্রিপ্টেন-এর ডিফল্ট শেল সহ একটি HTML হয়ে যায় যা JavaScript এবং WebAssembly লোড এবং ইনস্ট্যান্ট করার যত্ন নেয়।

দ্বিতীয়ত, যেহেতু আমি এম্বিন্ড এবং অ্যাসিনসিফাই ব্যবহার করছি, তাই আমাকে সেই বৈশিষ্ট্যগুলি সক্রিয় করতে হবে ( --bind -s ASYNCIFY ) সেইসাথে লিঙ্কার প্যারামিটারের মাধ্যমে গতিশীল মেমরি বৃদ্ধির ( -s ALLOW_MEMORY_GROWTH ) অনুমতি দিতে হবে। দুর্ভাগ্যবশত, কোনো লাইব্রেরির কাছে সেই পতাকাগুলি লিঙ্কারে রিপোর্ট করার কোনো উপায় নেই, তাই এই libusb পোর্ট ব্যবহার করে এমন প্রতিটি অ্যাপ্লিকেশনকে তাদের বিল্ড কনফিগারেশনে একই লিঙ্কার পতাকা যুক্ত করতে হবে।

পরিশেষে, পূর্বে উল্লিখিত হিসাবে, WebUSB-এর জন্য একটি ব্যবহারকারীর অঙ্গভঙ্গির মাধ্যমে ডিভাইস গণনা করা প্রয়োজন। libusb উদাহরণ এবং পরীক্ষাগুলি অনুমান করে যে তারা স্টার্ট-আপের সময় ডিভাইসগুলি গণনা করতে পারে এবং পরিবর্তন ছাড়াই ত্রুটি সহ ব্যর্থ হয়। পরিবর্তে, আমাকে স্বয়ংক্রিয় নির্বাহ ( -s INVOKE_RUN=0 ) অক্ষম করতে হয়েছিল এবং ম্যানুয়াল callMain() পদ্ধতিটি প্রকাশ করতে হয়েছিল ( -s EXPORTED_RUNTIME_METHODS=... )।

এই সমস্ত কিছু হয়ে গেলে, আমি একটি স্ট্যাটিক ওয়েব সার্ভারের সাথে জেনারেট করা ফাইলগুলি পরিবেশন করতে পারি, WebUSB শুরু করতে পারি এবং DevTools-এর সাহায্যে ম্যানুয়ালি সেই HTML এক্সিকিউটেবলগুলি চালাতে পারি।

একটি স্থানীয়ভাবে পরিবেশিত `testlibusb` পৃষ্ঠায় খোলা DevTools সহ একটি Chrome উইন্ডো দেখানো স্ক্রিনশট। DevTools কনসোল `navigator.usb.requestDevice({ filters: [] })` মূল্যায়ন করছে, যা একটি অনুমতি প্রম্পট ট্রিগার করেছে এবং এটি বর্তমানে ব্যবহারকারীকে একটি USB ডিভাইস বেছে নিতে বলছে যা পৃষ্ঠার সাথে শেয়ার করা উচিত। ILCE-6600 (একটি Sony ক্যামেরা) বর্তমানে নির্বাচিত হয়েছে৷

DevTools এখনও খোলা সহ পরবর্তী ধাপের স্ক্রিনশট। ডিভাইসটি নির্বাচন করার পরে, কনসোল একটি নতুন অভিব্যক্তি `Module.callMain(['-v'])` মূল্যায়ন করেছে, যা ভার্বোস মোডে `testlibusb` অ্যাপটি কার্যকর করেছে। আউটপুট পূর্বে সংযুক্ত USB ক্যামেরা সম্পর্কে বিভিন্ন বিস্তারিত তথ্য দেখায়: প্রস্তুতকারক Sony, পণ্য ILCE-6600, সিরিয়াল নম্বর, কনফিগারেশন ইত্যাদি।

এটি দেখতে খুব বেশি মনে হয় না, কিন্তু, একটি নতুন প্ল্যাটফর্মে লাইব্রেরি পোর্ট করার সময়, প্রথমবারের জন্য একটি বৈধ আউটপুট তৈরি করার পর্যায়ে পৌঁছানো বেশ উত্তেজনাপূর্ণ!

পোর্ট ব্যবহার করে

উপরে উল্লিখিত হিসাবে, পোর্টটি কয়েকটি Emscripten বৈশিষ্ট্যের উপর নির্ভর করে যা বর্তমানে অ্যাপ্লিকেশনের লিঙ্কিং পর্যায়ে সক্ষম করা প্রয়োজন। আপনি যদি আপনার নিজের অ্যাপ্লিকেশনে এই libusb পোর্টটি ব্যবহার করতে চান তবে আপনাকে যা করতে হবে তা এখানে:

  1. আপনার বিল্ডের অংশ হিসাবে একটি সংরক্ষণাগার হিসাবে সর্বশেষ libusb ডাউনলোড করুন বা এটি আপনার প্রকল্পে একটি গিট সাবমডিউল হিসাবে যুক্ত করুন।
  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

লাইব্রেরির বর্তমানে কিছু সীমাবদ্ধতা রয়েছে:

  • কোন স্থানান্তর বাতিল সমর্থন. এটি ওয়েবইউএসবি-এর একটি সীমাবদ্ধতা, যার ফলস্বরূপ, libusb-এ ক্রস-প্ল্যাটফর্ম স্থানান্তর বাতিলকরণের অভাব থেকে উদ্ভূত হয়।
  • কোন আইসোক্রোনাস স্থানান্তর সমর্থন নেই। উদাহরণ হিসাবে বিদ্যমান স্থানান্তর মোডগুলির বাস্তবায়ন অনুসরণ করে এটি যোগ করা কঠিন হওয়া উচিত নয়, তবে এটি কিছুটা বিরল মোড এবং এটি পরীক্ষা করার জন্য আমার কাছে কোনও ডিভাইস নেই, তাই আপাতত আমি এটিকে অসমর্থিত হিসাবে রেখেছি। আপনার যদি এই ধরনের ডিভাইস থাকে, এবং লাইব্রেরিতে অবদান রাখতে চান, পিআরগুলিকে স্বাগতম!
  • পূর্বে উল্লিখিত ক্রস-প্ল্যাটফর্ম সীমাবদ্ধতা । এই সীমাবদ্ধতাগুলি অপারেটিং সিস্টেম দ্বারা আরোপ করা হয়, তাই আমরা এখানে খুব বেশি কিছু করতে পারি না, ব্যবহারকারীদের ড্রাইভার বা অনুমতিগুলিকে ওভাররাইড করতে বলা ছাড়া৷ যাইহোক, আপনি যদি HID বা সিরিয়াল ডিভাইস পোর্ট করে থাকেন, তাহলে আপনি libusb উদাহরণ অনুসরণ করতে পারেন এবং অন্য কোনো লাইব্রেরি অন্য Fugu API-এ পোর্ট করতে পারেন। উদাহরণস্বরূপ, আপনি ওয়েবএইচআইডি -তে একটি সি লাইব্রেরি হিডাপি পোর্ট করতে পারেন এবং নিম্ন-স্তরের ইউএসবি অ্যাক্সেসের সাথে সম্পৃক্ত সেই সমস্ত সমস্যাগুলিকে একদিকে সরিয়ে দিতে পারেন।

উপসংহার

এই পোস্টে আমি দেখিয়েছি কিভাবে, Emscripten, Asyncify এবং Fugu API-এর সাহায্যে এমনকি libusb-এর মতো নিম্ন-স্তরের লাইব্রেরিগুলিকে কয়েকটি ইন্টিগ্রেশন কৌশল সহ ওয়েবে পোর্ট করা যেতে পারে।

এই ধরনের প্রয়োজনীয় এবং ব্যাপকভাবে ব্যবহৃত নিম্ন-স্তরের লাইব্রেরিগুলিকে পোর্ট করা বিশেষভাবে পুরস্কৃত করা হয়, কারণ, এর পরিবর্তে, এটি উচ্চ-স্তরের লাইব্রেরি বা এমনকি সম্পূর্ণ অ্যাপ্লিকেশনগুলিকেও ওয়েবে আনার অনুমতি দেয়৷ এটি এমন অভিজ্ঞতাগুলি খুলে দেয় যা আগে এক বা দুটি প্ল্যাটফর্মের ব্যবহারকারীদের মধ্যে সীমাবদ্ধ ছিল, সমস্ত ধরণের ডিভাইস এবং অপারেটিং সিস্টেমে, সেই অভিজ্ঞতাগুলিকে শুধুমাত্র একটি লিঙ্ক ক্লিক দূরে উপলব্ধ করে তোলে৷

পরবর্তী পোস্টে আমি ওয়েব gPhoto2 ডেমো তৈরির সাথে জড়িত পদক্ষেপগুলির মধ্য দিয়ে হেঁটে যাবো যা শুধুমাত্র ডিভাইসের তথ্য পুনরুদ্ধার করে না, তবে libusb-এর স্থানান্তর বৈশিষ্ট্যও ব্যাপকভাবে ব্যবহার করে। ইতিমধ্যে, আমি আশা করি আপনি libusb উদাহরণটি অনুপ্রেরণাদায়ক পেয়েছেন এবং ডেমোটি চেষ্টা করবেন, লাইব্রেরির সাথেই খেলবেন, অথবা সম্ভবত এগিয়ে যান এবং ফুগু API-এর একটিতেও অন্য একটি বহুল ব্যবহৃত লাইব্রেরি পোর্ট করবেন।