tmytのらくがき

個人の日記レベルです

シーリングライトを力押しでGoogle Assistantに対応させる (その2)

4か月前に作ったこれを、毎日使っていたのですが、今日ふと電話置き場を眺めてみるとこんなことに。あらやだ。

f:id:tmyt:20200501005509p:plain

電池って4か月ちょいでこんなことになってしまうんですね、こわいこわい。

さて、我が家のシーリングライト危機が訪れました。解決方法として考えられたのは

  1. 別のAndroidを生贄にする
  2. バッテリーのないAndroidを手に入れる

1は、まだまだあるので大丈夫といえば大丈夫なんですが4か月ごとに電話が死んでいくとなるとそれはさすがに困ります。 2は、あまり数は無く、あっても大抵が産業用なのでかなり高い。

そこで3つ目の解決方法がふと思いつきました。Raspberry Pi 3にAndroidをインストールして、そこで今までのシステムを動かす。 全開の図はこんな感じにアップデートされます。

f:id:tmyt:20200501010401p:plain

かなりスマートになりました。adbを使っていた部分はもはやAndroidの内側になってしまったのでADBは不要になり、ローカルのコマンドを実行するだけになりました。

Raspberry Pi 3 でAndroid を実行する

この作戦はRaspberry Pi 3 でAndroid が起動してかつBluetoothが比較的まともに動くことが条件でした。 今回は2個試して、ちゃんと使えたLineageOS 16.0を使います。

konstakang.com

インストールはビルド済みイメージをダウンロードして、SDに書き込み、Raspberry Pi 3にカードを入れて起動すれば数分後にはAndroidの初回セットアップ画面が起動します。

セットアップはそこそこにすすめて、いつものAndroidと同じように開発者オプションを有効にして、ADBを有効にします。加えて、Rootアクセスも有効にしておきました。 これで基本的なセットアップはおしまい。

シーリングライトの専用アプリをインストールする

専用アプリがないとライトを操作できないので、専用アプリをどうにかしてインストールします。

Gappsをインストールする方法もあるのですが、このアプリはGappsなしでも動作するので ほかのAndroidバイスからapkをコピーしました。コピー方法は割愛。

Bluetoothも正しそうに動いてBLEでライトと通信できる状態になりました。

NodeJSを実行する

この仕組みはインターネット上で動作しているNodeJSサーバが、LANの内側で動作しているNodeJSプロセスにSocket.IO通してGoogle Assistantからのコマンドを転送する仕組みになっていました。 大きく書き直すのは大変、そしてAndroidはなんだかんだLinuxなのでNodeJSが動くでしょう。ということでAndroid上でNodeJSを実行できるようにします。

Androidで使えるNodeJSのバイナリを探してみたものの、今風のバージョンは見つからず、ソースからビルドするのもいまいちうまくいかない。 そこで、Termuxをインストールし、その中で apt install nodejs した結果インストールされたバイナリを使うことにしました。

TermuxはAndroid上で動作するLinux環境のようなものです。公式にAPKが配布されているのでダウンロードしてadb install しました。

termux.com

インストールが終わったら、Termuxを立ち上げてそこからはLinuxの世界なのでてきとうにNodeJSをインストールします。

$ apt update
$ apt install nodejs

これでNodeJSがインストールできました。使う時は /data/data/com.termux/files/usr/bin/node を参照するといいです。

おしまい

あとはこのNodeJS環境でこのプログラムを実行するだけで今まで通りシーリングがON/OFFできます。うれしいですね!

const socket = require('socket.io-client')('https://***.com');
const { exec } = require('child_process');
const util = require('util');

const X = [136, 248];
const Y = [189, 373, 555];

const on = `${X[0]} ${Y[0]}`;
const off = `${X[1]} ${Y[2]}`;
const dimm = `${X[0]} ${Y[2]}`;

socket.on('connect', () => {
  console.log('connect');
});
socket.on('execute', data => {
  const input = data.inputs[0];
  for(const execution of input.payload.commands[0].execution){
    if(execution.command === 'action.devices.commands.OnOff'){
      const arg = execution.params.on ? on : off;
      exec(`input touchscreen tap ${arg}`);
    }
    if(execution.command === 'action.devices.commands.BrightnessAbsolute'){
      const arg = execution.params.brightness ? on : dimm;
      exec(`input touchscreen tap ${arg}`);
    }
  }
  console.log(util.inspect(data, true, null));
});

一つだけ問題があって、HDMIに何もつながってないと起動しません。そして起動後HDMIを抜くと、

