仙石浩明の日記

2024年3月21日

突然死した Pixel4 から nanaco 残高を救い出した話

何の前触れもなく Pixel 4 が壊れた。 寝る前フツーに使っていたのに、 朝 7:00 ごろ見ると電源オフ状態になっている。 電源ボタンを長押しても何の反応もない。

単なるバッテリー切れ? と思って USB ケーブルをつないで充電してみる。 が、 画面は暗いまま。 電流を測ってみると 200mA ほど。 充電が行われているようには見えない。 もちろん USB の信号線にも何の応答もない。 「電源ボタン + ボリューム小」を長押し (強制再起動) しても無反応。

自宅の Wi-Fi ルータのログを見ると、 朝 6:46:06 に Pixel 4 から Google (*.1e100.net) に対して https アクセスをしたのが Pixel 4 からの最後の通信。 朝方までは正常に動いていたみたい。 自宅の Wi-Fi に接続しているスマホは、 その状態を監視するために、 定期的にサーバから ping を打っているが、 6:51:21 に打った ping には応答がない。 つまり起床直前、 06:46 〜 06:51 ごろ Pixel 4 が機能を停止したのだろう。

一週間ほど USB ケーブルをつないだまま放置したが、 ついに何も変化は起きなかった。 バッテリーや充電回路の問題というよりは、 ロジックボードの突然死が疑われる。 つまり分解してバッテリーを直接充電したとしても、 復活する望みは薄い。

機種変更できるアプリは、 速攻で他のスマホにインストールし直したが、 問題は おサイフケータイ。 Suica や nanaco には残高があったはず。

Suica は PC 等でモバイルSuica会員メニューサイトにログインして、 利用停止手続き を行うだけ。 10分後に新しいスマホに引き継げる。 翌朝 5:00 までは残高は 0円と表示されるが、 それ以降に確認したら残高が復活していた。

ところがセブン・カードサービス (nanaco の運営会社) は、 会員メニューサイトにログインするだけでは本人確認が充分でないと考えているらしい。 他人が (何らかの方法で窃取したパスワードを使って) 勝手にログインして利用停止手続きを行い、 残高を奪うことを恐れているのだろう。 まあ、nanaco のパスワードなんてテキトーに設定している人も多いだろうから、 その懸念は理解できなくもない。

nanaco の場合、 まず nanaco の機能停止を行った上で 「引継番号」 を発行し、 その「引継番号」を使って新しいスマホに nanacoモバイルアプリをインストールする。 ただし、 この段階では nanaco 残高は引き継がれない。 セブン・カードサービスから送られてくる 「nanaco引継申請書」に新しいスマホの nanaco番号を記入し、 本人確認書類 (マイナンバーカードなど) のコピーを添付して返送する必要がある。

この引継番号は、 機種変更の際の「引継番号」と同じものと思われるが、 機種変更の場合は残高が直ちに引継がれる点が異なる。

「引継番号」の発行までは以下のように WEB で手続きできる:

nanaco transfer 1

Suica のように会員メニューから故障 (or 紛失) したスマホの Suica の機能停止ができれば簡単なのだが、 nanaco の場合は、 氏名・生年月日・電話番号 (以下、「登録名義」と略記) を指定することで、 故障 (or 紛失) したスマホの nanaco (以下、「紛失nanaco」と略記) を特定するらしい。

つまり登録名義さえ分かれば、 他人が勝手に nanaco を機能停止できる? これって会員メニューサイトのパスワードを窃取するより簡単じゃない? 誕生日なんて facebook 等で公開している人が多いような… (ちなみに私は公開していない)

まあ、 紛失nanaco のユーザ全員が会員メニューにログインできるとは限らない (そもそもパスワードを忘れてしまっていたりする) から、 このような方式にしているのだろう。

More...
Filed under: Android — hiroaki_sengoku @ 08:31
2023年7月5日

exFAT な Ubuntu ブータブル USBメモリ (exFAT Bootable USB Flash Drive) の作り方

USBメモリや DVD などのリムーバブルメディアから起動可能な Ubuntu (以下 「Live Ubuntu」 と呼ぶ) は、 PC がトラブったときのレスキュー (障害復旧) の道具として重宝する。 最近の USBメモリは容量が大きく、 3GB 程度の Live Ubuntu を入れておいても大して邪魔にならない。 ふだん持ち歩く USBメモリにも、 それぞれ Live Ubuntu を入れておくとイザというとき便利。

ところが、 Ubuntu (公式) が公開している Live Ubuntu は exFAT からの起動に対応していない。 exFAT は FAT の後継として Microsoft が開発したファイルシステムで、 従来 4GB までのファイルしか扱うことができなかった FAT の制約が大幅に緩和されている。

昨今の動画ファイルはサイズが 4GB を超えるものも多く、 USBメモリは FAT ではなく exFAT でフォーマットしたい。 もちろん、 NTFS でフォーマットすれば大きなファイルを入れられるし、 Live Ubuntu も起動できるが、 NTFS は USBメモリには牛刀すぎる。

USBメモリは様々な機器に挿す可能性があるわけで、 NTFS にするのは躊躇してしまう。 スマホやラズパイ、 さらにはコンビニ等のプリント機など、 その全てで NTFS が問題無く使えるのだろうか? やっぱりデフォルトである exFAT のほうが安心。

Ubuntu などのインストールメディアの ISO イメージを exFAT に置いて起動する方法 (Make an exFAT Bootable USB Flash Drive) が既に公開されているが、 仕組み (Ventoy) が複雑だし、 そもそも ISO イメージを loopback デバイス経由でマウントして起動すると重くなるし、 必要なメモリ量も多いので、 利用したいとは思わなかった。 トラブった PC のスペックが低い場合など、 道具は軽ければ軽いほど好ましい。

というわけで、 exFAT でフォーマットした USBメモリに、 Live Ubuntu を入れる方法を考えてみた。 要は Live Ubuntu の initrd ファイル (cpio アーカイブ) をどう改変して exFAT に対応させるか?

基本的には Live Ubuntu の initrd を展開して exFAT に対応させる修正を行なった後、 cpio コマンドを使って initrd を作り直せばいいのだが、 initrd は複数の cpio アーカイブをつなげた形になっていて、 作り方によっては互換性の問題が起きるかも? Live Ubuntu 自身の update-initramfs コマンドを使って initrd を更新したほうが手軽だし確実。

なお、 (PC の起動に必要な) UEFI パーティションは (いまのところ) FAT でフォーマットしておいた方が無難と思われる。 ほとんど (全て?) の PC が exFAT や NTFS でフォーマットした UEFI パーティションを認識するらしいが、 最大限の互換性を求めるなら FAT にしておくべきだろう。

というわけで、 USBメモリの末尾 4MB (以下の実行例では sdc2) だけ FAT でフォーマットして UEFI パーティションとし、 残り (sdc1, ここでは 「boot パーティション」と呼ぶ) を exFAT でフォーマットする:

senri:/ # gdisk -l /dev/sdc
GPT fdisk (gdisk) version 0.8.4

Partition table scan:
  MBR: protective
  BSD: not present
  APM: not present
  GPT: present

Found valid GPT with protective MBR; using GPT.
Disk /dev/sdc: 121098240 sectors, 57.7 GiB
Logical sector size: 512 bytes
Disk identifier (GUID): 619FAAB5-5325-4404-99C4-F0541D53B069
Partition table holds up to 128 entries
First usable sector is 34, last usable sector is 121098206
Partitions will be aligned on 2-sector boundaries
Total free space is 0 sectors (0 bytes)

Number  Start (sector)    End (sector)  Size       Code  Name
   1            2048       121090047   57.7 GiB    0700  Microsoft basic data
   2       121090048       121098206   4.0 MiB     EF00  EFI system partition
   3              34            2047   1007.0 KiB  EF02  BIOS boot partition
senri:/ # mkfs -t fat /dev/sdc2
mkfs.fat 4.0 (2016-05-06)
senri:/ # mkfs -t exfat /dev/sdc1
mkexfatfs 1.2.3
Creating... done.
Flushing... done.
File system created successfully.
senri:/ # 

UEFI パーティションは Windows ユーザ (あるいはコンビニのプリント機) からは見えないので、 boot パーティションのみを、 ふつうの USBメモリとして使うことになる。

次に grub-install コマンドで UEFI パーティションに GRUB (ブートローダ) をインストールする。 ついでに UEFI に対応していない PC (は滅多にないと思うが) でも起動できるように、 「--target i386-pc」オプションを使って MBR にも GRUB をインストールしておく。

senri:/ # mount /dev/sdc1 /mnt/usb
senri:/ # mount /dev/sdc2 /mnt/efi
senri:/ # grub-install --target x86_64-efi --efi-directory /mnt/efi --boot-directory=/mnt/usb/boot --removable
Installing for x86_64-efi platform.
Installation finished. No error reported.
senri:/ # grub-install --target i386-pc --boot-directory=/mnt/usb/boot --removable /dev/sdc
Installing for i386-pc platform.
Installation finished. No error reported.
senri:/ # blkid | grep sdc1
/dev/sdc1: UUID="E7F3-77CD" BLOCK_SIZE="512" TYPE="exfat" PTTYPE="dos" PARTLABEL="Microsoft basic data" PARTUUID="23041859-cc53-4164-be6a-44af6a966e5d"
senri:/ # 

UEFI パーティションに書込まれるのは GRUB コア (/EFI/BOOT/BOOTX64.EFI) のみで 124KB しかない。 UEFI パーティションは 4MB も要らないかも? GRUB 本体 (/boot/grub/x86_64-efi ディレクトリ) は boot パーティションに書込まれる。 /EFI/BOOT/BOOTX64.EFI は /boot/grub/x86_64-efi/core.efi の内容と同じ。

GRUB の設定ファイル /boot/grub/grub.cfg は ↓ こんな感じ:

set uuid="E7F3-77CD"
insmod all_video

menuentry "ubuntu 22.04.2 desktop amd64" {
  linux /casper/vmlinuz boot=casper uuid=$uuid
  initrd /casper/initrd
}

ここで "E7F3-77CD" は USBメモリの exFAT パーティション /dev/sdc1 の UUID (Universally Unique Identifier, 汎用一意識別子)。 blkid コマンドなどで調べることができる (上記 grub-install の実行例の末尾を参照)。

/casper ディレクトリは、 Ubuntu DVD (以下の実行例では /cdrom にマウントしている) からコピーする:

senri:/ # cp -a /cdrom/casper /mnt/usb/
senri:/ # ls -la /mnt/usb/casper
total 2813952
drwxr-xr-x 2 root root      32768 Feb 23 13:13 .
drwxr-xr-x 4 root root      32768 Jul  3 18:10 ..
-rwxr-xr-x 1 root root      59931 Feb 23 13:09 filesystem.manifest
-rwxr-xr-x 1 root root       2885 Feb 23 13:09 filesystem.manifest-minimal-remove
-rwxr-xr-x 1 root root       3578 Feb 23 13:09 filesystem.manifest-remove
-rwxr-xr-x 1 root root         11 Feb 23 13:09 filesystem.size
-rwxr-xr-x 1 root root 2731876352 Feb 23 13:09 filesystem.squashfs
-rwxr-xr-x 1 root root        833 Feb 23 13:12 filesystem.squashfs.gpg
-rwxr-xr-x 1 root root  137120699 Feb 23 13:09 initrd
-rwxr-xr-x 1 root root   12186376 Feb 23 13:09 vmlinuz
senri:/ # 

filesystem.squashfs が Live Ubuntu の root ファイルシステム。 これ以外の filesystem.* は不要なので削除して構わない。 vmlinuz が Linux カーネルで、 initrd がこれから書き換える initrd ファイル。

boot パーティションが FAT 等であれば、 この状態でブータブル USBメモリとして機能するが、 exFAT だと initrd が /casper/filesystem.squashfs を見つけられず Ubuntu を起動できない。

そこで initrd の内容を書き換えて exFAT を扱えるようにする。 といっても Linux Kernel 5.4 以降は (以前の exfat-fuse ではなく) カーネルレベルで exFAT を扱うことができる。 つまり必要なのはカーネルモジュール kernel/fs/exfat/exfat.ko を initrd へ追加することだけ。

まず Ubuntu DVD を用いて Live Ubuntu を起動する (インストール DVD から起動して「Ubuntu を試す」を選択)。 /etc/initramfs-tools/modules および /usr/share/initramfs-tools/scripts/casper-helpers に以下のパッチ ubuntu.patch をあてる:

--- etc/initramfs-tools/modules~	2023-02-23 12:59:33.000000000 +0900
+++ etc/initramfs-tools/modules	2023-07-03 08:46:22.000000000 +0900
@@ -9,3 +9,4 @@
 #
 # raid1
 # sd_mod
+exfat
--- usr/share/initramfs-tools/scripts/casper-helpers~	2022-05-30 23:40:38.000000000 +0900
+++ usr/share/initramfs-tools/scripts/casper-helpers	2023-07-03 08:48:03.000000000 +0900
@@ -36,7 +36,7 @@
     # FIXME: do something better like the scan of supported filesystems
     fstype="${1}"
     case ${fstype} in
-        vfat|iso9660|udf|ext2|ext3|ext4|btrfs|ntfs)
+        vfat|exfat|iso9660|udf|ext2|ext3|ext4|btrfs|ntfs)
             return 0
             ;;
     esac
@@ -234,7 +234,7 @@
             # will cause data loss when a live CD is booted on a system
             # where filesystems are in use by hibernated operating systems.
             case "$(get_fstype ${devname})" in
-                vfat)
+                vfat|exfat)
                     :;;
                 *)
                     continue;;
@@ -337,7 +337,7 @@
         for dev in $(subdevices "${sysblock}"); do
             devname=$(sys2dev "${dev}")
             case "$(get_fstype ${devname})" in
-                vfat|ext2)
+                vfat|exfat|ext2)
                     :;;
                 *)
                     continue;;
@@ -367,7 +367,7 @@
 is_supported_fs(){
     [ -z "${1}" ] && return 1
     case ${1} in
-        ext2|ext3|ext4|xfs|jfs|reiserfs|vfat|ntfs|iso9660|btrfs)
+        ext2|ext3|ext4|xfs|jfs|reiserfs|vfat|exfat|ntfs|iso9660|btrfs)
             return 0
             ;;
     esac
@@ -388,6 +388,7 @@
     modprobe xfs
     modprobe jfs
     modprobe vfat
+    modprobe exfat
     modprobe fuse
     [ "$quiet" != "y" ] && log_end_msg "...devs loaded..."
     touch /dev/.initramfs/lupin-waited-for-devs

この ↑ パッチでは、 起動時に (つまり initramfs で) 必要なカーネルモジュールを指定するファイル /etc/initramfs-tools/modules に 「exfat」 を追記している。 また、 initramfs 内のスクリプト 「/usr/share/initramfs-tools/scripts/casper-helpers」 を boot パーティションが exFAT でもエラーにならないよう修正している。

というか、 たったこれだけの修正で exFAT から起動できるのだから、 公式の Live Ubuntu で exFAT からの起動をサポートして欲しい。 exFAT を除外する理由でもあるのだろうか?

そして update-initramfs.distrib コマンドを使って新しい initrd を生成する。 ここで (通常の Ubuntu と同じ感覚で) update-initramfs を使ってしまうと 「update-initramfs is disabled since running on read-only media」 と言われてしまうので注意。 「read-only media」 だからダメなのではなく、 Live Ubuntu の update-initramfs は、 このメッセージを出力するだけの sh スクリプトに置き換えられている (いったい何のために?)。

例えば Live Ubuntu の Terminal を使って以下のように実行する:

root@ubuntu:/# wget https://www.gcd.org/sengoku/docs/ubuntu-22.04.2-desktop-amd64.patch
--2023-07-04 01:47:50--  https://www.gcd.org/sengoku/docs/ubuntu-22.04.2-desktop-amd64.patch
Resolving www.gcd.org (www.gcd.org)... 71.19.146.203, 74.207.241.21, 219.94.252.139, ...
Connecting to www.gcd.org (www.gcd.org)|71.19.146.203|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1799 (1.8K) [text/plain]
Saving to: ‘ubuntu-22.04.2-desktop-amd64.patch’

2023-07-04 01:47:57 (38.1 MB/s) - ‘ubuntu-22.04.2-desktop-amd64.patch’ saved [1799/1799]

root@ubuntu:/# patch -p0 < ubuntu-22.04.2-desktop-amd64.patch
patching file etc/initramfs-tools/modules
patching file usr/share/initramfs-tools/scripts/casper-helpers
root@ubuntu:/# uname -a
Linux ubuntu 5.19.0-32-generic #33~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Mon Jan 30 17:03:34 UTC 2 x86_64 x86_64 x86_64 GNU/Linux
root@ubuntu:/# update-initramfs.distrib -c -k 5.19.0-32-generic
update-initramfs: Generating /boot/initrd.img-5.19.0-32-generic
cryptsetup: ERROR: Couldn't resolve device /cow
cryptsetup: WARNING: Couldn't determine root device
W: Couldn't identify type of root file system for fsck hook
root@ubuntu:/# 

生成された /boot/initrd.img-5.19.0-32-generic を USBメモリの boot パーティションの /casper/initrd へコピーする。

以上で、 Ubuntu が起動できる exFAT な USBメモリができた。 ふだんは普通の USBメモリとして使える。 /casper/filesystem.squashfs は 2605MB もあるが、 いつでも Ubuntu DVD からコピーすることで元に戻せるので、 USBメモリに空きがないときは気軽に削除して構わない。

Filed under: システム構築・運用 — hiroaki_sengoku @ 07:55
2023年6月12日

Wio Node (ESP8266) に MicroPython をインストールして、Wi-Fi 照度&人感センサを作ってみた

市販されている IoT 機器は、 いつサービス終了してしまう (クラウドサーバ停止で使えなくなる) かも知れず、 そのたびに API を解析するのは現実的ではないなぁと思っていたところ、 MicroPython (日本語) を使えば手軽に IoT 機器を自作できることに気付いた。

例えば部屋の明るさを Wi-Fi 経由でサーバへ通知する照度センサは、 こんな感じで書ける:

import machine
import socket
import network_cfg

pwr = machine.Pin(15, machine.Pin.OUT)
pwr.value(1)

def webhook(v):
    addr = socket.getaddrinfo('gt.gcd.org', 12345)[0][-1]
    s = socket.socket()
    s.connect(addr)
    s.send('{"magic":"'+network_cfg.MAGIC+'","type":"Illuminance", "Darkness":"'+str(v)+'", "DeviceName":"wionode"}\n')
    s.close()

adc = machine.ADC(0)

