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

Harici cihazlarla etkileşime geçen kodun WebAssembly ve Fugu API'leriyle web'e nasıl taşınabileceğ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 dilinde yazılmış popüler bir USB kitaplığı olan libusb'yi WebAssembly'e (Emscripten aracılığıyla), Asyncify'ye 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ı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.

DSLR uzaktan kumandası fikrini seçtim. Özellikle gPhoto2 adlı açık kaynak proje, geriye dönük mühendislik yaparak çok çeşitli dijital kameralar için destek sunacak kadar uzun süredir bu alanda faaliyet gösteriyor. Çeşitli protokolleri destekler ancak en çok ilgilendiğim, libusb üzerinden 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 hileleri uygulamam gerektiğini açıklayacağı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 önizlemesi yapan 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ü özelliklerle ilgili not

Videoda ayarların değiştirilmesinin 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şimin nasıl olduğundan kaynaklanır.

Sony a6600, ISO, diyafram veya deklanşör hızı gibi değerleri doğrudan ayarlamak için bir API sağlamaz. Bunun yerine, bu değerleri belirtilen adım sayısı kadar artırma veya azaltma komutları sağlar. Daha da karmaşık hale getirmek gerekirse, aslında desteklenen değerlerin listesini de döndürmez. Döndürülen liste, birçok Sony kamera modelinde sabit kodlanmış gibi görünüyor.

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

  1. Seçilen değere doğru bir adım (veya birkaç adım) atın.
  2. Kameranın ayarları güncellemesi için biraz bekleyin.
  3. Kameranın gerçekten odaklandığı değeri geri okuyun.
  4. Son adımın, istenen değeri atlamadığını veya listenin sonuna ya da başına sarıldığını kontrol edin.
  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 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 memnuniyetle karşılanır (ancak önce sorunları resmi gPhoto2 istemcisinde yeniden oluşturduğunuzdan emin olun).

Platformlar arası uyumlulukla ilgili önemli notlar

Maalesef Windows'da DSLR kameralar da dahil olmak üzere "tanınan" tüm cihazlara WebUSB ile uyumlu olmayan bir sistem sürücüsü atanmıştır. Windows'ta denemeyi denemek istiyorsanız bağlı DSLR'nin sürücüsünü WinUSB veya libusb ile 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 uygundur ancak bu yaklaşımı kullanmanın doğurabileceği riskleri üstlenmeniz gerekir.

Linux'da, DSLR'nize WebUSB üzerinden erişime izin vermek için muhtemelen özel izinler belirlemeniz gerekir. Bu, dağıtımınıza 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!):

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

WebUSB'nin platformlar arası kullanımıyla ilgili daha ayrıntılı bir kılavuz için "WebUSB için cihaz oluşturma" başlıklı makalenin "Platforma özgü 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 shim API 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 hatalara açıktır ve daha fazla uzantı veya bakım işlemini zorlaştırır. İşleri doğru şekilde, gelecekte ana akışa katkıda bulunabilecek ve libusb ile birleştirilebilecek bir şekilde yapmak istedim.

Neyse ki libusb README'da şu bilgiler yer alıyor:

"libusb, diğer işletim sistemlerine taşınabilecek şekilde dahili olarak soyutlanmıştır. 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ılmıştı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üne sahip bir usbi_backend değişkeni göstermesi gerekir. Örneğin, Windows arka ucu aşağıdaki gibi 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, struct'ın arka uç adını, bir dizi özellik kümesini, işlev işaretçileri biçimindeki çeşitli düşük düzey 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ı, en azından bu tür tüm öğelerin işletim sistemi tutamaçlarını depolamak için yararlıdır. Tutamaçlar olmadan, belirli bir işlemin hangi öğe için geçerli olduğunu bilemeyiz. Web uygulamasında işletim sistemi anahtarları, temel WebUSB JavaScript nesneleri olur. Bunları Emscripten'de 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 bazıları C++'da uygulanır. Embind yalnızca C++ ile çalışır. Bu nedenle seçim benim yerime yapıldı ve gerekli yapıya ve özel veri alanları için sizeof(val) ile libusb/libusb/os/emscripten_webusb.cpp 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, gizli veriler için ayrılan alana hazır işaretçiler sağlar. Bu işaretçilerle val örnekleri olarak çalışmak için, onları 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))) {}
};

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

