非職業的技師の覚え書き

JK1EJPの技術的検討事項を中心に記録を残します。

RS-HFIQ(5)QuiskへのRS-HFIQ内蔵テスト機能の実装

HobbyPCB社から提供される「RS-HFIQ Control Panel」を、Omni-Rigを使用しないQuiskと併用することが出来ないことが判明しました。そこで、「RS-HFIQ Control Panel」のBIT(Built-In Test)機能をQuiskに組み込むことにしました。

QuiskへのRS-HFIQ内蔵テスト機能の実装

RS-HFIQ内蔵テスト機能を追加したソフトウェアの構成

SDRソフトウェアは規模が大きく複雑ですが、Quiskのソースコードにはコメント記入が少なく、理解に時間を要しました。適所にコメントが記入されていたKeith's SDRと比較して、そのような感想を持ちました。

Quiskの制御系はPythonオブジェクト指向でコーディングされています。クラスの継承や相互参照が複雑になると、インスタンス化がどこで行われているかを追跡し難くなります。今回、RS-HFIQを表す「Hardware」クラスのインスタンス化がどこで行われているか分かりませんでした。それでも、テスト機能の実装はできました。

RS-HFIQ内蔵テスト機能を追加したQuiskソフトウェアの関連部分の構成を下記に示します。

Quisk-based RS-HFIQ built-in test function configuration.

Software configuration with additional RS-HFIQ built-in test function.

ファイルの構成と改造内容の骨子は下記の通りです。

  1. quisk.py ・・・QuiskのMain GUI
    RS-HFIQ内蔵テスト機能の制御パネルを開くボタンの追加。

  2. rshfiq_lib.py ・・・RS-HFIQ内蔵テスト機能の制御パネル
    HobbyPCB社「RS-HFIQ Control Panel」のBIT機能相当の新規開発。

  3. hardware_usbserial.py ・・・RS-HFIQとのシリアル制御通信
    BIT機能に係わる通信部分("BF"、"OB")の追加。

QuiskのGUI Toolkit

アプリのGUIを1から構築するには大変な労力を要するため、Toolkit(GUIを構成する部品widgetの集合)を利用するのが一般的です。Pythonでも、TkinterPyQtwxPython、・・・等の複数のToolkitを利用可能です。

この中、QuiskのGUIwxPythonを利用して構築されています。wxPythonの特徴はsizer(採寸機?)を活用して、ボタン等の部品widgetを柔軟に配置して行く作法を取る点かと思います。画面に部品を配置するためには、部品の寸法と配置座標を画面座標系で厳密に定義する必要がありました。wxPythonでは部品の相対寸法と行列位置を指定すれば、sizerが寸法と配置座標を計算して自動配置してくれます。他方、例えばTkinterではpack関数を用いて部品を積み上げたり並べることで部品配置を行います。

Quiskに機能を追加するためには、まずwxPythonを攻略する必要があります。Quisk GUIの元締めはwxPythonのアプリクラスを継承したAppです。Appがボタンクリック等のユーザイベントをポーリングして、イベントに対応するコールバック関数を実行します。

Quiskのボタンパネルは、左右の2列と上下の4行で構成されています。

The Quisk button panel consists of two rows on the left and right and four rows on the top and bottom.

継続して開発されてきた過程でコーディングスタイルが変遷してきた面もあるようですが、基本はAppクラスのMakeButtons関数の中で以下のスタイルでボタンを配置しているようです。

  1. ボタンパネルの左右2列の各行に対してボタンリストを初期化
  2. 実装機能のボタンwidgetを作成
  3. 作成したボタンwidgetをボタンリストに格納
  4. 上記2~3を繰り返してボタンリストを完成
  5. sizerを用いてボタンリストの各ボタンwidgetを反復配置

各ボタンwidgetのコールバック関数はAppクラスのメンバとして作成します。

RS-HFIQ内蔵テストパネル起動ボタンの実装

ボタンパネルの左列5行目にRS-HFIQ内蔵テストパネル起動ボタンを増設することにしました。上記手順の1から3のボタン作成のコードをMakeButtons関数の中に追加します。QuiskPushbuttonクラスはquisk_widgets.pyの中に定義されています。OnBtnRSHFIQがRS-HFIQ内蔵テストパネルを起動するコールバック関数です。

left_row4 = []
## A plain push button widget to open RS-HFIQ control panel.
b = QuiskPushbutton(frame, self.OnBtnRSHFIQ, text='RSHFIQ')
left_row4.append(b)

