將 USB 應用程式轉移到網路。第 1 部分:libusb

瞭解如何使用 WebAssembly 和 Fugu API,將與外部裝置互動的程式碼移植到網路上。

Ingvar Stepanyan
Ingvar Stepanyan

前一篇文章中,我展示瞭如何使用 File System Access API、WebAssembly 和 Asyncify,將使用檔案系統 API 的應用程式移植到網路上。現在我想繼續討論相同的主題,也就是整合 Fugu API 與 WebAssembly,並將應用程式移植到網路,同時保留重要功能。

我會展示如何將 libusb (以 C 語言編寫的熱門 USB 程式庫) 轉移至 WebAssembly (透過 Emscripten)、Asyncify 和 WebUSB,藉此將與 USB 裝置通訊的應用程式攜碼轉移至網路。

首要之務:示範

移植程式庫時最重要的步驟就是選擇合適的示範,其中展示可攜式程式庫的功能,可讓您以多種方式進行測試,同時在視覺上發揮吸引力。

我選擇的想法是從遠端控制數位單眼相機具體來說,開放原始碼專案 gPhoto2 已在這個領域中,具有反向工程及針對多種數位相機提供支援功能。它支援多種通訊協定,但我最想看的是 USB 支援,可透過 libusb 執行。

以下將說明建立示範步驟的步驟 (分兩部分)。這篇網誌文章會說明我如何攜帶 Libusb 本身,以及將其他熱門程式庫移植到 Fugu API 時可能須具備哪些技巧。在第二篇文章中,我會進一步說明攜碼轉移及整合 gPhoto2 本身。

最後,我取得了運作中的網頁應用程式,能夠從數位單眼相機預覽即時影像,並透過 USB 控制設定。歡迎查看直播或預錄的示範影片,再閱讀技術詳細資料:

在連接 Sony 攝影機的筆記型電腦上執行這項示範

攝影機特定搞怪注意事項

你可能已經注意到,變更設定需要花一點時間處理影片。如同其他多數問題,這與 WebAssembly 或 WebUSB 的效能無關,而是 gPhoto2 與示範用的特定攝影機互動。

Sony a6600 不會公開 API 來直接設定 ISO、光圈或快門速度等值,而是僅提供指令以指定步數增加或減少這類值。為了讓情況更複雜,此 API 不會傳回實際支援的值清單,因為在許多 Sony 相機模型中,傳回的清單似乎都採用硬式編碼。

設定其中一個值時,gPhoto2 沒有其他選擇,需要進行以下動作:

  1. 沿著所選值的方向出價一個或幾步。
  2. 請稍待片刻,讓攝影機更新設定。
  3. 請回讀相機實際降落的價值。
  4. 確認最後一個步驟沒有跳動到所需值,也沒有包裝在清單結尾或開頭。
  5. 樂趣無限循環

可能需要一點時間,但如果相機確實支援該值,這個值也會在報告中,如果不支援,則會以最接近的支援值開始停止。

其他相機可能有不同的設定組合、基礎 API 和奇怪。請注意,gPhoto2 是開放原始碼專案,而且即使所有相機模型的自動化或手動測試並非可行,因此也歡迎您提供詳細的問題報告和 PR (但請務必先透過官方 gPhoto2 用戶端重現問題)。

關於跨平台相容性的重要須知

可惜的是 Windows 任何「已知」裝置 (包括 DSLR 攝影機) 獲派的系統驅動程式與 WebUSB 不相容。如要在 Windows 上試用,請使用 Zadig 之類的工具,將連接的 DSLR 驅動程式覆寫為 WinUSB 或 libusb。這個方法適用於我和許多其他使用者,但請自行承擔使用風險。

在 Linux 上,您可能需要設定自訂權限,才能透過 WebUSB 存取 DSLR,但這取決於您的發行版類型。

在 macOS 和 Android 上,示範模式應可立即使用。如果你是在 Android 手機上試用這項功能,請務必切換至橫向模式,因為我用了一點方式設計回應 (歡迎 PR !):

Android 手機已透過 USB-C 傳輸線連接至 Canon 相機。
在 Android 手機上運作的相同示範內容。相片來源:Surma

