Skip to Content
Plugin 開發第一個 Plugin 2:自訂指令

第一個 Plugin 2:自訂指令

步驟 1 的 Plugin 載入了,但使用者跟它沒有任何互動。這一步加一個自訂指令 hello: 使用者在指令列打 hello,Plugin 就在 Log 印一行字。

沒有畫面的 Plugin,自訂指令就是它唯一的介面。 之後每一個「讓 Plugin 做事」的功能, 不管最後有沒有面板,都會先做成指令。

完整程式碼

這一步的完整檔案。用它取代你的 src\hello_ic.c(記得把 HELLO_IC_ADDR 改回你自己的 IC 位址):

/**
 * @file    hello_ic.c
 * @brief   hello_ic 教學步驟 2:在步驟 1 之上加一個自訂指令 `hello`。
 *
 * 新概念只有一個:**自訂指令**。Plugin 在 `IcPlugin.commands` 放一張指令表,
 * 使用者掛載這顆 Plugin 之後,就能在主程式指令列打表裡的指令名稱。
 * 沒有畫面的 Plugin,自訂指令就是它唯一的互動方式。
 *
 * @see   docs/NEXUS_TUI_PLUGIN_GUIDE.md 第 1 章 1a(自訂指令不得與內建指令同名)、第 4 章(執行緒)、第 9 章(Batch 模式下呼叫自訂指令)
 */

#include "nexus_plugin_api.h"
#include "plugin_version.h"

/**
 * @brief 本 Plugin 對外的名稱。
 * @note  使用者在指令列打 `plugin list` 會看到它,打 `p hello_ic` 就掛載它。
 */
#define HELLO_IC_NAME   "hello_ic"

/**
 * @brief 你的 IC 的 7-bit I2C 位址。
 * @note  **請改成你自己的 IC 的位址。** 不知道位址時,在主程式指令列打 `scan`,
 *        它會列出匯流排上有回應的所有 7-bit 位址。本教學預設 0x58
 *        (模擬後端 mock 預設就有一顆 0x58)。
 */
#define HELLO_IC_ADDR   0x58

/**
 * @brief 本 Plugin 對外宣告的 slave 位址表(至少要有一列,否則 Core 拒載)。
 *
 * @note  欄位依序是:位址、位址模式(8-bit 暫存器位移)、bank 模式(無 bank)、
 *        bank 暫存器、bank 遮罩、旗標、保留、顯示名稱。
 * @warning 必須是靜態儲存期:Core 只讀不釋放,所以不能是函式內的區域變數。
 */
static const NxSlaveDecl s_slaves[] =
{
    { HELLO_IC_ADDR, NX_ADDR_MODE_8BIT, NX_BANK_MODE_NONE, 0, 0, 0, { 0, 0 }, "hello_ic" }
};

/**
 * @brief Core 提供的服務函式表,`init()` 時存下來,之後的 callback 才用得到。
 * @note  生命期涵蓋整個行程;不得修改其內容、不得釋放。
 */
static const NxHostApi *s_host = (const NxHostApi *)0;

/**
 * @brief Plugin 掛載時的初始化,Core 在 **UI Thread** 上呼叫。
 *
 * @param[in] ctx  本次掛載的上下文
 * @param[in] host HostAPI 函式表
 * @return `ERR_OK` 表示成功;非 `ERR_OK` 會使這次掛載中止。
 *
 * @warning 在 UI Thread 上執行,不得做 I2C 存取或任何會阻塞的事。
 * @note    把 `host` 存進 `s_host`:自訂指令的處理函式只拿得到 `ctx`,
 *          要呼叫 HostAPI 就得靠這個全域指標。
 */
static NxLong NX_CALL hello_ic_init(NxPluginCtx ctx, const NxHostApi *host)
{
    s_host = host;
    host->LogWrite(ctx, NX_LOG_INFO, "hello_ic: loaded");
    return ERR_OK;
}

/**
 * @brief Plugin 卸載時的清理。**強制實作,不得為 `NULL`。**
 *
 * @param[in] ctx 本次掛載的上下文
 * @return 無
 *
 * @warning 在 **I2C Worker Thread** 上呼叫,返回後 `ctx` 立即失效。
 * @note    把 `s_host` 清掉:卸載之後任何遲到的呼叫都不該再碰 HostAPI。
 */
