USB uygulamaları web'e bağlanıyor. 1. Bölüm: Libusb

Harici cihazlarla etkileşim kuran kodların WebAssembly ve Fugu API'leri ile internete nasıl aktarılabileceğini öğrenin.

Ingvar Stepanyan
Ingvar Stepanyan

Önceki bir gönderide, File System Access API, WebAssembly ve Asyncify ile dosya sistemi API'lerini kullanarak uygulamaların web'e nasıl taşınacağını göstermiştim. Şimdi Fugu API'lerini WebAssembly ile entegre etme ve önemli özellikleri kaybetmeden uygulamaları web'e taşıma konulu aynı konuya devam etmek istiyorum.

C'de yazılan popüler bir USB kitaplığı olan libusb'nin, WebAssembly'ye (Emscripten aracılığıyla), Asyncify ve WebUSB'ye taşınıp USB cihazlarıyla iletişim kuran uygulamaların internete nasıl taşınabileceğini göstereceğim.

Öncelikle: Demo

Bir kitaplığı taşırken yapılacak en önemli şey, doğru demoyu seçmektir. Bu, taşınan kitaplığın özelliklerini sergileyerek, çeşitli yollarla test etmenize ve aynı zamanda görsel açıdan ilgi çekici olmanıza imkan tanır.

Ben DSLR uzaktan kumandasını seçtim. Özellikle, gPhoto2 adlı açık kaynak projesi, çok çeşitli dijital kameralara yönelik tersine mühendislik ve destek uygulamak için yeterince uzun süredir bu alanda bulunuyor. Birkaç protokolü destekliyor, ama en çok ilgimi çeken şey, libusb üzerinden gerçekleştirilen USB desteğiydi.

Bu demoyu oluşturma adımlarını iki bölümde açıklayacağım. Bu blog yayınında, libusb'yu nasıl taşıdığımı ve diğer popüler kitaplıkları Fugu API'lerine taşımak için hangi hilelerin gerekli olabileceğini anlatacağım. İkinci gönderide, gPhoto2'nin kendisini taşıma ve entegre etmeyle ilgili ayrıntıları paylaşacağım.

Sonunda, bir DSLR'den canlı feed önizleyen ve ayarlarını USB üzerinden kontrol edebilen, çalışan bir web uygulamam oldu. Teknik ayrıntıları okumadan önce canlı veya önceden kaydedilmiş demoyu inceleyebilirsiniz:

Sony kameraya bağlı bir dizüstü bilgisayarda çalışan demo.

Kameraya özgü ayrıntılarla ilgili not

Videoda ayarları değiştirmenin biraz zaman aldığını fark etmiş olabilirsiniz. Diğer sorunların çoğunda olduğu gibi bu sorunun nedeni WebAssembly veya WebUSB'nin performansı değil, gPhoto2'nin demo için seçilen kamerayla etkileşimidir.

Sony a6600; ISO, diyafram açıklığı veya deklanşör hızı gibi değerleri doğrudan ayarlamak için bir API'yi açığa çıkarmaz. Bunun yerine, yalnızca belirtilen adım sayısına kadar bunları artırma veya azaltma komutu verir. Sorunları daha karmaşık hale getirmek için, gerçekte desteklenen değerlerin bir listesini döndürmez. Döndürülen liste, birçok Sony fotoğraf makinesi modeline koda gömülmüş gibi görünür.

Bu değerlerden birini ayarlarken, gPhoto2'nin başka bir seçeneği yoktur:

  1. Seçilen değere doğru bir (veya birkaç) adım atın.
  2. Kameranın ayarları güncellemesi için biraz bekleyin.
  3. Kameranın ulaştığı değeri okuyun.
  4. Son adımın istenen değerin üzerine çıkmadığından veya listenin sonuna ya da başına yuvarlanmadığından emin olun.
  5. Tekrarla.

Bu süreç biraz zaman alabilir ancak değer, kamera tarafından gerçekten destekleniyorsa hedefe ulaşır ve desteklenmediyse desteklenen en yakın değerde durur.

Diğer kameraların muhtemelen farklı ayar grupları, temel API'leri ve tuhaflıkları vardır. gPhoto2'nin açık kaynaklı bir proje olduğunu ve mevcut tüm kamera modellerinin otomatik veya manuel olarak test edilmesinin mümkün olmadığını unutmayın. Bu nedenle, ayrıntılı sorun raporlarına ve PR'lere her zaman açığız (ancak sorunları önce resmi gPhoto2 istemcisiyle yeniden oluşturduğunuzdan emin olun).

