انتقال برنامه های USB به وب قسمت 1: libusb

بیاموزید که چگونه می‌توان کدهایی را که با دستگاه‌های خارجی در تعامل است با WebAssembly و Fugu API به وب منتقل کرد.

در پست قبلی ، نحوه پورت برنامه ها با استفاده از APIهای سیستم فایل را به وب با File System Access API ، WebAssembly و Asyncify نشان دادم. اکنون می‌خواهم همان مبحث ادغام Fugu API با WebAssembly و انتقال برنامه‌ها به وب را بدون از دست دادن ویژگی‌های مهم ادامه دهم.

من نشان خواهم داد که چگونه برنامه‌هایی که با دستگاه‌های USB ارتباط برقرار می‌کنند، می‌توانند با انتقال libusb - یک کتابخانه USB محبوب که به زبان C نوشته شده است - به WebAssembly (از طریق Emscripten )، Asyncify و WebUSB به وب منتقل شوند.

اولین چیزها: یک نسخه ی نمایشی

مهم‌ترین کاری که باید هنگام انتقال یک کتابخانه انجام داد، انتخاب نسخه نمایشی مناسب است - چیزی که قابلیت‌های کتابخانه پورت‌شده را به نمایش بگذارد، به شما امکان می‌دهد آن را به روش‌های مختلف آزمایش کنید و در عین حال از نظر بصری جذاب باشید.

ایده ای که من انتخاب کردم کنترل از راه دور DSLR بود. به طور خاص، یک پروژه منبع باز gPhoto2 به اندازه کافی در این فضا بوده است تا مهندسی معکوس و پشتیبانی از طیف گسترده ای از دوربین های دیجیتال را اجرا کند. از چندین پروتکل پشتیبانی می کند، اما یکی از مواردی که بیشتر به آن علاقه مند بودم، پشتیبانی از USB بود که از طریق libusb انجام می شود.

مراحل ساخت این دمو را در دو قسمت شرح می دهم. در این پست وبلاگ، توضیح خواهم داد که چگونه لیباسب را منتقل کردم و چه ترفندهایی ممکن است برای انتقال سایر کتابخانه های محبوب به API های Fugu لازم باشد. در پست دوم ، من به جزئیات در مورد انتقال و یکپارچه سازی خود gPhoto2 خواهم پرداخت.

در پایان، من یک برنامه کاربردی وب دریافت کردم که فید زنده را از یک DSLR پیش‌نمایش می‌کند و می‌تواند تنظیمات آن را از طریق USB کنترل کند. قبل از مطالعه جزئیات فنی، می توانید نسخه آزمایشی زنده یا از پیش ضبط شده را بررسی کنید:

دمو در حال اجرا بر روی لپ تاپ متصل به دوربین سونی.

به ویژگی های خاص دوربین توجه کنید

شاید متوجه شده باشید که تغییر تنظیمات در ویدیو کمی طول می کشد. مانند بسیاری از مشکلات دیگری که ممکن است مشاهده کنید، این به دلیل عملکرد WebAssembly یا WebUSB نیست، بلکه به دلیل تعامل gPhoto2 با دوربین خاصی است که برای نسخه نمایشی انتخاب شده است.

Sony a6600 یک API را برای تنظیم مقادیری مانند ISO، دیافراگم یا سرعت شاتر به طور مستقیم در معرض نمایش نمی گذارد، و در عوض فقط دستوراتی برای افزایش یا کاهش آنها با تعداد مراحل مشخص شده ارائه می دهد. برای پیچیده‌تر کردن مسائل، فهرستی از مقادیر واقعی پشتیبانی‌شده را نیز برنمی‌گرداند—به نظر می‌رسد فهرست بازگشتی در بسیاری از مدل‌های دوربین سونی کدگذاری شده است.

