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

Harici cihazlarla etkileşime giren kodun WebAssembly ve Fugu API'leriyle web'e nasıl taşınabileceğini öğrenin.

Ingvar Stepanyan
Ingvar Stepanyan

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

USB cihazlarıyla iletişim kuran uygulamaların, libusb (C'de yazılmış popüler bir USB kitaplığı) WebAssembly'ye (Emscripten), Asyncify ve WebUSB'ye taşınarak 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, taşınan kitaplığın yeteneklerini sergileyerek onu çeşitli şekillerde test etmenize olanak tanır ve aynı zamanda görsel açıdan ilgi çekicidir.

DSLR uzaktan kumandayı seçtim. Özellikle, açık kaynaklı bir proje olan gPhoto2, tersine mühendislik gerçekleştirerek çeşitli dijital kameraların desteğini uygulayacak kadar uzun süredir bu alanda faaliyet gösteriyor. Çeşitli protokolleri destekliyor, ancak benim en çok ilgimi çeken, libusb üzerinden gerçekleştirilen USB desteğiydi.

Bu demoyu oluşturma adımlarını iki bölüm halinde açıklayacağım. Bu blog yayınında, libusb'ın kendisini 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 gerekebileceğini açıklayacağım. İkinci gönderide, gPhoto2'nin kendisini taşıma ve entegre etme konusundaki ayrıntıları ele alacağım.

Sonunda, bir DSLR'den canlı feed önizlemesi yapan ve ayarlarını USB üzerinden kontrol edebilen, çalışan bir web uygulaması edindim. Teknik ayrıntıları okumadan önce canlı yayına veya önceden kaydedilmiş demoya göz atabilirsiniz:

Sony kameraya bağlı dizüstü bilgisayarda çalıştırılan demo.

Kameraya özgü öğelere not

Videonun ayarlarını değiştirmenin biraz zaman aldığını fark etmiş olabilirsiniz. Karşılaşabileceğiniz diğer sorunların çoğunda olduğu gibi, bunun nedeni WebAssembly veya WebUSB performansı değil, gPhoto2'nin demo için seçilen kamerayla etkileşim kurma şeklidir.

Sony a6600; ISO, diyafram açıklığı veya deklanşör hızı gibi değerleri doğrudan ayarlamak için bir API göstermez. Bunun yerine, bunları yalnızca belirtilen sayıda adım artırmak veya azaltmak için komutlar sağlar. İşleri daha karmaşık hale getirmek için, gerçekte desteklenen değerlerin bir listesi de döndürülmez. Döndürülen liste, birçok Sony kamera modelinde sabit kodlanmıştı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 tekrar okuyun.
  4. Son adımın, istenen değerin üzerinden geçmediğinden veya listenin sonunda ya da başlangıcında bulunmadığından emin olun.
  5. Tekrarla.

Bu işlem biraz zaman alabilir, ancak değer kamera tarafından gerçekten destekleniyorsa buraya gelir ve desteklenmiyorsa desteklenen en yakın değerde durur.

Diğer kameralarda muhtemelen farklı ayar grupları, temel API'leri ve ilginç özellikleri olacaktır. gPhoto2'nin açık kaynak 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ı ve PR'ler her zaman kabul edilebilir (ancak sorunları önce resmi gPhoto2 istemcisinde yeniden oluşturduğunuzdan emin olun).

Platformlar arası uyumlulukla ilgili önemli notlar

Maalesef Windows'da DSLR kameralar da dahil olmak üzere "iyi bilinen" cihazlara 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 değiştirmek için Zadig gibi bir araç kullanmanız gerekir. Bu yaklaşım benim ve diğer birçok kullanıcının işine yarıyor, ancak riski size ait olmak üzere kullanmalısınız.

Linux'ta, dağıtımınıza bağlı olarak değişse de, DSLR'nize WebUSB üzerinden erişim izni vermek için muhtemelen özel izinler ayarlamanız gerekir.

macOS ve Android'de demo kullanıma hazırdır. Android telefonda deniyorsanız, duyarlı hale getirmek için fazla çaba sarf etmedim. Yatay moda geçtiğinizden emin olun (PR'leri rica edebiliriz!):

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