def illumi(p):
    darkness = adc.read()
    try:
        webhook(darkness)
    except OSError as exc:
        print("exception\n")

tim = machine.Timer(-1)
tim.init(period=60000, callback=illumi)

webhook(v) はサーバ (この例では gt.gcd.org のポート 12345番。 LAN 内のサーバなので外部からはアクセスできない) へ通知する関数。 「adc.read()」で照度センサの値 (0 〜 1024) を読み取って、 webhook(v) でサーバへ通知するだけ。 タイマを使って 60秒ごとに illumi 関数を実行して、 その時点の照度をサーバへ通知するようにしてみた。

C などのコンパイル型言語と違って、 Python だから対話的にコード片を実行してみることができて (REPL)、 とりあえず動くプログラムを書くだけなら、 あっと言う間に書ける。 Arduino のスケッチみたいに、 いちいち 「コンパイルしてボードへ書込んで実行」 を何度も何度も繰り返すのと比べると、 REPL は圧倒的に楽。 IoT 機器だと実行速度はさほど要求されない (ことが多い) わけで、 REPL 一択だと思う。

いわゆる 「ラズパイ電子工作」 だとブレッドボードで配線するのが定番で、 ブレッドボードだから配線を手軽にできるのはいいのだけど、 IoT 機器として実地に使おうとすると電子部品が剥き出しのままでは困るし、 ケースにいれると大きすぎて設置方法に困る。

WioNode + PIR & Light Sensor

その点、 Wio Node (Seeed Studio 102110057) は、 極めてコンパクトなのに Grove コネクタが 2個ついているので、 Grove シリーズと互換なセンサ類をつなぐだけ。 電子部品の出っ張りとかが無いので、 ケースにいれなくてもそのまま使えそう (というか実際、このままで実地に使っている)。

Wio Node の 2個の Grove コネクタに、 照度センサと人感センサをつないでみた。 写真中央左寄りが M5Stack用 光センサユニット (517円) で、 写真右上が HC-SR312 (AM312) 人体検知センサ (1個 166円)。 Wio Node (写真下) は私が買ったとき (2021年11月) は 1250円だったのに、 急に値上がりして今は 1700円くらい。

値上がりしてしまったとはいえ、 合計 2400円くらいで Wi-Fi 通信する照度&人感センサが作れてしまう。 いつサービス終了してしまうか恐れながら市販の IoT 機器を使うよりずっといい。

Grove コネクタは 黄 白 赤 黒 の 4ピン。 黄と白は信号線で、 赤は電源 (5V or 3.3V)、黒は GND。 Seeed studio オリジナルの Grove ケーブルは 1番ピンが黄色 (写真左側) だけど、 M5Stack などの互換ケーブルだと 1番ピンが白色で 白 黄 赤 黒 の順番 (写真右側)。

オリジナルと互換ケーブルとでは黄と白が入れ替わっているが、 違うのは色だけで互換性は配慮されている (と思う)。 例えば、 アナログ出力の Grove センサのほとんど (全て?) は、 1番ピンがアナログ信号線となっている。

3番ピン(赤) は、 Wio Node の場合 3.3V が供給されるが、 M5Stack や Arduino だと 5V のものが多いので注意を要する。 例えば人感センサの定番 HC-SR501 (1個 136円) の電源電圧は 5V 〜 20V なので Wio Node の Grove コネクタから電源を供給することはできない。

More...
2022年5月11日

WSL2 (Windows Subsystem for Linux 2) で物理ディスク上の独自 OS を動かしてみた

Windows で VMware Workstation Player を使って Linux ベースの独自 OS (以下 GCD OS と呼ぶ) を動かしていたのだけど、 WSL2 (Windows Subsystem for Linux 2) が物理ディスクをマウントできるようになったと聞き、 WSL2 が VMware の代りに使えるか試してみた。 ただし、物理ディスクをマウントするには Windows 11 ビルド 22000 以上が必要。

本当は Linux カーネルも自前でビルドしたものを使いたいが、 とりあえず WSL2 のカーネルをそのまま使い、 root ファイルシステムは物理ディスク上にインストールしてある GCD OS を使う方向で考えた。

以前、 OpenVZ な VPS サービス上で GCD OS を動かしたことがある (10年前!) ので、 その時と同様に chroot して物理ディスク上の GCD OS を起動するのが簡単そう。 つまり、 以下のシェルスクリプト gcd.sh を WSL2 上で実行するだけ:

#!/bin/sh
PATH="/mnt/c/WINDOWS/System32:/usr/bin:/bin:/usr/sbin:/sbin"
ROOT="/usr/local/GCD"

cd /mnt/c
wsl.exe --mount '\\.\PHYSICALDRIVE1' --bare

test -d $ROOT || mkdir $ROOT
mount LABEL=/ $ROOT

$ROOT/etc/init.d/chroot

このスクリプトは WSL2 の仮想ディスク (Virtual Hard Disk) 内に置いてもよいが、 私は Windows の C: ドライブに置いた。 WSL2 の内容を変更する必要がなくなるし、 WSL2 でどのディストリビューションを使っているかに依存しなくなる。 仮想ディスクが肥大化したら、インストールし直して初期状態に戻してもよい。

例えば C:\bin\gcd.sh に置いて、 次のように実行する:

C:\Windows\System32\wsl.exe -u root -- /mnt/c/bin/gcd.sh

タスクスケジューラーで Windows の起動時に自動的に実行すれば、 Windows にログインしなくても外部から ssh で GCD OS へログインできるので便利。 VMware より軽くて手軽で便利かも?

以下、このスクリプト gcd.sh を順に説明する:

まず、 「wsl.exe --mount '\\.\PHYSICALDRIVE1' --bare」 で WSL2 から物理ディスク 「PHYSICALDRIVE1」 が見えるようにする。 wsl.exe は Windows のコマンドだが、 WSL2 は Linux なのに /mnt/c にマウントされた C: ドライブ上の Windows のコマンドが実行できる (Linux カーネルの binfmt_misc を使っている)。

(Linux の) lsblk コマンドを使うと、 物理ディスクが見えることが確認できる:

Linux 5.10.16.3-microsoft-standard-WSL2.
kayano:~ $ lsblk
NAME   MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
sda      8:0    0   256G  0 disk
sdb      8:16   0 339.7M  1 disk
sdc      8:32   0   256G  0 disk /mnt/virtual
sdd      8:48   0   7.3T  0 disk
├─sdd5   8:53   0   200G  0 part 
├─sdd6   8:54   0    16G  0 part [SWAP]
└─sdd7   8:55   0   6.8T  0 part /

sdd が WSL2 から見えるようになった物理ディスク (8TB HDD)。 パーティションが 3つあることが分かる。 うち sdd7 が GCD OS の root ファイルシステム。

一点注意すべきなのは、 Windows のシステムドライブ 「PHYSICALDRIVE0」 は指定できないので、 システムドライブとは異なる物理ディスクを使う必要があるという点。 8TB HDD など大容量のハードディスクを、 パーティションで区切って Windows と Linux の両方をインストールしている人は多いと思うのだけど、 残念ながら Windows と同じディスク上にある Linux パーティションは、 WSL2 から使うことはできない。 VMware ならできるのに...

仕方がないので私は (わざわざ) M.2 NVMe SSD を買って Windows のシステムドライブを NVMe へ移し、 SATA0 につないだ 8TB HDD を (Windows をインストールしていたパーティション 1〜4 を削除して) Linux 専用にした。

また、WSL2 上で wsl.exe を実行するとき、 カレントディレクトリが WSL2 の仮想ディスクだと、 どんな引数でも常に 「Invalid argument」 エラーになる:

root@kayano:~# /mnt/c/Windows/System32/wsl.exe --shutdown
/mnt/c/Windows/System32/wsl.exe: Invalid argument
                                                 root@kayano:~#

エラー出力後の改行がうまくいかないあたり、 いかにもバグっぽい?

カレントディレクトリが C: ドライブだと正常に実行できるようなので、 wsl.exe を実行する前に 「cd /mnt/c」 を行っている。

物理ディスクが WSL2 で見えるようになったら、 次にこの物理ディスクをマウントする: 「mount LABEL=/ $ROOT」。 ここでは LABEL が 「/」 のパーティションを 「$ROOT」 つまり /usr/local/GCD へマウントしている。

あとは GCD OS を起動するだけ: 「$ROOT/etc/init.d/chroot」。 /etc/init.d/chrootOpenVZ 上で GCD OS を起動するときも使った、 以下のシェルスクリプト:

#!/bin/sh
root=`echo $0 | sed -e 's@/etc/init.d/chroot$@@'`
if [ ! -d $root ]; then
   echo "Can't find root: $root"
   exit 1
fi
sed -n 's/^[a-z][_a-z]* \([^ ][^ ]*\) .*/\1/p' < /proc/mounts | while read d; do
    if [ -d "$d" ]; then
	test -d "$root$d" || mkdir "$root$d"
	mount -obind "$d" "$root$d"
    elif [ -f "$d" ]; then
	test -f "$root$d" || touch "$root$d"
	mount -obind "$d" "$root$d"
    fi
done
if [ -d /lib/modules/`uname -r` ]; then
    mount -obind /lib/modules $root/boot/lib/modules
fi
chroot $root /bin/sh <<EOF
swapon -a
mount -a -t ext4
/etc/init.d/svscanboot &
/etc/rc.d/rc.M
EOF

このシェルスクリプトは、 まず WSL2 の /proc/mounts を参照して、 WSL2 がマウントしているディレクトリとファイルを、 そのまま GCD OS のルート (/usr/local/GCD) へマウントする。 これで GCD OS でも /mnt/c にマウントされた Windows コマンドを実行できるようになる。

次に 「chroot /usr/local/GCD /bin/sh」 を実行して、 chroot 環境下で svscanboot/etc/rc.d/rc.M を実行する。 svscanboot は daemontools の起動スクリプト。 GCD OS のほとんどのデーモン類は daemontools の管理下で起動される。 一方 /etc/rc.d/rc.M は、 GCD OS のブートスクリプトで、 ネットワーク等の各種設定と、 一部のデーモン類の起動を行なう。

ssh サーバや WWW サーバも /etc/rc.d/rc.M が立ち上げるが、 いままで WSL2 のネットワークは Windows 内でしか見えないバーチャルネットワーク (内部ネットワーク) だったらしい。 「WSL2 外部から接続」 などとググっても、 外部から WSL2 の ssh サーバにログインするには (netsh.exe の) portproxy を使え、という話ばかり出てくる。

Hyper-V Virtual Switch

どのバージョンから可能になったのかは知らないが、 「仮想スイッチ マネージャー」 で設定すれば VMware のようなブリッジモードが WSL2 でも使えるようになる。

まず Windows 管理ツールの 「Hyper-V マネージャー」 を実行する。 仮想マシン (この例では KAYANO) を選択し、 「操作(A)」 メニューから 「仮想スイッチ マネージャー(C)...」 を選ぶと、 「仮想スイッチ マネージャー」のウィンドウが開く。

左ペイン 「仮想スイッチ」 の中から 「WSL」 を選び、 右ペインに表示される 「接続の種類」 として 「外部ネットワーク(E):」を選択し、 適切なネットワーク アダプターを選択する。

これで WSL2 が外部ネットワークと通信できるようになる。 (いまのところ) タグVLAN が使えるようにはできていないが、 VMware ではタグVLAN が使えるので、 なんとかしてタグVLAN が使えるようにしたいところ...

他のマシン (以下の例では senri) から ssh で GCD OS (kayano) へログインしてみる:

senri:/ # ssh kayano
Last login: Wed May 11 04:16:34 2022 from senri.gcd.org
Linux 5.10.16.3-microsoft-standard-WSL2.
kayano:~ # ip addr show dev eth0
6: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
    link/ether 00:15:5d:ff:11:b1 brd ff:ff:ff:ff:ff:ff
    inet 192.168.18.40/24 brd 192.168.18.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::215:5dff:feff:11b1/64 scope link
       valid_lft forever preferred_lft forever
kayano:~ # tcpspray senri
Transmitted 102400 bytes in 0.000315 seconds (317460.317 kbytes/s)
kayano:~ # free
             total       used       free     shared    buffers     cached
Mem:       8055704     573892    7481812          0      30704     178088
-/+ buffers/cache:     365100    7690604
Swap:     18874364          0   18874364
kayano:~ # chroot_escape /bin/bash
groups: cannot find name for group ID 11
groups: cannot find name for group ID 14
root@kayano:/# lsb_release -d
Description:    Ubuntu 20.04.4 LTS
root@kayano:/# exit
kayano:~ #

chroot 環境から脱出 (chroot_escape /bin/bash) すると WSL2 の Ubuntu 環境に戻る。 で、exit すると GCD OS に戻る。

この GCD OS で物理ディスクの読み書き速度を測ってみた:

kayano:/ # hdparm -t /dev/sdd7

/dev/sdd7:
 Timing buffered disk reads:  538 MB in  3.01 seconds = 178.93 MB/sec

kayano:/ # dd if=/dev/zero of=/tmp/test bs=1024k count=10240
10240+0 records in
10240+0 records out
10737418240 bytes (11 GB, 10 GiB) copied, 60.5833 s, 177 MB/s

読み書きともに 177MB/sec くらい。 Core i5-8500 マシンだとこんなもの? 同じ PC (同じ Windows) で VMware 上の同じ GCD OS でも測ってみると、 読込みが 180.62 MB/sec で書き込みが 132 MB/s だった。 簡易な測定なので、ほぼ同等と言っていいと思う。

ちなみに仮想化なしで直接このマシン上で測ると、 読込み 178.65 MB/sec 書込み 233 MB/s なので、 仮想マシンによるオーバーヘッドは多少あるようだ。 とはいえ AMD FX-4100 マシンとかだと読込み 174.82 MB/sec 書込み 95.3 MB/s なので、 実用上は 177MB/sec もあれば充分?

AMD FX とかの 10年前のマシンだと CPU がボトルネックになってる感じ。そろそろ 「Sandyで十分おじさん」 は卒業すべき?

ネットワークの速度も測ってみた:

kayano:/ # iperf3 -c esaka
Connecting to host esaka, port 5201
[  5] local 172.17.14.235 port 35504 connected to 192.168.18.20 port 5201
[ ID] Interval           Transfer     Bitrate         Retr  Cwnd
[  5]   0.00-1.00   sec   116 MBytes   970 Mbits/sec    0   3.01 MBytes       
[  5]   1.00-2.00   sec   111 MBytes   933 Mbits/sec    0   3.01 MBytes       
[  5]   2.00-3.00   sec   112 MBytes   944 Mbits/sec    0   3.01 MBytes       
[  5]   3.00-4.00   sec   112 MBytes   944 Mbits/sec    0   3.01 MBytes       
[  5]   4.00-5.00   sec   112 MBytes   944 Mbits/sec    0   3.01 MBytes       
[  5]   5.00-6.00   sec   111 MBytes   933 Mbits/sec    0   3.01 MBytes       
[  5]   6.00-7.00   sec   112 MBytes   944 Mbits/sec    0   3.01 MBytes       
[  5]   7.00-8.00   sec   112 MBytes   944 Mbits/sec    0   3.01 MBytes       
[  5]   8.00-9.00   sec   112 MBytes   944 Mbits/sec    0   3.01 MBytes       
[  5]   9.00-10.00  sec   111 MBytes   933 Mbits/sec    0   3.01 MBytes       
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-10.00  sec  1.10 GBytes   943 Mbits/sec    0             sender
[  5]   0.00-10.05  sec  1.10 GBytes   938 Mbits/sec                  receiver

iperf Done.

1Gbits/sec の LAN なので、ほぼ上限の速度が出ている。 同条件で VMware でも測ってみると、 送信が 928 Mbits/sec で受信が 925 Mbits/sec だった。 仮想化なしでも、 送信が 943 Mbits/sec 受信が 941 Mbits/sec なのでほとんど同じ。

というわけで、 速度的にはディスクもネットワークも問題無さそう。

More...
Filed under: システム構築・運用 — hiroaki_sengoku @ 09:37
2022年3月9日

所得税を分割して、複数の高還元率クレジットカードを何回も使って納付してみた hatena_b

所得税の納付は、 確定申告書等作成コーナー経由だと全額一括払いになってしまうが、 国税クレジットカードお支払サイトを直接アクセスすることで、 任意に分割して支払うことができる。

この「お支払サイト」は、 氏名、住所、電話番号、納付先税務署、納付税目 (申告所得税及復興特別所得税) を入力した上で、 クレジットカードで任意の金額を支払う仕組みになっている。 それぞれの納税者が合計いくら納付済みか集計するには、 納税者と一対一に対応する ID (識別番号) が必要だが、 ID の入力は求められない。 つまり支払うだけの納付専用サイト。

e-Tax を使って確定申告を行った納税者なら e-Tax の 「利用者識別番号」が ID になるが、 e-Tax を使わなくても確定申告はできるわけで、 「お支払サイト」としては ID 無しで納付できる仕組みにするしかないのだろう。

この仕組みだと税務署側で、 「お支払サイト」 から送られてきた納付データと、 確定申告のデータを突合する作業が必要になる。 やっぱり手作業? DX への道は遥か遠い。 個人番号 (マイナンバー) を ID として入力させればいいのにねぇ...

ID とは言えないものの各税務署が「整理番号」を発行していて、 この「お支払サイト」でも「整理番号」を入力することができる。 整理番号の入力は必須ではないが、 入力しておくことで税務署の人たちの突合の手間を減らし、 間違いが起きるリスクを減らすことができるだろうから、 自分の「整理番号」を調べて入力したほうがヨサゲ。

Tax Payment Statement

整理番号は、 e-Tax のメッセージボックスに届く 「確定申告等についてのお知らせ」に記載されている他、 毎年 1月下旬に郵送で届く「確定申告のお知らせ」 「確定申告の納付書」や、 還付金がある場合は「国税還付金振込通知書」にも記載されている。

所得税をクレジットカードで支払う場合、 税額 1万円につき決済手数料 76円+消費税がかかる。 例えば所得税が 99万1円〜100万円の範囲なら税込 8,360円 (つまり 0.84%) もの決済手数料を払う必要があるわけで、 決済手数料より高いポイント等がもらえる高還元率クレジットカード (還元率が 1% 以上) でないと意味がない。

ところが高還元率クレジットカードは、 ポイント等の還元率が高いいっぽう還元を受けられる上限額が低いことが多い。 例えば「Visa LINE Pay クレジットカード」(以下「LINEカード」と略記) は、 支払額の 2% の LINE ポイントがもらえるが、 税金/保険において、1回あたりの支払につき 5万円を超える分は、 ポイント還元の対象外となる。

