Przenoszenie aplikacji USB do internetu. Część 1. libusb

Dowiedz się, jak za pomocą interfejsów API WebAssembly i Fugu można przenosić do internetu kod, który wchodzi w interakcję z urządzeniami zewnętrznymi.

W poprzednim poście pokazałem, jak przenosić do internetu aplikacje przy użyciu interfejsów API systemu plików przy użyciu interfejsów File System Access API, WebAssembly i Asyncify. Teraz chcę kontynuować ten sam temat: integracja interfejsów API Fugu z WebAssembly oraz przenoszenie aplikacji do internetu z zachowaniem ważnych funkcji.

Pokażę, jak aplikacje komunikujące się z urządzeniami USB można przenieść do internetu przez port libusb – popularnej biblioteki USB napisanej w języku C – do WebAssembly (przy użyciu Emscripten), Asyncify i WebUSB.

Zacznijmy od początku: prezentacja

Najważniejszą rzeczą do przeniesienia biblioteki jest wybranie odpowiedniej wersji demonstracyjnej, która prezentuje możliwości biblioteki przeniesionej, umożliwiając jej testowanie na różne sposoby przy jednoczesnym zachowaniu atrakcyjności wizualnej.

Wybrałem pilot do lustrzanki cyfrowej. W szczególności projekt typu open source gPhoto2 działa w tej przestrzeni na tyle długo, aby możliwe było przeanalizowanie wsteczne i wdrożenie obsługi szerokiej gamy aparatów cyfrowych. Obsługuje kilka protokołów, ale najbardziej zainteresowała mnie obsługa USB, którą można obsługiwać za pomocą biblioteki libusb.

Etapy tworzenia tej prezentacji przedstawię w 2 częściach. W tym poście opiszę sposób przeniesienia biblioteki libusb i tego, co może być konieczne do przeniesienia innych popularnych bibliotek do interfejsów API Fugu. W drugim poście przedstawię szczegółowe informacje o przenoszeniu i integrowaniu programu gPhoto2.

Ostatecznie dostałem działającą aplikację internetową, która wyświetla podgląd transmisji na żywo z lustrzanki cyfrowej i może kontrolować jej ustawienia przez USB. Zanim zapoznasz się ze szczegółami technicznymi, obejrzyj transmisję na żywo lub nagraną wcześniej wersję demonstracyjną:

Prezentacja uruchomiona na laptopie podłączonym do kamery Sony.

Uwaga na temat nietypowych funkcji kamery

Mogłeś zauważyć, że zmiana ustawień w filmie zajmuje trochę czasu. Podobnie jak w przypadku większości innych problemów, przyczyna problemu nie jest związana z wydajnością WebAssembly ani WebUSB, ale przez sposób, w jaki gPhoto2 wchodzi w interakcję z kamerą wybraną na potrzeby wersji demonstracyjnej.

Urządzenie Sony A6600 nie używa interfejsu API do bezpośredniego ustawiania wartości takich jak ISO, przysłona czy szybkość migawki, a zamiast tego udostępnia polecenia do ich zwiększania lub zmniejszania o określoną liczbę kroków. Aby jeszcze bardziej skomplikować sprawy, nie zwraca też listy rzeczywiście obsługiwanych wartości – zwracana lista wydaje się być zakodowana na stałe w wielu modelach aparatów Sony.

Podczas ustawiania jednej z tych wartości gPhoto2 nie ma innego wyjścia niż:

  1. Zrób krok (lub kilka) w kierunku wybranej wartości.
  2. Poczekaj, aż kamera zaktualizuje ustawienia.
  3. Odczytaj wartość, do której rzeczywiście wylądowała kamera.
  4. Sprawdź, czy ostatni krok nie przeskoczył po wybranej wartości i nie zawijał się na końcu lub na początku listy.
  5. Powtórz.

Może to trochę potrwać, ale jeśli kamera rzeczywiście obsługuje daną wartość, zostanie ona tam ustawiona, a w przeciwnym razie zostanie zatrzymana na najbliższej obsługiwanej wartości.

