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

Harici cihazlarla etkileşimde bulunan kodların WebAssembly ve Fugu API'leri ile web'e nasıl taşınabileceğini öğrenin.

Ingvar Stepanyan
Ingvar Stepanyan

Önceki bir yayında, dosya sistemi API'lerini kullanan uygulamaları File System Access API, WebAssembly ve Asyncify ile web'e nasıl taşıyacağınızı göstermiştim. Şimdi, Fugu API'lerini WebAssembly ile entegre etme ve önemli özellikleri kaybetmeden uygulamaları web'e taşıma konusuna devam etmek istiyorum.

C dilinde yazılmış popüler bir USB kitaplığı olan libusb'yi WebAssembly'ye (Emscripten aracılığıyla), Asyncify'a ve WebUSB'ye taşıyarak USB cihazlarla iletişim kuran uygulamaların web'e nasıl taşınabileceğini göstereceğim.

Öncelikle bir demo

Bir kitaplığı taşırken yapılması gereken en önemli şey, doğru demoyu seçmektir. Bu demo, taşınan kitaplığın özelliklerini sergilemeli, kitaplığı çeşitli şekillerde test etmenize olanak tanımalı ve aynı zamanda görsel olarak ilgi çekici olmalıdır.

Seçtiğim fikir DSLR uzaktan kumandasıydı. Özellikle açık kaynaklı bir proje olan gPhoto2, çok çeşitli dijital kameralar için tersine mühendislik ve destek uygulama konusunda uzun süredir bu alanda faaliyet göstermektedir. Birkaç protokolü destekler ancak benim en çok ilgilendiğim, libusb aracılığıyla gerçekleştirdiği USB desteğiydi.

Bu demoyu oluşturma adımlarını iki bölümde açıklayacağım. Bu blog yayınında, libusb'yi nasıl taşıdığımı ve diğer popüler kitaplıkları Fugu API'lerine taşımak için hangi püf noktalarının gerekli olabileceğini açıklayacağım. İkinci yayında, gPhoto2'nin taşınması ve entegrasyonuyla ilgili ayrıntılara değineceğim.

Sonuç olarak, DSLR'den canlı feed'i önizleyen ve ayarlarını USB üzerinden kontrol edebilen çalışan bir web uygulaması elde ettim. Teknik ayrıntıları okumadan önce canlı veya önceden kaydedilmiş demoya göz atabilirsiniz:

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

Kameraya özgü sorunlarla ilgili not

Videoda ayarları değiştirmenin biraz zaman aldığını fark etmiş olabilirsiniz. Karşılaşabileceğiniz diğer sorunların çoğunda olduğu gibi, bu sorun da WebAssembly veya WebUSB'nin performansından değil, gPhoto2'nin demo için seçilen kamerayla etkileşiminden kaynaklanır.

Sony a6600; ISO, diyafram açıklığı veya deklanşör hızı gibi değerleri doğrudan ayarlamak için bir API sunmaz. Bunun yerine, yalnızca bu değerleri belirtilen adım sayısı kadar artırma veya azaltma komutları sağlar. İşleri daha da karmaşık hale getiren bir diğer nokta ise, gerçekten desteklenen değerlerin listesini de döndürmemesidir. Döndürülen liste, birçok Sony kamera modelinde sabit kodlanmış gibi görünmektedir.

Bu değerlerden birini ayarlarken gPhoto2'nin şunları yapmaktan başka seçeneği yoktur:

  1. Seçilen değer yönünde bir (veya birkaç) adım atın.
  2. Kameranın ayarları güncellemesi için biraz bekleyin.
  3. Kameranın gerçekte ulaştığı değeri okuyun.
  4. Son adımın istenen değeri atlamadığından veya listenin sonuna ya da başına sarmadığından emin olun.
  5. Tekrarla.

Bu işlem biraz zaman alabilir ancak değer kamera tarafından destekleniyorsa bu değere ulaşılır. Desteklenmiyorsa en yakın desteklenen değerde durur.

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

Platformlar arası uyumlulukla ilgili önemli notlar

Maalesef Windows'ta DSLR kameralar da dahil olmak üzere "iyi bilinen" tüm cihazlara WebUSB ile uyumlu olmayan bir sistem sürücüsü atanır. Demoyu Windows'ta denemek istiyorsanız 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 benim ve diğer birçok kullanıcı için iyi sonuç veriyor ancak bu yaklaşımı kullanırken riskleri kabul etmiş olursunuz.