Platformlar arası uyumlulukla ilgili önemli notlar

Maalesef Windows'da "iyi bilinen" cihazlara DSLR kameralar da dahil olmak üzere WebUSB ile uyumlu olmayan bir sistem sürücüsü atanmıştır. Demoyu Windows'da denemek isterseniz bağlı DSLR'nin sürücüsünü WinUSB veya libusb olarak geçersiz kılmak için Zadig gibi bir araç kullanmanız gerekir. Bu yaklaşım ben ve diğer birçok kullanıcı için uygun. Ancak bu yaklaşımı kullanmanın doğurabileceği riskleri üstlenmeniz gerekiyor.

Linux'ta, DSLR'nize WebUSB üzerinden erişilmesine izin vermek için özel izinler ayarlamanız gerekir ancak bu izin dağıtım şeklinize bağlıdır.

macOS ve Android'de demo kullanıma hazır olarak sunulur. Bu özelliği bir Android telefonda deneyiyorsanız yatay moda geçtiğinizden emin olun, çünkü duyarlı hale getirmek için çok fazla çaba göstermedim (PR'lere katılmaktan mutluluk duyarız!):

Canon kameraya USB-C kablosuyla bağlı Android telefon.
Bir Android telefonda çalışan aynı demo. Resim: Surma.

WebUSB'nin platformlar arası kullanımı hakkında daha ayrıntılı bir kılavuz için "Platforma özgü hususlar" "WebUSB için cihaz oluşturma" bölümüne bakın.

Libusb'a yeni arka uç ekleme

Şimdi teknik ayrıntılara geçelim. Libusb'ye (daha önce başkaları tarafından yapılmış) benzer bir dolgu API'si sağlamak ve diğer uygulamaları buna bağlamak mümkün olsa da bu yaklaşım hata verme olasılığı vardır ve daha fazla uzatmayı veya bakımı zorlaştırır. Her şeyi doğru yapmak istiyordum. Bu şekilde, içerik üretimine katkısı ve ileride libusb ile birleştirilmesi mümkün olabilir.

Neyse ki libusb README şöyle diyor:

"Libusb, diğer işletim sistemlerine taşınabilir. Daha fazla bilgi için PORTING dosyasını inceleyin."

libusb, herkese açık API'nin "arka uçlardan" ayrı olacağı şekilde yapılandırılır. Bu arka uçlar, işletim sisteminin alt düzey API'leri aracılığıyla cihazları listelemeden, açmaktan, kapatmaktan ve cihazlarla iletişim kurmaktan sorumludur. libusb; Linux, macOS, Windows, Android, OpenBSD/NetBSD, Haiku ve Solaris arasındaki farkları bu şekilde soyutlamakta ve tüm bu platformlarda çalışmaktadır.

Emscripten+WebUSB "işletim sistemi" için başka bir arka uç eklemem gerekiyordu. Bu arka uçların uygulamaları libusb/os klasöründe bulunur:

~/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

Her arka uç, ortak türleri ve yardımcıları içeren libusbi.h başlığını içerir ve usbi_os_backend türünde bir usbi_backend değişkeni sunmalıdır. Örneğin, Windows arka ucu şuna benzer:

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

Özelliklere baktığımızda, struct'ın arka uç adını, bir dizi özelliği, fonksiyon işaretçileri biçimindeki çeşitli düşük seviye USB işlemlerine yönelik işleyicileri ve son olarak da özel cihaz/context-/aktarım düzeyindeki verileri depolamak için ayrılacak boyutları içerdiğini görebiliriz.

