Wake On LANに対応していないPCを無理やりリモート起動した話

最近、デスクトップパソコンを組みなおしました。

しっかり調べていなかったのが悪いのですが、どうやら新マザーボードWOLWake On LAN)に対応しておらず、シャットダウン(S5)状態からの復帰はおろか、スリープ状態からの復帰も色々試しましたができませんでした。

ただBIOS/UFEIのメニューを見ていると、「AC電源復帰時に電源を付ける」という機能があることがわかりました。

そのためAC電源復帰時は常にPCを起動する設定にしたうえで、スマートプラグを購入し、リモートからコンセントをオン・オフすることでリモート起動することに。

実際に購入したのはコレ

SwitchBot スマートプラグミニ(JP) | チップ3IN1、今ある家電を手軽にスマート化 – SwitchBot (スイッチボット)

 オン・オフの機能だけではなく消費電力も計測できるので、パソコンの消費電力測定とかもできてなかなか面白いです。

実際に組み込んでみて、問題なくリモート起動することができるようになりました。

ただ、PC起動中に間違ってオフにすると、いきなり電源を抜くのと同じなので注意が必要です。

そういった事故がないようにオン・オフについては、スクリプトを書いてスクリプトから行うようにしています。 APIも用意されているので簡単にスクリプトから操作可能です。

 

●起動する場合

pingでPCは起動しているか確認。起動している場合は何もしない

プラグの状態の確認、オンの場合はいったんオフにして少し待つ

プラグをオンにする

 

●シャットダウンする場合

pingでPCは起動しているか確認。起動している場合:

 リモートからPCのシャットダウンを行う(SSHでつなぎshutdownコマンド実行)

 pingがかえってこなくなるまでしばらく待つ

 念のためさらに10秒ほど10秒ほど待つ

プラグをオフにする

gitlabインストール後の502エラー

起こっていた問題

仕事で使っているgithubとかbitbucketのようなものを、趣味用に欲しい、オンプレで自宅サーバにいれたいと思ったので、gitlab-ceをインストールしてみました。(環境 Ubuntu 20.04)

Ubuntu 20.04にGitLabをインストールして構成する方法 - Tutorial Crawler

このページ等を参考にインストールしたのですが

・ページにアクセスしようとしたら502エラー(時間がたっても解消せず)

・bundleというプロセスが常時CPUを100%近く使用している状態

になってしまいうまくいきませんでした。

単純な原因だったですが、ドンピシャな情報が出てこず、究明までに時間がかったのでメモ。

原因と対策

8080ポートを別プロセスで使用していたため(自分の場合はJenkinsが使用)

Jenkinsのポートを変更することで解消しました。

 

もしくは下記ページ等を参考にrailsで使うポートを変更することでも大丈夫そうです(試してないです)。なお、今の環境ですと、unicornではなくpumaという名前のようです。

GitLab が使用する Unicorn 用のポート番号を変更する | まくまくGitノート

Jenkinsのポート変更(Ubuntu 20.04)

少しはまったのでメモ

結論

sudo systemctl start jenkins

のような形で起動している場合
/usr/lib/systemd/system/jenkins.service
を編集する必要がある

Environment="JENKINS_PORT=8080"

の部分のポートを変更する。

sudo vi /usr/lib/systemd/system/jenkins.service
sudo systemctl daemon-reload
sudo systemctl restart jenkins

ダメだった方法

/etc/default/jenkins
を編集する。
検索すると出てくる方法で、何かこれを変更すれば反映しそうな雰囲気があるファイルだが、自分の環境ではきかなかった。

Pythonのcgiで別プロセスを呼び出す際にはまったこと

やりたいこと

WEBアクセスに応じて、プロセスをたちあげて処理を開始する。
プロセスをたちあげたらレスポンスを返す(プロセスの終了を待たずにレスポンスを返す)

環境

ubuntu 20.04 server
標準で入っているApache
Python 3.8.10

うまくいかなかった例

別プロセスとして、以下のPythonスクリプトを呼び出すケースを考えます

mytask.py

import time
time.sleep(20)

以下のCGIスクリプトで処理をしようとしました。
test.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-
import subprocess

devnull = open('/dev/null', 'w')
subprocess.Popen(["python3", "mytask.py"], close_fds=True, stdout=devnull, stderr=devnull)
print("Content-Type: text/html")
print("")
print("OK")