Inne aparaty będą prawdopodobnie miały inne zestawy ustawień, bazowe interfejsy API i nietypowe cechy. Pamiętaj, że gPhoto2 jest projektem open source i zautomatyzowane lub ręczne przetestowanie wszystkich modeli aparatów jest po prostu niewykonalne. Zachęcamy więc do zapoznania się ze szczegółowymi raportami o problemach i działaniami PR (najpierw odtwórz te problemy w oficjalnym kliencie gPhoto2).

Ważne uwagi dotyczące zgodności na wielu platformach

Niestety w systemie Windows wszystkie „dobrze znane” urządzenia, w tym lustrzanki cyfrowe, mają przypisany sterownik systemowy, który nie jest zgodny z WebUSB. Jeśli chcesz wypróbować wersję demonstracyjną w systemie Windows, użyj narzędzia takiego jak Zadig, aby zastąpić sterownik podłączonej lustrzanki cyfrowej do WinUSB lub libusb. Ta metoda sprawdza się u mnie i u wielu innych użytkowników, ale korzystaj z niej na własne ryzyko.

W systemie Linux konieczne będzie ustawienie uprawnień niestandardowych, aby zezwolić na dostęp do lustrzanki cyfrowej przez WebUSB (zależnie od dystrybucji).

W przypadku systemów macOS i Android wersja demonstracyjna powinna działać od razu po uruchomieniu. Jeśli testujesz obraz na telefonie z Androidem, pamiętaj o przejściu w tryb poziomy, ponieważ włożyłem ja w to niezbyt długo, aby obraz był elastycznie.

Telefon z Androidem podłączony do aparatu Canon za pomocą kabla USB-C.
Ta sama wersja demonstracyjna na telefonie z Androidem. Autor zdjęcia: Surma.

Szczegółowy przewodnik po korzystaniu z WebUSB na wielu platformach znajdziesz w sekcji „Zalecenia dotyczące poszczególnych platform” artykułu „Tworzenie urządzenia z interfejsem WebUSB”.

Dodawanie nowego backendu do biblioteki libusb

Przejdźmy do szczegółów technicznych. Chociaż można udostępnić interfejs shim API podobny do libusb (co zrobili wcześniej inni użytkownicy) i połączyć z nim inne aplikacje, takie podejście jest podatne na błędy i utrudnia kolejne rozszerzenia lub konserwację. Chciałam robić to jak należy, aby w przyszłości można było dostarczać je z powrotem do biblioteki.

Na szczęście plik README libusb zawiera treść:

„Libusb jest wewnętrznie wyodrębniony, dzięki czemu może zostać przeniesiony do innych systemów operacyjnych. Więcej informacji znajdziesz w pliku PORTING”.

Struktura biblioteki libusb jest taka, że publiczny interfejs API jest oddzielony od „backendów”. Te backendy odpowiadają za wyświetlanie, otwieranie, zamykanie i rzeczywistą komunikację z urządzeniami za pomocą niskopoziomowych interfejsów API systemu operacyjnego. W ten sposób libusb już wyeliminuje różnice między systemami Linux, macOS, Windows, Android, OpenBSD/NetBSD, Haiku i Solaris, a następnie działa na wszystkich tych platformach.

Trzeba było dodać kolejny backend „systemu operacyjnego” Emscripten+WebUSB. Implementacje tych backendów znajdują się w folderze 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

Każdy backend zawiera nagłówek libusbi.h z typowymi typami i elementami pomocniczymi oraz musi udostępnić zmienną usbi_backend typu usbi_os_backend. Na przykład tak wygląda backend systemu 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),
};

Po przejrzeniu właściwości widać, że struktura zawiera nazwę backendu, zbiór jego możliwości, moduły obsługi różnych operacji na USB niskiego poziomu w postaci wskaźników funkcyjnych, a także rozmiary, które mają być przydzielone do przechowywania prywatnych danych na poziomie urządzenia, kontekstu lub transferu.

Pola prywatnych danych są przydatne przynajmniej do przechowywania nicków systemu operacyjnego, bo bez nicków nie wiemy, do którego elementu dana operacja ma zastosowanie. W implementacji internetowej uchwyty systemu operacyjnego będą podstawowymi obiektami JavaScript WebUSB. Naturalnym sposobem ich reprezentowania i przechowywania w Emscripten jest klasa emscripten::val, która jest dostępna w ramach Embind (systemu wiązań Emscripten).

