仙石浩明の日記

ハードウェアの認識と制御

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...
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...
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
2013年11月8日

Z80 コンピュータを作ってみた (27年前のお話) tweets

往年の 8ビット・マイクロプロセッサー Z80。 最近の若い IT エンジニアだと知らない人も多い? 現在でも組み込み用途で使われているのに、 プログラマの高齢化が進んでいるらしい。 私がコンピュータを学んだ思い出深い CPU なので、 このまま忘れ去ってしまうのもモッタイナイ。 思い出せる限り記録に残しておこうと思う。 コンピュータを原理から学ぼうとする人の参考になれば幸い。

以下は、私が大学一回生のとき (1986年, 昭和61年) 独学で作った CP/M (Control Program for Microcomputer, パソコン用シングルタスク OS) マシンの記録。

私は高校生のとき (1983年)、 シャープ製パソコン MZ-80K2E改造しながら独力でデジタル回路を学んだ。 当時のコンピュータ雑誌 (工学社 「I/O」 誌) に掲載された MZ-80K の回路図が大いに参考になった。 1983年版 TTL IC 規格表を片手に MZ-80K の回路図を読み解いて、 コンピュータの仕組みを学んだ。

受験勉強そっちのけでデジタル回路の勉強にのめり込んでしまったので一浪する (1985年) はめになったが、 無事大学に合格した後はたっぷり時間があったので、 Z80 コンピュータを作ってみた。 MZ-80 に倣って当時これを HZ-80 と呼んでいたので、 以下 HZ-80 と呼ぶ。

HZ-80 の写真 ↓ (上面 / 下面)

HZ80

HZ-80 にはディスプレイもキーボードも無いので、 改造した MZ-80K2E とパラレル通信ケーブルで結び、 MZ-80K2E を端末として使用した。 写真 ↑ 中央に見える黒色の 40ピン ソケットに、 パラレル通信ケーブルをつなぐ。 MZ-80K シリーズは画面が横 40文字しか表示できないので、 (MZ 本体とは別に) 80文字表示できるビデオ信号生成回路を作り、 MZ-80K2E の CRT へ出力して、 80桁x25行キャラクタ表示端末として使っていた。

写真左手に見える 36ピン アンフェノール ソケット (セントロニクス仕様のプリンタ用と同じ形状) に、 (PC-8001/8801シリーズ用の) 5インチ フロッピー ディスク ドライブ (以下 FDD と略記) をつなぐ。

写真右手に見える赤と黒の端子に +5V のスイッチング電源をつなぐ。

More...
Filed under: ハードウェアの認識と制御 — hiroaki_sengoku @ 07:57
2009年9月29日

無線LANコントローラ VIA VT6656 を Linux 2.6.30.8 で使ってみる

VIA Technologies の無線LAN コントローラ チップ VT6656 は、 SOTEC C10x や工人舎SC/SX などのネットブック / UMPC や、 Wyse USB 802.11 b/g Wireless Network Adapter などの USB 無線LAN アダプタなどで広く使われている。 香港で買った OLEVIA X10A-1160 でも、 このチップが使われてた (SOTEC C101 と同型機だから当然)。

幸い、 VIA Technologies が VIA Arena にて Linux 用の GPL なドライバ (vntwusb カーネル モジュール) VT6656_Linux_src_v1.19_12_x86.zip を公開しているので、 Linux 2.6.28 までは 簡単に無線LAN を利用できる。 Linux 2.6.29 になって、 「struct net_device」 (カーネル ソース include/linux/netdevice.h の中で定義されている) が変更されたので、 そのままではコンパイルできなくなったが若干のパッチをあてることで対応可能 (ではあるが、後述する方法のほうがお手軽)。

ところが、 VIA Arena が模様替えして、 上記無線LAN ドライバがダウンロードできなくなってしまった。 正確に言うと、 VT6656_Linux_src_v1.19_12_x86.zip のファイル自体はまだダウンロードできるのだが、 そこへ至るページが無くなってしまっている (私が見つけられないだけならいいのだが...)。 これは VIA Technologies としてはサポートを行なわないという意思表示なのか?

VIA Technologies がサポートしてくれなくても、 GPL で公開されているのだからなんとかなるだろうと思っていたら、 既に Linux 2.6.32-rc1 に Staging drivers (not of the "normal" Linux kernel quality level) として含まれていた。 内容を見てみると、 VIA Arena で公開されていたドライバほとんどそのままである。

というわけで、 Linux 2.6.32-rc1 のディレクトリ drivers/staging/vt6656 を Linux 2.6.30.8 の drivers/staging/ の下へそのままコピーし、 以下のパッチ (2行追加するだけ) をあててカーネルを再構築してみたら、 vt6656_stage.ko が生成された。

diff -ur linux-2.6.30.8.org/drivers/staging/Kconfig linux-2.6.30.8/drivers/staging/Kconfig
--- linux-2.6.30.8.org/drivers/staging/Kconfig        2009-09-25 00:28:02.000000000 +0900
+++ linux-2.6.30.8/drivers/staging/Kconfig        2009-09-28 13:49:36.026075562 +0900
@@ -115,5 +115,7 @@
 
 source "drivers/staging/serqt_usb/Kconfig"
 
+source "drivers/staging/vt6656/Kconfig"
+
 endif # !STAGING_EXCLUDE_BUILD
 endif # STAGING