Artık libusb'in senkronize işlemleri beklediği durumlarda asenkron WebUSB API'lerini işlemek için bir yönteme ihtiyaç var. Bunun için Asyncify'ı veya daha kesin olarak 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 Embind'in şu anda C++ tarafında JavaScript istisnalarını veya Promise retlerini işleme yöntemi yok. Bu sorun, JavaScript tarafında reddedilen öğeleri yakalayıp sonucu C++ tarafında güvenli bir şekilde ayrıştırılabilen bir { error, value } nesnesine dönüştürerek çö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 herhangi bir Promise üzerinde promise_result::await() kullanabilir ve error ve value alanlarını ayrı ayrı inceleyebilirim.

Örneğin, libusb_device_handle öğesinden USBDevice öğesini temsil eden bir val almak, open() yöntemini çağırmak, sonucunu beklemek ve libusb durum kodu olarak bir hata kodu döndürmek aşağıdaki gibi 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ı

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

Diğer platformların aksine, güvenlik nedeniyle web'de bağlı tüm USB cihazlarını saymanın bir yolu yoktur. 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ı, hangi cihazı göstermek istediğini manuel olarak seçer veya izin istemini reddeder. Ardından uygulama, navigator.usb.getDevices() üzerinden önceden onaylanmış ve bağlı cihazları listeler.

İlk başta requestDevice()'ü doğrudan get_device_list işleyicisinin uygulanmasında 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şimi (ör. bir sayfadaki düğme tıklaması) ile tetiklenmesi gerekir. Aksi takdirde her zaman reddedilen bir söz döndürülür. libusb uygulamaları genellikle uygulama başlatıldıktan sonra 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ırakmam ve yalnızca navigator.usb.getDevices()'ten önceden 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 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ü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 API'lerin çoğu senkronizedir ve etkinlik işleme buna istisna değildir. Genellikle bir dizi harici G/Ç kaynağından "anket yapan" (verileri okumaya çalışan veya bazı veriler mevcut olana kadar yürütmeyi engelleyen) ve bunlardan en az biri yanıt verdiğinde bunu ilgili işleyiciye etkinlik olarak ileten sonsuz bir döngü aracılığıyla uygulanır. İş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 birkaç sorun vardır.

Öncelikle, WebUSB temel cihazların ham tutamaçlarını göstermez ve gösteremez. Bu nedenle, bu cihazları doğrudan yoklamak bir seçenek değildir. İkinci olarak, libusb, diğer etkinliklerin yanı sıra işletim sistemlerinde ham cihaz tutamaçları olmadan aktarımları işlemek için eventfd ve pipe API'lerini kullanır. Ancak eventfd şu anda Emscripten'de desteklenmemektedir 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ünün olmasıdır. Bu genel etkinlik döngüsü, harici I/O 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. İç içe yerleştirilmiş başka bir sonsuz etkinlik döngüsü yürütmek, tarayıcının etkinlik döngüsünün ilerlemesini engeller. Bu da kullanıcı arayüzünün yanıt vermemesi ve kodun beklediği I/O etkinlikleri için hiçbir zaman bildirim almaması 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, uygulamayı G/Ç etkinliklerini ayrı bir iş parçacığında işlemek ve sonuçları ana iş parçacığına geri iletmek için yeniden düzenlemektir. Diğeri ise döngüyü duraklatmak ve etkinlikleri engellemeden beklemek için Asyncify'i kullanmaktır.

Libusb veya gPhoto2'de önemli değişiklikler yapmak istemedim ve Promise entegrasyonu için zaten Eşzamansız İlk kavram kanıtı için poll()'ün engelleme varyantını simüle etmek amacıyla aşağıda gösterilen 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. Bu durumda döngü durur. Aksi takdirde, Emscripten'in poll() uygulaması 0 ile hemen geri dönecektir.
  2. emscripten_sleep(0) numaralı telefondan arama. 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 tüm kullanıcı etkileşimlerini ve G/Ç etkinliklerini işlemesine olanak tanır.
  3. Belirtilen zaman aşımının henüz sona erip ermediğini kontrol edin. Erememişse döngüye devam edin.

Yorumda da belirtildiği gibi, henüz işlenecek USB etkinliği olmadığında bile (çoğu zaman) Asyncify ile çağrı yığınının tamamını kaydedip geri yüklemeye devam ettiği ve setTimeout()'ın modern tarayıcılarda minimum 4 ms süresi olduğu için bu yaklaşım en uygun yaklaşım değildi. Yine de kavram kanıtlama aşamasında DSLR'den 13-14 FPS canlı yayın oluşturmak için yeterince iyi çalıştı.