05-01 00:07:25.610   188   267 I hwc-drm-connector: force mode to 1280x720@0Hz
05-01 00:07:25.613   188   267 I hwc-drm-two: Unplug event @1596363841 for connector 29 on display 0
05-01 00:07:25.634  3546  3546 I HWC2    : Destroying display 0
05-01 00:07:25.637  3546  3546 E HWComposer: isConnected failed for display 0: Invalid display
05-01 00:07:25.637  3546  3546 E HWComposer: getPresentFence failed for display 0: Invalid display
05-01 00:07:25.637  3546  3582 E HWComposer: onVsync Failed to find display 0
05-01 00:07:25.637  3546  3546 E HWComposer: isConnected failed for display 0: Invalid display
05-01 00:07:25.637  3546  3546 E HWComposer: getPresentFence failed for display 0: Invalid display
05-01 00:07:25.651  3616  3639 I DisplayManagerService: Display device removed: DisplayDeviceInfo{"内蔵スクリーン": uniqueId="local:0", 1280 x 720, modeId 3, defaultModeId 3, supportedModes [{id=1, width=1280, height=720, fps=50.0}, {id=2, width=1280, height=720, fps=59.9402}, {id=3, width=1280, height=720, fps=60.0}], colorMode 0, supportedColorModes [0], HdrCapabilities android.view.Display$HdrCapabilities@40f16308, density 160, 54.186 x 53.788 dpi, appVsyncOff 1000000, presDeadline 16666667, touch INTERNAL, rotation 0, type BUILT_IN, state ON, FLAG_DEFAULT_DISPLAY, FLAG_ROTATES_WITH_CONTENT, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS}
05-01 00:07:25.684  3616  3631 E AndroidRuntime: *** FATAL EXCEPTION IN SYSTEM PROCESS: ActivityManager
05-01 00:07:25.684  3616  3631 E AndroidRuntime: java.lang.IllegalArgumentException: Can't remove the primary display.
05-01 00:07:25.684  3616  3631 E AndroidRuntime:        at com.android.server.am.ActivityStackSupervisor.handleDisplayRemoved(ActivityStackSupervisor.java:4337)
05-01 00:07:25.684  3616  3631 E AndroidRuntime:        at com.android.server.am.ActivityStackSupervisor.access$300(ActivityStackSupervisor.java:193)
05-01 00:07:25.684  3616  3631 E AndroidRuntime:        at com.android.server.am.ActivityStackSupervisor$ActivityStackSupervisorHandler.handleMessage(ActivityStackSupervisor.java:4733)
05-01 00:07:25.684  3616  3631 E AndroidRuntime:        at android.os.Handler.dispatchMessage(Handler.java:106)
05-01 00:07:25.684  3616  3631 E AndroidRuntime:        at android.os.Looper.loop(Looper.java:193)
05-01 00:07:25.684  3616  3631 E AndroidRuntime:        at android.os.HandlerThread.run(HandlerThread.java:65)
05-01 00:07:25.684  3616  3631 E AndroidRuntime:        at com.android.server.ServiceThread.run(ServiceThread.java:44)
05-01 00:07:25.685  3616  3703 I InputReader: Reconfiguring input devices.  changes=0x00000004
05-01 00:07:25.694  3616  3631 I Process : Sending signal. PID: 3616 SIG: 9
05-01 00:07:26.130   200   200 I lowmemorykiller: lmkd data connection dropped
05-01 00:07:26.131   200   200 I lowmemorykiller: closing lmkd data connection
05-01 00:07:26.152  3549  3597 W AudioFlinger: power manager service died !!!
05-01 00:07:26.152  3788  3803 W Sensors : sensorservice died [0xaba4dc00]
05-01 00:07:26.155   149   149 I ServiceManager: service 'telecom' died
05-01 00:07:26.155   149   149 I ServiceManager: service 'contexthub' died
(...省略)
05-01 00:07:26.166   149   149 I ServiceManager: service 'imms' died
05-01 00:07:26.166   149   149 I ServiceManager: service 'autofill' died
05-01 00:07:26.166   149   149 I ServiceManager: service 'profile' died

DisplayがなくなったのでDisplayManagerがDisplayを削除して…するとActivityManagerが対応したディスプレイを削除…しようとしたけどPrimaryなので削除できない!!といったあとシステムがダウンしていったのでした。悲しい。

その結果、部屋の真ん中にRaspberry Piが放置されることに…

f:id:tmyt:20200501012531p:plain