Chuyển các ứng dụng USB sang web. Phần 1: libusb

Tìm hiểu cách chuyển mã tương tác với thiết bị bên ngoài sang web bằng API WebAssembly và Fugu.

Trong bài đăng trước, tôi đã hướng dẫn cách chuyển ứng dụng bằng API hệ thống tệp lên web bằng File System Access API, WebAssembly và Asyncify. Bây giờ, tôi muốn tiếp tục chủ đề tương tự về việc tích hợp APIFugu với WebAssembly và chuyển các ứng dụng sang web mà không làm mất các tính năng quan trọng.

Tôi sẽ trình bày cách chuyển các ứng dụng giao tiếp với thiết bị USB sang web bằng cách chuyển libusb (một thư viện USB phổ biến được viết bằng ngôn ngữ C) sang WebAssembly (thông qua Emscripten), Asyncify và WebUSB.

Trước tiên: một bản minh hoạ

Điều quan trọng nhất cần làm khi chuyển đổi thư viện là chọn đúng bản minh hoạ — một bản minh hoạ thể hiện được các tính năng của thư viện đã chuyển, cho phép bạn thử nghiệm theo nhiều cách, đồng thời vẫn thu hút về mặt hình ảnh.

Ý tưởng tôi chọn là điều khiển từ xa DSLR. Đặc biệt, một dự án nguồn mở gPhoto2 đã tồn tại đủ lâu để thiết kế đảo ngược và triển khai dịch vụ hỗ trợ cho nhiều loại máy ảnh kỹ thuật số. Trình bổ trợ này hỗ trợ nhiều giao thức, nhưng giao thức tôi quan tâm nhất là khả năng hỗ trợ USB. Giao thức này hoạt động thông qua libusb.

Tôi sẽ mô tả các bước xây dựng bản minh hoạ này trong hai phần. Trong bài đăng này trên blog, tôi sẽ mô tả cách tôi chuyển chính libusb và những thủ thuật cần thiết để chuyển các thư viện phổ biến khác sang API Fugu. Trong bài đăng thứ hai, tôi sẽ trình bày chi tiết về cách chuyển và tích hợp chính gPhoto2.

Cuối cùng, tôi đã có một ứng dụng web đang hoạt động giúp xem trước nguồn cấp dữ liệu trực tiếp từ máy ảnh DSLR và có thể kiểm soát chế độ cài đặt của ứng dụng này qua USB. Vui lòng xem bản minh hoạ trực tiếp hoặc được ghi hình sẵn trước khi tìm hiểu chi tiết kỹ thuật:

Bản minh hoạ chạy trên máy tính xách tay kết nối với máy ảnh Sony.

Lưu ý về các tính năng đặc biệt dành riêng cho máy ảnh

Có thể bạn đã nhận thấy rằng việc thay đổi chế độ cài đặt sẽ mất một chút thời gian trong video. Giống như hầu hết các vấn đề khác bạn có thể thấy, điều này không phải do hiệu suất của WebAssembly hoặc WebUSB, mà là do cách gPhoto2 tương tác với máy ảnh cụ thể được chọn cho bản minh hoạ.

Sony a6600 không hiển thị trực tiếp API để thiết lập các giá trị như ISO, khẩu độ hoặc tốc độ màn trập, mà chỉ đưa ra các lệnh để tăng hoặc giảm các giá trị đó theo số bước được chỉ định. Để phức tạp hơn, phương thức này không trả về danh sách các giá trị thực sự được hỗ trợ. Danh sách trả về có vẻ được mã hoá cứng trên nhiều mẫu máy ảnh Sony.

Khi đặt một trong các giá trị đó, gPhoto2 không có lựa chọn nào khác ngoài việc:

  1. Thực hiện một bước (hoặc một vài) theo hướng của giá trị đã chọn.
  2. Hãy đợi một chút để máy ảnh cập nhật chế độ cài đặt.
  3. Đọc lại giá trị mà máy ảnh thực sự đặt được.
  4. Kiểm tra để đảm bảo rằng bước cuối cùng không vượt qua giá trị mong muốn cũng như không gói quanh phần cuối hoặc phần đầu của danh sách.
  5. Lặp lại.