また、ソニー銀行が発行する「Sony Bank WALLET デビット付きキャッシュカード」 (以下「Sonyカード」と略記) は、 Club S のステータスに応じて最大 2% のキャッシュバックがもらえるが、 寄付、納税、公共料金などの支払ではキャッシュバック合計額が最大 1万円/月までとなる。

どちらの高還元率カードも、 所得税を分割して納付することで上限額を超えてしまうことを回避できる。 LINEカードの場合は 1回 5万円ずつ複数回に分けて払えば 2% のポイントが得られるし、 Sonyカードの場合は (確定申告を 2月に行えば) 50万円ずつ 2月と 3月の 2回に分けて支払うことで各月 2% 満額 1万円のキャッシュバックが得られる。

例えば 140万円の納税を行う場合なら、 2月と 3月に Sonyカードで 50万円ずつ納付し、 3/15 までに LINEカードで 5万円ずつ 8回納付すれば良い。

実際の納付では決済手数料がかかるので、 Sonyカードでは納付額 495,820円と決済手数料 4,180円を、 LINEカードでは納付額 49,582円と決済手数料 418円を払うことになる。 ここで注意すべきなのは決済手数料が 1万円単位で、 1万円に満たない端数は切り上げられるという点。 分割の仕方によっては余計な決済手数料を払うことになってしまう。

例えば所得税額が 12万300円だった場合、 一括で納付すれば決済手数料は税込 1,086円 (=13*76*1.1) だが、 3等分して 4万100円ずつ納付すると決済手数料は税込 1,254円 (5*76*1.1 = 418円ずつ 3回) になってしまう。 3回に分けて納付したときの決済手数料を一括納付の場合と同額にするには、 まず 4万300円を納付 (決済手数料 5*76*1.1 = 418円) し、 残り 2回は 4万円ずつ納付 (決済手数料は 4*76*1.1 = 334円ずつ) すればよい。

というわけで、 所得税 1,390,400円を 9回に分けて 5枚のクレジットカードで納付してみた。 決済手数料は計 11,702円なのに対し、 キャッシュバックやポイント等で計 28,241円の還元を得た。 差し引き 16,539円の利益。

納付日税額手数料支払額カード還元額
2022-02-19495,820円4,180円500,000円Sony10,000円
2022-02-1949,582円418円50,000円LINE1,000円
2022-02-2479,996円668円80,664円Diners403円
2022-02-2540,000円334円40,334円LINE806円
2022-03-0240,000円334円40,334円LINE806円
2022-03-03100,000円836円100,836円SMBC NL3,025円
2022-03-0749,182円418円49,600円LINE992円
2022-03-08495,820円4,180円500,000円Sony10,000円
2022-03-0840,000円334円40,334円Toyota1,209円
合計1,390,400円11,702円1,402,102円28,241円

LINEカードでの支払は、 同じ納税を何度も繰り返すとポイント付与を止められるかも?と思って、 ポイント付与を確認して (さらに LINE証券へ送金して) から次の支払を行った。 幸い今回は 2% 満額の LINE ポイントを得たが、 分割納付する人が増えるとポイント付与ルールが改訂されるかもしれない。 もっとも、2% 還元自体が 2022年4月30日までであり、 5月以降どうなるか不明。

「三井住友カード ゴールド(NL)」(上表では「SMBC NL」と略記) は、 「最大30,000円相当の XRP交換券 プレゼントキャンペーン」(4月30日まで) を行っているので、 支払額の 3% 相当の XRP (暗号資産) が付与される。 また、 このカードには 「年間100万円のご利用で翌年以降の年会費永年無料」特典があり、 この納税で年間利用額 100万円を達成できた。

「ダイナースクラブカード」(上表では「Diners」と略記) は、 この納税で年間利用額が 60万円に達したので、 次年度の年会費が無料になる。 ポイント還元率はわずか 0.5% だが、 24,200円の年会費が無料になるのは大きい。

「TOYOTA Wallet」(上表では「Toyota」と略記) は、 物理的なカードが無いバーチャルカードで、 他のクレジットカード等から残高をチャージして使う。 残高の上限は 5万円なので 5万円を超える支払には利用できない。 実店舗では iD または Mastercard コンタクトレス (Apple Pay のみ) として使える。 支払額の 1% が残高にキャッシュバックされる。

オンライン支払では、 カード名義が 「TOYOTAWALLET MEMBER」 固定なので、 利用できないサイトも多いが、 そもそもこの 「お支払サイト」 はトヨタファイナンス(株)が運営するサイトなので、 同じトヨタファイナンスなのだから使えるかも?と思って試してみたら、 あっさり納付完了してしまった。

残高のチャージを高還元率カード、例えば Sonyカードで行えば、 チャージの際に 2% のキャッシュバックが得られるので、 TOYOTA Wallet のキャッシュバックと合わせて 3% の還元率となる。 上限が同じ 5万円なら、 LINEカードでなく最初から TOYOTA Wallet を使えばよかった。(^^;

Sonyカードはクレジットカードではなく Visaデビットだが、 決済代金が即引き落とされること以外はクレジットカードと変わらない。 もちろん 3Dセキュアに対応している。 高還元率カードとしては群を抜いている (しかも還元はポイントではなくキャッシュバック!) と思うのだけど、 イマイチ認知されていないのは何故だろう?

Filed under: その他 — hiroaki_sengoku @ 08:15
2021年8月24日

M5Stack ATOM Lite を USBシリアル変換アダプタにしてみた 〜 Raspberry Pi Pico の UART コンソールを使う 〜

はじめてラズパイを買ったら意外に面白くてハマってしまった。 電子工作なんて 30年ぶりで、 半田ごてを握るには年を食いすぎている (手元がふるえる) のだけど、 ブレッドボードやら電子パーツやらをいろいろ買い揃えて、 オリンピックが終わってからの 2週間、寝る間も惜しんで楽しんでいる。

Raspberry Pi Zero WH が 1848円、 Raspberry Pi Pico が 550円、 M5Stack ATOM Lite が 1287円。 安いので次々と買ってしまった。 ラズベリーパイ (Raspberry Pi)、略してラズパイと呼ばれるようになって久しいが、 M5Stack をはじめとする ESP32 (や ESP8266) を使ったコントローラは、 何と呼ばれているのだろう?

Raspberry Pi Zero WH は普通の Linux マシンなので何でもありだが、 他はマイコン (マイクロコンピュータ) ならぬマイクロコントローラなので、 普通の OS を動かすことは難しく制約が多くて一筋縄にはいかない。

ググってみると、 開発環境として Arduino IDE を使い C (C++ ?) や Java などで開発している人が多いようだ。 30年前ならいざ知らず、 マイクロコントローラと言えども計算リソースが潤沢にある (30年前の汎用機並?) 昨今、 なぜコンパイラ言語 (しかも C や Java みたいなアセンブラと大差ない低級言語) を使うのか? インタプリタ言語なら対話的にコード片を実行して、 動作を確認しながらプログラミングできる (REPL, Read-Eval-Print Loop) ので、 開発効率が圧倒的に高い。

というわけで、 わたし的には MicroPython 一択 (ちなみに Python を使うのは今回がはじめて) なのであるが、 困ったことに Raspberry Pi Pico (以下 Pi Pico と略記) は、 開発環境である PC との通信手段が限られる。 USB コネクタが一つしかなく、 M5Stack ATOM Lite 等と違って Wi-Fi 機能もない。 つまり、 Pi Pico に USB 機器をつなぐ (Pi Pico が USB ホスト) 場合は、 USB で PC へつなぐ (PC が USB ホスト) ことができなくなるので、 PC と通信する手段が無くなってしまう。 プログラム実行中に PC と通信できなくては REPL にならない。

USB (Universal Serial Bus) がダメなら Universal じゃないシリアル通信を使えばいい、ということで Pi Pico にもシリアル通信のための UART (Universal Asynchronous Receiver/Transmitter, 調歩同期式汎用送受信機) が装備されている (ただし Pi Pico 用の MicroPython は UART では REPL できないので再ビルドの必要がある。 後述)。 PC 側でも UART 機能があれば通信できる。 というか USB や Wi-Fi が無かった時代は UART 通信 (RS-232C など) の方が一般的だった。

ところが、 いまどきの UART は 3.3V だという。 ±3~25V の信号線を使っていた RS-232 規格とは隔世の感がある。 ±25V な機器はさすがに捨ててしまったが、 いまでも手元にある USBシリアル変換アダプタは 0〜5V (TTL レベル) のものばかり。

5V を Pi Pico が扱える 3.3V まで下げるのは抵抗を使って分圧すればいいが、 その逆、 つまり Pi Pico から PC へ 5V の信号を伝えるのは少々やっかいである。 3.3V のままでも PC に H レベルと認識してもらえなくもないが、 マージンが狭くなるのは否めない。 もちろん 115200bps とかなら問題も起きないだろうが、 現代なら 1.5Mbps くらいは出したいところ。

もちろん素直に 3.3V 対応の USB to TTLシリアルアダプタを買えばいいのだが、 USBシリアル変換アダプタを既に (何個も) 持っているのに新たに買うのはモッタイナイ気がするし、 元々 200〜300円くらいしかしないパーツを、 本体と同じくらいの送料を払って買うのも業腹である (こんど秋葉原へ行ったときにでも買おうっと)。

Double Pico ! ATOM Lite as a Serial Converter to Pi Pico

要は 3.3V な UART があればいいわけで、 M5Stack ATOM Lite (以下 ATOM Lite と略記) を USB シリアルアダプタにしてしまえばいい!と思いついた。 つまり ATOM Lite も Pi Pico と同様 USB で REPL が使えるが、 ATOM Lite の REPL ではなく、 ATOM Lite (写真上) と UART シリアル (写真上の 3本のジャンパー線, うち黒は GND) でつないだ先の Pi Pico (写真下) の REPL を使おうという目論見。 ATOM Lite は PC と Pi Pico との通信を中継するだけ。 ATOM Lite もチップの名前は ESP32-PICO-D4 なのでダブルピコ!

PC (開発環境) ←──USB──→ ATOM Lite ←──UART──→ Pi Pico

MicroPython では flash メモリに boot.py を置いておくと起動時に実行してくれる。 ATOM Lite を常にシリアルアダプタとして使いたいわけではないので、 ATOM Lite のボタンを押しながら起動したときだけシリアルアダプタとして機能するようにしてみた。 シリアルアダプタとして動作中はボタン中央の LED が緑色に点灯する。 もう一度ボタンを押すと LED が消灯し、 通常の REPL モードになる。 boot.py に以下のプログラムを追記した:

import machine
import sys
import neopixel
import utime
import _thread

btn = machine.Pin(39, machine.Pin.IN)
if btn.value():
    sys.exit()

pxls = neopixel.NeoPixel(machine.Pin(27), 1)
pxls[0] = (0, 25, 0)
pxls.write()
start_time = utime.time()

uart = machine.UART(1, 115200, tx=21, rx=25)
done = False

def thread():
    global done
    while not done:
        c = sys.stdin.read(1)
        if c == "\n":
            uart.write("\r\n")
        else:
            uart.write(c)
    _thread.exit()

_thread.start_new_thread(thread,())

while not done:
    if btn.value() == 0:
        if utime.time() - start_time > 10:
            done = True
    if uart.any() > 0:
        sys.stdout.write(uart.read(1))
    else:
        utime.sleep_ms(1)

pxls[0] = (0, 0, 0)
pxls.write()

ATOM Lite の GPIO 21番ピンを UART TX として、 GPIO 25番ピンを UART RX として使い、 それぞれ Pi Pico の UART0 RX および UART0 TX につなぐ。 Pi Pico の VBUS と GND に 5V 電源を供給する (写真右下の赤と黒のジャンパー線) ことで USB コネクタを使わずに空けておける。

More...
2021年7月18日

PayPay STEP の新基準をクリアしてみた 〜住民税と健康保険料の支払で1.5%還元〜

今月 7月から PayPay STEP の条件が改訂された。 先月までは合計 10万円以上 PayPay 残高払いすれば、 翌月の還元率が 0.5% から 1% へ 0.5 ポイントアップした。 ところがこれからは合計 5万円以上かつ 30回以上 PayPay 残高払いしないと 1% にならない。 10万円が 5万円に下がったが、 一ヶ月に 30回以上という条件は厳しすぎる。

なぜ還元率を 1% にしたいかというと、 住民税と健康保険料、合わせて 150万円の納付を PayPay 請求書払いで行いたいから。 還元率が 0.5% から 1% へ上がると 7500円ほど還元額が増える。 1% を超える高還元率のクレジットカードは多いが、 たいてい納税等には使えなかったり、 使えても上限額が低かったりする。 一ヶ月に 150万円までの納税等が 1% 還元でできる PayPay は貴重。

6月に健康保険料の第1期分 99,000円を PayPay 請求書払いで納付した。 で、その後ランチで 1133円を PayPay 残高払いした。 これで合計 10万円以上になったので、めでたく翌月 7月の還元率が 1% になった。

Toyonaka PayPay 20% campaign

7月になって請求書払いの還元率が 1% になったのが確認できたので、 住民税と健康保険料の残り (116万円ほど) を全て払ってしまおうと思ったが、 豊中市 x PayPay 20% 還元キャンペーンが始まってしまった。

私は普段 PayPay 残高払いを使わないが、 それは還元率が 0.5% と低いから。 PayPay しか使えないお店では PayPay クレジットカード払いを使うが、 PayPay 残高払いと違って PayPay クレジットカード払いだと PayPay の還元は無いし、 PayPay STEP の回数としてカウントされない。

たいていのお店でもっと高い還元率の支払手段があるし、 PayPay クレジットカード払いなら PayPay に登録したクレジットカードの還元 (私の場合は 2%) が得られる。 わざわざ低還元率の PayPay 残高払いを使う理由は何もない。 が、20% も還元してくれるとなると話は別である。

豊中市に住んでいて、 かつ普段ランチを食べる店や、 最寄りのドラッグストアが 20% 還元の対象店舗なので、 一ヶ月に 30回くらい PayPay 残高払いを使うのは造作もない。 これで 8月も還元率 1% を達成できるメドが付いた。

こうなってくると欲が出て、 さらに上の 1.5% 還元を目指したくなった。 還元率が 1% から 1.5% へ 0.5 ポイント上がると、 住民税と健康保険料の支払で付与される PayPay ボーナスが 7500円増える。 ただし付与上限が 15,000円なので、 1.5% 還元の場合は 1ヶ月に 100万円までの支払しか対象にならない。

1.5% 還元を得るには、以下の 4条件を満たさなければならない:

1) PayPay 支払
合計 5万円以上かつ 30回以上 PayPay 残高払い

2) 次の対象サービスのうち 3つ以上を利用
PayPayモール または Yahoo!ショッピング
PayPayフリマ または ヤフオク!
Yahoo!トラベル
ebookjapan
LOHACO by ASKUL
ただし、 ebookjapan は 300円以上の購入、それ以外は 1000円以上の購入が必要。

3) Yahoo!プレミアム会員登録
あるいはソフトバンクスマホユーザーかワイモバイルユーザー。

4) PayPayアカウントとYahoo! JAPAN IDを連携

条件 1) はメドが付いた。条件 3), 4) は登録するだけの話なので簡単。 私の場合 Yahoo!プレミアム会員登録が 6ヶ月無料だった。 問題は条件 2) である。 いずれのサービスも私は利用したことがない。 もちろん高々 1000円利用するだけだから、 要らないものを買ってもよければすぐ達成できる。 しかし還元率を 0.5 ポイント上げるために 3000円 (ebookjapan を利用する場合は 2300円) をドブに捨ててしまっては本末転倒である。 出来る限り有意義な買物をしたい。

More...
Filed under: その他 — hiroaki_sengoku @ 08:30
2020年9月16日

IFTTT のアプレットが 3個に制限されてしまったので、IFTTT を使わずに スマート家電リモコン RS-WFIREX4 をコントロールしてみた 〜 RS-WFIREX4 の通信プロトコルの解析 hatena_b

IoT機器を IFTTT (IF This Then That) に登録すると、 自前のプログラムからコントロールできるようになる。 つまり IFTTT の webhooks を使うことで、 IFTTT の特定の URL を自前のプログラムからアクセスするだけで IoT機器がコントロールできる。 例えばこんな感じ:

senri:~ $ curl https://maker.ifttt.com/trigger/light_on/with/key/dD-v7GCx46LnWaF1AD9nwSUeA_N1ALvDHKS57cP1_Md
Congratulations! You've fired the light_on event

「light_on」の部分は任意に定めることができる。 この例では照明を点灯させている。

Nature Remo のように API を公開している IoT機器なら、 自前のプログラムから API を直接たたけばよいが、 残念ながら IoT機器の多くが API 非公開なので、 IFTTT が唯一のコントロール手段となっていた。 IoT機器の操作一つ一つ (例えば light_on) に、 IFTTT アプレットを作ることになるので、 私の場合は 50個以上のアプレットを作っていた。

Get IFTTT Pro

ところが!

有料版の IFTTT Pro が新たに発表され、 従来の無料版は登録できるアプレットが 3個に制限されてしまった。 有料版なら無制限にアプレットを作ることができるが、 無料版だと最大 3個しかアプレットを作ることができない。

かくなる上は、 IoT機器の API (通信プロトコル) を解析して IFTTT 抜きで IoT機器をコントロールするしかない。 いままでも IFTTT に登録できない IoT機器については API を解析していたので、 なんとかなるだろう。

IoT機器のほとんど (全て?) がスマホからコントロールできるので、 root 権限を取得できる Android 端末があれば、 スマホのアプリと IoT機器との間の通信を tcpdump 等で観察することができる。 通信が暗号化されていなければ API を解析するのは (比較的) 容易。

RS-WFIREX4

というわけで、 ラトックシステム社 スマート家電リモコン RS-WFIREX4 の API を解析してみた。 幸い、 RS-WFIREX4 の Android 用アプリ スマート家電コントローラ は、 家中モード (スマホと RS-WFIREX4 が同一セグメントにある場合) では通信が暗号化されていない。 スマホ上で tcpdump を実行することで通信 (TCP/IP) 内容を見ることができた。

RS-WFIREX4 は温度、湿度、明るさを計測することができる。 アプリが RS-WFIREX4 の TCP ポート 60001番に対して 5バイトのデータ 「AA 00 01 18 50」 を送信すると、 RS-WFIREX4 から 13バイトのデータ 「AA 00 09 18 00 01 5E 00 E5 00 0A B2 08」 が返ってきた。 最初の 5バイト 「AA 00 09 18 00」 は部屋の温度等に関係なく常に同一だったので、 ヘッダ (つまり計測データは含まれない) と考えられる。

