Welcome to telecotele.com » Projects

ワイヤレス温湿度計の信号をデコードする

RTL-SDRとGNU Radioを用いてシチズン温湿度計THD501の無線信号をデコードします

tags: sdr house

ワイヤレス温湿度計の信号をデコードするのカバー画像

はじめに

「シチズン (CITIZEN) コードレス温湿度計 THD501」を使っています。 これは子機(下図左)と親機(下図右)とから構成される温湿度計で、子機は設置場所の温湿度を無線で親機に送信、親機はその情報(と親機設置場所の温湿度)を表示する、といったものです。

THD501 THD501(直前まで子機と親機は離れた場所に設置していたので温湿度がズレている。また、子機は日焼けしている)

子機は防滴構造となっており、屋外にも設置が可能。 購入当時(2016年)は5000円程度で販売されており、この価格で屋外の温湿度も測定できるというのは魅力的でした1

そして、この当時から考えていたことがあります。 それは、「子機が無線でデータを送信するなら、それをSDRか何かでキャッチしてデコードできるのでは?」ということです。 子機と親期間で行われる通信の詳細は公開されていませんが、もしデコードできれば安価に屋外の温湿度を集計するシステムが構成できます。

もっとも、こういった話は “sdr decode temperature sensor” とかでググればいくらでも出てくるでしょう。 新しい話では全くありません。 無事「まぁやればできるんじゃないですかね、知らんけど」と放置し、やがて屋外設置が可能かつAPIも用意されたNetatmo Weather Stationを導入。 それ経由でデータが取得できるようになったこともあり、積極的にTHD501のデコードをしたい動機もなくなりました。

でも、温湿度センサのメトリクスはなんぼあっても良いですからね。 また、最近まともにSDRを触るようになった2ので、思い出してデコードしてみます。 本稿ではまずTHD501の詳細について述べ、その後信号のデコードと分析を実施、そして最終的にはリアルタイムに温湿度データを可視化するまでを述べています。

ちなみに本稿で利用したコードやデータは、下記のリポジトリに入れました。

lrks/thd501_decoder
CITIZEN THD501 Decoder based on GNU Radio - Wireless signal decoder for > wireless temperature and humidity meter
https://github.com/lrks/thd501_decoder

THD501の詳細

まずはTHD501の詳細についてです。

技適を確認すると、技適マークがあるのは子機のみで親機は受信専用。 何かネゴシエーションするようなわけではなさそうですね。 そして説明書に記載されている子機の技適番号 (012-190006)3によれば、無線の周波数は426.025MHzとのこと。 電波の形式は「F1D」とあり、これは周波数変調らしい。

SDRを試した日記 その0の再掲ですが、周波数のスペクトラムを確認すると確かにそれっぽい信号が見えます。

426.025MHz付近の周波数スペクトラム 426.025MHz付近の周波数スペクトラム

ちなみに、何か手がかりがあるかなと思って子機のケースも開けてみました。 (これも再掲ですが)下図のとおり。

THD501子機の内部 THD501子機の内部

MCUとかRF ICが樹脂に覆われているので詳細は不明。 一応、親機のケースも開けてみます。

THD501親機の内部 THD501親機の内部

“MOS DESIGN”、“RF003 MODULE RECEIVER 426.025MHz”というのがありますが詳細は不明。 金属シールドは開けていないので何も分かりません。

また、いろいろ調べるうちに使用する周波数帯域や見た目が似た製品があることが分かりました。 具体的には「カスタム HI-01RF」や「佐藤計量器 SK-300R」。 もしかしたら何か基となる製品があって、そこから派生している?のかも知れません。

ちなみに、THD501は現在販売されておらず、代わりにTHM527という後継製品?が販売されています。 その製品の技適 (012-210005)を見ると、THD501と同様に426.025MHz F1Dとのこと。 もしかしたらTHD501と同じ信号を使っているのかも知れませんが、手元には無いため詳細は分かりません。

技適情報には補足資料も添付されており、“TM89P52M” という4bitマイコン(!)と、“CMT2119” という “single-chip (G)FSK/OOK transmitter” が組み込まれているようです。 とはいえ、これを知ったところで役立つような情報は見つけられませんでした。

URHでの調査

いまの時点で分かっているのは、THD501では426.025MHz 周波数変調の信号が使われているっぽい、ということです。 この詳細は不明なままなので、実際の信号をキャプチャして調査しましょう。 ここではURH (Universal Radio Hacker) を使用します。

jopohl/urh: Universal Radio Hacker: Investigate Wireless Protocols Like A Boss
https://github.com/jopohl/urh

