ওয়েবে USB ডিভাইসগুলি অ্যাক্সেস করুন৷

WebUSB API ইউএসবিকে ওয়েবে এনে ব্যবহার করাকে আরও নিরাপদ এবং সহজ করে তোলে।

ফ্রাঁসোয়া বিউফোর্ট
François Beaufort

যদি আমি স্পষ্টভাবে এবং সহজভাবে "USB" বলি, তাহলে একটি ভাল সুযোগ রয়েছে যে আপনি অবিলম্বে কীবোর্ড, ইঁদুর, অডিও, ভিডিও এবং স্টোরেজ ডিভাইসগুলির কথা ভাববেন৷ আপনি ঠিক বলেছেন, কিন্তু আপনি সেখানে অন্যান্য ধরণের ইউনিভার্সাল সিরিয়াল বাস (USB) ডিভাইস পাবেন।

এই অ-প্রমিত USB ডিভাইসগুলির জন্য হার্ডওয়্যার বিক্রেতাদের প্ল্যাটফর্ম-নির্দিষ্ট ড্রাইভার এবং SDK লিখতে হবে যাতে আপনি (ডেভেলপার) তাদের সুবিধা নিতে পারেন। দুঃখজনকভাবে এই প্ল্যাটফর্ম-নির্দিষ্ট কোডটি ঐতিহাসিকভাবে এই ডিভাইসগুলিকে ওয়েব দ্বারা ব্যবহার করা থেকে বাধা দিয়েছে৷ এবং এটি ওয়েবইউএসবি এপিআই তৈরির একটি কারণ: ওয়েবে USB ডিভাইস পরিষেবাগুলিকে প্রকাশ করার একটি উপায় প্রদান করা৷ এই API দিয়ে, হার্ডওয়্যার নির্মাতারা তাদের ডিভাইসের জন্য ক্রস-প্ল্যাটফর্ম জাভাস্ক্রিপ্ট SDK তৈরি করতে সক্ষম হবে।

কিন্তু সবচেয়ে গুরুত্বপূর্ণভাবে এটি ওয়েবে এনে USBকে নিরাপদ এবং সহজে ব্যবহার করবে

WebUSB API এর সাথে আপনি যে আচরণ আশা করতে পারেন তা দেখা যাক:

  1. একটি USB ডিভাইস কিনুন।
  2. এটি আপনার কম্পিউটারে প্লাগ করুন। এই ডিভাইসের জন্য সঠিক ওয়েবসাইটটি সহ একটি বিজ্ঞপ্তি এখনই প্রদর্শিত হবে৷
  3. বিজ্ঞপ্তিতে ক্লিক করুন। ওয়েবসাইট আছে এবং ব্যবহার করার জন্য প্রস্তুত!
  4. সংযোগ করতে ক্লিক করুন এবং একটি USB ডিভাইস চয়নকারী Chrome-এ প্রদর্শিত হবে যেখানে আপনি আপনার ডিভাইসটি বেছে নিতে পারেন।

টাডা !

WebUSB API ছাড়া এই পদ্ধতিটি কেমন হবে?

  1. একটি প্ল্যাটফর্ম-নির্দিষ্ট অ্যাপ্লিকেশন ইনস্টল করুন।
  2. যদি এটি আমার অপারেটিং সিস্টেমেও সমর্থিত হয়, তাহলে যাচাই করুন যে আমি সঠিক জিনিসটি ডাউনলোড করেছি।
  3. জিনিসটি ইনস্টল করুন। আপনি যদি ভাগ্যবান হন, আপনি ইন্টারনেট থেকে ড্রাইভার/অ্যাপ্লিকেশন ইনস্টল করার বিষয়ে আপনাকে সতর্ক করার জন্য কোনো ভীতিকর OS প্রম্পট বা পপআপ পাবেন না। আপনি দুর্ভাগ্যজনক হলে, ইনস্টল করা ড্রাইভার বা অ্যাপ্লিকেশনগুলি ত্রুটিপূর্ণ হয়ে আপনার কম্পিউটারের ক্ষতি করে। (মনে রাখবেন, ওয়েবটি ত্রুটিপূর্ণ ওয়েবসাইট ধারণ করার জন্য তৈরি করা হয়েছে)।
  4. আপনি যদি শুধুমাত্র একবার বৈশিষ্ট্যটি ব্যবহার করেন, কোডটি আপনার কম্পিউটারে থেকে যায় যতক্ষণ না আপনি এটি অপসারণ করবেন। (ওয়েবে, অব্যবহৃত স্থানটি অবশেষে পুনরায় দাবি করা হয়।)

