LoginSignup
0
1

More than 3 years have passed since last update.

ディスプレイがOFFになったらRaspberryPi経由でTV電源をOFFにしたい

Last updated at Posted at 2019-12-22

BRAVIA X9500Gを最近買ってHDMI経由でPCと接続しているのですが、このテレビにはPCからの入力がOFFになったときに、自動的にTV電源がOFFになる機能がありませんでした1
そこで、2年前くらいに買って転がっていたRaspberry Pi Zero WHを使ってRaspberry Pi経由で電源OFFを試してみました。

方針

TV操作について

HDMI-CECを使ってTVを操作します。
残念ながらPCについているHDMIからではHDMI-CEC信号を送れないことが多いらしく、対象のPC(NVIDIAグラボ)でも送れませんでした。
Raspberry PiはHDMI-CECを使えるようなので、HDMIでTVに接続してcec-clientを使ってTVを操作します。

cec-clientをインストールしておけば、echo "standby 0" | cec-client -s -d 1コマンドでTV電源をOFFにできます。

モニタOFF検知について

PCはWindows 10なのですが、簡単に検知する方法はなさそうでした。
Win32 APIのRegisterPowerSettingNotificationを使うと通知されるようになるWM_POWERBROADCASTメッセージから情報を取得します。

モニタOFFを検知したら、HTTPでサーバ(Raspberry Pi)にTV電源OFFを指示します。
なお、クライアント側・サーバ側ともにNode.jsを使っていきます。

環境

Raspberry Pi バージョン
OS Raspbian GNU/Linux 9.4 (stretch)
Node.js v8.11.1
cec-client (libCEC) 4.0.2

Raspberry PiはIPアドレスを192.168.1.100で固定してHTTP通信できるところまで設定済

PC バージョン
OS Windows 10 Pro 1909
Node.js v10.17.0

PC側で使用するライブラリは以下のとおり

dependencies
    "console-stamp": "^0.2.9",
    "ref-array": "^1.2.0",
    "ref-struct-di": "^1.1.0",
    "unirest": "^0.6.0",
    "win32-api": "^6.2.0"

Raspberry Pi側のソース

サーバ側は、リクエストBodyの内容をcec-clientに入力するだけです。

server.js
const http = require('http');
const { execSync } = require('child_process');

http.createServer((req, res) => {
    let body = [];
    req.on('error', (err) => {
        console.error(err);
    }).on('data', (chunk) => {
        body.push(chunk);
    }).on('end', () => {
        body = Buffer.concat(body).toString();
        console.log(`body: ${body}`);

        const command = `echo "${body}" | cec-client -s -d 1`;
        const result = execSync(command).toString();
        console.log(`result: ${result}`);

        res.writeHead(200, { 'Content-Type': 'text/plain' });
        res.end(result);
    });
}).listen(8080);

console.log('server started');
start.sh
#!/bin/sh

/opt/nodejs/bin/node /opt/cec_server/server.js &

起動時に動いてほしいので、cronで@rebootを指定します。
パイプでloggerにつないで、ログが/var/log/messagesに出るようにしています。

cron設定
@reboot /opt/cec_server/start.sh | logger -t cec_server

PC側のソース

クライアント側は少し大変で、メッセージ受信用のWindowとコールバック関数を作っていく必要があります。
また、受信したメッセージ内のPointerのアドレスをStructに変換していく仕組みも必要です。

まずは、struct.jsでGUIDPOWERBROADCAST_SETTINGのStructの定義と、ffiを使ってRegisterPowerSettingNotification関数を呼び出すための仕組みを作っていきます。

GUIDの値はGUID_CONSOLE_DISPLAY_STATEとGUID_MONITOR_POWER_ONのどちらでも使えますが、"New applications should use GUID_CONSOLE_DISPLAY_STATE"とのことなので、こっちを使っています。

struct.js
const ffi = require('ffi');
const ref = require('ref');
const Struct = require('ref-struct-di')(ref);
const ArrayType = require('ref-array');

const GUID = Struct({
    Data1: ref.types.long,
    Data2: ref.types.short,
    Data3: ref.types.short,
    Data4: ArrayType(ref.types.uchar, 8)
});
const LPCGUID = ref.refType(GUID);

function translateGUID(strGUID) {
    const pattern = /(\w+)-(\w+)-(\w+)-(\w{2})(\w{2})-(\w{2})(\w{2})(\w{2})(\w{2})(\w{2})(\w{2})/;
    const [d1, d2, d3, ...d4] = pattern
        .exec(strGUID)
        .slice(1)
        .map(v => parseInt(v, 16));
    return GUID({ Data1: d1, Data2: d2, Data3: d3, Data4: d4 });
}
function equalsGUID(guid1, guid2) {
    return JSON.stringify(guid1) == JSON.stringify(guid2);
}

const GUID_CONSOLE_DISPLAY_STATE = translateGUID('6FE69556-704A-47A0-8F24-C28D936FDA47');
// export const GUID_MONITOR_POWER_ON = translateGUID('02731015-4510-4526-99E6-E5A17EBD1AEA');