Linux'ta, dağıtımınıza bağlı olarak DSLR'nize WebUSB üzerinden erişime izin vermek için özel izinler ayarlamanız gerekebilir.

Demo, macOS ve Android'de kullanıma hazır olmalıdır. Android telefonda deniyorsanız yatay moda geçmeyi unutmayın. Duyarlı hale getirmek için çok fazla çaba harcamadım (PR'ler kabul edilir):

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

WebUSB'nin platformlar arası kullanımı hakkında daha ayrıntılı bir kılavuz için "Building a device for WebUSB" (WebUSB için cihaz oluşturma) başlıklı makalenin "Platform-specific considerations" (Platforma özel hususlar) bölümüne bakın.

Libusb'ye yeni bir arka uç ekleme

Şimdi teknik ayrıntılara geçelim. Libusb'ye benzer bir dolgu API'si sağlamak (bu daha önce başkaları tarafından yapılmıştır) ve diğer uygulamaları buna bağlamak mümkün olsa da bu yaklaşım hataya açıktır ve daha fazla genişletme veya bakımı zorlaştırır. İşleri doğru şekilde yapmak istedim. Böylece, gelecekte libusb'ye katkıda bulunabilir ve birleştirilebilirdi.

Neyse ki libusb BENİOKU dosyasında şu açıklama yer alıyor:

"libusb, diğer işletim sistemlerine taşınabilmesi için dahili olarak soyutlanmıştır. Daha fazla bilgi için lütfen PORTING dosyasına bakın."

libusb, herkese açık API'nin "arka uçlardan" ayrı olduğu bir yapıya sahiptir. Bu arka uçlar, işletim sisteminin alt düzey API'leri aracılığıyla cihazları listelemek, açmak, kapatmak ve cihazlarla iletişim kurmakla sorumludur. libusb, Linux, macOS, Windows, Android, OpenBSD/NetBSD, Haiku ve Solaris arasındaki farkları bu şekilde soyutlar ve tüm bu platformlarda çalışır.

Bunun için Emscripten+WebUSB "işletim sistemi"ne 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 göstermesi gerekir. Örneğin, Windows arka ucu şu şekilde görünür:

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, yapının arka uç adını, özelliklerini, işlev işaretçileri biçiminde çeşitli alt düzey USB işlemleri için işleyicileri ve son olarak özel cihaz/bağlam/aktarım düzeyindeki verileri depolamak için ayrılacak boyutları içerdiğini görebiliriz.

