nihsokのアイコン画像
nihsok 2023年11月03日作成 © GPL-3.0+
セットアップや使用方法 セットアップや使用方法 閲覧数 542
nihsok 2023年11月03日作成 © GPL-3.0+ セットアップや使用方法 セットアップや使用方法 閲覧数 542

SDRで受信した短波FAXを電子ペーパーに表示

SDRで受信した短波FAXを電子ペーパーに表示

本記事は2部構成からなる。

  1. SDRで受信した短波FAXをデコードして画像を取得
  2. Raspberry Pi Pico Wで画像をアップロードして電子ペーパーで表示

今回は気象庁の気象模写無線通報を対象とする。(https://www.jma.go.jp/jmh/jmhmenu.html

概念図

1. SDRで受信した短波FAXをデコードして画像を取得

短波をSDRドングルで受信して復調した音声をもとに、白~黒を割り当てて画像に変換する。

材料

  • SDR
  • アンテナ
    今回はRTL-SDR.com公式のアンテナとのセット(https://ja.aliexpress.com/item/32936334224.html?gatewayAdapt=glo2jpn)を購入。7000円程度
  • Raspberry Pi 3A+
    ハードの制限はあまりない。USBポートがありLANに繋がってOS(とPython)が動けばOK。スペックが高い方が画像がすぐできる。

受信した信号から画像のデコード

IOC576という規格に従う。特徴としては

  • USB (upper sideband) で変調されている。放送周波数の1.9kHz下でUSB受信したときに、1.5kHz~2.3kHzが白~黒に対応する。
  • 1分間に120行の画像が送信される(=0.5秒で改行)

この仕様に従い、まずはUSBで復調する。

usb.sh

timeout -k 20 1140 /usr/local/bin/rtl_fm -d 2 -f 7793.1k -M usb -s 23040 -g 49.6 -E dc -E direct2 | sox -t raw -e signed -c 1 -b 16 -r 23040 - /tmp/fax.wav

とりあえず20分弱受信している。rtl_fmはサンプリングレートを指定できるので、576の倍数Hzにすると後のパラメータのきりがよくなる。
次に音声のFFTにより最大振幅となる周波数を求め、白~黒に割り当てる。

fax.py

import numpy as np import wave from PIL import Image #0=0Hz,1=288Hz,4=1152Hz,5=1440Hz,8=2304Hz,20=5760Hz,40=11520Hz slide = 8 window = 80 lbound = 5 ubound = 9 factor = 80 taper = np.hanning(window) with wave.open('/tmp/fax.wav','r') as w: data = np.frombuffer(w.readframes(-1),dtype='int16') ary = np.array([ np.argmax(np.abs( np.fft.rfft( taper*data[i:i+window] )[lbound:ubound] )) for i in range(0,len(data)-window,slide) ]) width = int(2*5760/slide) ary = np.append( np.minimum(ary*factor,255) , [255]*( width - len(ary) % width ) ).reshape([-1,width]) ary = np.roll(ary,-np.argmin(np.sum(ary,axis=0)),axis=1) Image.fromarray(ary.astype(np.uint8)).save('tmp.png')

8点ずつスライドして1度に80点をFFTしている。この場合波数5~9が白~黒に対応(=5段階のグレースケール)。グレーの諧調を増やすためには周波数解像度を上げる必要があるが、そのためにはより長い時間幅についてFFTを行う必要があり、時間解像度が犠牲になる(不確定性原理)。0埋めしてからFFTすることで周波数解像度のみ上げることも考えられるが、計算時間が余計にかかってしまう。

現在は端から順番に計算しているため20分の音声データを変換するのに10分程度かかっているが、Numpy.lib.stride_tricks.sliding_window_viewで並列に計算すれば時間を短縮できるかも?

秒単位の制御では画像の開始をとらえることができない。IOC576の仕様では冒頭に同期のための位相信号が送信されるが、今回の手法ではノイズが多くあまりあてにならないので、ポストプロセスとして一番黒い部分を画像の端にシフトすることで解決している。

2. Raspberry Pi Pico Wに画像をアップロードして電子ペーパーで表示

市販のRaspberry Pi Pico HATを使い、電子ペーパーに画像を表示させる。また、オンライン(LAN経由)で画像をアップロードできるようにする。

材料

ハードウェアの準備

Pico Wにピンをつけて、ソケットに差し込む
HATの表側はピンを指すためのソケット、裏側はピンがとびでている構造で、表側に設置するようにと書かれている。はじめ手を抜いて裏側に設置しようとしたが、基板上のスイッチとPico WのMicroUSBポートとが干渉して設置できなかった。手許にPi 3A+から抜き取ったピンがあったので流用した。ピンは全部接続する必要はなく、11か所(VSYS, GND, RUN, GP2,3,8,9,10,11,12,13)で十分。

ソフトウェアの準備

ドキュメントに従って進める。
https://www.waveshare.com/wiki/Pico-ePaper-2.9#Hardware_Connection (2.9インチ用だが、基本的には同じはず)

Pico WはMicroPythonを動かすための初期設定を行う(省略)

以下の公式サンプルコードをコピペして実行すると、デモ用の画像が描画される。
https://github.com/waveshareteam/Pico_ePaper_Code

画像の準備

任意の画像を表示するためには、画像のサイズのframebufferに0(黒)1(白)を代入して与える。Image2Lcdという便利なソフトがあるらしいが、簡単に実装してみたのが以下のコード。

img2grayfb.py

from PIL import Image import numpy as np import sys inv_flag = True #True fo wb, False for rwb width = 800 height = 480 rotate = 0 gray = Image.open(sys.argv[1]).convert('LA') width_org, height_org = gray.size aspect_org = width_org/height_org if aspect_org < 1 : gray = gray.rotate(90,expand=True) width_org, height_org = height_org, width_org aspect_org = width_org/height_org # resize, monochrome(0/1), padding, and return narray if aspect_org > width/height: #high aspect -> adjust width & pad height height_tmp = round(width/aspect_org) img2d = np.vstack(( np.array(gray.resize((width,height_tmp)).convert('1')), np.ones((height-height_tmp,width),dtype=np.uint8) )) else: #low aspect -> adjust height & pad width width_tmp = round(height*aspect_org) img2d = np.hstack(( np.array(gray.resize((width_tmp,height)).convert('1')), np.ones((height,width-width_tmp),dtype=np.uint8) )) if inv_flag: img2d = 1-img2d shift=np.uint8(range(0,8)[::-1]) with open('tmp.buf','wb') as f: for j in range(0,height): f.write(np.array( [np.left_shift( img2d[j,i:i+8],shift ).sum() for i in range(0,width,8)], dtype=np.uint8 ))

画像がなるべく大きく表示されるように回転して、横長の画像は横幅に合わせ、縦長の画像は縦幅に合わせている。

最初はグレーも対応するつもりだったので少し複雑になっている。

画像のアップロード処理

公式サンプルコードのmainの部分のみ少し書き加えたコードをRaspberry Pi Pico Wで動かす。

main.py

if __name__=='__main__': epd = EPD_7in5() epd.Clear() ip = wifi.connect('ssid','password') print(ip[0]) epd.text(ip[0],5,10,0xff) epd.display(epd.buffer) s = usocket.socket() s.setsockopt(usocket.SOL_SOCKET,usocket.SO_REUSEADDR,1) s.bind(('0.0.0.0',80)) s.listen(1) epd.delay_ms(2000) epd.sleep() while True: cl, addr = s.accept() data = b'' while not b'\r\n\r\n' in data: data += cl.recv(1024) print(data) with open('tmp.buf','wb') as f: f.write(data[data.find(b'\r\n\r\n')+4:]) while True: data = cl.recv(4096) if not data: break f.write(data) cl.close() epd.init() with open('tmp.buf','rb') as f: for i in range(48): epd.buffer[i*1000:(i+1)*1000]=f.read(1000) epd.display(epd.buffer) epd.delay_ms(2000) epd.sleep()

wifi.py

import network import utime def connect(ssid,passwd,retry=10): wlan = network.WLAN(network.STA_IF) wlan.active(True) wlan.connect(ssid,passwd) utime.sleep(4) return wlan.ifconfig()

電源をつないだときにはIPアドレスが表示される。
あとはそのIPアドレスに先ほど作成したframebufferを送るたびに画像が書き換えられる。

send.sh

timeout 3 curl -X POST -H "Content-Type: application/octet-stream" --data-binary @tmp.buf http://192.168.10.101

結果

AWPN
FXFE572/FXFE82

まとめ

SDRで短波FAXを受信しで画像に変換し、電子ペーパーで表示した。

画像が結構ノイジーになる行がある。大丈夫なときははっきり見えているのでハード面、特にアンテナが短波用ではないのが原因と考えられる。
(現時点で、雨が降っているとノイズが増えることは確認済み)

FFT部分のパラメータはもう少し調整してもよいかもしれない。

今後の課題

  • FFTを並列にして高速化
  • スケジュール化
  • 電子ペーパーディスプレイに枠をつける
  • 短波用アンテナを試す
  • nihsok さんが 2023/11/03 に 編集 をしました。 (メッセージ: 初版)
  • Opening
    mipsparcのアイコン画像 mipsparc 2023/11/09

    NumPyの部分を、AWS Lambdaなどクラウドに投げてしまうことによって、一瞬で復調が完了するかなと思いました。クラウドコストは実行時間課金なので僅かだと思います。よかったらご検討ください!

    nihsokのアイコン画像 nihsok 2023/11/13

    確かに、処理能力の高いクラウド環境で並列化もかけてさっさと復調させるのもありですね。ただ今のままだと中間ファイルとして約30MB/20分のWAVデータを転送する必要があるので、もう少しうまい方法がないか考えてみようと思います。

    mipsparcのアイコン画像 mipsparc 2023/11/13

    WAVはZIPなど可逆圧縮をかけた時の圧縮率が高いのでそのうえで転送してもいいかもしれません。

    nihsokのアイコン画像 nihsok 2023/11/18

    それは思いつきませんでした。ただ、圧縮する時間が結局かかるのと、電波(というよりかは大気)の状態によって受信できていないときがあるので、ちゃんと画像ができないときの残念さを考えるとそこまでリソースを割かなくてもよいかなと思っています。

    3 件の返信が折りたたまれています
ログインしてコメントを投稿する