WebUSB'nin platformlar arası kullanımıyla ilgili daha ayrıntılı bir kılavuz için "WebUSB için cihaz derleme" başlıklı makalenin "Platforma özgü önemli noktalar" bölümüne bakın.

Libusb'a yeni bir arka uç ekleme

Şimdi de teknik ayrıntılara geçelim. Libusb'a benzer bir dolgu API'si (daha önce başkaları tarafından yapılmıştır) sağlamak ve diğer uygulamaları buna bağlamak mümkün olsa da bu yaklaşım hataya açıktır ve ek genişletme ya da bakım işlemlerini zorlaştırır. İşleri doğru şekilde yapmak istiyordum. Bunu, gelecekte yeniden yayın yapmaya katkı sağlayıp libusb ile birleştirilebilecek bir yolla yapmak istiyordum.

Neyse ki libusb README'nin mesajı:

"libusb, diğer işletim sistemlerine aktarılabilecek şekilde dahili olarak soyutlanıyor. Daha fazla bilgi için lütfen PORTING dosyasını inceleyin."

libusb, herkese açık API'nin "arka uçlardan" ayrı olduğu şekilde yapılandırılır. Bu arka uçlar; işletim sisteminin alt düzey API'leri aracılığıyla cihazları listelemek, açmak, kapatmak ve fiilen iletişim kurmaktan sorumludur. Bu yaklaşım sayesinde Libusb Linux, macOS, Windows, Android, OpenBSD/NetBSD, Haiku ve Solaris arasındaki farklılıkları soyutlayarak tüm bu platformlarda çalışıyor.

Emscripten+WebUSB "işletim sistemi" için başka bir arka uç eklemem gerekti. 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ürler ve yardımcılar içeren libusbi.h üstbilgisini içerir ve usbi_backend usbi_os_backend türünde bir değişken sunması 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),
};