Większość backendów w folderze jest zaimplementowana w języku C, ale niektóre z nich zostały wdrożone w C++. Embind działa tylko w C++, więc wybór został wybrany za mnie, więc dodałem libusb/libusb/os/emscripten_webusb.cpp o wymaganej strukturze i sizeof(val) do pól danych prywatnych:

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

Zapisywanie obiektów WebUSB jako uchwytów urządzenia

Biblioteka libusb udostępnia gotowe wskaźniki do przydzielonego obszaru na potrzeby prywatnych danych. Aby umożliwić działanie tych wskaźników jako instancji val, dodaję małe pomoce, które budują je we własnym miejscu, pobierają jako odwołania i przenoszą wartości:

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

Asynchroniczne internetowe interfejsy API w synchronicznych kontekstach C

Potrzebny był teraz sposób na obsługę asynchronicznych interfejsów WebUSB API w sytuacji, gdy libusb wymaga operacji synchronicznych. W tym celu mogę użyć usługi Asyncify, a dokładniej integracji Embind za pomocą val::await().

Chciałem też prawidłowo obsługiwać błędy WebUSB i przekonwertować je na kody błędów libusb, ale Embind obecnie nie obsługuje wyjątków JavaScript ani odrzuceń funkcji Promise po stronie C++. Problem ten można obejść, przechwytując odrzucenie po stronie JavaScript i przekształcając wynik w obiekt { error, value }, który można teraz bezpiecznie przeanalizować ze strony C++. Została ona utworzona za pomocą kombinacji makra EM_JS i interfejsów 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()};
  }
};

Teraz mogę użyć promise_result::await() w przypadku dowolnych danych Promise zwróconych przez operacje WebUSB i osobno zbadać pola error oraz value.

Na przykład pobranie obiektu val reprezentującego element USBDevice z obiektu libusb_device_handle, wywołanie metody open(), oczekiwanie na wynik i zwrócenie kodu błędu jako kodu stanu biblioteki libusb wygląda tak:

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

Wyliczenie urządzeń

Zanim otworzysz dowolne urządzenie, libusb musi pobrać listę dostępnych urządzeń. Backend musi wdrożyć tę operację za pomocą modułu obsługi get_device_list.

Trudność polega na tym, że w odróżnieniu od innych platform ze względów bezpieczeństwa nie ma możliwości wyliczenia wszystkich podłączonych urządzeń USB w internecie. Proces ten jest natomiast podzielony na 2 części. Najpierw aplikacja internetowa wysyła za pomocą navigator.usb.requestDevice() żądania urządzeń o określonych właściwościach, a użytkownik ręcznie wybiera urządzenie, które chce ujawnić, a które odrzuca prośbę o przyznanie uprawnień. Następnie aplikacja wyświetla listę zatwierdzonych i połączonych urządzeń w narzędziu navigator.usb.getDevices().

Początkowo próbowałem(-am) użyć funkcji requestDevice() bezpośrednio w implementacji modułu obsługi get_device_list. Jednak wyświetlenie prośby o przyznanie uprawnień wraz z listą połączonych urządzeń jest uważane za operację związaną z poufnymi danymi i musi zostać wywołana przez interakcję użytkownika (np. kliknięcie przycisku na stronie). W przeciwnym razie zawsze zwraca odrzuconą obietnicę. Aplikacje libusb często chcą wyświetlać listę połączonych urządzeń przy uruchomieniu, więc użycie requestDevice() nie było możliwe.

Zamiast tego musiałem zostawić wywołanie navigator.usb.requestDevice() u dewelopera i udostępnić tylko zatwierdzone urządzenia z 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;
}

Większość kodu backendu używa elementów val i promise_result w podobny sposób, jak pokazano powyżej. Kod do obsługi przesyłania danych jest o kilku ciekawszych trikach, ale z punktu widzenia tego artykułu nie mają one większego znaczenia. Jeśli interesuje Cię ten temat, zapoznaj się z kodem i komentarzami na GitHubie.

Przenoszenie pętli zdarzeń do internetu

