はじめに
Hamamatsu Micro Maker Faire 2023でカッティングプロッターをWebUSBで動かすというのを展示したのですが、WebUSBについてまとめていなかったままだったのでまとめておくことにしました。
下記展示ではScratch拡張機能としてJavascriptで実装したのですが、WebUSBだけを簡単に試せるように今回はp5.jsにて説明しています。
明日のHamamatsu Micro Maker Faire 2023 のYara:Makersブースで「Scratchでペンプロッター」を出展します。MFT2023に出したのと見た目はほとんど同じですが、WebUSBを使ってブラウザから直接プロッターに出力できるようになりました!
— テツオ (@ktetsuo) December 1, 2023
※本人は別のブースにいます。 #HamamatsuMMF pic.twitter.com/kE1iXNxK6o
WebUSBとは
WebUSBとは、WebブラウザからJavaScriptを使用して手元のUSB機器を制御する技術です。
インターネットのサイトにアクセスするとJavaScriptで書かれた制御コードが読み込まれ、手元の環境のUSB機器を直接制御することができます。
検索のサジェストが「WebUSB 廃止」「WebUSB 危険性」とか怪しいですが…
見ず知らずのWebサイトからUSB機器が勝手に操作されないように、いろいろな制限があります。
- デバイス使用前に必ずポップアップが出て使用許可を求められます。
- HTTPS対応のサイトでしか使用できません。
また、ブラウザ側の実装が安定しておらず、Google Chromeでしか動作しません。この記事の内容もChromeでしか動作確認をしていません。
p5.jsとは
Processing(Javaベースのビジュアルプログラミング言語)を、ブラウザ上で実行できるようにJavasriptで書けるようにしたものです。p5.js Web EditorにアクセスするだけでProcessingによるプログラミングができるようになります。
JavaScriptが動くので、WebUSBが使えます。また、p5.js Web EditorはHTTPSなのでサーバーを用意する必要もありません。
WebUSBの使い方
本題です。今回は、USBデバイスとしてローランドDG製のカッティングプロッター「iDecora iD-01(※生産終了)」を使用しています。
項目 | 環境 |
---|---|
OS | Windows 11 Home 23H2 |
ブラウザ | Google Chtome |
USBデバイス | iDecora iD-01 |
Linux(Raspberry Pi)でもWebUSBの出力に成功しましたが、また別の機会で。
ソースコード
今回作成したソースコードは下記です。
p5.js Web Editorのリンク
/* jshint esversion: 8 */ //asyncを使うため
let startButton;
let writeButton;
let usbDevice;
function setup() {
createCanvas(400, 100);
// ボタンを作成
startButton = createButton("Start USB Connection");
startButton.mousePressed(connectUSB);
startButton = createButton("End USB Connection");
startButton.mousePressed(disconnectUSB);
writeButton = createButton("Write Data");
writeButton.mousePressed(writeData);
}
function draw() {
background(220);
}
// WriteDataボタンが押されたときに呼ばれる
async function writeData() {
const textData = "PU0,0;PU0,1000;PU1000,1000;PU1000,0;PU0,0;"; // 送信するコマンド
try {
// データ転送
const dataArray = new TextEncoder().encode(textData); // テキストをバイト列にエンコード
const endpoint = 1; // Alternate内のEndpoint番号を指定
console.log("transferOut Start");
await usbDevice.transferOut(endpoint, dataArray); // 送信
console.log("transferOut End");
} catch(error) {
console.error(error);
}
}
// USB接続
async function connectUSB() {
try {
// USBデバイスを列挙するためのフィルタ
const filters = []; // フィルタなし = 全てのUSBデバイスを列挙
// フィルタをしたい場合はこちら
// const filters = [
// { vendorId: 0x0b75 } // 0b75: Roland DG Corporation
// ];
// USBデバイスの接続要求
usbDevice = await navigator.usb.requestDevice({ filters: filters });
// USBデバイスの詳細を表示
printUSBDevice(usbDevice);
// USBデバイスをオープン
console.log("open");
await usbDevice.open();
// USBデバイスの設定
const configuration = 1; // Configuration番号を指定
const interface = 0; // Configuration内のInterface番号を指定
const alternate = 0; // Interface内のAlternate番号を指定
console.log("selectConfiguration");
await usbDevice.selectConfiguration(configuration); // Configurationを選択
console.log("claimInterface");
await usbDevice.claimInterface(interface); // Interfaceを請求
console.log("selectAlternateInterface");
await usbDevice.selectAlternateInterface(interface, alternate); // InterfaceのAlternateを選択
} catch (error) {
console.error(error);
}
}
async function disconnectUSB() {
// USBデバイスをクローズ
await usbDevice.close();
}
// USBデバイスの詳細を表示する
function printUSBDevice(usbDevice) {
console.log(usbDevice);
console.log("Vendor ID : 0x" + usbDevice.vendorId.toString(16));
console.log("Product ID : 0x" + usbDevice.productId.toString(16));
console.log("Manufacturer : " + usbDevice.manufacturerName); // 製造者名
console.log("Product Name : " + usbDevice.productName); // 製品名
console.log("Serial Number : " + usbDevice.serialNumber); // シリアル番号
console.log("Device Class : 0x" + usbDevice.deviceClass.toString(16));
console.log("Device Sub Class : 0x" + usbDevice.deviceSubclass.toString(16));
console.log("Device Protocol : 0x" + usbDevice.deviceProtocol.toString(16));
// コンフィギュレーション情報を表示
usbDevice.configurations.forEach(function (configuration) {
console.log(
"Configuration " + configuration.configurationValue + ":"
);
// Interface
configuration.interfaces.forEach(function (interface) {
console.log(" Interface " + interface.interfaceNumber + ":");
// Alternate
interface.alternates.forEach(function (alternate) {
console.log(" Alternate " + alternate.alternateSetting + ":");
console.log(" Class : 0x" + alternate.interfaceClass.toString(16));
console.log(" Sub Class : 0x" + alternate.interfaceSubclass.toString(16));
console.log(" Protocol : 0x" + alternate.interfaceProtocol.toString(16));
// Endpoint
alternate.endpoints.forEach(function (endpoint) {
console.log(" Endpoint " + endpoint.endpointNumber + ":");
console.log(" Direction : " + endpoint.direction);
console.log(" Type : " + endpoint.type);
console.log(" Packet Size: " + endpoint.packetSize);
});
});
});
});
}
実行してみる
こちらを実行すると、以下の画面が表示されます。
「Start USB Connection」をクリックすると、ブラウザにポップアップが表示され、接続されているデバイスが表示されます。
ここで接続したデバイス「iD-01」を選択し、「接続」をクリックすると…
接続できると思いきや以下のエラーが出ます。
Error: Failed to execute 'open' on 'USBDevice': Access denied.
汎用USBドライバに差し替え
実はWindows上では WebUSBは「汎用USBドライバ(WinUSB)」での制御しか対応していません。 この状況としてはiD-01のメーカーが提供した専用のUSBドライバが使用されているため、このようなエラーが出ています。
解決方法として、Windowsドライバ差し替えツールの「Zadig」というソフトを使用します。
https://zadig.akeo.ie/
サイトの見た目がとても怪しいですが、ソースコードも公開されている(https://github.com/pbatard/libwdi)ようなので信じることにします。
起動すると、コンボボックスでUSBデバイスが選べます。何も表示されていない場合は、[Options]-[List All Devices]をチェック。
「iD-01」を選択して「WinUSB」を選択し、「Replace Driver」を実行します。
インストールに結構時間がかかりますが待ちます。
再実行
WinUSBドライバに入れ替えたところで、改めて実行し、「Start USB Connection」をクリックし、デバイス「iD-01」を選択するとコンソールに下記が出力されます。
Vendor ID : 0xb75
Product ID : 0x175
Manufacturer : Roland DG
Product Name : iD-01
Serial Number : A
Device Class : 0x0
Device Sub Class : 0x0
Device Protocol : 0x0
Configuration 1:
Interface 0:
Alternate 0:
Class : 0x7
Sub Class : 0x1
Protocol : 0x2
Endpoint 1:
Direction : out
Type : bulk
Packet Size: 64
Endpoint 2:
Direction : in
Type : bulk
Packet Size: 64
ここで表示されているのは選択したUSBデバイスの内部情報です。Vendor ID(製造者ID)、Product ID(製品ID)などを表示しています。
Confuguration以下はUSBデバイスのエンドポイント(簡単に言うとデータ通信ライン)の論理構成です。Configuration - Interface - Alternate - Endpoint のツリー構造になっています。
ここを見ると、「Configuration 1 - Interface 0 - Alternate 0 - Endpoint 1」が「Direction: out」「Type:bulk」となっています。「Direction: out」はPCからデバイスへの出力の方向を表しており、「Type: bulk」は「バルク転送」といって、大量のデータを送る用途を表しています。したがって、ここがカッティングプロッターにデータを送るエンドポイントであることが予測できます。
ConnectUSB()
関数の以下の部分で、Configuration 1 - Interface 0 - Alternate 0を使用するために選択しています。
// USBデバイスの設定
const configuration = 1; // Configuration番号を指定
const interface = 0; // Configuration内のInterface番号を指定
const alternate = 0; // Interface内のAlternate番号を指定
console.log("selectConfiguration");
await usbDevice.selectConfiguration(configuration); // Configurationを選択
console.log("claimInterface");
await usbDevice.claimInterface(interface); // Interfaceを請求
console.log("selectAlternateInterface");
await usbDevice.selectAlternateInterface(interface, alternate); // InterfaceのAlternateを選択
Endpoint 1は、writeData()
関数内で指定しています。
カッティングデータの出力
「Start USB Connection」を押して接続した状態で、「Write Data」ボタンをクリックすると、カッティングデータがデバイスに送られます。うまくいくと、カッティング動作が始まるはずです。
ソースコードのwriteData()
関数でカッティングのコマンドを出力しています。
余談:USBデータの解析方法
どんなデータが送られているかは、WiresharkでUSBラインに流れているデータを見ることができます。WiresharkはEthernet等のネットワークパケットキャプチャとして有名ですが、インストールの途中で「usbpcap」を追加することでUSBのパケットキャプチャもできるようになります。
純正のデータ出力ソフトなどで実際にデータを出力してみて、Wiresharkでデータを眺めてみてください。どのエンドポイントにカッティングデータが流れているかもわかります。
最後に
誰が得するかよくわからない記事でしたが、Qiita Engineer Festa 2024にて「この記事誰得? 私しか得しないニッチな技術で記事投稿!」というイベントが開催されたこともあり投稿してみました。
WebUSBはブラウザからUSBデバイスを直接制御することができる面白い技術だと思うのですが、普及するかどうかが怪しいです…。