Özellikleri incelediğimizde struct'ın arka uç adını, özelliklerini, işlev işaretçileri biçiminde çeşitli düşük seviyeli 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 öğelerin işletim sistemi tutma yerlerini depolamak için kullanışlıdır. Çünkü tanıtıcılar olmadan herhangi bir işlemin hangi öğe için geçerli olduğunu bilmeyiz. Web uygulamasında işletim sistemi işleyicileri temel WebUSB JavaScript nesneleri olur. Bunları Emscripten'da temsil etmenin ve depolamanın doğal yolu, Embind (Emscripten'in bağlama sistemi) kapsamında sağlanan emscripten::val sınıfıdır.

Klasördeki arka uçların çoğu C'de uygulanır, ancak birkaç arka uç C++'ta uygulanır. Embind yalnızca C++ ile çalışır, bu yüzden bu seçimi benim yerime yaptım. Gerekli yapıya sahip libusb/libusb/os/emscripten_webusb.cpp öğesini, özel veri alanları için de sizeof(val) ile 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 işleyicileri olarak depolama

libusb, gizli veriler için ayrılan alana kullanıma hazır işaretçiler sağlıyor. 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'ın eşzamanlı işlemler beklediği eşzamansız WebUSB API'lerini işlemek için bir yönteme ihtiyaç duyulmuştur. Bunun için Asyncify'ı veya daha ayrıntılı belirtmek gerekirse val::await() üzerinden Embind entegrasyonunu kullanabilirim.

Ayrıca, WebUSB hatalarını doğru şekilde işlemek ve bunları Libusb hata kodlarına dönüştürmek istedim. Ancak şu anda Embind'in, JavaScript istisnalarını veya Promise retlerini C++ tarafında işlemek için herhangi bir yolu yok. Bu sorun, JavaScript tarafında bir ret yakalanarak ve sonucu artık C++ tarafından güvenli bir şekilde ayrıştırılabilen bir { error, value } nesnesine dönüştürülerek çözülebilir. 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 tüm Promise öğelerinde promise_result::await()'i kullanabilir, error ve value alanlarını ayrı olarak inceleyebilirim.

libusb_device_handle öğesinden bir USBDevice öğesini temsil eden val öğesi alınırken, open() yöntemi çağrılır, sonucu beklenir ve sürüm kodu durum kodu olarak hata kodu döndürülürse aşağıdaki gibi olur:

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ı

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

Zorluk, diğer platformların aksine, güvenlik nedeniyle bağlı tüm USB cihazlarını web'de numaralandırmanın mümkün olmamasıdır. Bunun yerine akış iki bölüme ayrılır. Öncelikle, web uygulaması belirli özelliklere sahip cihazları navigator.usb.requestDevice() üzerinden ister ve kullanıcı hangi cihazı açmak istediğini manuel olarak seçer veya izin istemini reddeder. Sonrasında, uygulama navigator.usb.getDevices() aracılığıyla onaylanmış ve bağlı cihazları listeler.

Başta, doğrudan get_device_list işleyicisinin uygulamasında requestDevice() kullanmayı denedim. Ancak, bağlı cihazların listesiyle birlikte bir izin istemi göstermek hassas bir işlem olarak kabul edilir ve bu işlem, kullanıcı etkileşimiyle (sayfadaki bir düğmenin tıklanması gibi) tetiklenmelidir. Aksi takdirde, her zaman reddedilen bir söz döndürür. libusb uygulamaları genellikle uygulama başlatılırken bağlı cihazları listelemek isteyebilir. Bu nedenle, requestDevice() kullanmak bir seçenek değildi.

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

// 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 kodlarını benzer şekilde kullanır. Veri aktarımı işleme kodunda birkaç ilginç saldırı daha vardır, ancak bu uygulama ayrıntıları bu makalenin amaçları açısından daha az öneme sahiptir. İlginizi çekiyorsa GitHub'daki kod ve yorumları incelemeyi unutmayın.

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

Bahsetmek istediğim diğer bir konu da olay yönetimidir. Bir önceki makalede açıklandığı gibi, C gibi sistem dillerindeki çoğu API eşzamanlıdır ve olay işleme de bu konuda istisna değildir. Genellikle bir harici G/Ç kaynağı kümesinden "anketleri" (verileri okumaya çalışır veya bazı veriler kullanılabilir hale gelene kadar yürütmeyi engeller) bir sonsuz döngüyle uygular ve bu kaynaklardan en az biri yanıt verdiğinde, bunu ilgili işleyiciye bir etkinlik olarak aktarır. İşleyici tamamlandıktan sonra denetim, döngüye geri döner ve başka bir anket için duraklar.

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

Öncelikle, WebUSB, temel cihazların ham tutma yerlerini açığa çıkarmaz. Bu nedenle, bunları doğrudan yoklamak bir seçenek değildir. İkinci olarak, libusb, ham cihaz tanıtıcıları olmayan işletim sistemlerinde aktarımları işlemek ve diğer etkinlikler 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 veya Promise işleyicilerini çağırır. Başka bir iç içe geçmiş, sonsuz etkinlik döngüsünün gerçekleştirilmesi tarayıcının etkinlik döngüsünün sürekli ilerlemesini engeller. Bu da yalnızca kullanıcı arayüzünün yanıt vermemesine neden olmaz, aynı zamanda kodun beklediği G/Ç etkinlikleri için hiçbir zaman bildirim almayacağı anlamına gelir. Bu durum genellikle çıkmaza neden olur. Demoda Libusb kullanmaya çalıştığımda da bu olmuştu. 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 ve sonuçları ana işleme geri aktarmak için uygulamayı yeniden düzenlemektir. Diğeri ise döngüyü duraklatmak ve etkinlikleri engellemeyecek şekilde beklemek için Asyncify'ı kullanmaktır.

libusb veya gPhoto2'de önemli değişiklikler yapmak istemedim ve Promise entegrasyonu için Asyncify'ı zaten kullandım, bu yüzden bu yolu seçtim. poll() değişkeninin engelleme varyantını simüle etmek için, ilk kavram ispatı 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

Şunları yapar:

  1. Arka uç tarafından henüz herhangi bir etkinlik bildirilip bildirilmediğini kontrol etmek için poll() numaralı telefonu çağırır. Bunlar varsa döngü durur. Aksi takdirde, Emscripten'ın poll() uygulaması hemen 0 ile birlikte döndürülür.
  2. emscripten_sleep(0) numaralı telefonu arar. Bu işlev, temel olarak Eş zamansız ve setTimeout() özelliklerini kullanır ve burada, kontrolü tekrar ana tarayıcı etkinlik döngüsüne vermek için kullanılır. Bu, tarayıcının WebUSB dahil olmak üzere kullanıcı etkileşimlerini ve G/Ç etkinliklerini işlemesine olanak tanır.
  3. Belirtilen zaman aşımı süresinin dolduğunu kontrol edin ve dolmadıysa döngüye devam edin.

Yorumda da belirtildiği gibi, bu yaklaşım ideal değildi, çünkü henüz işlenecek herhangi bir USB etkinliği olmadığında bile (çoğu zaman) çağrı yığınının tamamını Asyncify ile geri yüklemeye devam ediyordu ve setTimeout() modern tarayıcılarda minimum 4 ms süresine sahipti. Yine de, kavram kanıtlama açısından DSLR'den 13-14 FPS'lik bir canlı yayın akışı sağlayacak kadar iyi performans gösterdi.

Daha sonra, tarayıcı etkinlik sisteminden yararlanarak etkinliği geliştirmeye karar verdim. Bu uygulamanın daha fazla geliştirilebileceği çeşitli yollar vardır, ancak şimdilik özel etkinlikleri belirli bir bağımsız veri yapısıyla ilişkilendirmeden, doğrudan global nesne üzerinde yayınlamayı seçtim. Bu işlemi EM_ASYNC_JS makrosuna dayanan aşağıdaki bekleme ve bildirim mekanizmasını kullanarak gerçekleştirdik:

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ü, bir em-libusb etkinliği alındığında veya zaman aşımı süresi dolduğunda eşzamansız 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;
}

