リモコンBOXの製作

当局のメイン無線機はredpitayaボードとSDRソフトthetisを組み合わせたSDR機です。
5年程使用しています。これまで、thetisをマウス操作とredpitayaに直接接続の外付けPTTスイッチで操作運用してきました。

ところが、最近になってHam Log, HQSL, Free DV等の無線関連ソフトの利用が増え、マウスのフォーカスの切り替えが頻繁になり、またマウスカーソルの所在も視力の衰えで確認が難しくなってきました。そんなことからthetisのマウスの誤操作で意図しない周波数変更等をしてしまうなどの事象が多くなってきました。そこで、thetisの操作は、マウスを使わず行える様、次の仕様のリモコンBOXを製作しました。

「仕様」

1.USB接続で、電源はUSBバスから供給されること。

2.PTT、音量増減操作を押しボタンSWで行えること。

3.送受信周波数をロータリエンコーダで可変できること。

4.  送受信周波数、周波数増分値、音量の現在値を表示すること。

5.thetisとの通信不具合時に備え、再接続、リセットが押しボタンSWで行えること。

6.thetisとの間の通信プロトコルはPOWE SDRのCATコマンド形式によること

前記仕様を目標に、ソフトウェア部分はマイコンボードRaspberry Pi Picoを使用し、開発環境thonny上でmicrophythonで行った。

ブレッドボードで試作

一応完成

ソースプログラムを以下に示す。

大きな文字表示のためmfontモジュール、ロータリエンコーダの読み取りのためにはRotaryIRQモジュールを使用した。

##  ver 1.01
# 1.00  初期バージョン ポーリング方式
# 1.01  UARTの受信処理を割り込みハンドラで行うように変更

from ssd1306 import SSD1306_I2C
from rotary_irq_rp2 import RotaryIRQ
from machine import Pin, I2C, UART
from mfont import mfont
import time

send_wait = 20

'''
###### raspberry pi pico (RP pico)から周辺への接続 ########

[RP pico]    [ Encoder, SW, タクトSW ]
GPIO04 ーー 送信表示赤色LED
GPIO05 ーー タクトSW(周波数ステップ切り替え)  
GPIO18 ーー ロータリエンコーダ       
GPIO19 ーー ロータリエンコーダ
GND ーーーー ロータリエンコーダ(GND)
GPIO20 ーー PTTスイッチ
GPIO21 ーー タクトSW(音量大きく)
GPIO22 ーー タクトSW(音量小さく)

[RP pico]  [ディスプレイ 128 x 32]
GPIO14 ーー OLEDディスプレイSCL
GPIO15 ーー OLEDディスプレイSDA
3.3_EN ーー RESET SW
GND    ーー GND
3.3v   ーー VCC

[RP pico]   [FT232RLモジュール]
GPIO16 ーー RX
GPIO17 ーー TX
GND    ーー GND
VSYS   ーー 5V out

「電源について」
 RP picoの電源はFT232RLモジュールから供給、FT232RLモジュール
 はPCとUSB接続されるのでUSB給電となる。
##########################################################
'''

i2c = I2C(1, scl=Pin(15), sda=Pin(14), freq = 400_000)    # I2Cを初期化
oled = SSD1306_I2C(128, 32, i2c) 	      # 幅=128、高さ=32 でSSD1306を初期化

#  ロータリエンコーダの初期化 ( GPIOピンの指定 & 初期値)
rotary_encoder = RotaryIRQ(
    pin_num_clk=18,
    pin_num_dt=19,
    min_val=0,
    max_val=1000,
    reverse=True,
    range_mode=RotaryIRQ.RANGE_UNBOUNDED,
)

#  GPIO20から22, 5を入力ピンとし、プルアップする。
ptt_pin = Pin(20, Pin.IN, Pin.PULL_UP)     # PTT SW
volinc_pin = Pin(21, Pin.IN, Pin.PULL_UP)  # 音量増大
voldec_pin = Pin(22, Pin.IN, Pin.PULL_UP)  # 音量減少
fstep_pin = Pin(5,Pin.IN, Pin.PULL_UP)     # 周波数変化量

# 送信を示すLED点灯ポート
tx_led = Pin(4, Pin.OUT)  # 送信表示LED

# フォントの表示
def drawFont(self, font, x, y, w, h, flg=False):
    bn = (w+7)>>3
    py = y
    for i in range(0, len(font), bn):
        px = x
        for j in range(bn):
            for k in range(8 if (j+1)*8 <=w else w % 8):
                self.pixel(px+k,py, 1 if font[i+j] & 0x80>>k else 0) 
            px+=8
        py+=1
    if flg:
        self.show()

# 改行
def newLine(self):
    self.x=0
    if self.y+self.mf.fs*2 > 32:
        self.scroll(0, -self.mf.fs)
        self.fill_rect(0, self.y, 128, 32-self.y, 0)
        self.show()
    else:
        self.y=self.y+self.mf.fs
    
# テキストの表示
def drawText(self, text, x, y, fs, wt=0):
    self.x = x
    self.y = y
    
    # フォントの設定
    self.mf = mfont(fs)
    self.mf.begin()

    # テキスト表示
    for c in text:
        if c == '\n': # 改行コードの処理
            self.newLine()
            continue
        code = ord(c) 
        font = self.mf.getFont(code)
        if self.x+self.mf.getWidth()>=128:
            self.newLine()
        self.drawFont(font, self.x, self.y, self.mf.getWidth(), self.mf.getHeight(), True)
        if wt:
            time.sleep_ms(wt)
        self.x+=self.mf.getWidth()
    self.mf.end()