これは無線プロトコルの調査に特化したGUIツールです。 直感的なUIでゆるふわに操作できます。 インストール方法や詳細な使い方はドキュメントやみんな大好きYouTubeを見てもらうとして……とりあえず使ってみましょう。

PCにRTL-SDRドングルなどを接続して、 “File” > “Record Signal” からRecordします。 周波数などを合わせてStartボタンを押し、信号っぽいのが来たらStopを押します。

URHでの記録 URHでの記録

Saveして画面を閉じると、自動的にそのファイルが開かれた状態となります。 ここで余計なデータ(無信号の領域など)を削除したりして、信号を復調したりする……わけですが、今回の場合だと復調しやすくするために事前にBand-pass filterをかける必要がありました。 画面左下部の “Signal view” を “Spectrogram” にして、信号っぽい領域を選択し右クリックから “Apply bandpass filter” を実行します。

URHでのBPF適用 URHでのBPF適用

すると、フィルタされた信号が作成されるのでここで復調を試します。 復調できたと思われる結果はこんな感じ。

URHでの復調結果 URHでの復調結果

このときのパラメータはこんな感じです。

  • Noise: フィルタも通ったので、ほとんどゼロとした
  • Center: Signal viewを “Demodulated” にして調整
  • Samples/Symbol: 時間領域で見ると2000くらいっぽい
  • Error tolerance: 多めに100
  • Modulation: もちろんFSK
  • Bits/Symbol: とりあえず1

これでうまく復調できていそうですね。 少なくとも、(Samples/Symbolより)500 baudくらいの、BFSK (Binary FSK)の信号ではありそうです。

復調されたデータに注目すると、この符号化は何か?たとえばNRZか?マンチェスタ符号か?というのはまだ明らかではありませんが、とりあえず最初は最初は111... というデータが見えます。 続いて連続する0101...が見えて、なんか本体のペイロードっぽいのがあって、最後は111...で終わる感じですね。 何度か受信してみましたが、最初と最後の111...1の数は変化する、冒頭のほうの0101..は常に固定でプリアンブルと思われる、データ本体っぽいのは固定長 (112 bits) ということが分かりました。

このあと本稿ではGNU Radioなどを使ってデコードしていくわけですが、このデータを分析するだけならURHのみで十分です。 GNU Radioなどの項はすっ飛ばして、「データの分析」に進んでもらっても違和感ないと思います。 URHは便利。 あとキャプチャした信号をリプレイ(送信)する機能もあります4からね。

ただ、URHはGUIによる操作が前提です。 たとえば「長期間に渡って動作するような、受信した温湿度のデータをデコードしてログに書き出す」といったことには向きません。 別の方法を用いて(最終的には)CLI上から自動的にデコードさせたい。

rtl_433によるデコード

「温湿度計などセンサーの無線データをCLI上からデコード」といえば、rtl_433です。

merbanan/rtl_433: Program to decode radio transmissions from devices on the ISM bands (and other frequencies)
https://github.com/merbanan/rtl_433

これは、さまざまな無線デバイスからの信号をデコードしてくれるCLIツールです。 ワイヤレス温湿度計なども(サポートされていれば)デコードしてくれます。

ただ、先に述べておきますが……今回の場合だとデコードできませんでした。

まず、THD501は既存のデコーダでは対応していないようです。 それは仕方ない。 でもrtl_433にはFlex decoderという機能があります。 これは復調に必要な各種パラメータを指定することで、それらしくデコードしてくれる機能です。

これまでの調査を通じて必要なパラメータは判明しているので、これでいけると思ったのですが……うまくいきませんでした。 一応、パラメータの指定はこんな感じです。 ファイルに保存したデータでも試してみましたがダメでした。

$ rtl_433 -vvv \
    -f 426.025M -s 1M \
    -Y autolevel -Y minmax -M level -M noise -M bits -R 0 \
    -X "n=hoge,m=FSK_PCM,s=2000,l=2000,t=100,r=448000"

Flex decoderに関して深い理解があれば、またはFlex decoderではなくちゃんとrtl_443に実装したらいけた……のかも知れないですし、いやそうじゃないかも知れません。 もっとも、後述の「データの分析」を行ってもまだデータ内で判明していない箇所はあるので、もし仮に実装できていたとしてもrtl_443のメインラインにマージなどは難しかったでしょう。

GNU Radioによるデコード

続いて試したのはGNU Radioです。 GNU Radioとは? SDRにまったく興味がなくとも聞いたことがあるかも知れませんが、SDR全般の開発ツールです。 GUIから操作できるほか、(ライブラリとして)CLIからも使えます。

特にGUIで使う場合はGNU Radio Companion (GRC)というプログラムを通じて、信号処理のブロックを繋いで受信機(または場合によっては送信機)を構成していきます。 GRCで構成した処理はPythonスクリプトに変換でき、これをCLI上から実行することも可能です。