আমি শুরু করার আগে

এই নিবন্ধটি অনুমান করে যে ইউএসবি কীভাবে কাজ করে সে সম্পর্কে আপনার কিছু প্রাথমিক জ্ঞান রয়েছে। যদি না হয়, আমি একটি NutShell এ USB পড়ার পরামর্শ দিই। USB সম্পর্কে ব্যাকগ্রাউন্ড তথ্যের জন্য, অফিসিয়াল USB স্পেসিফিকেশন দেখুন।

WebUSB API Chrome 61 এ উপলব্ধ।

মূল পরীক্ষার জন্য উপলব্ধ

ক্ষেত্রের মধ্যে WebUSB API ব্যবহার করে ডেভেলপারদের কাছ থেকে যতটা সম্ভব প্রতিক্রিয়া পাওয়ার জন্য, আমরা পূর্বে এই বৈশিষ্ট্যটি Chrome 54 এবং Chrome 57-এ একটি অরিজিন ট্রায়াল হিসেবে যুক্ত করেছি।

সর্বশেষ ট্রায়াল সফলভাবে সেপ্টেম্বর 2017 এ শেষ হয়েছে।

গোপনীয়তা এবং নিরাপত্তা

শুধুমাত্র HTTPS

এই বৈশিষ্ট্যটির ক্ষমতার কারণে, এটি শুধুমাত্র নিরাপদ প্রসঙ্গে কাজ করে৷ এর মানে আপনাকে TLS মাথায় রেখে তৈরি করতে হবে।

ব্যবহারকারীর অঙ্গভঙ্গি প্রয়োজন

নিরাপত্তা সতর্কতা হিসাবে, navigator.usb.requestDevice() শুধুমাত্র একটি স্পর্শ বা মাউস ক্লিকের মতো ব্যবহারকারীর অঙ্গভঙ্গির মাধ্যমে কল করা যেতে পারে।

অনুমতি নীতি

একটি পারমিশন পলিসি হল এমন একটি মেকানিজম যা ডেভেলপারদের বেছে বেছে বিভিন্ন ব্রাউজার ফিচার এবং API গুলিকে সক্ষম এবং অক্ষম করতে দেয়৷ এটি একটি HTTP শিরোনাম এবং/অথবা একটি iframe "অনুমতি" বৈশিষ্ট্যের মাধ্যমে সংজ্ঞায়িত করা যেতে পারে।

আপনি একটি অনুমতি নীতি নির্ধারণ করতে পারেন যা নিয়ন্ত্রণ করে যে usb অ্যাট্রিবিউটটি নেভিগেটর অবজেক্টে প্রকাশ করা হয় কিনা বা অন্য কথায় আপনি যদি ওয়েবইউএসবিকে অনুমতি দেন।

নীচে একটি শিরোনাম নীতির একটি উদাহরণ যেখানে WebUSB অনুমোদিত নয়:

Feature-Policy: fullscreen "*"; usb "none"; payment "self" https://payment.example.com

নীচে একটি কন্টেইনার নীতির আরেকটি উদাহরণ যেখানে USB অনুমোদিত:

<iframe allowpaymentrequest allow="usb; fullscreen"></iframe>

এর কোডিং শুরু করা যাক

WebUSB API জাভাস্ক্রিপ্ট প্রতিশ্রুতির উপর অনেক বেশি নির্ভর করে। আপনি যদি তাদের সাথে পরিচিত না হন তবে এই দুর্দান্ত প্রতিশ্রুতি টিউটোরিয়ালটি দেখুন। আরেকটি জিনিস, () => {} হল কেবল ECMAScript 2015 Arrow ফাংশন

USB ডিভাইসগুলিতে অ্যাক্সেস পান

আপনি হয় ব্যবহারকারীকে navigator.usb.requestDevice() ব্যবহার করে একটি একক সংযুক্ত USB ডিভাইস নির্বাচন করার জন্য অনুরোধ করতে পারেন অথবা ওয়েবসাইটটিকে অ্যাক্সেস দেওয়া সমস্ত সংযুক্ত USB ডিভাইসগুলির একটি তালিকা পেতে navigator.usb.getDevices() এ কল করুন৷