هنگام تنظیم یکی از آن مقادیر، gPhoto2 چاره دیگری ندارد جز اینکه:

  1. یک قدم (یا چند) در جهت مقدار انتخاب شده بردارید.
  2. کمی صبر کنید تا دوربین تنظیمات را به روز کند.
  3. مقداری را که دوربین واقعاً روی آن قرار گرفته است را دوباره بخوانید.
  4. بررسی کنید که آخرین مرحله از مقدار مورد نظر خارج نشده باشد یا در انتهای یا ابتدای لیست پیچیده نشده باشد.
  5. تکرار کنید.

ممکن است کمی طول بکشد، اما اگر مقدار واقعاً توسط دوربین پشتیبانی شود، به آنجا می‌رسد، و اگر نه، روی نزدیک‌ترین مقدار پشتیبانی‌شده متوقف می‌شود.

دوربین‌های دیگر احتمالاً مجموعه‌های مختلفی از تنظیمات، APIهای اساسی و ویژگی‌های متفاوت خواهند داشت. به خاطر داشته باشید که gPhoto2 یک پروژه منبع باز است و آزمایش خودکار یا دستی همه مدل‌های دوربین موجود در آنجا به سادگی امکان‌پذیر نیست، بنابراین گزارش‌های دقیق مشکل و روابط عمومی همیشه مورد استقبال قرار می‌گیرد (اما مطمئن شوید که مشکلات را با مقامات رسمی بازتولید کنید. ابتدا مشتری gPhoto2).

نکات مهم سازگاری بین پلتفرم

متأسفانه، در ویندوز به هر دستگاه "معروف"، از جمله دوربین های DSLR، یک درایور سیستم اختصاص داده شده است که با WebUSB سازگار نیست. اگر می‌خواهید نسخه آزمایشی را در ویندوز امتحان کنید، باید از ابزاری مانند Zadig برای لغو درایور DSLR متصل به WinUSB یا libusb استفاده کنید. این روش برای من و بسیاری از کاربران دیگر خوب کار می کند، اما شما باید با مسئولیت خود از آن استفاده کنید.

در لینوکس، احتمالاً باید مجوزهای سفارشی را برای دسترسی به DSLR خود از طریق WebUSB تنظیم کنید ، اگرچه این بستگی به توزیع شما دارد.

در macOS و Android، نسخه آزمایشی باید خارج از جعبه کار کند. اگر آن را روی یک تلفن اندرویدی امتحان می‌کنید، مطمئن شوید که به حالت افقی بروید، زیرا من تلاش زیادی برای پاسخ‌دهی آن انجام نداده‌ام (PR خوش آمدید!):

تلفن Android از طریق کابل USB-C به دوربین Canon متصل می شود.
همان نسخه ی نمایشی که روی گوشی اندرویدی اجرا می شود. عکس سورما .

برای راهنمای عمیق‌تر در مورد استفاده از WebUSB بین پلتفرم‌ها، به بخش «ملاحظات خاص پلتفرم» در «ساخت دستگاه برای WebUSB» مراجعه کنید.

افزودن یک باطن جدید به libusb

حالا به جزئیات فنی بپردازیم. در حالی که امکان ارائه یک API شیم مشابه libusb وجود دارد (این کار قبلاً توسط دیگران انجام شده است) و سایر برنامه‌ها را علیه آن پیوند می‌دهد، این رویکرد مستعد خطا است و هرگونه توسعه یا نگهداری بیشتر را سخت‌تر می‌کند. من می‌خواستم کارها را به‌درستی انجام دهم، به‌گونه‌ای که به طور بالقوه بتوان در بالادست کمک کرد و در آینده در libusb ادغام شد.

خوشبختانه، libusb README می گوید:

Libusb به گونه ای داخلی انتزاع شده است که امیدواریم بتوان آن را به سایر سیستم عامل ها منتقل کرد. لطفاً برای اطلاعات بیشتر به فایل PORTING مراجعه کنید."

libusb به گونه ای ساختار یافته است که API عمومی از "backends" جدا باشد. این پشتیبان ها مسئول فهرست کردن، باز کردن، بستن و در واقع برقراری ارتباط با دستگاه ها از طریق API های سطح پایین سیستم عامل هستند. اینگونه است که libusb تفاوت‌های بین Linux، macOS، Windows، Android، OpenBSD/NetBSD، Haiku و Solaris را از بین می‌برد و روی همه این پلتفرم‌ها کار می‌کند.