const POWERBROADCAST_SETTING = Struct({
    PowerSetting: GUID,
    DataLength: ref.types.ulong,
    Data: ArrayType(ref.types.uchar, 1)
});

const RegisterPowerSettingNotification = ffi.Library('user32.dll', {
    RegisterPowerSettingNotification: ['long', ['long', LPCGUID, 'long']]
}).RegisterPowerSettingNotification;

module.exports = {
    GUID_CONSOLE_DISPLAY_STATE: GUID_CONSOLE_DISPLAY_STATE,
    POWERBROADCAST_SETTING: POWERBROADCAST_SETTING,
    RegisterPowerSettingNotification: RegisterPowerSettingNotification,
    equalsGUID: equalsGUID
};

detect_poweroff.jsでは、createWindowでメッセージ受信用の非表示のウィンドウを作って、registerNotificationで通知の登録をしていきます。
Windowを作る際にも指定したWndProcでコールバック(メッセージ)を待ち受けます。

WndProcの中では、対象のメッセージのlParamPOWERBROADCAST_SETTINGへのポインタのアドレスが入っているので、bufferAtAddressを使ってアドレス数値からBufferを構築しています。
lParamがNumber型になっていて、整数を53ビットまでしか扱えないのに問題ないか気になりましたが、Windows 10ではサポートする仮想アドレス空間は48ビットのため大丈夫なようです2

外から呼び出されるstartMessageLoopでは、メッセージをWndProcに転送するメッセージループを開始します。

detect_poweroff.js
const { U, K, DTypes, DStruct, Config } = require('win32-api');
const [W, DS] = [DTypes, DStruct];
const ffi = require('ffi');
const ref = require('ref');
const Struct = require('ref-struct-di')(ref);
const {
    GUID_CONSOLE_DISPLAY_STATE,
    POWERBROADCAST_SETTING,
    RegisterPowerSettingNotification,
    equalsGUID
} = require('./struct.js');

const WM_POWERBROADCAST = 536;
const PBT_POWERSETTINGCHANGE = 32787;

const user32 = U.load();
const kernel32 = K.load();

var callback_f = () => {};
function registerCallback(f) {
    callback_f = f;
}

function registerNotification(hWnd, guid) {
    const hWndAddr = ref.address(hWnd);
    const hPowerNotify = RegisterPowerSettingNotification(hWndAddr, guid.ref(), 0);
    console.log('register result:', hPowerNotify);
    const error = kernel32.GetLastError();
    console.log('register error:', error);
}

function bufferAtAddress(address, bufferType) {
    if (address > Number.MAX_SAFE_INTEGER) {
        throw new Error('Address too high!');
    }
    const buff = Buffer.alloc(8);
    buff.writeUInt32LE(address % 0x100000000, 0);
    buff.writeUInt32LE(Math.trunc(address / 0x100000000), 4);

    buff.type = bufferType;
    return ref.deref(buff);
}

const WndProc = ffi.Callback('uint32', [W.HWND, W.UINT, W.WPARAM, W.LPARAM], (hwnd, uMsg, wParam, lParam) => {
    console.log('WndProc callback:', uMsg, wParam, lParam);

    if (uMsg == WM_POWERBROADCAST && wParam == PBT_POWERSETTINGCHANGE) {
        const buf = bufferAtAddress(lParam, ref.refType(POWERBROADCAST_SETTING));
        const setting = buf.deref();
        const state = setting.Data[0];
        console.info('power state:', state);

        if (equalsGUID(setting.PowerSetting, GUID_CONSOLE_DISPLAY_STATE)) {
            console.log('GUID_CONSOLE_DISPLAY_STATE');
            callback_f(state);
        }
    }
    return 0;
});

function createWindow(clazzName) {
    const className = Buffer.from(clazzName + '\0', 'ucs-2');
    const windowName = Buffer.from(clazzName + '_Window\0', 'ucs-2');

    const hInstance = ref.alloc(W.HINSTANCE);
    kernel32.GetModuleHandleExW(0, null, hInstance);

    const wClass = new Struct(DS.WNDCLASSEX)();
    wClass.cbSize = Config._WIN64 ? 80 : 48; // x86 = 48, x64=80
    wClass.style = 0;
    wClass.lpfnWndProc = WndProc;
    wClass.cbClsExtra = 0;
    wClass.cbWndExtra = 0;
    wClass.hInstance = hInstance;
    wClass.hIcon = ref.NULL;
    wClass.hCursor = ref.NULL;
    wClass.hbrBackground = ref.NULL;
    wClass.lpszMenuName = ref.NULL;
    wClass.lpszClassName = className;
    wClass.hIconSm = ref.NULL;

    if (!user32.RegisterClassExW(wClass.ref())) {
        throw new Error('Error registering class');
    }
    const hWnd = user32.CreateWindowExW(0, className, windowName, 0, 0, 0, 0, 0, null, null, hInstance, null);
    registerNotification(hWnd, GUID_CONSOLE_DISPLAY_STATE);
}

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