Có thể mất chút thời gian, nhưng nếu giá trị thực sự được máy ảnh hỗ trợ, thì giá trị đó sẽ đến đó. Nếu không, giá trị sẽ dừng trên giá trị gần nhất được hỗ trợ.

Các camera khác có thể sẽ có các nhóm chế độ cài đặt, API cơ bản và tính năng tương tự nhau. Xin lưu ý rằng gPhoto2 là một dự án nguồn mở và việc kiểm tra tự động hoặc thủ công tất cả các mẫu máy ảnh trên thị trường đơn giản là không khả thi, vì vậy các báo cáo sự cố và PR chi tiết luôn được hoan nghênh (nhưng hãy đảm bảo tái tạo sự cố với ứng dụng khách gPhoto2 chính thức trước tiên).

Ghi chú quan trọng về khả năng tương thích trên nhiều nền tảng

Rất tiếc, trên Windows, mọi ứng dụng và dịch vụ các thiết bị, bao gồm cả máy ảnh DSLR, được gán trình điều khiển hệ thống và không tương thích với WebUSB. Nếu muốn thử bản minh hoạ trên Windows, bạn sẽ phải sử dụng một công cụ như Zadig để ghi đè trình điều khiển cho DSLR đã kết nối thành WinUSB hoặc libusb. Phương pháp này hiệu quả đối với tôi và nhiều người dùng khác, nhưng bạn nên tự chịu rủi ro khi sử dụng.

Trên Linux, bạn có thể cần phải đặt quyền tuỳ chỉnh để cho phép truy cập vào DSLR của mình qua WebUSB, mặc dù việc này còn phụ thuộc vào phạm vi phân phối của bạn.

Trên macOS và Android, bản minh hoạ sẽ hoạt động ngay lập tức. Nếu bạn đang dùng thử trên điện thoại Android, hãy nhớ chuyển sang chế độ ngang vì tôi đã không dành nhiều nỗ lực để làm cho nó phản hồi (chúng tôi hoan nghênh các quan hệ PR!):

Điện thoại Android kết nối với máy ảnh Canon qua cáp USB-C.
Bản minh hoạ tương tự chạy trên điện thoại Android. Ảnh của Surma.

Để xem hướng dẫn chuyên sâu hơn về việc sử dụng WebUSB trên nhiều nền tảng, hãy xem phần "Những điểm cần cân nhắc cho từng nền tảng" trong phần "Xây dựng thiết bị cho WebUSB".

Thêm một phần phụ trợ mới vào libusb

Bây giờ, hãy cùng tìm hiểu chi tiết kỹ thuật. Mặc dù có thể cung cấp API shim tương tự như libusb (những người khác đã thực hiện việc này trước đây) và liên kết các ứng dụng khác dựa vào đó, nhưng phương pháp này dễ gặp lỗi và khiến việc mở rộng hoặc bảo trì thêm khó khăn hơn. Tôi muốn làm điều đúng đắn, theo cách có thể được đóng góp ngược trở lại và hợp nhất thành libusb trong tương lai.

May mắn thay, README của libusb cho biết:

“libusb được tóm tắt nội bộ theo cách có thể hy vọng chuyển được sang các hệ điều hành khác. Vui lòng xem tệp PORTING để biết thêm thông tin."

libusb được cấu trúc theo cách trong đó API công khai tách biệt với "phần phụ trợ". Các phần phụ trợ đó chịu trách nhiệm liệt kê, mở, đóng và thực sự giao tiếp với thiết bị thông qua các API cấp thấp của hệ điều hành. Đây là cách libusb đã loại bỏ sự khác biệt giữa Linux, macOS, Windows, Android, OpenBSD/NetBSD, Haiku và Sunis và hoạt động trên tất cả các nền tảng này.