Özel veri alanları, tutma yeri olmadan herhangi bir işlemin hangi öğe için geçerli olduğunu bilmediğimizden, işletim sistemi herkese açık kullanıcı adlarını bu alanlarda depolamak için kullanışlıdır. Web uygulamasında, işletim sistemi herkese açık kullanıcı adları temel WebUSB JavaScript nesneleridir. Bunları Emscripten'de temsil etmenin ve depolamanın doğal yolu, Embind'in (Emscripten'in bağlama sistemi) bir parçası olarak sağlanan emscripten::val sınıfıdır.

Klasördeki arka uçların çoğu C'de, bazıları ise C++ üzerinde uygulanır. Embind yalnızca C++ ile kullanılabildiğinden seçim benim yerime yapıldı ve gerekli yapıya sahip libusb/libusb/os/emscripten_webusb.cpp, özel veri alanları için de sizeof(val) eklendi:

#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 nesnelerini cihaz tanıtıcıları olarak depolama

libusb, özel veriler için ayrılan alana kullanıma hazır işaretçiler sağlar. Bu işaretçilerle val örnekleri olarak çalışmak için, bunları yerinde oluşturan, referans olarak alan ve değerleri dışarı taşıyan küçük yardımcılar ekledim:

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

Eşzamanlı C bağlamlarında eşzamansız web API'leri

Artık Libusb'nun eşzamanlı işlemler beklediği eşzamansız WebUSB API'lerini işlemek için bir yönteme ihtiyaç duyuldu. Bunun için Asyncify'ı veya daha kesin olarak val::await() üzerinden Embind entegrasyonunu kullanabilirim.

Ayrıca, WebUSB hatalarını doğru bir şekilde işlemek ve bunları kitaplık hata kodlarına dönüştürmek istedim ancak Embind'in şu anda C++ tarafından JavaScript istisnalarını veya Promise reddetmelerini ele almak için herhangi bir yolu yok. Bu sorun, JavaScript tarafında bir ret yakalanıp sonucu artık C++ tarafından güvenli bir şekilde ayrıştırılabilen bir { error, value } nesnesine dönüştürülerek giderilebilir. Bu işlemi EM_JS makrosu ve Emval.to{Handle, Value} API'lerinin bir kombinasyonuyla gerçekleştirdim:

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

Artık WebUSB işlemlerinden döndürülen tüm Promise üzerinde promise_result::await() kullanabilir ve bunun error ile value alanlarını ayrı ayrı inceleyebilirim.

Örneğin, libusb_device_handle öğesinden USBDevice öğesini temsil eden bir val alınması, bu yöntemin open() yöntemini çağırması, sonucunun beklenmesi ve hata kodunu libusb durum kodu olarak döndürmesi şu şekilde görünür:

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

Cihaz numaralandırması

Tabii ki herhangi bir cihazı açabilmem için önce Libusb'nun kullanılabilir cihazların listesini alması gerekiyor. Arka uç, bu işlemi bir get_device_list işleyici aracılığıyla uygulamalıdır.

Buradaki zorluk, diğer platformların aksine, güvenlik nedeniyle web'deki tüm bağlı USB cihazlarını numaralandırmanın mümkün olmamasıdır. Bunun yerine akış iki bölüme ayrılır. Öncelikle, web uygulaması navigator.usb.requestDevice() aracılığıyla belirli özelliklere sahip cihazları ister ve kullanıcı, izin istemini göstermek istediği cihazı manuel olarak seçer veya reddeder. Ardından, uygulamada onaylanmış ve bağlı cihazlar navigator.usb.getDevices() aracılığıyla listelenir.

İlk başta doğrudan get_device_list işleyici uygulamasında requestDevice() kullanmaya çalıştım. Bununla birlikte, bağlı cihazların listesini içeren bir izin isteminin gösterilmesi hassas bir işlem olarak kabul edilir ve kullanıcı etkileşimiyle (bir sayfadaki düğmenin tıklanması gibi) tetiklenmesi gerekir. Aksi takdirde, her zaman reddedilen bir söz döndürür. Libusb uygulamaları genellikle uygulama başlatıldığında bağlı cihazları listelemek isteyebilir. Bu nedenle, requestDevice() kullanmak bir seçenek değildi.

Bunun yerine, navigator.usb.requestDevice() çağrısını son geliştiriciye bırakmak ve yalnızca navigator.usb.getDevices() sağlayıcısından halihazırda onaylanmış cihazları göstermek zorunda kaldım:

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

Arka uç kodunun büyük bir kısmı, yukarıda gösterildiği gibi val ve promise_result öğelerini kullanır. Veri aktarımı işleme kodunda daha ilginç birkaç saldırı vardır ancak bu uygulama ayrıntıları, bu makalenin amaçları doğrultusunda daha az öneme sahiptir. İlgileniyorsanız GitHub'daki kodu ve yorumları kontrol edin.

Etkinlik döngüsünü web'e taşıma

Bahsetmek istediğim diğer bir libusb bağlantı noktası da etkinlik işleme. Önceki makalede açıklandığı gibi, C gibi sistem dillerindeki çoğu API eşzamanlıdır ve olay işleme de bu durumdan etkilenmez. Genellikle "anketler" ya da "anketler" (bazı veriler kullanılabilir olana kadar verileri okumaya çalışır veya yürütülmesini engeller) ve bunlardan en az biri yanıt verdiğinde ilgili işleyiciye etkinlik olarak iletir. İşleyici bittikten sonra kontrol döngüye geri döner ve başka bir anket için duraklatılır.

Web'de bu yaklaşımla ilgili bazı sorunlar vardır.

İlk olarak, WebUSB temel cihazların ham herkese açık kullanıcı adlarını göstermez ve gösteremez; dolayısıyla, bunları doğrudan yoklama bir seçenek değildir. İkinci olarak libusb, diğer etkinlikler ve ham cihaz tanıtıcıları olmayan işletim sistemlerindeki aktarımları işlemek için eventfd ve pipe API'lerini kullanır. Ancak eventfd şu anda Emscripten'de desteklenmemektedir ve pipe desteklenmekle birlikte şu anda spesifikasyona uymamaktadır ve etkinlikleri bekleyemez.

Son olarak, en büyük sorun web'in kendi etkinlik döngüsünün olmasıdır. Bu genel etkinlik döngüsü, tüm harici G/Ç işlemleri (fetch(), zamanlayıcılar veya bu durumda WebUSB dahil) için kullanılır ve ilgili işlemler tamamlandığında etkinlik ya da Promise işleyicilerini çağırır. İç içe yerleştirilmiş başka bir sonsuz etkinlik döngüsünü yürütmek, tarayıcının etkinlik döngüsünün ilerlemesini engeller. Bu durum, yalnızca kullanıcı arayüzünün yanıt vermemeye başlayacağı ve kodun, beklediği aynı G/Ç etkinlikleri için hiçbir zaman bildirim almayacağı anlamına gelir. Bu genellikle bir kilitlenmeye neden oluyor. Bir demoda libusb'ı kullanmaya çalıştığımda da bu durumla karşılaştım. Sayfa dondu.

Diğer engelleme G/Ç'lerinde olduğu gibi, bu tür etkinlik döngülerini web'e taşımak için geliştiricilerin ana iş parçacığını engellemeden bu döngüleri çalıştırmanın bir yolunu bulmaları gerekir. Bunun bir yolu, G/Ç etkinliklerini ayrı bir iş parçacığında işlemek için uygulamayı yeniden düzenlemek ve sonuçları ana ileti dizisine geri iletmektir. Diğeri ise, döngüyü duraklatmak ve etkinlikleri engellemeyecek bir şekilde beklemek için Asyncify uygulamasını kullanmaktır.

Libusb veya gPhoto2'de önemli değişiklikler yapmak istemedim ve Promise entegrasyonu için zaten Eşzamansız poll() işlevinin engelleme varyantını simüle etmek için, ilk kavram kanıtlama için aşağıda gösterildiği gibi bir döngü kullandım:

#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

Ne işe yarar?

  1. Arka uç tarafından henüz herhangi bir etkinliğin raporlanıp raporlanmadığını kontrol etmek için poll() öğesini çağırır. Sınır varsa döngü durur. Aksi takdirde, Emscripten'in poll() uygulaması 0 ile hemen geri dönecektir.
  2. emscripten_sleep(0) numaralı telefonu arar. Bu işlev, arka planda Asyncify ve setTimeout() özelliklerini kullanır ve burada, kontrolü ana tarayıcı etkinlik döngüsüne geri vermek için kullanılır. Bu, tarayıcının WebUSB dahil olmak üzere tüm kullanıcı etkileşimlerini ve G/Ç etkinliklerini işlemesine olanak tanır.
  3. Belirtilen zaman aşımı süresinin henüz sona erip ermediğini kontrol edin. Süre dolmadıysa döngüye devam edin.

Yorumda da belirtildiği gibi, bu yaklaşım ideal değildi. Bunun nedeni, henüz gerçekleştirilecek USB etkinliği olmasa bile (çoğu zaman) tüm çağrı yığınının Asyncify ile geri kaydedilmeye devam etmesi ve setTimeout() ürününün modern tarayıcılarda minimum 4 ms uzunluğuna sahip olmasıydı. Buna rağmen, konseptin kanıtında DSLR'den 13-14 FPS FPS'lik canlı yayın oluşturacak kadar iyi sonuç verdi.

Daha sonra, tarayıcı etkinlik sisteminden yararlanarak bu deneyimi geliştirmeye karar verdim. Bu uygulamanın daha da iyileştirilmesinin birkaç yolu vardır ancak şimdilik, özel etkinlikleri belirli bir kitaplık veri yapısıyla ilişkilendirmeden, doğrudan global nesne üzerinde yayınlamayı tercih ettim. Bu işlemi EM_ASYNC_JS makrosuna göre aşağıdaki bekleme ve bilgilendirme mekanizmasını kullanarak gerçekleştirdim:

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, veri aktarımının tamamlanması gibi bir etkinliği bildirmeye çalıştığında em_libusb_notify() işlevi kullanılır:

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
}

