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 yayında, File System Access API, WebAssembly ve Asyncify ile dosya sistemi API'lerini kullanan 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şımayla ilgili 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ılması gereken en önemli şey, doğru demoyu seçmektir. Bu demo, taşınan kitaplığın özelliklerini sergilemeli, çeşitli şekillerde test etmenize olanak tanımalı ve aynı zamanda görsel açıdan ilgi çekici olmalıdı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 yayında, gPhoto2'nin taşınması ve entegrasyonu hakkında ayrıntılı bilgi vereceğim.

Sonuç olarak, DSLR'den gelen canlı yayını ö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ü ö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. İşleri daha da karmaşık hale getiren bir diğer nokta da, aslında desteklenen değerlerin listesini döndürmemesidir. Döndürülen liste, birçok Sony kamera modelinde sabit kodlanmış gibi görünüyor.

gPhoto2, bu değerlerden birini ayarlarken aşağıdakilerden 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 işlem biraz zaman alabilir. Ancak değer kamera tarafından gerçekten destekliyorsa değere ulaşılır. Aksi takdirde, en yakın desteklenen değerde durur.

Diğer kameralarda farklı ayar grupları, temel API'ler ve tuhaflıklar olabilir. 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'ta DSLR kameralar da dahil olmak üzere "iyi bilinen" cihazlara WebUSB ile uyumlu olmayan bir sistem sürücüsü atanı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 benim için ve diğer birçok kullanıcı için işe yaradı ancak bu yöntemi kullanmanın riski size aittir.

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, kutudan çıkar çıkmaz çalışmalıdır. Android telefonda denerseniz duyarlı hale getirmek için çok fazla çaba göstermediğim için yatay moda geçtiğinizden emin olun (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ı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. Her şeyi 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şınabilmesi için 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 düşük seviyeli API'leri aracılığıyla cihazları listelemekten, açmaktan, kapatmaktan ve cihazlarla gerçek iletişimden sorumludur. libusb, Linux, macOS, Windows, Android, OpenBSD/NetBSD, Haiku ve Solaris arasındaki farklılıkları bu şekilde soyutlar ve tüm bu platformlarda çalışır.

Bunun için 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, yapının arka uç adını, bir dizi özelliğini, işlev işaretçisi biçiminde çeşitli düşük düzey USB işlemleri için işleyicileri ve son olarak da ö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 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 göstermenin 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çı C++'da uygulanır. Embind yalnızca C++ ile çalışır. Bu nedenle, seçim benim için yapıldı ve gerekli yapıya sahip libusb/libusb/os/emscripten_webusb.cpp ve ö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 adı olarak depolama

libusb, gizli veriler için ayrılan alana hazır işaretçiler sağlar. Bu işaretçileri val örnekleri olarak kullanmak için onları yerinde oluşturan, referans olarak alan ve değerleri dışarı aktaran 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'i veya daha spesifik 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şlemek için herhangi bir yolu 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ırma

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 promise 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 çoğu, yukarıda gösterildiği gibi val ve promise_result değerlerini benzer bir şekilde 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ı 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 yanıtı ilgili işleyiciye etkinlik olarak ileten sonsuz bir döngü aracılığıyla uygulanır. İşleyici tamamlandıktan 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üne sahip 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 yalnızca yanıt vermemesi değil, kodun beklediği I/O etkinlikleri için hiçbir zaman bildirim almaması anlamına gelir. Bu durum genellikle kilitlenmeye neden olur. libusb'yi bir demoda kullanmaya çalıştığımda da bu durumla karşılaştım. Sayfa dondu.

Diğer engelleyen G/Ç'lerde 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 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 daha önce Asyncify'i kullanmıştım. Bu nedenle bu yolu seçtim. İ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

Bu işlem şu şekilde gerçekleşir:

  1. Arka uç tarafından henüz herhangi bir etkinlik raporlanıp raporlanmadığını kontrol etmek için poll() çağrısı yapar. Bu durumda döngü durur. Aksi takdirde Emscripten'in poll() uygulaması hemen 0 ile döner.
  2. emscripten_sleep(0) numaralı telefondan arama. 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, 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 dolup dolmadığını kontrol edin. Dolmadıysa 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 sırada em_libusb_wait() kısmı, bir em-libusb etkinliği alındığında veya zaman aşımı sona erdiğinde Asyncify uykusundan "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ğladığı için 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, 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şaretlerin değiştirilmesidir:

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 genellikle dosya uzantıları yoktur. Ancak Emscripten, hangi uzantıyı istediğinize bağlı olarak farklı çıkışlar oluşturur. Bir paketteki tüm yürütülebilir dosyaların (testler ve örnekler) JavaScript ve WebAssembly'nin yüklenmesini ve örneklendirilmesini sağlayan Emscripten'in varsayılan kabuğunu içeren bir HTML dosyası haline gelmesi için yürütülebilir dosya uzantısını AC_SUBST(EXEEXT, …) ile .html olarak değiştiriyorum.

İ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 DevTools&#39;un açık olduğu bir 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.

Çok önemli bir şey gibi görünmese de kitaplıkları yeni bir platforma taşırken ilk kez geçerli bir çıkış ürettiği aşamaya ulaşmak oldukça heyecan vericidir.

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 ş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 ayarlamak için emconfigure ./configure –host=wasm32 –prefix=/some/installation/path komutunu çalıştırın.
  4. emmake make install'ü çalıştırın.
  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 aşağıdaki 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'nin bir sınırlamasıdır ve libusb'de platformlar arası aktarım iptalinin olmamasından kaynaklanır.
  • Eşzamanlı aktarım desteği yoktur. Mevcut aktarım modlarının uygulanmasını örnek alarak eklemek zor olmasa da bu mod biraz nadir olduğundan ve test edebileceğiniz cihazım olmadığından şimdilik desteklenmiyor olarak bıraktım. Bu tür cihazlarınız varsa ve kitaplığa katkıda bulunmak istiyorsanız PR'lerinizi 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 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ın web'e taşınmasına olanak tanır. Bu sayede, daha önce yalnızca bir veya iki platformun kullanıcılarına sunulan deneyimler her türlü cihaz ve işletim sistemine sunulur. Böylece, bu deneyimlere yalnızca bir bağlantı tıklamasıyla erişilebilir.

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 denemeyi deneyeceğinizi, kitaplıkla oynayacağınızı ya da yaygın olarak kullanılan başka bir kitaplığı Fugu API'lerinden birine taşıyacağınızı umuyoruz.