さて、このスクリプト

python3 test.py

と実行するとすぐに処理がおわり、psコマンドでpython3 test.pyのプロセスが走っていることが確認できます。
では、これでOKかと思いきや、CGIでtest.pyを呼び出すと20秒ほど応答が返ってきません。
Apacheか何かがすべての処理が終わるのを待っているようです。

対策

色々試した結果ですが、Popenの個所を以下のようにしたら、CGI経由でも意図通りに動きました。

subprocess.Popen("nohup python3 mytask.py &", shell=True, close_fds=True, stdout=devnull, stderr=devnull)

終わりに

Google Assistant経由で、声で時間がかかる処理を開始しようとしていたのですが、
この影響で処理ははじまるものの、Google Assistantから、「~と接続できません」と言われてしまっていて悩んでおりました。
少し強引な方法ですが、スマートに動くようになってよかったです。

Actions on Googleでログインできなくなっていた話

起こっていたこと

以前、以下の記事でactions on googleを設定して、テスト環境で使用する方法を記載していたのですが、昨日久しぶりに再リンクさせようとしたら、リンクできなくなっていました。

「デバイスのセットアップ」から、「Googleと連携させる」を選ぶと、確かに目的の自分のアプリがリストアップされるのですが、選択してもログイン画面などにもいくことなく、「Could not reach [test]アプリ名. Please try again.」と表示され先に進めません。

自分の認証サーバのログを見てもGoogleからアクセスがきた形跡が一切ないので、自サーバ側の問題ではなさそうです。いろいろGoogleアカウント側の設定を変更して試してみたのですが効果がなく、結構の時間が解決まで浪費しました。

解決法

いろいろ検索していたのですが、以下に解決法が提示されていました。Home Assistantというactions on googleを利用して操作できるようにできるライブラリを使っていても、同じ問題が最近発生していたようで、その解決方法です。

Can no longer link Google Assistant - #19 by Omnipius - Configuration - Home Assistant Community

Actions on GoogleのプロジェクトのDeployの「Directory Informartion」欄を埋める必要があるようです。

f:id:shimobepapa:20220410092630p:plain

 

こちらsaveした直後はつながらないままだったのですが、再度Test実行の処理などをしているうちに、つながるようになりました。

仕様を変えたら、公式ドキュメントも変更してほしいなと切に思いました。

shimobepapa.hatenadiary.jp

ミニPCをUbuntuで無線LANルーター化

概要

メルカリで購入した中古のGK45というLANポートが2つあるミニPCに、Ubuntuをいれて無線LANルータにしてみたのでその記録

動機

NAS自宅サーバ+ルータの機能が一台でできたら機器も減って、きっと省電力のはず。拠点間のVPNとかも簡単にはれそうだし。あとルータ回りでトラブルよくわかるけど、原因自分では深く追えないし、いっそのこと自分で構築する?みたいなところからスタートしています。

ひとまずは実家のルータ+NASにと、Celeron J4105のLANポートが二つついているミニPC「Kodlix GK45」にSSDを追加して、仕上げてみました。

導入したOS

Ubuntu Server 20.04.3 LTS

完成イメージ

2つあるLANポートのうち、一つをWAN側に、もう一つをLAN側にします。
LANのアドレスは
192.168.1.0/24
Wifiの親機としても動いて、WiFiで接続してきた子機も同じLANに繋ぎます。
(よくある無線LANルータの設定)

インターフェース名の調べ方

対応するポートがなんというインターフェース名か(OSでなんという名前で扱われているか)は
ip a
と入力して調べます。LANポートの場合抜き差しすると状態がかわるので、LANポートが複数ある場合は、ポートに抜き差しして、どのインターフェースに変化があるかをしらべて、インターフェース名を特定します。

自分のGK45では下記のようなインターフェース名でしたので、以下、下記の名前で設定していきます。

  • WAN用のポートにする有線LANインターフェース:eno1
  • LAN用のポートにする有線LANインターフェース:enp3s0
  • WiFiインターフェース:wlo2

ルーターに仕立てる設定1:netplan

最近のUbuntuはネットワークの設定をするのはnetplanを使うのが流儀のようです。
netplanで設定していきます。