diff -ur linux-2.6.30.8.org/drivers/staging/Makefile linux-2.6.30.8/drivers/staging/Makefile
--- linux-2.6.30.8.org/drivers/staging/Makefile        2009-09-25 00:28:02.000000000 +0900
+++ linux-2.6.30.8/drivers/staging/Makefile        2009-09-28 14:06:25.949515761 +0900
@@ -40,3 +40,4 @@
 obj-$(CONFIG_HECI)                += heci/
 obj-$(CONFIG_LINE6_USB)                += line6/
 obj-$(CONFIG_USB_SERIAL_QUATECH_ESU100)        += serqt_usb/
+obj-$(CONFIG_VT6656)                += vt6656/

起動してみると自動認識してドライバが組み込まれた:

vt6656_stage: module is from the staging directory, the quality is unknown, you have been warned.
VIA Networking Wireless LAN USB Driver 1.19_12<5>VIA Networking Wireless LAN USB Driver Ver. 1.19_12

vntwusb.ko と vt6656_stage.ko でモジュール名は異なるが、 バージョンはどちらも 1.19_12 で同じ。 「Staging」 だから 「Linux カーネルの品質レベルではない」 とのことだが、 今のところ WEP および WPA2 EAP-TLS で安定して接続できている。

Filed under: ハードウェアの認識と制御 — hiroaki_sengoku @ 19:21
2009年1月19日

SOTEC C101 (OLEVIA X10A-1160) で Linux (Ubuntu) を使う場合の注意点

香港の旺角電脳中心で買った OLEVIA X10A-1160 は、 Ubuntu 8.04 LTS がプレインストールされていた。 そのまま使うぶんには問題無いが、 Ubuntu のアップデートをインストールしてしまうと (正確に言えばカーネルを更新すると)、 OLEVIA X10A-1160 固有の設定が無効になり、 無線LAN やタッチパッドが使えなくなるなどの問題が生じる。 Ubuntu アップデートを行なった後、 あるいは新規に GNU/Linux (Ubuntu に限らず) をインストールする場合に、 OLEVIA X10A-1160 固有の設定を行なうためのメモ。

OLEVIA X10A-1160 は、 SOTEC C101 と (キーボード配列や天板のデザインを除けば) 同等であるため、 以下の対策は SOTEC C101 に Ubuntu 等の GNU/Linux をインストールして使う場合も、 そのまま適用できるはず。

i8042 のバグ対策を行なってタッチパッドを認識させる

他のいくつかのノートPC (dynabook Satellite P10, Thinkpad R31, Lifebook P7010, Dell XPS M1530 など) と同じく、 OLEVIA X10A-1160 (確認していないが、おそらく SOTEC C101 も同様) のキーボード/マウス コントローラ i8042 互換チップにはバグがある。 OLEVIA X10A-1160 の場合、カーネル起動時に以下のようなログを出力する:

PNP: PS/2 Controller [PNP0303:PS2K,PNP0f13:PS2M] at 0x60,0x64 irq 1,12
i8042.c: Detected active multiplexing controller, rev 1.1.
serio: i8042 KBD port at 0x60,0x64 irq 1
serio: i8042 AUX0 port at 0x60,0x64 irq 12
serio: i8042 AUX1 port at 0x60,0x64 irq 12
serio: i8042 AUX2 port at 0x60,0x64 irq 12
serio: i8042 AUX3 port at 0x60,0x64 irq 12
        ...(中略)...
input: PS/2 Generic Mouse as /devices/platform/i8042/serio4/input/input9
psmouse.c: Failed to enable mouse on isa0060/serio4

存在しないはずの外付けポート (AUX0~3) コントローラ (multiplexing controller) を認識してしまうようだ。 このため、 タッチパッド (SynPS/2 Synaptics TouchPad) が使えなくなってしまう。 この問題は、 カーネルパラメータに i8042.nomux=1 を追加することによって回避できる。 例えば GRUB の設定ファイル /boot/grub/menu.lst に、

title           Ubuntu 8.04.1, kernel 2.6.24-23-generic
root            (hd0,0)
kernel          /boot/vmlinuz-2.6.24-23-generic root=UUID=(省略) ro quiet splash vga=789 i8042.nomux=1
initrd          /boot/initrd.img-2.6.24-23-generic
quiet

といった感じで 「i8042.nomux=1」 を追加すればよい。 Linux を再起動すると、

PNP: PS/2 Controller [PNP0303:PS2K,PNP0f13:PS2M] at 0x60,0x64 irq 1,12
serio: i8042 KBD port at 0x60,0x64 irq 1
serio: i8042 AUX port at 0x60,0x64 irq 12
        ...(中略)...
Synaptics Touchpad, model: 1, fw: 5.10, id: 0x258eb1, caps: 0xa04711/0x0
input: SynPS/2 Synaptics TouchPad as /devices/platform/i8042/serio1/input/input7

などと出力し、 無事 SynPS/2 Synaptics TouchPad が認識できた。

なお Linux は、 i8042 にバグを持つ PC で問題を回避できるよう、 PC のブラックリストを持っている。 すなわち linux/drivers/input/serio/i8042-x86ia64io.h で定義されている配列 i8042_dmi_nomux_table に PC の ID を登録しておくと、 該当する PC では自動的に i8042.nomux=1 が設定される仕掛けだが、 あいにく OLEVIA X10A-1160 は固有の ID を持っていないようだ:

# head /sys/class/dmi/id/{sys_vendor,product_*}
==> /sys/class/dmi/id/sys_vendor <==
 

==> /sys/class/dmi/id/product_name <==
 

==> /sys/class/dmi/id/product_serial <==
Not Applicable

==> /sys/class/dmi/id/product_version <==
Not Applicable

DMI のベンダ名、プロダクト名が共に空白になっている。