この手の通信プロトコルでは、 可変長のデータを扱うためにヘッダにペイロード (ヘッダ以外のデータ本体) の長さが含まれる (ことが多い)。 この例ではヘッダ以外のデータの長さは 13 - 5 = 8 バイトなので、 おそらく 「00 09」 が (ビッグエンディアンの) ペイロード長だろうとあたりをつける。 つまりヘッダは 「AA 00 09 18」 の 4バイトで、 続く 9バイト 「00 01 5E 00 E5 00 0A B2 08」 がペイロードということになる。

┌─┬─┬─┬─┬─┐
│頭│ペイ長│測│検│
└─┴─┴─┴─┴─┘
│←─ヘッダ─→│ペ│

アプリが送信したデータ 「AA 00 01 18 50」 も、 先頭が 「AA」 (図中では 「頭」 と略記) であることから同じフォーマットである可能性が高い。 つまり 「00 01」 がペイロード長 (図中では 「ペイ長」 と略記) で、 「50」の 1バイトがペイロードなのだろう。 「50」は 「計測データを送信せよ」 という命令の可能性も無くはないが、 おそらく後述するチェックサムだろう (図中では 「検」 と略記)。

そして、 ヘッダ末尾の 「18」 のほうが、 「計測データを送信せよ」 という命令である可能性が高い (図中では 「測」 と略記)。 RS-WFIREX4 の応答のヘッダの末尾も 「18」 だが、 これは命令 「18」 に対する応答であることを示しているのだろう。

┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│頭│ペイ長│測│0│湿度%│温度℃│明るさ│安│検│
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
│←─ヘッダ─→│←─────ペイロード─────→│

部屋を明るくしたり暗くしたり、 温度を上げたり下げたりしたときに、 RS-WFIREX4 からの応答がどのように変化するか調べることで、 ペイロードの 2, 3バイト目 「01 5E」 (10進数で 350) は、 「湿度 35.0 %」 を示していると推測できた。 以下同様に、 4, 5バイト目 「00 E5」 (10進数で 229) は、 「温度 22.9 ℃」 を、 6, 7バイト目 「00 0A」 (10進数で 10) は、 「明るさ」 を示している。

ペイロードの 8バイト目 「B2」 は、 RS-WFIREX4 の電源を入れた瞬間は 0 で、 時間の経過と共に増大し、 充分時間が経つと 255 になる。 RS-WFIREX4 は電源投入後 30分間はセンサーが使えないので、 おそらくセンサーの安定度合い (0〜255) を示していて、 この数値が一定以上 (255?) でないとセンサーの値が正確でないことを示しているのだろう。

末尾の 1バイトは後述するチェックサムと思われる。 なぜなら、 末尾の 1バイトを除くペイロードの内容が同じ (つまり湿度・温度・明るさ・安定度の組合わせが同一) 応答データであれば、 末尾の 1バイトも (少なくとも私が観察した範囲では) 同じ値になっているから。

AA 00 AE 11
00 00 AA
22 11 04 04 05 04 04 0D 04 0D 04 04 05 0C 05 04
04 04 05 04 04 0D 04 04 05 04 04 0D 04 04 05 0C
05 04 04 0D 04 04 05 04 04 0D 04 04 05 04 04 04
05 04 04 04 05 04 04 0D 04 0D 04 04 05 0C 05 04
04 04 05 0C 05 04 04 0D 04 04 05 04 04 0D 04 04
05 04 04 FF FF FF 07 23 10 05 04 04 04 05 0C 05
0C 05 04 04 0D 04 04 05 04 04 04 05 0C 05 04 04
04 05 0C 05 04 04 0D 04 04 05 0C 05 04 04 04 05
0C 05 04 04 04 05 04 04 04 05 04 04 04 05 0C 05
0C 05 04 04 0D 04 04 05 04 04 0D 04 04 05 0C 05
04 04 04 05 0C 05 04 04 04 05
69

次に赤外線を発射させて家電をコントロールしてみる。

← 左の 178バイトのデータをアプリが送信すると、 RS-WFIREX4 が発射した赤外線を受けて天井照明が点灯し、 RS-WFIREX4 から 6バイトの応答 「AA 00 02 11 00 D1」 が返ってきた。

送信データの最初の 4バイト 「AA 00 AE 11」がヘッダで、 ペイロードの長さが 00AE (10進数だと 174) であることが分かる。 ヘッダ末尾の 「11」 が、 「赤外線を発射せよ」 という命令なのだろう (図中では 「射」 と略記)。 アプリを操作して RS-WFIREX4 にいろいろ (長さが異なる) 赤外線を発射させてみたところ、 3行目 「22 11 04 04 ...」から始まる 170バイトが赤外線の波形データ (後述) で、 その直前 (2行目) の 「00 AA」 (10進数で 170) が赤外線の波形データの長さ (図中では「デー長」と略記) を表わしているようだ。

┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│頭│ペイ長│射│0│デー長│赤外線の波形データ│検│
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
│←─ヘッダ─→│←─────ペイロード─────→│

ペイロード末尾の 1バイト 「69」 の算出方法は不明だが、 赤外線の波形データによって値が変わることと、 末尾であることからチェックサムのようなものと思われる。 もちろん単純なチェックサムではなく、 なんらかのエラー検出符号のようなものなのだろう (図中では 「検」 と略記)。 この 1バイトは、 「ペイロード長」には含まれるが、 「赤外線の波形データの長さ」には含まれない (12月18日追記: 「射」をヘッダではなくペイロードに含めて、その代わり「検」をペイロードから外すべきだった。後述)。

このペイロード末尾の 1バイトだけ異なるデータを送信すると RS-WFIREX4 はこの 178バイトの送信データ全体を無視する。 赤外線も発射しないし、 何の応答も返さない。 前述したように家中モードでは何のセキュリティもないので、 この末尾の 1バイトが (正規の) アプリから送信されたことを保証する唯一の認証手段なのだろう。 ここでは、 RS-WFIREX4 に対する送信データ (あるいは RS-WFIREX4 からの応答データ) におけるこの末尾の 1バイトを「チェックサム」と呼ぶことにする。

170バイトの赤外線の波形データは、 1バイト目の 「22」(10進数だと 34) が赤外線の ON の区間を表し、 2バイト目の 「11」が赤外線の OFF の区間を表し、 以下同様に奇数バイト目が赤外線の ON の区間を表し、 偶数バイト目が赤外線の OFF の区間を表す。 各バイトの数値の 1/10000 が各区間の秒数になる。 例えば 「04」 の場合は 0.4ミリ秒になり、 家製協(AEHA)フォーマットの 1T 区間に相当する。 1バイト目の 「22」は 8T 区間、 2バイト目の 「11」は 4T 区間、 8バイト目の 「0D」は 3T 区間に相当する。

IR wave

つまり赤外線の波形データの最初の行 (3行目) は、 赤外線 ON が 8T (3.2ミリ秒) 続き、 次に OFF が 4T (1.6ミリ秒) 続き、 以下 ON 1T, OFF 1T, ON 1T, OFF 1T, ON 1T, OFF 3T, ON 1T, OFF 1T, ON 1T, OFF 3T, ON 1T, OFF 1T という波形になる (上図 ↑)。 ただし ON の区間は赤外線が点きっぱなしになっているのではなく、 38kHz の赤外線パルス (デューティ比 1/3) を送信している。

この赤外線の波形データは、 アプリで 「リモコンデータ受け渡し⇒エクスポート⇒メールで送信」 を行うことで得られる XML データに含まれる (<code>...</code> の部分)。 あるいは他の学習リモコンの赤外線波形データを変換しても良い。

実を言うと、 ここまでは昨年の段階で解析済だった。 ペイロード末尾の 1バイトの算出方法が判明したら公開しようと思っていたのだが、 いろいろ他にも忙しくて :-) 放置してしまっていた (12月18日追記: 単なる CRC-8 だと判明。後述)。 アプリを逆コンパイルするのは骨が折れるのと、 算出しなくても tcpdump で見れば値が得られるので実用上は困らなかったから。 で、今回 IFTTT が有料化したので急遽公開することにした次第。

チェックサムの算出方法が分からないといっても高々 1バイトである。 256通りなんてブルートフォース攻撃というほど brute でもない。 幸い、 RS-WFIREX4 はチェックサムが違うデータを立て続けに受信しても、 異常動作することはないようだ (もちろん常時チェックサム違いのデータを送信することは推奨できない)。

赤外線の波形データごとに 00 〜 FF まで 256通りのチェックサムを試して、 RS-WFIREX4 から応答が返ってきたら、 赤外線の波形データにそのチェックサムを付加して記憶しておけばよい。 赤外線の波形データの前の 3バイトおよびヘッダは、 発射する際に都度算出すれば良い。

チェックサムが正しい場合、 RS-WFIREX4 は赤外線を発射して 「AA 00 02 11 00 D1」 を返す。 ヘッダ末尾の 「11」 は、 命令 「11」 (赤外線を発射せよ) の応答であることを示す。

┌─┬─┬─┬─┬─┬─┐
│頭│ペイ長│射│0│検│
└─┴─┴─┴─┴─┴─┘
│←─ヘッダ─→│←ペ→│

ペイロードは 「00 D1」 の 2バイトだが、 ペイロードが 2バイト以上の場合、 ペイロードの先頭は常に 「00」 であるようだ (図中では 「0」 と表記)。 末尾の 「D1」 はチェックサムだろう。 実質的にチェックサムだけのペイロードならば、 1バイトのペイロードで充分だと思うが、 アプリが送信するデータの場合は 1バイトのペイロードが有り得ても、 RS-WFIREX4 が返す応答データの場合は常に 2バイト以上になるのかもしれない。

RS-WFIREX4 をコントロールする perl スクリプト wfirex.pl を以下に示す:

#!/usr/bin/perl
use strict;
use warnings;
use Getopt::Std;
use IO::Socket::INET;

my %Ir;
$Ir{'on'} = pack("H*", "221104040504040D040D0404050C050404040504040D04040504040D0404050C0504040D04040504040D040405040404050404040504040D040D0404050C05040404050C0504040D04040504040D0404050404FFFFFF07231005040404050C050C0504040D040405040404050C05040404050C0504040D0404050C05040404050C050404040504040405040404050C050C0504040D04040504040D0404050C05040404050C050404040569");
$Ir{'off'} = pack("H*", "221005040404050C050C0504040D040405040404050C05040404050C0504040D0404050C05040404050C0504040405040404050C050C050C050C0504040D040405040404050C050C05040404050C0504040405FFFFFF07221104040504040D040D0404050C050404040504040D04040504040D0404050C0504040D04040504040D0404050404040504040D040D040D040D0404050C050404040504040D040D04040504040D040405040437");
$Ir{'small'} = pack("H*", "221104040405040D040D0404040D040504040504040D04040405040D0404050C0504040D04040504040D04040405040404050404050C050C050C0504040D04040405040D040D040D04040504040D0404040504FFFFFF08221104040405040D040D0404050C040504040405040D04040405040D0404050C0405040D04040405040D04040504040404050404040D050C050C0504040D04040504040D040D040D04040405040D0404040504E9");
$Ir{'aoff'} = pack("H*", "211005040404040d0404040d040404040504040c050c040405040404040d040c0405040404040504040405040404040405040404050404040404050c0404050404040405040404040504040c0504040404050404040d040404040504040404050404040c0504040c050c040d040c040d040c04d7");
$Ir{'study_on'} = pack("H*", "2210050C04050404040405040404050404040405040404050404040D0404040504040405040404050404040D040D040404050404040504040405040C0504040D0404040D040D040C050C0405040C0504040D04040405040404050404040405040404050C050C040D040C050C050C040D040D0404050C040405040404050404040504040C0504040D040C050C050C040D040D0404040D0404050404040504040C050C050C0405040C050C040D040D0404040504FF2E2111040C05040405040404050404040404050404040504040405040C0504040404050404040504040405040D040C040504040405040404050404040D0404050C0405040C050C040D040D0404040D0404050C04050404040504040404050404040504040D040C050C040D040D040C050C040D0404050C040504040405040404050404040D0404040D040D040D040C050C050C0405040C0504040404050404040D040D040C0504040D040C050C050C0405040404FF292111040D04040405040404050404040504040405040404050404040D0404040504040405040404050404040D040D040404050404040504040404050C0405040C0504040D040C050C040D0404050C0405040C05040404050404040504040404050404040D040D040C050C050C040D040C050C0504040D040404040504040405040404050C0504040C050C050C040D040D040C0504040D0404040504040404050C040D040D0404050C040D040C050C05040404042F");
$Ir{'study_off'} = pack("H*", "2210040D04050404040504040405040404040504040404050404040D0405040404050404040503050404040D040D040404050404040504040405040C0504040D0404040D040D040C040D0405040C0504040D04040405040404050404040504040405040C050C040D040D040C050C040D040D040C050C04050404040504040405040404050404040D040D040C050C040D040D0404040D0405040404040405040D040D040C0405040C050C040D040D0404040504FF2E2111040D04040405040404050404040504040405040404050404040D0404040504040405040404050404040D040C050404050305040404050404040D0405040C0504040D040C050C040D0404050C0405040C05040404050404040405040404050404040D040D040C050C040D040D040C050C040D040D04040504040405040404050404040504040C050C050C040D040C050C0504040D0404040504040404050C040D040D0404040D040D040C050C0504040404FF292210050C04050404040504040405040404050404040404050404040D0405040404050404040404050404040D040D040404050404040504040405040C0504040D0404040D040D040C050C0405040C0504040D04040405040404050404040405040404050C040D040D040C050C040D040D040D040C050C04050404040405040404050404040504040D040C050C040D040D040D0404040D0404040504040405040D040C040D0405040C040D040D040D040404050408");
$Ir{'study_aoff'} = pack("H*", "2c2b070f0610060f061006050605060f060506050605060406050610060f0605061006050604060506050605060505100610060f06100610060f0610060f060506050605060506040605060506050605060f06100605060f0610060506040605060506050605050506050605061005100610060506040605060506050605050506050610060505100610060505100610060505332d2b0610060f0610061005050605061006050505060506050605060f06100605060f0605060506050605050506050610060f0610061005100610060f06100605060506040605060506050605050506050610060f06050610060f06050605060506050505060506050605060505100610060f06050605060506050505060506050605060f06050610060f06050610060f06050675");

our ($opt_v, $opt_s);
getopts('vs') || help();
my $ip = shift || help();
my $command = shift || help();

if ($command eq "get") {
    my ($il, $te, $hu) = get_wfirex($ip);
    if (defined $il) {
        $te /= 10;
        $hu /= 10;
        print "il=$il te=$te hu=$hu\n";
    } else {
        print "wfirex get TIMEOUT $ip\n";
    }
} elsif (defined $Ir{$command}) {
    my $ret;
    if ($opt_s) {
        my $ir = substr($Ir{$command}, 0, length($Ir{$command})-1);
        for (my $i=0; $i < 256; $i++) {
            my $checksum = pack("C", $i);
            printf("try %02x ...\n", $i);
            $ret = send_wfirex($ip, $ir . $checksum);
            if ($ret) {
                printf("success ! checksum=%02x ret=%02x\n", $i, $ret);
                last;
            }
            sleep 1;
        }
    } else {
        $ret = send_wfirex($ip, $Ir{$command});
    }
}
exit 0;

sub get_wfirex {
    my ($ip) = @_;
    my $sock = IO::Socket::INET->new(
        PeerAddr  => $ip,
        PeerPort  => 60001,
        Proto     => "tcp",
        Timeout   => 5,
        );
    if ($sock) {
        print $sock "\xaa\x00\x01\x18\x50";
        my $buf;
        my $flags;
        $sock->recv($buf, 256, $flags);
        my @data = unpack("CCCCCnnnCC", $buf);
        close($sock);
        print join(" ", @data) . "\n" if $opt_v;
        return ($data[7], $data[6], $data[5]);
    }
    return undef;
}

sub send_wfirex {
    my ($ip, $ir) = @_;
    my $len = length($ir) - 1;
    $ir = "\xaa" . pack("n", $len+4) . "\x11\x00" . pack("n", $len) . $ir;
    my $sock = IO::Socket::INET->new(
        PeerAddr  => $ip,
        PeerPort  => 60001,
        Proto     => "tcp",
        Timeout   => 5,
        );
    if ($sock) {        
        print $sock $ir;
        my $buf;
        my $flags;
        $sock->recv($buf, 256, $flags);
        my @data = unpack("CCCCCC", $buf);
        close($sock);
        if (@data) {
            print join(" ", @data) . "\n" if $opt_v;
        }
        return $data[5];
    }
    return undef;
}

sub help {
    print <<EOF;
Usage: wfirex <opt> <IP> <com>
opt:   -v   ; verbose
       -s   ; scan checksum
com: get    ; get sensor value
EOF
    print "     " . join(" ", sort keys %Ir) . "\n";
    exit 1;
}

連想配列 %Ir に赤外線の波形データを 16進数の文字列で格納しておく。 末尾の 2文字 (つまり 16進数 1バイト) がチェックサム。 チェックサムが不明のときは、 とりあえず「00」をつけておいて「-s」オプションでチェックサムを探索する。

senri:~ $ ./wfirex -vs wfirex4l on
try 00 ...
try 01 ...
try 02 ...
try 03 ...

  … 中略 …

try 67 ...
try 68 ...
try 69 ...
170 0 2 17 0 209
success ! checksum=69 ret=d1

赤外線の波形データ $Ir{'on'} のチェックサムが 「69」 (16進数) であることが判明したので、 とりあえずつけた末尾の 「00」 を 「69」 で置き換える (前掲のスクリプトは置き換え済)。

More...
2020年9月2日

サーバの MAC アドレスを偽装 (MAC spoofing) してエッジ・スイッチの MAC アドレスと同一にしてみた 〜 プロバイダの接続台数制限を回避する

多くの機器で MACアドレス (Media Access Control address) は偽装できる。 例えば Linux サーバなら次のような感じ:

# ip link set dev eth0 down
# ip link set dev eth0 address 00:11:22:33:44:55
# ip link set dev eth0 up
# ip link show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 00:11:22:33:44:55 brd ff:ff:ff:ff:ff:ff