netplanの設定ファイルは
/etc/netplan/
にあるすべてのファイルを辞書順に読んでいくようです。

ディレクトリにあるすべてのファイルを別の場所に移し、こちらに以下の内容のファイルをつくります。

network:
        ethernets:
                eno1:
                        dhcp4: true
                enp3s0:
                        dhcp4: false
                        optional: true
        bridges:
                br0:
                        interfaces: [enp3s0]
                        addresses: [192.168.1.1/24]
                        nameservers:
                                addresses: [8.8.8.8, 8.8.4.4]
                        optional: true
        version: 2

そのうえで

sudo netplan apply

を実行します。

ここで行っているのは以下の設定です。
eno1はDHCPでアドレスを取得する。
enp3s0ではDHCPでアドレスを取得しない。
br0というブリッジを作り
 そこにenp3s0をつなげる
 IPアドレスを192.168.1.1
 DNSを8.8.8.8, 8.8.4.4

ここで注意ですが、WiFiはnetplanでは扱いません。
(最初はNetplanでやろうとしていたのですが、うまくいかずやめました)
後ほどここで作ったbr0というブリッジにWiFiもつなげます。

ルーターに仕立てる設定2:DHCPサーバ

DHCPサーバを立てます。isc-dhcp-serverを使います。
インストール

sudo apt install isc-dhcp-server

設定ファイルの記述
通常起動したときに監視するインターフェースを指定します。
編集ファイル:/etc/default/isc-dhcp-server

INTERFACESv4="br0"
INTERFACES="br0"

(INTERFACESv4だけでもいいかもしれませんが、一応念のため)

DHCPで払い出す内容の設定:/etc/dhcp/dhcpd.conf

option domain-name-servers 8.8.8.8, 8.8.4.4;
default-lease-time 600;
max-lease-time 7200;
subnet 192.168.1.0 netmask 255.2ip55.255.0 {
  option routers 192.168.1.1;
  option broadcast-address 192.168.1.255;
  option subnet-mask 255.255.255.0;
  range 192.168.1.100 192.168.1.254;
}

適用

sudo systemctl enable isc-dhcp-server
sudo systemctl start isc-dhcp-server

ルーターに仕立てる設定3:NAT

ufwを使用して設定するのが最近の推奨みたいなので、ufwで設定します。
参考:
ufwでNATとポートフォーワード - Qiita

ポートフォワード有効化
/etc/default/ufw

DEFAULT_FORWARD_POLICY=“ACCEPT"

/etc/ufw/sysctl.conf

net/ipv4/ip_forward=1

/etc/ufw/before.rules (最後に追記)

# NAT
*nat
:POSTROUTING ACCEPT [0:0]
:PREROUTING ACCEPT [0:0]
-F
-A POSTROUTING -s 192.168.1.0/24 -o eno1 -j MASQUERADE
COMMIT

(LANからきた通信を、eno1にとおします)

ufw有効化
ufwを有効化すると、基本すべてのポートの通信ができなくなるので、あらかじめ必要なポートを開放しておきます。
ここでは、22番ポート(SSH)と、LAN内からの通信はすべてOKの設定にします。

sudo ufw allow ssh
sudo ufw allow in on br0 to any

有効化

sudo ufw enable

(iptables-persistentなどをインストールしてしまっていると、再起動時にufwが無効化されるので注意(はまった))

ルータに仕立てる設定4:WiFi

hostapdを使います。
インストール

sudo apt install hostapd

設定ファイルの場所を記述
/etc/default/hostapd

DAEMON_CONF="/etc/hostapd/hostapd.conf"

設定ファイルの内容例
/etc/hostapd/hostapd.conf

interface=wlo2
bridge=br0
channel=7
logger_syslog=-1
logger_syslog_level=1
auth_algs=1
wpa=2
wpa_key_mgmt=WPA-PSK
rsn_pairwise=CCMP
ssid=myssid
wpa_passphrase=myoasswd
hw_mode=g
ieee80211n=1
ieee80211ac=0