Kolejnym aspektem portu libusb, który chcę omówić, jest obsługa zdarzeń. Jak opisano w poprzednim artykule, większość interfejsów API w językach systemowych, takich jak C, jest synchroniczna i obsługa zdarzeń nie jest wyjątkiem. Zwykle odbywa się to za pomocą nieskończonej pętli, które „ankiety” (próbują odczytać dane lub blokują wykonanie, dopóki nie będą dostępne) ze zbioru zewnętrznych źródeł wejścia-wyjścia. Gdy co najmniej 1 z tych źródeł odpowie, przekaże to zdarzenie jako zdarzenie do odpowiedniego modułu obsługi. Po zakończeniu modułu obsługi element sterujący wraca do pętli i zostaje wstrzymany na czas kolejnej ankiety.

Istnieje kilka problemów z tym podejściem w internecie.

Po pierwsze WebUSB nie ujawnia i nie może ujawniać nieprzetworzonych nicków używanych urządzeń, dlatego bezpośrednie odpytywanie ich nie jest dobrym rozwiązaniem. Po drugie, libusb używa interfejsów API eventfd i pipe do obsługi innych zdarzeń oraz do obsługi transferów w systemach operacyjnych bez nieprzetworzonych nicków urządzeń, ale eventfd nie jest obecnie obsługiwany w Emscripten i pipe, choć jest obsługiwany, obecnie nie spełnia wymagań i nie może czekać na zdarzenia.

Największym problemem jest to, że sieć ma własną pętlę zdarzeń. Ta globalna pętla zdarzeń jest używana w przypadku zewnętrznych operacji wejścia-wyjścia (w tym fetch(), liczników czasu lub w tym przypadku WebUSB) i wywołuje zdarzenia lub moduły obsługi Promise po zakończeniu odpowiednich operacji. Wykonanie kolejnej, zagnieżdżonej, nieskończonej pętli zdarzeń zablokuje pętlę zdarzeń przeglądarki, co oznacza, że nie tylko interfejs użytkownika przestanie reagować, ale też kod nigdy nie będzie otrzymywać powiadomień o tych samych oczekujących zdarzeniach wejścia-wyjścia. Zwykle prowadzi to do zakleszczenia i to właśnie się stało, gdy próbowałem użyć libusb w wersji demonstracyjnej. Strona się zawiesiła.

Podobnie jak w przypadku innych blokujących operacji wejścia-wyjścia, aby przenieść takie pętle zdarzeń do internetu, deweloperzy muszą znaleźć sposób ich uruchomienia bez blokowania wątku głównego. Jednym ze sposobów jest refaktoryzacja aplikacji w celu obsługi zdarzeń wejścia-wyjścia w osobnym wątku i przekazanie wyników z powrotem do głównego. Drugim sposobem jest użycie usługi Asyncify do wstrzymania pętli i oczekiwania na zdarzenia w sposób nieblokujący.

Nie chcę wprowadzać istotnych zmian w bibliotece libusb ani w gPhoto2, a aplikacja Asyncify służy już do integracji z usługą Promise i właśnie ją wybrałem. Aby zasymulować wariant blokujący element poll(), na wstępnym modelu koncepcyjnym wykorzystano pętlę jak poniżej:

#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

Działanie tej funkcji:

  1. Wywołuje poll(), aby sprawdzić, czy jakieś zdarzenia zostały już zgłoszone przez backend. Jeśli występują jakieś błędy, pętla się zatrzymuje. W przeciwnym razie implementacja elementu poll() w Emscripten powróci od razu z wartością 0.
  2. Dzwoni pod numer emscripten_sleep(0). Ta funkcja korzysta z metody Asyncify i setTimeout() w celu uzyskania kontroli z powrotem do głównej pętli zdarzeń przeglądarki. Dzięki temu przeglądarka może obsługiwać dowolne interakcje użytkownika i zdarzenia wejścia-wyjścia, w tym WebUSB.
  3. Sprawdź, czy nie upłynął limit czasu oczekiwania. Jeśli nie, kontynuuj pętlę.

