Learning Objectives
- 寫出固定週期的輪詢迴圈,讀取 PLC 的資料並換算成工程單位
- 用 csv 模組把每一次讀取記錄下來,再讀回檔案做簡單的統計
- 設計有遲滯的告警:只在狀態改變時通知,並用線圈驅動警報
- 用 try/except 處理通訊錯誤,讓監控程式不會因為一次失敗就停止
讓電腦替你看著儀表
PLC 負責控制;但「過去一週水箱的液位怎麼變化」「半夜液位太高有沒有人知道」這些問題,PLC 自己不太會回答。 監控軟體(SCADA、HMI)做的事情,用幾十行 Python 就能做出最基本的版本:
- 輪詢:每隔固定時間讀一次 PLC 的資料。
- 記錄:把每一次讀到的值寫進檔案或資料庫。
- 告警:數值超過門檻時通知人。
這一課的 PLC 自己用階梯圖控制水箱:低於 40% 開泵浦、高於 90% 關泵浦。Python 只看、只記錄、只在必要時響警報, 不改變控制邏輯。這是監控程式的基本分工:控制留在 PLC,監控程式出問題時,設備仍照常運轉。
輪詢週期要多久
| 考量 | 說明 |
|---|---|
| 製程變化多快 | 液位、溫度變化慢,1~10 秒一次通常足夠;壓力突波要更快 |
| 設備的負擔 | 每一次請求都佔用 PLC 的通訊資源;很多 client 一起高頻輪詢,可能拖慢 PLC |
| 資料量 | 每秒一筆,一天就有 86400 筆;長期保存時常改成記錄平均值或只記錄變化 |
逐步看:每秒讀一次液位
看水箱的動畫與即時階梯圖:PLC 自己在控制泵浦,Python 每秒讀一次 IW0。time.sleep(1) 決定輪詢週期,time.monotonic() 用來計算經過的秒數。
記錄成 CSV
CSV(逗號分隔值)是最通用的記錄格式:試算表、pandas、資料庫都讀得懂。Python 內建的 csv 模組會處理逗號、引號這些細節:
import csv
with open("levels.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["seconds", "level"]) # 第一列放欄位名稱
writer.writerow([0, 50.1])
newline="":讓 csv 模組自己處理換行,避免在 Windows 上每列之間多出空白列。encoding="utf-8":欄位裡有中文時不會變成亂碼。- 讀回來用
csv.DictReader:每一列變成一個字典,欄位名稱當鍵;讀到的值都是字串,計算前要轉成數字。 - 長時間記錄時,每一列要有時間戳記;實務上記錄 UTC 時間或 ISO 8601 格式(例如
datetime.now(timezone.utc).isoformat()),顯示時再轉成當地時間。
逐步看:寫入 CSV,再讀回來統計
前半段一邊輪詢一邊寫入,後半段把檔案讀回來。注意讀回來的 r["level"] 是字串,要用 float() 轉換才能算最大值與平均。
告警:只在狀態改變時通知
最簡單的告警是「超過 85% 就印出警告」。但輪詢每秒一次,液位停在 86% 的那幾分鐘,就會連續收到幾百則一樣的警告, 真正重要的訊息反而被淹沒。好的告警要做到:
- 記住狀態:用一個變數
alarm記住目前是不是在告警中。 - 只在改變時通知:從「正常」變「告警」時通知一次,從「告警」變「正常」時再通知一次。
- 遲滯(死區):超過 85% 才告警,降到 80% 以下才解除。否則液位在 85% 上下抖動時,告警會一直開開關關(和第 7 課的泵浦控制同樣的道理)。
通訊錯誤也要處理:網路偶爾斷一下,read_input_registers 就會丟出例外。如果沒接住,整個監控程式就停了,
之後真的發生高液位也沒人知道。用 try/except 接住 ModbusException(pymodbus 各種錯誤的共同父類別),記下次數,下一輪再試;
連續失敗太多次時,「通訊中斷」本身也應該是一個告警。
逐步看:有遲滯的告警與警報喇叭
看 alarm 變數與畫面上的警報喇叭:液位超過 85% 時只印一次「警報開始」並打開喇叭(Q0.2);之後液位繼續變化也不再重複印,直到降到 80% 以下才解除。
小測驗:輪詢與記錄
每 0.5 秒讀一次、連續記錄一天(24 小時),會有多少筆資料?
下面的程式會印出什麼?
import csv, io
f = io.StringIO("seconds,level\n0,50.5\n1,62.0\n")
rows = list(csv.DictReader(f))
print(rows[1]["level"] + "%")
用 open(..., "w") 寫 CSV 時,為什麼要加 newline=""?
小測驗:告警
下面的告警邏輯會印出幾次「告警」?
alarm = False
for level in [84, 86, 85, 87, 82, 79, 86]:
if not alarm and level > 85:
alarm = True
print("告警")
elif alarm and level < 80:
alarm = False
print("結束")
告警為什麼要用兩個門檻(85% 告警、80% 解除)?
重點整理
- 監控程式的三件事:輪詢(固定週期讀取)、記錄(寫入 CSV 或資料庫)、告警(超過門檻通知)。
- 控制留在 PLC,監控程式只讀取、記錄、通知;監控程式出問題時,設備照常運轉。
- 輪詢週期依製程的變化速度與設備的負擔決定;
time.monotonic()適合計算經過的時間。 - CSV 用
csv模組寫,開檔加newline=""與encoding="utf-8";讀回來的值是字串,要轉換才能計算。 - 告警要記住狀態、只在改變時通知,並用兩個門檻避免抖動。
- 用
try/except ModbusException接住通訊錯誤並計數;持續失敗本身也是告警。 - 若記錄的資料涉及人員(例如誰在什麼時間操作),保存與使用要依單位的規定與研究倫理。
下一課換個角色:用 Python 寫一個 Modbus 從站,模擬一台設備,讓別的程式來讀你。