Skip to Content
Plugin 開發設定持久化

設定持久化

Plugin 想跨執行記住東西(板子接線、閾值、某個開關),不用自己讀寫檔案。 主程式已經有 config.ini;你只要實作兩支 callback,Core 幫你收、幫你送:

callback什麼時候被呼叫你做什麼
on_config_load(ctx, key, value)掛載時,init 成功之後、probe 之前,每個鍵一次把你認得的鍵存進自己的變數
on_config_save(ctx, buf, buf_size)使用者卸載這顆 Plugin 之前,以及主程式結束存檔之前把要記住的設定完整寫進 buf

兩支都在 UI Thread 上執行(見生命週期與執行緒)。

存在 config.ini 的哪裡

你的設定固定放在 config.ini 的 [Plugin.<name>] 這一段,<name> 是 IcPlugin::name。 例如 hello_ic 的設定長這樣:

[Plugin.hello_ic] note=hello
  • 同名 Plugin 的所有掛載共用同一段。 使用者在兩個分頁各掛一份,它們讀寫的是同一段,而不是各自一份。 on_config_load 每次掛載各收到一次;on_config_save 收集時只會問其中一個代表實例。 不要假設兩個實例之間會即時同步:B 實例要等自己下一次重新掛載才會讀到 A 剛存的新值。
  • 段內是一般的 key=value 行。
  • 只送給主掛載(slaves[0])。你宣告了多個位址時,其他位址的掛載不會重複收到。
  • headless、widget、canvas 三種掛載型態待遇相同。

格式與長度限制