そんなGRCで、THD501の信号を受信して復調する処理を書いてみるとこんな感じ5になりました。

GRCで構成したブロック GRCで構成したブロック

パラメータについては品質よりも負荷を抑えることを優先した値になっています。 デバッグ用の入力や可視化もあるので、ちょっとごちゃごちゃですね。 この辺の中身について詳細は後述しますが、とりあえず実行するとこんな感じに。

GRCの実行結果 GRCの実行結果(時間軸が多少おかしい点は目をつぶってください)

それっぽいんじゃないでしょうか? コンソールにはデコードされたビット列がリアルタイムに送られてきます。

処理の説明は下記のとおり。

入力部

まず、信号の入力部分についてです。 主なブロックとしては、File SourceとRTL-SDR Sourceがあります。

File Sourceはファイルに保存された信号を読み取って流すブロックです。 ここではデバッグのために利用し、URHで保存した*.complex形式のファイルを指定しています。 URHでは*.complex16s形式のファイルも作成されますが、これは正しく読み込まれませんでした。 フォーマットが違うため当然といえばそのとおり。 仕様は明らかにされている6ので変換すれば読み込めそうですが試していません。

RTL-SDR SourceはRTL-SDRドングルからリアルタイムに信号を読み取って流すブロックです。 こっちは本番用で、周波数や帯域幅などを指定して使います。 GNU Radio本体には含まれておらず、(もしブロックが見当たらなければ)gr-osmosdrの導入も必要です。 なお、周波数は、本来受信したい周波数よりも100kHz程度ずらして指定する(そして後段のフィルタでずらした分を戻す)と良いとのこと。 これは受信した周波数の中心、つまり相対的には0Hzの位置にDC成分が乗ることで目当ての信号がかき消されるのを防ぐため、わざとオフセットさせて影響を受けなくするためらしい。 ただ、今回の場合はオフセットしなくてもそこそこうまくいった、またオフセットするとその分だけ帯域幅を広めないといけないというのがCPU負荷的に心配なため、本番運用ではオフセットは行っていません。 (一応パラメータで指定すればオフセットできるようにしています。この場合は取得する帯域幅も広める必要もあり。)

Throttleは気休めです。 RTL-SDR Sourceに対しては明確に要らなかったので外しています。 File Sourceに対しても……(この記事を書くにあたって改めて調べてみましたが)無いほうが良いかも。

Selectorは信号をSelectするだけです。 File SourceとRTL-SDR Sourceの切り替えに使っています。

復調部

次にFSKの復調部です。 入力部から渡された信号は、まずFrequency Xlating FIR Filterを通ります。 なんか3つあるんですけど……というのは、これは各入力で微妙にパラメータを変える必要があったためです。 1つの信号に対して3回処理しているわけではなく、それぞれの信号に対して1回だけ処理されます。

Frequency Xlating FIR Filterは、信号のダウンサンプリングと周波数オフセットを実施するブロックです。 パラメータに “Decimation: 10” と指定すると信号が1/10に間引きされ、たとえば入力サンプリングレートが230kだと出力サンプリングレートは23kになって出てきます。 Decimationの値を増やすと間引きされる量が増えて、後段で処理するデータ量が減るのでCPU負荷は減ります。 一方で間引きすぎると復調に失敗するようになるので注意が必要です。 また、パラメータ “Center Frequency” には周波数のオフセット量を指定します。 “Center Frequency: 500” だと500Hzオフセットされます。 BFSKの復調においては、ここにDeviation(LowとHighの周波数差 / 2)を指定しているようなコードを見かけたのでそうしてみましたが……これは受信したデータによるでしょう。 今回の場合だと、別に0でもいけました。 なお、パラメータの名前が “Center Frequency” だからといって、FSKにおける中心周波数と早とちりして “426.025MHz” などと指定してはいけません7。 File SourceもRTL-Sourceも、受信時に指定された周波数が中心 (0Hz) に出てきます。 そこから426.025MHzオフセットとは……?よく分からなくなります。

これで処理された信号は、Quadrature Demodに入ります。 これは直交復調(IQ復調)のブロックです。 SDRといえばこれ、みたいなところがありますよね。 説明は勘弁してください。 これに信号を通すと、下図のような信号が出力されてきます。

復調のイメージ 復調のイメージ

入力は周波数領域のグラフ(FFTの結果)、出力は時間領域のグラフです。 ともに縦軸は強度です。 出力を見るとの高/低に合わせて強度が変化し(ここでは周波数が高いと正の数、低いと負の数です)、1111…010101…と読み取れます。

