架構總覽
寫 Plugin 之前,先花五分鐘弄清楚三件事:主程式怎麼分層、你的 Plugin 站在哪裡、 你寫的每一段程式碼會跑在哪個執行緒上。第三件事是新手最常踩坑的地方。
分層
由上往下:
- Host:使用者看到的程式,終端機版與視窗版兩種。它們只負責「畫出來」與「收輸入」。
- host_common:兩個 Host 共用的部分——指令列怎麼解析、面板怎麼排版、設定檔怎麼讀寫。
- Core.dll:核心。有專門的 I2C Worker 執行緒(每支 dongle 一條),硬體存取由它執行
(背景輪詢等工作排進 Job Queue 由它依序處理,
ExecSync()則直接在它上面同步執行);你的 Plugin 也是由 Core 載入、由 Core 呼叫。 - nexus_hal.dll:硬體抽象層,把「讀一個暫存器」翻成各家轉接器的操作。
- dongles:每一支
.dll對應一種 USB 轉 I2C 硬體。mock是不需要硬體的模擬版。
Plugin 站在哪裡
Plugin 跟 Core 之間隔著兩張表,這就是 Plugin API 的全部:
IcPlugin:你填一張表交給 Core,表裡放名字、I2C 位址、以及一堆 callback (Core 在特定時機呼叫你的函式)。除了cleanup(必填)之外,每個 callback 都是選用的,不提供就填 0。NxHostApi(HostAPI):Core 交給你一張函式表,你要讀寫暫存器、寫 Log、 更新面板,都是呼叫這張表裡的函式。
你的 Plugin 看不到、也不該去碰 Host、host_common、HAL。 PDK 裡只有 sdk/ 的標頭檔,
沒有其他原始碼,這是刻意的:沒給的東西就沒辦法 #include,邊界就不會被不小心越過。
三種掛載模式
同一顆 Plugin DLL,依使用者怎麼掛載它,會落在三種模式之一:
| 模式 | 怎麼進入 | 有沒有面板 | 適合 |
|---|---|---|---|
| headless | 不需要使用者做什麼:由 Plugin 自己決定(它宣告「沒有面板」),plugin switch <名稱> 就是 headless,帶了 @p<N> 也會被忽略;批次模式不帶 @p<N> 時也是 headless | 沒有 | 只要自訂指令與背景輪詢 |
| widget | plugin switch <名稱> @p<N>:主程式會把那一格自動改成 Plugin Widget 面板 | 有:你宣告控制項,主程式排版 | 大多數 Plugin |
| canvas | plugin switch <名稱> @p<N>:那一格自動改成 Plugin Canvas 面板 | 有:你自己畫每一格 | 熱力圖、波形 |
@p<N> 是象限編號,@p0 是左上角那一格。要有面板的 Plugin(widget、canvas)一律帶 @p<N>:
互動模式不帶的話,它會掛在目前有焦點的面板上並把那一格改型;沒有任何面板有焦點時則報錯
plugin switch: no focused panel; pass @p<N>(後面會附上可用的 @p 清單)。本教學的 hello_ic 步驟 1~3 宣告沒有面板,所以是
headless;步驟 4 宣告了 widget 面板,要帶 @p0。Widget 與 Canvas 的差別見
Plugin Widget 與 Plugin Canvas。
三種模式收到的 callback 不完全一樣:例如 headless 沒有面板,所以收不到按鍵與 Widget 事件。完整對照表見生命週期與執行緒。
執行緒:哪一段程式跑在哪裡
Plugin 的 callback 會落在兩條執行緒之一:
| 執行緒 | 跑這些 callback | 能做什麼 |
|---|---|---|
| UI Thread(畫面與輸入) | init、on_key_press、on_layout_changed、設定的載入與收集等 | 不能讀寫 I2C,也不能睡或做任何耗時的事,否則畫面會卡住 |
| I2C Worker Thread | probe、poll_fn、自訂指令、on_widget_event、cleanup | 可以直接讀寫 I2C |
新手最容易記錯的兩點:
init在 UI Thread,cleanup卻在 Worker Thread。 兩者不對稱。- 讀寫硬體的
ExecSync()在 UI Thread 上呼叫會直接回傳錯誤ERR_WRONG_THREAD(-85), 而且不會碰硬體。 所以「想在init裡讀一下暫存器」是行不通的;要讀就放進自訂指令、probe或on_widget_event。步驟 3 會親手做這件事。
這樣設計是因為讀一次 I2C 可能要幾毫秒,甚至逾時等上更久。放在 UI Thread 上,使用者 的每一次打字、每一次捲動都會跟著卡。所以 Core 把所有硬體存取集中到 Worker 執行緒上執行, 你的 callback 落在哪條執行緒,就已經決定了你能做什麼。
完整的執行緒歸屬表在生命週期與執行緒,是所有 callback 的單一真相。
下一步
先準備編譯器。