Bu esnada em_libusb_wait() bölümü "uyanmak" için kullanılıyor bir em-libusb etkinliği alındığında veya zaman aşımı süresi dolduğunda Eşzamansız uyku modundan çıkarır:

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

Uyku ve uyandırma sayısındaki önemli ölçüde azalma nedeniyle bu mekanizma, emscripten_sleep() tabanlı önceki uygulamanın verimlilik sorunlarını düzeltti ve sorunsuz bir canlı feed için yeterli olan, 13-14 FPS'den DSLR demo işleme hızını tutarlı 30+ FPS'ye yükseltti.

Derleme sistemi ve ilk test

Arka ucu tamamladıktan sonra Makefile.am ve configure.ac uygulamalarına eklemem gerekti. Buradaki tek ilginç nokta, Emscripten'e özgü işaret değişikliğidir:

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

İlk olarak, Unix platformlarındaki yürütülebilir dosyaların normalde dosya uzantıları yoktur. Ancak Emscripten, hangi uzantıyı talep ettiğinize bağlı olarak farklı çıktılar üretir. Yürütülebilir uzantıyı .html olarak değiştirmek için AC_SUBST(EXEEXT, …) kullanıyorum. Böylece, paket içindeki yürütülebilir tüm dosyalar (testler ve örnekler) JavaScript ve WebAssembly'nin yüklenmesini ve örneklendirilmesini sağlayan Emscripten'in varsayılan kabuğuyla HTML haline gelir.