# SSD1306_I2Cにインスタンス・メソッドの追加
SSD1306_I2C.drawText = drawText
SSD1306_I2C.drawFont = drawFont
SSD1306_I2C.newLine  = newLine

# thetisの現在音量を問い合わせる 
def inq_vol():
    uart.write(b"ZZLA;")
    time.sleep_ms(send_wait)    

# RX1の音量をvolの値に設定
def set_vol():
    w = "00"+str(rvol)
    com = w[-3:]+";"
    uart.write(b"ZZLA"+com)

def uart_reset():
    uart = UART(0, baudrate=19200, tx=Pin(0), rx=Pin(1),
                 timeout = 50, flow = UART.RTS|UART.CTS)

# 現在の周波数を取得
def inq_freq():
    uart.write(b"ZZFA;")
    time.sleep_ms(send_wait)

def set_freq():
    w = "0000"+str(freq)
    com = w[-11:]+";"
    uart.write(b"ZZFA"+com)
    time.sleep_ms(send_wait)
       
# 送信モードへ
def tx_on():
   uart.write(b"ZZTX1;")
   
# 受信モードへ   
def tx_off():
   uart.write(b"ZZTX0;")
  
# 周波数、増分を表示
def oled_disp():
    ff = freq / 1000.0
    f = str(ff)+"0"
    s = f[0:7]
    ss = " "+str(f_step)+" Hz"
    l = len(ss)
    sss = ss[l-7:7]
    n = int(rvol/4)
    v = "-"*n + "         "
    v = v[0:14]
    oled.drawText(s, 0, 0, 24, 0)   
    oled.drawText("KHz", 91, 4, 12, 0)
    oled.drawText(sss, 72, 23, 11, 0) 
    oled.drawText(v, 0, 24, 11, 0)

# uart 受信割り込みハンドラー
# thetisに対する周波数、音量問い合わせの応答を得て
# freq, rvolに値を設定する
def uart_recv(uart):
    global freq, rvol
    buf = uart.read()
    if len(buf) == 16:
        freq = int(buf[7:15])
    elif len(buf) == 8:
        rvol = int(buf[4:7])
    else:
        return

# ロータリエンコーダ、タクトSWの初期値を記憶。
last_rotary_value = rotary_encoder.value()
last_fstep_state = fstep_pin.value()
last_ptt_state = ptt_pin.value()
last_volinc_state = volinc_pin.value()
last_voldec_state = voldec_pin.value()

# 初期値の設定
f_stepa = [100, 500, 1000, 2500, 5000]
f_stepi = 1
freq = 710_000_0
bfreq = 710_000_0
f_step = 500
update = 0
rvol = 10
tx = 0
n = 0

# oledの初期設定
oled.contrast(255)
oled.invert(False)

# thetisの実行を待つ、 PTTを押すと次へ進む
while ptt_pin.value() != 0:
    oled.drawText("Conecting", 0, 0, 24, 50) 
    time.sleep_ms(10)
    oled.fill(0)
    
#    oled.drawText("         ", 0, 0, 24, 50)
    
oled.fill(0)

# UARTの初期化
uart = UART(0, baudrate=19200, tx=Pin(0), rx=Pin(1),
               flow = UART.RTS|UART.CTS)

# UARTの割り込みハンドラ登録
uart.irq(handler=uart_recv, trigger=UART.IRQ_RXIDLE)
time.sleep_ms(10)

# 周波数、音量の現在値を得る
inq_vol()
inq_freq()
oled_disp()

# 以下永久ループで処理を行う

while True:

# ロータリエンコーダ、押しボタンSWの現在の値を読む
    current_rotary_value = rotary_encoder.value()
    current_ptt_state = ptt_pin.value()
    current_fstep_state = fstep_pin.value()
    current_volinc_state = volinc_pin.value()
    current_voldec_state = voldec_pin.value()    
    update = 0
    
# ロータリエンコーダ、押しボタンSWの状態変化により処理を行う

# PTT SWの変化
    if last_ptt_state and not current_ptt_state:
        if tx == 0:
            tx_on()
            tx = 1
            tx_led.value(1)
            next
        else:
            tx_off()
            tx = 0
            tx_led.value(0)
            next
            
# ロータリエンコーダの変化    
    if last_rotary_value < current_rotary_value:
        inq_freq()
        freq += f_step
        set_freq()
        oled_disp()
        update = 1
        next
    else:
        if last_rotary_value > current_rotary_value:
            inq_freq()
            freq -= f_step
            set_freq()
            oled_disp()
            update = 1
            next
            
# ロータリエンコーダの押しボタンSW
    if last_fstep_state and not current_fstep_state:
        f_stepi += 1
        f_stepi = f_stepi % 5
        f_step = f_stepa[f_stepi]
        update = 1
        next

# 音量ボタンSW
    if last_volinc_state and not current_volinc_state:
        rvol += 8
        update = 1
        if rvol > 100:
            rvol = 100
        set_vol()
        next

    if last_voldec_state and not current_voldec_state:
        rvol -= 8
        update = 1
        if rvol < 0:
            rvol = 0
        set_vol()
        next

# 100回に1回thetisと同期
    if (n % 100) == 0:
        inq_freq()
        inq_vol()
        oled_disp()
    
    n += 1    
    bvol = rvol
    bfreq = freq
    last_rotary_value = current_rotary_value
    last_fstep_state = current_fstep_state
    last_ptt_state = current_ptt_state
    last_volinc_state = current_volinc_state
    last_voldec_state = current_voldec_state
    time.sleep_ms(send_wait)