ちなみに、GNU Radio(の外部コンポーネントも含めて)にはFSK復調に対応したブロックがすでに存在するようです。 ただ、少し試してみたものの自分ではうまく使えませんでした。 記事を書くにあたって改めて確認すると、後述するシンボル同期も含めてやってくれるかも知れませんが、既存デコーダでもFrequency Xlating FIR Filterはかます必要はありそうで、さらにデコーダのパラメータが何を意味しているのか調べる必要もあり……なら最初から自分でブロックを組み合わせたほうが柔軟にできるので良いんじゃないかという印象です。 よく理解したうえで使うならまだしも、よく分からない状態で楽をしようと使っても結局あまり楽はできないと思います。

シンボル同期

さて、無事に復調できましたね。 ではこれを0と1のビット列に変換していきましょう。 その前段階として、シンボル同期(クロックリカバリとも呼ばれる)を行います。

ここで利用するブロックはRoot Raised Cosine FilterとSymbol Syncですが、まずはシンボル同期そのものについて説明します。

とはいえ、十分に説明できるほどは分かっていません。 「シンボル同期」という名前もよく分からない8。 理解が浅いながらも、これは「(だいたいの場合)多数の点で構成されているシンボル(今回は0と1)を、適切な位置で読み取って、少ない点(通常は1つ)で表現する」操作だと理解しました。図で表すとこういうことです。

シンボル同期のイメージ シンボル同期のイメージ

入力は復調後の信号、出力はシンボル同期によるものを表しています。 ともに縦軸が振幅(実数)、横軸は時間ですが……入力と出力の時間の値は一致していません。 出力は入力に比べて遅れます。画像はイメージです。

ところで、多数の点を少ない点に変換するだけなら別にシンボル同期を使わなくとも良いでしょう。 サンプリングレートを落とすということなので、ダウンサンプリングやリサンプリングの操作により実現できます。 さて、いま1つのシンボルが1000個のデータで表現されているとして、ある位置から1000個データを読み出しました。 すると、0を表すデータが500個、1を表すデータが500個ありました。 あ~あ、0と1が切り替わる境目を読んでしまったようです。 この場合の出力は?0を出しても、1を出しても50%の確率で誤りとなるでしょう。 そして、次の1000個を読み出しても(0の連続や1の連続でない限り)、また50%の確率で誤りとなります。

シンボル同期だとうまくシンボルに合わせた0と1の出力が可能です。 具体的な方法としては(シンボル同期全般ではなく特定のアルゴリズムのことを指してしまっているかも知れませんが、)シンボル区間の中心に振幅ピークがあることを期待してそれを検出することにより実現します。 ピークのない波形、たとえば矩形波には適用できません。

今回の 復調後の信号を見ると、完全に矩形波ですとは言いませんが、全く矩形波じゃないとも言い切れない。 あんまりピークも無さそうです。 そこでシンボルの中心にピークを作成するため、フィルタをかまします。 ここに用いるのが Root Raised Cosine Filter ブロックです。 これにより、それっぽいピークが作成されました(イメージではない実際の結果は図「GRCの実行結果」の “RRC Filter” 部分)。

この信号をSymbol Syncブロックに通します。 パラメータ “Timing Error Detector” については、復調済みFSKに向いているらしいEarly-Lateを選択しました9。 他のパラメータ、特に “Expected TED Gain” と “Loop Bandwidth” についてはいろいろ試せとのこと10で、GUI上から試してそれらしい値を埋めてみましたがこれがベストかは分かりません。

これらを経て、なんとかビット列の抽出まで終えられました(イメージではない実際の結果は図「GRCの実行結果」の “Symbols” 部分)。 Symbol SyncのError出力が大きいような気はしますが……それなりに動いているのでこういうものと思うことにします。

出力部

これまでの処理により、きっぱりした信号が出てきました。 まだ内部は [2.8, 2.79, -2.47, 2.501, ...] といった具合に実数ですが、これを0と1のビット列に変換しましょう。

そこで用いるのが、Binary Slicerブロックです。 信号が正の数なら0x01、負なら0x00に変換されます。 え?ビット列じゃなくてバイト列になっていませんか? まぁまぁ、細かいことは置いておきましょう11

これでビット(バイト)ストリームがリアルタイムに流れてきます。 あとは、信号のペイロードと思われる部分の前に必ずあったプリアンブルと思われる 0101... という部分を検知したら続くペイロードを読み取ってパースするだけです。

ここからは自分で処理を書かないといけない……と思ったら、プリアンブルを検出して外部連携用の形式に変換してくれる既存ブロックがある模様。 それが、Sync and create PDUブロックです。 GNU Radio本体ではなく、gr-satellitesに含まれています。 パラメータとして、プリアンブルの情報とペイロード長を入力すると、そのプリアンブルを検知したときにペイロードが流れてきます。