Việc tôi phải làm là thêm một phần phụ trợ khác cho "hệ điều hành" Emscripten+WebUSB. Quy trình triển khai cho các phần phụ trợ đó nằm trong thư mục 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

Mỗi phần phụ trợ bao gồm tiêu đề libusbi.h với các loại và trình trợ giúp phổ biến, đồng thời cần hiển thị biến usbi_backend thuộc loại usbi_os_backend. Ví dụ: phần phụ trợ của Windows sẽ có dạng như sau:

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

Xem qua các thuộc tính, chúng ta có thể thấy cấu trúc bao gồm tên phần phụ trợ, một tập hợp các chức năng của phần phụ trợ, trình xử lý cho nhiều hoạt động USB cấp thấp dưới dạng con trỏ hàm và cuối cùng là kích thước cần phân bổ để lưu trữ dữ liệu cấp thiết bị/ngữ cảnh/chuyển riêng.

Các trường dữ liệu riêng tư ít nhất cũng hữu ích cho việc lưu trữ tên người dùng hệ điều hành cho tất cả những điều đó, vì nếu không có tên người dùng, chúng ta sẽ không biết bất kỳ thao tác cụ thể nào được áp dụng cho mục nào. Trong quá trình triển khai web, hệ điều hành xử lý sẽ là các đối tượng JavaScript WebUSB cơ bản. Cách tự nhiên để biểu thị và lưu trữ chúng trong Emscripten là thông qua lớp emscripten::val. Lớp này được cung cấp như một phần của Embind (hệ thống liên kết của Emscripten).

Hầu hết phần phụ trợ trong thư mục này đều được triển khai trong C, nhưng một vài phần phụ trợ được triển khai trong C++. Embind chỉ hoạt động với C++ nên lựa chọn này là dành cho tôi và tôi đã thêm libusb/libusb/os/emscripten_webusb.cpp với cấu trúc bắt buộc và với sizeof(val) cho các trường dữ liệu riêng tư:

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

Lưu trữ các đối tượng WebUSB làm xử lý thiết bị

libusb cung cấp con trỏ sẵn sàng sử dụng tới khu vực được phân bổ cho dữ liệu riêng tư. Để xử lý các con trỏ đó dưới dạng thực thể val, tôi đã thêm các trình trợ giúp nhỏ để tạo các con trỏ tại chỗ, truy xuất chúng dưới dạng tệp tham chiếu và di chuyển các giá trị ra ngoài:

// 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 web không đồng bộ trong ngữ cảnh C đồng bộ

Hiện cần một cách để xử lý các API WebUSB không đồng bộ, trong đó libusb yêu cầu các hoạt động đồng bộ. Đối với vấn đề này, tôi có thể sử dụng tính năng Asyncify, cụ thể hơn là tích hợp Embind qua val::await().

Tôi cũng muốn xử lý chính xác các lỗi WebUSB và chuyển đổi các lỗi đó thành mã lỗi libusb, nhưng Embind hiện không có cách nào để xử lý các ngoại lệ đối với JavaScript hoặc việc từ chối Promise từ phía C++. Bạn có thể giải quyết vấn đề này bằng cách phát hiện sự từ chối ở phía JavaScript và chuyển đổi kết quả thành đối tượng { error, value } mà giờ đây có thể được phân tích cú pháp an toàn từ phía C++. Tôi thực hiện việc này bằng cách kết hợp macro EM_JS và API 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()};
  }
};

Bây giờ, tôi có thể sử dụng promise_result::await() trên mọi Promise được trả về từ các hoạt động WebUSB, đồng thời kiểm tra riêng các trường errorvalue của nó.

Ví dụ: truy xuất val biểu thị một USBDevice từ libusb_device_handle, gọi phương thức open(), đang chờ kết quả và trả về một mã lỗi dưới dạng mã trạng thái libusb có dạng như sau:

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

Liệt kê thiết bị

Tất nhiên, trước khi tôi có thể mở bất kỳ thiết bị nào, libusb cần truy xuất danh sách các thiết bị hiện có. Phần phụ trợ phải triển khai thao tác này thông qua trình xử lý get_device_list.