İkinci olarak, Embind ve Asyncify kullandığım için bu özellikleri (--bind -s ASYNCIFY) etkinleştirmem ve bağlayıcı parametreleri aracılığıyla dinamik bellek artışına (-s ALLOW_MEMORY_GROWTH) izin vermem gerekiyor. Maalesef bir kitaplığın bu işaretleri bağlayıcıya bildirmesinin bir yolu yoktur. Dolayısıyla, bu kitaplık bağlantı noktasını kullanan her uygulamanın aynı bağlayıcı işaretlerini kendi derleme yapılandırmasına da eklemesi gerekir.

Son olarak, daha önce belirtildiği gibi WebUSB, cihaz numaralandırmasının kullanıcı hareketiyle yapılmasını gerektirir. libusb örnekleri ve testleri, cihazların başlangıçta numaralandırılabildiğini ve herhangi bir değişiklik yapılmadan hata vererek başarısız olduğunu varsayar. Bunun yerine, otomatik yürütmeyi (-s INVOKE_RUN=0) devre dışı bırakıp manuel callMain() yöntemini (-s EXPORTED_RUNTIME_METHODS=...) kullanıma sunmak zorunda kaldım.

Bu adımın ardından oluşturulan dosyaları statik web sunucusuyla sunabilir, WebUSB'yi ilk kullanıma hazırlayabilir ve DevTools yardımıyla bu yürütülebilir HTML dosyalarını manuel olarak çalıştırabilirim.

Yerel olarak sunulan bir &quot;testlibusb&quot; sayfasında Geliştirici Araçları&#39;nın açık olduğu Chrome penceresini gösteren ekran görüntüsü. Geliştirici Araçları konsolu, bir izin istemini tetikleyen &quot;navigator.usb.requestDevice({ filtering: [] })&quot; değerini değerlendiriyor ve şu anda kullanıcıdan sayfayla paylaşılması gereken bir USB cihazını seçmesini istiyor. Şu anda ILCE-6600 (Sony kamera) seçili.