インタフェースが立ち上がった状態では変更できないので、 まず eth0 を down させているが、 サーバの起動時などインタフェースが立ち上がる前なら、もちろん不要。 そして MACアドレスを 「00:11:22:33:44:55」 に偽装している (MAC spoofing)。

     インターネット
        │
        │
   ┌────┴────┐
   │ マンション共用部│
   │   のルータ  │
   └─┬┬┬┬┬┬┬─┘
 ┌───┘│││││└───┐
 │ ┌──┘│││└──┐ │
 │ │ ┌─┘│└─┐ │ │
 : : :  │  : : :
  各戸へ   │   各戸へ
        │
     ┌──┴──┐
     |スイッチE|
     └┬─┬─┬┘
  ┌───┘ │ └───┐
  │     │     │
  :  ┌──┴──┐  :
 各部屋 |スイッチR| 各部屋
     └┬┬┬┬┬┘
  ┌───┘│││└───┐
  │ ┌──┘│└──┐ │
  ↓ ↓   ↓   ↓ ↓
  他のPC  サーバ  他のPC

私の自宅内の LAN において、 サーバ (左図下端) の MACアドレスを偽装して、 スイッチE (マンション共用部のルータにつながるエッジ・スイッチ) の MACアドレスと同一にしてみた。

なぜこんなことをするか?を、順を追って説明する:

マンション共用部のルータ (以下、「ルータM」と略記) は、 各戸に最大 5個のグローバル IPアドレスを割当てる。 つまり、 「ルータM」は各戸 (マンション専有部) ごとに接続された機器を数えていて、 接続を検知した順に先着 5台にのみグローバル IPアドレスを割当てる。

割当てられるグローバル IPアドレスが 5個なのではなく、 先着 5台の機器のみに、 DHCP リクエストがあれば IPアドレスを割当てる点に注意。 DHCP リクエストを行わない機器の MACアドレスがルータM に届いてしまえば、 それも 1台としてカウントされる。

困ったことにスイッチE (GS108E) は、 (L2スイッチのくせに) MACアドレスを持っていて、 定期的に UDP ブロードキャスト (NSDP) を行う。 このブロードキャストがルータM に届くと、 スイッチE が「接続機器」とみなされてしまい、 貴重なグローバル IPアドレス枠が一つ浪費されてしまう

NSDP のブロードキャストを止めることが可能ならそれが一番だが、 残念ながら GS108E の管理ツール (Windows アプリ) にはそういう設定項目は無い。 スイッチE の MACアドレスがルータM に届くのを阻止する術は無さそうだ。 もちろん、 スイッチE とルータM との間にブリッジを挟んでブロードキャストをフィルタリングすれば阻止できるが、 そんなボトルネックは作りたくない。

スイッチR (GS116E ポート数が異なる他は GS108E と同等) も同様に MACアドレスを持っていて NSDP のブロードキャストを行うし、 もちろん (グローバル IPアドレスを持たない) その他の PC 等も様々なパケットを送信するが、 いずれもルータM と直接つながっていないので、 VLAN を設定することでパケットがルータM へ届くのを阻止することが可能。

上記ネットワーク図には、 家庭内LAN に必ずある NATルータ (Wi-Fiルータ等) が見当たらないが、 Linux サーバが NATルータの役割を果たしている。 GS108E のようなタグVLAN 機能付スイッチを使うと、 物理的な配線にとらわれずに自在に LAN を構成できるので便利。

つまり、 MACアドレスがルータM に届くのを阻止できないのは、 ルータM と直接つながっているスイッチE のみ、 ということになる。

そこで、 スイッチE が「接続機器」と見なされるのが避けられないなら、 逆にスイッチE の MACアドレスでグローバル IPアドレスを取得してやろうと考えた次第。 サーバの MACアドレスを偽装してスイッチE の MACアドレスと同一にすることで、 このサーバは無事グローバル IPアドレスを取得できた。

もちろん、 これでは同一セグメント内に同じ MACアドレスを持つ機器が存在することになってしまう。 これはネットワークの教科書的には、 決してやってはいけないことだ。

一番の問題はスイッチの MACアドレス学習が混乱する点。 上図でいうと「スイッチR」 (部屋ごとに設置しているスイッチ) は、 上の「スイッチE」からも下の「サーバ」からも、 同じ MACアドレスを送信元とするパケットが届いてしまう。

また、2番目の問題として、 スイッチE に自身の MACアドレス宛のパケットが届いたとき、 それを正しくサーバへ中継するのではなく、 自身宛と見なして中継しない (そのまま捨ててしまう) 恐れがある。

まず 1番目の問題は、 スイッチE のブロードキャストの間隔が充分長ければ、 実用上の問題は起きないだろうと考えた。 確かに、 スイッチE のブロードキャストがスイッチR に届けば、 スイッチR で誤学習が起きる。 この状態でスイッチR にサーバ宛のパケットが届くと、 スイッチR はサーバへ送らずに、 スイッチE へ送ってしまう。

が、サーバは常時通信を行っているわけだから、 スイッチR には速やかにサーバからパケットが届いて誤学習状態は直ちに解消される。 一時的な誤学習は、 それが低頻度かつ短時間であればパフォーマンス上の問題は起きないだろう。

2番目の問題は、 スイッチの機種にも依存するが、 少なくとも私が使ってる GS108E-100JPS の場合は問題にならない。 つまり自身の MACアドレス宛のパケットが届いても、 それを自分宛とは見なさず、 他のパケットと同様に中継する。

例えばインターネットからサーバ宛に届くパケットの場合、 ルータM はサーバの MACアドレス (つまりスイッチE の MACアドレス) を宛先としたパケットを送信するが、 このパケットはスイッチE において何事もなく中継されて、 サーバに正しく届く。

なお、 GS108E-100JPS の後継機 GS108E-200JPS は、 管理ツール以外に WWW ブラウザで管理することもできる。 つまり (簡易な) Web サーバを内蔵しているわけで、 このようなスイッチの場合は、 スイッチの MACアドレスを宛先とするパケットは、 この Web サーバによって受信されてしまい、 このスイッチにおいては中継されないと予想される。

実際に GS105E-200JPS (ポート数が異なる他は GS108E-200JPS と同等と考えられる) で実験してみたところ、 スイッチの MACアドレスを宛先とするパケットは、 スイッチで中継されなかった。 したがって GS105E-200JPS や GS108E-200JPS を、 スイッチE として使うことは、 グローバル IPアドレス枠を一つ浪費することになるので適切ではない。

もちろん、 スイッチE として一番適切なのは、 無用なブロードキャストを吐かない (あるいは吐かない設定が可能な) スイッチである。 接続台数に制限があるプロバイダを利用する際は、 エッジ・スイッチの挙動の細かな違いにも気を配りたい。

Filed under: システム構築・運用 — hiroaki_sengoku @ 09:35
2019年12月10日

学習リモコンの赤外線波形データを変換してみた 〜 Nature Remo で取得した波形データを PC-OP-RS1 用に変換

人感センサ (人の動きを感知するセンサ) 付であることに魅力を感じて IoT な学習リモコン Nature Remo を買ったら、 人の動きをトリガーにした IFTTT との連携ができないばかりか、 センサの感度もあまりよくなかった。 仕方ないので人感センサを新たに買ってみた

人感センサとしての感度は、 だんぜんこの +Style ORIGINAL スマートセンサー(人感) PS-SMT-W01 のほうがいい。 IFTTT と連携できないので他の IoT 機器との連携を考えている場合は注意が必要だが、 私は IFTTT をショートカットするので無問題。 思わず買い増ししてしまった。

Nature Remo から人感センサを引き算したら、 残りは「学習リモコン」ということになるが、 そこで思い出したのが 13年前に買ったパソコン用学習リモコン PC-OP-RS1。 いま流行りの IoT では無いが、 サーバが置いてある部屋で使うのであれば IoT である必要はなく、 むしろ PC-OP-RS1 のように (ネットを介さず) USB で直接コントロールできるほうが、 赤外線を発射するまでの遅延が少なくてすむ。

学習リモコン PC-OP-RS1 と人感センサ PS-SMT-W01 を組合わせれば Nature Remo は不要? と思ったので押し入れの中から PC-OP-RS1 を発掘した。 ところが、 家電のリモコンの赤外線を学習させようと、 PC-OP-RS1 の受光部に向けて赤外線を発射しても、 PC-OP-RS1 側では何も受け取っていない様子。 10年くらい使ってなかったから赤外線受光素子が劣化してしまったのか?

赤外線の受光はできないものの、 発光は可能みたい。 13年前に書いた Perl スクリプトを使って Nature Remo に向けて赤外線を発射してみると、 ちゃんと Nature Remo で波形データを生成できた。 ということは、 波形データさえ用意できれば今でも使えそう。

ただし、 13年前に PC-OP-RS1 を買ったときは、 波形データのフォーマットを知らなくても使えたので、 単に PC-OP-RS1 が出力した波形データを 16進数の羅列として perl スクリプトに取り込んだだけ。 当時書いた「日記」からスクリプト (の冒頭部分) を引用:

#!/usr/bin/perl
use strict;
use warnings;
use Device::SerialPort;
use Getopt::Std;

my %Ir;
$Ir{'vPower'} = [
    pack("H*", "ffffffffffffffffffffff0700000000007ef0831ff8c00f7e00003f00800ffc00003f00801f00c00700f00300f8c10f7c00003f00801f00e00700f0831f00c00f7ee0033ff8c10ffc00003ff00100fc00007e00001f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"),
    ];
$Ir{'aPower'} = [
    pack("H*", "ffffffff0f00000080ff000000fc030000f01fc07f000000fe01fc070000e03f000000ff01fe030000f01f000080ff00fe010000f80fe03f000000ff000000fc07f01fc03f000000ff01fc07f80fe03f807f00ff01fc03f80f0000c07f00ff00fe03f80fe01fc07f00ffffffff07000000c03f000000ff010000f80fe01f000000ff00fe030000f01f0000807f00ff010000f80f0000c03f80ff000000fc07f00f0000c07f000000fe03f807f01f000080ff00fe01fc07f00fe03f80ff00fe01fc070000e03f807f00ff01fc03f80fe03f80ffffffff01000000f01f0000000000000000000000000000000000feffff"),
    ];

 ...以下略 ...

スクリプト中 「vPower」 はビデオテープレコーダ (VTR) の電源をオン/オフする赤外線のデータ。 「aPower」 は (おそらく) エアコンのオン/オフ。 後に続く 16進数の羅列が赤外線の波形データ。 どちらの家電もすでに無く (VTR なんてすでに死語?)、 そのリモコンも捨ててしまった。 なのでこのスクリプトが (今でも) ちゃんと機能するかは確認のすべがない。

とりあえず vPower の 16進数を 2進数で表示してみる:

senri:~ $ perl -e 'print unpack("b*", pack("H*", "ffffffffffffffffffffff0700000000007ef0831ff8c00f7e00003f00800ffc00003f00801f00c00700f00300f8c10f7c00003f00801f00e00700f0831f00c00f7ee0033ff8c10ffc00003ff00100fc00007e00001f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"))."\n"'
111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000001111110000011111100000111111000000111110000001111110000011111100000000000000000111111000000000000000001111100000011111100000000000000001111110000000000000000011111100000000000000000111110000000000000000011111100000000000000000111111000001111110000001111100000000000000000111111000000000000000001111110000000000000000111111000000000000000001111110000011111100000000000000000111111000001111110000001111100000011111100000111111000001111110000001111110000000000000000111111000000111110000000000000000011111100000000000000000111111000000000000000001111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

おお、 (なんとなく ^^;) 赤外線の波形データっぽい。 2進数で表示すると、 最初の 91個の「1」と続く 46個の「0」の連続を除けば、 「1」は 5〜6個続くのに対し、 「0」は 5〜6個か、15〜17個続く。 これは赤外線リモコンの通信フォーマットにおける 1T (5〜6個) および 3T (15〜17個) の区間に対応するのだろう。 ということは T (変調単位) は 2進数 5.5個くらいに対応する、 つまり 2進数 1個は 100μ秒くらいなのだろう。

ちなみに unpack("B*", ...) (descending bit order) も試してみたのだが、 「"b*"」(ascending bit order) のほうが赤外線の波形データっぽかったので、 「"b*"」と仮定して作業を進めた。 とりあえず 2進数に変換してみる、みたいな試行錯誤を 1行スクリプトでサクっと書けてしまえるのは perl ならでは。 さいきんあまり人気がない perl だが、 この手の試行錯誤をするときには今でも一番ではなかろうか?

いっぽう Nature Remo の赤外線波形データはこんな感じ:

senri:~ $ curl -i -X GET "http://Remo-XXXXXX.local/messages" -H "Accept: application/json" -H "X-Requested-With: curl" -H "Expect: "
HTTP/1.0 200 OK
Server: Remo/1.0.77-g808448c
Content-Type: application/json

{"format":"us","freq":37,"data":[3357,1717,385,1305,377,468,386,460,386,460,387,459,383,463,385,462,382,463,383,463,383,462,384,465,381,465,379,1306,386,463,382,460,385,465,381,467,381,459,385,462,384,462,377,1312,386,1309,384,459,387,462,382,460,386,460,385,461,385,462,385,1306,385,461,384,1307,384,461,386,1306,402,1291,399,1291,382,1307,388,456,404,1290,401,443,403,1287,407,440,402,445,401,443,386,462,400,445,384,463,382,464,381,464,384,1306,401,1292,383,1302,409,1285,402,1289,383,1309,383,1306,387,1304,379,1312,407,1285,402,443,405,441,386,460,403,443,385,460,404,442,385,464,403,440,404,1289,405,1285,404,1282,388,1304,409,1282,390,1301,406,441,386,1306,407,436,410,441,401,440,406,443,385,1304,407,1284,404,1287,405,441,386,1305,408,1287,404,1285,385,1304,407,441,383,462,389,40199,3376,1697,384,1306,407,441,402,442,407,440,384,465,400,443,405,442,383,460,385,464,384,461,401,445,402,441,404,1291,400,446,384,461,400,446,381,463,383,466,399,444,402,444,383,1304,407,1287,401,444,384,460,404,442,407,439,406,442,403,441,404,1287,386,460,403,1289,400,445,403,1287,407,1282,409,1285,385,1307,400,444,405,1285,404,442,404,1289,402,442,405,441,401,444,386,459,404,442,405,441,403,443,404,442,407,1286,405,1288,398,1286,406,1285,409,1283,404,1287,407,1286,401,1294,406,1284,401,1288,401,446,381,462,402,445,401,444,402,444,384,462,383,466,398,442,407,1286,401,1288,403,1288,404,1288,403,1288,401,1290,407,439,404,1284,406,441,404,441,409,436,408,439,407,1285,406,1285,403,1289,400,446,402,1288,403,1287,405,1284,404,1286,409,437,406,444,404,39760,3379,1697,402,1288,404,442,405,441,401,444,407,439,406,441,401,446,402,441,407,439,402,445,406,441,401,443,401,1290,405,437,404,446,405,441,402,441,402,442,406,441,403,441,404,1290,402,1290,400,446,404,439,407,439,406,439,404,442,404,442,406,1288,401,442,402,1290,385,462,401,1289,401,1290,404,1288,399,1288,404,441,386,1306,402,446,402,1287,403,446,401,442,402,444,401,445,404,441,402,444,402,444,402,446,400,1290,398,1290,402,1287,385,1309,404,1287,400,1289,403,1292,401,1283,390,1302,387,1304,404,445,384,460,408,436,405,442,385,462,402,443,404,441,404,442,385,1306,404,1287,402,1292,383,1307,401,1289,404,1290,400,443,405,1282,388,461,406,439,404,446,384,461,383,1303,404,1289,385,1303,405,442,404,1288,405,1286,404,1287,402,1290,403,442,406,440,405]}

Nature Remo に向けて赤外線を発射した後、 http でアクセスすれば JSON 形式で波形データを返してくれる。 で、この波形データの意味は? と思う間もなく答が見つかってしまった。 つまんない。

data配列の各要素は、赤外線ONの期間、OFFの期間、ONの期間、OFFの期間、、、、を表している。 厳密には、これは38kHzの変調をデコードしたあとの結果である。実際にはONの期間は38kHzの変調信号になっている。

ぱっと見 400前後の数値が多いなぁと思ったが、 1T 区間に対応するわけね、納得。 ざっと見た感じ 「赤外線ONの期間」 のほうが 「OFFの期間」 より短めになっている感じがしたので、 前者は 85 で割り算し、 後者は 115 で割り算してみた。 この「商」(割り算した結果) の個数だけ 2進数の 1 と 0 を並べ、 16進数に変換すればオシマイ。

Nature Remo 形式から PC-OP-RS1 形式への変換スクリプト:

#!/usr/bin/perl
use strict;
use warnings;

my @data = (3357,1717,385,1305,377,468,386,460,386,460,387,459,383,463,385,462,382,463,383,463,383,462,384,465,381,465,379,1306,386,463,382,460,385,465,381,467,381,459,385,462,384,462,377,1312,386,1309,384,459,387,462,382,460,386,460,385,461,385,462,385,1306,385,461,384,1307,384,461,386,1306,402,1291,399,1291,382,1307,388,456,404,1290,401,443,403,1287,407,440,402,445,401,443,386,462,400,445,384,463,382,464,381,464,384,1306,401,1292,383,1302,409,1285,402,1289,383,1309,383,1306,387,1304,379,1312,407,1285,402,443,405,441,386,460,403,443,385,460,404,442,385,464,403,440,404,1289,405,1285,404,1282,388,1304,409,1282,390,1301,406,441,386,1306,407,436,410,441,401,440,406,443,385,1304,407,1284,404,1287,405,441,386,1305,408,1287,404,1285,385,1304,407,441,383,462,389,40199,3376,1697,384,1306,407,441,402,442,407,440,384,465,400,443,405,442,383,460,385,464,384,461,401,445,402,441,404,1291,400,446,384,461,400,446,381,463,383,466,399,444,402,444,383,1304,407,1287,401,444,384,460,404,442,407,439,406,442,403,441,404,1287,386,460,403,1289,400,445,403,1287,407,1282,409,1285,385,1307,400,444,405,1285,404,442,404,1289,402,442,405,441,401,444,386,459,404,442,405,441,403,443,404,442,407,1286,405,1288,398,1286,406,1285,409,1283,404,1287,407,1286,401,1294,406,1284,401,1288,401,446,381,462,402,445,401,444,402,444,384,462,383,466,398,442,407,1286,401,1288,403,1288,404,1288,403,1288,401,1290,407,439,404,1284,406,441,404,441,409,436,408,439,407,1285,406,1285,403,1289,400,446,402,1288,403,1287,405,1284,404,1286,409,437,406,444,404,39760,3379,1697,402,1288,404,442,405,441,401,444,407,439,406,441,401,446,402,441,407,439,402,445,406,441,401,443,401,1290,405,437,404,446,405,441,402,441,402,442,406,441,403,441,404,1290,402,1290,400,446,404,439,407,439,406,439,404,442,404,442,406,1288,401,442,402,1290,385,462,401,1289,401,1290,404,1288,399,1288,404,441,386,1306,402,446,402,1287,403,446,401,442,402,444,401,445,404,441,402,444,402,444,402,446,400,1290,398,1290,402,1287,385,1309,404,1287,400,1289,403,1292,401,1283,390,1302,387,1304,404,445,384,460,408,436,405,442,385,462,402,443,404,441,404,442,385,1306,404,1287,402,1292,383,1307,401,1289,404,1290,400,443,405,1282,388,461,406,439,404,446,384,461,383,1303,404,1289,385,1303,405,442,404,1288,405,1286,404,1287,402,1290,403,442,406,440,405);
my $str = "";
my $bit = 1;
for my $d (@data) {
    if ($bit) {
        $str .= $bit x ($d / 85);
        $bit = 0;
    } else {
        $str .= $bit x ($d / 115);
        $bit = 1;
    }
}
$str = unpack("H*", pack("b*", $str)). "\n";
print "$str\n";