続いて、手順5のボタン配置のコードをMakeButtons関数の中に追加します。今回追加するボタンは「RSHFIQ」の1つだけですが、Quiskの作法に則り、反復配置でコーディングしています。これで、将来のボタン増設は容易になります。

row = 4
col = button_start_col
for b in left_row4:
    self.idName2Button[b.idName] = b
    ## Add() メソッドでは追加する widget の位置(行, 列)とサイズ(縦, 横)を指定
    botton_size_x = 1
    botton_size_y = 2
    gbs.Add(b, (row, col), (botton_size_x, botton_size_y), flag=flag)
    col += botton_size_y

最後に、ボタンクリックのコールバック関数OnBtnRSHFIQをAppクラスのメンバ関数として作成します。

def OnBtnRSHFIQ(self, event):
    print('OnBtnRSHFIQ() called back with event', event)
    rshfiq_lib.control_panel(Hardware)

RS-HFIQ内蔵テストパネルはGUIであり、rshfiq_lib.pyの中にcontrol_panelクラスとして新規実装しています(詳細後述)。ここでは、そのインスタンス化(実体化)を行うことで起動しています。コンストラクタの引数のHardwareインスタンスがRS-HFIQとのシリアルCAT通信機能をOmni-Rigに代ってQuiskでは担っており、RS-HFIQ内蔵テストパネルを実装する上で必須の引数になります。

Hardwareクラスは、ダウンロードしたhardware_usbserial.pyの中にQuiskのベースモデルクラスを継承する形で実装されています。元はVFO制御に係わるシリアル通信機能しか実装されていませんでしたが、今回改良してRS-HFIQ内蔵テストに係わるシリアル通信機能も追加実装しました(詳細後述)。

quisk.pyの先頭で新規開発のrshfiq_libを取り込んでおきます。インポート元のrshfiqはサブディレクトリ名です。

from rshfiq import rshfiq_lib  ## added for RS-HFIQ control pane

エラーが発生した際にlogファイルを自動表示する機能がQuiskには造り込まれています。親切な造りです。

A log file that is automatically displayed when an error occurs.

デバックが完了すれば、Quiskのボタンパネルの左下に「RSHFIQ」ボタンが表示されます。

RS-HFIQ built-in test panel activation button shown.

RS-HFIQ内蔵テスト制御パネルの実装

QuiskのGUIは前述の通りwxPythonをToolkitとして実装されていますが、RS-HFIQ内蔵テスト制御パネルはTKinterをToolkitとして実装しました。単なる慣れの問題で他意はありません。なお、オリジナルのHobbyPCB社提供の「RS-HFIQ Control Panel」はVisualBasicで実装されており、Windows専用になっています。

 RS-HFIQ built-in test control panel.

引数で受け取った前述のHardwareインスタンスのメソッド関数を用いて、GUIwidgetとRS-HFIQのシリアルCAT通信とを結びつけています。注意点はBIT周波数文字列のフォーマット変換でしょうか。人が直読し易いように3桁区切りのセミコロンを入れていますが、RS-HFIQは受け付けません。

約100行と短いため、付録にコードを掲載します。

RS-HFIQ内蔵テスト機能用シリアル通信の実装

ダウンロードしたhardware_usbserial.pyでサポートされていたRS-HFIQとのインターフェースコマンドは以下のVFO制御に係わるコマンドだけでした。

Interface commands for VFO control with RS-HFIQ originally supported in hardware_usbserial.py.

RS-HFIQ内蔵テストに係わる以下のインターフェースコマンドを追加実装しました。

Added interface commands for RS-HFIQ built-in testing.

迷ったのはPLLシンセサイザ(SI5351A)の出力電流レベルです。2 mA、4 mA、6 mA、8 mAから選択するのですが、RS-HFIQのInterface Commandsマニュアルには「Generally a 4 ma drive seems like it works OK.」と記載されており、通常は4 mAで良いとされているようです。駆動先回路の容量に依存していると思うのですが、RS-HFIQでもQSD回路(74AC74、2並列)を駆動するVFO用途では4 mAが設定されています。内臓テスト用途ではLNA前段のBPFに入力する受信信号レベルとするために小さい方が妥当と考え、最小の2 mAとしました。GUIで切り換えられるようにすると良かったかもしれません。

付録に追加したコードを掲載します。

Quisk上でのRS-HFIQ内蔵テストの試行

試行結果

