LoginSignup
0
0

More than 5 years have passed since last update.

node_canvasを使って温湿度計を作る

Last updated at Posted at 2019-01-01

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文字盤を制御するためのクラスライブラリを作りました。

minidisp.js
'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します。

index.js
'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してみてください。温度と湿度が交互に表示されれば成功です。

以上です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0