後段では、とりあえずMessage Debugブロックを使ってコンソールに出力してみるのも良いでしょう。 今回は、Embedded Pythonブロックを使って独自のPythonスクリプト “THD501 Decoder” でデータを受け取ることにしました。 ここで作成したPythonスクリプトは「ペイロードを受け取ってパースし、結果をstdoutに出す」ものです。 具体的な内容はこれですが……後述するデータ分析の内容も含むのでネタバレとなります。 なお、Sync and create PDUの出力はSocket PDUブロックを用いてSocket経由でも受け取れるので、必ずしもEmbedded Pythonブロックを使う必要はありません。

ほか

その他のブロックについても一応述べておきます。

まず、ParameterブロックとVariableブロック。 どちらもIDとValueを設定でき、ここで指定したIDを他ブロックのパラメータとして指定するとValueの数値が代入されます。 Parameterブロックは(詳細は後述しますが)GRCをPythonスクリプトに変換したとき、そのスクリプトの引数として設定できるようになります。 一方、VariableブロックはGRCの内部だけで有効です。 なお、Valueの値は(実数だったら)10e3 などの指数表記で入力するのが無難だと思います。

はい次。GRCを見ると、QT GUI SinkとかQT GUI RangeとかQT GUI xxxブロックがありますよね。 これはお察しのとおりGUI系で、グラフやスライダーなどを表示してくれます。 画面内の配置はGUI Hintを与えることで調整可能です。 設定次第でコントロールパネル(グラフの一時停止が可能)の表示ON/OFFや凡例を消したり描画スタイルを変更することもできます。 また、パラメータとして”Center Frequency”や”Bandwidth”、“Sample Rate”などもありますが、これはあくまで表示上の話です。 誤った値を指定したからといってデータ自体が意図せず書き換えられるようなことはありません。 とはいえ表示は変わってしまい、理解の妨げになるので可能な限り正しい値を指定したほうが良いでしょう。

あとVirtual Sink、Virtual Sourceってブロックもありますね。 これはただ線の代わりです。 このブロックを消して線で繋いでも良い。 でもそうするとごちゃごちゃするので、それを避けるために使います。

また、ブロック全般の話ですが、ブロックを配置する際は画面右のメニューから目当てのブロックを探すわけですが……目grepしなくてもCtrl+Fで検索もできます。

そうしてブロックを配置していくと、ブロック同士の入出力フォーマットが合わずエラーになることがあるかも知れません。 ブロックの出力形式や入力形式は変えられることがあるので、適切なフォーマットにしましょう。

データの分析

さて、なんとかビット列まで抽出できましたね。 このビット列の符号化は?フォーマットは? 結論からいえば、符号化は単純なNRZ。周波数の高/低が0/1に対応しています。 (もしかしたらNRZIとして反転しているかも知れないが、データ自体に反転と非反転のデータが含まれているのであまり意識しなくても良いです。) フォーマットは下記の構成になっていると思われます。

フレームフォーマット フレームフォーマット

Head (n Bits)
「111...」が続きます。長さは一定ではありません。ゲイン調整用信号?かも知れませんがよく分かっていません。
Preamble (32 Bits)
「01010101010101010101010101010101」で一定です。同期用信号?こっちがゲイン調整用?かも知れませんがよく分かっていません。
Unknown (16 Bits)
ここから合計112 bitsのペイロード(と思われるデータ)が始まります。とはいえ、この先頭16 bitsが何を意味しているかは不明。手元の環境では「1101001000101011」で一定でした。もしかしたら製品個別のID……なのか、実はこっちが同期用信号か、さらに16 bitsの中で細分化されるかも知れませんが、やっぱりよく分かっていません。
Data0 (48 Bits)
温湿度のデータが含まれます。後述の「Data1」を全ビット反転させたデータと一致。
Data1 (48 Bits)
温湿度のデータが含まれます。前述の「Data0」を全ビット反転させたデータと一致。こっちをパースすると数値は(反転させず)そのまま読めて都合が良いので主にこちらで説明します。具体的な内容は後述。
Tail (n Bits)
「111...」が続きます。長さは一定ではありません。

Data1の内容はこんな感じです。(各文字は1bitを表す)

cccc cccc ... おそらくCRCやチェックサム
cccc cccc       と思われるが詳細は不明。
HHHH HHHH ... 相対湿度のBCD表現。上位4bitsは十の桁、下位4bitsは一の位を表す。
???? S??? ... 画面表示系と思われるが大部分は不明。"S"の箇所は温度の符号で、1ならマイナス。
???? TTTT ... 上位4bitsは不明。下位4bitsは温度のBCD表現で、十の位。
TTTT TTTT ... 温度のBCD表現。上位4bitsは一の位、下位4bitsは少数第一位。