navigator.usb.requestDevice() ফাংশন একটি বাধ্যতামূলক জাভাস্ক্রিপ্ট অবজেক্ট নেয় যা filters সংজ্ঞায়িত করে। এই ফিল্টারগুলি প্রদত্ত বিক্রেতা ( vendorId ) এবং ঐচ্ছিকভাবে, পণ্য ( productId ) শনাক্তকারীর সাথে যেকোন USB ডিভাইসের সাথে মেলাতে ব্যবহৃত হয়৷ classCode , protocolCode , serialNumber , এবং subclassCode কীগুলিও সেখানে সংজ্ঞায়িত করা যেতে পারে।

Chrome-এ USB ডিভাইস ব্যবহারকারী প্রম্পটের স্ক্রিনশট
ইউএসবি ডিভাইস ব্যবহারকারী প্রম্পট।

উদাহরণস্বরূপ, উত্সের অনুমতি দেওয়ার জন্য কনফিগার করা একটি সংযুক্ত Arduino ডিভাইসে কীভাবে অ্যাক্সেস পেতে হয় তা এখানে।

navigator.usb.requestDevice({ filters: [{ vendorId: 0x2341 }] })
.then(device => {
  console.log(device.productName);      // "Arduino Micro"
  console.log(device.manufacturerName); // "Arduino LLC"
})
.catch(error => { console.error(error); });

আপনি জিজ্ঞাসা করার আগে, আমি জাদুভাবে এই 0x2341 হেক্সাডেসিমেল সংখ্যাটি নিয়ে আসিনি। আমি কেবল এই ইউএসবি আইডির তালিকায় "Arduino" শব্দটি অনুসন্ধান করেছি।

উপরের পূর্ণ প্রতিশ্রুতিতে ফিরে আসা USB device ডিভাইস সম্পর্কে কিছু মৌলিক, তবুও গুরুত্বপূর্ণ তথ্য রয়েছে যেমন সমর্থিত USB সংস্করণ, সর্বাধিক প্যাকেটের আকার, বিক্রেতা এবং পণ্যের আইডি, ডিভাইসটির সম্ভাব্য কনফিগারেশনের সংখ্যা। মূলত এটি USB বর্ণনাকারী ডিভাইসের সমস্ত ক্ষেত্র রয়েছে।

// Get all connected USB devices the website has been granted access to.
navigator.usb.getDevices().then(devices => {
  devices.forEach(device => {
    console.log(device.productName);      // "Arduino Micro"
    console.log(device.manufacturerName); // "Arduino LLC"
  });
})

যাইহোক, যদি একটি USB ডিভাইস WebUSB-এর জন্য তার সমর্থন ঘোষণা করে, সেইসাথে একটি ল্যান্ডিং পৃষ্ঠা URL সংজ্ঞায়িত করে, যখন USB ডিভাইসটি প্লাগ ইন করা থাকে তখন Chrome একটি স্থায়ী বিজ্ঞপ্তি দেখাবে৷ এই বিজ্ঞপ্তিটি ক্লিক করলে ল্যান্ডিং পৃষ্ঠাটি খুলবে৷

Chrome-এ WebUSB বিজ্ঞপ্তির স্ক্রিনশট
WebUSB বিজ্ঞপ্তি।

একটি Arduino USB বোর্ডের সাথে কথা বলুন

ঠিক আছে, এখন দেখা যাক USB পোর্টের মাধ্যমে একটি WebUSB সামঞ্জস্যপূর্ণ Arduino বোর্ড থেকে যোগাযোগ করা কতটা সহজ। আপনার স্কেচগুলিকে WebUSB-সক্ষম করতে https://github.com/webusb/arduino- এ নির্দেশাবলী দেখুন।

চিন্তা করবেন না, আমি এই নিবন্ধে পরে নীচে উল্লিখিত সমস্ত WebUSB ডিভাইস পদ্ধতিগুলি কভার করব৷

let device;

navigator.usb.requestDevice({ filters: [{ vendorId: 0x2341 }] })
.then(selectedDevice => {
    device = selectedDevice;
    return device.open(); // Begin a session.
  })