実装したRS-HFIQ内蔵テスト機能を40mバンドで評価しました。

Evaluation results of the implemented RS-HFIQ built-in test function on the 40m band.

LOを7.020MHzとして、7.021MHzのテスト信号をUSB側に印加しました。LSB側に奇数次のイメージが発生しています。目視で、テスト信号の強度は-20dB、イメージの強度は-62dBとなり、イメージ抑圧性能は42dBcでした。この結果は、先のHDSDRの結果と誤差の範囲で一致します。

気付き

RS-HFIQ内蔵テストパネルは並列プロセスとして起動するのが理想的ですが、Omni-RigのようにHardwareインスタンスの共有を簡単に行う方法が分からなかったため、Quiskプロセス内の関数呼び出しで起動しています。プロセスの制御はRS-HFIQ内蔵テストパネルのGUIイベントループに移るため、IQサンプリングは停止すると予想していましたが、完全には停止しませんでした。GUIイベントループの隙間でサンプリングを実行できているのかもしれません。ただし、Quiskのスペクトル画面の更新周期は不安定になるため、BITテストを設定したらRS-HFIQ内蔵テストパネルを逐一Quitした方が良いようです。

今後は、Image抑圧性能の定量的計測、自動IQバランス調整に進むべきですが、さらなるQuiskコードへのダイブが必要になるため逡巡しています。

付録 コードの実装例

趣味の範疇の実装例であり、動作の保証はありません。

付録 RS-HFIQ内蔵テスト制御パネルの実装例

import tkinter as tk

class control_panel():
    ## Constructor
    def __init__(self, Hardware):
        self.Hardware = Hardware    ## USB serial to RS-HFIQ

        self.DEBUG = 0
        TEST = 0
        if TEST:
            self.set_BIT_to_LO_1kHz_up()
            self.set_BIT_on()
        
        self.open_panel()   ## GUI and event loop until closing

    #
    #   Methods to control the RS-HFIQ built-in test hardware.
    #
    def set_BIT_to_LO(self):
        """set BIT to LO freq"""
        _, vfo_string = self.Hardware.ReturnFrequency()
        frequency = int(vfo_string)
        if frequency > 1024000 and frequency < 55000000:
            if self.DEBUG:
                print('set_BIT_to_LO()>', vfo_string, '->', frequency)
            self.insert_entry(frequency)
        else:
            print("BIT frequency out of range.")
 
    def set_BIT_to_LO_1kHz_down(self):
        """set BIT to LO freq -1 kHz"""
        _, vfo_string = self.Hardware.ReturnFrequency()
        frequency = int(vfo_string) - 1000
        if frequency > 1024000 and frequency < 55000000:
            if self.DEBUG:
                print('set_BIT_to_LO_1kHz_down()>', vfo_string, ' ->', frequency)
            self.insert_entry(frequency)
        else:
            print("BIT frequency out of range.")

    def set_BIT_to_LO_1kHz_up(self):
        """set BIT to LO freq +1 kHz"""
        _, vfo_string = self.Hardware.ReturnFrequency()
        frequency = int(vfo_string) + 1000
        if frequency > 1024000 and frequency < 55000000:
            if self.DEBUG:
                print('set_BIT_to_LO_1kHz_up()>', vfo_string, ' ->', frequency)
            self.insert_entry(frequency)
        else:
            print("BIT frequency out of range.")

    def set_BIT_on(self):
        f_string = self.etyFrequency.get()
        frequency = int(f_string.replace(',',''))
        if frequency > 1024000 and frequency < 55000000:
            if self.DEBUG:
                print('set_BIT_to_LO_1kHz_up()>', f_string, ' ->', frequency)
            self.frequency = frequency
            self.Hardware.set_BIT_frequency(frequency)
            self.Hardware.set_BIT_on()
    
    def set_BIT_off(self):
        self.Hardware.set_BIT_off()

    #
    #   RS-HFIQ control panel 
    #       using tkinter: the standard Python interface to the Tcl/Tk GUI toolkit.
    #
    def open_panel(self):
        self.panel = tk.Tk()
        self.panel.title("RS-HFIQ control panel")

        frmTop = tk.Frame(self.panel, bd=1) ## [Top]=[Left][Center][Right]
        tk.Label(frmTop, text="BIT Oscillator Control").pack(side=tk.TOP, padx=1, pady=1, anchor=tk.NW)
        ## Buttons to set BIT frequency
        frmLeft = tk.Frame(frmTop, bd=4)
        tk.Button(frmLeft, text="Set BIT to LO Freq +1 kHz", command=self.set_BIT_to_LO_1kHz_up  ).pack(side=tk.TOP, padx=1, pady=1, fill=tk.BOTH)
        tk.Button(frmLeft, text="Set BIT to LO",             command=self.set_BIT_to_LO          ).pack(side=tk.TOP, padx=1, pady=1, fill=tk.BOTH)
        tk.Button(frmLeft, text="Set BIT to LO Freq -1 kHz", command=self.set_BIT_to_LO_1kHz_down).pack(side=tk.TOP, padx=1, pady=1, fill=tk.BOTH)
        frmLeft.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)
        ## Entry of BIT frequency
        frmCenter = tk.Frame(frmTop, bd=4)
        tk.Label(frmCenter, text="BIT Frequency (Hz)").pack(side=tk.TOP, padx=1, pady=1, anchor=tk.NW)
        self.etyFrequency = tk.Entry(frmCenter, bg='yellow', font=("MSゴシック", "12", "bold"))
        self.etyFrequency.pack(side=tk.TOP, padx=1, pady=1)
        frmCenter.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)
        ## Buttons to activate BIT
        frmRight = tk.Frame(frmTop, bd=4)
        tk.Button(frmRight, text="BIT ON",  command=self.set_BIT_on  ).pack(side=tk.TOP, padx=1, pady=1, expand=True, fill=tk.BOTH)
        tk.Button(frmRight, text="BIT OFF", command=self.set_BIT_off ).pack(side=tk.TOP, padx=1, pady=1, expand=True, fill=tk.BOTH)
        frmRight.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)
        frmTop.pack(side=tk.TOP, expand=True, fill=tk.BOTH)
        ## Button to quit
        frmBottom = tk.Frame(self.panel, bd=1)
        tk.Button(frmBottom, text="Quit", command=self.close_panel).pack(side=tk.TOP, padx=1, pady=1, expand=True, fill=tk.BOTH)
        frmBottom.pack(side=tk.BOTTOM, expand=True, fill=tk.BOTH)

        self.set_BIT_to_LO()    ## set LO as an initial value.
        self.panel.mainloop()   ## wait a button event.
    
    def insert_entry(self, frequency):
        f_string = format( frequency, '08,')
        if self.DEBUG:
            print('insert_entry()> f_string =', f_string)
        self.etyFrequency.delete(0,tk.END)
        self.etyFrequency.insert(0, f_string)
        
    def close_panel(self):
        self.panel.destroy()