async function startMessageLoop() {
    const msg = new Struct(DS.MSG)();
    while (true) {
        if (user32.PeekMessageW(msg.ref(), null, 0, 0, 1)) {
            user32.TranslateMessageEx(msg.ref());
            user32.DispatchMessageW(msg.ref());
        }
        await sleep(100);
    }
}

createWindow('Node.js_idle-poweroff');

module.exports = {
    registerCallback: registerCallback,
    startMessageLoop: startMessageLoop
}

send_poweroff.jsでは、モニタON/OFFを検知した際に呼ばれるコールバックを登録して、メッセージループを開始します。
コールバックが呼ばれたら、HTTPでHDMI-CECコマンドを送信します。

send_poweroff.js
require('console-stamp')(console, 'yyyy-mm-dd HH:MM:ss');
const unirest = require('unirest');
const { registerCallback, startMessageLoop } = require('./detect_poweroff.js');

const DISPLAY_OFF = 0;
const DISPLAY_ON = 1;

const url = 'http://192.168.1.100:8080';

function sendCommand(command) {
    console.info('send command:', command);
    unirest
        .post(url)
        .send(command)
        .end(res => {
            console.log('response body:', res.body);
        });
}

registerCallback(state => {
    if (state == DISPLAY_OFF) {
        sendCommand('standby 0');
    } else if (state == DISPLAY_ON) {
        sendCommand('on 0');
    }
});
startMessageLoop();
start_idle_poweroff.bat
cd %~dp0
node send_poweroff.js

実行結果は以下のとおり。
PC側のON/OFFに合わせてTV電源が連動するようになりました。

実行結果
[2019-12-22 10:23:04] [LOG]    WndProc callback: 36 0 267690495392
[2019-12-22 10:23:04] [LOG]    WndProc callback: 129 0 267690495296
[2019-12-22 10:23:04] [LOG]    WndProc callback: 131 0 267690495424
[2019-12-22 10:23:04] [LOG]    WndProc callback: 1 0 267690495296
[2019-12-22 10:23:04] [LOG]    register result: -1532784720
[2019-12-22 10:23:04] [LOG]    register error: 0
[2019-12-22 10:23:04] [LOG]    WndProc callback: 536 32787 2382176198256
[2019-12-22 10:23:04] [INFO]   power state: 1
[2019-12-22 10:23:04] [LOG]    GUID_CONSOLE_DISPLAY_STATE
[2019-12-22 10:23:04] [INFO]   send command: on 0
[2019-12-22 10:23:04] [LOG]    WndProc callback: 799 1 0
[2019-12-22 10:23:08] [LOG]    response body: opening a connection to the CEC adapter...

[2019-12-22 10:29:48] [LOG]    WndProc callback: 536 32787 2382174664240
[2019-12-22 10:29:48] [INFO]   power state: 0
[2019-12-22 10:29:48] [LOG]    GUID_CONSOLE_DISPLAY_STATE
[2019-12-22 10:29:48] [INFO]   send command: standby 0
[2019-12-22 10:29:52] [LOG]    WndProc callback: 537 7 0
[2019-12-22 10:29:52] [LOG]    response body: opening a connection to the CEC adapter...

[2019-12-22 10:31:49] [LOG]    WndProc callback: 536 32787 2382147455920
[2019-12-22 10:31:49] [INFO]   power state: 1
[2019-12-22 10:31:49] [LOG]    GUID_CONSOLE_DISPLAY_STATE
[2019-12-22 10:31:49] [INFO]   send command: on 0
[2019-12-22 10:31:52] [LOG]    response body: opening a connection to the CEC adapter...

感想

Node.jsでRegisterPowerSettingNotificationを使うサンプルが見当たらなかったので作ってみましたが、予想以上に大変でした。
特にrefの使い方は試行錯誤が必要で、hWndをログ出力しようとするとログが出ずにプログラムが落ちるなど、解析が難しい場面が多くありました。
また、lParamの型は W.LPARAMのところをW.PINT64などに変えるとBuffer型になるのですが、そこからうまくStructに変換ができませんでした。

はじめはPCのアイドル時間を(これもWin32 API経由で)取得して電源OFFをするようにしていました。その場合はPC側のコードは30行程度で済みます。
しかし、それだと動画を見ている最中に電源がOFFになってしまうという課題がありました。

参考にしたページ

https://stackoverflow.com/questions/48720924/python-3-detect-monitor-power-state-in-windows
https://github.com/waitingsong/node-win32-api/blob/master/demo/create_window.ts
https://github.com/TooTallNate/ref/issues/96


  1. 「無操作電源オフ」の設定はあるものの、これはテレビのリモコンを操作しなかった場合にOFFになる設定のため、使えませんでした。サポートにも問い合わせましたが機能はないとのこと 

  2. アドレス数値をBufferに変換する方法は他に見つかりませんでした 

0
1
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
1