Jak wspomniano w komentarzu, to podejście nie było optymalne, ponieważ w nowoczesnych przeglądarkach rejestrował i przywracał cały stos wywołań nawet wtedy, gdy nie ma jeszcze żadnych zdarzeń USB do obsługi (co jest już najczęściej), a w nowoczesnych przeglądarkach minimalny czas trwania setTimeout() wynosi 4 ms. W ramach modelu koncepcyjnego udało się jednak na tyle dobrze przygotować transmisję na żywo z prędkością 13–14 FPS z lustrzanki cyfrowej.

Później postanowiłem go ulepszyć, wykorzystując system zdarzeń przeglądarki. Istnieje kilka sposobów na dalsze ulepszenie tej implementacji, ale na razie ustawiam zdarzenia niestandardowe bezpośrednio w obiekcie globalnym, bez kojarzenia ich z konkretną strukturą danych libusb. Zrobiłem to za pomocą poniższego mechanizmu oczekiwania i powiadomień opartych na makrze 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);
  }
});

Funkcja em_libusb_notify() jest używana, gdy libusb próbuje zgłosić zdarzenie, takie jak ukończenie transferu danych:

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
}

Część em_libusb_wait() służy do „wybudzenia” z uśpienia asynchronicznego po otrzymaniu zdarzenia em-libusb lub po upływie limitu czasu:

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

Ze względu na znaczne ograniczenie snu i pobudek ten mechanizm rozwiązał problemy z wydajnością we wcześniejszej implementacji wykorzystującej emscripten_sleep() i zwiększył przepustowość demonstracyjną lustrzanki cyfrowej z 13–14 FPS do stałych 30 FPS, co wystarczyło do płynnego wyświetlania transmisji na żywo.

Kompilacja systemu i pierwszy test

Po zakończeniu backendu trzeba było dodać go do Makefile.am i configure.ac. Jedynym interesującym fragmentem jest modyfikacja flag specyficznych dla 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']"
  ;;

Po pierwsze, pliki wykonywalne na platformach Unix zwykle nie mają rozszerzeń plików. Emscripten generuje jednak różne dane wyjściowe w zależności od żądanego rozszerzenia. Używam AC_SUBST(EXEEXT, …), aby zmienić rozszerzenie pliku wykonywalnego na .html tak, aby wszystkie pliki wykonywalne w pakiecie (testy i przykłady) stały się kodem HTML z domyślną powłoką Emscripten, która odpowiada za wczytywanie i tworzenie instancji JavaScript oraz WebAssembly.

Po drugie, ponieważ używam Embind i Asyncify, muszę włączyć te funkcje (--bind -s ASYNCIFY) oraz umożliwić dynamiczny wzrost pamięci (-s ALLOW_MEMORY_GROWTH) za pomocą parametrów łączących. Niestety biblioteka nie ma możliwości zgłoszenia tych flag do tagu łączącego, dlatego każda aplikacja korzystająca z tego portu libusb musi dodać te same flagi do konfiguracji kompilacji.

Jak już wspomnieliśmy, WebUSB wymaga, aby urządzenia były rejestrowane za pomocą gestu użytkownika. Przykłady w bibliotece libusb i testy zakładają, że mogą one zliczać urządzenia podczas uruchamiania, a w przypadku błędu bez zmian pojawia się błąd. Musiałem(-am) wyłączyć automatyczne wykonywanie (-s INVOKE_RUN=0) i udostępnić ręczną metodę callMain() (-s EXPORTED_RUNTIME_METHODS=...).

Po wykonaniu tych czynności można udostępnić wygenerowane pliki na statycznym serwerze WWW, zainicjować WebUSB i ręcznie uruchomić te pliki wykonywalne HTML za pomocą Narzędzi deweloperskich.

Zrzut ekranu przedstawiający okno Chrome z otwartymi Narzędziami deweloperskimi na lokalnej stronie „testlibusb”. Konsola Narzędzi deweloperskich ocenia działanie „navigator.usb.requestDevice({filter: [] })”, które uruchomiło prośbę o przyznanie uprawnień. Obecnie użytkownik prosi o wybranie urządzenia USB, które ma zostać udostępnione stronie. Obecnie wybrany jest ILCE-6600 (aparat Sony).