你在 on_config_save 寫進 buf 的內容要遵守:

  • 每一行都是 key=value;鍵不能以 [、;、# 開頭;鍵與值都不能含控制字元(包含 Tab)。
  • 輸出裡不要夾註解行。使用者手寫 config.ini 時可以加註解、縮排,那些會被容忍;你自己輸出的內容沒有這層寬容, 有任何一行不合格,整份輸出都會被拒收(視同你回傳了非 ERR_OK)。全空白的行會被跳過。
  • 每一行(不含換行)最多 287 位元組。
  • buf_size 恆為 4096(含結尾 NUL),但有效內容最多 4094 位元組,而且要以 NUL 結尾。超過或找不到 NUL 也是整份被拒收。

回傳值

on_config_save 回 ERR_OK 時,buf 必須是完整的設定,不是差量。

  • 空字串代表「我沒有任何要記住的設定」,config.ini 這一段的鍵會全部被移除。
  • 回任何不是 ERR_OK 的值(不限負值)=「這次先別存」:Core 記一則警告,維持你上一次成功交出的內容 (本次執行從沒成功交出過,就維持磁碟上原本的),你不會丟掉已經存好的設定。 適合在你的設定正處於不完整的中間狀態時使用。
  • 寧可回 ERR_BUFFER_TOO_SMALL 也不要輸出半截內容。

最容易踩的兩個坑

1. 整段取代,不是合併

Host 把你這次輸出當成這一段的完整內容,你沒輸出的鍵一律被刪掉。這有一個實際後果: 使用者從新版降級回你的舊版,或者別的工具在這一段加了一個你這個版本不認得的鍵, 你若只輸出自己認得的鍵,存檔時那個鍵就永久消失了。

作法:on_config_load 收到的每一個鍵都存起來(包含你不認得的),on_config_save 時連同你自己更新過的鍵一起原樣輸出。 下面的範例就是這樣做的。

2. on_config_save 不能碰硬體

它在 UI Thread 上,而且主程式結束時會落在關閉流程的寬限期內(約 5 秒,期間不等任何工作),你擋在整個關閉流程正中央。 所以:必須快、不能阻塞,ExecSync 在這裡會回 ERR_WRONG_THREAD。 要記住的東西應該早就在你的記憶體裡(上次 on_config_load 收到的,或使用者透過 widget/自訂指令改過的), 不要想「存檔前最後讀一次硬體」。

範例

config_persist.c下載 config_persist.c
/**
 * @file    config_persist.c
 * @brief   Remembering one setting across runs with on_config_load and
 *          on_config_save. A reading aid for the tutorial page
 *          "config-persistence", not a complete plugin: the two callbacks
 *          would be assigned to IcPlugin::on_config_load / on_config_save.
 *
 * @note    Kept ASCII-only on purpose and C89 so it passes the syntax-only
 *          guard. Only the key "note" is understood; every other key that
 *          shows up is kept and written back, so downgrading or a hand-edited
 *          config.ini never loses data (the save output REPLACES the whole
 *          [Plugin.<name>] section).
 */

#include <stdio.h>
#include <string.h>

#include "nexus_plugin_api.h"

/** @brief The setting this sample understands; empty until loaded. */
static char s_note[64] = "";

/** @brief Keys this sample does not understand, kept verbatim ("key=value\n" lines). */
static char s_extra[1024] = "";

/**
 * @brief  Receives one key=value pair from the [Plugin.<name>] section.
 * @param[in] ctx   Main mount context (unused).
 * @param[in] key   Key, leading and trailing blanks already trimmed.
 * @param[in] value Value, trimmed the same way.
 * @note   UI Thread, after init() and before probe(). Only the primary
 *         mount (slaves[0]) receives it. The same key is delivered once.
 */
void NX_CALL cfg_load(NxPluginCtx ctx, const char *key, const char *value)
{
    size_t used;
    size_t need;

    NX_UNUSED_ARG(ctx);
    if (strcmp(key, "note") == 0)
    {
        used = strlen(value);
        if (used > sizeof(s_note) - 1)
        {
            used = sizeof(s_note) - 1;
        }
        memcpy(s_note, value, used);
        s_note[used] = '\0';
        return;
    }
    /* Unknown key: remember it so on_config_save can echo it back. */
    used = strlen(s_extra);
    need = strlen(key) + strlen(value) + 2;
    if (used + need < sizeof(s_extra))
    {
        sprintf(s_extra + used, "%s=%s\n", key, value);
    }
}

/**
 * @brief  Fills buf with the COMPLETE set of settings to remember.
 * @param[in]  ctx      Main mount context (unused).
 * @param[out] buf      Output buffer, already an empty string.
 * @param[in]  buf_size Always NX_PLUGIN_CONFIG_MAX (4096, NUL included).
 * @return ERR_OK when buf holds the full settings; any other value means
 *         "do not save this time, keep what was saved before".
 * @note   UI Thread, before unload and before the host writes config.ini
 *         at exit. Must be fast; ExecSync is not available here.
 *         Every line must be key=value with no control characters and no
 *         comment lines; the whole text must be at most 4094 bytes.
 */
NxLong NX_CALL cfg_save(NxPluginCtx ctx, char *buf, int buf_size)
{
    char   line[96];
    size_t len_line;
    size_t len_extra;

    NX_UNUSED_ARG(ctx);
    sprintf(line, "note=%s\n", s_note);
    len_line  = strlen(line);
    len_extra = strlen(s_extra);
    if ((int)(len_line + len_extra) > buf_size - 2)
    {
        return ERR_BUFFER_TOO_SMALL;         /* better an error than half an output */
    }
    memcpy(buf, line, len_line);
    memcpy(buf + len_line, s_extra, len_extra + 1);   /* +1 copies the NUL */
    return ERR_OK;
}

(同前面的範例一樣,這是閱讀輔助:兩支函式要自己填進 IcPlugin 的 on_config_load/on_config_save。)

版本相容:只准向後相容

  • 新版本新增的鍵,你的讀取端要有自己的預設值:舊的 config.ini 沒有那個鍵,不能因此出錯、崩潰或讓 Plugin 進不了 probe。
  • 舊版本讀到不認得的鍵要忽略它:不得報錯,也不得拒收整段。

理由同上:使用者可能把 Plugin 降版,降版後的舊版仍要能正常開機。

什麼時候不會存

on_config_save 寫的值沒進 config.ini,依序檢查:

  1. 回傳值不是 ERR_OK。
  2. 輸出有不合格的行(沒有 =、含控制字元、鍵以 [/;/# 開頭)。
  3. 輸出超過 4094 位元組。
  4. on_config_save 崩潰了(見下)。
  5. 這是批次模式:--batch 只會呼叫 on_config_load,永遠不呼叫 on_config_save,也不會回寫 config.ini。見Batch/headless。
  6. 這顆 Plugin 本次執行從沒被掛載過,沒有東西可以寫回。

另外,如果使用者手改 config.ini 讓你的那一段啟動時就不合格(例如夾了控制字元),Core 只記一則警告、讓你用內建預設值跑; 即使本次你正常回了 ERR_OK,主程式結束時也不會用預設值把那個壞掉的段覆蓋掉,使用者原本寫的內容原封不動留著, 直到他自己修好為止。

崩潰時會怎樣

兩支 callback 都在例外隔離之內,崩潰只會讓這顆 Plugin 下線,不會波及主程式,但下場不同:

  • on_config_load 崩潰:這次掛載指令仍算成功,但主掛載立刻被標記為故障(分頁標題顯示 IC:<name>(faulted))、記一筆錯誤, 不會送 probe,還沒送完的其餘鍵也不再送。
  • on_config_save 崩潰:這次收集作廢(等於什麼都沒存到),該掛載被標記為故障。如果崩潰發生在使用者卸載這顆 Plugin 的收集點, 卸載本身照常完成。

x86 建置沒有這層例外隔離,見架構要對:x86/x64。