এক্সটার্নাল ডিভাইসের সাথে ইন্টার্যাক্ট করে এমন কোড কীভাবে WebAssembly এবং Fugu API-এর মাধ্যমে ওয়েবে পোর্ট করা যায় তা জানুন।
আগের একটি পোস্টে, আমি দেখিয়েছি কীভাবে File System Access API, WebAssembly এবং Asyncify-এর সাহায্যে ওয়েবে ফাইলসিস্টেম API ব্যবহার করে অ্যাপ পোর্ট করতে হয়। এখন আমি WebAssembly-এর সাথে Fugu API ইন্টিগ্রেট করা এবং গুরুত্বপূর্ণ ফিচার না হারিয়ে ওয়েবে অ্যাপ পোর্ট করার একই বিষয় নিয়ে আলোচনা চালিয়ে যেতে চাই।
USB ডিভাইসের সাথে যোগাযোগ করে এমন অ্যাপ কীভাবে ওয়েবে পোর্ট করা যায় তা আমি দেখাব। এর জন্য libusb—C-তে লেখা একটি জনপ্রিয় USB লাইব্রেরি—WebAssembly-তে (Emscripten-এর মাধ্যমে), Asyncify এবং WebUSB-এ পোর্ট করা হবে।
সবচেয়ে জরুরি কাজ: একটি ডেমো
লাইব্রেরি পোর্ট করার সময় সবচেয়ে গুরুত্বপূর্ণ কাজ হল সঠিক ডেমো বেছে নেওয়া—এমন কিছু যা পোর্ট করা লাইব্রেরির ক্ষমতা প্রদর্শন করবে, আপনাকে বিভিন্ন উপায়ে এটি পরীক্ষা করার সুযোগ দেবে এবং একই সাথে ভিজ্যুয়ালি আকর্ষণীয় হবে।
আমি যে আইডিয়া বেছে নিয়েছিলাম সেটি হল DSLR রিমোট কন্ট্রোল। বিশেষ করে, একটি ওপেন সোর্স প্রোজেক্ট gPhoto2 এই স্পেসে অনেকদিন ধরে রয়েছে যা রিভার্স-ইঞ্জিনিয়ারিং করে এবং বিভিন্ন ধরনের ডিজিটাল ক্যামেরার জন্য সাপোর্ট প্রয়োগ করে। এটি বেশ কয়েকটি প্রোটোকল সাপোর্ট করে, তবে আমি যেটিতে সবচেয়ে বেশি আগ্রহী ছিলাম সেটি হল USB সাপোর্ট, যা এটি libusb-এর মাধ্যমে করে।
আমি এই ডেমো তৈরির ধাপগুলি দুটি ভাগে বর্ণনা করব। এই ব্লগ পোস্টে, আমি বর্ণনা করব কীভাবে আমি libusb পোর্ট করেছি এবং অন্যান্য জনপ্রিয় লাইব্রেরিগুলিকে Fugu API-তে পোর্ট করার জন্য কী কী কৌশল প্রয়োজন হতে পারে। দ্বিতীয় পোস্টে, আমি gPhoto2 পোর্ট ও ইন্টিগ্রেট করার বিষয়ে বিস্তারিত আলোচনা করব।
শেষ পর্যন্ত, আমি একটি কার্যকরী ওয়েব অ্যাপ্লিকেশন পেয়েছি যা DSLR থেকে লাইভ ফিডের প্রিভিউ দেখায় এবং USB-এর মাধ্যমে এর সেটিংস নিয়ন্ত্রণ করতে পারে। টেকনিক্যাল বিবরণ পড়ার আগে লাইভ বা আগে থেকে রেকর্ড করা ডেমো দেখতে পারেন:
ক্যামেরা-নির্দিষ্ট অদ্ভুত আচরণ সম্পর্কে নোট
আপনি হয়ত লক্ষ্য করেছেন যে ভিডিওতে সেটিংস পরিবর্তন করতে কিছুটা সময় লাগে। আপনি দেখতে পেতে পারেন এমন অন্যান্য সমস্যার মতো, এটি WebAssembly বা WebUSB-এর পারফর্ম্যান্সের কারণে হয় না, তবে ডেমোর জন্য বেছে নেওয়া নির্দিষ্ট ক্যামেরার সাথে gPhoto2 কীভাবে ইন্টার্যাক্ট করে তার কারণে হয়।
Sony a6600-এ ISO, অ্যাপারচার বা শাটার স্পিডের মতো ভ্যালু সরাসরি সেট করার জন্য কোনও API নেই, এর পরিবর্তে নির্দিষ্ট সংখ্যক স্টেপ অনুযায়ী এগুলি বাড়ানো বা কমানোর জন্য কমান্ড প্রদান করে। বিষয়টিকে আরও জটিল করে তুলতে, এটি আসলে কাজ করে এমন ভ্যালুর তালিকাও ফেরত দেয় না—ফেরত দেওয়া তালিকাটি অনেক Sony ক্যামেরা মডেল জুড়ে হার্ডকোড করা বলে মনে হয়।
এইসব ভ্যালুর মধ্যে কোনও একটি সেট করার সময়, gPhoto2-এর কাছে এগুলি করা ছাড়া আর কোনও উপায় থাকে না:
- বেছে নেওয়া মানের দিকে এক (বা কয়েক) ধাপ এগিয়ে যান।
- ক্যামেরা সেটিংস আপডেট করার জন্য কিছুক্ষণ অপেক্ষা করুন।
- ক্যামেরা আসলে যে ভ্যালুতে ল্যান্ড করেছে তা পড়ে শোনান।
- শেষ ধাপটি কাঙ্খিত মানকে এড়িয়ে যায়নি বা তালিকার শেষ বা শুরুতে মোড়ানো হয়নি তা চেক করুন।
- আবার করুন।
এতে কিছুটা সময় লাগতে পারে, তবে ক্যামেরায় যদি সেই ভ্যালু কাজ করে, তাহলে এটি সেখানে পৌঁছে যাবে এবং যদি না হয়, তাহলে এটি নিকটতম কাজ করা ভ্যালুতে থেমে যাবে।
অন্যান্য ক্যামেরায় সম্ভবত আলাদা আলাদা সেটিংস, অন্তর্নিহিত API এবং অদ্ভুত বৈশিষ্ট্য থাকবে। মনে রাখবেন যে gPhoto2 হল একটি ওপেন-সোর্স প্রোজেক্ট এবং সেখানে থাকা সব ক্যামেরা মডেলের অটোমেটিক বা ম্যানুয়াল টেস্টিং করা সহজ নয়, তাই বিস্তারিত সমস্যা সংক্রান্ত রিপোর্ট এবং PR-কে সবসময় স্বাগত জানানো হয় (তবে প্রথমে অফিসিয়াল gPhoto2 ক্লায়েন্টের সাথে সমস্যাটি আবার তৈরি করতে ভুলবেন না)।
ক্রস-প্ল্যাটফর্ম কম্প্যাটিবিলিটি সংক্রান্ত গুরুত্বপূর্ণ তথ্য
দুর্ভাগ্যবশত, Windows-এ DSLR ক্যামেরা সহ যেকোনও "পরিচিত" ডিভাইসে একটি সিস্টেম ড্রাইভার অ্যাসাইন করা থাকে, যা WebUSB-এর সাথে মানানসই নয়। আপনি Windows-এ ডেমো ব্যবহার করে দেখতে চাইলে, কানেক্ট করা DSLR-এর ড্রাইভারকে WinUSB বা libusb-এ ওভাররাইড করার জন্য আপনাকে Zadig-এর মতো টুল ব্যবহার করতে হবে। এই পদ্ধতি আমার এবং অন্যান্য অনেক ব্যবহারকারীর জন্য ভাল কাজ করে, তবে আপনি নিজের দায়িত্বে এটি ব্যবহার করুন।
Linux-এ, WebUSB-এর মাধ্যমে আপনার DSLR-এ অ্যাক্সেসের অনুমতি দিতে আপনাকে সম্ভবত কাস্টম অনুমতি সেট করতে হবে, যদিও এটি আপনার ডিস্ট্রিবিউশনের উপর নির্ভর করে।
macOS এবং Android-এ, ডেমোটি কোনও সমস্যা ছাড়াই কাজ করা উচিত। আপনি যদি Android ফোনে এটি করার চেষ্টা করেন, তাহলে ল্যান্ডস্কেপ মোডে পরিবর্তন করতে ভুলবেন না কারণ আমি এটিকে রেসপন্সিভ করার জন্য খুব বেশি চেষ্টা করিনি (PR-এর জন্য স্বাগত!):
WebUSB-এর ক্রস-প্ল্যাটফর্ম ব্যবহার সম্পর্কে আরও বিস্তারিত গাইডের জন্য, "WebUSB-এর জন্য ডিভাইস তৈরি করা" নিবন্ধের "প্ল্যাটফর্ম-নির্দিষ্ট বিবেচনা" বিভাগ দেখুন।
libusb-এ নতুন ব্যাকএন্ড যোগ করা হচ্ছে
এবার টেকনিক্যাল বিবরণ সম্পর্কে জেনে নেওয়া যাক। libusb-এর মতো শিম API প্রদান করা (এটি আগে অন্যরা করেছেন) এবং এর সাথে অন্যান্য অ্যাপ্লিকেশন লিঙ্ক করা সম্ভব হলেও, এই পদ্ধতিতে ভুল হওয়ার সম্ভাবনা থাকে এবং এর ফলে আরও এক্সটেনশন বা রক্ষণাবেক্ষণ করা কঠিন হয়ে পড়ে। আমি সবকিছু ঠিকভাবে করতে চেয়েছিলাম, যাতে ভবিষ্যতে libusb-এ আপস্ট্রিম ও মার্জ করার ক্ষেত্রে সম্ভাব্য অবদান রাখা যায়।
ভাগ্যক্রমে, libusb README-তে বলা আছে:
“libusb ইন্টার্নালি এমনভাবে অ্যাবস্ট্র্যাক্ট করা হয়েছে যাতে আশা করা যায় যে এটি অন্যান্য অপারেটিং সিস্টেমে পোর্ট করা যাবে। আরও তথ্যের জন্য PORTING ফাইল দেখুন।”
libusb এমনভাবে স্ট্রাকচার করা হয়েছে যাতে পাবলিক API "ব্যাকএন্ড" থেকে আলাদা থাকে। এইসব ব্যাকএন্ড অপারেটিং সিস্টেমের লো-লেভেল API-এর মাধ্যমে ডিভাইসের সাথে তালিকাভুক্ত করা, খোলা, বন্ধ করা এবং আসলে যোগাযোগ করার জন্য দায়ী। এইভাবে libusb আগে থেকেই Linux, macOS, Windows, Android, OpenBSD/NetBSD, Haiku ও Solaris-এর মধ্যে পার্থক্যগুলি অ্যাবস্ট্র্যাক্ট করে এবং এই সব প্ল্যাটফর্মে কাজ করে।
আমাকে যা করতে হয়েছিল তা হল Emscripten+WebUSB "অপারেটিং সিস্টেম"-এর জন্য আরেকটি ব্যাকএন্ড যোগ করা। libusb/os ফোল্ডারে সেইসব ব্যাকএন্ডের ইমপ্লিমেন্টেশন লাইভ আছে:
~/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
প্রতিটি ব্যাকএন্ডে সাধারণ ধরন ও হেল্পার সহ libusbi.h হেডার থাকে এবং usbi_os_backend ধরনের usbi_backend ভেরিয়েবল এক্সপোজ করতে হয়। যেমন, Windows ব্যাকএন্ড দেখতে এইরকম হয়:
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),
};
প্রপার্টিগুলি দেখলে আমরা বুঝতে পারি যে স্ট্রাক্টে ব্যাকএন্ডের নাম, এর ক্ষমতার সেট, ফাংশন পয়েন্টার আকারে বিভিন্ন লো-লেভেল USB অপারেশনের হ্যান্ডলার এবং সবশেষে, ব্যক্তিগত ডিভাইস-/কন্টেক্সট-/ট্রান্সফার-লেভেল ডেটা স্টোর করার জন্য বরাদ্দ করার সাইজ অন্তর্ভুক্ত রয়েছে।
ব্যক্তিগত ডেটা ফিল্ডগুলি অন্তত সেই সমস্ত জিনিসের জন্য OS হ্যান্ডেলগুলি স্টোর করার জন্য উপযোগী, কারণ হ্যান্ডেল ছাড়া আমরা জানি না যে কোনও প্রদত্ত অপারেশন কোন আইটেমে প্রযোজ্য। ওয়েব ইমপ্লিমেন্টেশনের ক্ষেত্রে, OS হ্যান্ডেলগুলি হল অন্তর্নিহিত WebUSB JavaScript অবজেক্ট। Emscripten-এ এগুলি দেখানোর ও স্টোর করার স্বাভাবিক উপায় হল emscripten::val ক্লাস, যা Embind-এর (Emscripten-এর বাইন্ডিং সিস্টেম) অংশ হিসেবে প্রদান করা হয়।
ফোল্ডারের বেশিরভাগ ব্যাকএন্ড C-তে প্রয়োগ করা হয়, তবে কিছু C++-এ প্রয়োগ করা হয়। Embind শুধুমাত্র C++-এর সাথে কাজ করে, তাই আমার জন্য এই বিকল্পটি বেছে নেওয়া হয়েছে এবং আমি প্রয়োজনীয় স্ট্রাকচার সহ libusb/libusb/os/emscripten_webusb.cpp এবং ব্যক্তিগত ডেটা ফিল্ডের জন্য sizeof(val) যোগ করেছি:
#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 অবজেক্ট স্টোর করা হচ্ছে
libusb ব্যক্তিগত ডেটার জন্য বরাদ্দ করা এলাকায় ব্যবহার করার জন্য প্রস্তুত পয়েন্টার প্রদান করে। val ইনস্ট্যান্স হিসেবে সেইসব পয়েন্টার নিয়ে কাজ করতে, আমি ছোট হেল্পার যোগ করেছি যা সেগুলিকে ইন-প্লেস তৈরি করে, রেফারেন্স হিসেবে সেগুলি ফিরিয়ে আনে এবং ভ্যালু সরিয়ে দেয়:
// 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))) {}
};
সিঙ্ক্রোনাস C কনটেক্সটে অ্যাসিঙ্ক ওয়েব API
এখন অ্যাসিঙ্ক WebUSB API ম্যানেজ করার উপায় প্রয়োজন যেখানে libusb সিঙ্ক্রোনাস অপারেশন আশা করে। এর জন্য, আমি Asyncify ব্যবহার করতে পারি, বা আরও নির্দিষ্টভাবে, val::await()-এর মাধ্যমে এর Embind ইন্টিগ্রেশন।
আমি WebUSB সংক্রান্ত সমস্যা সঠিকভাবে ম্যানেজ করতে এবং সেগুলিকে libusb সমস্যা কোডে কনভার্ট করতে চেয়েছিলাম, কিন্তু Embind-এর কাছে বর্তমানে JavaScript ব্যতিক্রম বা C++ সাইড থেকে Promise প্রত্যাখ্যান ম্যানেজ করার কোনও উপায় নেই। JavaScript-এর দিকে রিজেকশন ক্যাচ করে এবং ফলাফলকে { error, value } অবজেক্টে কনভার্ট করে এই সমস্যার সমাধান করা যেতে পারে, যা এখন C++-এর দিক থেকে নিরাপদে পার্স করা যেতে পারে। আমি EM_JS ম্যাক্রো এবং Emval.to{Handle, Value} API-এর কম্বিনেশন ব্যবহার করে এটি করেছি:
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()};
}
};
এখন আমি WebUSB অপারেশন থেকে পাওয়া যেকোনও Promise-এ promise_result::await() ব্যবহার করতে পারব এবং আলাদাভাবে এর error ও value ফিল্ড পরীক্ষা করতে পারব।
যেমন, libusb_device_handle থেকে USBDevice-এর প্রতিনিধিত্বকারী val ফিরিয়ে আনা, এর open() পদ্ধতি কল করা, এর ফলাফলের জন্য অপেক্ষা করা এবং libusb স্ট্যাটাস কোড হিসেবে একটি ত্রুটি কোড ফিরিয়ে আনা, এইরকম দেখায়:
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;
}
ডিভাইস এনিউমারেশন
অবশ্যই, কোনও ডিভাইস খোলার আগে, libusb-কে উপলভ্য ডিভাইসের তালিকা পেতে হবে। ব্যাকএন্ডকে অবশ্যই get_device_list হ্যান্ডলারের মাধ্যমে এই অপারেশনটি বাস্তবায়ন করতে হবে।
সমস্যা হল, অন্যান্য প্ল্যাটফর্মের মতো, নিরাপত্তার কারণে ওয়েবে কানেক্ট করা সমস্ত USB ডিভাইসের তালিকা তৈরি করার কোনও উপায় নেই। পরিবর্তে, ফ্লোটি দুটি ভাগে বিভক্ত। প্রথমে, ওয়েব অ্যাপ্লিকেশন navigator.usb.requestDevice()-এর মাধ্যমে নির্দিষ্ট প্রপার্টি সহ ডিভাইসের জন্য অনুরোধ করে এবং ব্যবহারকারী ম্যানুয়ালি বেছে নেন যে কোন ডিভাইস তিনি দেখাতে চান অথবা অনুমতির প্রম্পট বাতিল করেন। এর পরে, অ্যাপ্লিকেশনটি navigator.usb.getDevices()-এর মাধ্যমে আগে থেকে অনুমোদিত এবং কানেক্ট করা ডিভাইসগুলির তালিকা দেখায়।
প্রথমে আমি get_device_list হ্যান্ডলারের ইমপ্লিমেন্টেশনে সরাসরি requestDevice() ব্যবহার করার চেষ্টা করেছিলাম। তবে, কানেক্ট করা ডিভাইসের তালিকা সহ অনুমতির প্রম্পট দেখানোকে একটি সংবেদনশীল অপারেশন হিসেবে বিবেচনা করা হয় এবং এটি অবশ্যই ব্যবহারকারীর ইন্টার্যাকশন (যেমন কোনও পৃষ্ঠায় বোতামে ক্লিক করা) দ্বারা ট্রিগার করা উচিত, অন্যথায় এটি সবসময় একটি প্রত্যাখ্যাত প্রমিস রিটার্ন করে। libusb অ্যাপ্লিকেশনগুলি প্রায়শই অ্যাপ্লিকেশন স্টার্ট-আপের সময় কানেক্ট করা ডিভাইসগুলির তালিকা করতে চাইতে পারে, তাই requestDevice() ব্যবহার করা কোনও বিকল্প ছিল না।
পরিবর্তে, আমাকে এন্ড ডেভেলপারকে navigator.usb.requestDevice()-এর ইনভোকেশন ছেড়ে দিতে হয়েছিল এবং শুধুমাত্র navigator.usb.getDevices() থেকে আগে থেকেই অনুমোদিত ডিভাইসগুলি প্রকাশ করতে হয়েছিল:
// 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;
}
বেশিরভাগ ব্যাকএন্ড কোড val এবং promise_result-কে উপরে দেখানো একই উপায়ে ব্যবহার করে। ডেটা ট্রান্সফার ম্যানেজ করার কোডে আরও কিছু আকর্ষণীয় হ্যাক আছে, তবে এই নিবন্ধের জন্য সেইসব প্রয়োগের বিবরণ কম গুরুত্বপূর্ণ। আপনি আগ্রহী হলে, Github-এ কোড ও কমেন্ট চেক করতে ভুলবেন না।
ওয়েবে ইভেন্ট লুপ পোর্ট করা
libusb পোর্টের আরও একটি অংশ যা আমি আলোচনা করতে চাই তা হল ইভেন্ট হ্যান্ডলিং। আগের নিবন্ধে যেমন বর্ণনা করা হয়েছে, C-এর মতো সিস্টেম ল্যাঙ্গুয়েজের বেশিরভাগ API সিঙ্ক্রোনাস এবং ইভেন্ট হ্যান্ডলিংও এর ব্যতিক্রম নয়। এটি সাধারণত একটি ইনফিনিট লুপের মাধ্যমে প্রয়োগ করা হয় যা এক্সটার্নাল I/O সোর্সের সেট থেকে "পোল" (ডেটা পড়ার চেষ্টা করে বা কিছু ডেটা উপলভ্য না হওয়া পর্যন্ত এক্সিকিউশন ব্লক করে) করে এবং যখন সেগুলির মধ্যে অন্তত একটি উত্তর দেয়, তখন সংশ্লিষ্ট হ্যান্ডলারের কাছে একটি ইভেন্ট হিসেবে সেটি পাস করে। হ্যান্ডলারের কাজ শেষ হয়ে গেলে, কন্ট্রোল লুপে ফিরে আসে এবং অন্য পোলের জন্য পজ করে।
ওয়েবে এই পদ্ধতি ব্যবহার করার ক্ষেত্রে কিছু সমস্যা আছে।
প্রথমত, WebUSB অন্তর্নিহিত ডিভাইসের র হ্যান্ডেল প্রকাশ করে না এবং করতে পারে না, তাই সরাসরি পোলিং করা কোনও বিকল্প নয়। দ্বিতীয়ত, libusb অন্যান্য ইভেন্টের জন্য eventfd এবং pipe API ব্যবহার করে, এর পাশাপাশি র ডিভাইস হ্যান্ডেল ছাড়াই অপারেটিং সিস্টেমে ট্রান্সফার ম্যানেজ করার জন্য ব্যবহার করে, কিন্তু eventfd বর্তমানে Emscripten-এ কাজ করে না এবং pipe কাজ করলেও, বর্তমানে স্পেসিফিকেশন মেনে চলে না এবং ইভেন্টের জন্য অপেক্ষা করতে পারে না।
সবশেষে, সবচেয়ে বড় সমস্যা হল, ওয়েবের নিজস্ব ইভেন্ট লুপ আছে। এই গ্লোবাল ইভেন্ট লুপ যেকোনও এক্সটার্নাল I/O অপারেশনের (fetch(), টাইমার বা এই ক্ষেত্রে WebUSB সহ) জন্য ব্যবহার করা হয় এবং সংশ্লিষ্ট অপারেশন শেষ হলেই এটি ইভেন্ট বা Promise হ্যান্ডলারকে কল করে। অন্য, নেস্টেড, অসীম ইভেন্ট লুপ এক্সিকিউট করলে ব্রাউজারের ইভেন্ট লুপের প্রোগ্রেস চিরতরে ব্লক হয়ে যাবে, যার অর্থ হল UI শুধু যে কাজ করা বন্ধ করে দেবে তাই নয়, কোডটি যে I/O ইভেন্টের জন্য অপেক্ষা করছে সেটির জন্য কখনও বিজ্ঞপ্তি পাবে না। এর ফলে সাধারণত ডেডলক হয় এবং আমি যখন একটি ডেমোতে libusb ব্যবহার করার চেষ্টা করেছিলাম তখন ঠিক এটাই ঘটেছিল। পৃষ্ঠাটি ফ্রিজ হয়ে গেছে।
অন্যান্য ব্লকিং I/O-এর মতো, এই ধরনের ইভেন্ট লুপ ওয়েবে পোর্ট করার জন্য, ডেভেলপারদের মূল থ্রেড ব্লক না করে সেই লুপগুলি চালানোর উপায় খুঁজে বের করতে হবে। একটি উপায় হল, আলাদা থ্রেডে I/O ইভেন্ট ম্যানেজ করার জন্য অ্যাপ্লিকেশন রিফ্যাক্টর করা এবং ফলাফল মূল থ্রেডে ফেরত পাঠানো। অন্যটি হল, লুপ পজ করতে Asyncify ব্যবহার করা এবং নন-ব্লকিং ফ্যাশনে ইভেন্টের জন্য অপেক্ষা করা।
আমি libusb বা gPhoto2-এর কোনওটিতেই উল্লেখযোগ্য পরিবর্তন করতে চাইনি এবং আমি ইতিমধ্যেই Promise ইন্টিগ্রেশনের জন্য Asyncify ব্যবহার করেছি, তাই আমি এই পথটি বেছে নিয়েছি। poll()-এর ব্লকিং ভেরিয়েন্ট সিমুলেট করতে, এই আইডিয়াটি বাস্তবে ব্যবহার করা যাবে কিনা তা প্রাথমিকভাবে পরীক্ষা করার জন্য আমি নিচে দেখানো লুপ ব্যবহার করেছি:
#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
এটি যা করে তা হল:
- ব্যাকএন্ড থেকে এখনও পর্যন্ত কোনও ইভেন্ট রিপোর্ট করা হয়েছে কিনা তা চেক করতে
poll()-এ কল করে। কিছু থাকলে, লুপ বন্ধ হয়ে যায়। অন্যথায়, Emscripten-এরpoll()-এর ইমপ্লিমেন্টেশন অবিলম্বে0-এ ফিরে যাবে। emscripten_sleep(0)-কে কল করা। এই ফাংশনটি Asyncify এবংsetTimeout()ব্যবহার করে এবং মূল ব্রাউজার ইভেন্ট লুপে কন্ট্রোল ফিরিয়ে দেওয়ার জন্য এখানে ব্যবহার করা হয়। এটি ব্রাউজারকে WebUSB সহ যেকোনও ব্যবহারকারীর ইন্টার্যাকশন এবং I/O ইভেন্ট ম্যানেজ করার অনুমতি দেয়।- নির্দিষ্ট টাইমআউটের মেয়াদ শেষ হয়ে গেছে কিনা তা চেক করুন এবং যদি না হয়, তাহলে লুপ চালিয়ে যান।
কমেন্টে যেমন উল্লেখ করা হয়েছে, এই পদ্ধতিটি সর্বোত্তম ছিল না, কারণ এটি Asyncify-এর মাধ্যমে সম্পূর্ণ কল স্ট্যাক সেভ-রিস্টোর করতে থাকে, এমনকি যখন কোনও USB ইভেন্ট হ্যান্ডেল করার জন্য থাকে না (যা বেশিরভাগ সময়ই হয়) এবং কারণ আধুনিক ব্রাউজারে setTimeout()-এর ন্যূনতম সময়কাল ৪ মিলি সেকেন্ড। তবুও, এটি প্রুফ-অফ-কনসেপ্টে DSLR থেকে ১৩-১৪ FPS লাইভস্ট্রিম তৈরি করার জন্য যথেষ্ট ভাল কাজ করেছে।
পরে, আমি ব্রাউজার ইভেন্ট সিস্টেমের সুবিধা নিয়ে এটি উন্নত করার সিদ্ধান্ত নিই। এই প্রয়োগটি আরও উন্নত করার বিভিন্ন উপায় আছে, তবে আপাতত আমি কাস্টম ইভেন্টগুলি সরাসরি গ্লোবাল অবজেক্টে প্রকাশ করার সিদ্ধান্ত নিয়েছি, কোনও নির্দিষ্ট libusb ডেটা স্ট্রাকচারের সাথে সেগুলি যুক্ত না করেই। আমি EM_ASYNC_JS ম্যাক্রো-এর উপর ভিত্তি করে নিম্নলিখিত অপেক্ষা ও বিজ্ঞপ্তি মেকানিজমের মাধ্যমে এটি করেছি:
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 যখনই কোনও ইভেন্ট রিপোর্ট করার চেষ্টা করে, যেমন ডেটা ট্রান্সফার সম্পূর্ণ হওয়া, তখন em_libusb_notify() ফাংশন ব্যবহার করা হয়:
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
}
এদিকে, em_libusb_wait() অংশটি Asyncify স্লিপ থেকে "জেগে উঠতে" ব্যবহৃত হয় যখন হয় একটি em-libusb ইভেন্ট পাওয়া যায়, বা টাইমআউটের মেয়াদ শেষ হয়ে যায়:
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;
}
স্লিপ ও ওয়েক-আপের সংখ্যা উল্লেখযোগ্যভাবে কমে যাওয়ার কারণে, এই মেকানিজম আগের emscripten_sleep()-ভিত্তিক ইমপ্লিমেন্টেশনের এফিশিয়েন্সি সংক্রান্ত সমস্যার সমাধান করেছে এবং DSLR ডেমো থ্রুপুট ১৩-১৪ FPS থেকে বাড়িয়ে ৩০+ FPS করেছে, যা মসৃণ লাইভ ফিডের জন্য যথেষ্ট।
সিস্টেম তৈরি করা এবং প্রথম পরীক্ষা
ব্যাকএন্ডের কাজ হয়ে যাওয়ার পরে, আমাকে এটি Makefile.am ও configure.ac-এ যোগ করতে হয়েছিল। এখানে একমাত্র আকর্ষণীয় বিষয় হল Emscripten-নির্দিষ্ট ফ্ল্যাগ পরিবর্তন:
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']"
;;
প্রথমত, Unix প্ল্যাটফর্মে এক্সিকিউটেবল ফাইলের সাধারণত কোনও ফাইল এক্সটেনশন থাকে না। তবে, আপনি কোন এক্সটেনশনের জন্য অনুরোধ করছেন তার উপর নির্ভর করে Emscripten বিভিন্ন আউটপুট তৈরি করে। আমি AC_SUBST(EXEEXT, …) ব্যবহার করে এক্সিকিউটেবল এক্সটেনশন পরিবর্তন করে .html করছি যাতে প্যাকেজের মধ্যে থাকা যেকোনও এক্সিকিউটেবল—টেস্ট ও উদাহরণ—Emscripten-এর ডিফল্ট শেল সহ HTML হয়ে যায় যা JavaScript ও WebAssembly লোড ও ইনস্ট্যান্টিয়েট করার বিষয়টি দেখে।
দ্বিতীয়ত, আমি Embind ও Asyncify ব্যবহার করছি বলে আমাকে সেইসব ফিচার (--bind -s ASYNCIFY) চালু করতে হবে এবং লিঙ্কার প্যারামিটারের মাধ্যমে ডায়নামিক মেমরি গ্রোথ (-s ALLOW_MEMORY_GROWTH) চালু করতে হবে। দুর্ভাগ্যবশত, কোনও লাইব্রেরির পক্ষে লিঙ্কারকে সেই ফ্ল্যাগগুলি রিপোর্ট করার কোনও উপায় নেই, তাই এই libusb পোর্ট ব্যবহার করে এমন প্রতিটি অ্যাপ্লিকেশনকে তাদের বিল্ড কনফিগারেশনেও একই লিঙ্কার ফ্ল্যাগ যোগ করতে হবে।
অবশেষে, আগে যেমন উল্লেখ করা হয়েছে, WebUSB-এর ক্ষেত্রে ইউজার জেসচারের মাধ্যমে ডিভাইস এনিউমারেশন করতে হয়। libusb-এর উদাহরণ ও টেস্টে ধরে নেওয়া হয় যে স্টার্ট-আপের সময় ডিভাইস এনিউমারেশন করা যাবে এবং কোনও পরিবর্তন না করা হলে এরর দেখাবে। পরিবর্তে, আমাকে অটোমেটিক এক্সিকিউশন (-s INVOKE_RUN=0) বন্ধ করতে হয়েছে এবং ম্যানুয়াল callMain() পদ্ধতি (-s EXPORTED_RUNTIME_METHODS=...) এক্সপোজ করতে হয়েছে।
এইসব কিছু হয়ে গেলে, আমি স্ট্যাটিক ওয়েব সার্ভারের মাধ্যমে জেনারেট করা ফাইলগুলি পরিবেশন করতে, WebUSB শুরু করতে এবং DevTools-এর সাহায্যে ম্যানুয়ালি সেইসব HTML এক্সিকিউটেবল চালাতে পারি।
![স্ক্রিনশটে দেখা যাচ্ছে যে স্থানীয়ভাবে পরিবেশিত `testlibusb` পৃষ্ঠায় DevTools সহ Chrome উইন্ডো খোলা আছে। DevTools কনসোল `navigator.usb.requestDevice({ filters: [] })` মূল্যায়ন করছে, যা একটি অনুমতির প্রম্পট ট্রিগার করেছে এবং এটি বর্তমানে ব্যবহারকারীকে একটি USB ডিভাইস বেছে নিতে বলছে যা পৃষ্ঠার সাথে শেয়ার করা উচিত। ILCE-6600 (একটি Sony ক্যামেরা) বর্তমানে বেছে নেওয়া হয়েছে।](https://web.developers.google.cn/static/articles/porting-libusb-to-webusb/image/screenshot-showing-chrom-b19a9e4e1b916.png?hl=bn)
![DevTools এখনও খোলা আছে, পরবর্তী ধাপের স্ক্রিনশট। ডিভাইস বেছে নেওয়ার পরে, Console একটি নতুন এক্সপ্রেশন `Module.callMain(['-v'])` মূল্যায়ন করেছে, যা ভার্বোস মোডে `testlibusb` অ্যাপ এক্সিকিউট করেছে। আউটপুটে আগে কানেক্ট করা USB ক্যামেরা সম্পর্কে বিভিন্ন বিস্তারিত তথ্য দেখানো হয়েছে: প্রস্তুতকারক Sony, প্রোডাক্ট ILCE-6600, সিরিয়াল নম্বর, কনফিগারেশন ইত্যাদি।](https://web.developers.google.cn/static/articles/porting-libusb-to-webusb/image/screenshot-the-next-step-bd4e4c7f85be7.png?hl=bn)
এটি খুব বেশি বলে মনে হয় না, তবে নতুন প্ল্যাটফর্মে লাইব্রেরি পোর্ট করার সময়, প্রথমবার সঠিক আউটপুট পাওয়ার পর্যায়ে পৌঁছানো খুবই উত্তেজনাপূর্ণ!
পোর্ট ব্যবহার করা
উপরে যেমন উল্লেখ করা হয়েছে, পোর্টটি কিছু Emscripten ফিচারের উপর নির্ভর করে যেগুলি বর্তমানে অ্যাপ্লিকেশনের লিঙ্কিং স্টেজে চালু করতে হবে। আপনি নিজের অ্যাপ্লিকেশনে এই libusb পোর্ট ব্যবহার করতে চাইলে, আপনাকে যা করতে হবে তা এখানে দেওয়া হল:
- আপনার বিল্ডের অংশ হিসেবে আর্কাইভ হিসেবে লেটেস্ট libusb ডাউনলোড করুন অথবা আপনার প্রোজেক্টে গিট সাবমডিউল হিসেবে এটি যোগ করুন।
libusbফোল্ডারেautoreconf -fivচালান।- ক্রস-কম্পাইলেশনের জন্য প্রোজেক্ট শুরু করতে এবং আপনি যেখানে বিল্ট আর্টিফ্যাক্ট রাখতে চান সেখানে একটি পাথ সেট করতে
emconfigure ./configure –host=wasm32 –prefix=/some/installation/pathরান করুন। emmake make installরান করুন।- আগে বেছে নেওয়া পাথে libusb সার্চ করার জন্য আপনার অ্যাপ্লিকেশন বা উচ্চ-স্তরের লাইব্রেরি পয়েন্ট করুন।
- আপনার অ্যাপ্লিকেশনের লিঙ্ক আর্গুমেন্টে নিম্নলিখিত ফ্ল্যাগ যোগ করুন:
--bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH।
লাইব্রেরিতে বর্তমানে কিছু সীমাবদ্ধতা রয়েছে:
- ট্রান্সফার বাতিল করার কোনও সুবিধা নেই। এটি WebUSB-এর একটি সীমাবদ্ধতা, যা libusb-এ ক্রস-প্ল্যাটফর্ম ট্রান্সফার বাতিল করার সুবিধা না থাকার কারণে ঘটে।
- আইসোক্রোনাস ট্রান্সফার সাপোর্ট নেই। উদাহরণ হিসেবে, আগে থেকে থাকা ট্রান্সফার মোডের প্রয়োগ অনুসরণ করে এটি যোগ করা কঠিন হওয়া উচিত নয়, তবে এটি কিছুটা বিরল মোড এবং এটি পরীক্ষা করার জন্য আমার কাছে কোনও ডিভাইস ছিল না, তাই আপাতত আমি এটিকে কাজ করে না হিসেবে রেখেছি। আপনার কাছে এই ধরনের ডিভাইস থাকলে এবং লাইব্রেরিতে কন্ট্রিবিউট করতে চাইলে, PR-এর জন্য স্বাগত!
- আগে উল্লেখ করা ক্রস-প্ল্যাটফর্ম সংক্রান্ত সীমাবদ্ধতা। এইসব সীমাবদ্ধতা অপারেটিং সিস্টেমের দ্বারা আরোপ করা হয়, তাই ব্যবহারকারীকে ড্রাইভার বা অনুমতি ওভাররাইড করতে বলা ছাড়া আমরা এখানে বেশি কিছু করতে পারি না। তবে, আপনি HID বা সিরিয়াল ডিভাইস পোর্ট করলে, libusb উদাহরণ অনুসরণ করতে এবং অন্য Fugu API-তে অন্য কোনও লাইব্রেরি পোর্ট করতে পারবেন। যেমন, আপনি WebHID-এ C লাইব্রেরি hidapi পোর্ট করতে পারেন এবং লো-লেভেল USB অ্যাক্সেসের সাথে সম্পর্কিত সমস্যাগুলি এড়াতে পারেন।
উপসংহার
এই পোস্টে আমি দেখিয়েছি যে Emscripten, Asyncify ও Fugu API-এর সাহায্যে libusb-এর মতো লো-লেভেল লাইব্রেরিও কীভাবে কয়েকটি ইন্টিগ্রেশন ট্রিকের মাধ্যমে ওয়েবে পোর্ট করা যায়।
এই ধরনের প্রয়োজনীয় এবং ব্যাপকভাবে ব্যবহৃত লো-লেভেল লাইব্রেরি পোর্ট করা বিশেষভাবে ফলপ্রসূ, কারণ, এর ফলে, এটি হাই-লেভেল লাইব্রেরি বা এমনকি সম্পূর্ণ অ্যাপ্লিকেশনও ওয়েবে নিয়ে আসার অনুমতি দেয়। এর ফলে, আগে এক বা দুটি প্ল্যাটফর্মের ব্যবহারকারীদের মধ্যে সীমিত অভিজ্ঞতা এখন সব ধরনের ডিভাইস ও অপারেটিং সিস্টেমের জন্য উপলভ্য হবে এবং শুধু একটি লিঙ্কে ক্লিক করেই সেইসব অভিজ্ঞতা পাওয়া যাবে।
পরবর্তী পোস্টে আমি ওয়েব gPhoto2 ডেমো তৈরি করার ধাপগুলি দেখাব, যা শুধু ডিভাইসের তথ্যই ফিরিয়ে আনে না, libusb-এর ট্রান্সফার ফিচারও ব্যাপকভাবে ব্যবহার করে। ইতিমধ্যে, আমি আশা করি আপনি libusb উদাহরণটি অনুপ্রেরণাদায়ক পেয়েছেন এবং ডেমোটি চেষ্টা করবেন, লাইব্রেরির সাথেই খেলবেন, বা এমনকি এগিয়ে যাবেন এবং অন্য একটি বহুল ব্যবহৃত লাইব্রেরিও Fugu API-এর একটিতে পোর্ট করবেন।