如需深入瞭解如何使用 WebUSB 跨平台使用指南,請參閱「平台特定注意事項」。

將新後端新增至 libusb

現在來談談技術細節雖然可提供與 libusb 類似的輔助 API (之前已有其他人使用),並將其他應用程式連結在一起,但此方法很容易出錯,還會造成進一步擴充或維護的困難。我希望能把事情做得很好,這樣的話,可能會讓上游在日後造成再整合到垃圾車上。

幸好,libusb README 說:

「libusb 的架構在內部進行抽象化,希望能夠應用於其他作業系統。詳情請參閱 PORTING 檔案。」

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 變數。例如,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),
};

透過屬性查看結構體,我們可以發現結構體包含後端名稱、其功能組合、適用於各種低階 USB 運算的處理常式 (以功能指標形式),以及最後分配用於儲存私人裝置/情境/傳輸層級資料的大小。

私人資料欄位至少要用來儲存 OS 處理的一切資料,因為我們不知道任何指定作業適用於哪個項目。在網路實作中,OS 控制代碼為基礎的 WebUSB JavaScript 物件。在 Emscripten 中呈現和儲存這些資料的自然方法是透過 emscripten::val 類別,這是 Embind (Emscripten 的繫結系統) 的一部分。

資料夾內的大部分後端都是以 C 實作,但有些後端是在 C++ 中實作。Embind 只能搭配 C++ 使用,因此已為我自己選擇,並為私人資料欄位新增了 libusb/libusb/os/emscripten_webusb.cpp,並提供必要結構和 sizeof(val)

#include <emscripten.h>
#include <emscripten/val.h>

#include "libusbi.h"

using namespace emscripten;

// …function implementations

const usbi_os_backend usbi_backend = {
  .name = "Emscripten + WebUSB backend",
  .caps = LIBUSB_CAP_HAS_CAPABILITY,
  // …handlers—function pointers to implementations above
  .device_priv_size = sizeof(val),
  .transfer_priv_size = sizeof(val),
};

將 WebUSB 物件儲存為裝置控制代碼

libusb 會提供立即可用的指標,指向分配的私人資料區域。為了將這些指標做為 val 執行個體使用,我們已新增一些小型輔助程式來就地建構指標、將其擷取為參照,然後移出值:

// We store an Embind handle to WebUSB USBDevice in "priv" metadata of
// libusb device, this helper returns a pointer to it.
struct ValPtr {
 public:
  void init_to(val &&value) { new (ptr) val(std::move(value)); }

  val &get() { return *ptr; }
  val take() { return std::move(get()); }

 protected:
  ValPtr(val *ptr) : ptr(ptr) {}

 private:
  val *ptr;
};

struct WebUsbDevicePtr : ValPtr {
 public:
  WebUsbDevicePtr(libusb_device *dev)
      : ValPtr(static_cast<val *>(usbi_get_device_priv(dev))) {}
};

val &get_web_usb_device(libusb_device *dev) {
  return WebUsbDevicePtr(dev).get();
}

struct WebUsbTransferPtr : ValPtr {
 public:
  WebUsbTransferPtr(usbi_transfer *itransfer)
      : ValPtr(static_cast<val *>(usbi_get_transfer_priv(itransfer))) {}
};

同步 C 結構定義中的非同步網路 API

現在,您需要一種方法來處理 libusb 預期執行同步作業的非同步 WebUSB API。為此,我可以透過 val::await() 使用 Asyncify,具體來說是 Embind 整合。

我也想正確處理 WebUSB 錯誤,並轉換成 libusb 錯誤代碼,但 Embind 目前無法處理任何來自 C++ 端的 JavaScript 例外狀況或 Promise 拒絕情形。如要解決這個問題,可以擷取 JavaScript 端的拒絕作業,並將結果轉換為 { 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(),並分別檢查其 errorvalue 欄位。

舉例來說,從 libusb_device_handle 擷取代表 USBDeviceval、呼叫其 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;
}

大部分的後端程式碼使用 valpromise_result 的方式如上所示。資料移轉處理程式碼有一些有趣的駭客攻擊,但這些實作細節較不重要。請務必前往 GitHub 查看程式碼和註解 (如果有興趣)。

