MBrush (PrinCube) の研究② - node.js に移植しようとして先を越された
mb-node
とりあえず、MBrush のアプリを介さず CUI から画像を印刷できるようにした。
のだが、依存するレポジトリにも同様の機能が入ってしまったので、現状では存在意義が微妙……(泣
使い方
git clone --depth=1 --recursive https://github.com/hunamizawa/mb-node cd mb-node ./build.sh npm ci --production
MBrush を接続してから
node app.js input.png
これでWi-Fi 経由でプリンターに画像が転送される。USB 接続の場合は -c usb を足してくだち。
複数枚の画像を印刷したい場合は
node app.js input1.png input2.png
とすれば、MBrush の元々のアプリで印刷領域を複数設定したのと同じ挙動になる。
前回の続き
WebAssembly (WASM) の存在は、多少は知っていた。C++ とか何かの言語で書いたソースを、Java でいうバイトコード、.NET でいう IL のような中間コードに変換して、Web ブラウザ上で実行できるようにする。JS よりも効率よく動作し、ランタイムをインストールする手間もない、お手軽クロスプラットフォーム、という話は聞いたことがあったが、実際に WebAssembly で実装されたアプリを見るのは初めてだった。
Chrome の開発者ツールで .wasm ファイルを覗くと、バイナリから勝手に human-readable なテキスト表現(S式)には変換してくれるが、アセンブリに近い言語なので読む気にはならない。他言語への移植は難しそう。
node.js に移植できそうな気がした
mbc.wasm と同じディレクトリにある mbc.js は、何かのツールで生成したコードのようである。調べたところ、Emscripten で C/C++ から WASM をコンパイルした際に生成される、JavaScript から WASM を呼ぶためのラッパーだと判明した。このラッパーを観察すると NODEFS 等、node.js に関係しそうな変数名が使われている。
で、前回話した、画像を .mbd 形式に変換する最中に、console に何かデバッグメッセージが流れてくる。その中にこんなのがあった。
mbc: (7) ["128", "168", "60", "-i0", "-o0", "-w0", "-s2"]
-i0 とか -o0 とか、なんかコマンドライン引数みたいだなあ、と思いつつ、この配列をどこで使ってるのか調べたところ、WASM の中にある callMain() という関数に渡されている様子。いかにもコンソールアプリ臭がする名前だ。
また、実験の過程で次のような ImageMagick っぽいエラーメッセージを吐いており、この WASM には ImageMagick が静的リンクされていると推測している。
UnableToOpenBlob 'ori.png': No such file or directory @ error/blob.c/OpenBlob/3497.
InvalidArgument '-colorize' '100-geometry' at CLI arg 12 @ error/operation.c/CLISimpleOperatorImage/2020.
おそらく開発者は、PNG 画像を .mbd 形式のファイルに変換するために、まず
$ ./foobar 128 168 60 -i0 -o0 -w0 -s2
のように使うコンソールアプリを作り、クロスプラットフォーム対応のために WASM へ移植したのだろう。
mbc.wasm の仕様
- ファイル I/O は Emscripten の File API (仮想ファイルシステム)を介して行う*1
- 入力
- 元画像
ori.png - 色補正用の
m.bmp
- 元画像
- 出力
- .mbd 形式のファイル
mb.dat
- .mbd 形式のファイル
コマンドの引数:
[String(brightness), String(saturation), String(density), '-i' + invert, '-o' + c_order, '-w' + c_width, '-s' + dpi_step]
各変数の意味は https://github.com/dukelec/mb/blob/master/doc/dev.md に書いてあった:
args: img_dat: input png image, in type: Uint8Array brightness: 0~200, default 100 saturation: 0~500, default 100 density: 1~100, default 100 cal.c,m,y: 1~255, default 255 invert: print direction: 0: left to right, 1: right to left c_order: 0: CMY, 1: CYM. default 0 c_width: cartridge chip width: 0: 4.4mm, 1: 4.9mm. default 0 dpi_step: dpi setting: 1: 1200x1200, 2: 1200x600, 4: 1200x300 st_cb: progress status callback function ret: .mbd file data, in type: Uint8Array
WASM の初期化が重い
node.js の起動 + WASM の初期化で結構時間がかかる。画像が複数枚あると意外とストレスを感じる。もっとサクッと処理して欲しいなぁ。
というわけで、サーバーを立てて HTTP 経由で画像を渡せるようにする実験をしている。これなら WASM の初期化はサーバープロセス起動時の一度だけで済むよね。拙作の mb-node は今後この向きで育てていきたい。
*1:node.js 用に NODEFS なるものが用意されていたけど、使い方がよく分からなかった
MBrush (PrinCube) の研究①
「ハンディープリンター」というジャンルをご存知だろうか? ハンディスキャナーのごとく、小型の筐体を手に持って左右にスライドさせると、対象物に文字や画像を印刷できる。国内メーカーではリコーが唯一製品化していて、4万円ながら入荷待ち多数と大人気なようだ。
一方海外に目を向ければ、同様のコンセプトの製品がいくつかある。今回はその中から、中国メーカーの MBrush (PrinCube) を使ってみた。
購入の動機
- 大量(数百枚)の Blu-ray ディスクに、ユニークな QR コードを印刷したい。
- レーベル印刷できる(普通の)プリンターは、専用トレイにいちいちディスクをセットして、プリンターに挿入して…… という作業が面倒。1枚印刷するのに時間がかかりすぎる。もっと手軽に、高速に印刷したい。
MBrush を選択した理由
- 安価(送料込みで95米ドル前後)
- 日本国内でインク調達可能
- Web アプリ経由で印刷するので、ハックして他アプリとの連携できそう
MBrush と PrinCube
MBrush と酷似の商品で PrinCube というモノがあるが、どうも両者は同一製品らしい。商標の都合?らしく、中国国内では MBrush、それ以外では PrinCube の名で売っているようだ。
MBrush (PrinCube) のしくみ
ヒューレット・パッカード (HP) のプリンターで、インクタンクとプリンタヘッドが一体になっている製品がある。これをハックして小型化したのが MBrush (PrinCube) だ。ヘッド付きインクは昔からあったので、超小さいプリンター作れるんじゃないかな~、と思ったことがあるが、まさにそれ。
HP のプリンターは日本国内でも売ってるので、インクの入手はたいへん容易(サプライを個人輸入しないで済むのは大きい)。62XL/64XL/804 あたりが使える。黒インクは使えないっぽい?

底面の写真がこちら。電源スイッチと、インクを固定するフタのロック、移動方向を制限する2本のローラー、あとはセンサーが見える。これはおそらく赤外線センサーで、マウスと同じ原理で移動量を検出していると思われる。

上フタもツメで止まっているだけなので簡単に開けられる。下側に MPU の Ingenic X1021 が見える。Wi-Fi の IF は蟹さん…ではなく ESP8089(8266EX じゃないよ!)、あと FPGA (Lattice iCE5) が積んであって、これでヘッドに送る信号を生成するのかな? 基板裏面にも何かありそうだけど、外すのが面倒なのでやめといた。
使ってみる
電源を入れるとアクセスポイントが立つので、そこに接続。192.168.44.1 にアクセスすると Web アプリが読み込まれる。
(MBrush 持ってない人もアプリの使い勝手はここで体験できる)
最初から入ってるサンプルを開いてみると、印刷したい領域を指定するクロップ枠を複数指定できるようになっている。

クロップ枠の高さを変えてみると、うーん悲しいかな、横に書いてある実際の印字サイズの高さは 14.29 mm 固定で、変えられるのは幅だけのようだ。複数行印刷用の定規がついてきたのに、複数行に分けて印刷するようなオプションも見当たらない。
画像ファイルを読み込んで即印刷という機能もなく(これをやりたかった)、新規プロジェクトを立ち上げて、画像をキャンバスに追加して、クロップ枠を指定、という手順を必ず踏まないといけないようだ。
なんか微妙だなぁ。
Web アプリの動作を観察してみる
使いにくいので、さっそくハックしてみよう。ちなみにアプリのソースコードは探したら GitHub に転がってた。
印刷ボタンをクリックした時の流れを観察すると、PC 側で PNG 画像を一旦 .mbd なる内部ファイル形式に変換して、それを POST している。この処理は Web Worker 上で実行して、UI のブロッキングを防ぐ作りになっている。とてもモダンな感じだ。
で、肝心のコア部分は mbc.wasm で実装されている。wasm? そう、WebAssembly である。
『天鳳の牌譜を画像に変換β』をリリースしていた
サービスを公開したのに、説明をどこにも書かず放置していたので今書く。
これは何かというと、天鳳牌譜エディタβで入力した牌譜データを、画像1枚に整形して出力するツール。
説明
- 天鳳
- 麻雀が無料で打てるオンラインゲーム。
- 牌譜
- 麻雀の対局において、その経過(各プレイヤーの配牌や摸打)を記録したもの。
天鳳牌譜エディタβとは、紙の牌譜やリアル対局を、後でアニメーションとして再生させられる、大変便利なツールである。
『天鳳の牌譜を画像に変換β』はその真逆を行く(不便な方へ向かう)ツールなわけで、誰得感が半端ないが、これを作ったのは PHP の勉強のためなのでご容赦願いたい。
作業のあれこれ
思いついてからリリースまでの経過は、だいたいこんな感じ。
- なんとなく形になる:3日
- ブラッシュアップ(出力の体裁を整える):4日
- apache を勉強しながら立てる:3日
- Imagick のエラーで躓く:1週間
結局、開発環境と運用環境のわずかな差で躓いて、時間を浪費してしまった気がする。環境を揃えるって大事。
利用実績
公開してから1ヶ月半ほど経過したが、現在までの利用者は 0 人でございます(うp主調べ)。
まあ、鯖に負荷かからないからいっか……。
Git/GitHub で困った時に見るリンク集
全体の流れ
やらかした時
- 間違って
git reset --hardしてしまった
マージ
元に戻す
履歴操作
差分を見る
はじめての Apache on Ubuntu その1
大体のことはここ↓を見れば分かる。
CentOS ではなく Ubuntu を選択したのは、そこに Ubuntu machine があったから。特に深い意味はない。
ドメインの取得
サーバーはすでに所有しているものとする。筆者はさくらの VPS をサーバーとして使う。
まず、どこかからドメインを買ってくる。筆者は お名前.com で hunamizawa.com を購入。
DNSの設定
正引き
FQDN (hunamizawa.com) から IPアドレスを得る、基本的な設定。お名前.com の管理画面から、サーバーのIPアドレスをAレコード・AAAAレコードに設定する。メールは使う予定がないのでMXレコードは設定しない。
DNS がある程度浸透するまで数時間待つ。
逆引き
IPアドレスから FQDN (hunamizawa.com) を得るための設定。
nslookup hunamizawa.com が通るようになったので、さくらのVPSの管理画面から、ホスト名逆引き登録で hunamizawa.com を設定する。
この後、DNS が完全に浸透するまで3日ぐらい必要なので、その間にローンチの準備をするのが吉。
Apache のセットアップ
とりあえずここ↓を見て、http://hunamizawa.com/ にアクセスすると "It works!" と言ってくれる状態にする。
で、冒頭に申し上げた通り Ubuntu を使っているので、よく使われる CentOS とは細かい違いがある。
- Apache のユーザー名は
www-data(apacheではない) - 設定ファイルの置き場は
/etc/apache2
SSL 証明書を取得
HTTP over SSL に必要な証明書を、Let's Encrypt から貰ってくる。ちょっと前なら SSL 証明書を貰うのにウン万円必要だったのに、すごい時代になったなぁ。
まず、certbot を入れる。apt-get で入るやつは古いから使わないほうが良いとのこと。
apt-get install python3 pip3 pip3 install certbot
依存関係で足りないモジュールがあれば都度 pip3 install する。自分の場合は「cryptography が古いよ」と言われたので install --upgrade cryptography した。
次に、80番ポートを一瞬開けておいて、次を実行する。
certbot certonly --webroot -w /var/www/html -d hunamizawa.com
連絡先のメアドを入れたり、利用規約への同意にチェックしたら、ACME によるドメイン名所有の認証が行われて、証明書が発行される。
Apache のセットアップ(続き)
以降は HTTPS で serve すればよいが、ACME の仕様上 HTTP も開けておかないと証明書の更新ができない。サイト利用者の利便性も考えて、HTTP (80) 宛のリクエストを全部 HTTPS (443) にリダイレクトするように設定しておく。
RewriteEngine on
RewriteCond %{HTTP_HOST} ^hunamizawa\.com
RewriteRule ^/(.*)$ https://hunamizawa.com/$1 [R=301,L]
SSL 証明書のパスを適切に設定する。
SSLCertificateFile /etc/letsencrypt/live/hunamizawa.com/fullchain.pem SSLCertificateKeyFile /etc/letsencrypt/live/hunamizawa.com/privkey.pem
設定ファイルの構文チェック。
apachectl configtest
必要なモジュールをロードし、サイトの設定ファイルを読み込ませる。a2en**/a2dis** は *-enabled にシンボリックリンクを張ったり剥がしたりするコマンド。
a2enmod ssl a2enmod rewrite a2dissite 000-default a2ensite hunamizawa.com systemctl restart apache2
80番と443番を開けて、SSL 関係のエラーが出ないこと、HTTP 宛リクエストが正常に 301 リダイレクトされることを確認する。
certbot を cron で回す
crontab に1行書き加えれば良い。
certbot renew
ここまで来れば、static なコンテンツを HTTPS で配信できる状態になった。後は PHP なり何なりのセットアップをすればよい。
Ubuntu での ImagickException: unable to open file `/tmp/magick-***' には extra package を入れろ
環境
- 開発:Windows 10 + PHP 7.4.5 (cli) + ImageMagick 6.9.7
- 運用:Ubuntu 18.04 + Apache 2.4 + PHP 7.4.6 + ImageMagick 7.0.10
症状
$img = new Imagick('hoge.svg');
すると、次の例外がスローされる。
PHP Fatal error: Uncaught ImagickException: unable to open file `/tmp/magick-25662B639-gQXNdPk': No such file or directory @ error/constitute.c/ReadImage/544
原因と対処
Ubuntu で提供されている libmagickcore-6.q16-3 には、SVG のサポートが含まれていない。そのため SVG を読むと当然コケるのだが、なんの関連もない unable to open file なる例外が投げつけられるので、原因の把握には相当な時間を要することになる。
すなわち、libmagickcore-6.q16-3-extra を手動でインストールすれば問題は解消する。なお、php-imagick の依存関係で自動的にインストールされるのは無印 package だけであり、extra package は手動でインストールする必要がある。
apt-get install libmagickcore-6.q16-3-extra
事の顛末
私は PHP 初心者。ImageMagick と組み合わせたアプリを作り、サーバーで走らせたところ、このような例外に遭遇した。
ただ画像を読み込むだけなので、例外の原因の検討がつかない。ひとまず @ error/constitute.c と見えるので、imagick 側の問題か?と考える。
最初に疑ったのは、/tmp/ 絡みの例外なので PrivateTmp の影響。そこで PrivateTmp = false してみても、$img->setRegistry('tmp-directory', '/hogemoge') してみても、何も変わらない。
unable to open file tmp magick とかで検索してみると、「ImageMagick の特定バージョンでそういうバグがある」的な Issue を見たので、もしかしてバグを踏み抜いてる?
なら最新の ImageMagick を入れるか、と思って PECL の準備を始めた。ビルドの依存関係になるので何気なく aptitude show libmagickcore-6.q16-3-extra を見てみると
This package adds support for SVG, WMF, OpenEXR, DjVu and Graphviz to MagickCore.
あっ……
apt-get install libmagickcore-6.q16-3-extra で万事解決しましたとさ。
ESP8266/ESP32 でうるう秒を扱えるライブラリ『ESPPerfectTime』を作ったので使ってほしい
ESP8266/Arduino で置時計を作った。せっかくなので、うるう秒(午前8時59分60秒)を表示できるようにしたくなった。
SNTP とうるう秒
ESP8266/ESP32 で時計を作る利点として、SDK に SNTP クライアントが含まれているので、Arduino なら configTime(...) と1行書くだけで簡単に時刻合わせができる。時刻同期やその管理を気にしなくていいのは楽だ。
ところで NTP パケットには、うるう秒の挿入・削除を予告する Leap Indicator (LI) というフィールドが含まれている。残念ながら ESP8266/ESP32 組み込みの SNTP クライアントは、うるう秒に対応していない。なので自前で NTP パケットを解析して、LI の値を見ればいい、という話。
もう一つの問題:Round-Trip Delay
(ぶっちゃけ、時計を作るならこっちの方が大きな問題になる)
SNTP は、サーバーの現在時刻をネットワーク越しにクライアントに送ってもらうことで、時刻を合わせている。ということは、ネットワークの遅延が時計の精度にモロに影響してくるわけである。極端な例えをすると、RTT = 10秒の通信路なら、サーバーが送ったパケットがクライアントに届くまで平均5秒かかるので、何も考えなければクライアントの時計は5秒遅れてしまう*1。
そこでクライアントは、リクエストを送る瞬間の時刻をパケットに記録しておいて、レスポンスが帰ってきた時に引き算で RTT を求めて、遅延を補正する処理をしている。詳しくは下の記事を見てほしい。
で、手元で実験したところ、ESP8266/ESP32 組み込みの SNTP クライアントは、残念ながら RTT を補正する処理を実装していないようだ*2。ついでに、こちらも実装することにしよう。
実装
lwip に含まれている sntp.c を、うるう秒対応&RTT を補正するように改造した。
リクエストパケットを生成する initialize_request() の中で Transmit Timestamp を記録している。本来は UNIX time から NTP time に変換しなきゃいけないけど、RFC4330 の中で「サーバーは、クライアントから送られてきた Transmit Timestamp を、そのまま Originate Timestamp にコピーして送り返さねばならない」と規定されている*3ので、この変換をケチって UNIX time をそのまま記録している。
実際の RTT の計算は、レスポンスが帰ってきた後の process() で実行している。NTP のタイムスタンプは 1 bit = 1/(232) [sec] なので、上位 32 bit はそのまま秒として読めるし、下位 32 bit は 4295 で割るとマイクロ秒になる*4。
使い方
README.md とか、Sample を見ればわかると思う。
gmtime() localtime() の使い方が2種類あることには注意してほしい。
// 現在時刻を取得する使い方 struct tm current = *pftime::localtime(nullptr); // time_t の値を struct tm に変換する関数としての使い方 // time_t t; struct tm tm = *pftime::localtime(t); // 現在時刻を取得するのに、こういう使い方はしてほしくない time_t t = time(nullptr); struct tm current = *pftime::localtime(t);
欠点
世の中には、うるう秒が原因で障害を起こすシステムもある*5。そういうところ向けに、時計を少しずつ、何時間もかけてずらすことで、うるう秒の存在を消し去ってくれる(SLEW モード)NTP サーバーもある(例えば google のとか)。SLEW モードで運用されている NTP サーバーを同期先として設定していると、当然 LI = 0 しか帰ってこないので、「うるう秒を表示したい」という当初の目的は達成できない*6。
NICT の NTP サーバー ntp.nict.jp のように、今まで STEP モード(うるう秒を正しく挿入する)で運用されてきた NTP サーバーでも、次回のうるう秒挿入時も STEP モードで運用してくれる保証はない。うるう秒の瞬間を確実に見たい人は、うるう秒挿入が近づいたら、サーバー管理者の告知をチェックするべきだろう。
*1:現実には、ESP8266 のデフォルトでは RTT >= 3秒でタイムアウトするので、1.5 秒以上遅れることはないが。
*2:まあ、普通の組み込み環境で、時刻の精度にそこまでこだわる必要は無いし……
*3:RFC から引用: If the server is unsynchronized or first coming up, ... the Transmit Timestamp field of the request is copied unchanged to the Originate Timestamp field of the reply. It is important that this field be copied intact, as an NTP or SNTP client uses it to avoid bogus messages. ... If the server is synchronized, ... The Originate Timestamp field is set as in the unsynchronized case above.
*4: (1/106) ÷ (1/232) = 4294.6...
*5:障害の原因になるのでうるう秒を廃止しよう、という議論があるくらいである。
*6:このような場合でも、デフォルトで1時間ごとに同期するので、時刻が大きくズレることはない。