1行スクリプトに書けなくもないが、 まあ無理に 1行にしなくても、 このくらいならソッコーで書ける。 やっぱり perl が一番 :-)。

実行してみると ↓ こんな感じ。 波形データを PC-OP-RS1 形式に変換して初めて気付いたが、 80個以上の 0 が連なる区間 (2進数だと 320個以上、つまり 32ミリ秒以上の空白) があり、 3つの波形データに分けられることが分かる。

senri:~ $ ./irconv.pl
ffffffff7f00e001f0f0f0f07878787878787878003c3c3c3c3c1e1e1e000f80c7c3c3c3c3c303e0e101f0f00078003c001e008f07c0e301f0783c1e1e0f0f0f0f8007c003e001f00078003c001e000f8007c0e3f1f078783c3c1e000f8007c003e001f000783c001e8fc7e301f00078003c1e000f8007c003e0f1f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0ffffffff0f003c001e8fc7c3e3f1f0f0f0783c001e0f8f8787c7e301f000783c3c1e8fc703e0e101f078003c001e000f80c703e0f100783c1e8fc7e3f178003c001e000f8007c003e001f00078003c001e0f8fc7e3e1e1f10078003c001e000f8007c0e301f0783c1e0f8007c003e0f10078003c001e008fc703000000000000000000000000000000000000000000000000000000000000000000000000000000000000f8ffffffff03000f80c7e3f1783c1e8fc7e3f100783c1e8fc7e3f10078003c1e8fc7e3f100783c001e1e000f8007c003e0f100783c001e8fc7e3f1783c1e000f8007c003e001f00078003c001e000f80c7c3e3f1f0783c1e000f8007c003e001f000783c001e1e8f8707c003e001f078003c001e000f80c7e301

というわけで、 上記「変換スクリプト」をちょこっと書き直して、 赤外線信号が表現しているデータを表示するようにしてみる。 NECフォーマットでも、 家製協(AEHA, 家電製品協会)フォーマットでも、 赤外線OFFの期間が 1T のとき「0」で、 3T のとき「1」だから、 赤外線ONの期間は無視して、 赤外線OFFの期間が 1000以上の時は 1 で、以下なら 0、 そして 3000以上なら信号の切れ目。

Nature Remo 形式から家製協(AEHA)フォーマットへの変換スクリプト:

#!/usr/bin/perl
use strict;
use warnings;

my @data = (3357,1717,385,1305,377,468,386,460,386,460,387,459,383,463,385,462,382,463,383,463,383,462,384,465,381,465,379,1306,386,463,382,460,385,465,381,467,381,459,385,462,384,462,377,1312,386,1309,384,459,387,462,382,460,386,460,385,461,385,462,385,1306,385,461,384,1307,384,461,386,1306,402,1291,399,1291,382,1307,388,456,404,1290,401,443,403,1287,407,440,402,445,401,443,386,462,400,445,384,463,382,464,381,464,384,1306,401,1292,383,1302,409,1285,402,1289,383,1309,383,1306,387,1304,379,1312,407,1285,402,443,405,441,386,460,403,443,385,460,404,442,385,464,403,440,404,1289,405,1285,404,1282,388,1304,409,1282,390,1301,406,441,386,1306,407,436,410,441,401,440,406,443,385,1304,407,1284,404,1287,405,441,386,1305,408,1287,404,1285,385,1304,407,441,383,462,389,40199,3376,1697,384,1306,407,441,402,442,407,440,384,465,400,443,405,442,383,460,385,464,384,461,401,445,402,441,404,1291,400,446,384,461,400,446,381,463,383,466,399,444,402,444,383,1304,407,1287,401,444,384,460,404,442,407,439,406,442,403,441,404,1287,386,460,403,1289,400,445,403,1287,407,1282,409,1285,385,1307,400,444,405,1285,404,442,404,1289,402,442,405,441,401,444,386,459,404,442,405,441,403,443,404,442,407,1286,405,1288,398,1286,406,1285,409,1283,404,1287,407,1286,401,1294,406,1284,401,1288,401,446,381,462,402,445,401,444,402,444,384,462,383,466,398,442,407,1286,401,1288,403,1288,404,1288,403,1288,401,1290,407,439,404,1284,406,441,404,441,409,436,408,439,407,1285,406,1285,403,1289,400,446,402,1288,403,1287,405,1284,404,1286,409,437,406,444,404,39760,3379,1697,402,1288,404,442,405,441,401,444,407,439,406,441,401,446,402,441,407,439,402,445,406,441,401,443,401,1290,405,437,404,446,405,441,402,441,402,442,406,441,403,441,404,1290,402,1290,400,446,404,439,407,439,406,439,404,442,404,442,406,1288,401,442,402,1290,385,462,401,1289,401,1290,404,1288,399,1288,404,441,386,1306,402,446,402,1287,403,446,401,442,402,444,401,445,404,441,402,444,402,444,402,446,400,1290,398,1290,402,1287,385,1309,404,1287,400,1289,403,1292,401,1283,390,1302,387,1304,404,445,384,460,408,436,405,442,385,462,402,443,404,441,404,442,385,1306,404,1287,402,1292,383,1307,401,1289,404,1290,400,443,405,1282,388,461,406,439,404,446,384,461,383,1303,404,1289,385,1303,405,442,404,1288,405,1286,404,1287,402,1290,403,442,406,440,405);
my $str = "";
my $bit = 1;
my $skip = 2;
for my $d (@data) {
    next if $skip-- > 0;
    if ($bit) {
        $bit = 0;
    } else {
        if ($d > 3000) {
            print unpack("h*", pack("b*", $str)). "\n";
            $str = "";
            $skip = 2;
        } elsif ($d > 1000) {
            $str .= "1";
        } else {
            $str .= "0";
        }
        $bit = 1;
    }
}
print unpack("h*", pack("b*", $str)). "\n";

実行結果を以下に示す。 3つの波形は同じデータ 「10010305fa00ff30cf2cd3」(低 nybble が先) の繰り返しだった。 前掲した PC-OP-RS1 形式への変換スクリプトで得た波形データは 454バイトもあったが、 3つの波形が同じなら最初の 1波形 124バイトだけでよいことになる。 PC-OP-RS1 は一度に送ることができる赤外線データが 240バイトという制限があるので、 1波形のみ送ることにした。

senri:~ $ ./iraeha.pl
10010305fa00ff30cf2cd3
10010305fa00ff30cf2cd3
10010305fa00ff30cf2cd3

以下は、 PC-OP-RS1 で赤外線の送信を行うスクリプト。 -d オプションで PC-OP-RS1 のデバイスを指定する。 受光部分が壊れてしまったので、赤外線を学習する機能はない。 前述したような方法 (Nature Remo 等の学習リモコンで元データを生成して変換) で赤外線の波形データを作成し、 連想配列 %Ir に設定する。

緊張しながらこのスクリプトを実行 「./pc-op-rs1 -d /dev/PC-OP-RS1 off」 すると...
みごと 日立LED照明器具 LEC-AHS810K が消灯した。 ということは日立製作所のメーカ識別コードが 0x1001 ってこと? どこかに家製協のメーカ識別コード (カスタマーコード) の一覧って無いだろうか? ちなみに「全灯」ボタンは「10010305fa00ff20df2cd3」だった。

#!/usr/bin/perl
use strict;
use warnings;
use Getopt::Std;
use Device::SerialPort;

my %Ir;
$Ir{'on'} = pack("H480", "ffffffff7f00e001f0f0783c1e8fc7e3f1783c001e1e8fc7e3f178003c001e1e8fc7e3f100783c001e1e000f8007c003e0e101f0f00078783c1e8fc7e3f10078003c001e000f8007c003e001f078003c1e0f8fc7c303e0e101f00078003c001e000f80c703e0e1e1f1f00078003c001e1e000f8007c003e0e1f1");
$Ir{'off'} = pack("H480", "ffffffff7f00e001f0f0f0f07878787878787878003c3c3c3c3c1e1e1e000f80c7c3c3c3c3c303e0e101f0f00078003c001e008f07c0e301f0783c1e1e0f0f0f0f8007c003e001f00078003c001e000f8007c0e3f1f078783c3c1e000f8007c003e001f000783c001e8fc7e301f00078003c1e000f8007c003e0f1f0");

our ($opt_v, $opt_d, $opt_c);
getopts("vd:c:") || help();
defined $opt_d || die "option -d is needed\n";

my $port = new Device::SerialPort($opt_d) || help();
$port->user_msg(1);
$port->error_msg(1);
$port->baudrate(115200);
$port->databits(8);
$port->parity("none");
$port->stopbits(1);
$port->handshake("none");
$port->read_const_time(100); # 0.1 sec
$port->read_char_time(5);
send_ir($port, "\x69");
recv_ir($port, 1, 3);

my $ch = 1;
if ($opt_c) {
    if ($opt_c =~ /^[1-4]$/) {
        $ch = $opt_c;
    } else {
        help();
    }
}

while ($_ = shift @ARGV) {
    defined $Ir{$_} || help();
    send_ir($port, "\x74")
        && recv_ir($port, 1, 3) eq "\x59"
        && send_ir($port, pack("C", 0x30+$ch))
        && recv_ir($port, 1, 3) eq "\x59"
        && send_ir($port, $Ir{$_})
        && recv_ir($port, 1, 3) eq "\x45"
        && next;
    die;
}
$port->close;
exit 0;


sub send_ir {
    my ($port, $data) = @_;
    $port->write($data);
    print STDERR "send: ", unpack("H*", $data), "\n" if $opt_v;
}

sub recv_ir {
    my ($port, $len, $timeout) = @_;
    my $i = 0;
    my $j = 0;
    my $data;
    while ($i < $len) {
        my ($l, $d) = $port->read(1);
        if ($l > 0) {
            $data .= $d;
            $i += $l;
            $j = 0;
        } else {
            $j++;
            if ($timeout > 0 && $j > $timeout) {
                print STDERR "TIMEOUT to read $len byte\n";
                return "";
            }
        }
    }
    print STDERR "recv: ", unpack("H*", $data), "\n" if $opt_v;
    return $data;
}

sub help {
    print STDERR <<EOF;
Usage: pc-op-rs1 [opt] <com>...
opt:   -d <dev>   device (MUST)
       -c <ch>    channel (1..4)
       -v         verbose
EOF
    print "com: ", join(" ", sort keys %Ir), "\n";
    exit 1;
}
2019年11月28日

IFTTT に登録できないのでお蔵入りになってた Eco Plugs RC-028W & CT-065W が、UDP パケットを送るだけでコントロールできた!

IoT 機器の多くが、 専用のスマホアプリだけでなく Googleアシスタントや Amazonアレクサからコントロールできる。 しかし、 いちいち音声でコントロールするのはメンドクサイ (なぜ音声以外の方法でもコントロールできるようにしないのか?)。 出かけるときに毎回 「行ってきま〜す」 などと Googleアシスタントに呼び掛けるのは、 いかがなものかと思う。 外出を勝手に検知して家電を適切にコントロール (例えば電気ポットの電源を切る) してくれるほうがずっといい。

IoT 機器を IFTTT に登録すると、 自前のプログラムからコントロールできるようになる。 IoT 機器は Googleアシスタントでコントロールするより、 自前のプログラムでコントロールするに限る。 例えば自宅の Wi-Fi LAN にスマホが繋がっているかプログラムで監視し、 繋がってるスマホがいなくなったら留守になったと判断して、 自動的に電気ポットの電源を切れば、 電気ポットのコンセントを抜いたかどうか出先で心配せずに済む。 あるいはコンセントを抜くのを忘れて寝てしまい、 翌朝電気ポットのお湯が熱いままなのを見て愕然とするより (先月の電気使用量が 600kWh だったので驚いた)、 部屋が暗いときは自動的に電源が切れている方がいい (これはプログラムを書かなくても IFTTT だけで実現できる)。

というわけで持ってる IoT 機器を片っ端から IFTTT に登録したのだけど、 IFTTT に登録できない IoT 機器も残念ながら若干ある。 いまどき IFTTT に登録できない IoT 機器に何の意味があるのだろう? (今なら絶対に買わない) と思うのだけど、 IFTTT の便利さを知る前に買ってしまったのだから後悔先に立たず。 IFTTT の便利さを知ってからは、 お蔵入りになっていた。

EcoPlugs RC-028W

Eco Plugs もそんな「使えない」IoT 機器の一つ。 当時としては安価だった (今ならもっと安い) ので Walmart で購入してしまった。 Googleアシスタントや Amazonアレクサには登録できるのに、 肝心の IFTTT に登録できない。

といって通信プロトコルを解析しようにも、 いまどきの IoT 機器はクラウド (ベンダが運用するサーバ) と https で通信するので調べる取っ掛かりがない。 最後の手段、 分解するしかないのか?

ところがググっていると、 Eco Plugs は平文で通信しているという投稿を見つけた。 Eco Plugs はクラウドに登録しなくても、 同一 LAN セグメントならスマホアプリでコントロールできるが、 同一 LAN 内では平文の UDP パケットを飛ばしているらしい。