將事件迴圈移植到網頁

接下來要討論的一大段 libusb 連接埠是事件處理。如前文所述,C 等系統語言中的大部分 API 都是同步的,事件處理也不例外。通常以「輪詢」的無限迴圈導入從一組外部 I/O 來源讀取資料或封鎖執行作業,直到有可用資料為止,並在至少有一個回應時,將該事件以事件形式傳遞至對應的處理常式。處理常式完成後,控制項會回到迴圈,並且會暫停另一個輪詢。

在網路上,這種方法有幾個問題。

首先,WebUSB 不會且無法公開基礎裝置的原始控制代碼,因此請勿直接輪詢這些控制代碼。其次,libusb 會使用 eventfdpipe API 處理其他事件,以及在沒有原始裝置控制的作業系統上處理轉移作業,但 Emscripten 和 pipe 目前不支援 eventfd,但是目前不符合規格,無法等待事件。

最後,最大的問題在於網路有自己的事件迴圈。這個全域事件迴圈可用於任何外部 I/O 作業 (包括 fetch()、計時器或 WebUSB),且每當相應的作業完成時,都會叫用事件或 Promise 處理常式。執行另一個巢狀、無限事件迴圈,會使瀏覽器的事件迴圈無法運作,這不僅表示 UI 沒有回應,程式碼也永遠不會收到所等待的相同 I/O 事件的通知。這通常會導致死結,當我嘗試在示範中使用 Libusb 時,也會發生這種狀況。頁面凍結。

和其他造成阻斷的 I/O 一樣,開發人員需要設法讓這類事件迴圈在不封鎖主執行緒的情況下執行,才能順利轉移至網路。其中一個方法是重構應用程式,以處理獨立執行緒中的 I/O 事件,並將結果傳回主要執行緒。另一種是使用 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(),並可在此處將控制權轉回主要瀏覽器事件迴圈。這可讓瀏覽器處理任何使用者互動和 I/O 事件,包括 WebUSB。
  3. 檢查指定的逾時時間是否已過期,如未過期,則繼續進行迴圈。

如註解所言,這種做法並非最佳,因為即使沒有可處理的 USB 事件 (在大部分時間),這個做法仍能透過 Asyncify 儲存整個呼叫堆疊,而且在新版瀏覽器中,setTimeout() 本身的持續時間最短為 4 毫秒。儘管如此,在概念驗證中,這款遊戲以數位單眼相機拍攝的每秒影格數為 13 至 14。

後來,我決定利用瀏覽器事件系統來改善這項功能。有幾種方法可以進一步改善這個實作方式,但目前我選擇直接在全域物件上觸發自訂事件,而不是與特定 Libusb 資料結構建立關聯。我已根據 EM_ASYNC_JS 巨集執行下列等待程序和通知機制:

EM_JS(void, em_libusb_notify, (void), {
  dispatchEvent(new Event("em-libusb"));
});

EM_ASYNC_JS(int, em_libusb_wait, (int timeout), {
  let onEvent, timeoutId;

  try {
    return await new Promise(resolve => {
      onEvent = () => resolve(0);
      addEventListener('em-libusb', onEvent);

      timeoutId = setTimeout(resolve, timeout, -1);
    });
  } finally {
    removeEventListener('em-libusb', onEvent);
    clearTimeout(timeoutId);
  }
});

每當 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 事件或逾時逾時時,Asyncify 即會休眠:

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.amconfigure.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']"
  ;;

首先,Unx 平台上的執行檔通常不會有副檔名。不過,Escripten 會根據你要求的副檔名,產生不同的輸出內容。我使用 AC_SUBST(EXEEXT, …) 將可執行檔擴充功能變更為 .html,讓套件中的所有執行檔 (測試和範例) 變成採用 Emscripten 預設 shell 的 HTML,負責載入 JavaScript 和 WebAssembly 的例項。

其次,因為我使用 Embind 和 Asyncify,所以必須啟用這些功能 (--bind -s ASYNCIFY),並透過連接器參數允許動態記憶體增加 (-s ALLOW_MEMORY_GROWTH)。遺憾的是,程式庫無法向連結器回報這些標記,因此凡是使用此 libusb 連接埠的應用程式,也都必須在建構設定中加入相同的連結器標記。