.then(() => device.selectConfiguration(1)) // Select configuration #1 for the device.
.then(() => device.claimInterface(2)) // Request exclusive control over interface #2.
.then(() => device.controlTransferOut({
    requestType: 'class',
    recipient: 'interface',
    request: 0x22,
    value: 0x01,
    index: 0x02})) // Ready to receive data
.then(() => device.transferIn(5, 64)) // Waiting for 64 bytes of data from endpoint #5.
.then(result => {
  const decoder = new TextDecoder();
  console.log('Received: ' + decoder.decode(result.data));
})
.catch(error => { console.error(error); });

মনে রাখবেন যে আমি যে WebUSB লাইব্রেরিটি ব্যবহার করছি তা শুধুমাত্র একটি উদাহরণ প্রোটোকল বাস্তবায়ন করছে (স্ট্যান্ডার্ড USB সিরিয়াল প্রোটোকলের উপর ভিত্তি করে) এবং নির্মাতারা তাদের ইচ্ছামত যেকোন সেট এবং ধরনের শেষ পয়েন্ট তৈরি করতে পারে। কন্ট্রোল ট্রান্সফার বিশেষ করে ছোট কনফিগারেশন কমান্ডের জন্য চমৎকার কারণ তারা বাসের অগ্রাধিকার পায় এবং একটি সুনির্দিষ্ট কাঠামো থাকে।

এবং এখানে যে স্কেচটি Arduino বোর্ডে আপলোড করা হয়েছে।

// Third-party WebUSB Arduino library
#include <WebUSB.h>

WebUSB WebUSBSerial(1 /* https:// */, "webusb.github.io/arduino/demos");

#define Serial WebUSBSerial

void setup() {
  Serial.begin(9600);
  while (!Serial) {
    ; // Wait for serial port to connect.
  }
  Serial.write("WebUSB FTW!");
  Serial.flush();
}

void loop() {
  // Nothing here for now.
}

উপরের নমুনা কোডে ব্যবহৃত তৃতীয় পক্ষের WebUSB Arduino লাইব্রেরি মূলত দুটি জিনিস করে:

  • ডিভাইসটি একটি WebUSB ডিভাইস হিসেবে কাজ করে যা Chrome-কে ল্যান্ডিং পৃষ্ঠার URL পড়তে সক্ষম করে।
  • এটি একটি WebUSB সিরিয়াল API প্রকাশ করে যা আপনি ডিফল্টটিকে ওভাররাইড করতে ব্যবহার করতে পারেন৷

জাভাস্ক্রিপ্ট কোড আবার দেখুন। একবার আমি ব্যবহারকারীর দ্বারা বাছাই করা device পেয়ে গেলে, device.open() USB ডিভাইসের সাথে একটি সেশন শুরু করার জন্য সমস্ত প্ল্যাটফর্ম-নির্দিষ্ট পদক্ষেপ চালায়। তারপর, আমাকে device.selectConfiguration() এর সাথে একটি উপলব্ধ USB কনফিগারেশন নির্বাচন করতে হবে। মনে রাখবেন যে একটি কনফিগারেশন নির্দিষ্ট করে কিভাবে ডিভাইসটি চালিত হয়, এর সর্বোচ্চ শক্তি খরচ এবং এর ইন্টারফেসের সংখ্যা। ইন্টারফেসের কথা বললে, আমাকে device.claimInterface() এর সাথে একচেটিয়া অ্যাক্সেসের অনুরোধ করতে হবে কারণ ইন্টারফেস দাবি করা হলে ডেটা শুধুমাত্র একটি ইন্টারফেসে বা সংশ্লিষ্ট শেষ পয়েন্টে স্থানান্তর করা যেতে পারে। অবশেষে WebUSB সিরিয়াল API-এর মাধ্যমে যোগাযোগের জন্য উপযুক্ত কমান্ড সহ Arduino ডিভাইস সেট আপ করতে device.controlTransferOut() কল করা প্রয়োজন।

সেখান থেকে, device.transferIn() ডিভাইসে বাল্ক ট্রান্সফার করে যাতে জানানো হয় যে হোস্ট বাল্ক ডেটা পাওয়ার জন্য প্রস্তুত। তারপর, প্রতিশ্রুতিটি একটি ডেটাভিউ data ধারণকারী result বস্তুর সাথে পূর্ণ হয় যা যথাযথভাবে পার্স করতে হবে।

আপনি যদি USB-এর সাথে পরিচিত হন তবে এই সবগুলিকে বেশ পরিচিত দেখা উচিত৷