無線LAN ドライバ vntwusb をインストールする

OLEVIA X10A-1160 (確認していないが、おそらく SOTEC C101 も同様) は、 無線LAN チップとして VIA Technologies の VT6656 を使用している。 しかし残念ながらこのチップは、 現時点の Linux 標準カーネルではサポートしていない。 したがってドライバをインストールする必要がある。

まず VIA Technologies のサポートページ VT6656 WLAN Linux Driver Source から、 最新版のドライバ vt6656_wlan_linux_v118.zip (1/18 現在) をダウンロードする。 展開して make すれば、 VT6656 用のカーネルモジュール vntwusb.ko が作られる。

% wget http://www.viaarena.com/Driver/vt6656_wlan_linux_v118.zip
--10:19:24--  http://www.viaarena.com/Driver/vt6656_wlan_linux_v118.zip
           => `vt6656_wlan_linux_v118.zip'
Resolving www.viaarena.com... 74.54.151.131
Connecting to www.viaarena.com|74.54.151.131|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 4,483,650 (4.3M) [application/x-zip-compressed]

    0K .......... .......... .......... .......... ..........  1%   68.92 KB/s
        ...(中略)...

% unzip vt6656_wlan_linux_v118.zip
Archive:  vt6656_wlan_linux_v118.zip
   creating: VT6656_WLAN_Linux_V118/
   creating: VT6656_WLAN_Linux_V118/driver/
        ...(中略)...
% cd VT6656_WLAN_Linux_V118/driver
VT6656_WLAN_Linux_V118/driver % make
make -C /lib/modules/2.6.24-23-generic/build SUBDIRS=/home/sengoku/VT6656_WLAN_Linux_V118/driver modules
make[1]: ディレクトリ `/usr/src/linux-headers-2.6.24-23-generic' に入ります
  CC [M]  /home/sengoku/VT6656_WLAN_Linux_V118/driver/main_usb.o
        ...(中略)...
  LD [M]  /home/sengoku/VT6656_WLAN_Linux_V118/driver/vntwusb.ko