最後,如前所述,WebUSB 規定必須透過使用者手勢完成裝置列舉。libusb 範例和測試假設它們可以在啟動時列舉裝置,並在未進行變更的情況下發生錯誤。我必須停用自動執行功能 (-s INVOKE_RUN=0),並公開手動 callMain() 方法 (-s EXPORTED_RUNTIME_METHODS=...)。

完成後,我可以使用靜態網路伺服器提供產生的檔案、初始化 WebUSB,然後透過 DevTools 手動執行這些 HTML 執行檔。

螢幕截圖:顯示已在本機提供的「testlibusb」頁面中開啟開發人員工具的 Chrome 視窗。開發人員工具控制台正在評估 `navigator.usb.requestDevice({ Filter: [] })`。此動作觸發了權限提示,且目前要求使用者選擇應與網頁共用的 USB 裝置。目前已選取 ILCE-6600 (Sony 相機)。

下一個步驟的螢幕截圖,開發人員工具仍處於開啟狀態。選取裝置後,主控台會評估新的運算式 `Module.callMain([&#39;-v&#39;])`,此運算式會在詳細模式中執行 `testlibusb` 應用程式。輸出內容會顯示先前連接的 USB 攝影機的各種詳細資訊:製造商 Sony、產品 ILCE-6600、序號、設定等。

儘管事情不大,但當將程式庫移植到新平台時,把程式庫移到新平台首次產生有效輸出內容的階段,可說是件令人振奮的事!

使用通訊埠

前文所述,通訊埠取決於部分 Emscripten 功能,但這項功能目前必須在應用程式連結階段啟用。如果您要在自己的應用程式中使用這個 libusb 連接埠,請執行以下步驟:

  1. 將最新的 libusb 下載為封存檔,或是將其新增為專案中的 Git 子模組。
  2. libusb 資料夾中執行 autoreconf -fiv
  3. 執行 emconfigure ./configure –host=wasm32 –prefix=/some/installation/path 來初始化專案以進行跨編譯,並設定建構成果的所在路徑。
  4. 執行 emmake make install
  5. 指向您的應用程式或更高層級的程式庫,以搜尋先前所選路徑下的 libusb。
  6. 將下列標記新增至應用程式的連結引數:--bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH

程式庫目前有一些限制:

  • 不支援取消轉移作業。這是 WebUSB 的限制,因而導致 libusb 本身並未取消跨平台傳輸作業。
  • 不支援均等轉移。導入現有傳輸模式的範例並不困難,但也採用較罕見的模式,也沒有裝置能進行測試,因此我目前將其標示為不支援。如果您已擁有這類裝置,並想為資料庫貢獻一己之力,歡迎您的 PR!
  • 先前提到的跨平台限制。這些限制是由作業系統實施,所以我們不能這麼做,除非使用者覆寫驅動程式或權限。不過,如果您要移植 HID 或序列裝置,可以依照 libusb 範例,將其他程式庫的通訊埠移植到其他 Fugu API。例如,您可以將 C 程式庫 hidapi 移植到 WebHID,並側面處理與低階 USB 存取相關聯的問題。

結論

在這篇文章中,我說明瞭 Emscripten、Asyncify 和 Fugu API 等低階程式庫 (如 libusb) 的方法,都能透過幾個整合技巧轉移到網路上。

移植如此重要且廣為使用的低階程式庫,可帶來莫大助益,因為這樣也能將較高階的程式庫,甚至是整個應用程式帶入網路。過去只有一或兩個平台的使用者才能進入所有的裝置和作業系統,只要點擊一下就能觀看內容。

下一篇文章中,我們會逐步說明建立 Web gPhoto2 示範的相關步驟。這個示範不僅會擷取裝置資訊,還會廣泛使用 libusb 的傳輸功能。與此同時,希望您找到 libusb 範例激發靈感,並嘗試展示、操作程式庫本身,或者您也可以直接把其他廣為使用的程式庫移植到其中一個 Fugu API。