static void NX_CALL hello_ic_cleanup(NxPluginCtx ctx)
{
    NX_UNUSED_ARG(ctx);
    s_host = (const NxHostApi *)0;
}

/**
 * @brief 自訂指令 `hello` 的處理函式:寫一行 Log,回 `ERR_OK`。
 *
 * @param[in] ctx  本次掛載的上下文
 * @param[in] argc 參數個數(含指令本身)
 * @param[in] argv 參數陣列;只在本次呼叫期間有效
 * @return `ERR_OK`。指令處理函式自己的回傳值就是這次指令的結果:
 *         回 `ERR_OK` 或錯誤碼;非 `ERR_OK` 時主程式會顯示錯誤。
 *         (batch 模式下,非 ERR_OK 的回傳值會成為 Exit Code 的絕對值。)
 *
 * @warning 在該掛載所綁定後端的 **I2C Worker Thread** 上呼叫,不是 UI Thread;
 *          所以之後可以在這裡直接讀寫 I2C。
 */
static NxLong NX_CALL hello_ic_cmd_hello(NxPluginCtx ctx, int argc, const char *const *argv)
{
    NX_UNUSED_ARG(argc);
    NX_UNUSED_ARG(argv);
    s_host->LogWrite(ctx, NX_LOG_INFO, "hello_ic: hello!");
    return ERR_OK;
}

/**
 * @brief 本 Plugin 的自訂指令表:名稱、`help` 說明、用法、處理函式。
 *
 * @note  靜態生命期,Core 只讀不釋放。
 * @warning 說明字串請用純 ASCII 收尾(VC6 以 CP950 掃描原始碼,中文結尾的
 *          字串會讓引號無法關閉);這裡整段用英文最保險。
 */
static const NxCommandDef s_commands[] =
{
    { "hello", "say hello in the log", "hello", hello_ic_cmd_hello }
};

/**
 * @brief 本 Plugin 的能力宣告,Core 只讀不釋放。
 *
 * @note  必須是靜態儲存期:靜態變數一開始全是 0,而 0 對每一個選用 callback
 *        而言正是「我不提供」。
 */
static IcPlugin s_plugin;

/**
 * @brief Plugin DLL 的進入點:Core 載入 DLL 之後第一個呼叫的函式。
 *
 * @param[in] host_abi_major Host 端的 `NX_PLUGIN_ABI_MAJOR`
 * @param[in] host_abi_minor Host 端的 `NX_PLUGIN_ABI_MINOR`(本函式刻意不比較:minor 不同不是拒載的理由)
 * @return 指向靜態生命期 `IcPlugin` 的指標;ABI major 不相容時回傳 `NULL`。
 *
 * @warning 在 UI Thread 上呼叫,不得做 I2C 存取或阻塞操作。
 * @note    欄位一律用**具名賦值**填寫,沒填的欄位維持 0(=不提供該 callback)。
 */
NX_PLUGIN_EXPORT struct IcPlugin * NX_CALL NexusPluginGetInterface(NxDword host_abi_major,
                                                                   NxDword host_abi_minor)
{
    NX_UNUSED_ARG(host_abi_minor);

    if (host_abi_major != NX_PLUGIN_ABI_MAJOR)
    {
        return (struct IcPlugin *)0;
    }

    s_plugin.abi_major       = NX_PLUGIN_ABI_MAJOR;
    s_plugin.abi_minor       = NX_PLUGIN_ABI_MINOR;
    s_plugin.size_of_struct  = sizeof(IcPlugin);

    s_plugin.name            = HELLO_IC_NAME;
    s_plugin.description     = "hello_ic tutorial plugin";

    s_plugin.slaves          = s_slaves;
    s_plugin.slave_count     = (NxByte)(sizeof(s_slaves) / sizeof(s_slaves[0]));
    s_plugin.slave_decl_size = (NxDword)sizeof(NxSlaveDecl);
    s_plugin.panel_mode      = NX_PANEL_MODE_NONE;

    s_plugin.init            = hello_ic_init;
    s_plugin.cleanup         = hello_ic_cleanup;

    s_plugin.commands        = s_commands;
    s_plugin.command_count   = (NxWord)(sizeof(s_commands) / sizeof(s_commands[0]));

    s_plugin.version_major   = (NxWord)PLUGIN_VER_MAJOR;
    s_plugin.version_minor   = (NxWord)PLUGIN_VER_MINOR;
    s_plugin.version_patch   = (NxWord)PLUGIN_VER_PATCH;

    return &s_plugin;
}