## End of class control_panel():

付録 RS-HFIQ内蔵テストに係わるHardwareクラス追加部分の実装例

class Hardware(BaseHardware):
    ## (既存コード省略)
    #=========================================================================
    # [Additional supported interface commands]
    # '*Bf\r'   : Sets the Built-in test (BIT) Generator to frequency f Hz.  
    # '*OB1\r'  : Sets the output level, 1 (2 mA drive), for the BIT frequency
    # '*OB0\r'  : Sets the output level, 0 (off), for the BIT frequency
    #=========================================================================
    def set_BIT_frequency( self, frequency ):
        """set Built-in Test frequency to RS-HFIQ
        [arg]
            frequency : integer 8-digit target frequency
        """
        rtn_flag = 0
        if serialport.isOpen():
            f_string = format( frequency, '08')
            command = "*B" + f_string + '\r'
            print("Set BFO to: ", f_string)
            serialport.write(command.encode())
            rtn_flag = 1
        else:
            print("Failed to set BFO of RS-HFIQ as serialport not open.")
        return rtn_flag

    def set_BIT_on(self):
        """BIT ON"""
        rtn_flag = 0
        if serialport.isOpen():
            #self.set_BIT_freq( self.bfo )
            command = '*OB1\r'
            serialport.write(command.encode())
            time.sleep(.25)
            serialport.flush()
            rtn_flag = 1
        else:
            print("Failed to turn on the BIT of RS-HFIQ as serialport not open.")
        return rtn_flag

    def set_BIT_off(self):
        """BIT OFF"""
        rtn_flag = 0
        if serialport.isOpen():
            command = '*OB0\r'
            serialport.write(command.encode())
            time.sleep(.25)
            serialport.flush()
            rtn_flag = 1
        else:
            print("Failed to turn off the BIT of RS-HFIQ as serialport not open.")
        return rtn_flag
## End of class Hardware(BaseHardware):