make[1]: ディレクトリ `/usr/src/linux-headers-2.6.24-23-generic' から出ます

あとは、make install して modprobe vntwusb するだけ。

VT6656_WLAN_Linux_V118/driver % sudo make install
make -C /lib/modules/2.6.24-23-generic/build SUBDIRS=/home/sengoku/VT6656_WLAN_Linux_V118/driver modules
mkdir -p /lib/modules/2.6.24-23-generic/kernel/drivers/net
install -m 644 -o root vntwusb.ko /lib/modules/2.6.24-23-generic/kernel/drivers/net
/sbin/depmod -a || true
VT6656_WLAN_Linux_V118/driver % sudo modprobe vntwusb

ただし前もって、 カーネルモジュールを make するための環境を整えておく必要がある。 Ubuntu (というか debian) の場合であれば、 module-assistant パッケージと linux-headers パッケージをインストールしておけばよい。 あるいは、 上記 VT6656_WLAN_Linux_V118/driver ディレクトリを、 Linux カーネルソースにコピーしてカーネルを再構築してもよい。

私は、 Linux カーネルソースツリーに、 linux/drivers/net/wireless/vntwusb ディレクトリを作って、 VT6656_WLAN_Linux_V118/driver ディレクトリの内容をコピーし、 linux/drivers/net/wireless/Kconfig および linux/drivers/net/wireless/Makefile に以下のパッチをあてている。

--- linux-2.6.27.11.org/drivers/net/wireless/Kconfig        2008-10-10 07:13:53.000000000 +0900
+++ linux-2.6.27.11/drivers/net/wireless/Kconfig        2009-01-06 14:52:10.459276155 +0900
@@ -702,5 +702,6 @@
 source "drivers/net/wireless/b43legacy/Kconfig"
 source "drivers/net/wireless/zd1211rw/Kconfig"
 source "drivers/net/wireless/rt2x00/Kconfig"
+source "drivers/net/wireless/vntwusb/Kconfig"
 
 endmenu

diff -ur linux-2.6.27.11.org/drivers/net/wireless/Makefile linux-2.6.27.11/drivers/net/wireless/Makefile
--- linux-2.6.27.11.org/drivers/net/wireless/Makefile        2008-10-10 07:13:53.000000000 +0900
+++ linux-2.6.27.11/drivers/net/wireless/Makefile        2009-01-06 14:52:10.491275159 +0900
@@ -58,6 +58,7 @@
 
 obj-$(CONFIG_IWLWIFI)        += iwlwifi/
 obj-$(CONFIG_RT2X00)        += rt2x00/
+obj-$(CONFIG_VNTWUSB)        += vntwusb/
 
 obj-$(CONFIG_P54_COMMON)        += p54/
 

Ubuntu などのディストリビューションのアップデートに頼らずに、 カーネルのバージョンアップを行なっている場合は、 カーネルソースツリーに組み込んでおく方が、 ドライバ込みでカーネルを再構築できるので手間が省ける。

Filed under: ハードウェアの認識と制御 — hiroaki_sengoku @ 08:59
2008年12月29日

香港の旺角電腦中心で NetBook OLEVIA X10A-1160 を買ってみた

香港の旺角電腦中心 (Mongkok Computer Centre) の 4階 (香港の呼び方だと 3樓) 320號にあるパソコンショップ 「道亨顧問有限公司」 (Dorman Consultants Limited) で、 NetBook OLEVIA X10A-1160 を HK$2880 (約 33,000円) で売っていた。

Olevia X10A 10.1" notebook

Olevia X10A 10.1" notebook

Operation SystemLinux (Windows Compatible)
ChipsetIntel Atom N270 1.6GHz
BIOSPhoenixBIOS 1.0.00-31
Memory1GB DDR2
LCD10.1” 1024x600 SVWGA
HDD160 GB (SATA) WD1600BEVT
I/OD-sub*1 , USB 2.0*3 , Mic-in
Headphone, LAN
BatteryLithium-Ion 11.1V 2.2Ah (3 cells)
Webcam1.3M Webcam
SoundStereo speakers
Dimension26.5*18.5*2.5cm
Weight~1.3 Kg (including 3 cells battery)

物価が安い香港と言えど、HK$3000 以下の NetBook というと EeePC 701SDX あたりになってしまうわけで、 メモリ 1GB, HDD 160GB の Atom N270 で HK$2880 というのは興味深い。 その場で悩み込むこと数分間。 店員がべらべら説明を始めたが、広東語なので何も分からない (^^;)。

同等スペックの NetBook としては、 例えば acer Aspire One AOA150 がある。 メモリ 1GB, HDD 160GB, Atom N270, WSVGA, 無線LAN あたりは同等で、 違いは Aspire One AOA150 の方が若干小さく (画面サイズも 1インチほど小さい)、 LAN が 100Mbps (X10A は 1Gbps) で、 Webカメラの画素数が 30万画素 (X10A は 130万画素) である点など。 Aspire One AOA150 の最安値は 35000円くらい?

何より OS が Linux というのがいい。 香港版の Windows XP なんて広東語を喋れない私には無用であるわけで、 HK$600 くらいはする Windows XP ライセンスが付いた PC は避けたいところ。 一応 「Windows Compatible」 を謳っていて 「Designed for Microsoft WindowsXP」 シールも貼ってある。

買う一歩手前まで行ったのだが、最後の背中のひと押しが無かったのと、 妻がホテルで待っているというのがあって、 これから帰る旨を電話で伝えて、 後ろ髪を引かれつつお店を後にした。

ところが

More...
Filed under: ハードウェアの認識と制御,香港 — hiroaki_sengoku @ 08:25
2008年8月25日

ギガビット・スイッチング・ハブ CG-SW08GTV2 が故障したと思ったら… hatena_b

自宅サイト GCD の 2台のゲートウェイ (senri と asao) の NIC が同時に link down した。

Aug 18 10:31:30 senri [144077.315932] tg3: eth0: Link is down.
Aug 18 10:33:57 senri [144201.921009] tg3: eth0: Link is up at 1000 Mbps, full duplex.
Aug 18 10:33:57 senri [144201.921228] tg3: eth0: Flow control is on for TX and on for RX.
Aug 18 10:31:30 asao [894103.906436] tg3: eth0: Link is down.
Aug 18 10:33:57 asao [894253.756472] tg3: eth0: Link is up at 1000 Mbps, full duplex.
Aug 18 10:33:57 asao [894253.756472] tg3: eth0: Flow control is on for TX and on for RX.

この日 8/18 はたまたま早朝から出張だった (帰宅したのは翌日 8/19 23:00 近く) ので link down に気付かなかった (もちろんヘルスチェックは行なっているが自宅なのでタイムアウトは長め)。 留守中、 link down 後しばらくたつと link up (正常状態に復旧)、 という繰り返しが何度も起きたようだ。 ログから down 時間を集計してみるとこんな感じ:

link downupdown秒数
8/18 10:31:3010:33:57147
8/18 10:34:1210:47:45813
8/18 10:54:3410:54:373
8/18 18:18:0718:21:50223
8/18 18:22:3718:42:511214
8/19 15:38:1315:40:35142
8/19 15:45:0515:51:06361
8/19 16:05:5416:05:573
8/19 16:07:4816:07:502
8/19 16:08:2616:10:08102
 
link downupdown秒数
8/19 16:10:1016:10:122
8/20 08:00:1208:02:59167
8/20 08:03:0408:07:47283
8/20 08:07:5708:09:57120
8/20 08:10:2908:22:47738
8/20 08:31:3308:31:363
8/20 08:32:3709:00:141657
8/20 12:56:0613:00:05239
8/20 13:00:2713:08:02455
8/20 13:32:5013:37:15265

3 秒で復旧することもあれば、 20分以上落ちたままのこともあり規則性がないが、 いったん link down/up すると、 down/up を何度か短時間に繰り返す傾向がある。 また次第に down する頻度が上がり、 down している時間も長くなる傾向にあった。 複数のマシンで同時に link down することから、 ハブの故障だと判断できる。 ハブは、 コレガ製 1000BASE-T/100BASE-TX/10BASE-T 8ポート スイッチングHUB CG-SW08GTV2。 2005年11月9日に購入したものなので1年の保証期間は切れているものの、 まだ3年弱しか経過しておらず自宅LAN の中では比較的新しいハブである。

ちょっと壊れるのが早すぎなんじゃないかと思いつつ、 とりあえず余っていた 100Mbps スイッチング・ハブと置き換えて急場をしのぎつつ、 秋葉原に新しいハブを買いに行った。 買ったのは同じくコレガ製の、 1000BASE-T/100BASE-TX/10BASE-T 8ポート スイッチングハブ CG-SW08GTRT-Zone で 3742円だった。 8 port の gigabit が 4000円以下とは、 ずいぶん安くなったものだ (標準価格は 9975円)。

というわけで故障したハブを新しいハブで置き換えて一件落着なのであるが、 3年たたずに故障し、 しかも断続的に down するという症状が腑に落ちなかったので、 壊れた CG-SW08GTV2 の筐体を開けてみた。 すると...

More...
Filed under: ハードウェアの認識と制御 — hiroaki_sengoku @ 07:52
2008年7月14日

なぜ DELL PowerEdge SC440 は ICH7R の Watchdog Timer 機能を利用できないのか?

DELL PowerEdge SC440 は、 インテル 82801GR I/O コントローラー・ハブ (ICH7R) を使っている。 ところが、ICH7 にあるはずの ウォッチドッグ・タイマ (Watchdog Timer) 機能が利用できない。 例えば Linux だとブート時に 「reboot disabled by hardware」すなわち 「ハードウェアによってリブートが禁止されている」とカーネル・ログに出力される:

iTCO_vendor_support: vendor-support=0
iTCO_wdt: Intel TCO WatchDog Timer Driver v1.02 (26-Jul-2007)
iTCO_wdt: failed to reset NO_REBOOT flag, reboot disabled by hardware
iTCO_wdt: No card detected

この「reboot disabled by hardware」とは何なのかを調べてみると、 Intel I/O Controller Hub 7 (ICH7) Family Datasheet の 78ページ、 Table 2-23. Functional Strap Definitions (Sheet 3 of 3) に、

SignalUsageWhen SampledComment
SPKRNo RebootRising Edge of PWROK The signal has a weak internal pull-down. If the signal is sampled high, this indicates that the system is strapped to the "No Reboot" mode (ICH7 will disable the TCO Timer system reboot feature). The status of this strap is readable via the NO REBOOT bit (Chipset Config Registers:Offset 3410h:bit 5).

と書いてある。 つまり ICH7 の SPKR 端子 (Ball# A19) はスピーカ出力なのであるが、 入力端子としても使われていて、 PWROK 入力端子 (Ball# AA4) の立ち上がりエッジ時 (すなわち電源投入時) において SPKR 端子が H レベルであれば、 「No Reboot」モードに固定される。

「No Reboot」モードというのは、 TCO タイマ (つまり Watchdog Timer) による再起動を行なわないモードで、 通常はソフトウェアでこのモードの設定/解除が可能なのであるが、 SPKR 端子によって「固定される」と No Reboot モードのまま変更できなくなる。

具体的には、 「No Reboot」モードの設定/解除は、 ICH7 の「NO REBOOT bit」に 1/0 を書込むことによって行ない、 逆にこの「NO REBOOT bit」を読むことによって現在のモードを確認できる。 Linux カーネルの drivers/watchdog/iTCO_wdt.c に、 このビットを解除し、解除できたか確認するコードがある:

static int iTCO_wdt_unset_NO_REBOOT_bit(void)
{
        ...
        /* Unset the NO_REBOOT bit: this enables reboots */
        if (iTCO_wdt_private.iTCO_version == 2) {
                val32 = readl(iTCO_wdt_private.gcs);
                val32 &= 0xffffffdf;
                writel(val32, iTCO_wdt_private.gcs);

                val32 = readl(iTCO_wdt_private.gcs);
                if (val32 & 0x00000020)
                        ret = -EIO;
        ...
        return ret; /* returns: 0 = OK, -EIO = Error */
}

この iTCO_wdt_unset_NO_REBOOT_bit 関数は、 GCS (General Control and Status Register, オフセット 0x3410) のビット5 に 0 を書込むことによって No Reboot モードの解除を試み、 続いてこのビットを読んで正しく 0 に変わっているか確認し、 確認結果を返す。 つまりこの関数が正常に 0 を返せば、 ICH7 の Watchdog Timer が利用可能であることを示し、 -EIO を返せば、 (No Reboot モード固定なので) 利用不可能であることを示す。

というわけで、 DELL PowerEdge SC440 で Intel TCO Watchdog Timer が利用できない理由は、 電源投入時に SPKR 端子に電圧がかかってしまっていて、 No Reboot モード固定になっているためだろう。 SPKR 端子は内部的にプルダウン (20kΩ) されているので、 SPKR 端子につながっているスピーカを切り離せば SPKR 端子は L レベルになり、 Watchdog Timer が利用できるようになるはず。

More...
Filed under: ハードウェアの認識と制御 — hiroaki_sengoku @ 07:45
2008年7月11日

外国のメーカに修理/交換してもらうとき課税される関税・消費税を減免する方法 hatena_b

ハードディスクが故障したので、RMA 手続きを行なった上でメーカへ送り返したら、わずか4日で代替品が返ってきた (6月28日)。 RMA++ と思っていたら、 10日後の 7月8日にシンガポールのフェデラル・エクスプレスから 「請求書在中」と書かれた AIR MAIL な封書が送られて来た。 え~ せっかく RMA を誉め称えるブログを書いたのに、 今になって何か追加で請求されるとわ... orz と、 暗澹たる気持ちで開封してみると...

どきどきしながら「請求書 INVOICE」と書かれた一枚目の書類に目を通すと、 請求額は 1000円。 そんなに高額の請求ではなかったので一安心。 内訳を見ると、

Other Charges  消費税/付加価値税(Consumption Tax/VAT) 500
Other Charges  関税・消費税特別手数料(D/T Advancement Fee)  500
合計(Total) 1,000

関税? 日本って工業製品に対して関税なんてかけていたっけ? と思いつつ
二枚目の「輸入許可通知書」に目を通すと、

税科目税額合計個数
D 関税¥00
F 消費税¥4001
A 地方消費税¥1001

なるほど、 メーカが申告した価格 (CIF) $108.00 に対して約 5% の消費税がかかる、 ということで納税額が 500円なのか。 でも「関税・消費税特別手数料」って何なのだろう? 少なくともこの「輸入許可通知書」には「特別手数料」の文字は見当たらない。

まあ高々 1000円だから払っちゃおうか、という考えが頭をよぎる。 ご丁寧にコンビニ払いの用紙まで添付されている。 コンビニ払いか銀行振込を選択可能なようだ。

消費税ってのはいわゆる付加価値税のことである (なぜ VAT (= 付加価値税) という世界的に一般的な名称ではなく、 「消費税」という聞きなれない名称にしたのやら...)。 物品に価値が加わったときにその増分に対して一定割合 (現在は 5%) を納税するのが趣旨であるが、 故障したハードディスクを交換したことが「付加価値」に該当するのだろうか?

確かに私にとっては、 故障したハードディスクは価値ゼロで、 交換してもらったハードディスクは 13000円 (現時点での WD10EACS の小売価格) くらいの価値があるから一見価値が増えたように見えるが、 より厳密に考えると「故障したハードディスク」には「RMA 保証」がついていた (だからこそ交換してもらうことができた) ので、 13000円の価値があったと言ってもよい。

そもそも消費税は、このハードディスクを買ったときに支払っている。 当時は 28140円もしたから、なんと 1340円 (本体価格 26800円の 5%) も、 消費税を払っているわけである。 この上、さらに 500円も消費税を払わされてはかなわない。 これは同一物に対する二重課税ではないのか~っ。 もんもんと考えているうちに、だんだんハラが立ってきた (^^;)。

というわけで、 なんとかこの理不尽な課税を回避できないか試みることにした。 もちろん、たかが 500円の課税であるので回避できたところで、 かけた労力に見合うわけはないのだが、 まあなにごとも経験である。 軽減できる税金は最大 500円であっても、 軽減交渉という経験はプライスレス。

More...
Filed under: ハードウェアの認識と制御 — hiroaki_sengoku @ 10:25
2008年7月3日

Western Digital RMA チームから届いた文字化けメールを解読してみた hatena_b

故障した HDD WD10EACS を RMA (Return Merchandise Authorization, 返却承認) 手続きで交換してみた」で書いたように、 RMA 手続きを行なった上で Western Digital へ故障したハードディスク ドライブ (以下 HDD と略記) を送ったら、 激しく文字化けしたメールが送られてきた。

あとは HDD が送られてくるのを のんびり待つだけと思っていたら、 わずか一日後 6/26 18:44 に Western Digital からメールが来た。 しかし文字化けがひどくて読めない。 最初は何語で書いてあるかすら判然としなかったのだが、 どうやら Shift JIS で書かれた文面を quoted-printable エンコードする際に なにか問題があったようだ。 例えば 0x82 が「,」に、0x95 が「.」に置き換わってしまっている。 置換が規則的でないので、 暗号解読よろしく一文字一文字置き換え規則を推測していくしかない。

文面を再現するのに時間がかかりそうだなぁ~と思っている間に、 交換品の HDD が届いてしまったので、 「暗号」解読するモチベーションを失ってしまっていたのだが、

Posted by 通りすがり 2008年07月02日 00:36
結局、メールにはなんて書いてあったのでしょうか?

というコメントを頂いてしまったので、 暗号解読してみることにした。

以下、Western Digital からの文字化けメールを全文引用 (一部伏字) する:

From: "Western Digital RMA" <noreply@wdc.com>
To: <sengoku@gcd.org>
Date: Thu, 26 Jun 2008 02:44:25 -0700
MIME-Version: 1.0
Content-Type: text/plain;
        charset="iso-8859-1"
Content-Transfer-Encoding: quoted-printable
X-Mailer: Microsoft CDO for Windows 2000
Content-Class: urn:content-classes:message
X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2800.1896
X-OriginalArrivalTime: 26 Jun 2008 09:44:25.0728 (UTC) FILETIME=[3861F800:01C8D771]

HIROAKI SENGOKU -l,=D6=81A

^=C8?=BA,=C9.\Z=A6,=B3,=EA,=BD RMA =
,=CCfXfe=81[f^fX,=F0Sm"F,=B5,=C4,=AD,=BE,=B3,=A2=81B  RMA
,=C9S=D6,=B7,=E9,=A8-=E2,=A2=8D?,=ED,=B9,=CD,=B1,=CCf=81=81[f<,=C9.=D4=90=
M,=B5,=C4,=AD,=BE,=B3,=A2=81B
=8F=EE.=F1,=AA=90=B3,=B5,=A2=8F=EA=8D?=81A,=B1,=CC"dZqf=81=81[f<,=C9,=CD.=
=D4=90M,=B5,=C8,=A2,=C5,=AD,=BE,=B3,=A2=81B


RMA "=D4=8D?=81F 8083XXXX

--------------------------------------------------------------

O=F0S=B7fhf?fCfu,=F0 5=81`7 ?c<=C6"=FA'?,=C9"=AD'-,=B5,=DC,=B7=81B