Daha sonra tarayıcı etkinlik sisteminden yararlanarak bunu iyileştirmeye karar verdim. Bu uygulamanın daha da iyileştirilebileceği birkaç yol vardır ancak şimdilik özel etkinlikleri belirli bir libusb veri yapısıyla ilişkilendirmeden doğrudan genel nesnede yayınlamayı tercih ettim. Bunu, EM_ASYNC_JS makrosuna dayalı aşağıdaki bekleme ve bildirim 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 etkinlik (ör. veri aktarımı 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 esnada em_libusb_wait() bölümü, bir em-libusb etkinliği alındığında ya da 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 işlemlerinde önemli bir azalma sağlayan bu mekanizma, önceki emscripten_sleep() tabanlı uygulamanın verimlilik sorunlarını düzeltti ve DSLR demo aktarım hızını 13-14 FPS'den tutarlı bir şekilde 30 FPS'nin üzerine çıkardı. Bu da sorunsuz bir canlı yayın için yeterlidir.

Derleme sistemi ve ilk test

Arka uç tamamlandıktan sonra Makefile.am ve configure.ac'a eklemem gerekiyordu. 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ı istediğinize bağlı olarak farklı çıkışlar oluşturur. 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ştirmenin yanı sıra bağlayıcı parametreleri aracılığıyla dinamik bellek büyümesine (-s ALLOW_MEMORY_GROWTH) izin vermem gerekiyor. Maalesef bir kitaplığı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ırmalarına da eklemesi gerekir.

Son olarak, daha önce de belirtildiği gibi WebUSB, cihaz numaralandırmasının kullanıcı hareketi aracılığıyla yapılmasını gerektirir. libusb örnekleri ve testleri, cihazları başlangıçta numaralandırabileceğini varsayar ve değişiklik yapılmadan hatayla başarısız olur. Bunun yerine, otomatik yürütmeyi (-s INVOKE_RUN=0) devre dışı bırakıp manuel callMain() yöntemini (-s EXPORTED_RUNTIME_METHODS=...) göstermem gerekiyordu.

Tüm bu işlemler tamamlandıktan sonra, oluşturulan dosyaları statik bir web sunucusuyla yayınlayabilir, WebUSB'yi başlatabilir ve bu HTML yürütülebilir dosyalarını DevTools'un yardımıyla manuel olarak çalıştırabilirdim.

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ü. DevTools konsolu, &quot;navigator.usb.requestDevice({ filters: [] })&quot; ifadesini değerlendiriyor. Bu ifade, bir izin istemi tetikledi ve şu anda kullanıcıdan sayfayla paylaşılacak bir USB cihazı seçmesini istiyor. ILCE-6600 (Sony kamera) şu anda seçili.

Geliştirici Araçları hâlâ açıkken sonraki adımın ekran görüntüsü. Cihaz seçildikten sonra Console, &quot;testlibusb&quot; uygulamasını ayrıntılı modda çalıştıran yeni bir ifade olan &quot;Module.callMain([&#39;-v&#39;])&quot; ifadesini değerlendirdi. Çıkışta, daha önce bağlı olan USB kamerayla ilgili çeşitli ayrıntılı bilgiler gösterilir: üretici Sony, ürün ILCE-6600, 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ğlantı 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 son libusb'yi derlemenizin bir parçası olarak arşiv olarak indirin veya projenize git alt modülü olarak ekleyin.
  2. libusb klasöründe autoreconf -fiv dosyasını ç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. Uygulamanızı veya üst düzey kitaplığınızı, daha önce seçilen yol altında libusb'yi aramaya 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ıkta şu anda birkaç sınırlama vardır:

  • Aktarım iptal desteği sunulmaz. 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ı ö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 bahsedilen platformlar arası kısıtlamalar. 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. Ancak HID veya seri cihazları taşıyorsanız libusb örneğini uygulayabilir ve başka bir kitaplığı başka bir Fugu API'sine taşıyabilirsiniz. Örneğin, bir C kitaplığını (hidapi) WebHID'e taşıyabilir ve düşük düzey USB erişimi ile ilişkili bu sorunları tamamen atlayabilirsiniz.

Sonuç

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

Bu tür temel ve yaygın olarak kullanılan düşük düzey kitaplıkların taşınması özellikle ödüllendiricidir. Çünkü bu, daha yüksek düzey kitaplıkların veya hatta uygulamaların tamamını da web'e taşımaya 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.

Sonraki yayında, yalnızca cihaz bilgilerini almakla kalmayıp libusb'in aktarım özelliğini de yoğun şekilde kullanan web gPhoto2 demosunu oluşturmayla ilgili adımları 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.