کاری که من باید انجام می دادم این بود که یک Backend دیگر برای "سیستم عامل" Emscripten+WebUSB اضافه کنم. پیاده سازی ها برای آن backend ها در پوشه 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_backend از نوع usbi_os_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),
};

با نگاهی به ویژگی‌ها، می‌توانیم ببینیم که ساختار شامل نام پشتیبان، مجموعه‌ای از قابلیت‌های آن، کنترل‌کننده‌هایی برای عملیات‌های مختلف USB سطح پایین به شکل نشانگرهای تابع، و در نهایت، اندازه‌هایی برای تخصیص برای ذخیره‌سازی دستگاه/متن خصوصی است. -/داده های سطح انتقال.

فیلدهای داده خصوصی حداقل برای ذخیره کردن دسته‌های سیستم‌عامل برای همه آن چیزها مفید هستند، زیرا بدون دستگیره نمی‌دانیم که هر عملیاتی برای کدام مورد اعمال می‌شود. در پیاده‌سازی وب، دسته‌های سیستم‌عامل، اشیاء زیربنایی جاوا اسکریپت WebUSB خواهند بود. راه طبیعی برای نمایش و ذخیره آنها در 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))) {}
};

همگام سازی API های وب در زمینه های C همزمان

اکنون به راهی برای مدیریت APIهای WebUSB غیرهمگام نیاز داریم که در آن libusb انتظار عملیات همزمان را دارد. برای این کار، می‌توانم از Asyncify یا به‌طور خاص‌تر از ادغام Embind آن از طریق val::await() استفاده کنم.

من همچنین می‌خواستم خطاهای WebUSB را به درستی مدیریت کنم و آنها را به کدهای خطای libusb تبدیل کنم، اما Embind در حال حاضر هیچ راهی برای رسیدگی به استثناهای جاوا اسکریپت یا رد Promise از سمت C++ ندارد. این مشکل را می توان با گرفتن رد در سمت جاوا اسکریپت و تبدیل نتیجه به یک شی { error, value } حل کرد که اکنون می تواند به طور ایمن از سمت C++ تجزیه شود. من این کار را با ترکیبی از APIهای ماکرو 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()};
 
}
};

اکنون می‌توانم از promise_result::await() در هر Promise که از عملیات WebUSB بازگردانده شده است استفاده کنم و فیلدهای error و value آن را جداگانه بررسی کنم.

به عنوان مثال، بازیابی یک val که نشان دهنده یک USBDevice از libusb_device_handle است، فراخوانی متد 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 باید لیستی از دستگاه های موجود را بازیابی کند. پشتیبان باید این عملیات را از طریق یک handler get_device_list اجرا کند.

مشکل این است که بر خلاف سایر پلتفرم ها، به دلایل امنیتی، هیچ راهی برای شمارش تمام دستگاه های USB متصل در وب وجود ندارد. در عوض، جریان به دو قسمت تقسیم می شود. ابتدا، برنامه وب از طریق navigator.usb.requestDevice() دستگاه هایی با ویژگی های خاص را درخواست می کند و کاربر به صورت دستی انتخاب می کند که کدام دستگاه را می خواهد افشا کند یا درخواست مجوز را رد می کند. پس از آن، برنامه از طریق navigator.usb.getDevices() دستگاه‌های مورد تأیید و متصل را فهرست می‌کند.