Zrzut ekranu przedstawiający następny krok, gdy Narzędzia deweloperskie są nadal otwarte. Po wybraniu urządzenia konsola sprawdziła nowe wyrażenie „Module.callMain([&#39;-v&#39;])”, które uruchomiło aplikację „testlibusb” w trybie szczegółowego. Dane wyjściowe zawierają różne szczegółowe informacje o podłączonej wcześniej kamerze USB, takie jak producent Sony, produkt ILCE-6600, numer seryjny, konfiguracja itp.

Wygląda to niewiele, ale gdy przenosisz biblioteki na nową platformę, osiągnięcie etapu, na którym po raz pierwszy uzyskasz prawidłowe dane wyjściowe, jest bardzo ekscytujące.

Korzystanie z portu

Jak wspomnieliśmy powyżej, port zależy od kilku funkcji Emscripten, które obecnie należy włączyć na etapie łączenia aplikacji. Jeśli chcesz użyć tego portu libusb w swojej aplikacji, wykonaj te czynności:

  1. Pobierz najnowszą wersję libusb jako archiwum w ramach kompilacji lub dodaj ją jako moduł podrzędny git w projekcie.
  2. Uruchom plik autoreconf -fiv w folderze libusb.
  3. Uruchom emconfigure ./configure –host=wasm32 –prefix=/some/installation/path, aby zainicjować projekt do kompilacji krzyżowej i ustawić ścieżkę, w której chcesz umieścić skompilowane artefakty.
  4. Uruchom emmake make install.
  5. Wskaż aplikację lub bibliotekę wyższego poziomu, aby wyszukać plik libusb pod wybraną wcześniej ścieżką.
  6. Dodaj te flagi do argumentów linku aplikacji: --bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH.

Biblioteka ma obecnie kilka ograniczeń:

  • Brak obsługi w celu anulowania przeniesienia. Jest to ograniczenie WebUSB, które z kolei wynika z braku anulowania przesyłania między platformami w bibliotece.
  • Brak obsługi transferu izochronicznego. Dodanie go nie powinno sprawiać trudności, jeśli na przykładach wykonam implementację istniejących trybów przenoszenia. Jest on jednak dość rzadki, a nie mam żadnych urządzeń, na których można by go przetestować, dlatego na razie zostawiam go jako nieobsługiwany. Jeśli macie takie urządzenia i chcecie pomóc rozwijać bibliotekę, zachęcamy do udziału PR-ów.
  • Wcześniej omawiane ograniczenia obejmujące wiele platform. Ograniczenia te są nakładane przez systemy operacyjne, więc nie możemy tutaj wiele zrobić. Wyjątkiem są prośby użytkowników o zastąpienie sterownika lub uprawnień. Jeśli jednak przenosisz urządzenia HID lub szeregowe, możesz skorzystać z przykładu dotyczącego biblioteki libusb i przenieść inną bibliotekę do innego interfejsu API Fugu. Można na przykład przenieść bibliotekę C hidapi do WebHID i uniknąć problemów związanych z niskim poziomem dostępu do USB.

Podsumowanie

W tym poście pokazaliśmy, jak za pomocą interfejsów API Emscripten, Asyncify i Fugu przenieść nawet biblioteki niskiego poziomu, takie jak libusb, do internetu i wykorzystać kilka sztuczek w zakresie integracji.

Przenoszenie tak ważnych i powszechnie używanych bibliotek niskopoziomowego jest szczególnie satysfakcjonujące, ponieważ umożliwia przeniesienie do internetu bibliotek wyższego poziomu, a nawet całych aplikacji. Pozwala to otworzyć doświadczenia, które wcześniej były ograniczone do użytkowników jednej lub dwóch platform, na wszystkie urządzenia i systemy operacyjne. Dostęp do tych funkcji można uzyskać jednym kliknięciem.

W następnym poście omówię poszczególne etapy tworzenia internetowej wersji demonstracyjnej gPhoto2, która nie tylko pobiera informacje o urządzeniu, ale też intensywnie korzysta z funkcji przesyłania biblioteki libusb. Mam nadzieję, że przykład korzystania z biblioteki libusb był dla Ciebie inspirujący i wypróbowałeś wersję demonstracyjną, pobawiliśmy się biblioteką, a nawet przeniesiesz inną popularną bibliotekę do jednego z interfejsów API Fugu.