Özel veri alanları, en azından tüm bu öğeler için OS tanıtıcılarını depolamak açısından faydalıdır. Tanıtıcılar olmadan, belirli bir işlemin hangi öğeye uygulandığını bilemeyiz. Web uygulamasında, işletim sistemi tanıtıcıları temel WebUSB JavaScript nesneleri olur. Bunları Emscripten'de temsil etmenin ve depolamanın doğal yolu, Embind'ın (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 dilinde, birkaçı ise C++ dilinde uygulanır. Embind yalnızca C++ ile çalıştığından seçim benim için yapıldı ve gerekli yapıya sahip libusb/libusb/os/emscripten_webusb.cpp ile özel veri alanları için sizeof(val) ekledim:

#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

Şimdi, libusb'nin eşzamanlı işlemler beklediği eşzamansız WebUSB API'lerini işlemenin bir yolunu bulmamız gerekiyordu. Bunun için Asyncify'ı veya daha spesifik olarak val::await() aracılığıyla Embind entegrasyonunu kullanabilirim.

Ayrıca WebUSB hatalarını doğru şekilde işlemek ve libusb hata kodlarına dönüştürmek istedim ancak Embind'de şu anda JavaScript istisnalarını veya C++ tarafındaki Promise reddetmelerini işlemek için bir yöntem yok. JavaScript tarafında bir reddi yakalayıp sonucu C++ tarafında güvenli bir şekilde ayrıştırılabilen bir { error, value } nesnesine dönüştürerek bu sorunu çözebilirsiniz. Bunu EM_JS makrosu ve Emval.to{Handle, Value} API'lerinin bir kombinasyonuyla yaptım:

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 herhangi bir Promise üzerinde promise_result::await() kullanabilir ve error ile value alanlarını ayrı ayrı inceleyebilirim.

Örneğin, libusb_device_handle üzerinden USBDevice'yi temsil eden bir val alma, open() yöntemini çağırma, sonucunu bekleme ve libusb durum kodu olarak bir hata kodu döndürme işlemi ş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ırma

Elbette herhangi bir cihazı açabilmem için libusb'nin kullanılabilir cihazların listesini alması gerekir. Arka uç, bu işlemi bir get_device_list işleyici aracılığıyla uygulamalıdır.

Ancak diğer platformların aksine, güvenlik nedeniyle web'deki tüm bağlı USB cihazlarını listelemenin bir yolu yoktur. Bunun yerine akış iki bölüme ayrılır. İlk olarak web uygulaması, navigator.usb.requestDevice() aracılığıyla belirli özelliklere sahip cihazlar ister ve kullanıcı, hangi cihazı göstermek istediğini manuel olarak seçer veya izin istemini reddeder. Ardından uygulama, navigator.usb.getDevices() aracılığıyla önceden onaylanmış ve bağlanmış cihazları listeler.

İlk başta requestDevice() işleyicisinin uygulamasında doğrudan get_device_list kullanmayı denedim. Ancak bağlı cihazların listesini içeren bir izin istemi göstermek hassas bir işlem olarak kabul edilir ve kullanıcı etkileşimiyle (sayfadaki bir düğmeyi tıklama gibi) tetiklenmesi gerekir. Aksi takdirde her zaman reddedilen bir vaat döndürür. libusb uygulamaları genellikle uygulama başlatıldığında bağlı cihazları listelemek ister. Bu nedenle requestDevice() kullanmak bir seçenek değildi.

Bunun yerine, navigator.usb.requestDevice() çağrısını son geliştiriciye bırakmam ve yalnızca navigator.usb.getDevices() tarafından onaylanmış cihazları göstermem gerekti:

// 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 çoğu, yukarıda gösterildiği gibi val ve promise_result kullanır. Veri aktarımı işleme kodunda birkaç ilginç hile daha vardır ancak bu uygulama ayrıntıları bu makalenin amaçları açısından daha az önemlidir. İlgileniyorsanız Github'daki kodu ve yorumları inceleyebilirsiniz.

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

Libusb bağlantı noktasının ele almak istediğim bir diğer parçası da etkinlik işleme. Önceki makalede açıklandığı gibi, C gibi sistem dillerindeki çoğu API eşzamanlıdır ve etkinlik işleme de istisna değildir. Genellikle bir dizi harici G/Ç kaynağından "anket yapan" (veri okumaya veya bazı veriler kullanılabilir olana kadar yürütmeyi engellemeye çalışan) sonsuz bir döngü aracılığıyla uygulanır ve bunlardan en az biri yanıt verdiğinde bunu ilgili işleyiciye bir etkinlik olarak iletir. İşleyici tamamlandığında kontrol döngüye geri döner ve başka bir yoklama için duraklatılır.

Web'de bu yaklaşımla ilgili birkaç sorun vardır.

İlk olarak, WebUSB, temel cihazların ham tanıtıcılarını göstermez ve gösteremez. Bu nedenle, bunları doğrudan sorgulamak mümkün değildir. İkinci olarak, libusb diğer etkinlikler için ve ham cihaz tanıtıcıları olmayan işletim sistemlerinde aktarımları işlemek için eventfd ve pipe API'lerini kullanır ancak eventfd şu anda Emscripten'de desteklenmez ve pipe desteklenmesine rağmen şu anda spesifikasyona uygun değildir ve etkinlikleri bekleyemez.

Son olarak, en büyük sorun web'in kendi etkinlik döngüsüne sahip olmasıdır. Bu global etkinlik döngüsü, tüm harici G/Ç işlemleri (fetch(), zamanlayıcılar veya bu örnekte WebUSB dahil) için kullanılır ve ilgili işlemler tamamlandığında etkinlik veya Promise işleyicileri çağırır. Başka bir iç içe yerleştirilmiş sonsuz etkinlik döngüsünün yürütülmesi, tarayıcının etkinlik döngüsünün ilerlemesini engeller. Bu da yalnızca kullanıcı arayüzünün yanıt vermemesiyle kalmaz, aynı zamanda kodun beklediği G/Ç etkinlikleri için hiçbir zaman bildirim almayacağı anlamına gelir. Bu durum genellikle kilitlenmeye neden olur. Ben de bir demoda libusb'yi kullanmaya çalıştığımda bu sorunla karşılaştım. Sayfa dondu.

Diğer engelleyici G/Ç işlemlerinde olduğu gibi, bu tür etkinlik döngülerini web'e taşımak için geliştiricilerin bu döngüleri ana iş parçacığını engellemeden çalıştırmanın bir yolunu bulması gerekir. Bunun bir yolu, uygulamayı G/Ç etkinliklerini ayrı bir iş parçacığında işleyecek ve sonuçları ana iş parçacığına geri iletecek şekilde yeniden düzenlemektir. Diğer yöntem ise Asyncify kullanarak döngüyü duraklatmak ve engelleme yapmadan etkinlikleri beklemektir.

libusb veya gPhoto2'de önemli değişiklikler yapmak istemedim ve Promise entegrasyonu için Asyncify'ı zaten kullandığım için bu yolu seçtim. poll() öğesinin engelleyici bir varyantını simüle etmek için ilk kavram kanıtlama aşamasında 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

Bu özellik:

  1. Arka uç tarafından herhangi bir etkinliğin bildirilip bildirilmediğini kontrol etmek için poll() yöntemini çağırır. Varsa döngü durur. Aksi takdirde, Emscripten'in poll() uygulaması hemen 0 ile geri döner.
  2. Telefon aramaları emscripten_sleep(0). Bu işlev, arka planda Asyncify ve setTimeout() kullanır ve burada kontrolü ana tarayıcı etkinlik döngüsüne geri vermek için kullanılır. Bu sayede tarayıcı, WebUSB dahil olmak üzere tüm kullanıcı etkileşimlerini ve G/Ç etkinliklerini işleyebilir.
  3. Belirtilen zaman aşımı süresinin dolup dolmadığını kontrol edin ve dolmadıysa döngüyü devam ettirin.

Yorumda belirtildiği gibi, bu yaklaşım ideal değildi. Bunun nedeni, henüz işlenecek USB etkinliği olmadığında bile (çoğu zaman) Asyncify ile tüm çağrı yığınını kaydetmeye ve geri yüklemeye devam etmesi ve setTimeout() işlevinin modern tarayıcılarda minimum 4 ms süreye sahip olmasıydı. Yine de kavram kanıtlama aşamasında DSLR'den 13-14 FPS canlı yayın üretecek kadar iyi çalıştı.

Daha sonra, tarayıcı etkinlik sisteminden yararlanarak bu özelliği iyileştirmeye karar verdim. Bu uygulama daha da geliştirilebilir ancak şimdilik özel etkinlikleri belirli bir libusb veri yapısıyla ilişkilendirmeden doğrudan global nesne üzerinde yayınlamayı seçtim. Bunu, EM_ASYNC_JS makrosunu temel alan aşağıdaki bekleme ve bildirme mekanizması aracılığıyla yaptım:

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

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

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

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

em_libusb_notify() işlevi, libusb bir etkinliği (ör. veri aktarımının tamamlanması) bildirmeye çalıştığında 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 sırada em_libusb_wait() kısmı, bir em-libusb etkinliği alındığında veya zaman aşımı süresi dolduğunda Asyncify uyku modundan "uyanmak" için kullanılı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 uyanma sürelerinin önemli ölçüde azalması sayesinde bu mekanizma, önceki emscripten_sleep() tabanlı uygulamanın verimlilik sorunlarını düzeltti ve DSLR demo işleme hızını 13-14 FPS'den tutarlı bir şekilde 30+ FPS'ye çıkardı. Bu da sorunsuz bir canlı yayın için yeterlidir.

Derleme sistemi ve ilk test

Arka uç tamamlandıktan sonra bunu Makefile.am ve configure.ac'e 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, istediğiniz uzantıya bağlı olarak farklı çıkışlar üretir. Yürütülebilir uzantıyı .html olarak değiştirmek için AC_SUBST(EXEEXT, …) kullanıyorum. Böylece bir paketteki tüm yürütülebilir dosyalar (testler ve örnekler), JavaScript ve WebAssembly'nin yüklenmesini ve örneklendirilmesini sağlayan Emscripten'in varsayılan kabuğuna sahip bir 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 büyümesine (-s ALLOW_MEMORY_GROWTH) izin vermem gerekiyor. Maalesef kitaplıkların bu işaretleri bağlayıcıya bildirmesi mümkün değildir. Bu nedenle, bu libusb bağlantı noktasını kullanan her uygulamanın aynı bağlayıcı işaretlerini derleme yapılandırmasına da eklemesi gerekir.

Son olarak, daha önce de belirtildiği gibi WebUSB, cihaz numaralandırmanın bir kullanıcı hareketiyle yapılmasını gerektirir. libusb örnekleri ve testleri, başlangıçta cihazları numaralandırabileceklerini varsayar ve değişiklik yapılmadan hata vererek başarısız olur. Bunun yerine, otomatik yürütmeyi (-s INVOKE_RUN=0) devre dışı bırakmam ve manuel callMain() yöntemini (-s EXPORTED_RUNTIME_METHODS=...) kullanmam gerekti.

Tüm bunlar tamamlandıktan sonra, oluşturulan dosyaları statik bir web sunucusuyla sunabilir, WebUSB'yi ilk kullanıma hazırlayabilir ve bu HTML yürütülebilir dosyalarını Geliştirici Araçları'nın yardımıyla manuel olarak çalıştırabilirdim.

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

Geliştirici Araçları açıkken sonraki adımın ekran görüntüsü. Cihaz seçildikten sonra Console, `testlibusb` uygulamasını ayrıntılı modda yürüten yeni bir ifadeyi (`Module.callMain(['-v'])`) değerlendirdi. Çıkışta, daha önce bağlanmış USB kamera hakkında çeşitli ayrıntılı bilgiler gösterilir: üretici Sony, ürün ILCE-6600, seri numarası, yapılandırma vb.

Çok önemli bir şey gibi görünmeyebilir ancak kitaplıkları yeni bir platforma taşırken ilk kez geçerli bir çıkış ürettiği aşamaya gelmek oldukça heyecan verici.

Bağlantı noktasını kullanma

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

  1. En yeni libusb'yi derlemenizin bir parçası olarak arşiv şeklinde indirin veya projenize git alt modülü olarak ekleyin.
  2. libusb klasöründe autoreconf -fiv komutunu çalıştırın.
  3. Projeyi çapraz derleme için ilk kullanıma hazırlamak ve derlenen yapıları yerleştirmek istediğiniz yolu ayarlamak için emconfigure ./configure –host=wasm32 –prefix=/some/installation/path komutunu çalıştırın.
  4. emmake make install komutunu çalıştırın.
  5. Uygulamanızı veya daha üst düzey kitaplığınızı, daha önce seçilen yolun altında libusb'yi arayacak şekilde 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ığın şu anda birkaç sınırlaması vardır:

  • Aktarım iptali desteklenmez. Bu, WebUSB'nin bir sınırlamasıdır ve libusb'nin kendisinde platformlar arası aktarım iptali olmamasından kaynaklanır.
  • Eşzamanlı aktarım desteği yoktur. Mevcut aktarım modlarının uygulanışını örnek alarak bu modu eklemek zor olmasa gerek. Ancak bu mod pek yaygın değil ve test edebileceğim bir cihazım yoktu. Bu nedenle, şimdilik desteklenmeyen bir mod olarak bıraktım. Bu tür cihazlarınız varsa ve kitaplığa katkıda bulunmak istiyorsanız PR'larınızı bekliyoruz.
  • Daha önce bahsedilen platformlar arası sınırlamalar. Bu sınırlamalar işletim sistemleri tarafından uygulandığından, kullanıcılardan sürücüyü veya izinleri geçersiz kılmalarını istemek dışında yapabileceğimiz pek bir şey yoktur. Ancak HID veya seri cihazları taşıyorsanız libusb örneğini takip edebilir ve başka bir kitaplığı başka bir Fugu API'ye taşıyabilirsiniz. Örneğin, bir C kitaplığı hidapi'yi WebHID'ye taşıyabilir ve düşük düzey USB erişimiyle ilişkili bu sorunları tamamen ortadan kaldırabilirsiniz.

Sonuç

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

Bu tür temel ve yaygın olarak kullanılan alt düzey kitaplıkları taşımak özellikle faydalıdır. Çünkü bu sayede daha üst düzey kitaplıkları, hatta tüm uygulamaları da web'e taşıyabilirsiniz. Bu sayede, daha önce bir veya iki platformun kullanıcılarıyla sınırlı olan deneyimler her tür cihaz ve işletim sistemine açılır ve bu deneyimlere yalnızca bir bağlantı tıklamasıyla erişilebilir.

Sonraki yayında, yalnızca cihaz bilgilerini almakla kalmayıp libusb'nin aktarım özelliğini de kapsamlı bir şekilde kullanan web gPhoto2 demosunu oluşturma adımlarını inceleyeceğim. Bu arada, libusb örneğini ilham verici bulduğunuzu ve demoyu deneyeceğinizi, kitaplıkla oynayacağınızı veya hatta yaygın olarak kullanılan başka bir kitaplığı Fugu API'lerinden birine taşıyacağınızı umuyoruz.