SはちゃんとTHD501を冷蔵庫に突っ込んで観測しました。 いまのところ2年連続で夏になると温度センサを冷蔵庫に入れています12

THD501では、温湿度が測定範囲外になるとLoHiの表示になったり、温度がマイナスの場合は湿度が測定されず--という表示になったりします。 このときの温湿度のBCD表現は、一の位が0b1101 (13)などあり得ないデータになるので、そこで判別が可能です。

?の箇所は不明でした。 これは確認した範囲だと常に同じ値のようです。 おそらく電池残量のデータも含まれているんじゃないかとは思いますが……まぁこれは分からなくても大した影響はないでしょう。

cの箇所も不明です。 温湿度が変化すると、この値が変わります。 一方で、温湿度の変化がないとこの値も変わらないため、ランダムデータやカウンタではなさそうです。 この値はおそらくCRCやチェックサムなどと思われて、これは解明したかった。

CRCを確認しなくても、ペイロードに(反転すれば)同じ内容のData0とData1が含まれているなら、この一致性を確認することである程度のエラー検知はできるでしょう。 ただ、たまに「Data0とData1は不一致だけど、温湿度的にData1側は正しく、Data0側だけ誤りっぽい」ということが起きていました(逆にData0側が正しいパターンもある)。 このとき、Data0内とData1内それぞれのCRCを確認できれば、どちらか正しい方を採用できるので全体のエラーが減ります。 おそらく受信機側でこういった操作をさせることを前提に、事前に再送するというかForward Error Correctionというかで、わざと冗長なデータを送っているのでしょう。

でもやっぱり計算方法は分かりませんでした。 少なくとも単純なチェックサムではない。 CRC-16とみなしても、2つのCRC-8が連結されているとみなしても計算結果は一致しない。 CRC RevEng13で調べてみても該当なし。

特に謎なのがこのデータについて。

0xb05279936300 (crc=0xb052, humid=79%, temp=30.0C)
0xb05278936302 (crc=0xb052, humid=78%, temp=30.2C)

(CRCの計算元となる)データが異なっているのに、CRC(と思われる部分)が一致しています。

CRCが衝突してしまうこともあるでしょう。鳩の巣原理。 でもそんなレアな瞬間を観測できてしまったというのは、運が良すぎませんか? まぁ信号のキャプチャに失敗して誤ってデコードされてしまった可能性もあるでしょう。 でもこれはData0(の反転)とData1が一致しているんですよね……。 もしかしたらCRCの計算はデータの一部だけを使って行われるのかも知れません。 でも温湿度が変わっているのに変化しないCRCの意味とは……? 謎ですね14

CLI上での実行

一応デコードはできたので、GRCをCLI上から実行します。 最終的には温湿度のメトリクスをInfluxDBに突っ込んでGrafanaで可視化するところまでやりましょう。

これまで信号の受信には、RTL-SDR Blog V3ドングルを使っていました。 が、これを温湿度の受信程度に使うのはもったいない。 ここでの受信には2014年にaitendoで購入した “DVB-T+DAB+FM” と書かれたいしにえのUSBドングルを使いました。 こんな見た目のやつ、ケースは加水分解でベタベタ15です。

RTL-SDRドングル(上がRTL-SDR Blog V3ドングル、下がいにしえのドングル) RTL-SDRドングル(上がRTL-SDR Blog V3ドングル、下がいにしえのドングル)

一応、付属のアンテナでも信号を受信できました。 ただ、USBドングル内部の水晶発振器に由来すると思われる周波数のズレが大きい。 RTL-SDR Blogのドングルだとまったく気にならなかったところですが……こういう問題もあるんですね。 それでも調整するとなんとか正常に受信でき、信号のデコードもおおむね問題なし。 いや、たまにやっぱりダメかもと思うときはありますが……。

PC側はRaspberry Pi Zero Wを使います。 2Wではなく無印、シングルコアのarmhfです。 CPUにはなかなかリソースを食う、いややっぱり食いすぎてダメかもとと思うときもありますが、なんとかいけました。

以下からはこの環境でのセットアップと実行の様子について述べます。

セットアップ

ではセットアップしていきます。

とりあえず、GRCからPythonスクリプトを作成しておきましょう。 基本的にはこれまでGRCを開発していた環境(または後述の手順によりGNU Radioを導入していれば実行環境でも可)で、GRCを開いて “Run” > “Generate” すれば完了です。簡単ですね。 ただ、今回の場合はデバッグ入力用のブロックや、実行環境では不要なGUI画面があります。 これはどうしたかと言うと……手動で要らないブロックを消して別名で保存しました16。 こういうもの?他に方法はない?かは不明です。 開発用GRCと二重管理になるのであまり良くありません。