Geliştirici Araçları hâlâ açık olan bir sonraki adımın ekran görüntüsü. Cihaz seçildikten sonra Console, &quot;testlibusb&quot; uygulamasını ayrıntılı modda yürüten yeni &quot;Module.callMain([&#39;-v&#39;])&quot; ifadesini değerlendirdi. Çıkışta, önceden bağlanmış USB kamerayla ilgili çeşitli ayrıntılı bilgiler gösterilir: üretici Sony, ILCE-6600 ürünü, seri numarası, yapılandırma vb.

Pek bir şey değilmiş gibi gözükse de kitaplıkları yeni bir platforma taşırken ilk kez geçerli bir çıktı üreteceği aşamaya geçmek oldukça heyecan verici.

Bağlantı noktasını kullanma

Yukarıda belirtildiği gibi, bağlantı noktası, şu anda uygulamanın bağlama aşamasında etkinleştirilmesi gereken birkaç Emscripten özelliğine bağlıdır. Bu kitaplık bağlantı noktasını kendi uygulamanızda kullanmak istiyorsanız şunları yapmanız gerekir:

  1. En yeni libusb dosyasını, derlemenizin parçası olarak bir arşiv olarak indirin veya projenize bir git alt modülü olarak ekleyin.
  2. libusb klasöründe autoreconf -fiv komutunu çalıştırın.
  3. Projeyi çapraz derleme için başlatmak ve derlenen yapıları yerleştirmek istediğiniz yolu belirlemek için emconfigure ./configure –host=wasm32 –prefix=/some/installation/path komutunu çalıştırın.
  4. emmake make install çalıştır.
  5. Daha önce seçilen yolun altında kitaplık aramak için uygulamanızı veya daha üst düzey kitaplığınızı yönlendirin.
  6. Uygulamanızın bağlantı bağımsız değişkenlerine şu işaretleri ekleyin: --bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH.

Kitaplık şu anda birkaç sınırlamaya sahiptir:

  • Aktarım iptali desteği yok. Bu, WebUSB'deki bir sınırlamadır ve web üzerinde platformlar arası aktarımın iptal edilmemesinden kaynaklanır.
  • Eşzamanlı aktarım desteği yoktur. Mevcut aktarım modlarının uygulamasını örnek olarak uygulayarak bu modu eklemek zor olmayacaktır. Ancak bu aynı zamanda biraz nadir kullanılan bir moddur ve test edebileceğim herhangi bir cihazım olmadığından şimdilik desteklenmiyor olarak bırakıldım. Elinizde bu tür cihazlar varsa ve kitaplığa katkıda bulunmak istiyorsanız halkla ilişkiler kurarak destek olabilirsiniz.
  • Daha önce platformlar arası sınırlamalardan bahsetmiştik. Bu sınırlamalar, işletim sistemleri tarafından uygulandığı için burada, kullanıcılardan sürücüyü veya izinleri geçersiz kılmalarını istemek dışında pek bir şey yapamıyoruz. Bununla birlikte, HID veya seri cihazları bağlıyorsanız libusb örneğini takip edebilir ve başka bir kitaplığı başka bir Fugu API'sine taşıyabilirsiniz. Örneğin, bir C kitaplığının hidapi'sini WebHID'ye bağlayabilir ve düşük düzeyli USB erişimiyle ilişkili tüm bu sorunların önüne geçebilirsiniz.

Sonuç

Bu gönderide, Emscripten, Asyncify ve Fugu API'lerinin yardımıyla, libusb gibi alt düzey kitaplıkların bile birkaç entegrasyon numarasıyla web'e nasıl taşınabileceğini gösterdim.

Böylesine gerekli ve yaygın olarak kullanılan alt düzey kitaplıkları taşımak özellikle avantajlıdır. Çünkü sonuç olarak, daha üst düzey kitaplıkların, hatta tüm uygulamaların web'e getirilmesine olanak tanır. Eskiden yalnızca bir veya iki platformun kullanıcılarıyla sınırlı olan deneyimler, her tür cihaz ve işletim sistemiyle sınırlanıyor. Böylece, deneyimler yalnızca bir bağlantı uzağınızda kullanılabiliyor.

Bir sonraki gönderide, yalnızca cihaz bilgilerini almakla kalmayıp libusb'un aktarım özelliğini de kapsamlı bir şekilde kullanan web gPhoto2 demosunu oluşturma adımlarını anlatacağım. Bu arada, libusb örneğini ilham verici bulduğunuzu ve demoyu deneyeceğinizi, kütüphanenin kendisiyle oynayacağınızı veya belki de yaygın olarak kullanılan başka bir kitaplığı Fugu API'lerinden birine taşıyacağınızı umuyorum.