Khó khăn là không giống như trên các nền tảng khác, không có cách nào để liệt kê tất cả thiết bị USB được kết nối trên web vì lý do bảo mật. Thay vào đó, quy trình này sẽ được chia thành hai phần. Trước tiên, ứng dụng web yêu cầu các thiết bị có những thuộc tính cụ thể thông qua navigator.usb.requestDevice() và người dùng sẽ tự chọn thiết bị họ muốn hiển thị hoặc từ chối lời nhắc cấp quyền. Sau đó, ứng dụng sẽ liệt kê các thiết bị đã được phê duyệt và được kết nối qua navigator.usb.getDevices().

Ban đầu, tôi cố sử dụng requestDevice() trực tiếp trong quá trình triển khai trình xử lý get_device_list. Tuy nhiên, việc hiện lời nhắc cấp quyền kèm theo danh sách các thiết bị đã kết nối được xem là một hoạt động nhạy cảm và phải được kích hoạt bằng hoạt động tương tác của người dùng (chẳng hạn như lượt nhấp vào nút trên một trang), nếu không thì thao tác này sẽ luôn trả về một lời hứa bị từ chối. các ứng dụng libusb thường muốn liệt kê các thiết bị được kết nối khi khởi động ứng dụng, vì vậy, không thể sử dụng requestDevice().

Thay vào đó, tôi phải để lệnh gọi navigator.usb.requestDevice() cho nhà phát triển cuối và chỉ hiển thị các thiết bị đã được phê duyệt của 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;
}

Hầu hết mã phụ trợ đều sử dụng valpromise_result theo cách tương tự như đã trình bày ở trên. Có một vài thủ thuật thú vị hơn trong mã xử lý chuyển dữ liệu, nhưng những chi tiết triển khai đó không quá quan trọng đối với mục đích của bài viết này. Đừng quên kiểm tra mã và nhận xét trên GitHub nếu bạn quan tâm.

Chuyển đổi vòng lặp sự kiện sang web

Một phần nữa về cổng libusb mà tôi muốn thảo luận là xử lý sự kiện. Như đã mô tả trong bài viết trước, hầu hết API trong ngôn ngữ hệ thống như C đều đồng bộ và việc xử lý sự kiện cũng không phải là ngoại lệ. Phương thức này thường được triển khai thông qua một vòng lặp vô hạn "thăm dò" (cố gắng đọc dữ liệu hoặc chặn quá trình thực thi cho đến khi có một số dữ liệu) từ một tập hợp các nguồn I/O bên ngoài và khi ít nhất một trong các nguồn đó phản hồi, sẽ chuyển dữ liệu đó dưới dạng sự kiện cho trình xử lý tương ứng. Sau khi trình xử lý hoàn tất, đối tượng điều khiển sẽ quay lại vòng lặp và tạm dừng cho một cuộc thăm dò ý kiến khác.

Có một vài vấn đề với phương pháp này trên web.

Thứ nhất, WebUSB không và không thể hiển thị tay cầm thô của các thiết bị cơ bản, vì vậy việc thăm dò trực tiếp các thiết bị đó không phải là một lựa chọn. Thứ hai, libusb sử dụng API eventfdpipe cho các sự kiện khác cũng như để xử lý quá trình chuyển trên hệ điều hành không có xử lý thiết bị thô, nhưng eventfd hiện không được hỗ trợ trong Emscripten và pipe, mặc dù được hỗ trợ, hiện không phù hợp với quy cách và không thể chờ sự kiện.