^=C8?=BA,=CCfhf?fCfu,=F0 Western Digital =
,=CDZ=F3-=CC,=B5,=DC,=B5,=BD=81F

     fVfSfAf<"=D4=8D?     =90=BB.i"=D4=8D?             =
Z=F3-=CC"=FA=81iGMT=81j
     ------------     ---------------      -------------
     WCASJxxxxxxx     WD10EACS-00ZJB0      6/25/2008

--------------------------------------------------------------

^=C8?=BA,=C9.\Z=A6,=B3,=EA,=BD RMA =
"=AD'-=8F=F3<=B5,=F0Sm"F,=B5,=C4,=AD,=BE,=B3,=A2=81B

-A'-<=C6Z=D2,=CCfVfXfef?,=CC=8DX=90V,=C91?c<=C6"=FA,=AA,=A9,=A9,=E8=81A,=BB=
,=CCO=E3"=AD'-'=C7=90=D5"=D4=8D?,=AA-LO=F8,=C9,=C8,=E8,=DC,=B7,=CC,=C5=81=
A,=B2-=B9=8F=B3,=AD,=BE,=B3,=A2=81B

O=F0S=B7fhf?fCfu,=CC'-.t=90=E6=81F

     HIROAKI SENGOKU
     XXXXXXXXXXXXXXXXXXXXXXXXX TAKATSU
     KAWASAKI, Japan 213-XXXX
     JAPAN

