Learning Objectives
- 把一台儀器的 SCPI 指令包成類別,讓測試程式讀起來像測試步驟
- 用 __enter__/__exit__ 讓 with 區塊結束時一定關閉輸出
- 把資源字串與測試條件放進設定檔,換儀器、換條件不必改程式
- 讓所有通訊經過同一個地方並記錄下來,用介面相同的假儀器在沒有硬體時測試程式
程式長大之後
到目前為止的程式都是一條直線:開儀器、下指令、印結果。程式只有二十行時沒問題; 變成兩百行、有三台儀器、好幾個人一起改的時候,會出現這些狀況:
- 同一句
psu.write("APPL 5,0.03")散在十個地方,換一台指令不同的電源供應器要改十個地方。 - 資源字串寫死在程式裡,實驗室把電錶換到另一個 GPIB 位址,就得改程式。
- 程式出錯時,不知道最後送了哪一個指令。
- 沒有接儀器就完全不能測試程式。
這一課介紹測試程式常見的四個整理方式:
| 問題 | 做法 |
|---|---|
| 指令散落各處 | 儀器類別(驅動):一台儀器一個類別,SCPI 只出現在類別裡 |
| 出錯時輸出沒關 | __enter__/__exit__:用 with 保證收尾 |
| 條件寫死在程式裡 | 設定檔:資源字串、電壓、限值放在 JSON 檔 |
| 不知道發生什麼事、沒有硬體不能測 | 記錄所有通訊;介面相同的假儀器可以替換 |
逐步看:把電源供應器包成類別
PowerSupply 類別就是這台電源供應器的驅動(driver):SCPI 指令只出現在類別裡,
下面的測試程式只呼叫 set()、output()、current()。看執行時怎麼跳進類別的方法裡,又回到主程式。
分層:驅動、測試步驟、主程式
把程式分成幾層,每一層只做一件事:
主程式/測試流程 讀設定檔、決定量哪些點、判斷合格、寫報表
↓ 呼叫
儀器驅動(類別) PowerSupply.set()、DMM.voltage():把動作翻譯成 SCPI
↓ 呼叫
PyVISA 送出與接收字串
↓
介面與儀器 USB、GPIB、LAN
好處是改動只影響一層:換一台指令不同的電源供應器,只改驅動;改測試條件,只改設定檔;主程式不必動。 許多儀器廠商也提供自己的驅動程式(例如 IVI 驅動,或各廠的 Python 套件),觀念相同:把 SCPI 藏在方法後面。
下一個範例加上兩件事:設定檔,以及讓 with 保證收尾的 __enter__/__exit__。
逐步看:設定檔與 with
station.json 放資源字串、掃描電壓與限流。看最後一步:離開 with 區塊時,程式自動跳進 __exit__,送出 OUTP OFF。
就算 with 裡面出錯,__exit__ 也一樣會執行,效果和上一課的 try/finally 相同,但寫在類別裡,每次用都不會忘。
逐步看:所有通訊都經過同一個地方
Instrument 是所有儀器共用的基底類別,write() 與 query() 都會先記錄再送出。
因為每一筆通訊都經過這兩個方法,只要在這裡加一行,整個程式的通訊都有紀錄;出錯時打開記錄檔,就知道最後送了什麼。
記錄的等級與 logging 模組
剛才的記錄器用了三種等級,實務上常用:
| 等級 | 記什麼 |
|---|---|
| DEBUG | 每一筆通訊、中間值:除錯時才看 |
| INFO | 測試開始、結束、每個步驟的結果 |
| WARN(WARNING) | 不正常但還能繼續:例如進入定電流、讀值接近限值 |
| ERROR | 測試無法繼續的錯誤 |
Python 標準函式庫有 logging 模組,可以設定等級門檻、同時輸出到螢幕與檔案、自動加上時間;
在自己的電腦上寫測試程式,建議直接用它(logging.basicConfig(...)、logging.getLogger(...))。
本頁的執行環境不支援 logging,所以範例自己寫了一個最簡單的版本,觀念相同。
記錄要加上時間:真實的記錄器會在每一行前面加日期時間,事後才能和其他事件對照。本頁的範例省略了時間。
逐步看:同一個測試,換一個驅動
LabPSU(真的儀器)和 FakePSU(用公式算的假儀器)有相同的方法名稱:set_volts()、current()、off()。
led_test() 不在乎拿到的是哪一個,只要有這三個方法就能跑。看兩次呼叫 led_test() 時,跳進的是不同類別的方法。
為什麼要能替換驅動
「方法名稱一樣就能互換」在 Python 叫鴨子型別(duck typing):會呱呱叫、會走路,就當它是鴨子。 在測試程式裡,它帶來幾個實際的好處:
- 沒有硬體也能開發:儀器借不到、還在採購,先用假儀器把流程、報表、錯誤處理寫好。
- 自動測試測試程式:假儀器可以故意回傳超出限值的讀值、或丟出逾時錯誤,檢查判斷邏輯對不對。
- 換廠牌只換驅動:兩台不同廠牌的電源供應器各寫一個類別,方法名稱一樣,測試流程不必改。
常見的寫法是在設定檔加一個欄位決定用哪一個驅動:
DRIVERS = {"lab": LabPSU, "fake": FakePSU}
psu = DRIVERS[cfg["driver"]](...)
要讓「方法一樣」更有保障,可以用 abc 模組定義抽象基底類別(ABC),列出每個驅動都必須有的方法。
本頁其實就是一個大型的假儀器:你的程式送出的 SCPI,是由模擬的儀器回答的。
小測驗:類別與 with
下面的程式會印出什麼?
log = []
class Output:
def __enter__(self):
log.append("ON")
return self
def __exit__(self, *exc):
log.append("OFF")
try:
with Output():
log.append("量測")
raise TimeoutError
except TimeoutError:
log.append("逾時")
print(log)
實驗室把數位電錶從 GPIB 位址 22 改成 24。程式哪一種寫法最好改?
下面的程式會印出什麼?
import json
cfg = json.loads('{"volts": [3, 5], "limit_A": 0.03}')
print(len(cfg["volts"]), cfg["limit_A"] * 1000)
小測驗:替換驅動
下面的程式會印出什麼?
class FakeDMM:
def __init__(self, readings):
self.readings = list(readings)
def voltage(self):
return self.readings.pop(0)
def average(dmm, n):
return sum(dmm.voltage() for _ in range(n)) / n
d = FakeDMM([1.0, 2.0, 6.0, 9.0])
print(average(d, 3), len(d.readings))
假儀器(模擬驅動)最適合用來做什麼?
動手寫:一台假的電錶
寫一個 FakeDMM 類別:建立時給一串讀值;每呼叫一次 voltage() 就依序回傳下一個;
讀值用完了就丟出 RuntimeError("沒有讀值了")。下面的 average_voltage() 不用改,它不知道拿到的是假電錶。
重點整理
- 一台儀器一個類別(驅動):SCPI 只出現在類別裡,測試程式只呼叫有名字的方法。
__enter__/__exit__讓with區塊結束時一定收尾(關輸出、關連線),出錯也一樣。- 資源字串、測試條件放在設定檔(例如 JSON),換儀器、換條件不改程式。
- 所有通訊經過同一個基底類別,在那裡記錄;記錄分等級,真實的程式用
logging模組並加上時間。 - 方法名稱相同的驅動可以互換:假儀器讓你沒有硬體也能開發與測試程式。
下一課用這些積木做產線測試:每個產品一個序號、每個量測都有限值、判斷 Pass/Fail、產生報表。