Bu mekanizma, uyku ve uyandırma sayısındaki önemli düşüş nedeniyle ö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 canlı feed için yeterli olan 30+ FPS'ye yükseltti.

Derleme sistemi ve ilk test

Arka uç tamamlandıktan sonra Makefile.am ve configure.ac sitelerine eklemem gerekti. Buradaki tek ilginç kısım, 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']"
  ;;

Öncelikle, Unix platformlarındaki yürütülebilir dosyaların normalde dosya uzantıları yoktur. Ancak Emscripten, istediğiniz uzantıya 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 bir paket içindeki yürütülebilir dosyalar (testler ve örnekler), JavaScript ve WebAssembly'nin yüklenmesi ve örneklerinin yüklenmesi görevini üstlenen, 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 artışına (-s ALLOW_MEMORY_GROWTH) izin vermem gerekiyor. Maalesef bir kitaplığın bu işaretleri bağlayıcıya bildirmesinin bir yolu yoktur. Bu nedenle, bu Libusb bağlantı noktasını kullanan her uygulamanın derleme yapılandırmasına aynı bağlayıcı işaretlerini de eklemesi gerekir.

Son olarak, daha önce de belirtildiği gibi WebUSB, cihaz numaralandırma işleminin bir kullanıcı hareketi aracılığıyla yapılmasını gerektirir. Libusb örnekleri ve testleri, cihazları başlangıçta numaralandırabileceklerini ve değişiklik yapmadan bir hata vererek başarısız olduklarını varsayar. Bunun yerine, otomatik yürütmeyi (-s INVOKE_RUN=0) devre dışı bırakmak ve manuel callMain() yöntemini (-s EXPORTED_RUNTIME_METHODS=...) kullanıma sunmak zorunda kaldım.

Tüm bunlar yapıldıktan sonra, oluşturulan dosyaları statik bir web sunucusuyla sunabildim, WebUSB'yi başlatabildim ve bu HTML yürütülebilir dosyalarını Geliştirici Araçları'nın yardımıyla manuel olarak çalıştırabildim.