"z'-<=C6Z=D2=81F     Fedex
"z'-'=C7=90=D5"=D4=8D?=81F XXXXXXXXXXXX

     fVfSfAf<"=D4=8D?     =90=BB.i"=D4=8D?             =
"=AD'-"=FA=81iGMT=81j
     ------------     ---------------      -------------
     WCASJXXXXXXX     WD10EACS-32ZJB0      6/26/2008

--------------------------------------------------------------

S=D6~AfSf"fN=81F
RMAZ=E8=8F?ZwZ=A6=8F=EE.=F1,=CC?{--/^=F3=8D=FC
  - =
http://websupport.wdc.com/rd.asp?t=3D102&l=3Djp&p=3Dm&r=3D8083XXXX&f=3De

"=AD'-,=C6=8D=AB.=EF,=CC=8F=EE.=F1
  - http://websupport.wdc.com/rd.asp?t=3D103&l=3Djp&p=3Drp

RMAfXfe=81[f^fX,=CC?{--
  - =
http://websupport.wdc.com/rd.asp?t=3D104&l=3Djp&p=3Dv&r=3D8083XXXX&z=3D21=
3-XXXX

Western Digital fTf|=81[fgfz=81[f?fy=81[fW
  - http://websupport.wdc.com/rd.asp?t=3D105&l=3Djp&p=3Dh

^=C8=8F=E3=81A
WD RMA f`=81[f?
http://websupport.wdc.com/rd.asp?t=3D105&l=3Djp&p=3Dh

ヘッダに「quoted-printable」と書いてあるとおり、 quoted-printable エンコーディングを行なったのだろうが、 のっけから「^=C8?=BA,=C9.\Z=A6,=B3,=EA,=BD」となっていて、 一体何語なんだ?と思わせる始まり方である。

ちなみに quoted-printable というのは 8bit データを、 「印字可能 (printable)」つまり 7bit の英数字・記号だけで表現するための方法 (エンコーディング) で、 印字可能でない 8bit データは 16進数で表わして前に「=」をつける (「=」自身は「=3D」で表現する)。 例えば「^=C8?=BA,=C9.\Z=A6,=B3,=EA,=BD」は、 16進数で書くと 「5E C8 3F BA 2C C9 2E 5C 5A A6 2C B3 2C EA 2C BD」 という 8bit データ列を意味する。

腕に覚えのあるかたは、解答を見ずに解読を試みてはいかがだろうか?

More...
2008年6月30日

故障した HDD WD10EACS を RMA (Return Merchandise Authorization, 返却承認) 手続きで交換してみた hatena_b

廉価な 1TB SATA ハードディスク ドライブ (以下 HDD と略記) として有名な、 Western Digital 製 WD Caviar GP WD10EACS は、 省電力・静音を謳っている。 環境に優しいのは結構なことだが、 その実現方法:

IntelliPower - きめ細かく調整された ディスク回転速度 転送速度 及びキャッシュサイズの調和により飛躍的な省電力と確実なパフォーマンスを提供します
IntelliPark - 風損を減らす為のアイドル時の自動ヘッド退避によりドライブは消費電力を低減する
IntelliSeek - 電力消費量、ノイズおよび振動を低減させるために、最適なシーク速度を計算します。

のうち、特に「アイドル時の自動ヘッド退避」というのがいただけない。 ヘッドを退避すれば空気抵抗が減ってモーターの負荷が減るから 低消費電力が実現できる (実際、アイドル時の消費電力は際立って低い)、 ということなのだろうが、 常時使用する PC (特にサーバ) だと「自動ヘッド退避」の回数が とんでもないことになる。 SMART 情報を見ると:

# smartctl -a /dev/sdb
smartctl version 5.37 [i686-pc-linux-gnu] Copyright (C) 2002-6 Bruce Allen
Home page is http://smartmontools.sourceforge.net/

=== START OF INFORMATION SECTION ===
Device Model:     WDC WD10EACS-00ZJB0
Serial Number:    WD-WCASJxxxxxxx
Firmware Version: 01.01B01
User Capacity:    1,000,204,886,016 bytes
        ...
Vendor Specific SMART Attributes with Thresholds:
ID# ATTRIBUTE_NAME          FLAG     VALUE WORST THRESH TYPE      UPDATED  RAW_VALUE
        ...
  9 Power_On_Hours          0x0032   095   095   000    Old_age   Always   3826
 12 Power_Cycle_Count       0x0032   100   100   000    Old_age   Always   24
192 Power-Off_Retract_Count 0x0032   200   200   000    Old_age   Always   19
193 Load_Cycle_Count        0x0032   197   197   000    Old_age   Always   11327
        ...

わずか 3826 時間 (159日、約5ヶ月) で、 実に 11327回もの自動ヘッド退避が行なわれている。

この異常な回数の自動ヘッド退避が原因かどうかは分からないが、 この 1TB HDD は 2007年11月30日に 28140円で購入してから わずか 5ヶ月余りの 2008年5月2日に故障した。 こんなに早く故障するとは思ってもいなかったので、 T-Zone の延長保証 (200円の追加で通常3ヶ月保証を最長6ヵ月保証) を掛けていなかった。 6ヶ月ではどーせ故障しないから保証を掛けるだけ無駄と判断したのだった (ついでに言うと、6ヶ月では故障しないだろうと高を括っていて、 まだ一部バックアップしていないデータがあったので、 復旧にえらく手間取った。1TB もあると復旧も一筋縄にはいかない)。

運が悪かったとあきらめるしかない (二度とグリーン・パワー・ハードディスクは買わないぞっ!) と思っていたら、 RMA (Return Merchandise Authorization, 返却承認) という メーカによる保証があるということを同僚から教えてもらった (社内IRC で HDD の故障を嘆いていたら、哀れに思って教えてくれたらしい ^^;)。 RMA番号を取得した上で故障品をメーカへ送ると、 代替品を送り返してくれる、という保証。

ステップ3
製品が故障していて交換が必要と判断された場合、 RMA タイプを下から選択して RMA(返却承認)手続きを開始してください。

Standard Replacement
お客様から故障製品を受け取ったあとに代替製品を送付します。 RMA が作成されてから30日以内に故障製品を返送してください。

Western Digital の エンドユーザー向け保証確認ページで、 故障した HDD のシリアル番号を入力してみると、

交換に適合するドライブ

おお、「限定保証期間内」と表示された。 有効期間は再来年の 12月まで、三年間もあるらしい。 もしかしたら交換してもらえるかも?と期待が膨らむ。 といっても RMA なんて今まであることすら知らなかったわけで、 どうやって故障品を送ったらいいのか見当もつかない。

More...
Filed under: ハードウェアの認識と制御 — hiroaki_sengoku @ 08:12
2008年6月13日

HP ProLiant ML115 で、IPMI ハードウェア・ウォッチドッグ・タイマー ipmi_watchdog を使ってみる hatena_b

ハードウェア・ウォッチドッグ・タイマー iTCO_wdt のススメ」へのリンクを張って頂いた:

HP ML115 には IPMI (Intelligent Platform Management Interface) という 便利なインターフェイスが内蔵されています。 ... (中略) ... IPMI 機能の一つ、watchdog timer 機能を利用してみようと思います。
... (中略) ...
watchdog timer の動作に関しては、 こちらの方が 詳しいので興味ある方はご確認ください。 さて、どうやって watchdog を起こすかというと、 先ほどインストールしたドライバと ipmitool を利用します。

うっ、ML115 の IPMI には、 ウォッチドッグ・タイマーの機能もあったのか (何たる不覚 orz)。 今まで ML115 では、 ソフトウェア版ウォッチドッグ (softdog.ko モジュール) を 使っていたので、 速攻で IPMI のハードウェア・ウォッチドッグ・タイマーに乗り換えてみた。

Linux カーネル・ソースの Documentation/IPMI.txt を見ると、

A watchdog timer is provided that implements the Linux-standard
watchdog timer interface.  It has three module parameters that can be
used to control it:

  modprobe ipmi_watchdog timeout=<t> pretimeout=<t> action=<action type>
      preaction=<preaction type> preop=<preop type> start_now=x
      nowayout=x ifnum_to_use=n

ブート時に「modprobe ipmi_watchdog」を実行するだけで使えそうである。 タイムアウトを「timeout=<t>」で指定できるが、 私のマシンでは「蹴り代行」デーモンを走らせている (「ハードウェア・ウォッチドッグ・タイマー iTCO_wdt のススメ」参照) ので、 デフォルトのタイムアウトである 10 秒のままでも問題無い。

「action=<action type>」には、タイムアウト時の挙動を指定する。 「reset」(ハードウェア・リセット)、 「power_cycle」(電源OFF してから電源ON)、 「power_off」(電源OFF) を指定できるが、 デフォルトは「reset」なので、これも指定しなくて構わない。

「nowayout=0」を指定すると、 /dev/watchdog をクローズ時にウォッチドッグ・タイマーが止まる。 ウォッチドッグ・タイマーは、 いったん動き出したら何が起ろうと止めるべきではないと思うし、 デフォルトは「nowayout=1」(つまりクローズしても止まらない) なので、 これも指定する必要はない。

というわけで、ipmi_watchdog を使ってみる:

# modprobe ipmi_watchdog
# echo @ > /dev/watchdog

/dev/watchdog が存在しない場合は、「mknod /dev/watchdog c 10 130」を実行して、 /dev/watchdog を作成しておく。

10秒後、勝手にリセットがかかった (めでたしめでたし)。 リセットを防ぐには 10秒以内に /dev/watchdog へ書込み続けなければならない。 例えば、

#!/bin/sh
while true; do
    echo @ > /dev/watchdog
    sleep 5
done

といったスクリプトを走らせ続ける。 このスクリプトだとシンプルすぎて、 システムに異常が起きたときも動き続けてしまう (だからタイムアウトが発生しない) 恐れがあるので、 while ループの中にシステム異常を検知する (異常が起きたときは 10秒以上時間がかかる) ようなコマンドを入れておくとよい。

何かの都合でタイマーを止めたい (止めるべきではないが) ときは、 「V」を /dev/watchdog に書込む。

Filed under: ハードウェアの認識と制御 — hiroaki_sengoku @ 08:17
Older Posts »