در ابتدا سعی کردم از requestDevice() به طور مستقیم در پیاده سازی handler get_device_list استفاده کنم. با این حال، نشان دادن یک درخواست مجوز با لیستی از دستگاه‌های متصل، یک عملیات حساس در نظر گرفته می‌شود و باید با تعامل کاربر (مانند کلیک روی یک صفحه) فعال شود، در غیر این صورت همیشه یک وعده رد شده را برمی‌گرداند. برنامه های 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 که می‌خواهم درباره آن بحث کنم، مدیریت رویداد است. همانطور که در مقاله قبلی توضیح داده شد، اکثر APIها در زبان های سیستمی مانند C همزمان هستند و مدیریت رویداد نیز از این قاعده مستثنی نیست. معمولاً از طریق یک حلقه نامتناهی که "نظرسنجی" (تلاش می‌کند تا داده‌ها را بخواند یا اجرا را تا زمانی که برخی داده‌ها در دسترس نباشد مسدود می‌کند) از مجموعه‌ای از منابع ورودی/خروجی خارجی پیاده‌سازی می‌شود، و وقتی حداقل یکی از آن‌ها پاسخ می‌دهد، آن را به عنوان یک رویداد ارسال می‌کند. به کنترل کننده مربوطه هنگامی که کنترل کننده تمام شد، کنترل به حلقه باز می گردد و برای نظرسنجی دیگر مکث می کند.

چند مشکل با این رویکرد در وب وجود دارد.

اولاً، WebUSB دستگیره‌های خام دستگاه‌های زیرین را آشکار نمی‌کند و نمی‌تواند، بنابراین نظرسنجی مستقیم از آن‌ها یک گزینه نیست. دوم، libusb از eventfd و pipe API برای رویدادهای دیگر و همچنین برای مدیریت انتقال در سیستم‌عامل‌های بدون دسته دستگاه خام استفاده می‌کند، اما eventfd در حال حاضر در Emscripten پشتیبانی نمی‌شود، و pipe ، در حالی که پشتیبانی می‌شود، در حال حاضر با مشخصات مطابقت ندارد و می‌تواند منتظر اتفاقات باش

در نهایت، بزرگترین مشکل این است که وب حلقه رویداد خاص خود را دارد. این حلقه رویداد جهانی برای هر عملیات ورودی/خروجی خارجی (از جمله fetch() ، تایمرها، یا در این مورد WebUSB استفاده می‌شود، و هر زمان که عملیات مربوطه تمام شد، رویداد یا Promise handlers را فراخوانی می‌کند. اجرای یک حلقه رویداد نامحدود، تودرتو، مانع از پیشرفت حلقه رویداد مرورگر می شود، به این معنی که نه تنها رابط کاربری پاسخگو نمی شود، بلکه کد هرگز برای همان رویدادهای ورودی/خروجی که منتظر آن است، اعلان دریافت نمی کند. این معمولاً منجر به یک بن بست می شود، و این همان چیزی است که وقتی سعی کردم از libusb در دمو نیز استفاده کنم، اتفاق افتاد. صفحه یخ زد.

مانند سایر ورودی/خروجی های مسدودکننده، برای انتقال چنین حلقه های رویدادی به وب، توسعه دهندگان باید راهی برای اجرای آن حلقه ها بدون مسدود کردن رشته اصلی بیابند. یک راه این است که برنامه را تغییر دهیم تا رویدادهای ورودی/خروجی را در یک رشته جداگانه مدیریت کند و نتایج را به رشته اصلی برگرداند. مورد دیگر این است که از Asyncify برای مکث کردن حلقه و منتظر رویدادها به صورت غیر مسدود کننده استفاده کنید.

من نمی‌خواستم تغییرات قابل توجهی در libusb یا gPhoto2 انجام دهم و قبلاً از Asyncify برای 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) فرا می خواند. این تابع از Asyncify و setTimeout() در زیر هود استفاده می کند و در اینجا برای بازگرداندن کنترل به حلقه رویداد اصلی مرورگر استفاده می شود. این به مرورگر اجازه می دهد تا هرگونه تعامل با کاربر و رویدادهای ورودی/خروجی از جمله WebUSB را مدیریت کند.
  3. بررسی کنید که آیا بازه زمانی مشخص شده هنوز منقضی شده است یا خیر، و اگر نه، حلقه را ادامه دهید.