新增了什麼

跟步驟 1 比,只多了四件事。

1. s_host:把 HostAPI 存起來。

static const NxHostApi *s_host = (const NxHostApi *)0;

init() 拿到 host 之後存進 s_host,cleanup() 再清掉。理由:指令的處理函式只拿得到 ctx,拿不到 host,要呼叫 HostAPI(寫 Log、讀暫存器)就得靠這個全域指標。 卸載後把它清成 NULL,讓任何遲到的呼叫不會碰到已經失效的東西。

2. 指令的處理函式 hello_ic_cmd_hello()。

static NxLong NX_CALL hello_ic_cmd_hello(NxPluginCtx ctx, int argc, const char *const *argv) { ... s_host->LogWrite(ctx, NX_LOG_INFO, "hello_ic: hello!"); return ERR_OK; }

argc/argv 是使用者打的指令與參數,跟 C 的 main 一樣(argv[0] 是指令本身); 這個指令用不到,所以標成 NX_UNUSED_ARG 避免編譯器警告。回傳值是這次指令的結果: ERR_OK 表示成功,其他錯誤碼主程式會顯示給使用者看。

這個函式跑在 I2C Worker Thread,不是 UI Thread。 這很重要:所以下一步可以直接在這裡讀寫 I2C。

3. 指令表 s_commands[]。

static const NxCommandDef s_commands[] = { { "hello", "say hello in the log", "hello", hello_ic_cmd_hello } };

四個欄位依序是:指令名稱、help 顯示的說明、用法字串、處理函式。 說明字串用純英文,理由是相容最舊的編譯器(VC6 那一頁有解釋)。 必須是 static,跟 s_slaves 一樣。

4. 把指令表掛到 IcPlugin 上。

s_plugin.commands = s_commands; s_plugin.command_count = (NxWord)(sizeof(s_commands) / sizeof(s_commands[0]));

Core 收到使用者打的指令時,會在已掛載 Plugin 的指令表裡找名字,找到就呼叫處理函式。 所以必須先掛載這顆 Plugin,指令才存在。

自訂指令不得與內建指令同名(例如 run 是內建的 run <script.lua>),取名時避開。 移植既有韌體見移植既有韌體;在批次模式下呼叫自訂指令的注意事項見Batch/headless。

建置、載入、執行

  1. 重新建置:build.bat clangmingw x64 release(車道與架構照你的選擇)。

  2. 把新的 hello_ic.dll 複製進主程式的 plugins\,取代舊的。 主程式必須先關掉重開:Windows 不讓你覆蓋一顆正在被載入的 DLL,而且已載入的舊 Plugin 也不會自動換成新版。

  3. 啟動主程式(沒有硬體就加 --dongle mock),掛載並執行:

    plugin switch hello_ic hello

Log 面板出現一行:

終端機版的 I2C Bus Log 面板,一行 INFO 訊息:[hello_ic] hello_ic: hello!
終端機版:hello 在 Log 印出一行(點圖看完整原圖)
視窗版的 I2C Bus Log 面板,一行 INFO 訊息:[hello_ic] hello_ic: hello!
視窗版:同一行

Log 行前面的 [hello_ic] 是主程式自動加的來源標籤,讓你知道這行是誰寫的。

打了 hello 沒有反應或說找不到指令:Plugin 沒有掛載(先 plugin switch hello_ic), 或舊的 DLL 還在被使用、新的沒換進去(確認主程式有關掉重開)。

下一步

步驟 3:讀暫存器:把指令從「印字」升級成「真的讀一次 IC」。