そしてこれを実行環境で動作させるわけですが、その環境にもGNU Radioや関連パッケージ、今回はgr-osmosdrとgr-satellitesのインストールも必要です。 もちろん、(過去に導入していなければ)RTL-SDR用のドライバのインストールが必要となります。

これらの導入はgr-satellitesを除いてapt installで問題ないと思いますが、gr-satellitesについてはパッケージが古く最新のGNU Radioには対応していません。 その他の方法で導入しましょう。 方法はいくつかあります17が、今回はソースコードからの導入を選びました。

これらの導入手順の一例としてはこんな感じです。 なお、この方法だとGNU RadioなどはシステムワイドのPythonライブラリとして導入されます。 それが嫌ならradiocondaを使うことになるでしょう。

ほとんど自分用のメモなので折りたたみ
# RTL-SDR
$ sudo apt install rtl-sdr
$ wget https://raw.githubusercontent.com/osmocom/rtl-sdr/refs/heads/master/rtl-sdr.rules
$ sudo mv rtl-sdr.rules /etc/udev/rules.d/
# reboot
$ rtl_test # 認識されているかテスト

# GNU Radio & gr-osmosdr
$ sudo apt install gnuradio gr-osmosdr

# gr-satellites
$ sudo apt install \
    liborc-0.4-dev libsndfile1-dev libspdlog-dev \
    python3-requests python3-construct python3-websocket \
    cmake
$ wget https://github.com/daniestevez/gr-satellites/archive/refs/tags/v5.7.0.tar.gz
$ tar zxvf v5.7.0.tar.gz
$ cd gr-satellites-5.7.0/
$ mkdir build
$ cd build
$ cmake ..
$ make
$ sudo make install
$ sudo ldconfig

gr-satellitesのビルドをRaspberry Pi Zero上で行うのは時間がかかりすぎてちょっと後悔しましたが……そのうち終わっていました。

実行の様子

セットアップが済めば、GRCから作成したPythonスクリプトがこんな感じに動作するはずです。

$ python3 thd501_decoder_release.py --help
usage: thd501_decoder_release.py [-h] [--base-samp-rate BASE_SAMP_RATE] [--down-samp-rate DOWN_SAMP_RATE]
                                 [--offset-tuning-freq OFFSET_TUNING_FREQ] [--ppm-correction PPM_CORRECTION]
                                 [--rrc-filter-coe RRC_FILTER_COE] [--samp-bw SAMP_BW]

options:
  -h, --help            show this help message and exit
  --base-samp-rate BASE_SAMP_RATE
                        Set capture sample rate [default='240.0k']
  --down-samp-rate DOWN_SAMP_RATE
                        Set down sampling rate [default='24.0k']
  --offset-tuning-freq OFFSET_TUNING_FREQ
                        Set offset tuning frequency [default='0.0']
  --ppm-correction PPM_CORRECTION
                        Set frequency correction (ppm) [default='0.0']
  --rrc-filter-coe RRC_FILTER_COE
                        Set rrc filter coe [default='10.0']
  --samp-bw SAMP_BW     Set capture bandwidth [default='2.5k']

# 負荷軽減のためにサンプリングレートや負荷に関連するフィルタの係数を落とす
# また周波数のずれを修正するためにRTL-SDRのppmを補正する
$ python3 thd501_decoder_release.py --down-samp-rate 8k --ppm-correction 42 --rrc-filter-coe 1
  :
THD501Decoder {"raw": "d22b2ead9e6c9cecd15261936313", "isValid": true, "data0": {"temperature": 31.3, "humidity": 61, "absolute_humidity": 19.874260292396634}, "data1": {"temperature": 31.3, "humidity": 61, "absolute_humidity": 19.874260292396634}}
  :
THD501Decoder {"raw": "d22b3fad9e6c9cedc05261936312", "isValid": true, "data0": {"temperature": 31.2, "humidity": 61, "absolute_humidity": 19.768142038197404}, "data1": {"temperature": 31.2, "humidity": 61, "absolute_humidity": 19.768142038197404}}

isValidはデータが正しく受信できているか示しています。 先頭がd22bから始まっているかつData0(の反転)とData1が一致するとtrue、それ以外はfalseです。 d22bは製品IDの可能性があり、もしかしたら他の環境だと正しく受信できているのにisValidがfalseになってしまうかも知れません。 temperatureとかhumidityは受信した信号由来の値。 もし測定範囲外だったらnullになります。 absolute_humiditytemperaturehumidityから計算した容積絶対湿度 [g/m³] です。 これも計算できなければnullになります。