Cuối cùng, vấn đề lớn nhất là web có vòng lặp sự kiện riêng. Vòng lặp sự kiện chung này được dùng cho mọi thao tác I/O bên ngoài (bao gồm cả fetch(), bộ tính giờ hoặc trong trường hợp này là WebUSB). Vòng lặp sự kiện này sẽ gọi trình xử lý sự kiện hoặc Promise bất cứ khi nào các thao tác tương ứng kết thúc. Việc thực thi một vòng lặp sự kiện vô hạn, lồng nhau khác sẽ chặn không cho vòng lặp sự kiện của trình duyệt tiếp tục. Điều này có nghĩa là không chỉ giao diện người dùng sẽ không phản hồi mà còn mã sẽ không bao giờ nhận được thông báo cho cùng các sự kiện I/O mà nó đang chờ. Việc này thường dẫn đến tình huống tắc nghẽn và trường hợp này xảy ra khi tôi cố gắng sử dụng libusb trong bản minh hoạ. Trang đã bị treo.

Giống như với thao tác chặn I/O khác, để chuyển các vòng lặp sự kiện như vậy lên web, nhà phát triển cần tìm cách chạy các vòng lặp đó mà không chặn luồng chính. Có một cách là tái cấu trúc ứng dụng để xử lý các sự kiện I/O trong một luồng riêng biệt rồi chuyển kết quả về luồng chính. Hai là sử dụng Không đồng bộ hoá để tạm dừng vòng lặp và chờ các sự kiện theo kiểu không chặn.

Tôi không muốn thực hiện các thay đổi đáng kể đối với libusb hoặc gPhoto2 và tôi đã sử dụng Không đồng bộ hóa để tích hợp Promise, vì vậy đó là đường dẫn tôi đã chọn. Để mô phỏng một biến thể chặn của poll(), đối với bằng chứng về khái niệm ban đầu, tôi đã sử dụng một vòng lặp như dưới đây:

#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

Tác dụng:

  1. Gọi poll() để kiểm tra xem có sự kiện nào mà chương trình phụ trợ đã báo cáo hay không. Nếu có, vòng lặp sẽ dừng lại. Nếu không, quá trình triển khai poll() của Emscripten sẽ ngay lập tức trả về 0.
  2. Gọi emscripten_sleep(0). Hàm này sử dụng Asyncify và setTimeout() nâng cao và được dùng tại đây để chuyển quyền kiểm soát trở lại vòng lặp sự kiện chính của trình duyệt. Điều này cho phép trình duyệt xử lý mọi tương tác của người dùng và các sự kiện I/O, bao gồm cả WebUSB.
  3. Kiểm tra xem thời gian chờ được chỉ định đã hết hạn hay chưa và nếu chưa, hãy tiếp tục vòng lặp.

Như nhận xét đề cập, phương pháp này không tối ưu vì nó liên tục lưu lại toàn bộ ngăn xếp lệnh gọi bằng tính năng Không đồng bộ hoá ngay cả khi chưa có sự kiện USB nào để xử lý (phần lớn thời gian) và vì bản thân setTimeout() có thời lượng tối thiểu là 4 mili giây trong trình duyệt hiện đại. Dù vậy, thiết bị này vẫn đủ khả năng để phát trực tiếp tốc độ 13-14 khung hình/giây qua DSLR.

Sau đó, tôi quyết định cải thiện công cụ này bằng cách tận dụng hệ thống sự kiện trình duyệt. Có một số cách để việc triển khai này có thể được cải thiện hơn nữa, nhưng hiện tại tôi đã chọn phát các sự kiện tuỳ chỉnh trực tiếp trên đối tượng toàn cục mà không liên kết chúng với một cấu trúc dữ liệu libusb cụ thể. Tôi đã thực hiện việc này thông qua cơ chế chờ và thông báo sau đây dựa trên macro 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);
  }
});

Hàm em_libusb_notify() được dùng bất cứ khi nào libusb cố gắng báo cáo một sự kiện, chẳng hạn như hoàn tất quá trình chuyển dữ liệu:

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
}

Trong khi đó, phần em_libusb_wait() dùng để "đánh thức" từ chế độ ngủ Không đồng bộ hoá khi nhận được sự kiện em-libusb hoặc hết thời gian chờ:

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

Do số giờ ngủ và đánh thức giảm đáng kể, nên cơ chế này đã khắc phục các vấn đề về hiệu quả của việc triển khai dựa trên emscripten_sleep() trước đó, đồng thời tăng thông lượng bản minh hoạ DSLR từ 13-14 khung hình/giây lên 30 khung hình/giây trở lên ổn định, đủ cho một nguồn cấp dữ liệu trực tiếp mượt mà.

