Skip to Content
Plugin 開發生命週期與執行緒

生命週期與執行緒

前四步你已經寫出一顆會動的 Plugin。這一章把「它什麼時候被呼叫、在哪條執行緒上被呼叫」講清楚。 寫 Plugin 最常見的錯誤幾乎都出在這裡:把 I2C 讀寫放在錯的 callback,或是在錯的執行緒上碰了不該碰的東西。

一顆 Plugin 的一生

使用者打 p hello_ic @p3(見第一個 Plugin 1),這一次「掛載」從 DLL 被載入到被卸載, 依序經過下面這些階段:

順序階段在哪條執行緒做什麼
1NexusPluginGetInterface()UI Thread載入期,回傳你的 IcPlugin 結構。ABI 主版本不符就在這裡被拒絕,見版本號與 ABI 相容
2initUI Thread存下 ctx 與 host、宣告 widget 面板、寫一行 Log。不能碰硬體。回傳失敗或崩潰時,這顆 Plugin 進入失敗狀態(見下方),不會呼叫 cleanup
3on_config_loadUI Thread有存過設定才會有:依序把 config.ini 裡屬於你的每個 key=value 交給你,見設定持久化
4probeI2C Worker Thread第一次能碰硬體的地方。回 ERR_OK 才算掛載成功;探測失敗時,指令加了 force 會維持掛載並記一則警告,沒加則進入失敗狀態(見下方)
5存活期間見下表輪詢、自訂指令、widget 事件、按鍵、繪圖……反覆發生
6cleanupI2C Worker Thread卸載。強制實作,IcPlugin 裡只有 name 與 cleanup 兩樣不能省。只有 init 成功過才會呼叫

幾個容易誤會的地方:

  • cleanup 不在 UI Thread。 偵測到「這顆 Plugin 的工作已經全部做完」的是 Worker,而批次模式(見Batch/headless)根本沒有 UI 迴圈, 所以由 Worker 收尾。複製別人的程式碼時,別假設 cleanup 裡可以像 init 一樣做「我在 UI Thread」的事。 Core 會等這顆 Plugin 還在排隊的工作全部清空之後才呼叫 cleanup,返回後 ctx 就失效了。
  • 失敗的 Plugin 不會消失,也不會自動重試。 init 回傳失敗或崩潰、probe 回報失敗(沒帶 force)時,這顆 Plugin 的整組掛載進入失敗狀態並留在畫面上: 標題顯示失敗標記、已宣告的 widget 變灰、不再收到任何 callback,Core 管理的 GPIO/UART 立刻收回。 要結束失敗狀態,用 plugin restart(可加 @p<N> 指定面板、加 force 在沒有硬體時照樣掛載):它卸載後把同一顆 Plugin 掛回同一個槽位與面板,再走一次 init、probe;失敗的 Log 末尾會直接寫出該打的指令。想單純卸掉就 plugin unload。probe 失敗的 Plugin,在 Core 偵測到它所用的 dongle 離線、之後又重新連線時,會被自動重跑一次(輪詢關閉時不會);Core 只在傳輸失敗後才察覺離線,沒有任何東西在讀寫該 dongle 時拔插不一定被察覺,仍需手動 plugin restart。DLL 不會被卸掉,全域變數在重跑之間保留,init 不要假設自己是第一次執行。 init 回傳失敗前,請自己釋放已經配置的資源——init 從沒成功過,Core 不會呼叫 cleanup。 probe 失敗則相反:init 成功過,所以 cleanup 照常呼叫,只是延到使用者卸載當下,不是失敗當下。 手動掛載失敗的 Plugin 不會被記進設定檔;原本就寫在設定檔裡的,失敗後仍保留,下次啟動會再試一次。
  • probe 之前你什麼硬體都碰不到。 init 在 UI Thread 上,你在裡面呼叫 ExecSync 會得到 ERR_WRONG_THREAD(-85), 這是第三步就遇過的那個錯誤。

哪個 callback 在哪條執行緒

這張表是單一真相:寫任何 callback 之前,先確認自己落在哪一列。

callback執行緒一句話
initUI Thread載入期,同步回報成不成功
on_config_load/on_config_saveUI Thread讀寫 config.ini 裡你的那一段
on_key_pressUI Thread攔截按鍵(widget/canvas 面板)
on_mouse_eventUI Thread只有 Canvas 面板
on_layout_changedUI Thread面板尺寸變了
on_command_pre_executeUI Thread指令執行前竊聽與改寫
on_tab_activate/on_tab_deactivateUI Thread所在的分頁被切進來/切出去
ui_render_fnUI Thread只有 Canvas 模式,見Canvas 模式
probeI2C Worker Thread探測 IC
poll_fnI2C Worker Thread背景輪詢;poll off 時不會被呼叫
interrupt_handlerI2C Worker Thread中斷處理(廣播型)
on_tickI2C Worker Thread純軟體的定時脈動,poll off 時仍會被呼叫
自訂指令的 handlerI2C Worker Thread使用者打了你的指令
on_widget_eventI2C Worker Thread使用者操作了 widget。反應多半是讀寫 I2C,本來就不該在 UI Thread
on_watch_triggeredI2C Worker Thread監看條件成立(只送給那個監看項目所屬的掛載)
cleanupI2C Worker Thread卸載
on_uart_rx_ex那個 UART 埠自己的 Worker ThreadUART 收到資料
on_log_generatedCore 的 Log 廣播執行緒有新的 Log;非同步,不是產生那筆 Log 的執行緒