このスクリプトをsystemdから操作できるようにしたりして、この出力をInfluxDBに格納してGrafanaで可視化した結果がこちら。

Grafanaの画面 Grafanaの画面

おおむねうまくいっていますがたまにデータが欠損、つまり受信できていないときがあります。 主な原因は周波数のずれ。 ppmを補正しているものの、時間が経って温まってきたり、周囲の温度変化とともにずれていきます。 困りましたね。 根本的な解決としては、USBドングル内部のクロックをTCXOに交換するしかない。 でも、この交換用TCXOが1000円くらいします。 これ本体は999円で買ったドングルなんですけど……(MRKBNN)。 信号の周波数は既知なので、頑張ってピーク周波数を検知して補正というのもできるかも知れません。 ただ、そうこうしているうちにUSBドングルがアチアチになって安定してきました。 それはそれで大丈夫なんですかね……というのはありますが、しばらくはこれで様子を見ましょう。

なお、CPU使用率についてはおおむね40%くらいです。 品質を犠牲にしてパラメータを調整してもこれくらいが限界でした。 それでもたまにCPU使用率が100%となってOverrunし、取りこぼしが発生するんですけど……(UTMAOB)。 まぁこれも様子を見てみます18

おわりに

温湿度計THD501の無線信号について調査し、RTL-SDRとGNU Radioを用いてデコードしました。

SDRでセンサーの信号をデコードする、なんてのはありきたりな話ですがうまくいって良かったです。 たかがBFSKの低速信号をデコードしただけですが、それでも嬉しい。 THD501の購入当初から考えていたことが実現できました。 アップロードに10年かかった動画です。 今回うまくいったことで、もう無線信号はすべてデコードできるようになったといっても過言ではない19

でも今回の信号が、2値で固定長で暗号化なしの信号で良かったです。 4FSKとかQPSKとか16QAMによる変調で、可変長で、暗号化もされています、とかだとお手上げです。 そんな高度な信号を使う温湿度計があるとしたら、それ本当に温湿度計?という話ですけどね。

おわり。

Footnotes

  1. 現在だと同じ機能を持ちながら安価で高精度な製品もあったり、BLEやESPNowとかガワにはタカチのケースを使ってDIYすることも視野に入りますが……当時はESP-WROOM-02が技適通ったくらいのタイミングです。BLEは……よく分かりませんが、安価に済ませたければPICと市販のBluetooth-USBドングルを組み合わせるのが主流くらいのときじゃないでしょうか。

  2. SDRを試した日記 その0

  3. 「工事設計認証をした年月日 令和元年」?THD501の発売は2012年なのに?と思ったら、古い番号の技適 (004YVA0050)もある模様。おそらく手持ちの子機はこっちの番号が振られていることでしょう。

  4. 今回の場合はハードウェア(RTL-SDR)的に送信できません。仮にF5OEO/rpitなどを使うとしてもこれはこれで法律的に送信はできないんじゃないかという気がしますが……。

  5. どんな感じ?ソンな感じではない、具体的なファイルはこれです。

  6. Supported signal file formats · jopohl/urh Wiki

  7. こうなります。でもなんとなく動いているように見えるのがすごい。

  8. シンボルに同期するのでシンボル同期?そう言われるとそうですが……。名前だけだとよく分からない。「eBPF(Extended Berkeley Packet Filterという名前の割に、もはやBerkeleyやPacket Filterの風味は薄い)」「シリアル通信(シリアルな通信)、シリアルな通信(シリアル通信)」のと同じ気配を感じます。

  9. 慣習としては “Clock Recovery MM” が選択されることもあるようです。GNU RadioでもClock Recovery MMブロックが存在しますが現在はDeprecatedで、代わりにSymbol Syncブロック内でClock Recovery MMを選択することが推奨されています。パラメータの対応についてはSymbol Syncの説明スライド P32付近に記載あり。

  10. Symbol Sync - GNU Radio

  11. Pack K Bitsブロックを使えば(実質ビット列の)バイト列を圧縮して、本当のバイト列にできるかも知れませんがよく分かっていません。

  12. 冷蔵庫内気温と消費電力量の測定

  13. 使い方についてはこちらの記事が詳しい: Reverse Engineering Cyclic Redundancy Codes | Hackaday

  14. 生データのログ(項目の意味については本記事「実行の様子」に記載)はあるので、検証しようと思えばできますが……。

  15. ワセリンとエタノールで拭いてみましたが……まだちょっとベタベタします。

  16. GRCはこれ、これを基に作成されたPythonスクリプトはこれこれです。

  17. Installation — gr-satellites 5.7.0 documentation

  18. 今回は様子見としてチケットクローズいたします(先送り)。

  19. 過言。