Xây dựng hệ thống và bài kiểm thử đầu tiên

Sau khi hoàn tất phần phụ trợ, tôi phải thêm phần phụ trợ đó vào Makefile.amconfigure.ac. Bit thú vị duy nhất ở đây là việc sửa đổi cờ dành riêng cho 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']"
  ;;

Thứ nhất, các tệp thực thi trên nền tảng Unix thường không có đuôi tệp. Tuy nhiên, Emscripten sẽ tạo ra đầu ra khác nhau tuỳ thuộc vào việc bạn yêu cầu phần mở rộng nào. Tôi đang sử dụng AC_SUBST(EXEEXT, …) để thay đổi tiện ích thực thi thành .html để mọi tệp thực thi trong gói (kiểm thử và ví dụ) đều trở thành HTML có shell mặc định của Emscripten, giúp tải và tạo thực thể JavaScript và WebAssembly.

Thứ hai, vì tôi đang sử dụng Embind và Asyncify, tôi cần bật các tính năng đó (--bind -s ASYNCIFY) cũng như cho phép tăng trưởng bộ nhớ động (-s ALLOW_MEMORY_GROWTH) thông qua các tham số của trình liên kết. Rất tiếc, không có cách nào để thư viện báo cáo các cờ đó cho trình liên kết, vì vậy mọi ứng dụng sử dụng cổng libusb này cũng sẽ phải thêm cờ trình liên kết tương tự vào cấu hình bản dựng của chúng.

Cuối cùng, như đã đề cập trước đó, WebUSB yêu cầu thực hiện liệt kê thiết bị thông qua một cử chỉ của người dùng. các ví dụ và kiểm thử libusb giả định rằng chúng có thể liệt kê các thiết bị khi khởi động và không thành công với lỗi mà không có thay đổi nào. Thay vào đó, tôi phải tắt tính năng thực thi tự động (-s INVOKE_RUN=0) và hiển thị phương thức callMain() thủ công (-s EXPORTED_RUNTIME_METHODS=...).

Sau khi hoàn tất tất cả những việc này, tôi có thể phân phát các tệp được tạo bằng một máy chủ web tĩnh, khởi chạy WebUSB và chạy các tệp thực thi HTML đó theo cách thủ công với sự trợ giúp của Công cụ cho nhà phát triển.

Ảnh chụp màn hình cho thấy một cửa sổ Chrome có Công cụ cho nhà phát triển đang mở trên trang &quot;testlibusb&quot; phân phát cục bộ. Bảng điều khiển Công cụ cho nhà phát triển đang đánh giá &quot;navigation.usb.requestDevice({ filter: [] })&quot;. Bảng điều khiển này đã kích hoạt lời nhắc cấp quyền và hiện đang yêu cầu người dùng chọn một thiết bị USB cần chia sẻ với trang. Hiện đang chọn ILCE-6600 (máy ảnh Sony).

