node_canvasというnpmモジュールがありまして、HTML5のCanvasに似せて作ったそうです。これを使ってみたいと思い、ネタを探しておりました。
node-canvas
https://github.com/Automattic/node-canvas
一方で、ちょうど使っていないガジェットがありました。
USB LED文字盤:J842-3 LED 4MOJI
https://www.pc-koubou.jp/products/detail.php?product_id=568334
(今はもう売っていませんが。。。)
これは、11×44個のLEDが文字盤のようになっていて、USBに接続すると仮想COMポートとして見え、ドットマトリクス液晶のようにドット絵を描くことができます。
温湿度のセンサーはというと、すでに以下で稼働中です。定期的にMQTTでPublishしています。
Xiaomi Mijia 温湿度計 をIoTデバイスとして使う
これら組み合わせると、温湿度計ができあがります。
① Raspberryに USB LED文字盤をつなげ、MQTTをSubscribeしておきます。
② 定期的に、Xiaomi Mijia温湿度計のアドバタイジングデータをスキャンし、温湿度をPublishします。
③ Pulishされた温湿度を受信すると、LED文字盤に温度と湿度を表示します。
②では、以下のトピックで温湿度データがPublishされてきます。
トピック名:awsiot/mijia
メッセージ内容:
{
productId: 426,
counter: 125,
mac: 'XXXXXXXXXXXX',
tmp: 20.2,
hum: 41.4,
createdat: 1546343788634,
createdatstr: '2019-1-1 20:56:28'
}
npmセットアップする
利用するnpmモジュールは以下の通りです。
- canvas
- dotenv
- mqtt
- serialport
canvasのnpmモジュールをインストールする前に、いくつかのセットアップが必要です。
私の環境では、以下が不足していましたので、事前にインストールしておきました。
apt-get install libpixman-1-dev
apt-get install libcairo2-dev
apt-get install libpango1.0-dev
apt-get install libjpeg-dev
serialportについて補足すると、USB LED文字盤は、USB接続すると仮想COMポートとして見えます。Prolificのチップを使っているようで、特に追加のドライバのインストールは不要でした。
フォントをセットアップする
node_canvasでは、文字表示にTTFフォントを使うことができます。
今回のUSB LED文字盤の表示領域は44×11ドットと小さいため、普通のフォントを使うと文字が欠けてしまいます。そこで、10px程度を想定したフォントを使いました。
以下のフォントを使わせていただきました。(ありがとうございました)
PixelMplus(ピクセル・エムプラス) ‥ 8bitビットマップふうフリーフォント
http://itouhiro.hatenablog.com/entry/20130602/font
これをダウンロードしておきます。
実装
まずフォルダを用意し、npmモジュールをインストールします。
mkdir mqtt_disp
cd mqtt_disp
npm init -y
npm install --save canvas
npm install --save dotenv
npm install --save mqtt
npm install --save serialport
フォントとして、10pxをベースに作られた「PixelMplus10-Regular.ttf」をカレントフォルダにコピーしておきます。
まずは、USB LED文字盤を制御するためのクラスライブラリを作りました。
'use strict';
var SerialPort = require('serialport');
class MiniDisplay{
constructor(port, num = 1, width = 44){
this.MINIDISP_EFFECT_LEFT = 0x00;
this.MINIDISP_EFFECT_RIGHT = 0x01;
this.MINIDISP_EFFECT_UP = 0x02;
this.MINIDISP_EFFECT_DOWN = 0x03;
this.MINIDISP_EFFECT_FIX = 0x04;
this.MINIDISP_EFFECT_ANIME = 0x05;
this.MINIDISP_EFFECT_SNOW = 0x06;
this.MINIDISP_EFFECT_MIDDLE = 0x07;
this.MINIDISP_BLINK = 0x08;
this.MINIDISP_MARQUEE = 0x80;
this.num = num;
this.width = width;
this.height = 11;
this.unit = Math.ceil(this.width / 8);
this.ctrls = [];
this.images = [];
for( var i = 0 ; i < this.num ; i++ ){
this.ctrls.push(this.MINIDISP_EFFECT_FIX);
var tmp = new Buffer(this.unit * this.height);
tmp.fill(0);
this.images.push(tmp);
}
this.null_buffer = new Buffer(this.unit);
this.cmd_header = new Buffer(6 + 8 + 1 + 4 * 8);
const hello = new Buffer('Hello');
hello.copy(this.cmd_header);
this.sp = new SerialPort(port, {
baudRate: 4800
});
}
set_speed(no, speed){
if( no >= this.num || no < 0 )
return;
this.ctrls[no] = ((speed & 0x07) << 4) | (this.ctrls[no] & 0x8f);
}
set_effect(no, effect){
if( no >= this.num || no < 0 )
return;
this.ctrls[no] = (effect & 0x8f) | (this.ctrls[no] & 0x70 );
}
put_pixel(no, x, y, val){
if( no >= this.num || no < 0 )
return;
if (x >= this.width || x < 0 || y >= this.height || y < 0)
return;
if (val)
this.images[no][this.unit * y + Math.floor(x / 8)] |= (0x01 << (7 - (x % 8)));
else
this.images[no][this.unit * y + Math.floor(x / 8)] &= ~(0x01 << (7 - (x % 8)));
}
get_pixel(no, x, y){
if( no >= this.num || no < 0 )
return -1;
if (x >= this.width || x < 0 || y >= this.height || y < 0)
return -1;
var tmp = this.images[no][this.unit * y + Math.floor(x / 8)];
return ((tmp >> (7 - (x % 8)) & 0x01 ) & 0x01 ) == 0x00 ? 0 : 1;
}
clear_image(no){
if( no >= this.num || no < 0 )
return -1;
this.images[no].fill(0);
}
async minidisp_display(){
console.log('minidisp_display called');
var i;
for (i = 0; i < this.num; i++)
this.cmd_header[6 + i] = this.ctrls[i];
for (; i < 8; i++)
this.cmd_header[6 + i] = 0x00;
this.cmd_header[6 + 8] = 0x00;
var offset = 0;
for (i = 0; i < this.num; i++){
this.cmd_header[6 + 8 + 1 + 4 * i] = 0x08;
this.cmd_header[6 + 8 + 1 + 4 * i + 1] = offset;
this.cmd_header[6 + 8 + 1 + 4 * i + 2] = 0x00;
this.cmd_header[6 + 8 + 1 + 4 * i + 3] = Math.ceil(this.width / 8);
offset += this.cmd_header[6 + 8 + 1 + 4 * i + 3];
}
for (; i < 8; i++){
this.cmd_header[6 + 8 + 1 + 4 * i] = 0x08;
this.cmd_header[6 + 8 + 1 + 4 * i + 1] = offset;
this.cmd_header[6 + 8 + 1 + 4 * i + 2] = 0x00;
this.cmd_header[6 + 8 + 1 + 4 * i + 3] = 0x00;
}
await sp_write(this.sp, this.cmd_header, 0, this.cmd_header.length);
await wait_async(200);
var j;
for (i = 0; i < 11; i++){
for (j = 0; j < this.num; j++){
await sp_write(this.sp, this.images[j], this.unit * i, this.unit);
}
}
for (j = 0; j < this.num; j++){
await sp_write(this.sp, this.null_buffer, 0, this.unit );
}
}
};
function wait_async(timeout){
return new Promise((resolve, reject) =>{
setTimeout(resolve, timeout);
});
}
function sp_write(sp, buffer, offset, length){
var tmp = buffer.slice(offset, offset + length );
return new Promise((resolve, reject) =>{
sp.write(tmp, 'binary', (error) =>{
if( error )
return reject(error);
resolve(length);
});
})
}
module.exports = MiniDisplay;
コンストラクタは、2つの引数を受け取ります。
port:USBデバイス名です。
num:画面数です。2以上の場合には、2つの画面が交互に文字盤に表示されます。
width:画面の横の長さです。44以上の場合には、画面が長すぎて1度に文字盤に表示できないので、左右に文字を流すことができます。
そして、それを使ってMQTTをSubscribeします。
'use strict';
require('dotenv').config();
var mqtt = require('mqtt');
var Canvas = require('canvas');
const MiniDisplay = require('./minidisp');
const PORT_NAME = process.env.PORT_NAME || 【USB LED文字盤のUSBデバイス名】;
const MQTT_HOST = process.env.MQTT_HOST || 【MQTTサーバのホスト名】;
const MQTT_TOPIC = process.env.MQTT_TOPIC || 【Subscribeするトピック名】;
const FONT_FNAME = process.env.FONT_FNAME || 【TTFフォントファイル名】;
var disp = new MiniDisplay(PORT_NAME, 2);
disp.set_speed(0, 0);
disp.set_speed(1, 0);
var canvas = Canvas.createCanvas(disp.width, disp.height * 2);
var ctx = canvas.getContext('2d');
ctx.antialias = 'none';
ctx.textBaseline = 'top';
Canvas.registerFont(FONT_FNAME, { family: 'MyFont' });
//ctx.font = '16px Sans-serif';
ctx.font = '10px "MyFont"';
var called = false;
console.log('MQTT_HOST=' + MQTT_HOST);
console.log('MQTT_TOPIC=' + MQTT_TOPIC);
var client = mqtt.connect(MQTT_HOST);
client.subscribe(MQTT_TOPIC, (err, granted) => {
if( err ){
console.log(err);
return;
}
console.log('subscriber.subscribed.');
});
client.on('message', async (topic, message) =>{
if( called )
return;
called = true;
console.log('subscriber.on.message', 'topic:', topic);
var payload = JSON.parse(message.toString());
console.log(payload);
if( !payload.tmp || !payload.hum ){
console.log('date not notified');
called = false;
return;
}
ctx.fillText("温度 " + payload.tmp, 0, 0);
ctx.fillText("湿度 " + payload.hum, 0, disp.height);
var imageData1 = ctx.getImageData(0, 0, disp.width, disp.height);
var imageData2 = ctx.getImageData(0, disp.height, disp.width, disp.height);
for (var y = 0; y < disp.height; y++ ) {
for (var x = 0; x < disp.width; x++) {
var val1 = to_mono(imageData1.data[(x + y * disp.width) * 4], imageData1.data[(x + y * disp.width) * 4 + 1], imageData1.data[(x + y * disp.width) * 4 + 2], imageData1.data[(x + y * disp.width) * 4 + 3]);
disp.put_pixel(0, x, y, val1);
var val2 = to_mono(imageData2.data[(x + y * disp.width) * 4], imageData2.data[(x + y * disp.width) * 4 + 1], imageData2.data[(x + y * disp.width) * 4 + 2], imageData2.data[(x + y * disp.width) * 4 + 3]);
disp.put_pixel(1, x, y, val2);
}
}
await disp.minidisp_display();
console.log('displayed');
ctx.clearRect(0, 0, disp.width, disp.height * 2);
disp.clear_image(0);
disp.clear_image(1);
called = false;
});
function to_mono(r, g, b, a){
var grey = Math.round( r * 0.299 + g * 0.587 + b * 0.114 );
if( a > 128 || grey > 128)
return 1;
else
return 0;
}
以下を環境に合わせて変更する必要があります。
【USB LED文字盤のUSBデバイス名】:おそらく「/dev/tty/ttyUSB0」かと思います。
【MQTTサーバのホスト名】 :Mosquittoサーバを立ち上げたホスト名です。
【Subscribeするトピック名】:以前の投稿通りであれば、「awsiot/mijia」です。
【TTFフォントファイル名】:「PixelMplus10-Regular.ttf」とします。
補足すると、以下の通り進みます。
- CANVASをインスタンス化します。
- MQTT受信すると、CANVASに温湿度の文字列を描画します。
- CANVASからイメージデータを生成します。
- イメージデータをモノクロにしてUSB LED文字盤に出力します。
実行
Raspberry Piに、USB LED文字盤を接続します。
dmesgを実行すると、検出し、ttyUSB0がアタッチされていることがわかります。
[127270.880222] Indeed it is in host mode hprt0 = 00021501
[127271.090035] usb 1-1: new full-speed USB device number 16 using dwc_otg
[127271.090299] Indeed it is in host mode hprt0 = 00021501
[127271.331248] usb 1-1: New USB device found, idVendor=067b, idProduct=2303
[127271.331267] usb 1-1: New USB device strings: Mfr=1, Product=2, SerialNumber=0
[127271.331276] usb 1-1: Product: USB-Serial Controller
[127271.331283] usb 1-1: Manufacturer: Prolific Technology Inc.
[127271.350945] pl2303 1-1:1.0: pl2303 converter detected
[127271.353270] usb 1-1: pl2303 converter now attached to ttyUSB0
Raspberry Piで、以下の通り、実行します。
node index.js
以降、起動し続け、MQTTでSubscribe状態となり、Publish受信するごとに、USB LED文字盤の表示を切り替えるはずです。
別のホストから、Xiaomi Mijiaを検出し、温湿度をMQTT Publishしてみてください。温度と湿度が交互に表示されれば成功です。
以上です。