আমি আরো চাই

WebUSB API আপনাকে সমস্ত USB স্থানান্তর/এন্ডপয়েন্ট প্রকারের সাথে ইন্টারঅ্যাক্ট করতে দেয়:

  • কন্ট্রোল ট্রান্সফার, একটি USB ডিভাইসে কনফিগারেশন বা কমান্ড প্যারামিটার পাঠাতে বা গ্রহণ করতে ব্যবহৃত হয়, controlTransferIn(setup, length) এবং controlTransferOut(setup, data) দিয়ে পরিচালনা করা হয়।
  • INTERRUPT স্থানান্তর, অল্প সময়ের সংবেদনশীল ডেটার জন্য ব্যবহৃত হয়, transferIn(endpointNumber, length) এবং transferOut(endpointNumber, data) এর সাথে BULK স্থানান্তরের মতো একই পদ্ধতিতে পরিচালনা করা হয়।
  • আইসোক্রোনাস ট্রান্সফার, ভিডিও এবং সাউন্ডের মতো ডেটার স্ট্রিমের জন্য ব্যবহৃত, isochronousTransferIn(endpointNumber, packetLengths) এবং isochronousTransferOut(endpointNumber, data, packetLengths) দিয়ে পরিচালনা করা হয়।
  • বাল্ক স্থানান্তর, একটি নির্ভরযোগ্য উপায়ে প্রচুর পরিমাণে অ-সময়-সংবেদনশীল ডেটা স্থানান্তর করতে ব্যবহৃত হয়, transferIn(endpointNumber, length) এবং transferOut(endpointNumber, data) দিয়ে পরিচালনা করা হয়।

আপনি মাইক সাও-এর ওয়েবলাইট প্রকল্পটিও দেখতে চাইতে পারেন যা WebUSB API (এখানে Arduino ব্যবহার না করে) জন্য ডিজাইন করা একটি USB-নিয়ন্ত্রিত LED ডিভাইস তৈরির একটি গ্রাউন্ড-আপ উদাহরণ প্রদান করে। আপনি হার্ডওয়্যার, সফ্টওয়্যার এবং ফার্মওয়্যার পাবেন।

একটি USB ডিভাইসে অ্যাক্সেস প্রত্যাহার করুন

ওয়েবসাইটটি USBDevice ইন্সট্যান্সে forget() কল করে একটি USB ডিভাইস অ্যাক্সেস করার অনুমতিগুলি পরিষ্কার করতে পারে যার আর প্রয়োজন নেই৷ উদাহরণস্বরূপ, অনেক ডিভাইসের সাথে একটি শেয়ার্ড কম্পিউটারে ব্যবহৃত একটি শিক্ষামূলক ওয়েব অ্যাপ্লিকেশনের জন্য, প্রচুর পরিমাণে জমা হওয়া ব্যবহারকারী-উত্পাদিত অনুমতিগুলি একটি দুর্বল ব্যবহারকারীর অভিজ্ঞতা তৈরি করে।

// Voluntarily revoke access to this USB device.
await device.forget();

যেহেতু forget() Chrome 101 বা পরবর্তীতে উপলব্ধ, এই বৈশিষ্ট্যটি নিম্নলিখিতগুলির সাথে সমর্থিত কিনা তা পরীক্ষা করুন:

if ("usb" in navigator && "forget" in USBDevice.prototype) {
  // forget() is supported.
}

স্থানান্তর আকারের সীমাবদ্ধতা

কিছু অপারেটিং সিস্টেম মুলতুবি থাকা USB লেনদেনের অংশ হতে কতটা ডেটা হতে পারে তার উপর সীমা আরোপ করে। আপনার ডেটাকে ছোট লেনদেনে বিভক্ত করা এবং একবারে কয়েকটি জমা দেওয়া সেই সীমাবদ্ধতাগুলি এড়াতে সহায়তা করে৷ এটি ব্যবহৃত মেমরির পরিমাণও হ্রাস করে এবং স্থানান্তর সম্পূর্ণ হওয়ার সাথে সাথে আপনার অ্যাপ্লিকেশনটিকে অগ্রগতির প্রতিবেদন করার অনুমতি দেয়।