Ảnh chụp màn hình bước tiếp theo vẫn mở Công cụ cho nhà phát triển. Sau khi bạn chọn thiết bị, Bảng điều khiển đã đánh giá biểu thức mới &quot;Module.callMain([&#39;-v&#39;])&quot;, biểu thức này đã thực thi ứng dụng &quot;testlibusb&quot; ở chế độ chi tiết. Đầu ra cho thấy nhiều thông tin chi tiết về máy ảnh USB đã kết nối trước đó: nhà sản xuất Sony, sản phẩm ILCE-6600, số sê-ri, cấu hình, v.v.

Điều này trông có vẻ không nhiều, nhưng khi chuyển các thư viện sang một nền tảng mới, việc chuyển sang giai đoạn tạo ra đầu ra hợp lệ lần đầu tiên là điều khá thú vị!

Sử dụng cổng

Như đã đề cập ở trên, cổng phụ thuộc vào một số tính năng Emscripten cần được bật ở giai đoạn liên kết của ứng dụng. Nếu muốn sử dụng cổng libusb này trong ứng dụng của riêng mình, thì sau đây là những việc bạn cần làm:

  1. Tải libusb mới nhất xuống dưới dạng tệp lưu trữ trong bản dựng hoặc thêm tệp này dưới dạng mô-đun con git trong dự án của bạn.
  2. Chạy autoreconf -fiv trong thư mục libusb.
  3. Chạy emconfigure ./configure –host=wasm32 –prefix=/some/installation/path để khởi chạy dự án cho quá trình biên dịch chéo và thiết lập một đường dẫn mà bạn muốn đặt các cấu phần phần mềm đã tạo.
  4. Chạy emmake make install.
  5. Trỏ ứng dụng hoặc thư viện cấp cao hơn của bạn để tìm kiếm libusb theo đường dẫn đã chọn trước đó.
  6. Thêm các cờ sau vào đối số liên kết của ứng dụng: --bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH.

Thư viện này hiện có một vài hạn chế:

  • Không hỗ trợ huỷ chuyển. Đây là một hạn chế của WebUSB, mà do thiếu tính năng huỷ chuyển dữ liệu trên nhiều nền tảng trong chính libusb.
  • Không hỗ trợ chuyển đồng thời. Sẽ không có gì khó khăn khi thêm tính năng này bằng cách làm theo các ví dụ như triển khai các chế độ chuyển hiện có. Tuy nhiên, đây cũng là một chế độ hiếm gặp và tôi không có bất kỳ thiết bị nào để thử nghiệm, vì vậy, tạm thời tôi để chế độ này không được hỗ trợ. Nếu bạn có những thiết bị như vậy và muốn đóng góp cho thư viện, chúng tôi rất hoan nghênh các quan hệ công chúng!
  • Các hạn chế trên nhiều nền tảng như đã đề cập trước đó. Những giới hạn đó do hệ điều hành đặt ra, vì vậy chúng ta không thể làm gì nhiều trong trường hợp này, ngoại trừ việc yêu cầu người dùng ghi đè trình điều khiển hoặc quyền. Tuy nhiên, nếu bạn đang chuyển đổi thiết bị HID hoặc thiết bị nối tiếp, bạn có thể làm theo ví dụ về libusb và chuyển một số thư viện khác sang API Fugu khác. Ví dụ: bạn có thể chuyển một hidapi thư viện C sang WebHID rồi loại bỏ hoàn toàn các vấn đề đó, liên quan đến quyền truy cập USB cấp thấp.

Kết luận

Trong bài đăng này, tôi đã chỉ cho với sự trợ giúp của các API Emscripten, Asyncify và Fugu, ngay cả các thư viện cấp thấp như libusb cũng có thể được chuyển sang web bằng một số thủ thuật tích hợp.

Việc chuyển đổi các thư viện cấp thấp cần thiết và được sử dụng rộng rãi như vậy đặc biệt hữu ích, vì đổi lại, nó cho phép đưa các thư viện cấp cao hơn hoặc thậm chí toàn bộ ứng dụng lên web. Việc này sẽ mở ra những trải nghiệm trước đây chỉ dành cho người dùng của một hoặc hai nền tảng, cho tất cả các loại thiết bị và hệ điều hành, giúp bạn có được những trải nghiệm đó chỉ bằng một cú nhấp chuột vào đường liên kết.

Trong bài đăng tiếp theo, tôi sẽ hướng dẫn các bước xây dựng bản minh hoạ gPhoto2 trên web. Bản minh hoạ này không chỉ truy xuất thông tin thiết bị mà còn sử dụng rộng rãi tính năng chuyển của libusb. Trong thời gian chờ đợi, tôi hy vọng bạn thấy ví dụ về libusb đầy cảm hứng và sẽ thử bản minh hoạ, thử nghiệm với chính thư viện hoặc thậm chí tiếp tục chuyển một thư viện được sử dụng rộng rãi khác sang một trong những API Fugu.