همانطور که در کامنت ذکر شد، این رویکرد بهینه نبود، زیرا کل پشته تماس را با Asyncify ذخیره و بازیابی می‌کرد، حتی زمانی که هنوز هیچ رویداد USB برای رسیدگی وجود نداشت (که در بیشتر مواقع اینطور است)، و چون setTimeout() خود دارای یک حداقل مدت زمان 4 میلی ثانیه در مرورگرهای مدرن. با این حال، به اندازه کافی خوب کار کرد تا در اثبات مفهوم، پخش زنده 13-14 FPS را از DSLR تولید کند.

بعداً تصمیم گرفتم با استفاده از سیستم رویداد مرورگر آن را بهبود بخشم. راه‌های مختلفی وجود دارد که این پیاده‌سازی را می‌توان بیشتر بهبود بخشید، اما در حال حاضر من انتخاب کرده‌ام که رویدادهای سفارشی را مستقیماً بر روی شی سراسری منتشر کنم، بدون اینکه آنها را با ساختار داده 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 sleep زمانی که یک رویداد 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 فریم در ثانیه به 30+ فریم بر ثانیه افزایش داد، که برای یک کار روان کافی است. خوراک زنده

ساخت سیستم و اولین تست

پس از اتمام Backend، مجبور شدم آن را به 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 بسته به اینکه کدام برنامه افزودنی را درخواست می کنید، خروجی متفاوتی تولید می کند. من از AC_SUBST(EXEEXT, …) برای تغییر پسوند اجرایی به .html استفاده می‌کنم تا هر فایل اجرایی درون یک بسته (تست‌ها و مثال‌ها) به یک HTML با پوسته پیش‌فرض Emscripten تبدیل شود که وظیفه بارگیری و نمونه‌سازی جاوا اسکریپت و WebAssembly را بر عهده دارد.

دوم، چون من از Embind و Asyncify استفاده می کنم، باید آن ویژگی ها را فعال کنم ( --bind -s ASYNCIFY ) و همچنین اجازه رشد حافظه پویا ( -s ALLOW_MEMORY_GROWTH ) را از طریق پارامترهای پیوند دهنده بدهم. متأسفانه، هیچ راهی برای کتابخانه وجود ندارد که آن پرچم‌ها را به پیوند دهنده گزارش کند، بنابراین هر برنامه‌ای که از این پورت libusb استفاده می‌کند باید همان پرچم‌های پیوند دهنده را به پیکربندی ساخت خود اضافه کند.

در نهایت، همانطور که قبلا ذکر شد، WebUSB نیاز دارد که شمارش دستگاه از طریق یک حرکت کاربر انجام شود. نمونه‌ها و آزمایش‌های libusb فرض می‌کنند که می‌توانند دستگاه‌ها را در هنگام راه‌اندازی شمارش کنند، و با یک خطا بدون تغییر شکست می‌خورند. در عوض، مجبور شدم اجرای خودکار را غیرفعال کنم ( -s INVOKE_RUN=0 ) و روش دستی callMain() در معرض نمایش بگذارم ( -s EXPORTED_RUNTIME_METHODS=... ).

هنگامی که همه این کارها انجام شد، می‌توانم فایل‌های تولید شده را با یک وب سرور استاتیک ارائه کنم، WebUSB را مقداردهی اولیه کنم و با کمک DevTools آن فایل‌های اجرایی HTML را به صورت دستی اجرا کنم.

نماگرفتی که پنجره کروم را با ابزار DevTools در صفحه «testlibusb» که به صورت محلی ارائه می‌شود، نشان می‌دهد. کنسول DevTools در حال ارزیابی «navigator.usb.requestDevice({ filters: [] })» است که یک درخواست مجوز را راه‌اندازی کرد و در حال حاضر از کاربر می‌خواهد یک دستگاه USB را انتخاب کند که باید با صفحه به اشتراک گذاشته شود. ILCE-6600 (یک دوربین سونی) در حال حاضر انتخاب شده است.