যেহেতু একটি এন্ডপয়েন্টে জমা দেওয়া একাধিক স্থানান্তর সর্বদা ক্রমানুসারে কার্যকর হয়, তাই USB স্থানান্তরের মধ্যে বিলম্ব এড়াতে একাধিক সারিবদ্ধ অংশ জমা দিয়ে থ্রুপুট উন্নত করা সম্ভব। প্রতিবার একটি খণ্ড সম্পূর্ণরূপে প্রেরণ করা হলে এটি আপনার কোডকে অবহিত করবে যে এটি নীচের সহায়ক ফাংশন উদাহরণে নথিভুক্ত হিসাবে আরও ডেটা সরবরাহ করবে।

const BULK_TRANSFER_SIZE = 16 * 1024; // 16KB
const MAX_NUMBER_TRANSFERS = 3;

async function sendRawPayload(device, endpointNumber, data) {
  let i = 0;
  let pendingTransfers = [];
  let remainingBytes = data.byteLength;
  while (remainingBytes > 0) {
    const chunk = data.subarray(
      i * BULK_TRANSFER_SIZE,
      (i + 1) * BULK_TRANSFER_SIZE
    );
    // If we've reached max number of transfers, let's wait.
    if (pendingTransfers.length == MAX_NUMBER_TRANSFERS) {
      await pendingTransfers.shift();
    }
    // Submit transfers that will be executed in order.
    pendingTransfers.push(device.transferOut(endpointNumber, chunk));
    remainingBytes -= chunk.byteLength;
    i++;
  }
  // And wait for last remaining transfers to complete.
  await Promise.all(pendingTransfers);
}

পরামর্শ

ক্রোমে ইউএসবি ডিবাগ করা সহজ হয় অভ্যন্তরীণ পৃষ্ঠার সাথে about://device-log যেখানে আপনি সমস্ত USB ডিভাইস সম্পর্কিত ইভেন্টগুলি এক জায়গায় দেখতে পাবেন৷

Chrome-এ WebUSB ডিবাগ করতে ডিভাইস লগ পৃষ্ঠার স্ক্রিনশট
WebUSB API ডিবাগ করার জন্য Chrome-এ ডিভাইস লগ পৃষ্ঠা।

about://usb-internals অভ্যন্তরীণ পৃষ্ঠাটিও কাজে আসে এবং আপনাকে ভার্চুয়াল WebUSB ডিভাইসগুলির সংযোগ এবং সংযোগ বিচ্ছিন্ন করার অনুকরণ করতে দেয়৷ এটি বাস্তব হার্ডওয়্যার ছাড়াই UI পরীক্ষা করার জন্য দরকারী।

Chrome-এ WebUSB ডিবাগ করতে অভ্যন্তরীণ পৃষ্ঠার স্ক্রিনশট
WebUSB API ডিবাগ করার জন্য Chrome-এ অভ্যন্তরীণ পৃষ্ঠা।

বেশিরভাগ লিনাক্স সিস্টেমে, ইউএসবি ডিভাইসগুলিকে ডিফল্টরূপে শুধুমাত্র পঠনযোগ্য অনুমতি দিয়ে ম্যাপ করা হয়। Chrome কে একটি USB ডিভাইস খোলার অনুমতি দিতে, আপনাকে একটি নতুন udev নিয়ম যোগ করতে হবে। নিম্নলিখিত বিষয়বস্তু সহ /etc/udev/rules.d/50-yourdevicename.rules এ একটি ফাইল তৈরি করুন:

SUBSYSTEM=="usb", ATTR{idVendor}=="[yourdevicevendor]", MODE="0664", GROUP="plugdev"

যেখানে [yourdevicevendor] হল 2341 যদি আপনার ডিভাইসটি একটি Arduino হয়। আরও নির্দিষ্ট নিয়মের জন্য ATTR{idProduct} যোগ করা যেতে পারে। নিশ্চিত করুন যে আপনার user plugdev গ্রুপের সদস্য । তারপর, শুধু আপনার ডিভাইস পুনরায় সংযোগ করুন.

সম্পদ

হ্যাশট্যাগ #WebUSB ব্যবহার করে @ChromiumDev- এ একটি টুইট পাঠান এবং আপনি এটি কোথায় এবং কীভাবে ব্যবহার করছেন তা আমাদের জানান।

স্বীকৃতি

এই নিবন্ধটি পর্যালোচনা করার জন্য জো মেডলিকে ধন্যবাদ।