ありがたいことに Eco Plugs をコントロールする JavaScript プログラムが GitHub に公開されていた。 JavaScript は文法もロクに知らない (^^; のだけど、 見よう見まねで perl で書き直してみる:

#!/usr/bin/perl
use strict;
use warnings;
use Getopt::Std;
use IO::Socket::INET;
our ($opt_v);
(getopts('v') && @ARGV == 3) || &help;
my ($ip, $id, $on) = @ARGV;

my $state = 0x0100;
$state = 0x0101 if $on eq "on";
my $buf = pack("H260", 0);
# Byte 0:3 - Command 0x16000500 = Write, 0x17000500 = Read
substr($buf, 0, 4) = pack("N", 0x16000500);
# Byte 4:7 - Command sequence num - looks random
substr($buf, 4, 4) = pack("N", rand(0xffffffff));
# Byte 8:9 - Not sure what this field is - 0x0200 = Write, 0x0000 = Read
substr($buf, 8, 2) = pack("n", 0x0200);
# Byte 16:31 - ECO Plugs ID ASCII Encoded - <ECO-xxxxxxxx>
substr($buf, 16, 16) = $id;
# Byte 116:119 - The current epoch time in Little Endian
substr($buf, 116, 4) = pack("L", time());
# Byte 124:127 - Not sure what this field is - this value works, but i've seen others 0xCDB8422A
substr($buf, 124, 4) = pack("N", 0xCDB8422A);
# Byte 128:129 - Power state (only for writes)
substr($buf, 128, 2) = pack("n", $state);

my $sock = IO::Socket::INET->new(PeerAddr => $ip, PeerPort => 80,
    Proto => 'udp', Timeout => 1) || die;
my $flags;
print unpack("H*", $buf) . "\n" if $opt_v;
print $sock $buf;
$sock->recv($buf, 1024, $flags);
print unpack("H*", $buf) . "\n" if $opt_v;

# Byte 10:14 - ASCII encoded FW Version - Set in readback only?
my $fwver = substr($buf, 10, 5);
# Byte 48:79 - ECO Plugs name as set in app
my $name = substr($buf, 48, 32);
$name =~ s/\0*$//;
printf("%s (ver %s)\n", $name, $fwver);

sub help {
    print <<EOF;
Usage: ecoplugs <opt> <IP> <ID> <on/off>
opt:   -v           ; verbose
EOF
    exit 1;
}

長さ 130 バイトの UDP パケット (変数 $buf) を作って Eco Plugs へ送信している (print $sock $buf;) だけなので、 いたってシンプル。 ユーザ認証もないので LAN 内なら誰でもコントロールできる。

Eco Plugs の IP アドレス (第1引数) と、 Eco Plugs の ID 「ECO-XXXXXXXX」(第2引数, XXXXXXXX は MACアドレスの第3〜6オクテット, ただし 16進数の A〜F は大文字限定)、 および「on」あるいは「off」の 3引数を付けて、 この perl プログラムを実行すると、 該当 Eco Plugs をオン/オフし、 Eco Plugs の名前 (スマホアプリで設定できる。以下の例では 「potplug」) と、 ファームウェアのバージョン (以下の例では 「1.6.3」) を表示する。

senri:~ $ ecoplugs -v 192.168.15.123 ECO-01234567 on
16000500940c163b020000000000000045434f2d303132333435363700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a625df5d00000000cdb8422a0101
160005000000163b0000312e362e330045434f2d30313233343536370000000000000000000000000000000000000000706f74706c7567000000000000000000000000000000000000000000000000003031323334353637000000000000000000000000000000000000000000000000a8e23b7ea625df5d00000000cdb8422a
potplug (ver 1.6.3)

「-v」オプションを付けた場合、 最初に表示される行が Eco Plugs へ送った長さ 130バイトの UDP パケット (260桁の 16進数)、 2行目が Eco Plugs から返ってきた長さ 128バイトの UDP パケット (256桁の 16進数)。 第1引数で指定した IP アドレスが Eco Plugs のものでなかった場合、 あるいは第2引数で指定した ID が間違っている場合など、 応答が返ってこない時は待ち続ける。 ID の 16進数において A〜F が小文字だと応答しないので注意。

Eco PlugsRC-028W (屋外用) および CT-065W (屋内用) で動作を確認したが、 おそらく同シリーズの他の機器でも使えるだろう。 Woods の WiON (スマホアプリが Eco Plugs そっくり) でも使えるらしい。

More...
2019年11月19日

IoT な人感センサをトリガーとした照明の点灯/消灯を IFTTT を使って行っていたけど反応が遅いので IFTTT をショートカットしてみた

さいきん流行りの IoT 機器。 多くの家電がネットからコントロールできるようになった。 IFTTT を使うと、 そういった機器を手軽に連携できるので便利。 IoT 機器同士だけでなく、 (私が管理する) WWW サーバを IFTTT がアクセスするように設定したり、 あるいは逆に私のサーバが IFTTT をアクセスする (トリガーを送る) こともできるので、 思いのままに IoT 機器を制御できる。

例えば、 人感センサで照明を点灯/消灯させる場合、 防犯用ライトなら人の動きを感知したときだけ点灯し、 人の動きが無くなれば速やかに消灯する、 といった単純なルールで充分だが、 部屋の照明となると人の動きが無くなったからと言ってすぐに消されては困る。 部屋を退出したことを確認してから消灯して欲しいし、 時間帯、あるいは在宅/不在時に応じて (さらにはその時々の天気に応じて)、 適切な点灯/消灯制御を行いたい。

つまり、 部屋の外にも人感センサを設置し、 部屋の中で人の動きが検知できなくなった後、 部屋の外で人の動きを検知すれば、 部屋を出ていったと判断し部屋の照明を消灯する。 さらに、 特定のスマホが LAN (家庭内 Wi-Fi) に接続していないときは不在とみなし、 部屋の外で人の動きを検知しただけでは照明を点灯しないけど、 そのスマホが LAN に接続した直後は帰宅したとみなし、 夜間であれば人の動きを検知したら速やかに点灯するなど。 私自身のサーバ (以下、「自サーバ」と略記) を IFTTT と連携させれば、 いくらでも複雑な制御ルールを設定できる。

IFTTT (IF This Then That) は、 その名の通り特定の条件 (This) が満たされたとき特定の動作 (That) を行わせることができる。 IoT 機器の多くは IFTTT との連携をサポートしているので、 例えば「This」として、 「人感センサが人の動きを検知」を設定し、 「That」として、 「照明をオン」を設定すれば、 単純な防犯用ライトが実現できる。

この連携に自サーバを絡めるには、 「This」および「That」を自サーバと結び付ければ良い。 それには IFTTT の 「webhooks」を用いる。

「This」は、 IFTTT の特定の URL をアクセスするだけ。 例えばこんな感じ:

senri:~ $ curl https://maker.ifttt.com/trigger/light_on/with/key/dD-v7GCx46LnWaF1AD9nwSUeA_N1ALvDHKS57cP1_Md
Congratulations! You've fired the light_on event

「light_on」の部分は任意に定めることができる。 「with/key/」以降の部分はユーザごとに IFTTT が割当てる認証用キー。 このキーが他人に漏れると勝手に操作されてしまうので適切な管理が必要。 そして、 「https://maker.ifttt.com/trigger/light_on/with/key/... へのアクセスがあった」(This) ならば、 「照明をオン」(That) を行う、 というルールを設定することで、 自サーバから照明を点灯させることが可能になる。

いっぽう 「That」 は、 IFTTT に自サーバをアクセスさせる。 例えば 「https://www.gcd.org/ifttt へ POST メソッドでアクセス」させる。 POST の body として json データを送るよう設定することができて、

{"magic": "0svikYKbcsxDbkty", "type": "Motion detected", "CreatedAt": "{{CreatedAt}}", "DeviceName": "{{DeviceName}}"}

などと設定する。 「"magic": "0svikYKbcsxDbkty"」は認証用。 https://www.gcd.org/ifttt は誰でもアクセスできるので、 "magic" の文字列が一致しないリクエストは無視する。 「"type"」は 「This」の機器の種類 (この例では人感センサ) を伝えるために設定。 「{{CreatedAt}}」と 「{{DeviceName}}」は、 「This」の機器が IFTTT へ送信したデータ。 例えば人感センサが検知 (This) すると IFTTT が次のようなアクセスを www.gcd.org へ行ってくれる (That)。

POST /ifttt HTTP/1.1
Content-type: application/json
host: www.gcd.org
content-length: 134
x-newrelic-id: ZW1uPtmAO9tRDSFGGvmp
x-newrelic-transaction: VGhpcyBpcyBmYWtlIHgtbmV3cmVsaWMtdHJhbnNhY3Rpb24uCg==
Connection: close

{"magic": "0svikYKbcsxDbkty",
 "type": "Motion detected",
 "CreatedAt": "November 19, 2019 at 09:15AM",
 "DeviceName": "廊下センサ"
}

この IFTTT からのアクセスを受信することで、 人感センサが人の動きを検知したことを自サーバが知ることができる。 そして自サーバにおいて様々な条件を加味した後、 前述した 「https://maker.ifttt.com/trigger/light_on/with/key/...」 へアクセスすれば照明を点灯することができる。

以上で、 IoT の連携に自サーバを絡ませることができるようになった。 ところがこの方法は、いかんせん遅い。 人感センサ ⇒ IFTTT ⇒ 自サーバ ⇒ IFTTT ⇒ 照明 などと IFTTT とのやりとりを 2度も行うため、 人の動きを検知してから照明が点灯するまで 6秒ほどかかってしまう。 部屋に入るまで 6秒も待てないので、 暗いままの部屋に入る羽目になる。 なお、 点灯するのは素早さが肝要だが、 消灯するのは数秒程度の遅れなら全く問題にならない。

More...
Filed under: システム構築・運用,ハードウェアの認識と制御 — hiroaki_sengoku @ 15:58
2019年9月4日

UEFI ブートでキーボードが無いと GRUB がハングするバグを修正してみた hatena_b

遅ればせながら手元の PC を MBR ブートから UEFI ブートに切り替えた。 ハードディスクの最初の 512バイト MBR (Master Boot Record) を読み込んで起動するのが MBR ブート。 Windows だと 2TB 超のディスクは MBR ブートできない (Linux ならブート可能)。 2TB では手狭になってきたのが切り替えを決意した理由だが、 ついでに Linux 専用マシンも PC のファームウェアが対応しているものは UEFI ブートに切り替えた。

UEFI (Unified Extensible Firmware Interface) だと、 普通の FAT32 ボリュームにブートローダのファイルを置いておくだけなので、 わざわざ MBR を書き換えたりするより簡単だし分かりやすい。 PC が起動しなくなった、などのトラブルはよくあるが、 トラブル発生時は気が急くし時間的余裕がないことも多いので、 トラブル時の作業は簡単であればあるほど、 分かりやすければ分かりやすいほど好ましい。

UEFI ブートに切り替えて 1ヶ月ほど経ったある日、 CPU を換装するために落としていた PC の電源を入れたら GRUB のメニュー画面でフリーズしたので私も凍り付いた。 CPU の性能をむやみに上げると、マザーボードとのミスマッチが起りがち。 せっかく買った新しい CPU が問題を起こしたのかと思った。 電源ボタンを長押しして強制的に電源を落とす。

BIOS 設定を確認するためにキーボードをつないで再度電源を入れてみる。 設定に何の問題もない。 続いて GRUB を立ち上げる。問題無く立ち上がる。 Linux を起動する。問題無い。 さっきのフリーズは何だったのだろう?

この時は因果関係に気付かなかったが、 キーボードをつないでいないと GRUB 2.04 (および最新版の 2.05 も) がメニュー画面のカウントダウンでハングする (最初の秒を表示したまま止まる)。 PC を再起動するときは、たいていキーボードをつないでいるので正常に起動し、 この症状は見たことがなかった。

実はこの時も、 なぜキーボードをつないでいなかったかというと明確な理由はない。 CPU 換装後の動作確認なのだからキーボードをつないでおくべきだと思うのだけど、 たまたまつなぐのを忘れていただけかも。 Web で 「headless GRUB hang bug UEFI without keyboard」 などを検索しても似た症例がほとんど見つからないのは、 キーボードをつながずに起動させる人がほとんどいないから?

とはいえ、 何台もあるサーバそれぞれにキーボードをつないでいては邪魔である。 意図して再起動するときはキーボードをつなぐなり KVM スイッチ (Keyboards, Video monitors, Mice Switch) を切り替えてキーボードが効く状態にするのが通常だが、 トラブルや Watchdog タイマーで勝手に再起動したとき、 あるいは遠隔から再起動させたときに、 立ち上がらずにフリーズしてしまったら非常に困る。 不測の再起動に備えて、 急遽テンキーをつないでおくことにした。

GRUB がキー入力を検知する状態であれば、 画面表示の有無自体は関係なくハングする。 逆に言うと、 GRUB がキー入力を検知しなければ、 例えば grub.cfg でキー入力を読む命令が無ければ (menuentry 等が無ければ) ハングしない。 例えば grub.cfg が次のように特定の Linux を起動するだけなら、 正常に起動できる。

insmod all_video
insmod part_gpt
insmod search_label
search --no-floppy --label --set=root /
linux /boot/linuz-4.19.69-x86_64 root=LABEL=/ resume=LABEL=swap ro
initrd /boot/initz-4.19.69-x86_64
boot

もちろんこれでは GRUB を使う意味がないが、 少なくとも問題が GRUB のキー入力関連にあることが分かった。 同じ PC、同じ GRUB でも、 UEFI ブートではなく MBR ブートなら、 キーボードをつながなくてもハングしない (MBR ブート専用の USB メモリを作って確認した。末尾の「おまけ」参照)。 また、UEFI ブートであっても UEFI ファームウェアによってはハングしない場合もあると思われる (未確認)。 が、少なくとも私の PRIMERGY MX130 S2 では確実に再現する。

というわけで、 問題の所在はおおむね絞り込めたので、 GRUB のソースコードを読み始めた。 並行して facebook に、 ことの顛末を書込んだ

すると野中尚道さんから grub-core/tem/efi/console.c が怪しそう、 とのヒントを頂いた。 ありがたい! なにぶん EFI のソースコードを読むのは初めてなので、 この時点ではまだ流れを追いきれていなかった。 efi/console.c の grub_console_getkey まわりを重点的に読んでみる。

grub-core/tem/efi/console.cのget_keyで key_exとkey_conを呼び分けている処理が失敗しているような気がします。 key_exの方はオプション機能なので実装有無を確認してるのですが

なるほど確かに grub_console_getkey_ex は、 grub_efi_open_protocol の返り値が NULL で無いとき (key_ex の実装が有るとき) に限り呼び出されているが、 grub_console_getkey_con には対応するものがない。 key_con は必ず実装されているから確認不要? でも、キーボードをつないでいない場合はどうなる? key_ex の有無を確認するコード:

text_input_ex_guid = GRUB_EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL_GUID;
        ...
text_input = grub_efi_open_protocol(grub_efi_system_table->console_in_handler,
                                    &text_input_ex_guid,
                                    GRUB_EFI_OPEN_PROTOCOL_GET_PROTOCOL);

をマネして、 key_con の有無を確認するコードをでっち上げてみる。 単に 「_EX_」 の部分を取り除いただけだが、 キーボードをつないでいるか否かを正しく検出しているようだ。

text_input_guid = GRUB_EFI_SIMPLE_TEXT_INPUT_PROTOCOL_GUID;
text_input = grub_efi_open_protocol(grub_efi_system_table->console_in_handler,
                                    &text_input_guid,
                                    GRUB_EFI_OPEN_PROTOCOL_GET_PROTOCOL);

あとはキーボードの有無に応じて grub_efi_console_input_init の返り値を変えるだけ。 grub_efi_console_input_init が 1 (非ゼロ?) を返せば、 GRUB は grub_console_getkey を呼ばなくなるようだ。

以上をまとめると、 次のようなパッチになった:

diff --git a/grub-core/term/efi/console.c b/grub-core/term/efi/console.c
index 4840cc5..f2be32f 100644
--- a/grub-core/term/efi/console.c
+++ b/grub-core/term/efi/console.c
@@ -207,7 +207,12 @@ grub_efi_console_input_init (struct grub_term_input *term)
                                       GRUB_EFI_OPEN_PROTOCOL_GET_PROTOCOL);
   term->data = (void *)text_input;
 
-  return 0;
+  if (text_input) return 0;
+  grub_efi_guid_t text_input_guid = GRUB_EFI_SIMPLE_TEXT_INPUT_PROTOCOL_GUID;
+  text_input = grub_efi_open_protocol(grub_efi_system_table->console_in_handler,
+                                      &text_input_guid,
+                                      GRUB_EFI_OPEN_PROTOCOL_GET_PROTOCOL);
+  return text_input == NULL;
 }
 
 static int

わずか 6行だが、 これで GRUB がハングしなくなった。

More...
Filed under: システム構築・運用,プログラミングと開発環境 — hiroaki_sengoku @ 21:21
2019年8月26日

Windows 10 上の VMware Workstation Player のゲストOS でタグVLAN を使ってみる

Windows 上の仮想化ソフトウェアの定番 VMware Workstation Player で、 タグVLAN (tagged VLAN, IEEE 802.1Q) を使うことはできないと今まで思い込んでいたが、 レジストリエディタ (regedt32.exe) でレコードを一つ追加するだけで使えるようになった。 ググっても Windows 上のゲストOS でタグVLAN を使う話は見かけないのでメモしておく。

私の自宅内LAN はタグVLAN を利用している。 つまり物理的な配線は一本のイーサケーブルで、 複数のネットワーク (宅内LAN, DMZ, 対外セグメントなど) を同居させている。 タグVLAN はオフィス等で使われることが多いが、 美観上の理由からケーブルを何本も這わすわけにはいかない家庭内LAN においてこそ、 タグVLAN は有用と思う。

Linux OS などタグVLAN 対応の OS が走るマシンへは、 タグが付いたままのパケットを流し、 Windows OS などタグVLAN に非対応な OS が走るマシンへは、 スマートスイッチでタグを取り除いた (通常の) パケットを流している。

ところが、 マシンによっては Linux と Windows の両方の OS を走らせることがある (いわゆるデュアルブート)。 OS を切り替えるたびにスマートスイッチの設定を変更するのは面倒なので、 そういうマシンには Linux と Windows の両方がアクセスするネットワーク (つまり自宅内LAN) のパケットはタグ無しで流しつつ、 Linux のみがアクセスするネットワーク (DMZ や対外セグメント) のパケットはタグ付で流すことになる。

Windows が立ち上がってるときもタグ付パケットが届くが無視される。 Linux が立ち上がっているときはタグ付、タグ無し両方のパケットを受け取る。

さいきんは PC のメモリも大きくなり、 普段は Windows を使うマシンでも、 VMware Workstation Player (以下 VMware と略記) などの仮想化ソフトウェアを使って Linux をゲストOS として走らせておくことが増えてきた。 デュアルブートよりも同時に走らせておくほうが便利に決まってる。 となってくると、 ゲストOS でもタグVLAN を使いたくなるのが人情というもの。

ゲストOS でタグVLAN が扱えると、 本来 DMZ へ置くべきようなサーバ (WWW サーバとか) をゲストOS 上で動かすことが可能になる。 また、 ゲストOS でも自宅内LAN とインターネットとの両方のネットワークへアクセスできるわけで、 ルータの役割を担わせてもよい。 いずれにしても仮想マシンの応用範囲が一気に広がる。

しかし VMware を使って起動した仮想マシン上では、 (仮想)ネットワークインタフェースにタグ付パケットが上がってこないので、 ゲストOS でタグ付パケットを拾うことができない。 VMware がタグVLAN に対応していないのだろうと諦めていた。 まあタダで利用させてもらってるソフトウェアだし、 対応してなくても仕方がないなぁと。

ところが、 実は VMware 自体はタグVLAN に対応しているらしい。 Windows 10 がタグ付パケットを落としているから、 VMware までパケットが届かないということらしい。 細かく言えば Windows 10 が悪いというよりは、 ネットワークインターフェース (以下 NIC と略記) のドライバがタグ付パケットを落としているらしい。

私が使ってる Windows 10 では NIC が 「Broadcom NetLink (TM) Gigabit Ethernet」 と表示されるので、 「windows10 broadcom capture vlan」 あたりのキーワードで検索していたら、 Windows 上でタグ付パケットをキャプチャする方法について書かれたページを見つけた。

なお google では同じキーワードを使って検索しても、 このページを見つけ出すことはできなかった。 ニッチなものを見つけたい場合に、 google 検索が全く役に立たなくなったのは、 google がモバイル検索にシフトし始めた頃だったろうか? 探しているページが全く見つけられないので、 最近は google 検索を使わなくなってしまった。 google お得意の AI で賢く検索するより、 バカ正直にキーワード検索してくれたほうが (少なくとも私にとっては) 役に立つ。 Stay Foolish !

この見つけたページには、 Windows 上のパケットキャプチャソフトウェア Wireshark でタグ付パケットを見る方法が書かれている:

Display VLAN tags in Wireshark on laptops with Broadcom B57 chipsets から引用:

In order to make these tags visible to Wireshark, specialized drivers or specific NICs that support VLAN tags are usually needed. In the case of the Broadcom B57 chipset in some Dell Latitude laptops, the NIC itself supports VLAN tags (display only, it cannot actively tag outgoing traffic) with a small registry modification and a specific driver.

(意訳) Wireshark でタグ付パケットを見るには、 NIC ドライバがタグVLAN に対応している必要がある。 Broadcom B57 チップセット自体はタグVLAN に対応しているので、 ドライバのレジストリをいじればタグ付パケットを見ることができるようになる。 ただし見るだけでタグ付パケットを送信することはできない。

このページには 「送信できない」 と書いてあるが、 後述するようにゲストOS からタグ付パケットを送信できている。 Wireshark と VMware とでは違うのかも?

レジストリをいじるには、 まず NIC ドライバのインスタンスを見つけなければならない。 このページでは 「TxCoalescingTicks」 を検索せよと説いている。 Broadcom B57 チップセットのドライバのインスタンスであれば、 この名前のレコードを必ず持っているからだろう。

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Class\{クラスID}\番号 を見つけたら、 このインスタンスに 「PreserveVlanInfoInRxPacket」 という名前で文字列値 「1」 を追加すればよい。

ちなみに、 「tx coalesce ticks」 を WWW で検索すると Broadcom 関連のページばかりが出てくる。 Broadcom 製 NIC 特有の機能なのかも? Linux の BCM5700 用ドライバ にも tx_coalesce_ticks というパラメータがあるようだ。

というわけで、 Broadcom 製 NIC でタグ付パケットを落とさないようにする方法は以下のようになる:

VLAN capture setup から引用:

  1. Run the Registry Editor (regedt32).
  2. Search for "TxCoalescingTicks" and ensure this is the only instance that you have.
  3. Right-click on the instance number (eg. 0008) and add a new string value.
  4. Enter "PreserveVlanInfoInRxPacket" and give it the value "1".