bridge=br0
とすることで、このWiFiAPをbr0ブリッジに接続されるようになります。
また、
ieee80211n=1
ieee80211ac=1
などと書かないと、11n, 11ac通信が使えません。
hw_modeは、2.4gHz帯ならばg、5gHz帯ならばaになります。
(手元のGK45は5gHz帯に2.4gHz帯で設定していますs。

sudo enable hostapd
sudo start hostapd


ここまでで、よくある無線LANルータの設定になりました。
ここから色々VPNで他拠点とつないだりとか、NASとしての機能を設定したり、Jenkinsなどをたてて自宅サーバとして色々させてみたりとか、やりたいことがUbuntuなので簡単にネットで検索して実現できそうです。
さらにニッチな設定については機会があれば別記事で。

Webカメラを監視し、赤ちゃんが泣いたらGoogle Home (Nest)で泣き声を流す

今月、次女が誕生しました。母子とも健康でありがたい限りです。

赤ちゃんとは別の部屋に寝ているため、赤ちゃんの様子を監視するためWebカメラを導入しました。
カメラには動体検知がついてはいますが、ほぼ動かない新生児には役に立ちません。泣いたら通知が欲しいとは思いますが、音声検知がついているベビーモニターもありますが基本的にはアプリに通知のみなので、スマホの通知に気づかなかったら意味がありません。
ということで、DIYで音声検知機能を付けることにしました。

Webカメラは、
TP-LINK Tapo C200
というものを使っています。プライムセールで安かったから買ってしまったものです。
天井に吊るすように設置しています。
さて、こちらのWebカメラでは撮影している映像をRTSPでライブストリーミングしてくれます。
同じLAN内にあるサーバで、このライブストリーミングを監視して、大きな音を検知したら、その音をGoogle Homeで流す仕組みを作りました。

ライブストリーム動画からの音声の切り出し
ライブストリーム動画からffmpegを使用して、以下のコマンドで5秒間だけ音声をWAVデータで取り出します。

ffmpeg -rtsp_transport tcp -i rtsp://user:pass@192.168.1.10:554/stream2 -vn -acodec copy -t 00:00:05 ./tmp.wav

(CameraのURLを192.168.1.10としています)
これで、音声だけ、tmp.wavに抜き出せました。

wavファイルの解析
さて、このwavファイルを操作して、泣き声を検知することを考えます。
本当にしっかりやりたければ、「FFTなどで周波数ごとの音の強さを調べて、泣き声の特徴とマッチする場合・・」ということをするべきとは思います。
時間があれば、やってみたいとは思いますが、実用的には「大きな音がなっていれば泣き声と判定する」で十分でしたので、全体の音圧のみを調べることにします。
ここでも、ffmpegを使います。

ffmpeg -nostats -i tmp.wav -filter_complex ebur128 -f null /dev/null 2>&1

長い出力が出ますが、必要な音圧は

  • I -70.0 LUFS

というように表示されている部分です。そのためgrepで絞ります。

ffmpeg -nostats -i tmp.wav -filter_complex ebur128 -f null /dev/null 2>&1 | grep I:
    I:         -70.0 LUFS
[Parsed_ebur128_0 @ 0xca71c0] t: 0.0999792  M:-120.7 S:-120.7     I: -70.0 LUFS     LRA:   0.0 LU
[Parsed_ebur128_0 @ 0xca71c0] t: 0.199979   M:-120.7 S:-120.7     I: -70.0 LUFS     LRA:   0.0 LU
[Parsed_ebur128_0 @ 0xca71c0] t: 0.299979   M:-120.7 S:-120.7     I: -70.0 LUFS     LRA:   0.0 LU
[Parsed_ebur128_0 @ 0xca71c0] t: 0.399979   M: -69.4 S:-120.7     I: -69.4 LUFS     LRA:   0.0 LU
.
.
.

一定時間ごとにその区間の音圧が出力されています。
ここからの処理はシェルスクリプトではやりづらいので、Pythonに流します。

test.py

import sys
import re
result = re.findall('I:\s*([0-9\.\-]*)\s*LUFS', sys.stdin.read(), re.S)
print(result)
ffmpeg -nostats -i tmp.wav -filter_complex ebur128 -f null /dev/null 2>&1 | grep I: | python test.py

出力

['-70.0', '-70.0', '-70.0', '-70.0', '-69.4', '-69.1', '-49.9', '-45.0', '-43.3', '-42.4', '-42.0', '-42.0', '-41.9', '-41.9', '-40.4', '-38.1', '-36.7', '-35.7', '-35.2', '-35.2', '-35.3', '-35.5', '-35.5', '-35.5', '-35.5', '-35.5', '-35.5', '-35.7', '-36.0', '-36.1', '-36.2', '-36.4', '-36.5', '-36.6', '-36.8', '-36.8', '-36.8', '-36.8', '-36.8', '-36.8', '-36.8', '-36.6', '-36.4', '-36.1', '-35.9', '-35.9', '-36.0', '-36.1', '-36.1', '-36.2', '-36.1', '-36.1', '-36.2', '-36.2', '-36.3', '-36.3', '-36.3', '-36.3']

というように、音圧の変化を配列として得ることができます。
あとは、Pythonスクリプトの中でこの配列を処理して、大きな音がなっているかを判定します。
環境や検知したい音によって判定方法はいろいろあると思いますが、筆者は上記配列の最大値が-30.0以上のとき、通知をするようにしています。

音が大きいと判断したとき、Google homeで音を流す
今までの処理は、5秒間Webカメラから音を取得して、そのときの音が大きいか判定する処理です。
まず、この処理を定期的に実施する必要があります。
筆者の自宅サーバにはJenkinsが動いていたので、Jenkinsにのせ定期実行しています。(cron等でもいいと思います)
以下Jenkins上で動かす前提で進めます。

音が大きいとき、うえで取得したwavファイルをGoogle Homeで流したいのですが、どうやらwavファイルには対応していなさそうです。(ドキュメントなどを見たわけではないですがwavでは動作はしませんでした)
そのためmp3ファイルに変換します。また、wavファイルはカメラのマイクの質にもよりますが、基本的には音量が小さいので、音圧も一緒にあげます。(どのくらい音量をあげるかは環境にあわせます)
ここも同じくffmpegを使います。

ffmpeg -i "tmp.wav" -vn -af volume=15dB -ar 44100 -acodec libmp3lame -f mp3 "tmp.mp3"

このmp3ファイルを、httpでGoogle Homeからアクセスできる場所に置きます。
mp3をGoogle Homeで流すには、pychromecastを使います(pipでインストール)。

Jenkinsで実行するシェルスクリプトと合わせると、以下のような形になります。
/var/www/html以下がWebサーバで公開される前提
サーバのIPアドレス:192.168.1.50
Google Home:192.168.1.200
Tapo C200:192.168.1.10
python 3.6

rm tmp.* -f
rm /var/www/html/*.mp3 -f
ffmpeg -rtsp_transport tcp -i rtsp://user:pass@192.168.1.10:554/stream2 -vn -acodec copy -t 00:00:05 ./tmp.wav
ffmpeg -i "tmp.wav" -vn -af volume=15dB -ar 44100 -acodec libmp3lame -f mp3 "tmp.mp3"
cp tmp.mp3 
/var/www/html/tmp$BUILD_NUMBER.mp3
ffmpeg -nostats -i tmp.wav -filter_complex ebur128 -f null /dev/null 2>&1 | grep I: | python wavcheck.py tmp$BUILD_NUMBER.mp3

wavcheck.py

import sys
import re
import pychromecast

def play_googlehome(ipaddr, url):
    speaker = pychromecast.Chromecast(ipaddr)
    try:
        if not speaker.is_idle:
            speaker.quit_app()
            time.sleep(5)
        speaker.wait()
        speaker.media_controller.play_media(url, 'audio/mp3')
        speaker.media_controller.block_until_active()
    except Exception as error:
        print(str(error))

result = re.findall('I:\s*([0-9\.\-]*)\s*LUFS', sys.stdin.read(), re.S)
maxv = float(max(result, key=float))
if maxv > -40:
    play_googlehome("192.168.1.200", "http://192.168.1.50/" + sys.argv[1])

一回ごとmp3のファイル名を変えていますが、ファイル名が同じだとキャッシュがきいてしまって、前回と同じ音声が流れてしまうことがあるようでその対策のためです。

まとめ
WAVの解析の部分をもう少しこったことをするとより快適になると思います。
numpyなどを使って、Pythonで音声解析する方法もあるようです。
娘をだっこした状態でブログを書いてきましたが、そろそろしんどくなってきたのでこの辺で・・