Yerel olarak sunulan &quot;testlibusb&quot; sayfasında Geliştirici Araçları&#39;nın açık olduğu bir Chrome penceresini gösteren ekran görüntüsü. DevTools konsolu &quot;navigator.usb.requestDevice({ filters: [] })&quot; işlemini değerlendiriyor. Bu işlem, bir 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ı&#39;nın hâlâ açık olduğu bir sonraki adımın ekran görüntüsü Cihaz seçildikten sonra Console, yeni &quot;Module.callMain([&#39;-v&#39;])&quot; ifadesini değerlendirerek &quot;testlibusb&quot; uygulamasını ayrıntılı modda yürüttü. Çıkışta, daha önce bağlanmış USB kamerayla ilgili çeşitli ayrıntılı bilgiler gösterilir: Sony üreticisi, ILCE-6600 ürünü, seri numarası, yapılandırma vb.

Pek bir şey ifade etmiyor gibi görünse de kitaplıkları yeni bir platforma taşırken ilk kez geçerli bir çıktı ürettiği aşamaya ulaşmak oldukça heyecan vericidir.

Bağlantı noktasını kullanma

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

  1. En son libusb'ı derlemenizin bir parçası olarak arşiv olarak 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 bir yol belirlemek 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. Önceki seçilen yolun altında libusbı aramak için uygulamanıza veya üst düzey kütüphanenize işaret edin.
  6. Uygulamanızın bağlantı bağımsız değişkenlerine şu işaretleri ekleyin: --bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH.

Kitaplıkta şu anda birkaç sınırlama vardır:

  • Aktarım iptali desteği yoktur. Bu, WebUSB'nin bir sınırlamasıdır. Sonuç olarak, Libusb'ın kendisinde platformlar arası aktarım iptalinin olmamasından kaynaklanır.
  • Eşzamanlı aktarım desteği yoktur. Mevcut aktarım modlarını örnek olarak uygulayarak eklemenin zor olmaması gerekiyor. Ancak bu, nadir karşılaşılan bir mod ve test edecek cihazım olmadığı için şimdilik bu modu desteklenmiyor olarak bıraktım. Bu tür cihazlarınız varsa ve kitaplığa katkıda bulunmak istiyorsanız halkla ilişkiler departmanını kullanabilirsiniz.
  • Yukarıda, platformlar arası sınırlamalardan bahsedilmişti. Bu sınırlamalar işletim sistemleri tarafından uygulanır. Kullanıcılardan sürücüyü veya izinleri geçersiz kılmalarını istemek dışında bu konuda yapabileceğimiz pek bir şey yoktur. Ancak, HID veya seri cihazları taşıyorsanız libusb örneğini uygulayarak başka bir kitaplığı başka bir Fugu API'sine bağlayabilirsiniz. Örneğin, bir C kitaplığını hidapi olarak WebHID'ye taşıyabilir ve düşük düzeyli USB erişimiyle ilişkili bu sorunların tamamen önüne geçebilirsiniz.

Sonuç

Bu yayında Emscripten'in yardımıyla, 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österiyorum.

Böyle temel ve yaygın olarak kullanılan alt düzey kitaplıkların taşınması özellikle avantajlıdır, çünkü sonuç olarak üst düzey kitaplıkların, hatta uygulamaların tamamını da web'e getirmeye olanak sağlar. Bu sayede, daha önce bir veya iki platformun kullanıcıları ile sınırlı olan deneyimler her tür cihaz ve işletim sistemine açılır ve bu deneyimler bir bağlantı tık uzağınızdadır.

Sonraki gönderide, yalnızca cihaz bilgilerini almakla kalmayıp, libusb'ın aktarım özelliğini de kapsamlı bir şekilde kullanan web gPhoto2 demosunu oluştururken gereken adımları ele alacağım. Bu arada, libusb örneğini ilham verici bulduğunuzu ve demoyu deneyeceğinizi, kitaplıkla oynayacağınızı, hatta isterseniz diğer bir kitaplığı da Fugu API'lerinden birine taşıyacağınızı umuyorum.