意訳:

  1. レジストリエディタ regedt32 を起動する
  2. 「TxCoalescingTicks」 を検索する。 見つけたインスタンスが (Backup 用のインスタンスを除いて) 唯一であることを確認する。
  3. 見つけたインスタンス番号 (例えば 0008) を右クリックし、 「新規 文字列値」 を追加する。
  4. 「PreserveVlanInfoInRxPacket」 を入力し、 その値を 「1」 とする。

Intel 製 NIC の場合も、 ドライバのレジストリをいじることで、 タグ付パケットを落とさないようにできるらしい。

レジストリをいじった後は、 再起動すれば NIC ドライバがタグ付パケットを捨てずに受け取るようになっている。 ドライバが捨てなければ、 タグ付パケットは Windows OS から VMware に渡され、 ゲストOS まで届く。 逆にゲストOS から発せられたタグ付パケットは、 Windows OS から NIC へ送られ、 ネットワークへ送出される。 つまりゲストOS がタグVLAN を扱えるようになった。

Filed under: システム構築・運用 — hiroaki_sengoku @ 09:06
2018年4月12日

NETGEAR のスマートスイッチ GS108E/GS116E のポートごとの通信量を取得して Cacti でグラフ化してみた 〜 通信量を見ることができない Wi-Fi中継機への対処法 〜

たまたま買った Wi-Fiアクセスポイント (以下 Wi-Fi AP と略記) が、 SNMP 非対応なのはもちろん、 Web管理画面でさえ通信量を見ることができなかったので、 この Wi-Fi AP を接続しているスイッチ (ネットワークハブ) NETGEAR GS108E (と GS116E) のポートの通信量を取得して Cacti (最近の流行りは Grafana ?) で可視化してみた。 Wi-Fi そのものの通信量ではないが、 Wi-Fi AP とスイッチとの間の通信量を測ることができる。

買ったのは NETGEAR WiFi中継機/802.11ac wave2 Nighthawk X4 EX7300-100JPS (以下 EX7300 と略記)。 たまたま amazon で税込5,980円 (送料無料) で売っていて、 中継機 (ワイヤレスエクステンダー) としてだけでなくアクセスポイントとしても使えるとのことなので (NETGEAR製ということもあって) 衝動買い。 もともとルータの機能は必要ないし、 コンセント直挿しで場所を取らないので、 リビング用としてちょうどいいかなと思った次第。

さいきん Wi-Fi中継機が流行りらしく、 各社からお手頃価格で多数の製品が発売されている。 コンセント直挿しで場所を取らないので家庭向きと言えるが、 残念なことにアクセスポイントとして使える製品は (NETGEAR や TP-Link などのごく一部の例外を除くと) ほとんど無い。
中継機も (広義の) アクセスポイントと言えるが、 親機 (Wi-Fiルータ) と無線でつながるのが中継機で、 有線でつながるのが (狭義の) アクセスポイント。 あたりまえだが、 有線でつなぐことができる (家庭内でケーブルを配線できる) なら、 有線のほうがいいに決まってる。
ちなみに私が初めて Wi-Fi中継機を使ったのは 10年前なので、 いまさら流行って少々戸惑いを覚える。

EX7300 は、 たった 6000円なのに性能的には悪くない。 ルータ機能が不要ならお買得と思う。 ノートPC Lavie HZ を Wi-Fi で EX7300 につないで iperf3 を走らせて測定してみたら 530Mbps くらい出ている。

C:\Users\sengoku>c:\bin\iperf3.exe -c esaka -P 5
Connecting to host esaka, port 5201
[  4] local 192.168.18.147 port 50119 connected to 192.168.18.20 port 5201
[  6] local 192.168.18.147 port 50120 connected to 192.168.18.20 port 5201
[  8] local 192.168.18.147 port 50121 connected to 192.168.18.20 port 5201
[ 10] local 192.168.18.147 port 50122 connected to 192.168.18.20 port 5201
[ 12] local 192.168.18.147 port 50123 connected to 192.168.18.20 port 5201
[ ID] Interval           Transfer     Bandwidth
[  4]   0.00-1.00   sec  13.4 MBytes   112 Mbits/sec
[  6]   0.00-1.00   sec  13.2 MBytes   111 Mbits/sec
        ...

[SUM]   9.00-10.01  sec  63.9 MBytes   533 Mbits/sec
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bandwidth
[  4]   0.00-10.01  sec   130 MBytes   109 Mbits/sec                  sender
[  4]   0.00-10.01  sec   130 MBytes   109 Mbits/sec                  receiver
[  6]   0.00-10.01  sec   128 MBytes   108 Mbits/sec                  sender
[  6]   0.00-10.01  sec   128 MBytes   108 Mbits/sec                  receiver
[  8]   0.00-10.01  sec   128 MBytes   107 Mbits/sec                  sender
[  8]   0.00-10.01  sec   128 MBytes   107 Mbits/sec                  receiver
[ 10]   0.00-10.01  sec   126 MBytes   106 Mbits/sec                  sender
[ 10]   0.00-10.01  sec   126 MBytes   106 Mbits/sec                  receiver
[ 12]   0.00-10.01  sec   124 MBytes   104 Mbits/sec                  sender
[ 12]   0.00-10.01  sec   124 MBytes   104 Mbits/sec                  receiver
[SUM]   0.00-10.01  sec   636 MBytes   534 Mbits/sec                  sender
[SUM]   0.00-10.01  sec   636 MBytes   534 Mbits/sec                  receiver

iperf Done.

たまたま手元にあった TP-Link WiFi 無線LAN ルーター Archer C3150 11ac にも Wi-Fi でつないで同様に iperf3 を走らせて比較してみたら、 ほとんど差がない。 同等と言ってもいいレベル。 値段は 3倍くらい違うのだから、 EX7300 をブッチぎって欲しかった。

しかもこの Archer C3150 は OpenVPN を使ってると腐るバグがあるように感じる (いちど電源を切らないと復旧しない)。 ヘンテコな付加機能よりも安定性こそ一番重要なのではないか? ついでに言うと勝手に外部と通信しているのが気持ち悪い。 値段が高い Wi-Fiルータにはもうちょっと頑張ってもらいたいところ。

もちろん、 このノートPC を無線LAN ではなく有線LAN につなげば 940Mbps くらいは出るので、 ノートPC の性能がボトルネックになっているわけではない。 というか Wi-Fi のパフォーマンスは周囲の電波状況に強く影響され (もちろん空いてるチャンネルを使っている)、 有線側の状況はほとんど関係がない。 電波暗室とかで測定すれば、 EX7300 と Archer C3150 の差が開くのかもしれないが、 実用上は無意味。

性能でも安定性でも申し分のない EX7300 ではあるが、 残念なことに Web管理画面を隅から隅まで調べてみても通信量を見る方法がない。 どんな安物 Wi-Fiルータ (or Wi-Fi AP) でも、 通信量 (パケット数とか) を表示させる方法はあるものだと思っていたが、 中継機は Wi-Fiルータとは製品コンセプトが違うのかも?

通信量を見ることができない通信機器があるとは思っていなかったので、 意表を突かれた感じ。 今後 Wi-Fi AP を買う際は注意したい。 やっぱり (ルータ機能が必要なくても) 素直に Wi-Fiルータを買うべきなのかも。 ほんとうは (ルータ機能がない) アクセスポイント専用機が欲しいのだけど...

幸い、 この EX7300 を接続しているスイッチ NETGEAR GS108E が「スマート」で、 スイッチの各ポートごとの通信量 (≒ Wi-Fi AP の通信量) を GS108E/GS116E 付属の Windows アプリ ProSafe Plus で見ることができる。

ProSafePlus GS116E Port Statistics

Web アプリではなく Windows アプリなので、 表示された通信量の数値を Web スクレイピングなどのお手軽な方法で取り込むことはできないが、 google で検索したら ProSafe Plus と同等のことができる Python プログラムが見つかった。 6年前に開発が終了しているが、 このスイッチを買ったのも 6年前なので無問題。

さっそくダウンロードして (ざっと内容を確認したのち) 走らせてみる:

senri $ git clone https://github.com/Z3po/Netgearizer.git ↵
Cloning into 'Netgearizer'...
remote: Counting objects: 88, done.        
remote: Total 88 (delta 0), reused 0 (delta 0), pack-reused 88        
Unpacking objects: 100% (88/88), done.
Checking connectivity... done.
senri $ Netgearizer/netgearizer.py ↵
please select one of the following switches with "selectSwitch $NR"
--> 0: 192.168.18.239
Information: 
switch-name: living
switch-dhcp: disabled
switch-type: GS108Ev2
switch-ip: 192.168.18.239
switch-mac: 4c:60:de:69:01:23
switch-firmware: 1.00.06
switch-netmask: 255.255.255.0
switch-gateway: 192.168.18.1
(Cmd) 

スイッチは 3つ (GS116E と GS108E 2台) あるのに、 なぜか 1つ (リビングに設置した GS108E) しか表示されない。 selectSwitch コマンドでスイッチを選択し (選択肢は 1つしか無いが)、 getPortStatistics コマンドでポートごとの通信量を表示できるようだ。

実行してみる:

(Cmd) selectSwitch 0 ↵
(Cmd) getPortStatistics ↵
switch-port-statistics: 
 -> Port 01: 
   >> send: 2664610542955
   >> receive: 5993649462240
   >> crcerrors: 0
 -> Port 02: 
   >> send: 6062498001
   >> receive: 328715447216
   >> crcerrors: 0
 -> Port 03: 
   >> send: 183424114643
   >> receive: 8723285000144
   >> crcerrors: 0
 -> Port 04: 
   >> send: 39935317815
   >> receive: 26550837425456
   >> crcerrors: 0
 -> Port 05: 
   >> send: 137960667631
   >> receive: 790398362288
   >> crcerrors: 0
 -> Port 06: 
   >> send: 3948410207
   >> receive: 1156872131840
   >> crcerrors: 0
 -> Port 07: 
   >> send: 2789623
   >> receive: 20875141728
   >> crcerrors: 0
 -> Port 08: 
   >> send: 1989939088
   >> receive: 2852348890112
   >> crcerrors: 0
(Cmd) 

一見、 正しく動いているように見えるが、 「send:」に表示されている数値は (ProSafe Plus で見たときの) 受信バイト数で、 「receive:」に表示されている数値は送信バイト数を 16倍した値になっている。

受信と送信が入れ替わっているのはご愛敬だが、 16倍になっているのは... netgearizer.py (Python で書かれたスクリプト) を読んでみると、 単純なバグだった。 次のパッチをあてると正常に動作した:

--- netgearizer.py.org        2018-04-07 22:04:42.292912972 +0900
+++ netgearizer.py        2018-04-09 11:34:50.124297214 +0900
@@ -284,5 +284,5 @@
             for port in hexvalue:
-                sendstats = self.__convertFromHex(port[2:18],'cipher')
-                receivestats = self.__convertFromHex(port[19:35],'cipher')
-                crcerrors = self.__convertFromHex(port[36:53],'cipher')
+                receivestats = self.__convertFromHex(port[2:18],'cipher')
+                sendstats = self.__convertFromHex(port[18:34],'cipher')
+                crcerrors = self.__convertFromHex(port[82:98],'cipher')
                 result.append(( 'Port ' + str(port[:2]), (('send', sendstats), ('receive', receivestats), ('crcerrors', crcerrors))))
@@ -345,15 +345,16 @@
                 for key in self.switches.keys():
                     self.switchList.append(key)
                     print '--> ' + str(counter) + ': ' + key
                     print 'Information: '
                     self.selectedSwitch = key
                     self.__printResult(True)
                     self.selectedSwitch = None
-                    return True
+                    counter += 1
+                return True
             else:
                 print 'please select one of the switches you get with getSwitches first.'
                 return False
 
         if 'ERROR' in resultdict:
             found = None
             for key in  self.switchattributes.keys():

送信バイト数が 16倍になるのは、 部分文字列の切り出しで「port[18:34]」 (変数「port」の文字列の 18文字目から 34文字目まで切り出す) とすべきところを、 「port[19:35]」 としてしまっていたため。 変数「port」には、 スイッチから受信したデータを 16進数に変換した文字列が格納されている。 1文字ずれて切り出したため、 16倍 (16進数で 1桁ぶん) になっていた。

16進数で 16文字つまり 64bit だが、 ProSafe Plus で表示される GS116E の送受信バイト数が小さすぎると思っていたら、 GS116E はバイト数が 32bit (4GiB) でラップアラウンドしているようだ。 つまり上位 32bit は常にゼロ。ポートによっては毎日ラップアラウンドしている。
GS108Ev2 は 40bit (1TiB) を超えるバイト数になっているポートもあるので、 どこでラップアラウンドするか確認できるまで、 まだだいぶかかりそう。 仮に 1ヶ月で 1TiB くらいの通信量だとすると、 もし 48bit (256TiB) が上限なら 20年もかかってしまう。
もし 64bit 全部使ってカウントしているなら、 上限は 16EiB (エクスビバイト) ということになり、 140万年くらいかかってしまう。 ちょっとのコスト増を惜しんでラップアラウンドするより、 64bit きっちり使って半永久にラップアラウンドしないほうがいい。

実は、 私は今まで Python でプログラミングしたことが無い。 文法もろくに知らなかったのだけど、 部分文字列を切り出す際の Python の位置の指定方法は独特だと思う。 このプログラムの作者も Python は慣れていなくて、 うっかりこのようなバグを作り込んでしまった (前の行で 18文字目まで切り出したので、 その次は 19文字目からとしたくなる気持ちは分かる) のではないか?

さらに興味深いのが、 3つあるはずのスイッチが 1つしか見つからなかったバグ。 なんと、 「return True」文のインデント (字下げ) が間違っていただけ。 インデントでブロックを表わす Python ならではのバグ?

インデントが一段階深かったので、 「return True」文が forループの中に入ってしまい、 必ずループが 1回だけで終了してしまっていた。 だからスイッチを 1つ見つけてすぐループを終了していた。 「return True」文の前の空白を削除してループの外へ出したら、 正しく 3つのスイッチを見つけるようになった。 空白の有無で挙動が変わってしまうのは、 やっぱり気持ち悪い。

この netgearizer.py は対話的にコマンドを入力するソフトウェアだが、 通信量を自動計測する際には使いにくいので、 対話せず通信量を取得するだけのプログラムに書き換えた。 不要な機能をばっさり削除したので、 プログラムの行数が半分以下の 317行になった。 この改変版は、

https://www.gcd.org/sengoku/docs/netgearizer.py

からダウンロードできる。 わずかな改変とはいえ、 なにぶん私が初めて書く Python プログラムなので、 変なところがあったらご指摘頂けると幸い。

この改変版を実行する (引数としてインタフェースを指定する) と、 以下のように LAN 内 (同一セグメント内) の全ての NETGEAR スマートスイッチ (GS116E, GS108E, GS105E など) の、 各ポートの受信バイト数、送信バイト数、エラー数を表示する。

senri:~ $ netgearizer.py eth0 ↵
IP:   192.168.18.239
MAC:  4c:60:de:69:01:23
Type: GS108Ev2
switch-port-statistics: 
 Port01 rx:2673873720246 tx:375671471563 err:0
 Port02 rx:6410023533 tx:21992185424 err:0
 Port03 rx:183424114643 tx:545205312509 err:0
 Port04 rx:40052812544 tx:1664344574414 err:1
 Port05 rx:138616484625 tx:52251007903 err:0
 Port06 rx:3954090302 tx:72431873899 err:0
 Port07 rx:2802623 tx:1308888305 err:0
 Port08 rx:1989939088 tx:178271805632 err:0
IP:   192.168.18.237
MAC:  4c:60:de:69:04:56
Type: GS108Ev2
switch-port-statistics: 
 Port01 rx:786331851 tx:1283347 err:0
 Port02 rx:0 tx:0 err:0
 Port03 rx:0 tx:0 err:0
 Port04 rx:0 tx:0 err:0
 Port05 rx:0 tx:0 err:0
 Port06 rx:474556 tx:875905 err:0
 Port07 rx:0 tx:0 err:0
 Port08 rx:0 tx:0 err:0
IP:   192.168.18.236
MAC:  84:1b:5e:90:81:72
Type: GS116E
switch-port-statistics: 
 Port01 rx:2245190893 tx:3192491053 err:0
 Port02 rx:2079058908 tx:4030809209 err:0
 Port03 rx:0 tx:0 err:0
 Port04 rx:2815311290 tx:1592352771 err:0
 Port05 rx:3838533539 tx:2573311206 err:0
 Port06 rx:1441584273 tx:1249733039 err:0
 Port07 rx:747795050 tx:97733030 err:0
 Port08 rx:889171012 tx:559797414 err:0
 Port09 rx:3757714660 tx:749158734 err:0
 Port0a rx:3210386057 tx:2724670950 err:0
 Port0b rx:441710237 tx:2183781562 err:0
 Port0c rx:0 tx:0 err:0
 Port0d rx:4837745 tx:252496639 err:0
 Port0e rx:2058650283 tx:3610878767 err:0
 Port0f rx:1435416601 tx:403439849 err:0
 Port10 rx:0 tx:0 err:0
senri:~ $ 

私の自宅の LAN ではタグVLAN を使っているため、 以上のコマンドを実行している Linux マシン「senri」の eth0 インタフェースには IP アドレスを割当てていないが、 問題なく動作している。 しかも、 この Linux マシンは GS116E の Port04 に接続していて、 2台の GS108E には GS116E 経由で間接的に繋がっているのだけど、 GS108E の通信量も取得できている。 Windows アプリの ProSafe Plus だと、 スイッチに直接繋げないと管理できないことがある。

改変版 netgearizer.py で取得した送受信バイト数を Cacti (最近の流行りは Prometheus とか Fluentd ?) に読み込ませて EX7300 の通信量をグラフ化すると、 こんな感じ:

NETGEAR EX7300 Cacti

In がスイッチのポートの送信バイト数、 つまりスイッチから EX7300 が受信したバイト数、 すなわち Wi-Fi 子機 (スマホなど) がダウンロードした合計。

Out がスイッチのポートの受信バイト数、 つまりスイッチへ EX7300 が送信したバイト数、 すなわち Wi-Fi 子機がアップロードした合計。

実は ProSafe Plus のもう一つの Python 実装である ProSafeLinux も試してみたのだが、 IP アドレスを割当てていないインタフェースを受付けないので、 先に netgearizer.py の書き換えを試みた次第。

ProSafeLinux のほうが (active ではないにせよ) 今でも開発が続いているし、 前述したようなバグも無いようなので、 試してみるべきだとは思うが、 プログラムの行数が netgearizer.py の倍以上 (私が作った改変版の 5倍以上) もある。 通信量の取得といった単純な目的であれば、 短い方がいい。

Filed under: システム構築・運用 — hiroaki_sengoku @ 10:10
Older Posts »