API 參考:NxHostApi
本頁內容與 PDK 內的
docs\NEXUS_TUI_PLUGIN_API_REFERENCE.md相同(依章節分頁,連結已改成站內連結)。
2.0 資料欄位
NxHostApi::abi_major
型別:NxDword
意義:Core 目前實作的 Plugin ABI 主版本(即 NX_PLUGIN_ABI_MAJOR),供 Plugin 判定自己編譯期認得的介面版面是否仍然適用。
合法值/哨兵:恆填 NX_PLUGIN_ABI_MAJOR。Host 填,Plugin 只讀——Plugin 不得修改本欄位的內容。
何時填:Host 在行程啟動時一次填好,IcPlugin::init 收到的 host 指標終生指向同一份唯讀資料。Host 填,Plugin 只讀。ABI 相容規則見 0.4 ABI 現況與版本規則。
NxHostApi::abi_minor
型別:NxDword
意義:Core 目前實作的 Plugin ABI 次版本。ABI 4.0 之後 minor 只會往上加,故只有當你的 Plugin 使用了比自己編譯期基線更晚的 minor 才新增的欄位時,才需要用 NX_ABI_HAS_FIELD 確認對方(可能較舊的)Host 是否有這個服務;判斷該欄位存不存在,實際依據永遠是 size_of_struct,不得直接比較 abi_minor 大小——本欄位只用來標示「Core 目前實作到第幾個 minor」,不是拿來被比較的判準。
合法值/哨兵:恆填 NX_PLUGIN_ABI_MINOR。Host 填,Plugin 只讀。
何時填:Host 在行程啟動時一次填好,與 abi_major 同時決定。Host 填,Plugin 只讀。ABI 相容規則見 0.4 ABI 現況與版本規則。
NxHostApi::size_of_struct
型別:NxDword
意義:NxHostApi 本身的位元組大小,是 Plugin 以 NX_ABI_HAS_FIELD(host->size_of_struct, NxHostApi, 欄位名) 判定「這個服務是否存在」的唯一依據。
合法值/哨兵:恆填 sizeof(NxHostApi)。Host 填,Plugin 只讀,不得修改或另行快取後失去同步。
何時填:Host 在行程啟動時一次填好,與 abi_major 同時決定。Host 填,Plugin 只讀。ABI 相容規則見 0.4 ABI 現況與版本規則。
2.1 硬體存取
NxHostApi::SubmitJob
簽章
NxLong (NX_CALL *SubmitJob)(NxPluginCtx ctx, const NxJobRequest *req);用途:非同步送出一筆硬體存取 Job,依 req->priority 入高/低優先級佇列後立即返回,不等待硬體回應;完成時經 req->on_complete(若非 NULL)回呼。
執行緒
- 正確執行緒:UI Thread 與 I2C Worker Thread 皆可呼叫。
- 在錯的執行緒呼叫會怎樣:不存在「呼叫錯執行緒」的情境——本函式只負責把請求排入佇列(短暫持有 Job Queue 內部鎖),真正的硬體存取發生在該 Job 目標後端自己的 Worker Thread 上,與呼叫本函式時所在的執行緒無關;因此本函式一律非阻塞。
- 如何確認:查 PLUGIN_GUIDE 第 4 章執行緒歸屬表;本函式「任一執行緒可呼叫、一律非阻塞」直接寫在
sdk/nexus_plugin_api.h對SubmitJob的@note。
使用時機:不需要在目前 callback 返回前拿到結果、或需要指定完成回呼時使用(典型是背景輪詢排程一次讀取、或 on_widget_event 送出「送出去就不管」的寫入)。需要在返回前拿到結果改用 NxHostApi::ExecSync。req 的欄位語意與 ExecSync 逐字相同,見 NxJobRequest::slave_addr、NxJobRequest::bank、NxJobRequest::bus_name。
最小範例
/* 放在 on_widget_event() 裡,回應一顆「非同步讀取」按鈕 */
NxJobRequest req;
NxLong rc;
memset(&req, 0, sizeof(req)); /* 全 0 = 沿用本掛載的位址、bank、後端 */
req.size_of_struct = (NxDword)sizeof(req);
req.op = NX_JOB_READ;
req.priority = NX_JOB_PRI_HIGH;
req.offset = 0x00;
req.length = 1;
rc = s_host->SubmitJob(ctx, &req);
if (rc != ERR_OK)
{
s_host->LogWrite(ctx, NX_LOG_WARN, "submit failed");
}常見錯誤與症狀
ERR_QUEUE_FULL:佇列已滿,立即拒絕,絕不阻塞;高優先級佇列深度建議上限 32。ERR_POLL_DISABLED:低優先級 Job 在poll off(或該後端被判定離線、背景輪詢暫停)狀態下被拒——高優先級 Job(使用者指令)不受影響。ERR_INVALID_PARAM:op不合法;NX_JOB_WRITE的範圍涵蓋卻不恰好等於bank_reg單一位元組;NONE掛載或表外位址帶了非0的bank;NX_JOB_RMW但length != 1。ERR_HW_OFFLINE:bus_name非空但找不到這個名稱(先於任何佇列判斷發生)。ERR_INVALID_CONTEXT:ctx無效,或反查本次呼叫自己所屬掛載/解析slave_addr對應掛載的過程本身失敗(不是「找不到」——找不到會落回表外分支並繼續,不是這個錯誤碼)。- 回呼從未被呼叫:忘記檢查
SubmitJob本身的回傳值——入列失敗時on_complete不會有任何一次被呼叫的機會。
NxHostApi::SetPollingInterval
簽章
NxLong (NX_CALL *SetPollingInterval)(NxPluginCtx ctx, NxDword interval_ms);用途:設定本 Plugin 掛載的背景輪詢週期,供 IcPlugin::poll_fn 依此節奏被呼叫。
執行緒
- 正確執行緒:無限制——本函式目前對
ctx/interval_ms皆不讀取,任一執行緒呼叫的結果都相同。 - 在錯的執行緒呼叫會怎樣:不適用——本函式目前恆為未完工的骨架,見下方常見錯誤。
- 如何確認:查 PLUGIN_GUIDE 第 4 章執行緒歸屬表。
使用時機:—(目前不要呼叫本函式:見下方常見錯誤。poll_fn 目前依 Core 內建的輪詢排程被呼叫,不受本函式影響;需要純軟體、不受 poll off 影響的節拍改用 IcPlugin::on_tick)。
最小範例
— 尚未提供可驗證的範例:本函式目前對任何輸入恆回 ERR_NOT_IMPLEMENTED,寫出「成功路徑」的範例會誤導讀者以為它現在能用。
常見錯誤與症狀
- 恆為
ERR_NOT_IMPLEMENTED(-5):本函式本身尚未接線,對任何輸入恆直接回傳這個錯誤碼,不改變任何狀態。這與標頭 Doxygen 的@return ERR_OK 或 ERR_INVALID_PARAM不符——實作是目前唯一的真相來源。 - 誤以為呼叫失敗代表
poll_fn完全不會被呼叫:不會——背景輪詢本身照常運作,poll_fn依該掛載的輪詢週期設定(config.ini或 Host 的掛載設定畫面,不同 Host 的預設值不必相同)被排程呼叫,poll off(或該後端被判定離線、背景輪詢暫停)時停止;該設定為0時本掛載完全不做背景輪詢(不是「盡快輪詢」)。本函式現在完全不影響這個既有節奏,SetPollingInterval()呼叫成功與否對它沒有作用。
NxHostApi::ExecSync
簽章
NxLong (NX_CALL *ExecSync)(NxPluginCtx ctx, const NxJobRequest *req,
NxByte *out, NxWord out_cap);用途:同步執行一筆硬體存取,不入佇列,直到硬體回應才返回。是 Plugin 同步存取硬體的唯一入口。
執行緒
- 正確執行緒:I2C Worker Thread 上的 callback(
probe/poll_fn/interrupt_handler/自訂指令 handler/on_widget_event/on_tick),或 Canvas 的 IcPlugin::ui_render_fn(render 執行緒:Core 轉交 I2C Worker 以高優先級執行,呼叫端最多等 5 秒);bus_name非空時(Worker callback)必須是那條具名匯流排自己的 Worker Thread。UI Thread 呼叫回ERR_WRONG_THREAD。 - 呼叫錯執行緒:回
ERR_WRONG_THREAD(-85),且不做任何硬體存取。 - 如何確認:查 PLUGIN_GUIDE 第 4 章執行緒歸屬表中你所在 callback 的那一列。
使用時機:需要在 callback 返回前就拿到結果時(典型是 IcPlugin::probe 判定 IC 在不在)。背景輪詢、「送出去之後再處理」改用 NxHostApi::SubmitJob;ui_render_fn 要顯示的暫存器值必須在 render 前用 NxHostApi::GetRegSnapshot 取快照複製到自己的緩衝區,ui_render_fn 本身只讀那份緩衝,不呼叫本函式。req 的欄位語意見 NxJobRequest::slave_addr、NxJobRequest::bank、NxJobRequest::bus_name。
最小範例
/* 放在 probe() 裡;s_host 是 init() 存下的 const NxHostApi * */
NxJobRequest req;
NxByte id_reg;
NxLong rc;
memset(&req, 0, sizeof(req)); /* 全 0 = 沿用本掛載的位址、bank、後端 */
req.size_of_struct = (NxDword)sizeof(req);
req.op = NX_JOB_READ;
req.offset = 0x00;
req.length = 1;
rc = s_host->ExecSync(ctx, &req, &id_reg, (NxWord)sizeof(id_reg));
if (rc != ERR_OK)
{
return rc; /* 讓掛載失敗,錯誤碼進 Log */
}常見錯誤與症狀
ERR_WRONG_THREAD(-85):寫在init()、on_config_load等 UI Thread callback 裡;或bus_name指到另一條匯流排。ERR_HW_OFFLINE(-40):dongle 未連線或後端開啟失敗;或bus_name指到一個不存在的後端名稱(名稱先解析,不會被誤報成ERR_WRONG_THREAD)。ERR_INVALID_PARAM(-2):沒有 bank register 的掛載或表外位址帶了非0的bank;寫入範圍涵蓋 bank register 卻不恰好是那一個位元組;op不是五種之一。ERR_BUFFER_TOO_SMALL:out_cap小於要讀回的長度。- 仍會存取到錯誤位址:
slave_addr填了自己slaves[]裡宣告的位址而不是0;或使用者用addr/mount改過位址後每一次都打錯——這兩種情況與位址寬度無關,ExecSync不會、也無法替呼叫端猜出「應該是哪一顆」。韌體給的 8-bit(write-address,含 R/W 位元)數值不必自己右移一位:ExecSync與 NxHostApi::SubmitJob 兩個入口一致,會把> 0x7F的slave_addr自動>> 1換算成 7-bit(0x90/0x91皆換成0x48)並記一筆NX_LOG_WARN(同一個掛載只記第一次),詳見 NxJobRequest::slave_addr——但仍建議 Plugin 作者自己先填 7-bit,這個換算是安全網,不是鼓勵這樣寫。 - 對已掛載的 8-bit 裝置回
ERR_INVALID_PARAM:NxJobRequest::offset> 0xFF時,ExecSync與SubmitJob兩個入口一致拒絕,不論位址是否屬於某個掛載——8-bit 定址下沒有無損的換算方式可猜,故是拒絕而不是像slave_addr那樣自動換算。
NxHostApi::ShouldAbort
簽章
NxBool (NX_CALL *ShouldAbort)(NxPluginCtx ctx);用途:查詢 Host 是否要求本 Plugin 儘快從目前 callback 返回;供分鐘級的長迴圈(如 ISP 燒錄)自行每一輪檢查一次。
執行緒
- 正確執行緒:任一執行緒皆可呼叫。
- 在錯的執行緒呼叫會怎樣:不適用——沒有執行緒限制;但不是無鎖:本版實測共五次短臨界區進出(
ctx→掛載身分換算、掛載狀態、關機旗標、掛載設定、該後端是否已被判定離線),全部無巢狀、彼此不新增取鎖順序;五次無爭用的臨界區進出合計數十奈秒等級,比它要守護的一筆 I2C 交易(100 kHz 下單位元組約 90 微秒)低三個數量級,故「分鐘級迴圈每一輪都查」不需要任何節流。 - 如何確認:查 PLUGIN_GUIDE 第 4 章執行緒歸屬表;成本細節的權威來源是
sdk/nexus_plugin_api.h對ShouldAbort的@warning。
使用時機:init()/on_config_load 等一次性 callback 以外,任何可能執行超過一瞬間的迴圈都應在每一輪查一次本函式並在非零時儘快返回——Host 不會替 Plugin 中斷任何東西。回傳非零的四個來源:本 Plugin 已被標記待移除;Host 已開始關閉流程;本掛載綁定的後端目前被判定離線、背景輪詢暫停中(尚未開啟的後端回 FALSE,不是 TRUE,避免在後端真正完成第一次惰性開啟之前就誤判離線);ctx 本身無效。
最小範例
/* 放在自訂指令 handler 裡,模擬一段分鐘級迴圈 */
int i;
for (i = 0; i < 1000000; ++i)
{
if (s_host->ShouldAbort(ctx))
{
return ERR_CANCELLED; /* 儘快從目前 callback 返回 */
}
/* [範本] 這裡放一輪耗時的工作,例如燒錄一個 flash page */
}
return ERR_OK;常見錯誤與症狀
- 從不呼叫本函式的分鐘級迴圈:關機時 Host 只會等該 I2C Worker Thread 一段固定的寬限期(約兩秒,全部匯流排共用一份預算),逾時即記一則 ERROR 並放棄該執行緒(不等它結束、不回收其資源),該執行緒此後的每一次存取都不再受任何互斥保護,讀寫的是正在被拆除中的內部狀態——這是「行程關不乾淨」從理論變成常態的成因。
- 把非零回傳值當成「可以忽略一次」:四個來源任一成立即代表 Host 已經決定要收回這個掛載,忽略只會延後而不會消除稍後的強制放棄。
- 誤以為本函式無鎖、可以在極熱的迴圈內無限制呼叫:它比一次 I2C 交易便宜三個數量級,但仍是五次真實的
CRITICAL_SECTION進出,不是常數時間的記憶體讀取。
2.2 Log
NxHostApi::LogWrite
簽章
void (NX_CALL *LogWrite)(NxPluginCtx ctx, NxDword level, const char *msg);用途:寫入一筆純文字 Log。
執行緒
- 正確執行緒:任一執行緒皆可呼叫(Core 內部自行上鎖)。
- 在錯的執行緒呼叫會怎樣:不適用——沒有執行緒限制;
ctx無效時本函式靜默不做任何事(回傳型別是void,沒有錯誤碼可回報)。 - 如何確認:查
sdk/nexus_plugin_api.h對LogWrite的@note(「可在任何執行緒呼叫」)。
使用時機:需要留下一筆給使用者看的診斷訊息時使用(例如 probe 失敗的細節、設定值不合法)。需要對訊息內特定字串上色時改用 NxHostApi::LogWriteStyled。on_log_generated 派發期間呼叫本函式會被直接丟棄(防止一筆 Log 觸發自己再產生新 Log 的無限遞迴)。
最小範例
/* 放在 probe() 裡,回報一次判定失敗的細節 */
s_host->LogWrite(ctx, NX_LOG_WARN, "id register mismatch");常見錯誤與症狀
level不在白名單(NX_LOG_IC/INFO/WARN/ERROR/DEBUG/TRACE)內:靜默改記NX_LOG_IC,不報錯、不拒絕——NX_LOG_READ/WRITE/POLL/SUCCESS保留給 Core 自己描述真實發生過的匯流排事件,Plugin 冒用只會讓使用者誤判硬體行為。- 訊息超過
NX_LOG_MSG_MAX(256)位元組:截斷並補...,不是錯誤。 - 自己在
msg裡手動加[<plugin_name>]前綴:前綴由 Core 加上,自己加會顯示成雙重前綴。 - 在
cleanup()裡呼叫:不是錯誤——掛載槽此時雖已被清空,Core 仍以卸載前的 Tab 與 Plugin 名稱照常記錄;這是cleanup()內唯二照常運作的以ctx反查掛載的 HostAPI(另一支是 NxHostApi::LogWriteStyled),見 IcPlugin::cleanup。
NxHostApi::LogWriteStyled
簽章
NxLong (NX_CALL *LogWriteStyled)(NxPluginCtx ctx, NxDword level, const char *msg,
const NxLogSpan *spans, NxDword span_count);用途:寫入一筆帶多區間語意著色的 Log;spans/span_count 為 NULL/0 時語意等同 NxHostApi::LogWrite(純文字)。
執行緒
- 正確執行緒:任一執行緒皆可呼叫(Core 內部自行上鎖,同
LogWrite)。 - 在錯的執行緒呼叫會怎樣:不適用——沒有執行緒限制。
- 如何確認:查
sdk/nexus_plugin_api.h對LogWriteStyled的@note。
使用時機:需要在訊息內特定字串套用前景/背景色或樣式(粗體、斜體、反白、閃爍)時使用,例如標出讀到的暫存器值。msg 必須為純 ASCII——spans[].start/len 的單位是位元組,Log 渲染把位元組數直接當畫面欄數;spans[].start 的位移基準是 Plugin 自己這份還沒有前綴的 msg,Core 會在存入前自動把每個 span 往後位移前綴長度,Plugin 不要自己預留前綴寬度。
最小範例
/* 放在 poll_fn() 裡,把讀到的數值標成強調色 */
static const char msg[] = "status = 0x42";
NxLogSpan spans[1];
memset(spans, 0, sizeof(spans));
spans[0].start = 9; /* "0x42" 在 msg 內的位元組位移 */
spans[0].len = 4;
spans[0].fg = NX_COLOR_ACCENT;
spans[0].bg = NX_COLOR_DEFAULT;
if (s_host->LogWriteStyled(ctx, NX_LOG_INFO, msg, spans, 1) != ERR_OK)
{
s_host->LogWrite(ctx, NX_LOG_WARN, "styled log rejected");
}常見錯誤與症狀
span_count > NX_LOG_SPAN_MAX(8)、任兩個 span 重疊、start非嚴格遞增、或陣列中混入len == 0的元素(不論位置):ERR_INVALID_PARAM,整筆不寫入,不做部分寫入或靜默修正——要少幾個 span 就把span_count縮小,不要留空槽。fg/bg用了 Core 保留值(OK/WARN/ERROR/INFO/CHANGED/DISABLED/BG_*等)或>= NX_COLOR_COUNT:不拒絕整筆,改記安全值(fg改NX_COLOR_ACCENT、bg改NX_COLOR_DEFAULT)並記一次 WARN。- 自己先把
spans[].start加上預估的前綴長度:前綴位移由 Core 內部處理,自己加會讓著色整段往右偏移。 - 在
on_log_generated派發期間呼叫:與LogWrite相同,直接丟棄。
2.3 UI 刷新
NxHostApi::NotifyUiRefresh
簽章
void (NX_CALL *NotifyUiRefresh)(NxPluginCtx ctx);用途:請求 Host 重繪;不攜帶任何呈現資料——Core 本身不產生顏色、字元格座標這類 Host 專屬的呈現細節。
執行緒
- 正確執行緒:任一執行緒皆可呼叫;這是 Worker Thread 唯一合法的「通知 UI」手段——Worker Thread 不得直接呼叫渲染函式,必須透過這個通知讓 UI Thread 自己決定何時重繪。
- 在錯的執行緒呼叫會怎樣:不適用——沒有執行緒限制。
ctx無效時靜默不做任何事。呼叫成本是一次短臨界區進出(在臨界區內抄出回呼指標與user_data,放鎖後才呼叫該回呼,避免 Host 的回呼回頭拉快照造成自我死鎖)。 - 如何確認:查
sdk/nexus_plugin_api.h對NotifyUiRefresh的@note。
使用時機:I2C Worker Thread 上的 callback(poll_fn/on_widget_event/interrupt_handler 等)更新完自己的狀態後,用來提示 Host「有東西變了,請自己拉快照重繪」。同一畫面更新週期內的多次呼叫由 Host 端自己的摺疊機制併為一次重繪請求——不需要自己節流呼叫次數。
最小範例
/* 放在 poll_fn() 裡,讀完狀態後提示 Host 重繪 */
s_host->NotifyUiRefresh(ctx);常見錯誤與症狀
- 期待本函式攜帶「哪個 Tab 變了」之類的資訊:不會——通知語意是「有東西變了,請自己拉快照」,本函式的回呼不帶
tab_id;即使如此,本函式仍會先驗證ctx的身分,失效的ctx不能叫動 Host 重畫。 - 在還沒有
init()保存ctx之前呼叫:ctx反查失敗,靜默不做任何事。 - 誤以為每次呼叫都會立即觸發一次實際重繪:實際重繪節奏由 Host 端摺疊機制決定,本函式只是提出請求。
2.4 Canvas 繪圖
NxHostApi::DrawText
簽章
NxLong (NX_CALL *DrawText)(NxPluginCtx ctx, NxWord x, NxWord y,
NxByte fg, NxByte bg, const char *text);用途:於面板相對座標繪製一段文字。只能在 IcPlugin::ui_render_fn 內呼叫(Context Guard)。
執行緒
- 正確執行緒:Core 的 render 執行緒,且必須在
ui_render_fn的呼叫堆疊內。 - 在錯的執行緒呼叫會怎樣:不在
ui_render_fn內(含在別的 UI Thread callback 內、或不同執行緒)呼叫一律回ERR_INVALID_CONTEXT——Context Guard 同時比對執行緒 ID 與ctx,兩者皆須與目前進行中的那一次 render 相符;ui_render_fn逾時下線後、或本來就是 Widget 模式(不會進入 render 週期)呼叫,同樣回ERR_INVALID_CONTEXT。 - 如何確認:查 PLUGIN_GUIDE 第 4 章執行緒歸屬表中
ui_render_fn那一列;ERR_INVALID_CONTEXT與「呼叫錯執行緒」用的ERR_WRONG_THREAD是刻意分開的兩種錯誤(sdk/nexus_errors.h對ERR_INVALID_CONTEXT的@note)。
使用時機:Canvas 模式(panel_mode == NX_PANEL_MODE_CANVAS)下逐段繪製文字時使用。ui_render_fn 內禁止 Sleep、malloc,與 UI callback 共用的狀態要自行加鎖(render 在獨立執行緒上並行執行);需要暫存器資料可在 render 前用 NxHostApi::GetRegSnapshot 取到自己的緩衝區,或在 render 內用 NxHostApi::ExecSync 同步讀取。座標超出面板寬度的部分由 Host 剪裁,本函式不做座標檢查。
最小範例
/* 放在 ui_render_fn() 裡 */
NxLong rc;
rc = s_host->DrawText(ctx, 0, 0, NX_COLOR_PRIMARY, NX_COLOR_DEFAULT, "status: ok");
if (rc != ERR_OK)
{
return; /* 不在合法的 render 呼叫堆疊內,放棄本次繪製 */
}常見錯誤與症狀
- 在
ui_render_fn以外呼叫:ERR_INVALID_CONTEXT。 text傳NULL:ERR_INVALID_PARAM(此檢查排在 Context Guard 判定之前,即使呼叫時機正確也一樣拒絕)。- 自己指定邊框字元或假設固定字元集:本函式只接受純文字內容,實際字元集由 Host 依主題決定。
<windows.h>的DrawText巨集把NxHostApi::DrawText這個欄位名也一併改寫:<windows.h>把DrawText定義成一個物件式巨集(展開為DrawTextA或DrawTextW),前置處理器不分辨「這是結構成員名」還是「這是函式呼叫」,凡是原始碼中出現的DrawText字元序列一律被取代——包含sdk/nexus_plugin_api.h內NxHostApi結構本身對這個欄位的宣告。若 Plugin 原始碼先#include <windows.h>、之後才#include "nexus_plugin_api.h":處理標頭時巨集已生效,結構欄位在本編譯單元裡被一併改名成DrawTextA;之後原始碼寫host->DrawText(...)同樣會被改寫成host->DrawTextA(...),兩邊改名一致,可以編譯。若順序相反(先#include "nexus_plugin_api.h",之後才#include <windows.h>):結構欄位在被處理當下巨集尚未生效,維持字面的DrawText;但<windows.h>之後,原始碼裡任何host->DrawText(...)都會被改寫成host->DrawTextA(...),與結構實際欄位名DrawText對不上,編譯失敗(找不到叫DrawTextA的成員)。正確做法:固定在#include "nexus_plugin_api.h"之前先#include <windows.h>(讓兩邊一起被改名,永遠一致),或在#include <windows.h>之後立刻#undef DrawText(DrawTextW版本同理,直接消掉巨集),兩者擇一即可,不要依賴「剛好沒踩到相反順序」這種巧合。
NxHostApi::DrawBox
簽章
NxLong (NX_CALL *DrawBox)(NxPluginCtx ctx, NxWord x, NxWord y,
NxWord w, NxWord h, NxByte fg, NxByte bg);用途:於面板相對座標繪製一個矩形框。只能在 ui_render_fn 內呼叫(Context Guard)。
執行緒
- 正確執行緒:Core 的 render 執行緒,且必須在
ui_render_fn的呼叫堆疊內。 - 在錯的執行緒呼叫會怎樣:同 NxHostApi::DrawText——回
ERR_INVALID_CONTEXT。 - 如何確認:同
DrawText。
使用時機:Canvas 模式下畫出面板內的分隔框、選取框等結構性邊框時使用。實際使用的邊框字元由 Host 依主題決定(TUI 三套字元集/GUI 為 GDI 線條)——Plugin 不指定字元,否則傳統 CP437 路徑會出現亂碼。
最小範例
/* 放在 ui_render_fn() 裡 */
NxLong rc;
rc = s_host->DrawBox(ctx, 0, 0, (NxWord)width, (NxWord)height,
NX_COLOR_PRIMARY, NX_COLOR_DEFAULT);
if (rc != ERR_OK)
{
return;
}常見錯誤與症狀
- 在
ui_render_fn以外呼叫:ERR_INVALID_CONTEXT。 - 自行指定框線字元(例如把它當成
DrawText的特例自己組 ASCII art):邊框樣式是 Host 的呈現決策,Plugin 只描述「這裡要一個框」。 w/h為0:不會報錯(本函式不做座標檢查),但畫出來的框沒有任何可見效果,容易誤判為呼叫失敗。
NxHostApi::DrawRegion
簽章
NxLong (NX_CALL *DrawRegion)(NxPluginCtx ctx, NxWord x, NxWord y,
NxWord w, NxWord h, const NxCanvasCell *cells);用途:一次提交整塊字元格區域(批次繪圖)。只能在 ui_render_fn 內呼叫(Context Guard)。
執行緒
- 正確執行緒:Core 的 render 執行緒,且必須在
ui_render_fn的呼叫堆疊內。 - 在錯的執行緒呼叫會怎樣:同
DrawText——回ERR_INVALID_CONTEXT。 - 如何確認:同
DrawText。
使用時機:需要一次畫較大面積時使用——逐格呼叫 DrawText 畫一個 40x10 的區域是 400 次跨模組呼叫,DrawRegion 把它壓成一次。cells 為 w * h 個字元格、逐列排列(row-major)。
最小範例
/* 放在 ui_render_fn() 裡;s_cells 是先前準備好的 static 陣列,非動態配置 */
NxLong rc;
rc = s_host->DrawRegion(ctx, 0, 0, (NxWord)width, (NxWord)height, s_cells);
if (rc != ERR_OK)
{
return;
}常見錯誤與症狀
- 在
ui_render_fn以外呼叫:ERR_INVALID_CONTEXT。 cells傳NULL,或w/h任一為0:ERR_INVALID_PARAM。- 呼叫返回後才繼續往
cells寫入資料:cells是借用指標,僅本次呼叫期間有效,Host 回呼必須在返回前用完它,Plugin 不得假設它會被保存。 - 在 render 路徑內用
malloc準備cells:渲染路徑禁止動態記憶體配置,cells應該是 render 前就已存在的static緩衝區。
2.5 暫存器快照
NxHostApi::GetRegSnapshot
簽章
NxLong (NX_CALL *GetRegSnapshot)(NxPluginCtx ctx, NxByte slave_index,
NxByte bank,
NxByte *buf, NxDword size,
NxByte *valid, NxDword valid_size);用途:取得指定 slave在指定 Bank 的暫存器快照,並一併取得 256-bit 有效遮罩;不觸發硬體存取,只回傳 Core 目前的資料模型。
執行緒
- 正確執行緒:任一執行緒皆可呼叫——本函式只讀取 Core 內部已存在的資料模型,不觸碰硬體。
- 在錯的執行緒呼叫會怎樣:不適用——沒有執行緒限制。但不得在 IcPlugin::ui_render_fn 內呼叫:本函式內部會取用 Core 的資料模型鎖,建議在 render 前(
poll_fn或SubmitJob的on_complete)呼叫,把結果複製到自己的靜態緩衝區(以你自己的鎖保護),ui_render_fn只讀那份緩衝,render 就不會因 Core 的鎖而拖慢。 - 如何確認:查 PLUGIN_GUIDE 第 4 章執行緒歸屬表與第 5 章「render 前取快照」一節;
sdk/nexus_plugin_api.h對ui_render_fn的@warning(建議在 render 前以GetRegSnapshot()取快照到自己的緩衝區)是這條建議的來源。
使用時機:Canvas 模式要在 ui_render_fn 顯示暫存器值時,於 render 前的 poll_fn(或 SubmitJob 的 on_complete)呼叫本函式,把結果複製進自己的 static 緩衝區供之後的 ui_render_fn 讀取;也適用於任何想在不觸發硬體存取的情況下讀取「Core 目前記得的值」的場景。slave_index 是 IcPlugin::slaves 的索引,0 ~ slave_count - 1 皆合法,每個索引各自對應自己的掛載與快照。快照與遮罩必須由同一次呼叫取得:拆成兩次呼叫會有非原子性的錯判——遮罩說「有效」但快照拿到的是更早一次讀取前的舊值。
最小範例
/* 放在 poll_fn() 裡,把快照複製進 ui_render_fn 稍後要讀的靜態緩衝區 */
NxByte snap[256];
NxByte valid[32];
NxLong rc;
rc = s_host->GetRegSnapshot(ctx, 0, 0, snap, (NxDword)sizeof(snap),
valid, (NxDword)sizeof(valid));
if (rc == ERR_OK)
{
memcpy(s_snap, snap, sizeof(s_snap)); /* s_snap/s_valid 是 static 緩衝 */
memcpy(s_valid, valid, sizeof(s_valid)); /* ui_render_fn 稍後只讀這兩個 */
}常見錯誤與症狀
buf容量< 256或valid容量< 32:ERR_BUFFER_TOO_SMALL,且不寫入任何位元組(容量檢查在寫入前完成)。slave_index >= slave_count:ERR_INVALID_PARAM——這是 Plugin 自己的程式錯誤,不是「找不到」。buf/valid任一為NULL:ERR_INVALID_PARAM。ctx無效:ERR_INVALID_CONTEXT。- 把
valid全零的位元組當成「讀到0x00」:0x00是最常見的合法值,valid遮罩沒設的位元代表「從未讀過」,不是「讀到了 0」——兩者的畫面呈現不應相同。
2.6 後端與能力查詢
NxHostApi::GetBackendName
簽章
NxLong (NX_CALL *GetBackendName)(NxPluginCtx ctx, char *buf, NxDword size);用途:取得本 mount 實際使用的 Dongle 後端名稱(使用者在 config.ini/mount 指令設定的字串;空字串視為沿用全域預設後端)。
執行緒
- 正確執行緒:任一執行緒皆可呼叫,包含
init/cleanup等可能出現在 UI Thread 或 I2C Worker Thread 兩側的 callback。 - 在錯的執行緒呼叫會怎樣:不適用——沒有執行緒限制;不依賴任何僅 Worker Thread 安全的查詢管道(空字串時查的是全域預設後端的既有快取值,任一執行緒皆可安全讀取)。
- 如何確認:查
sdk/nexus_plugin_api.h對GetBackendName的@note。
使用時機:需要知道「這次掛載的存取實際會打到哪一個後端」時使用,例如記 Log 時附上後端名稱。不得以本函式回傳的名稱字串判斷「是否支援 16-bit 定址」——能力一律以實際存取回傳的 ERR_NOT_SUPPORTED 為準;需要別名解析後的正式名稱改用 NxHostApi::GetResolvedBackendName。
最小範例
/* 放在 probe() 裡,記一筆帶後端名稱的 Log */
char name[NX_BACKEND_NAME_MAX];
char msg[64];
NxLong rc;
rc = s_host->GetBackendName(ctx, name, (NxDword)sizeof(name));
if (rc == ERR_OK)
{
sprintf(msg, "probing on backend: %s", name);
s_host->LogWrite(ctx, NX_LOG_INFO, msg);
}常見錯誤與症狀
buf/size不合法(NULL/0):ERR_INVALID_PARAM。ctx無效:ERR_INVALID_CONTEXT。buf容量小於名稱長度:ERR_BUFFER_TOO_SMALL。- 全域預設後端目前未上線:
ERR_HW_OFFLINE。 - 把回傳的裸名稱(例如使用者設的別名
mcp2221a)拿去比對「是不是某顆特定實體」:裸名稱不是解析後的正式名稱,要做這件事必須改用GetResolvedBackendName。
NxHostApi::GetHostCaps
簽章
NxDword (NX_CALL *GetHostCaps)(NxPluginCtx ctx);用途:查詢 Host 支援的能力位元遮罩(NX_HOSTCAP_* 的或)。
執行緒
- 正確執行緒:任一執行緒皆可呼叫——只讀取一個行程啟動時就決定、結束前才會變動的內部旗標,Plugin callback 執行期間必為穩定值。
- 在錯的執行緒呼叫會怎樣:不適用——沒有執行緒限制;
ctx無效時回傳0(無法解析 context 的 Plugin 不該得知任何能力資訊)。 - 如何確認:查 PLUGIN_GUIDE 第 4 章執行緒歸屬表;沒有任何 callback 因為呼叫本函式而受到執行緒限制。
使用時機:需要判定 Host 是否支援 Canvas 繪製、IMAGE/FILEPICK widget 的原生呈現之前查詢一次。目前定義的能力位元:NX_HOSTCAP_CANVAS(Canvas 模式繪製)、NX_HOSTCAP_IMAGE(IMAGE widget 真實點陣圖)、NX_HOSTCAP_FILEDIALOG(FILEPICK 原生對話框)。(0x2 曾是 3.x 的 Chart 側通道能力位,4.0 起不再使用也不重新分配;CHART 的樣本改走 NX_WP_SERIES。)本專案兩個官方 Host 皆宣告 NX_HOSTCAP_CANVAS;未宣告 NX_HOSTCAP_IMAGE/NX_HOSTCAP_FILEDIALOG 的 Host 分別退化為具名占位符與 TEXTBOX,不是錯誤。
最小範例
/* 放在 init() 裡,判定 Host 是否能畫 Canvas */
NxDword caps;
caps = s_host->GetHostCaps(ctx);
if ((caps & NX_HOSTCAP_CANVAS) != 0)
{
/* [範本] 這個 Host 支援 Canvas 繪製 */
}常見錯誤與症狀
ctx無效:回傳0(視同「什麼能力都沒有」),不是錯誤碼——本函式的回傳型別是NxDword位元遮罩,沒有錯誤碼通道。- 用字串或版本號推斷能力:一律以
GetHostCaps()的位元遮罩為準,不得用 Host 名稱或 ABI 版本猜測。 - 忘記位元測試用
&:caps == NX_HOSTCAP_IMAGE在有多個位元同時設定時會誤判為不支援。
NxHostApi::GetResolvedBackendName
簽章
NxLong (NX_CALL *GetResolvedBackendName)(NxPluginCtx ctx, char *buf,
NxDword size);用途:取得本掛載實際連到的後端正式名稱(已做別名解析)。
執行緒
- 正確執行緒:該掛載所綁定後端的 I2C Worker Thread——底層的別名解析查詢只能在 I2C Worker Thread 呼叫。
- 在錯的執行緒呼叫會怎樣:回
ERR_WRONG_THREAD;判定排在任何硬體相關查詢之前——UI Thread 不得做任何 I2C 讀寫。 - 如何確認:查 PLUGIN_GUIDE 第 4 章執行緒歸屬表中你所在 callback 的那一列;只有落在 I2C Worker Thread 上的 callback(
probe/poll_fn/interrupt_handler/自訂指令 handler/on_widget_event/on_tick等)才合法。
使用時機:需要記住「這一顆實體 Dongle 的某支 GPIO 接了什麼」之類、必須知道實際硬體身分而非使用者設定字串的場景時使用。與 NxHostApi::GetBackendName 的差別是解析:後者回傳使用者設定的原始字串(可能只是一個別名),本函式回傳別名解析後的正式名稱。能力判斷仍一律走 GpioGetCaps(),不得用本函式的字串推斷。
最小範例
/* 放在 probe() 裡,確認自己連到的實體後端正式名稱 */
char resolved[NX_BACKEND_NAME_MAX];
NxLong rc;
resolved[0] = '\0'; /* 失敗時本函式不一定會清空 buf,先自己清 */
rc = s_host->GetResolvedBackendName(ctx, resolved, (NxDword)sizeof(resolved));
if (rc != ERR_OK)
{
s_host->LogWrite(ctx, NX_LOG_WARN, "resolve backend name failed");
}常見錯誤與症狀
- 在 UI Thread callback(如
init/on_config_load)內呼叫:ERR_WRONG_THREAD。 - 名稱未註冊:
ERR_NOT_FOUND——buf不會被清空或改寫,內容維持呼叫前的原樣;不得在檢查rc之前就讀取buf,呼叫前應自行把buf初始化成空字串(見最小範例),不要依賴本函式失敗時會幫忙清空。 - 掛載沿用全域預設後端(
dongle_name為空字串)時,本函式改走與 NxHostApi::GetBackendName 空字串分支相同的查詢路徑:硬體離線或 Host 尚未初始化回ERR_HW_OFFLINE(此路徑會先把buf清成空字串再回傳)。 - 找名稱用的內部查表尚未就緒:
ERR_NOT_INITIALIZED,buf同樣不被觸碰。 buf容量不足:ERR_BUFFER_TOO_SMALL。- 把裸名稱與解析後名稱混用做身分比對:同一次掛載對
GetBackendName與GetResolvedBackendName呼叫,使用者設別名時兩者可能是不同的字串。
2.7 Widget
Widget 模式(PANEL_PLUGIN_WIDGET)自 ABI 4.0 起只有一條路:版面寫在 .nxlay 檔裡,Plugin 用 LoadLayout()(或 LoadLayoutData())載入,之後以 SetWidgetProp()/GetWidgetProp() 讀寫屬性,使用者操作經 IcPlugin::on_widget_event 送回。3.x 的 DeclarePanelWidgets/SetWidgetValue 等九支服務與 NxWidgetDecl 系列結構已整批移除(major 3 → 4),舊 Plugin 以 ERR_ABI_MISMATCH 拒載,須用新 PDK 重編。
六支服務是 NxHostApi 中連續的六個欄位,順序固定如下;欄位是否存在一律以 NX_ABI_HAS_FIELD(host->size_of_struct, NxHostApi, 欄位名) 判定(ABI 4.x 的 Host 一律具備):
| 服務 | 一句話 |
|---|---|
| LoadLayout | 以資源(或開發用檔案)取代整個面板的 widget 樹 |
| LoadLayoutData | 以記憶體內的 .nxlay 文字取代整個面板的 widget 樹 |
| CreateWidget | 執行期新增單一 widget |
| DestroyWidget | 移除單一 widget |
| SetWidgetProp | 寫一個屬性(值、標籤、選項、列資料、啟用/隱藏…) |
| GetWidgetProp | 讀一個屬性(Core 影子表的已承諾值) |
共通規則
ERR_OK的意思:已通過同步檢查並寫入 Core 的影子表,下一幀生效;不是「畫面已更新」。- 不會產生自己的事件:Plugin 呼叫
SetWidgetProp()不會觸發on_widget_event(該事件只描述使用者行為)。唯一例外:調小 TEXTBOX 的NX_WP_MAX_LENGTH使目前的值被截短時,送一次NX_WEVT_CHANGED,且走非同步路徑(見 IcPlugin::on_widget_event)。 - 文字欄位上限:
NX_WP_LABEL/NX_WP_AUX每段最多 31 位元組(NX_WLABEL_MAX_BYTES,不含 NUL);NX_WP_ITEMS的每項文字最多 255 位元組(NX_WDROPDOWN_ITEM_TEXT_MAX)、NX_WP_ROWS每列的名稱與值各最多 255 位元組(NX_WTABLE_ROW_TEXT_MAX),都不含 NUL。LABEL/AUX超過時SetWidgetProp()回ERR_FULL(容量不足,舊值保留);ITEMS/ROWS的單項文字超過 255 時回ERR_INVALID_PARAM(blob 欄位超界,舊值保留);.nxlay的label=/aux=超過 31 時是版面語法錯誤,LoadLayout()/LoadLayoutData()回ERR_INVALID_PARAM。多位元組字元要由 Plugin 自己在字元邊界切,Core 不替你截。 - 配額:單一面板最多
NX_MAX_WIDGETS_PER_PANEL(256)個 widget;影子表的文字配額為每面板 512 KiB、全體 4 MiB,用盡回ERR_FULL(容量不足,舊值保留)。 NxPropVal恆為 24 位元組(x86/x64 相同):呼叫端先填SizeOfStruct與kind;字串與 blob 在SetWidgetProp()返回時已複製,緩衝區可立刻重用。blob 記錄(NX_WP_ITEMS/NX_WP_ROWS等)每一筆的長度對齊到 4 位元組,即(記錄頭 + 本文 + 3) & ~3;補的位元組內容不拘。- 無效指標不是可回報的錯誤:
name、data、val、val->u.str.ptr等指標無效屬於未定義行為。有 SEH 的建置(clangmingwx64)會使該 Plugin 下線;x86clangmingw沒有 SEH,整個行程可能直接崩潰。 - 未知的
type=或屬性:版面檔含此 Host 不認得(或尚未實作)的type=時,LoadLayout()/LoadLayoutData()整份失敗並回ERR_NOT_SUPPORTED,不會載一半;其他語法或驗證問題回ERR_INVALID_PARAM。SetWidgetProp()對未知屬性回ERR_NOT_SUPPORTED。 - 執行緒:六支服務皆無執行緒限制,可在任何 Plugin callback 內呼叫(含
init與on_widget_event),也都不等 UI Thread。
NxHostApi::LoadLayout
簽章
NxLong (NX_CALL *LoadLayout)(NxPluginCtx ctx, NxByte panel_id,
const char *name, NxDword flags);用途:以版面檔取代指定面板的整棵 widget 樹,是 Widget 模式的起點——面板要顯示什麼由這份 .nxlay 一次決定,Host 據此排版與渲染。name 為 NULL 表示清空該面板。
執行緒
- 正確執行緒:任一 Plugin callback 所在的執行緒皆可呼叫。
- 在錯的執行緒呼叫會怎樣:不適用——沒有執行緒限制。
- 如何確認:查 PLUGIN_GUIDE 第 4 章執行緒歸屬表;慣例是在 IcPlugin::init(UI Thread)載入一次,之後不再重載。
使用時機:init() 載入面板 0 的版面。name 預設是嵌在 DLL 內的 RCDATA 資源名稱(ASCII [A-Za-z0-9_]、1 ~ 63 字元、不分大小寫);開發階段想改了就看效果、不想重編 DLL 時,flags 帶 NX_LAYOUT_F_FILE,name 改為相對於 Plugin DLL 所在目錄的檔案路徑(只讀一般檔案;路徑指到 NUL、COM1 這類裝置名一律回 ERR_NOT_FOUND)。保留位必須為 0。目前每個掛載只有一個邏輯面板,panel_id 傳 0。
內容與目前逐位元組相同(比的是解析後的規範化內容)時是 no-op,不重建、不打斷正在編輯的 TEXTBOX;內容不同時整棵樹重建,已存在的 widget 的值不保留(Plugin 要再 SetWidgetProp())、正在編輯的輸入框先提交、在途事件作廢。版面驗證失敗的原因(規則編號與 widget_id)寫進該 Plugin 的 Log(WARN),回傳碼是 ERR_INVALID_PARAM;佈局裡有 Host 不認得或尚未實作的 type= 時是 ERR_NOT_SUPPORTED。
最小範例
/* 放在 init() 裡;TEMPLATE_PANEL 由 .rc 的 RCDATA 嵌入 */
NxLong rc;
rc = s_host->LoadLayout(ctx, 0, "TEMPLATE_PANEL", 0);
if (rc != ERR_OK)
{
return rc;
}資源宣告(template_plugin.rc,路徑相對於 src 目錄,建置腳本把 .\src 放在資源編譯器的 include path 上):
TEMPLATE_PANEL RCDATA "../template_panel.nxlay"常見錯誤與症狀
ERR_NOT_FOUND:資源名稱打錯、.rc沒編進 DLL,或(NX_LAYOUT_F_FILE)檔案不存在。靜態登錄的 Plugin 沒有 DLL 資源,使用資源模式一律ERR_NOT_FOUND,請改用 LoadLayoutData。ERR_INVALID_PARAM:保留位非0、名稱不合法、.nxlay語法或驗證失敗(y未依列非遞減、同列x重疊、widget_id重複、GROUPBOX的h == 0、label=/aux=超過 31 位元組…)、版面格式版本比 Host 新。原因看 Plugin 的 Log。ERR_NOT_SUPPORTED:版面檔含此 Host 不認得或尚未實作的type=;整份載入失敗。ERR_FULL:widget 數超過 256,或影子表文字配額用盡。ERR_FAIL:只在NX_LAYOUT_F_FILE模式,檔案存在但讀取失敗(其他 I/O 錯誤)。ERR_NO_MEMORY:Core 配置失敗。ERR_SHUTTING_DOWN:Core 正在關閉。ERR_INVALID_CONTEXT:ctx無效。- 每幀重載:內容真的不同的載入會重建整棵樹;請載入一次,之後用
SetWidgetProp()更新。
NxHostApi::LoadLayoutData
簽章
NxLong (NX_CALL *LoadLayoutData)(NxPluginCtx ctx, NxByte panel_id,
const void *data, NxDword len, NxDword flags);用途:以記憶體內的 .nxlay 內容取代指定面板的整棵 widget 樹。給靜態登錄的 Plugin、版面在執行期組出來、或自己用其他方式取得版面文字的 Plugin 使用。
執行緒
- 正確執行緒:任一 Plugin callback 所在的執行緒皆可呼叫。
- 在錯的執行緒呼叫會怎樣:不適用——沒有執行緒限制。
- 如何確認:同 LoadLayout。
使用時機:data 是 UTF-8 的 .nxlay 文字(容忍 BOM 與 CRLF;不要求 NUL 結尾,內容裡出現 NUL 位元組視為語法錯誤;結尾沒有換行的最後一行照常解析)。len 為 1 ~ NX_LAYOUT_MAX_BYTES(131072)。flags 保留,必須為 0。Core 在呼叫內把 data[0 .. len) 複製一次,返回後緩衝區歸還呼叫端,Core 不保存指標。內容視為不受信任輸入,驗證規則與 LoadLayout() 完全相同;效果、no-op 判定與重建語意也相同。
最小範例
/* 靜態登錄的 Plugin:版面嵌在程式碼裡 */
static const char s_layout[] =
"[w.1]\n"
"type=VALUE\n"
"y=0\n"
"min_w=12\n"
"weight=1\n"
"h=1\n"
"label=Status\n";
NxLong rc;
rc = s_host->LoadLayoutData(ctx, 0, s_layout, (NxDword)(sizeof(s_layout) - 1), 0);
if (rc != ERR_OK)
{
return rc;
}常見錯誤與症狀
ERR_INVALID_PARAM:data為NULL、len為0或超過NX_LAYOUT_MAX_BYTES、flags非0、內容含 NUL、語法或驗證失敗、版面格式版本比 Host 新、label=/aux=超過 31 位元組。ERR_NOT_SUPPORTED:含此 Host 不認得或尚未實作的type=,整份載入失敗。ERR_FULL、ERR_NO_MEMORY、ERR_SHUTTING_DOWN、ERR_INVALID_CONTEXT:同 LoadLayout。本函式沒有ERR_NOT_FOUND,也沒有ERR_FAIL(沒有檔案可找、沒有 I/O 可失敗)。sizeof(s_layout)沒減 1:把結尾 NUL 算進len,內容含 NUL 位元組而被拒絕。data/len無效(指標不可讀、len大於實際緩衝區):未定義行為,後果見上方共通規則。
NxHostApi::CreateWidget
簽章
NxLong (NX_CALL *CreateWidget)(NxPluginCtx ctx, NxByte panel_id,
NxWord parent_id, NxWord type, NxWord widget_id);用途:在指定面板新增一個 widget,給版面檔無法事先決定的情況(例如依偵測到的通道數動態長出一排開關)。
執行緒
- 正確執行緒:任一 Plugin callback 所在的執行緒皆可呼叫。
- 在錯的執行緒呼叫會怎樣:不適用——沒有執行緒限制。
- 如何確認:同 LoadLayout。
使用時機:parent_id 目前只接受 NX_WIDGET_ROOT(影子表是扁平的一層;以 GROUPBOX 為 parent 的巢狀建立尚未支援)。type 為 NX_WTYPE_*,widget_id 在同一面板內唯一、範圍 0 ~ 0xFFFE(NX_WIDGET_ROOT 為 0xFFFF,保留給面板根)。新 widget 的屬性全為預設;版面屬性(NX_WP_Y/NX_WP_MIN_W/NX_WP_H…)、標籤與值接著用 SetWidgetProp 補,在同一個呼叫緒連續寫即可在同一幀生效。panel_id 必須是本 Plugin 自己的面板 0。面板尚未載入時隱含建立只有根的空面板。不使在途事件作廢。
最小範例
/* 在 init() 載入版面之後,再長出一個按鈕 */
NxPropVal v;
NxLong rc;
rc = s_host->CreateWidget(ctx, 0, NX_WIDGET_ROOT, (NxWord)NX_WTYPE_BUTTON, 20);
if (rc == ERR_OK)
{
memset(&v, 0, sizeof(v));
v.SizeOfStruct = (NxDword)sizeof(v);
v.kind = (NxWord)NX_WKIND_STR;
v.u.str.ptr = "Extra";
v.u.str.len = 5;
s_host->SetWidgetProp(ctx, 0, 20, NX_WP_LABEL, &v);
}常見錯誤與症狀
ERR_ALREADY_EXISTS:widget_id已被使用(含0xFFFF面板根)。ERR_NOT_FOUND:parent_id不存在。ERR_NOT_SUPPORTED:type尚未實作,或parent_id是既有的GROUPBOX(容器型,巢狀尚未支援)。ERR_INVALID_PARAM:parent_id不是容器、widget_id超界、panel_id不是0。ERR_FULL:超過每面板 256 個 widget,或面板池已滿(全行程共用)。- 忘了補版面屬性:新 widget 的
NX_WP_Y、NX_WP_MIN_W、NX_WP_H等為預設值,可能與既有列重疊或排在奇怪位置;版面能事先決定的請寫進.nxlay。
NxHostApi::DestroyWidget
簽章
NxLong (NX_CALL *DestroyWidget)(NxPluginCtx ctx, NxByte panel_id, NxWord widget_id);用途:移除指定面板的一個 widget 及其整棵子樹。
執行緒
- 正確執行緒:任一 Plugin callback 所在的執行緒皆可呼叫。
- 在錯的執行緒呼叫會怎樣:不適用——沒有執行緒限制。
- 如何確認:同 LoadLayout。
使用時機:移除用 CreateWidget 長出來的 widget,或版面檔裡不再需要的項目。widget_id 之後可能被重用成別的型別,所以該面板在途的 widget 事件一律作廢,不會送到 on_widget_event。widget_id 不得為 NX_WIDGET_ROOT;要清空整個面板用 LoadLayout(ctx, panel_id, NULL, 0)。
最小範例
NxLong rc;
rc = s_host->DestroyWidget(ctx, 0, 20);
if (rc != ERR_OK && rc != ERR_NOT_FOUND)
{
s_host->LogWrite(ctx, NX_LOG_WARN, "DestroyWidget failed");
}常見錯誤與症狀
ERR_NOT_FOUND:面板或 widget 不存在(重複移除第二次即是)。ERR_INVALID_PARAM:widget_id是NX_WIDGET_ROOT,或panel_id無效。- 預期還會收到移除前排入的點擊:不會,在途事件作廢是設計。
NxHostApi::SetWidgetProp
簽章
NxLong (NX_CALL *SetWidgetProp)(NxPluginCtx ctx, NxByte panel_id, NxWord widget_id,
NxDword prop, const NxPropVal *val);用途:設定指定面板中某個 widget(或面板根 NX_WIDGET_ROOT)的一個屬性。取代 3.x 的 SetWidgetValue/SetWidgetVisible/SetDropdownItems/SetValueListRows/SetChartSamples/SetFilePath 等一整批專用服務。
執行緒
- 正確執行緒:任一 Plugin callback 所在的執行緒皆可呼叫,含
on_widget_event。 - 在錯的執行緒呼叫會怎樣:不適用——沒有執行緒限制。
- 如何確認:同 LoadLayout。
使用時機:Plugin 需要把自己算出來或量到的結果反映到畫面上時(例如把 poll 到的計數寫進一個 VALUE)。不會回頭觸發自己的 IcPlugin::on_widget_event,可放心在收到硬體資料後呼叫,不必擔心遞迴。
常用屬性(完整編號見 sdk/nexus_widget_ids.h):
| 屬性 | kind | 用途 |
|---|---|---|
NX_WP_VALUE | 依型別:I64(CHECKBOX/DROPDOWN/CHOICE/SPIN/METER/SLIDER)、STR(TEXTBOX/VALUE/FILEPICK) | widget 的值。TEXTBOX/VALUE 的字串最長 NX_WTEXTBOX_MAX_LENGTH(32767)個 Unicode 碼點,超過的部分在碼點邊界截斷(回 ERR_OK,不會切在多位元組字元中間;不是合法 UTF-8 的位元組串,截斷只把非延續位元組算一個碼點) |
NX_WP_MAX_LENGTH | I64 | TEXTBOX 最多幾個 Unicode 碼點:1 ~ NX_WTEXTBOX_MAX_LENGTH(32767,也是預設值),超界回 ERR_INVALID_PARAM(不夾)。設值時超過上限的部分在碼點邊界截斷(ERR_OK);使用者打字與貼上到上限即停;調小時現有的值立即截斷,並送一次 NX_WEVT_CHANGED(只在值真的變短時,非同步,見 IcPlugin::on_widget_event) |
NX_WP_LABEL/NX_WP_AUX | STR | 標籤;IMAGE 檔名/FILEPICK 篩選。每段最多 31 位元組(ITEMS/ROWS 的文字最多 255 位元組,見上) |
NX_WP_ENABLED/NX_WP_HIDDEN | I64 | 啟用/隱藏(隱藏用 NX_WP_HIDDEN,不要用凍結屬性表裡的 NX_WP_VISIBLE(0x0006,不開放給 Plugin)) |
NX_WP_ITEMS | BLOB | DROPDOWN/CHOICE 的選項(NxItemHead 記錄) |
NX_WP_ROWS | BLOB | TABLE 的資料列(NxRowHead 記錄) |
NX_WP_SERIES | BLOB | CHART 的一條資料線(NxSeriesHead 加 16 位元小端樣本,最多 256 個) |
NX_WP_NUM_MIN/NUM_MAX/NUM_STEP | I64 | SPIN/SLIDER/METER 範圍 |
NX_WP_USER_DATA | U64 | 不透明 cookie,事件原樣帶回 |
NX_WP_Y/X/MIN_W/MAX_W/WEIGHT/H/X_GUIDE | I64 | 版面屬性(與 .nxlay 同名鍵) |
NX_WP_GAP/NX_WP_GUIDES/NX_WP_TEXT_INSET | I64/BLOB/I64 | 面板級屬性,設在 NX_WIDGET_ROOT 上 |
最小範例
/* 把數字寫進 VALUE widget 1 */
NxPropVal v;
NxLong rc;
memset(&v, 0, sizeof(v));
v.SizeOfStruct = (NxDword)sizeof(v);
v.kind = (NxWord)NX_WKIND_STR;
v.u.str.ptr = "42";
v.u.str.len = 2;
rc = s_host->SetWidgetProp(ctx, 0, 1, NX_WP_VALUE, &v);常見錯誤與症狀
ERR_FULL:容量不足,舊值保留。成因三種:blob 超過容量上限;NX_WP_LABEL/NX_WP_AUX超過 31 位元組;或影子表文字配額(每面板 512 KiB、全體 4 MiB)用盡(ITEMS每項實際占4 + 文字長 + 1位元組、ROWS每列占4 + 名稱長 + 1 + 值長 + 1 + 1位元組,所以 255 位元組的選項在一個面板約放 2016 項、255+255 的列約放 1014 列);選項超過上限(DROPDOWN4096、CHOICE8)同為ERR_FULL。選項另受套件的 ITEMS arena 限制:整個 Host 行程共用一份 1 MiB(NXW_DROPDOWN_ARENA_MAX,所有面板、所有 Plugin 合計),每項占24 + 文字長度位元組;255 位元組的選項每項 279 位元組,全體約放 3758 項,所以兩個面板各放 2016 項會超過,第二個回ERR_FULL(舊值保留)。不是ERR_INVALID_PARAM。ERR_INVALID_PARAM:val為NULL、SizeOfStruct太小、kind與屬性不符、值超界(含ITEMS/ROWS的單項文字超過 255 位元組)、屬性不適用該型別,或寫入唯讀屬性(NX_WP_TYPE)。ERR_NOT_SUPPORTED:未知屬性,或NX_WP_VISIBLE(0x0006,改用NX_WP_HIDDEN)。ERR_NOT_FOUND:面板或 widget 不存在。- 掛載故障後 Core 把該掛載所有 widget 強制為停用;之後寫入
NX_WP_ENABLED照常回ERR_OK但不生效。 - blob 長度沒補到 4 位元組:每筆記錄 = 記錄頭+本文,總長補到
(head + body + 3) & ~3。例:NxItemHead(12 位元組)加 5 位元組文字是 17,補成 20;NxRowHead(8 位元組)加名稱 3、值 4 位元組是 15,補成 16。 - 調小
NX_WP_MAX_LENGTH預期同步收到事件:截短造成的NX_WEVT_CHANGED在SetWidgetProp()返回之後才非同步交付。 - 傳入無效的
val或其ptr:未定義行為(見共通規則)。
NxHostApi::GetWidgetProp
簽章
NxLong (NX_CALL *GetWidgetProp)(NxPluginCtx ctx, NxByte panel_id, NxWord widget_id,
NxDword prop, NxPropVal *out, void *buf,
NxDword buf_cap, NxDword *needed);用途:讀取指定面板中某個 widget 的一個屬性(Core 影子表的已承諾值)。
執行緒
- 正確執行緒:任何執行緒。
- 在錯的執行緒呼叫會怎樣:不適用——沒有執行緒限制。
- 如何確認:同 LoadLayout。
使用時機:讀回使用者最後提交的值(例如 Worker 輪詢時看 CHECKBOX 目前的狀態),或讀回自己先前寫入的值。可讀的屬性:NX_WP_TYPE、NX_WP_USER_DATA、NX_WP_ENABLED、NX_WP_HIDDEN、NX_WP_VALUE、NX_WP_NUM_MIN/NUM_MAX/NUM_STEP、NX_WP_LABEL;其餘屬性是寫入專用,回 ERR_NOT_SUPPORTED。讀到的是 Plugin 最後寫入的值與使用者最後提交的值中較新者;TEXTBOX 編輯中尚未提交的字不可見。SetWidgetProp() 返回後立刻讀得到新值。
字串型屬性先問大小:以 buf == NULL 且 buf_cap == 0 呼叫,回 ERR_BUFFER_TOO_SMALL 並在 *needed 填入所需位元組數(含 NUL);數值型屬性不需要緩衝,照常回 ERR_OK。
最小範例
/* 讀 CHECKBOX(widget 3)目前是否勾選 */
NxPropVal out;
NxLong rc;
memset(&out, 0, sizeof(out));
out.SizeOfStruct = (NxDword)sizeof(out);
rc = s_host->GetWidgetProp(ctx, 0, 3, NX_WP_VALUE, &out, NULL, 0, NULL);
if (rc == ERR_OK && out.kind == NX_WKIND_I64 && out.u.i64 != 0)
{
/* 已勾選 */
}常見錯誤與症狀
ERR_BUFFER_TOO_SMALL:容量不足,*needed已填,out與buf不動——依*needed重新配置後再呼叫。ERR_INVALID_PARAM:out為NULL、out->SizeOfStruct太小,或buf為NULL卻buf_cap > 0。ERR_NOT_SUPPORTED:寫入專用的屬性。ERR_NOT_FOUND:面板或 widget 不存在。- 讀到
kind == NX_WKIND_NONE且flags帶NX_WPV_F_UNKNOWN:尚未被寫入過、也沒有預設的值,回ERR_OK;不要當成0。
2.8 Tab
NxHostApi::AddTab
簽章
NxLong (NX_CALL *AddTab)(NxPluginCtx ctx, NxByte *out_tab_id);用途:於執行期新增一個屬於本 Plugin 的 Tab。
執行緒
- 正確執行緒:任一執行緒皆可呼叫。
- 在錯的執行緒呼叫會怎樣:不適用——沒有執行緒限制。
- 如何確認:
sdk/nexus_plugin_api.h對AddTab的@note明文「可在任何執行緒呼叫」。
使用時機:需要在原本固定的 4 個 Tab 之外,動態開一個新 Tab 顯示額外內容時。新增的 Tab 名稱由 Host 依目前掛載狀態自動填入,不由 Plugin 指定;新增的 Tab 不持久化,重新啟動後不會自動復原——需要的話應在 IcPlugin::init 內重建。
最小範例
/* 放在自訂指令 handler 裡 */
NxByte new_tab;
NxLong rc;
rc = s_host->AddTab(ctx, &new_tab);
if (rc != ERR_OK)
{
s_host->LogWrite(ctx, NX_LOG_WARN, "AddTab failed");
}常見錯誤與症狀
out_tab_id為NULL:ERR_INVALID_PARAM。- 已達 Tab 數量上限:
ERR_FULL,Host 已記一次 WARN;不得重試,上限是硬性的,重試只會反覆製造 WARN。 ctx無效:ERR_INVALID_CONTEXT。- 誤以為新 Tab 會在重啟後自動回來:不會,只有固定的 4 個 Tab 會持久化,動態新增的 Tab 需要 Plugin 自己在
init()重建。
NxHostApi::RemoveTab
簽章
NxLong (NX_CALL *RemoveTab)(NxPluginCtx ctx, NxByte tab_id);用途:移除一個先前以 NxHostApi::AddTab 新增的 Tab。
執行緒
- 正確執行緒:任一執行緒皆可呼叫。
- 在錯的執行緒呼叫會怎樣:不適用——沒有執行緒限制。
- 如何確認:
sdk/nexus_plugin_api.h對RemoveTab的@note明文「可在任何執行緒呼叫」。
使用時機:動態新增的 Tab 不再需要時清理它;固定 Tab 一律拒絕移除。
最小範例
/* 放在自訂指令 handler 裡 */
NxByte tab_id;
NxLong rc;
tab_id = 5; /* 範例值:應為先前 AddTab() 回傳的 out_tab_id */
rc = s_host->RemoveTab(ctx, tab_id);
if (rc != ERR_OK)
{
s_host->LogWrite(ctx, NX_LOG_WARN, "RemoveTab failed");
}常見錯誤與症狀
tab_id超出範圍:ERR_INVALID_PARAM。tab_id未配置(未曾成功AddTab()過):ERR_NOT_FOUND。tab_id指到固定 Tab(Tab1~Tab4):ERR_NOT_SUPPORTED。- 該 Tab 上仍掛著 Plugin(任一掛載槽未卸載):
ERR_ALREADY_IN_USE——先卸載該 Tab 上的全部掛載再重試;本函式不會自動卸載,因為卸載需等待 Job 佇列清空,自動串接會讓呼叫在佇列很深時像當掉。 ctx無效:ERR_INVALID_CONTEXT。- 誤以為只有自己新增的 Tab 才能移除:本函式不檢查「這是不是本 Plugin 新增的」——任何 Plugin 傳對
tab_id都能移除別人新增的動態 Tab(只要上面沒有仍掛著的 Plugin)。
2.9 UART
NxHostApi::Uart_Open
簽章
NxLong (NX_CALL *Uart_Open)(NxPluginCtx ctx, const char *port, NxDword baud,
NxByte databits, NxByte parity, NxByte stopbits,
NxByte flow, char *err, NxWord err_cap);用途:開啟本 Plugin 掛載專屬的 UART 埠。這是本掛載私有的埠,與該掛載所在面板的 UART_LOG 面板完全獨立——兩者互不可見、互不干擾,即使剛好開的是同一個實體 COM 埠名稱。
執行緒
- 正確執行緒:任一 Plugin callback 所在的執行緒皆可呼叫。
- 在錯的執行緒呼叫會怎樣:不適用——沒有執行緒限制,但會阻塞呼叫端執行緒至多 1.5 秒(內部一律先做一次「先關再開」,等待舊 UART Worker Thread 結束)。在 UI Thread 上執行的 callback(IcPlugin::init、
on_key_press、IcPlugin::on_command_pre_execute、on_tab_activate、IcPlugin::on_config_load、IcPlugin::on_config_save)呼叫本函式會讓整個 UI 卡住到這個上限,時間敏感的呼叫端應改在 IcPlugin::on_tick/poll_fn/自訂指令 handler(I2C Worker Thread)中呼叫。 - 如何確認:查
sdk/nexus_plugin_api.h對Uart_Open的@warning。
使用時機:Plugin 需要一條自己專屬、與面板 UART_LOG 互不干擾的 UART 通道時(例如與 IC 的除錯埠溝通)。同一掛載重複呼叫視為「先關再開」,不會洩漏上一次的埠槽。
最小範例
/* 放在自訂指令 handler 裡(I2C Worker Thread),避免卡住 UI */
char err[64];
NxLong rc;
err[0] = '\0'; /* ctx 無效時 err 不會被寫入,先清空避免印出未初始化內容 */
rc = s_host->Uart_Open(ctx, "COM5", 115200, 8,
NX_UART_PARITY_NONE, NX_UART_STOP_1,
NX_UART_FLOW_NONE, err, (NxWord)sizeof(err));
if (rc != ERR_OK)
{
s_host->LogWrite(ctx, NX_LOG_WARN, err);
}常見錯誤與症狀
port為NULL,或格式不是COM1~COM255:ERR_INVALID_PARAM。baud/databits/parity/stopbits/flow有不合法值(例如databits不在5~8):ERR_INVALID_PARAM。- 埠名已被別的面板/掛載開啟:
ERR_ALREADY_IN_USE。 - 全域埠槽池已滿:
ERR_FULL。 - 內部「先關再開」的關閉步驟未在時限內完成:
ERR_TIMEOUT,可稍後重試。 ctx無效:ERR_INVALID_CONTEXT——這條路徑不會寫入err,呼叫端若沒有事先清空err[0]就直接拿去印,印出的是未初始化的堆疊內容。- 誤以為開的是面板
UART_LOG的埠:不是,兩者完全獨立、互不可見,即使開的是同一個實體 COM 埠名稱,後開的一方仍會依既有的埠獨佔規則被拒。
NxHostApi::Uart_Close
簽章
NxLong (NX_CALL *Uart_Close)(NxPluginCtx ctx);用途:關閉本 Plugin 掛載專屬的 UART 埠。
執行緒
- 正確執行緒:任一 Plugin callback 執行緒皆可呼叫,含本埠自己的 UART Worker Thread 以外的任何執行緒——pull 模型下沒有任何 Plugin callback 會跑在該 Worker Thread 上,故呼叫本函式不會自己等自己。
- 在錯的執行緒呼叫會怎樣:不適用——沒有執行緒限制,但全程同步,會等待該埠的 UART Worker Thread 結束,上限與 NxHostApi::Uart_Open 相同(1.5 秒);在 UI Thread 上執行的 callback 中呼叫會有與
Uart_Open相同的卡住風險。 - 如何確認:查
sdk/nexus_plugin_api.h對Uart_Close的@warning。
使用時機:不再需要私有 UART 埠時主動釋放。Uart_Open() 內部本來就會先做一次同樣的關閉步驟,故多數情況不必顯式呼叫本函式。
最小範例
/* 放在自訂指令 handler 裡 */
NxLong rc;
rc = s_host->Uart_Close(ctx);
if (rc != ERR_OK)
{
s_host->LogWrite(ctx, NX_LOG_WARN, "Uart_Close failed");
}常見錯誤與症狀
ctx無效:ERR_INVALID_CONTEXT。- 本來就沒開:
ERR_OK(冪等,不是錯誤)。 - 該埠的 UART Worker Thread 未在時限內結束:
ERR_TIMEOUT——與 NxHostApi::Uart_Open 內部「先關再開」步驟同一種逾時;此時埠槽不會被釋放,資源留給行程收尾處理,可稍後重試。 - 在 UI Thread 上執行的 callback 中呼叫:不會因此多回一種錯誤碼,但會讓整個 UI 卡住至多 1.5 秒——這是效能陷阱,不是功能缺陷。
NxHostApi::Uart_Read
簽章
NxLong (NX_CALL *Uart_Read)(NxPluginCtx ctx, NxByte *buf, NxDword cap,
NxDword *out_got);用途:消費本 Plugin 掛載私有埠目前已收到、尚未讀取的原始位元組。Pull 模型:Core 不會主動把 RX 資料推給 Plugin。
執行緒
- 正確執行緒:任一 Plugin callback 所在的執行緒皆可呼叫。
- 在錯的執行緒呼叫會怎樣:不適用——沒有執行緒限制。
- 如何確認:查 PLUGIN_GUIDE 第 4 章執行緒歸屬表;
sdk/nexus_plugin_api.h對本函式未設任何執行緒限制。
使用時機:在 IcPlugin::on_tick/poll_fn/自訂指令 handler 等既有週期性 callback 中輪詢,取得 UART RX 資料。不做行切分——收到什麼位元組就吐出什麼位元組,可安全承載任意二進位協定。
最小範例
/* 放在 on_tick() 裡 */
NxByte buf[64];
NxDword got;
NxLong rc;
rc = s_host->Uart_Read(ctx, buf, (NxDword)sizeof(buf), &got);
if (rc == ERR_OK && got > 0)
{
/* [範本] buf[0..got) 是收到的原始位元組,逐位元組原樣,未做行切分 */
}常見錯誤與症狀
buf或out_got為NULL:ERR_INVALID_PARAM。- 本掛載目前沒有已開啟的私有埠:
ERR_NOT_FOUND。 ctx無效:ERR_INVALID_CONTEXT。- 無資料可讀:
out_got為0,回傳仍是ERR_OK,不是錯誤。 - 讀取跟不上裝置輸出速率:環狀緩衝區容量有限,最舊的未讀位元組會被靜默覆寫,不當機、不報錯。
NxHostApi::Uart_Write
簽章
NxLong (NX_CALL *Uart_Write)(NxPluginCtx ctx, const NxByte *buf, NxDword len);用途:送出資料到本 Plugin 掛載專屬的 UART 埠。不附加任何換行字元——與面板 UART_LOG 的巨集/輸入框不同,逐位元組原樣送出。
執行緒
- 正確執行緒:任一 Plugin callback 所在的執行緒皆可呼叫。
- 在錯的執行緒呼叫會怎樣:不適用——沒有執行緒限制。
- 如何確認:查 PLUGIN_GUIDE 第 4 章執行緒歸屬表;
sdk/nexus_plugin_api.h對本函式未設任何執行緒限制。
使用時機:需要主動送出資料到私有 UART 埠時。內部會把 buf 依序切成每段至多 256 位元組的區塊分別送入佇列;任一區塊送入失敗(佇列已滿或埠已離線)就立即返回,不阻塞,但此時前面的區塊可能已經成功入列。當 len 超過 256 時,失敗後不得用相同的 buf/len 整段盲目重試——那會讓已經入列的前段位元組在線路上重複出現一次;需要「要嘛全部送出、要嘛完全不送」的呼叫端應把每次呼叫的 len 控制在 256 位元組以內,一段失敗只重送那一段。
最小範例
/* 放在自訂指令 handler 裡 */
static const NxByte s_cmd[] = { 0x01, 0x02 };
NxLong rc;
rc = s_host->Uart_Write(ctx, s_cmd, (NxDword)sizeof(s_cmd));
if (rc != ERR_OK)
{
s_host->LogWrite(ctx, NX_LOG_WARN, "Uart_Write failed");
}常見錯誤與症狀
buf為NULL或len為0:ERR_INVALID_PARAM。- 埠未開啟或已離線:
ERR_HW_OFFLINE。 - TX 佇列已滿:
ERR_QUEUE_FULL——立即返回,不阻塞。 ctx無效:ERR_INVALID_CONTEXT。- 誤以為會自動附加換行字元:不會,與面板
UART_LOG的巨集/輸入框不同,本函式逐位元組原樣送出。 len超過 256 時失敗了一半:buf內部是逐段送入佇列的,某個區塊失敗時它之前的區塊多半已經入列成功——回傳的錯誤碼不代表「整批什麼都沒送」,也沒有任何欄位回報實際送出了多少;用相同引數整段重試會讓前段位元組在線路上重複一次。
NxHostApi::Uart_SetParams
簽章
NxLong (NX_CALL *Uart_SetParams)(NxPluginCtx ctx, NxDword baud, NxByte databits,
NxByte parity, NxByte stopbits, NxByte flow);用途:變更本 Plugin 掛載私有埠目前的通訊參數,埠本身不重新開啟。
執行緒
- 正確執行緒:任一 Plugin callback 所在的執行緒皆可呼叫。
- 在錯的執行緒呼叫會怎樣:不適用——沒有執行緒限制,但回傳語意依執行緒而異:在掛載所屬匯流排的 Worker Thread 上,函式會等新參數實際套用完才返回(有上限,期間可能被埠上一次卡住的傳輸拖到數秒);在 UI Thread(
init、on_key_press等)及其他非 Worker 執行緒上不等待,貼出請求即回ERR_OK,這只代表「已受理」,不代表已生效,套用結果(成功或失敗)由 Host 寫一筆 Log。需要確知結果時請在 Worker 執行緒上呼叫(自訂指令 handler、on_widget_event等)。 - 如何確認:查 PLUGIN_GUIDE 第 4 章執行緒歸屬表;
sdk/nexus_plugin_api.h對本函式未設任何執行緒限制。
使用時機:連線後才需要調整通訊參數的場景,例如 IC 進入某個模式後改變了通訊鮑率。
最小範例
/* 放在自訂指令 handler 裡 */
NxLong rc;
rc = s_host->Uart_SetParams(ctx, 9600, 8, NX_UART_PARITY_NONE,
NX_UART_STOP_1, NX_UART_FLOW_NONE);
if (rc != ERR_OK)
{
s_host->LogWrite(ctx, NX_LOG_WARN, "Uart_SetParams failed");
}常見錯誤與症狀
- 尚未
Uart_Open():ERR_NOT_FOUND。 baud/databits/parity/stopbits/flow有不合法值:ERR_INVALID_PARAM。ctx無效:ERR_INVALID_CONTEXT。- 目前使用的後端不支援執行期變更通訊參數:
ERR_NOT_SUPPORTED。 - 埠目前不是開啟狀態(例如驗證參數之後、實際套用之前的極短空窗期被關閉或重開,或正在關閉/已離線):
ERR_HW_OFFLINE。 - 在 Worker Thread 上等了約 3.5 秒仍未被套用(埠的 Worker 卡在一次傳輸逾時裡):
ERR_TIMEOUT;請求若尚未被取走已撤回,已被取走則結果不明,之後以埠的實際行為為準。 - 在 UI Thread 上收到
ERR_OK就假設已生效:UI Thread 上不等待,ERR_OK只代表已受理;失敗只出現在 Log。 - 底層驅動實際套用參數時失敗:直接回傳驅動自己的錯誤碼,不是固定的某一個值——不得假設只會是上面列出的幾種。
2.10 GPIO
NxHostApi::GpioGetCaps
簽章
NxLong (NX_CALL *GpioGetCaps)(NxPluginCtx ctx, NxGpioCaps *out_caps);用途:查詢本掛載所綁後端的 GPIO 能力。回報的是硬體能力,不是這塊板子的接線——「GP0 接了 reset」是 Plugin 自己的知識,經 on_config_load/on_config_save 持久化,不在 NxGpioCaps 裡。
執行緒
- 正確執行緒:本掛載所綁後端自己的 I2C Worker Thread 上的 callback(
probe/poll_fn/interrupt_handler/自訂指令 handler/IcPlugin::on_widget_event/IcPlugin::on_tick;on_watch_triggered契約上也在這條執行緒,但目前 Core 不呼叫它,見 IcPlugin::on_watch_triggered)。 - 在錯的執行緒呼叫會怎樣:回
ERR_WRONG_THREAD,且不做任何硬體存取;判定在任何硬體動作之前完成。 - 如何確認:查 PLUGIN_GUIDE 第 4 章執行緒歸屬表中你所在 callback 的那一列;
init()在 UI Thread 上,故 GPIO 能力探測必須寫在probe(),不能寫在init()(sdk/nexus_plugin_api.h對GpioGetCaps的@warning)。
使用時機:宣告腳位前先探測後端是否具備 GPIO 能力、以及哪些腳位可用;不具備時應優雅降級(例如不建立依賴 GPIO 的 widget),不使整個掛載失敗。
最小範例
/* 放在 probe() 裡 */
NxGpioCaps caps;
NxLong rc;
memset(&caps, 0, sizeof(caps));
caps.size_of_struct = (NxDword)sizeof(caps);
rc = s_host->GpioGetCaps(ctx, &caps);
if (rc == ERR_OK && (caps.gpio_pin_mask & 0x01) != 0)
{
/* [範本] pin 0 可用,稍後可呼叫 GpioConfigure() */
}常見錯誤與症狀
- 在 UI Thread 上執行的 callback(如
init())中呼叫:ERR_WRONG_THREAD。 out_caps為NULL:ERR_INVALID_PARAM。- 後端未宣告 GPIO 能力:
ERR_NOT_SUPPORTED——這不是失敗,是「這顆後端沒有 GPIO」的正常回報,Plugin 應據此優雅降級。 - 硬體離線:
ERR_HW_OFFLINE。 ctx無效:ERR_INVALID_CONTEXT。- 把「這塊板子的 GP0 接了 reset」誤以為是本函式該回報的內容:不是,接線是 Plugin 自己的知識,不在
NxGpioCaps裡。
NxHostApi::GpioConfigure
簽章
NxLong (NX_CALL *GpioConfigure)(NxPluginCtx ctx, NxByte pin, NxByte func,
NxByte init_value);用途:宣告一支腳位並設定其功能。先宣告者持有,規則與 Plugin 命名衝突(先靜態、後 DLL、先註冊者優先)同一立場。
執行緒
- 正確執行緒:與 NxHostApi::GpioGetCaps 相同——本掛載所綁後端自己的 I2C Worker Thread 上的 callback。
- 在錯的執行緒呼叫會怎樣:回
ERR_WRONG_THREAD,且不做任何硬體存取。 - 如何確認:
sdk/nexus_plugin_api.h對GpioConfigure的@warning「執行緒限制同GpioGetCaps()」。
使用時機:確認 NxHostApi::GpioGetCaps 回報的能力涵蓋目標腳位後,宣告要把它用來做什麼(輸出、輸入、或中斷偵測)。
最小範例
/* 放在 probe() 裡,緊接著 GpioGetCaps() 成功之後 */
NxLong rc;
rc = s_host->GpioConfigure(ctx, 0, NX_PIN_FUNC_GPIO_OUT, 0);
if (rc != ERR_OK)
{
s_host->LogWrite(ctx, NX_LOG_WARN, "GpioConfigure failed");
}常見錯誤與症狀
- 在錯的執行緒呼叫:
ERR_WRONG_THREAD。 - 該腳位已被其他掛載持有:
ERR_ALREADY_IN_USE。 func為NX_PIN_FUNC_ADC/NX_PIN_FUNC_DAC(保留號段,本版不支援),或不是任何已定義的NX_PIN_FUNC_*值:ERR_NOT_SUPPORTED——不是ERR_INVALID_PARAM,「這個功能還沒做」與「參數本身不合法」是兩個不同的碼。pin超出腳位總數上限,或(依func是GPIO_IN/GPIO_OUT還是INT)不在後端宣告的gpio_pin_mask/int_pin_mask範圍內:ERR_INVALID_PARAM。- 硬體離線:
ERR_HW_OFFLINE。 ctx無效:ERR_INVALID_CONTEXT。- 同一掛載重複宣告同一支腳:視為改設功能,不算衝突,不是錯誤。
- 宣告
NX_PIN_FUNC_INT卻誤以為自己是唯一收到中斷的掛載:中斷成立時會廣播給本匯流排上所有實作了interrupt_handler的掛載,不限於宣告者——宣告只表示「設定權」(誰決定邊緣、誰負責武裝)。
NxHostApi::GpioWrite
簽章
NxLong (NX_CALL *GpioWrite)(NxPluginCtx ctx, NxByte pin, NxByte value);用途:寫入自己持有的輸出腳。
執行緒
- 正確執行緒:與 NxHostApi::GpioGetCaps 相同。
- 在錯的執行緒呼叫會怎樣:回
ERR_WRONG_THREAD,且不做任何硬體存取。 - 如何確認:
sdk/nexus_plugin_api.h對GpioWrite的@warning「執行緒限制同GpioGetCaps()」。
使用時機:已用 NxHostApi::GpioConfigure 宣告為 NX_PIN_FUNC_GPIO_OUT 的腳位,需要改變輸出電位時。
最小範例
/* 放在 on_widget_event() 裡,收到 BUTTON 觸發後翻轉一支輸出腳 */
NxLong rc;
rc = s_host->GpioWrite(ctx, 0, (NxByte)1);
if (rc != ERR_OK)
{
s_host->LogWrite(ctx, NX_LOG_WARN, "GpioWrite failed");
}常見錯誤與症狀
- 在錯的執行緒呼叫:
ERR_WRONG_THREAD。 - 未持有該腳,或該腳不是
NX_PIN_FUNC_GPIO_OUT(例如還沒GpioConfigure(),或被宣告成別的功能):ERR_INVALID_PARAM。 - 硬體離線:
ERR_HW_OFFLINE。 ctx無效:ERR_INVALID_CONTEXT。
NxHostApi::GpioRead
簽章
NxLong (NX_CALL *GpioRead)(NxPluginCtx ctx, NxByte pin, NxByte *out_value);用途:讀取自己持有的腳位電位。
執行緒
- 正確執行緒:與 NxHostApi::GpioGetCaps 相同。
- 在錯的執行緒呼叫會怎樣:回
ERR_WRONG_THREAD,且不做任何硬體存取。 - 如何確認:
sdk/nexus_plugin_api.h對GpioRead的@warning「執行緒限制同GpioGetCaps()」。
使用時機:讀回 GPIO 輸入腳(或輸出腳的即時電位);NX_PIN_FUNC_INT 腳的中斷邊緣需另外由 interrupt_handler 得知,本函式讀到的只是即時電位。
最小範例
/* 放在 poll_fn() 裡 */
NxByte value;
NxLong rc;
rc = s_host->GpioRead(ctx, 1, &value);
if (rc == ERR_OK)
{
/* [範本] value 非零表示高電位 */
}常見錯誤與症狀
- 在錯的執行緒呼叫:
ERR_WRONG_THREAD。 - 未持有該腳(未
GpioConfigure(),或被別的掛載持有):ERR_INVALID_PARAM。 - 硬體離線:
ERR_HW_OFFLINE。 ctx無效:ERR_INVALID_CONTEXT。out_value傳NULL:不會被擋下也不會回錯誤碼——內部一律先讀進自己的區域變數,只有在out_value非NULL時才寫回去;成功時的讀值會被靜默丟棄,呼叫端拿不到任何結果也看不到任何症狀,故務必自行保證out_value有效。
NxHostApi::GpioRelease
簽章
NxLong (NX_CALL *GpioRelease)(NxPluginCtx ctx, NxByte pin);用途:釋放自己持有的腳位,還原為高阻抗輸入。
執行緒
- 正確執行緒:與 NxHostApi::GpioGetCaps 相同。
- 在錯的執行緒呼叫會怎樣:回
ERR_WRONG_THREAD。 - 如何確認:
sdk/nexus_plugin_api.h對GpioRelease的@warning「執行緒限制同GpioGetCaps()」。
使用時機:Plugin 主動提前釋放不再需要的腳位。多數情況不必呼叫——IcPlugin::cleanup 之後 Core 會強制回收全部持有中的腳位。
最小範例
/* 放在自訂指令 handler 裡,提前歸還一支不再需要的腳 */
NxLong rc;
rc = s_host->GpioRelease(ctx, 0);
if (rc != ERR_OK)
{
s_host->LogWrite(ctx, NX_LOG_WARN, "GpioRelease failed");
}常見錯誤與症狀
- 在錯的執行緒呼叫:
ERR_WRONG_THREAD。 ctx無效:ERR_INVALID_CONTEXT。- 硬體離線:
ERR_HW_OFFLINE——這個情況不會清除內部持有記錄,因為判定發生在查找持有記錄之前;離線恢復後仍可再次呼叫本函式正常釋放。 - 本來就沒持有:
ERR_OK(冪等,不是錯誤)。 - 釋放一支你沒有持有的腳位(不論那支腳位目前空著、還是被另一個 Plugin 持有):
ERR_OK(冪等空操作,不動任何東西)——GpioRelease()只釋放呼叫端自己持有的腳位,pin引數不受信任,傳一個別的 Plugin 正在用的腳位不會有任何效果,也不會回錯誤。 - 硬體已確認在線、且已查得持有記錄之後,實際把腳位還原為輸入的那一步硬體呼叫失敗:本函式仍會清除內部持有記錄並回
ERR_OK——留著一筆沒有人能釋放的記錄,比讓那支腳暫時維持輸出更糟;這與前一項「硬體離線」是兩個不同階段的失敗,只有這一項會被吞掉。
2.11 Dongle 專屬服務
(ABI 4.1 起)以下五支服務與 IcPlugin::on_int_event 都是 ABI 4.1 在結尾追加的,舊 Host 沒有:使用前一律以 NX_ABI_HAS_FIELD(host->size_of_struct, NxHostApi, IntConfigure)(或該服務自己的名字)確認,不要比較 abi_minor。五支服務共用同一組執行緒規則:只能在本掛載綁定後端的 I2C Worker Thread 上的 callback 內呼叫,UI Thread 一律回 ERR_WRONG_THREAD 且不碰硬體(Canvas 的 render 執行緒也一樣,包括 ExecSync(NX_JOB_VENDOR))。FAULTED(已下線)的掛載呼叫 IntConfigure/IntClear/BusRecover/GpioPulse 會回 ERR_INVALID_CONTEXT;唯讀的 IntGetStatus 與 ExecSync(NX_JOB_VENDOR) 不擋。
INT 與 poll_fn 的關係(務必讀):設定了 INT 之後,Core 不會自動停掉 poll_fn。兩者互不相干,也沒有「有 INT 就免輪詢」的魔法。建議做法二選一:
- 在
poll_fn內自行節流:INT 生效時直接回ERR_OK,或每 N 次才做一次保險讀;或由使用者把該掛載的輪詢週期設成0(config.ini或 Host 的掛載設定)。NxHostApi::SetPollingInterval 目前未實作(恆回ERR_NOT_IMPLEMENTED),不能用它停poll_fn。 - 保留慢速保險輪詢。INT 涵蓋不全的 IC,以及 wired-OR(多顆 IC 共線)的 INT 線上漏掉的邊緣,都需要保險輪詢。
另外請記得:INT 事件的取回本身就是 Core 在背景輪詢 Dongle,同樣受 poll off 管制(poll off 時不讀,poll on 後一次交出累積的事件,容量有限、超出的計入 dropped)。所以關掉 poll_fn 不等於沒有 USB 流量。
設定的壽命與共享:INT 設定有擁有權:Core 逐掛載記錄各 Plugin 的 IntConfigure 請求,卸載或下線時自動釋放本掛載的請求;最後一位請求者釋放後才關閉 INT、停止每 20 ms 的事件輪詢。不需要在 IcPlugin::cleanup 呼叫 IntConfigure(ctx, NX_INT_EDGE_NONE, NX_INT_PULL_NONE)。事件環則是 append-only:派發與每個 Core 內部讀者各有自己的游標,互不消耗。
NxHostApi::IntConfigure
簽章
NxLong (NX_CALL *IntConfigure)(NxPluginCtx ctx, NxByte edge, NxByte pull);用途:設定本匯流排 Dongle 的外部中斷(INT)腳要偵測哪個邊緣、用哪種內部上下拉;edge 不是 NX_INT_EDGE_NONE 時,Core 開始在背景取回逐邊緣事件並以 IcPlugin::on_int_event 交付。
執行緒
- 正確執行緒:本掛載綁定後端的 I2C Worker Thread 上的 callback(
probe/poll_fn/自訂指令 handler/on_widget_event/on_tick/on_int_event)。 - 在錯的執行緒呼叫會怎樣:回
ERR_WRONG_THREAD,且不做任何硬體存取。 - 如何確認:PLUGIN_GUIDE 第 4 章執行緒歸屬表。
使用時機:IC 的中斷輸出接到 Dongle 的 INT 腳,且需要「每個邊緣一筆帶時間戳的事件」而不是輪詢暫存器時使用。一個後端只有一條 INT 腳,Core 把同後端所有請求者(Plugin 掛載、Host 指令、日後的 Lua)的請求合併後才套用到硬體:邊緣取聯集(下降+上升 = 雙緣),上下拉取請求順序中第一個非 NX_INT_PULL_NONE 者(之後要求不同上下拉的請求被記錄但不生效,Core 寫一次 WARN)。edge 為 NX_INT_EDGE_NONE 只釋放本掛載自己的請求,不影響其他 Plugin(沒請求過也回 ERR_OK);請求表滿(每後端 16 位)回 ERR_FULL。有效設定沒變時不會再碰硬體。硬體設定是各請求者邊緣的聯集,所以 on_int_event 可能收到你沒有要求的邊緣,請依事件的 level 自行過濾。與 GpioConfigure(NX_PIN_FUNC_INT) 那條路徑互不相干,可並存。取值:edge 為 NX_INT_EDGE_NONE/FALLING/RISING/BOTH(0~3),pull 為 NX_INT_PULL_NONE/UP/DOWN(0~2)。
最小範例
/* 放在 probe() 裡;s_host 是 init() 存下的 const NxHostApi * */
if (NX_ABI_HAS_FIELD(s_host->size_of_struct, NxHostApi, IntConfigure))
{
NxLong rc = s_host->IntConfigure(ctx, NX_INT_EDGE_FALLING, NX_INT_PULL_UP);
if (rc == ERR_OK)
{
s_use_int = 1; /* [範本] 之後由 on_int_event 驅動;poll_fn 改成慢速保險輪詢 */
}
}常見錯誤與症狀
ERR_WRONG_THREAD:寫在init()等 UI Thread callback 裡。ERR_INVALID_PARAM:edge或pull超出範圍。ERR_NOT_SUPPORTED:Dongle 沒有逐邊緣 INT 能力。ERR_HW_BUS_PARKED:該 Dongle 的腳位目前不在 I2C 模式。- 設了 INT 卻以為
poll_fn會停:不會,見本節開頭。
NxHostApi::IntGetStatus
簽章
NxLong (NX_CALL *IntGetStatus)(NxPluginCtx ctx, NxIntStatus *out);用途:讀取 INT 腳目前的設定與 Dongle 端的累計狀態。
執行緒
- 正確執行緒:與 NxHostApi::IntConfigure 相同。
- 在錯的執行緒呼叫會怎樣:回
ERR_WRONG_THREAD,且不做任何硬體存取。 - 如何確認:同上。
使用時機:確認 IntConfigure() 生效、讀目前電位、或比對 Dongle 端的邊緣計數與你收到的事件數。NxIntStatus 成員:SizeOfStruct(呼叫端先填)、edge、pull、level(目前電位)、reserved0、count(自上次 IntClear() 起的邊緣數,會繞回)、dropped(Dongle 端累計丟棄數,會繞回)、last_t_us(最近一次邊緣的 Dongle 本地微秒時間戳,count 非零才有效)。Core 只寫 SizeOfStruct 容得下的欄位,所以較舊的(結構較短的)呼叫端不會被寫過界。
最小範例
NxIntStatus st;
memset(&st, 0, sizeof(st));
st.SizeOfStruct = (NxDword)sizeof(st);
if (s_host->IntGetStatus(ctx, &st) == ERR_OK)
{
/* [範本] st.level 是 INT 腳目前電位 */
}常見錯誤與症狀
ERR_INVALID_PARAM:out為NULL,或SizeOfStruct小於第一個欄位。ERR_NOT_SUPPORTED/ERR_WRONG_THREAD/ERR_HW_OFFLINE:同IntConfigure()。- 忘了填
SizeOfStruct:視為過小,回ERR_INVALID_PARAM。
NxHostApi::IntClear
簽章
NxLong (NX_CALL *IntClear)(NxPluginCtx ctx);用途:清除 Dongle 端的邊緣計數與尚未取走的事件,同時清掉 Core 端的事件佇列與丟棄統計。
執行緒
- 正確執行緒:與 NxHostApi::IntConfigure 相同。
- 在錯的執行緒呼叫會怎樣:回
ERR_WRONG_THREAD。 - 如何確認:同上。
使用時機:初始化後想從乾淨狀態開始計數,或處理完一輪事件想歸零。清除是資料重置、不檢查擁有權:會影響同後端上其他 Plugin 尚未收到的事件;Core 每次清除寫一則 INFO Log 並註明呼叫者種類。
最小範例
rc = s_host->IntClear(ctx);
if (rc != ERR_OK)
{
s_host->LogWrite(ctx, NX_LOG_WARN, "IntClear failed");
}常見錯誤與症狀
ERR_WRONG_THREAD/ERR_NOT_SUPPORTED/ERR_HW_OFFLINE/ERR_INVALID_CONTEXT:同IntConfigure()。- 清掉了別的 Plugin 還沒處理的事件:共享狀態的設計取捨,需要協調時自行約定誰負責清除。
NxHostApi::BusRecover
簽章
NxLong (NX_CALL *BusRecover)(NxPluginCtx ctx, NxDword *out_pins);用途:嘗試恢復被卡住的 I2C 匯流排(例如從裝置拉住 SDA,需要補送 9 個 SCL 時脈)。
執行緒
- 正確執行緒:與 NxHostApi::IntConfigure 相同。
- 在錯的執行緒呼叫會怎樣:回
ERR_WRONG_THREAD,且不做任何硬體存取。 - 如何確認:同上。
使用時機:連續出現匯流排層級的錯誤(例如 NACK 不是 IC 的問題)後,試著把匯流排救回來。ERR_OK 只代表「動作做完」,不代表匯流排已經恢復:請看 out_pins(bit0 = SCL 高、bit1 = SDA 高,兩者皆高才算釋放)。
最小範例
NxDword pins = 0;
if (s_host->BusRecover(ctx, &pins) == ERR_OK && (pins & 0x3) == 0x3)
{
/* [範本] 匯流排已釋放,可重試先前失敗的存取 */
}常見錯誤與症狀
out_pins可傳NULL(不要結果)。ERR_NOT_SUPPORTED:Dongle 沒有此能力;ERR_WRONG_THREAD/ERR_HW_OFFLINE/ERR_INVALID_CONTEXT:同IntConfigure()。- 把
ERR_OK當成「已恢復」:不是,必須檢查out_pins。
NxHostApi::GpioPulse
簽章
NxLong (NX_CALL *GpioPulse)(NxPluginCtx ctx, NxByte pin, NxByte level, NxDword width_us);用途:在自己持有的輸出腳輸出一個定寬脈衝(例如復位脈衝),Core 的 Worker 等滿脈衝寬度才返回。
執行緒
- 正確執行緒:與 NxHostApi::IntConfigure 相同。
- 在錯的執行緒呼叫會怎樣:回
ERR_WRONG_THREAD,且不做任何硬體存取。 - 如何確認:同上。
使用時機:需要微秒級精確寬度的脈衝,用 GpioWrite() 兩次湊不出來時。pin 必須已以 GpioConfigure(..., NX_PIN_FUNC_GPIO_OUT, ...) 持有;level 是脈衝期間的電位(非零為高);width_us 範圍 1~1000000。呼叫期間本匯流排的 Worker 被佔住(其他 Job 與 callback 都得等),所以寬度上限是 1 秒。脈衝結束後的靜止電位由 Dongle 決定,Core 的腳位表不更新;需要特定靜止電位請在返回後自己 GpioWrite() 一次。
最小範例
/* 已在 probe() 以 GpioConfigure(ctx, 1, NX_PIN_FUNC_GPIO_OUT, 0) 持有腳 1 */
rc = s_host->GpioPulse(ctx, 1, 1, 2000); /* 高電位 2 ms */
if (rc == ERR_OK)
{
(void)s_host->GpioWrite(ctx, 1, 0); /* [範本] 明確還原靜止電位 */
}常見錯誤與症狀
ERR_INVALID_PARAM:沒有持有該腳、該腳不是NX_PIN_FUNC_GPIO_OUT,或width_us為0或超過1000000。ERR_NOT_SUPPORTED/ERR_WRONG_THREAD/ERR_HW_OFFLINE/ERR_INVALID_CONTEXT:同IntConfigure()。- 以為脈衝後腳位電位會被 Core 記下:不會,見上。