本記事は2部構成からなる。
- SDRで受信した短波FAXをデコードして画像を取得
- 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経由)で画像をアップロードできるようにする。
材料
- Raspberry Pi Pico W
秋月電子で1200円(税込) - 800×480, 7.5inch E-Ink display HAT for Raspberry Pi
(https://www.waveshare.com/product/raspberry-pi/displays/e-paper/7.5inch-e-paper-hat.htm)
$56.99+送料$5=計9000円程度
ハードウェアの準備
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
結果
まとめ
SDRで短波FAXを受信しで画像に変換し、電子ペーパーで表示した。
画像が結構ノイジーになる行がある。大丈夫なときははっきり見えているのでハード面、特にアンテナが短波用ではないのが原因と考えられる。
(現時点で、雨が降っているとノイズが増えることは確認済み)
FFT部分のパラメータはもう少し調整してもよいかもしれない。
今後の課題
- FFTを並列にして高速化
- スケジュール化
- 電子ペーパーディスプレイに枠をつける
- 短波用アンテナを試す
投稿者の人気記事
-
nihsok
さんが
2023/11/03
に
編集
をしました。
(メッセージ: 初版)
Opening
mipsparc
2023/11/09
nihsok
2023/11/13
mipsparc
2023/11/13
nihsok
2023/11/18
ログインしてコメントを投稿するNumPyの部分を、AWS Lambdaなどクラウドに投げてしまうことによって、一瞬で復調が完了するかなと思いました。クラウドコストは実行時間課金なので僅かだと思います。よかったらご検討ください!
確かに、処理能力の高いクラウド環境で並列化もかけてさっさと復調させるのもありですね。ただ今のままだと中間ファイルとして約30MB/20分のWAVデータを転送する必要があるので、もう少しうまい方法がないか考えてみようと思います。
WAVはZIPなど可逆圧縮をかけた時の圧縮率が高いのでそのうえで転送してもいいかもしれません。
それは思いつきませんでした。ただ、圧縮する時間が結局かかるのと、電波(というよりかは大気)の状態によって受信できていないときがあるので、ちゃんと画像ができないときの残念さを考えるとそこまでリソースを割かなくてもよいかなと思っています。