這裡的「I2C Worker Thread」指這次掛載綁定的那個後端的 Worker。接了多個 dongle 時每個後端有自己的 Worker, 不是全世界只有一條。

規則只有兩條,記這兩條就夠:

  1. 碰硬體(ExecSync)只能在 I2C Worker 那一列的 callback 裡。 在 UI Thread 那一列呼叫,一律回 ERR_WRONG_THREAD,而且完全不會去碰硬體。
  2. 要讓畫面更新,從 Worker 端呼叫 NotifyUiRefresh,不要直接畫。 Worker 上的 callback 沒有資格畫圖; Widget 模式下更簡單:你用 SetWidgetProp 改值,畫面自然會跟上,見Widget 模式深入。

不管在哪條執行緒,都要遵守的事

  • callback 裡的崩潰會被攔下。 每個 callback 都在例外隔離之內被呼叫,你的 callback 崩潰只會讓這顆 Plugin 進入失敗狀態(Tab 標題會顯示 (faulted)、widget 變灰、不再收到任何 callback),不會帶走主程式。要再用它,修好後 plugin restart 重新掛載(或先 plugin unload);Log 會寫出該打的指令,崩潰類失敗還會建議在一直失敗時重啟程式。 但 x86 建置沒有這層保護,見架構要對:x86/x64。
  • on_log_generated 會被多執行緒進入。 它在 Core 自己的廣播執行緒上非同步送達:呼叫時那筆 Log 早已寫入,而且可能比產生的時刻晚一點。 你的程式要假設它可能和別的 callback 同時在跑。在這個 callback 裡呼叫 LogWrite 會被直接丟棄(避免無限遞迴)。 init 成功返回之前不會收到它,cleanup 開始之後也不會收到。
  • on_uart_rx(沒有 _ex)已經永久停用,Core 不會呼叫它。要收 UART 資料請用 on_uart_rx_ex。

不是每個掛載都收得到每個 callback

同一顆 DLL,看使用者怎麼掛它,落在三種型態之一:

callbackheadlesswidgetcanvas
init/probe/cleanup收得到收得到收得到
poll_fn/on_tick/自訂指令收得到收得到收得到
on_tab_activate/on_tab_deactivate收得到收得到收得到
on_widget_event收不到收得到收不到
on_key_press收不到收得到收得到
on_layout_changed收不到收得到收得到
on_mouse_event收不到收不到收得到
ui_render_fn不呼叫不呼叫呼叫
on_command_pre_execute收不到收得到收得到

headless 沒有面板,所以一切「從畫面來的事件」(按鍵、widget 操作、繪圖)都到不了它,這是設計,不是缺陷: headless 的用途就是讓自訂指令與背景輪詢在不佔任何面板的情況下運作。 on_tab_activate/on_tab_deactivate 是「分頁存在」的通知,不算畫面事件,所以 headless 也收得到。 (這張表未列的 on_config_load/on_config_save/on_watch_triggered/on_log_generated/on_uart_rx_ex 三種型態都收得到。)

怎麼進入哪一種型態,見架構總覽。

四個常被誤解的行為

這四個 callback 有「有條件才會來」的細節。這些不是 bug,寫之前要知道:

on_key_press

  • 送的是 sdk\nexus_plugin_keys.h 裡的 NX_PKEY_* 鍵碼,不是 Win32 虛擬鍵碼。可列印字元不會經由它送達。
  • 只送給目前分頁的作用中 Plugin。
  • 視窗版裡,如果鍵盤焦點正在一個 widget(文字框、核取方塊、下拉選單、按鈕)上,按鍵由該 widget 自己吃掉,不會送到你。 所以 widget 面板要攔的鍵,要避開控件會吃的方向鍵、Tab、Space、Enter。終端機版沒有這個差異。
  • ESC 永遠不會送給 Plugin(那是使用者關閉浮動層、中止腳本的逃生口);Ctrl+C、Ctrl+A、Ctrl+F、Ctrl+R、Ctrl+U、Ctrl+H 這六個組合鍵是主程式保留的,也不送。
  • 對話框、選單、版面配置器、暫存器表格就地編輯開啟期間,兩個版本都不派發。

on_mouse_event

  • 只送給 Canvas 面板。 Widget 面板的互動走 on_widget_event;headless 沒有面板。
  • 座標是面板內容區裡的相對抽象格(左上角是 0,0),不是像素。標題列與框線上的點擊不屬於 Plugin。
  • 按下即捕獲:在內容區按下滑鼠之後,直到放開之前,DRAG 與 UP 一定送給同一個面板,就算游標已經移出面板。 這時座標可以是負數或超過面板寬高,用它當陣列索引之前先檢查範圍。
  • 沒有 hover。 沒按著任何鍵的滑鼠移動不會送達;也沒有雙擊與修飾鍵。DRAG 只在游標所在的格改變時才送。

on_layout_changed

  • 只在內容區尺寸真的有變時才送。 面板綁定後第一次一定送;之後主控台/視窗縮放、面板分割/合併/放大、切回這個分頁時尺寸與上次不同才送。 面板不再是 Plugin 面板時送一次 0,0。所以不要期待它每次縮放都響。
  • Widget 與 Canvas 面板都會送,headless 不送。
  • 單位是抽象格。你應該把它快取起來,Canvas 模式畫圖時用。

on_log_generated

前面提過:Core 的廣播執行緒上非同步送達,多執行緒進入,裡面呼叫 LogWrite 會被丟掉。 如果你太慢,Log 環形緩衝被蓋掉而還沒送到的幾筆會被略過(Core 記一則警告),不會拖住寫 Log 的那一方。

接下來