اسکرین شات مرحله بعدی، با DevTools هنوز باز است. پس از انتخاب دستگاه، کنسول عبارت جدید «Module.callMain(['-v'])» را ارزیابی کرده است که برنامه «testlibusb» را در حالت کلامی اجرا می‌کند. خروجی اطلاعات دقیق مختلفی را در مورد دوربین USB قبلاً متصل شده نشان می دهد: سازنده Sony، محصول ILCE-6600، شماره سریال، پیکربندی و غیره.

خیلی به نظر نمی رسد، اما، هنگام انتقال کتابخانه ها به یک پلت فرم جدید، رسیدن به مرحله ای که برای اولین بار یک خروجی معتبر تولید می کند، بسیار هیجان انگیز است!

با استفاده از پورت

همانطور که در بالا ذکر شد، پورت به چند ویژگی Emscripten بستگی دارد که در حال حاضر باید در مرحله پیوند برنامه فعال شوند. اگر می خواهید از این پورت libusb در برنامه شخصی خود استفاده کنید، این کار باید انجام دهید:

  1. جدیدترین libusb را به عنوان یک بایگانی به عنوان بخشی از ساخت خود دانلود کنید یا آن را به عنوان یک زیرماژول git در پروژه خود اضافه کنید.
  2. autoreconf -fiv در پوشه libusb اجرا کنید.
  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 ناشی می شود.
  • بدون پشتیبانی از انتقال هم زمان اضافه کردن آن با پیروی از اجرای حالت های انتقال موجود به عنوان مثال نباید سخت باشد، اما این حالت نیز تا حدودی نادر است و من هیچ دستگاهی برای آزمایش آن نداشتم، بنابراین فعلاً آن را به عنوان پشتیبانی نشده رها کردم. اگر چنین دستگاه هایی دارید و می خواهید به کتابخانه کمک کنید، از روابط عمومی استقبال می شود!
  • محدودیت‌های بین پلتفرمی که قبلاً ذکر شد . این محدودیت‌ها توسط سیستم‌عامل‌ها اعمال می‌شوند، بنابراین ما نمی‌توانیم در اینجا کارهای زیادی انجام دهیم، جز اینکه از کاربران بخواهیم درایور یا مجوزها را لغو کنند. با این حال، اگر HID یا دستگاه‌های سریال را انتقال می‌دهید، می‌توانید از مثال libusb پیروی کنید و کتابخانه دیگری را به Fugu API دیگری منتقل کنید. به عنوان مثال، می‌توانید یک هیداپی کتابخانه C را به WebHID پورت کنید و این مشکلات را که با دسترسی سطح پایین USB مرتبط است، کنار بگذارید.

نتیجه گیری

در این پست نشان دادم که چگونه با کمک Emscripten، Asyncify و Fugu APIهای حتی کتابخانه های سطح پایین مانند libusb را می توان با چند ترفند یکپارچه سازی به وب منتقل کرد.

انتقال چنین کتابخانه‌های سطح پایین ضروری و پرکاربرد به‌ویژه ارزشمند است، زیرا به نوبه خود امکان آوردن کتابخانه‌های سطح بالاتر یا حتی کل برنامه‌های کاربردی را به وب نیز فراهم می‌کند. این تجربه‌هایی را باز می‌کند که قبلاً به کاربران یک یا دو پلتفرم محدود می‌شد، به انواع دستگاه‌ها و سیستم‌عامل‌ها، و این تجربیات را تنها با یک کلیک پیوند در دسترس قرار می‌دهد.

در پست بعدی مراحل مربوط به ساخت نسخه نمایشی وب gPhoto2 را که نه تنها اطلاعات دستگاه را بازیابی می کند، بلکه به طور گسترده از ویژگی انتقال libusb نیز استفاده می کند، مرور خواهم کرد. در همین حال، امیدوارم مثال libusb را الهام بخش یافته باشید و نسخه ی نمایشی را امتحان کنید، با خود کتابخانه بازی کنید، یا شاید حتی پیش بروید و یک کتابخانه پرکاربرد دیگر را به یکی از API های Fugu نیز منتقل کنید.