菜单

Util.h说明

下载

Util.h 详细使用手册

本文档依据当前工程中的 src/util/Util.h 和 src/util/Util.cpp 编写,以 .cpp 的实际实现为准。文档覆盖 Util.h 中的公共类与接口:

  • Util
  • KVUtil
  • SysInfo
  • IniFile 与 iniFileUtil
  • DEBUG 与 DEBUG::MEM
  • INDEV、KEYPADER、BUTTONER、ENCODER
  • NumPad

DEBUG::CPU 在头文件中没有 public:,其成员是私有成员,外部代码不能直接调用;本文只在注意事项中说明,不把它作为公共 API。

1. 基本用法

1.1 引入头文件和命名空间

cpp 复制代码
#include "util/Util.h"

#include <cstdint>
#include <iostream>
#include <string>
#include <vector>

using stt::KV_Str_Str;
using stt::util::BUTTONER;
using stt::util::DEBUG;
using stt::util::ENCODER;
using stt::util::INDEV;
using stt::util::IniFile;
using stt::util::KEYPADER;
using stt::util::KVUtil;
using stt::util::NumPad;
using stt::util::SysInfo;
using stt::util::Util;
using stt::util::iniFileUtil;

这些工具类的大部分方法都是静态方法,无需构造对象:

cpp 复制代码
uint64_t now = Util::getCurrentTimerMS();
std::string os = SysInfo::getOS();

1.2 平台差异

实现通过 __arm__ 区分 ARM 真机和 Windows 模拟器。部分 API 在模拟器上只是占位实现:

API ARM Windows 模拟器
SysInfo 的型号、序列号、版本信息 读取真机信息 多数返回空字符串
SysInfo::getOS() 返回 Linux 仍返回 Linux
iniFileUtil::readIniValue / writeIniValue 真正读写 直接返回 0,但不读写数据
iniFileUtil::load 返回对象 返回 nullptr
changeCfgPassword 真正修改 直接返回 0
enableWatchdog 真正写入配置 直接返回 true
setMmaSize* 修改启动参数 返回 -1
DEBUG::MEM 读取 /proc/meminfo 不执行实际监控

不能仅凭模拟器上的“成功”返回值断言真机操作已经正确执行。

1.3 线程与运行时要求

  • LVGL、页面、控件、编码器和数字键盘相关 API 应在 HMI/UI 线程中调用。
  • DEBUG::MEM 使用分离线程,停止方法只修改标志,不等待线程退出。
  • Util::exe 启动分离线程执行 shell 命令,返回时命令通常尚未结束。
  • DEBUG::lastErrorString、监控标志、输入设备状态等是全局静态状态,没有统一的线程同步保护。

2. Util 接口速查

功能 方法
BCD 转换 DecimalToBCD、BCDtoDecimal、BCD16toDecimal、DecimalToBCD16、BCD32toDecimal、DecimalToBCD32
JSON getValFromJsonc、getValsFromJsonc、getArrayFromJsonc、convertArrayToJson
TCP socketConnect、socketCreate、socketAccept、socketClose、socketWrite、socketReadAll
串口 openComPort、uartWrite、uartClose、uartAvailable、uartRead、uartReadAll
时间与休眠 getCurrentTimerMS、sleepfor
动态库 dllopen、dllclose、dllsym、dllerror、loadDll、loadFunc
UI/颜色 rgb、getScrSize
命令执行 exe、exeSync
系统配置 changeCfgPassword、checkPassword、enableWatchdog、setMmaSize、setMmaSizeSafe
其他 randnum、simpleCalc

3. BCD 转换

3.1 DecimalToBCD 与 BCDtoDecimal

cpp 复制代码
static int DecimalToBCD(int decimal);
static unsigned int BCDtoDecimal(unsigned int bcd);

DecimalToBCD 把十进制数字逐位编码到 4 bit BCD;BCDtoDecimal 执行反向转换。

cpp 复制代码
int bcd = Util::DecimalToBCD(1234);          // 0x1234
unsigned int value = Util::BCDtoDecimal(bcd); // 1234

注意事项:

  • DecimalToBCD 只适合非负整数。负数会直接得到 0。
  • BCDtoDecimal 不校验每个半字节是否为 0..9;例如非法 BCD 0x1A 仍会参与计算。
  • 位数过多时可能发生整数溢出。
  • 需要有符号和输入校验时,应使用 16 位或 32 位专用版本。

3.2 BCD16toDecimal 与 DecimalToBCD16

cpp 复制代码
static int16_t BCD16toDecimal(int16_t bcd);
static int16_t DecimalToBCD16(int16_t decimal);

16 位版本使用最高位作为符号位,因此有效十进制范围是 -7999..7999。

cpp 复制代码
int16_t bcd = Util::DecimalToBCD16(-1234);
int16_t value = Util::BCD16toDecimal(bcd); // -1234

行为说明:

  • 超出范围时不会报错,而是钳位到 -7999 或 7999 后转换。
  • BCD16toDecimal 发现任意 BCD 位大于 9 时返回 -7999。
  • -7999 同时是合法结果和错误哨兵,调用方无法仅凭返回值区分两者。

3.3 BCD32toDecimal 与 DecimalToBCD32

cpp 复制代码
static int32_t BCD32toDecimal(int32_t bcd);
static int32_t DecimalToBCD32(int32_t decimal);

32 位版本的有效十进制范围为 -79999999..79999999,规则与 16 位版本相同。

cpp 复制代码
int32_t bcd = Util::DecimalToBCD32(20261020);
int32_t value = Util::BCD32toDecimal(bcd); // 20261020

非法 BCD 返回 -79999999,该值同样可能是合法结果。关键业务应在调用前自行逐半字节验证 BCD。

4. JSON 处理

所有 JSON 接口基于 json-c。KV_Str_Str 的 k 表示键,v 表示字符串形式的值。

4.1 getValFromJsonc

cpp 复制代码
static string getValFromJsonc(
    const string& json,
    const string& key);

读取 JSON 对象第一层中的单个键。

cpp 复制代码
std::string json = R"({"name":"motor","speed":1500,"enabled":true})";

std::string name = Util::getValFromJsonc(json, "name");       // motor
std::string speed = Util::getValFromJsonc(json, "speed");     // 1500
std::string enabled = Util::getValFromJsonc(json, "enabled"); // true

JSON 无效、键不存在或值为 null 时都返回空字符串,因此不能用空字符串区分这三种情况。方法不支持 a.b 形式的嵌套路径。

4.2 getValsFromJsonc

cpp 复制代码
static bool getValsFromJsonc(
    const string& json,
    vector<KV_Str_Str>& kvs);

有两种模式。

模式一:kvs 预先包含键,只填充这些键的 v:

cpp 复制代码
std::vector<KV_Str_Str> fields;
KVUtil::build(fields, "name,speed,missing", "");

bool ok = Util::getValsFromJsonc(
    R"({"name":"motor","speed":1500})",
    fields);

if (ok) {
    std::cout << KVUtil::getVal(fields, "name") << std::endl;
    std::cout << KVUtil::getVal(fields, "speed") << std::endl;
    // missing 不存在,对应值保持为空字符串。
}

模式二:传入空列表,枚举对象第一层的全部属性:

cpp 复制代码
std::vector<KV_Str_Str> all;
bool ok = Util::getValsFromJsonc(
    R"({"name":"motor","enabled":true,"count":3})",
    all);

注意事项:

  • 空列表模式会向 kvs 追加数据;如果要覆盖旧结果,调用前先 clear()。
  • 预置键模式中布尔值为字符串 true/false;空列表模式中布尔值为 1/0,两种模式不一致。
  • 空列表模式会跳过值为 null 的属性。
  • 对象和数组值会转成 JSON 文本字符串,不会递归生成嵌套的 KV 层级。

4.3 getArrayFromJsonc

cpp 复制代码
static bool getArrayFromJsonc(
    const string& json,
    vector<vector<KV_Str_Str>>& kvs);

用于解析“对象数组”:

cpp 复制代码
std::vector<std::vector<KV_Str_Str>> rows;
bool ok = Util::getArrayFromJsonc(
    R"([{"id":1,"name":"A"},{"id":2,"name":"B"}])",
    rows);

if (ok) {
    for (std::vector<KV_Str_Str>& row : rows) {
        std::cout
            << KVUtil::getVal(row, "id") << ": "
            << KVUtil::getVal(row, "name") << std::endl;
    }
}

JSON 为空、语法无效或根节点不是数组时返回 false。结果会追加到 kvs,不会先清空。实现假设数组元素都是对象;不要传数字、字符串或 null 元素组成的数组。

4.4 convertArrayToJson

cpp 复制代码
static bool convertArrayToJson(
    vector<vector<KV_Str_Str>>& kvs,
    string& json);
cpp 复制代码
std::vector<KV_Str_Str> row1;
KVUtil::push_back(row1, "id", "1");
KVUtil::push_back(row1, "name", "A");

std::vector<KV_Str_Str> row2;
KVUtil::push_back(row2, "id", "2");
KVUtil::push_back(row2, "name", "B");

std::vector<std::vector<KV_Str_Str>> rows = {row1, row2};
std::string json;
bool ok = Util::convertArrayToJson(rows, json);
// 结果形如:[{"id":"1","name":"A"},{"id":"2","name":"B"}]

输入为空时返回 false。输出通过 append 写入,因此调用前应 json.clear()。普通值一律输出为 JSON 字符串;只有值本身以 {...} 或 [...] 包裹时才原样作为嵌套 JSON 插入。

当前序列化底层没有对键和值中的双引号、反斜杠或控制字符做完整 JSON 转义。外部输入包含这些字符时,生成结果可能不是合法 JSON。

5. TCP Socket

这些是低层接口,调用方负责生命周期、协议分帧、重试和缓冲区安全。

5.1 客户端连接:socketConnect

cpp 复制代码
static int socketConnect(const char* hostname, int port);

成功返回 socket 描述符,失败返回 -1。hostname 实际只支持点分十进制 IPv4 地址,不做 DNS 解析。

cpp 复制代码
int sock = Util::socketConnect("192.168.1.100", 9000);
if (sock < 0) {
    std::cerr << "连接失败" << std::endl;
    return;
}

// 使用完成后关闭。
Util::socketClose(sock);

限制:

  • hostname 长度小于或等于 5 会直接失败。
  • ARM 连接等待约 2 秒;Windows 通过循环等待,时长与实现有关。
  • 返回的 socket 保持非阻塞模式。
  • 参数指针不能为 nullptr。

5.2 服务端:socketCreate 与 socketAccept

cpp 复制代码
static int socketCreate(int port, bool block = false);
static int socketAccept(int listen_st);

socketCreate 绑定所有本机地址并监听,成功返回监听 socket,失败返回 0。block == false 时监听 socket 为非阻塞。

socketAccept 成功返回客户端 socket;失败或非阻塞模式下暂时没有连接时返回 -1。

cpp 复制代码
int listener = Util::socketCreate(9000, true);
if (listener == 0) {
    std::cerr << "监听失败" << std::endl;
    return;
}

int client = Util::socketAccept(listener);
if (client >= 0) {
    // 处理 client...
    Util::socketClose(client);
}
Util::socketClose(listener);

部分 socketCreate 失败分支没有关闭刚创建的 socket,频繁失败重试可能泄漏资源。调用方应避免无间隔循环创建。

5.3 写入:socketWrite

cpp 复制代码
static int socketWrite(
    int64_t handler,
    char* buff,
    int len,
    int flag = 0);

该方法直接调用 send,返回本次写入字节数或底层错误值。一次调用不保证写完全部数据。

cpp 复制代码
std::string payload = "PING\n";
int written = Util::socketWrite(
    sock,
    payload.data(),
    static_cast<int>(payload.size()),
    0);

if (written != static_cast<int>(payload.size())) {
    // 非阻塞 socket 需要根据错误码和已写字节做重试。
}

5.4 定长读取:socketReadAll

cpp 复制代码
static int socketReadAll(
    int64_t handle,
    char* buff,
    uint8_t recvLen,
    uint16_t timeOut,
    bool printinfo = false);

目标是在 timeOut 毫秒内累计读取至少 recvLen 字节。成功返回累计字节数,超时返回 -1。

由于当前实现每次固定请求 256 字节,并没有按缓冲区剩余容量限制读取长度,调用方即使只请求少量数据也必须提供至少 512 字节可写空间:

cpp 复制代码
char buffer[512] = {0};
int received = Util::socketReadAll(
    sock,
    buffer,
    static_cast<uint8_t>(8),
    static_cast<uint16_t>(1000),
    false);

if (received >= 8) {
    // 只按实际协议处理 received 字节,不能假定结尾有 '\0'。
}

重要限制:

  • recvLen 是 uint8_t,最大只能表示 255。
  • 返回字节数可能大于 recvLen。
  • 方法不会自动追加字符串结束符。
  • 连接关闭、recv 返回错误或暂时无数据时,它会继续忙循环直到超时。
  • printinfo 当前没有实际输出效果。

5.5 socketClose

cpp 复制代码
static int socketClose(int st);

描述符小于或等于 0 时直接返回 0;其他情况返回底层关闭结果。Windows 上每次关闭还会调用 WSACleanup()。

cpp 复制代码
if (sock > 0) {
    Util::socketClose(sock);
    sock = -1;
}

6. 串口

6.1 openComPort

cpp 复制代码
static int64_t openComPort(
    const char* pFileName,
    int baudRate,
    int byteSize,
    int parity,
    int stopBits);

参数:

  • pFileName:ARM 示例 /dev/ttyS1;Windows 示例 COM8,较大编号可使用系统接受的设备格式。
  • baudRate:波特率。
  • byteSize:数据位,通常为 5..8。
  • parity:0 无校验,1 偶校验,2 奇校验。
  • stopBits:1 或 2。

成功返回句柄,失败返回负数。

cpp 复制代码
int64_t port = Util::openComPort(
    "/dev/ttyS1",
    115200,
    8,
    0,
    1);

if (port < 0) {
    std::cerr << "串口打开失败" << std::endl;
    return;
}

ARM 对常见波特率使用 termios,其他波特率尝试 termios2/BOTHER。参数合法性没有统一前置校验,调用方应只传上述支持值。

6.2 uartWrite

cpp 复制代码
static int uartWrite(
    int64_t handle,
    unsigned char* bytes,
    int offset,
    int size);
cpp 复制代码
unsigned char frame[] = {0x01, 0x03, 0x00, 0x00, 0x00, 0x02};
int written = Util::uartWrite(port, frame, 0, sizeof(frame));
if (written != static_cast<int>(sizeof(frame))) {
    std::cerr << "串口写入不完整" << std::endl;
}

Windows 实现按最多 256 字节分块并使用 offset;ARM 实现一次调用 write,忽略 offset。为了跨平台一致,建议总是传 offset == 0,如需偏移则由调用方调整指针。

6.3 uartAvailable

cpp 复制代码
static int uartAvailable(int64_t handle);

返回接收队列中当前可读的字节数。Windows 查询失败返回 -1;ARM 查询失败返回 0,会与“当前无数据”混淆。

cpp 复制代码
int available = Util::uartAvailable(port);
if (available > 0) {
    // 可以开始读取。
}

6.4 uartRead

cpp 复制代码
static int uartRead(
    int64_t handle,
    unsigned char* bytes,
    int offset,
    int size);

当前实现忽略 offset。更重要的是,传给底层读取的长度表达式实际会令 size <= 256 时仍读取最多 256 字节。因此缓冲区至少应有 256 字节:

cpp 复制代码
unsigned char buffer[256] = {0};
int count = Util::uartRead(port, buffer, 0, sizeof(buffer));
if (count > 0) {
    // 处理 count 字节。
}

不要传小于 256 字节的缓冲区。Windows 异步读内部可能无限等待完成;ARM 的单次 select 等待时间很短,无数据时返回 0。

6.5 uartReadAll

cpp 复制代码
static int uartReadAll(
    int64_t handle,
    char* buff,
    uint8_t recvLen,
    uint16_t timeOut,
    bool printinfo = false);

接口语义与 socketReadAll 类似。因为内部每次调用 uartRead(..., 256),缓冲区应至少为 512 字节:

cpp 复制代码
char response[512] = {0};
int count = Util::uartReadAll(port, response, 8, 1000, false);
if (count >= 8) {
    // 处理二进制响应。
}

超时返回 -1,不会返回已经累计的部分长度;printinfo 当前未使用。

6.6 uartClose

cpp 复制代码
static void uartClose(int64_t handle);
cpp 复制代码
Util::uartClose(port);
port = -1;

该方法没有返回值,也不检查句柄是否合法。一个句柄只能关闭一次。

6.7 完整串口示例

cpp 复制代码
bool queryDevice() {
    int64_t port = Util::openComPort("/dev/ttyS1", 9600, 8, 0, 1);
    if (port < 0) {
        return false;
    }

    unsigned char request[] = {0x01, 0x03, 0x00, 0x00, 0x00, 0x01};
    int written = Util::uartWrite(port, request, 0, sizeof(request));
    if (written != static_cast<int>(sizeof(request))) {
        Util::uartClose(port);
        return false;
    }

    char response[512] = {0};
    int received = Util::uartReadAll(port, response, 7, 1000, false);
    Util::uartClose(port);
    return received >= 7;
}

7. 时间、休眠和随机数

7.1 getCurrentTimerMS

cpp 复制代码
static uint64_t getCurrentTimerMS(char* szTimer = NULL);

返回 Unix 时间的毫秒数,并可选地写出十进制字符串。

cpp 复制代码
uint64_t timestamp = Util::getCurrentTimerMS();

char text[32] = {0};
Util::getCurrentTimerMS(text);
std::cout << text << std::endl;

szTimer 没有长度参数,调用方应至少提供 21 字节,推荐 32 字节。该时间是墙上时钟,不是单调时钟;系统校时后可能前跳或后退,不适合测量严格超时。

7.2 sleepfor

cpp 复制代码
static void sleepfor(int ms);
cpp 复制代码
Util::sleepfor(20); // 阻塞当前线程约 20 ms

不要在 UI 线程中长时间调用,否则界面和事件处理会卡住。负值没有显式校验。

7.3 randnum

cpp 复制代码
static int randnum(int max = 0);
  • max > 0:返回 [0, max),不包含 max。
  • max == 0:返回底层 rand() 的非负结果。
  • max < 0:原样返回 max。
cpp 复制代码
int index = Util::randnum(10); // 0..9

实现每次调用都会用当前秒重新执行 srand,同一秒内多次调用很可能得到相同结果。它不适合高频随机、统计模拟、密码或安全令牌。

8. 动态库加载

8.1 原始封装

cpp 复制代码
static void* dllopen(const char* file, int mode);
static int dllclose(void* handle);
static void* dllsym(void* handle, const char* name);
static char* dllerror(void);

这些方法直接封装 dlopen、dlclose、dlsym、dlerror,Windows 依赖工程中的兼容层。

cpp 复制代码
void* lib = Util::dllopen("./lib/libdemo.so", RTLD_LAZY | RTLD_GLOBAL);
if (!lib) {
    std::cerr << Util::dllerror() << std::endl;
    return;
}

void* symbol = Util::dllsym(lib, "demo_init");
if (!symbol) {
    std::cerr << Util::dllerror() << std::endl;
}

Util::dllclose(lib);
lib = nullptr; // dllclose 内部不能把调用方变量置空。

函数指针转换必须与导出函数的 ABI、调用约定和签名完全一致。

8.2 便捷封装:loadDll 与 loadFunc

cpp 复制代码
static void* loadDll(void* pdll, const char* path);
static void* loadFunc(void* lib, const char* funcname);

loadDll 在传入句柄非空时直接返回原句柄,避免重复加载。loadFunc 在库或符号无效时返回 nullptr 并输出警告。

cpp 复制代码
using AddFn = int (*)(int, int);

void* lib = nullptr;
lib = Util::loadDll(lib, "./lib/libdemo.so");
if (!lib) {
    return;
}

AddFn add = reinterpret_cast<AddFn>(
    Util::loadFunc(lib, "demo_add"));

if (add) {
    std::cout << add(2, 3) << std::endl;
}

Util::dllclose(lib);
lib = nullptr;

动态库必须在所有由它取得的函数指针不再使用后才能关闭。

9. UI 与颜色

9.1 rgb

cpp 复制代码
static int rgb(int r, int g, int b);

当前实现是未完成的占位函数,无论输入什么都返回 0:

cpp 复制代码
int color = Util::rgb(255, 0, 0); // 当前仍为 0,不是红色

在实现完成前不要使用该方法生成颜色。工程其他接口通常直接接受 0xRRGGBB:

cpp 复制代码
int red = 0xFF0000;

9.2 getScrSize

cpp 复制代码
static Area getScrSize();

返回当前 LVGL 活动屏幕的宽高:

cpp 复制代码
stt::Area size = Util::getScrSize();
std::cout << size.w << " x " << size.h << std::endl;

必须在 LVGL 已初始化且活动屏幕存在时调用,通常应在 UI 线程中使用。它返回当前活动屏幕对象尺寸,不一定等于物理面板原始分辨率。

10. shell 命令

10.1 exe:异步执行

cpp 复制代码
static int exe(const string& cmdline, const string& mode);

参数非空时创建分离线程调用 popen,并立即返回 0;参数为空返回 1。

cpp 复制代码
int rc = Util::exe("sync", "r");
if (rc != 0) {
    std::cerr << "命令未发起" << std::endl;
}

返回 0 只表示线程已创建,不表示命令启动成功或退出码为零。无法等待结束,也无法读取输出。

10.2 exeSync:同步执行并读取输出

cpp 复制代码
static bool exeSync(
    const string& cmdline,
    const string& mode,
    string& result);
cpp 复制代码
std::string output;
bool started = Util::exeSync("uname -a", "r", output);
if (started) {
    std::cout << output << std::endl;
}

注意事项:

  • result 是追加写入,调用前应 clear()。
  • 实现会在 fgets 读到的内容后再次追加 \n,输出可能出现双换行。
  • 返回 true 只表示 popen 成功;实现忽略 pclose 返回的进程退出码。
  • 主要使用模式应为 "r"。"w" 模式下代码仍尝试读取流,语义不可靠。
  • cmdline 会交给 shell。严禁直接拼接未校验的用户输入、文件名、网络数据或数据库内容,否则存在命令注入风险。

11. 密码、看门狗和 MMA

11.1 changeCfgPassword

cpp 复制代码
static int changeCfgPassword(
    const string& oldPwd,
    const string& newPwd);

ARM 返回值:

返回值 含义
0 成功
1 已配置密码且旧密码不匹配
2 写入新密码失败
cpp 复制代码
int rc = Util::changeCfgPassword("old-pass", "new-pass");
if (rc == 1) {
    std::cerr << "原密码错误" << std::endl;
} else if (rc != 0) {
    std::cerr << "密码保存失败" << std::endl;
}

Windows 模拟器无条件返回 0,但不会保存密码。密码以环境配置值形式处理,不应假定使用了安全哈希存储。

11.2 checkPassword

cpp 复制代码
static bool checkPassword(const string& pwd);
cpp 复制代码
if (!Util::checkPassword(inputPassword)) {
    std::cerr << "密码错误" << std::endl;
}

未配置密码,即存储值为空时,任何输入都返回 true。该语义适合“未启用密码保护”,不适合作为必须认证的安全边界。

11.3 enableWatchdog

cpp 复制代码
static bool enableWatchdog(int timeout);
  • timeout == 0:关闭看门狗。
  • timeout == 1..100:设置守护秒数。
  • 小于 0 或大于 100:返回 false。
cpp 复制代码
bool ok = Util::enableWatchdog(30);

Windows 模拟器直接返回 true,不执行任何操作。此方法只写配置;具体喂狗机制由系统其他组件负责。

11.4 setMmaSize 与 setMmaSizeSafe

cpp 复制代码
static int setMmaSize(int mSize);
static int setMmaSizeSafe(int mSize);

两个方法都只在 ARM 上有效,目标范围为 4..70 MB;超出范围会被钳位,不是直接拒绝。成功返回 0,失败返回 -1。

cpp 复制代码
int rc = Util::setMmaSizeSafe(32);
if (rc != 0) {
    std::cerr << "MMA 大小修改失败" << std::endl;
}

应优先使用 setMmaSizeSafe,它会:

  • 只查找 mma_heap= 参数范围内的 sz=0x...。
  • 检查拼接后的缓冲区长度。
  • 写入后重新读取并验证。
  • 验证失败时尝试恢复原值。

这是修改 bootargs 的高风险操作,可能影响设备启动和显示内存。不要在 C 系列等不支持的平台调用;修改前应保留原 bootargs,并按设备发布流程验证和重启。

12. 简单表达式:simpleCalc

cpp 复制代码
static string simpleCalc(const string& str);

实现把表达式拼接成 SELECT <表达式>,交给默认 SQLite 数据库计算。

cpp 复制代码
std::string result = Util::simpleCalc("(10 + 2) * 3");
// 预期结果:36,具体字符串格式由 DBSqlite::calc 决定。

空字符串返回空字符串。该方法依赖默认数据库已经初始化,而且没有检查数据库指针是否为空。

虽然名称是“简单表达式”,实质上执行 SQLite SELECT 片段。严禁传入不可信输入;分号、子查询或 SQLite 函数可能扩大执行范围。它也不是通用 C++ 表达式解析器。

13. KVUtil 键值对工具

13.1 push_back

cpp 复制代码
static void push_back(
    vector<KV_Str_Str>& vct,
    const string& k,
    const string& v);
cpp 复制代码
std::vector<KV_Str_Str> items;
KVUtil::push_back(items, "name", "motor");
KVUtil::push_back(items, "speed", "1500");

方法只追加,不检查重复键。

13.2 build

cpp 复制代码
static void build(
    vector<KV_Str_Str>& vct,
    const string& keys,
    const string& vals);
cpp 复制代码
std::vector<KV_Str_Str> items;
KVUtil::build(items, "name, speed,enabled", "motor,1500,1");

keys 中的所有普通空格都会被删除;vals 中空格保留。值数量不足时补空字符串,值多于键时忽略多余部分。结果追加到 vct,不会清空。

该接口只是逗号分割,不支持带引号的逗号值。

13.3 buildFromRow

cpp 复制代码
static vector<KV_Str_Str> buildFromRow(
    TTable& tbl,
    int sel);

把表格某行转换成“列名 -> 单元格值”。

cpp 复制代码
std::vector<KV_Str_Str> row = KVUtil::buildFromRow(table, 0);
std::string name = KVUtil::getVal(row, "NAME");

sel >= table.size() 时返回空列表。当前实现没有检查 sel < 0;负数可能继续传给 getRow,调用方必须先保证 0 <= sel < table.size()。

13.4 列表版 getVal 与 setVal

cpp 复制代码
static string getVal(
    vector<KV_Str_Str>& vct,
    const string& key);

static bool setVal(
    vector<KV_Str_Str>& vct,
    const string& key,
    string value);
cpp 复制代码
std::string speed = KVUtil::getVal(items, "speed");
bool changed = KVUtil::setVal(items, "speed", "1600");

两者只处理第一个匹配键。getVal 在键不存在和键存在但值为空时都返回空字符串;setVal 不会自动插入缺失键。

13.5 两个 getKVector 重载

cpp 复制代码
static vector<string> getKVector(
    vector<KV_Str_Str>& vct,
    bool k = true);

static vector<string> getKVector(
    vector<KV_Str_Str>& vct,
    const string& deli,
    const string& padl = "",
    const string& padr = "");
cpp 复制代码
std::vector<std::string> keys = KVUtil::getKVector(items, true);
std::vector<std::string> values = KVUtil::getKVector(items, false);

std::vector<std::string> assignments =
    KVUtil::getKVector(items, "=", "'", "'");
// 例如:name='motor'

第二个重载只是字符串拼接,不执行 SQL 转义。不要把其结果直接用于含不可信数据的 SQL。

13.6 数据库配置版 getVal 与 setVal

cpp 复制代码
static string getVal(
    const string& key,
    const string& defv);

static void setVal(
    const string& key,
    const string& val);

操作默认数据库中的 S_CFG(K, V) 表:

cpp 复制代码
std::string theme = KVUtil::getVal("theme", "light");
KVUtil::setVal("theme", "dark");

键和值会通过 StringUtil::sqlEsc 转义。调用前必须保证默认数据库已创建且 S_CFG 表存在。写接口没有返回值,无法直接判断数据库执行是否成功;并发写入也没有额外事务保护。

14. SysInfo 系统信息

14.1 设备与版本信息

cpp 复制代码
static string getHMIModel();
static string getSerialNumber();
static string getSysVersion();
static string getSysDay();
static string getOS();
static string getFwVersion();
static string getFwDay();
cpp 复制代码
std::cout << "型号: " << SysInfo::getHMIModel() << '\n';
std::cout << "序列号: " << SysInfo::getSerialNumber() << '\n';
std::cout << "系统版本: " << SysInfo::getSysVersion() << '\n';
std::cout << "系统发布日期: " << SysInfo::getSysDay() << '\n';
std::cout << "OS: " << SysInfo::getOS() << '\n';
std::cout << "固件版本: " << SysInfo::getFwVersion() << '\n';
std::cout << "固件发布日期: " << SysInfo::getFwDay() << '\n';

ARM 上版本信息来自 /customer/cfginfo.ini 的 [stt_hmi] 段。读取失败返回空字符串。模拟器上除 getOS() 固定返回 Linux 外,其余通常为空。

14.2 存储设备

cpp 复制代码
static vector<string> getUdisks();
static vector<string> getUdisksHot();
static vector<string> getSDCards();
static void setHotPlugin(bool b);
static bool getHotPlugin();
cpp 复制代码
std::vector<std::string> disks;
if (SysInfo::getHotPlugin()) {
    disks = SysInfo::getUdisksHot();
} else {
    disks = SysInfo::getUdisks();
}

for (const std::string& path : disks) {
    std::cout << path << std::endl;
}
  • getUdisks 枚举 /vendor/ 下名称匹配 udisk_* 的项。
  • getSDCards 枚举 /vendor/ 下类似 sd、sda、sda1 的名称。
  • 返回项只按名称正则筛选,没有再次验证类型或挂载状态。
  • 结果顺序由目录枚举决定,不保证排序。
  • getUdisksHot 会在每个候选 U 盘上创建并删除测试目录,只返回可写项。它会产生真实闪存写入,不应高频轮询。
  • setHotPlugin 只设置进程内全局标志,本身不执行设备扫描。

14.3 设备列表占位接口

cpp 复制代码
static vector<string> getPrinters();
static vector<string> getInputDevs();
static vector<string> getSerialComs();
static vector<string> getEths();
static vector<string> getWLan();
static vector<string> getFGs();

当前实现全部直接返回空列表:

cpp 复制代码
std::vector<std::string> ports = SysInfo::getSerialComs();
// 当前实现中 ports 为空,不能据此断定设备不存在。

这些接口尚未实现,不应作为设备发现或状态判断依据。

14.4 进程查询与终止

cpp 复制代码
static int getProcessByName(const string& name);
static void killProcessByName(const string& name);
cpp 复制代码
int pid = SysInfo::getProcessByName("demo_service");
if (pid > 0) {
    SysInfo::killProcessByName("demo_service");
}

仅 ARM 执行真实操作;模拟器查询返回 0,终止方法不执行任何操作。killProcessByName 没有返回值,无法确认目标是否存在或终止是否成功。终止进程是破坏性操作,只能传受信任的固定服务名。

15. INI 文件

INI 键使用 section:key 格式,例如 wifi:enabled。

15.1 一次性读写:iniFileUtil

cpp 复制代码
static int readIniValue(
    const string& key,
    string& value,
    const string& file);

static int writeIniValue(
    const string& key,
    const string& value,
    const string& file);
cpp 复制代码
std::string value;
int rc = iniFileUtil::readIniValue(
    "wifi:enabled",
    value,
    "/customer/wifi.ini");

if (rc == 0) {
    std::cout << value << std::endl;
}

rc = iniFileUtil::writeIniValue(
    "wifi:enabled",
    "1",
    "/customer/wifi.ini");

ARM 上 0 表示成功。读取失败会把 value 清空。写入接口拒绝空 key、空 value 或空文件名,因此不能用它写入空字符串值。

Windows 模拟器中两个方法都直接返回 0:读取不会设置 value,写入不会修改文件。这是最容易造成误判的平台差异之一。

15.2 批量操作:load、IniFile 与 free

cpp 复制代码
IniFile(const string& path);
~IniFile();

static IniFile* iniFileUtil::load(const string& path);
static void iniFileUtil::free(IniFile* f);

bool IniFile::readString(
    const string& key,
    string& value,
    const string& defaultValue);

bool IniFile::writeString(
    const string& key,
    const string& value);

IniFile 构造函数加载完整文件到内存字典;析构函数把字典整体写回原文件并释放字典。通常不直接构造,而是使用 iniFileUtil::load 与 iniFileUtil::free,这也与工程现有调用方式一致。

cpp 复制代码
IniFile* ini = iniFileUtil::load("/customer/app.ini");
if (!ini) {
    std::cerr << "INI 加载失败或当前平台不支持" << std::endl;
    return;
}

std::string server;
bool exists = ini->readString(
    "network:server",
    server,
    "127.0.0.1");

if (!exists) {
    server = "127.0.0.1"; // ARM 上键不存在时应显式采用默认值。
}

ini->writeString("network:server", "192.168.1.10");

// 析构时才把完整字典写回文件。
iniFileUtil::free(ini);
ini = nullptr;

注意事项:

  • load 在 Windows 模拟器返回 nullptr。
  • ARM 上文件加载失败时仍可能返回非空 IniFile*,但其内部字典为空,读写返回 false。
  • writeString 只修改内存字典;free/析构时才覆盖写回原文件。
  • 析构写文件没有检查 fopen 是否成功,路径失效时存在风险。
  • 不要直接 delete 后继续使用指针;推荐统一用 iniFileUtil::free 并置空。
  • 当前 writeString 内部把 key 作为 sprintf 格式字符串使用。键名包含 % 时存在未定义行为,只能使用受信任的字母、数字、下划线和 : 键名。
  • 同一文件并发加载和写回可能互相覆盖。

16. DEBUG 日志与错误信息

16.1 最后错误字符串

cpp 复制代码
static string lastErrorString;
static void setLastErrorString(string s);
static string getLastErrorString();
cpp 复制代码
DEBUG::setLastErrorString("配置无效");
std::string error = DEBUG::getLastErrorString();

这是进程级静态字符串,不是线程局部状态。成功操作不会自动清空旧错误,多线程读写存在竞争。业务代码必须先看当前 API 的返回值,再把错误字符串作为补充信息。

16.2 日志级别:DEBUG::set

cpp 复制代码
static void set(
    int level,
    bool verbose = false,
    bool retarget = false);

级别定义:

值 DebugLevel 输出范围
0 NO 关闭分级日志;LOGD 仍按 NO 路径输出
1 LOG LOG
2 WARN LOG、WARN
3 ERR LOG、WARN、ERR
cpp 复制代码
DEBUG::set(stt::constant::DebugLevel::WARN, true, false);

verbose == true 时输出文件名、行号和级别;retarget 在 ARM 实现中仍是 TODO,没有真正通过网络重定向。

16.3 DEBUG::LOG 与宏

cpp 复制代码
static void LOG(
    int level,
    const char* file,
    int line,
    const char* format,
    ...);

通常不应手工填写文件和行号,而应使用宏:

cpp 复制代码
LOGD("value=%d", value);       // level 0
_LOG_("connected: %s", host); // level 1
_WARN_("retry=%d", retry);    // level 2
_ERR_("open failed: %s", path); // level 3

_LOG_、_WARN_、_ERR_ 定义在 base/internal/StaticMember.h,并非只包含 Util.h 就一定可用。

格式字符串和参数类型必须严格匹配。当前 DEBUG::LOG 的可变参数处理会重复消费同一个 va_list,尤其在 ARM 上存在未定义行为;复杂日志或关键诊断信息不应只依赖该方法。

16.4 内存监控:DEBUG::MEM

cpp 复制代码
static void startMemoryMonitor(int ms = 100);
static void stopMemoryMonitor();
static void printAvailableMemory();
static string availableMemory;
cpp 复制代码
DEBUG::MEM::startMemoryMonitor(1000); // 实际单位是毫秒

// ...运行一段时间...
std::cout << DEBUG::MEM::availableMemory << std::endl;

DEBUG::MEM::stopMemoryMonitor();
  • 仅 ARM 有效。
  • ms 实际是毫秒,头文件注释中的“每 n 秒”不准确。
  • 重复启动时,如果已有监控线程则不再创建。
  • 线程会打印当前、最小、最大可用内存以及差值。
  • stopMemoryMonitor 不等待线程退出,且标志/字符串没有原子或互斥保护。
  • ms <= 0 会导致高频循环,应使用合理正数,例如 1000。
  • printAvailableMemory 只读取和打印一次 /proc/meminfo 中的 MemAvailable。

DEBUG::CPU 当前是私有嵌套类,外部无法调用;即使实现文件有监控代码,也不属于可用公共接口。

17. 输入设备公共约定

以下 API 直接操作 LVGL 输入设备、全局焦点组和 HMI 运行时,必须在运行时初始化完成后调用。推荐仅在 UI 线程调用。类型约定:

类型 含义
1 指针类设备,包括触摸、鼠标及实现定义的硬按键
2 键盘
3 外部按键 Buttoner
4 编码器

17.1 INDEV

cpp 复制代码
static void deleteIndev(int type);
static void enableIndev(int type, bool en);
static void enableExtDriver();
static void disableExtDriver();
static void encoderNext();
static void encoderPrev();
static void encoderEnter();
cpp 复制代码
INDEV::enableExtDriver();

INDEV::encoderNext();
INDEV::encoderPrev();
INDEV::encoderEnter();

INDEV::disableExtDriver();
  • deleteIndev 会永久从 LVGL 删除匹配类型的全部输入设备,通常无法仅靠 enableIndev 恢复。
  • enableIndev 只启用/禁用当前已注册的匹配设备;找不到时静默不操作。
  • enableExtDriver 会启用编码器及光标、外部按键和键盘,并把键码类型设为 2。
  • disableExtDriver 会禁用这些设备,并把键码类型恢复为 0。

17.2 KEYPADER

cpp 复制代码
static void setKeycodeType(int type);
static void enableInputMode(bool en);

setKeycodeType:

  • 0:SDL 原键码。
  • 1:SDL 原始扫描码。
  • 2:STT 外部按键顺序。
cpp 复制代码
KEYPADER::setKeycodeType(2);
KEYPADER::enableInputMode(false);

关闭输入模式可避免一次物理按键既触发 Buttoner/Encoder 事件,又向输入框输入字符。实现没有范围校验,只应传 0..2。

18. BUTTONER 外部按键

cpp 复制代码
static bool enabled;
static void resetPoints();
static void sendId(int id);
static void enableButtoner(bool b);
static void sendCode(uint8_t code);

启用外部按键时会自动启用编码器:

cpp 复制代码
BUTTONER::enableButtoner(true);
BUTTONER::sendCode(10); // 查找 buttonerCode == 10 的控件并触发

sendId 的 id 与当前 LVGL 编码器组中的控件顺序相关。sendCode 则先按控件配置的 buttonerCode 查找控件,再换算组索引。

注意事项:

  • 必须先建立正确的编码器焦点组,并启用 Buttoner。
  • resetPoints 会根据组中控件当前坐标重建按钮点数组。
  • 当前实现取得类型 3 输入设备后未判空就设置点数组;未注册 Buttoner 驱动时可能崩溃。
  • sendCode 和 resetPoints 都依赖运行时控件映射与 LVGL 对象仍然有效。
  • 直接修改公共字段 enabled 会绕过驱动启停逻辑,应使用 enableButtoner。

19. ENCODER 编码器与焦点组

19.1 启用和发送按键

cpp 复制代码
static void enableEncoder(bool b);
static void sendKey(uint32_t key);
static void sendChar(uint32_t c);
static int lastKey;
static bool enabled;

编码器键值:0 无按键、1 确认、2 上一个、3 下一个。

cpp 复制代码
ENCODER::enableEncoder(true);
ENCODER::sendKey(3);  // 下一个
ENCODER::sendKey(1);  // 确认
ENCODER::sendChar('A');

禁用时 sendKey、sendChar 等大多数方法静默返回。应通过 enableEncoder 修改状态,不要直接写 enabled。

19.2 管理焦点组

cpp 复制代码
static void addObject(STTWidget* w, STTPage* frm = NULL);
static void addObjects(vector<void*> vec);
static void addObjects(vector<STTWidget*> vec);
static void removeObject(STTWidget* w);
static vector<void*> clearObjects();
static void focusObject(STTWidget* w);
static STTWidget* getFocused();
static void reset();

页面初始化示例:

cpp 复制代码
ENCODER::enableEncoder(true);
ENCODER::clearObjects();

ENCODER::addObject(btnBack, currentPage);
ENCODER::addObject(txtName, currentPage);
ENCODER::addObject(btnOk, currentPage);
ENCODER::focusObject(btnBack);

clearObjects 返回清除前的原始 LVGL 对象指针列表,可用于临时保存和恢复:

cpp 复制代码
std::vector<void*> oldGroup = ENCODER::clearObjects();
ENCODER::addObject(dialogOk);

// 对话框结束后:
ENCODER::clearObjects();
ENCODER::addObjects(oldGroup);

注意事项:

  • addObject 没有判空,w 必须有效。
  • vector<void*> 重载要求每个指针都是真实有效的 lv_obj_t*。
  • removeObject 不会同步清理页面保存的 encoder group 列表。
  • getFocused 在编码器禁用、无焦点或对象无法映射回 STT 控件时返回 nullptr。
  • reset 使用最后页面保存的焦点组和最后焦点恢复状态。

19.3 焦点导航和模式

cpp 复制代码
static void focusNext();
static void focusPrev();
static void enter();
static void focusFreeze(bool b);
static void wrap(bool b);
static void editing(bool b);
cpp 复制代码
ENCODER::wrap(true);        // 最后一项之后回到第一项
ENCODER::editing(false);    // 浏览模式
ENCODER::focusNext();
ENCODER::focusFreeze(true); // 暂时冻结当前焦点
ENCODER::focusFreeze(false);
ENCODER::enter();

这些方法作用于全局 LVGL group,不是每个页面独立的 group。

19.4 光标与焦点轮廓

cpp 复制代码
static void showCursor(bool b);
static void setCursorType(int type);
static void showOutline(bool b);
static void setOutline(int color, int width);
static void showWidgetOutline(STTWidget* w);

static int color;
static int width;
static bool isShowOutline;
cpp 复制代码
ENCODER::showCursor(true);
ENCODER::setCursorType(1);       // 0 箭头,1 手指
ENCODER::setOutline(0x00FF00, 3);
ENCODER::showOutline(true);
ENCODER::showWidgetOutline(widget);

showOutline 只修改全局标志,不会自动遍历已有控件应用样式;具体控件需要调用 showWidgetOutline。不要直接修改 color、width 或 isShowOutline。

19.5 输入法集成

cpp 复制代码
static void addIme();
static void removeIme();
static void focusToImeOkButton();
static void openImeOnce(bool b);
static bool isImeOnce();
cpp 复制代码
ENCODER::addIme();
ENCODER::focusToImeOkButton();

ENCODER::openImeOnce(true);
bool once = ENCODER::isImeOnce();

ENCODER::removeIme();

focusToImeOkButton 假设默认输入法对象、按键矩阵和 OK 键都存在,内部没有空指针检查。只能在输入法已经打开后调用。

20. NumPad 数字键盘

20.1 键码

NumPad 定义了以下类别:

  • KEY_0..KEY_9:ASCII 数字。
  • KEY_A..KEY_Z:ASCII 大写字母。
  • KEY_a..KEY_z:ASCII 小写字母。
  • KEY_Point:.。
  • KEY_BACK:退格。
  • KEY_TAB:Tab,目前没有专门分支,会因不在可打印范围而不操作。
  • KEY_ENTER:发送 LV_EVENT_VALUE_CHANGED。
  • KEY_ESC:Escape,目前没有专门分支而不操作。
  • KEY_DEL:向前删除。
  • KEY_SIGN:正负号切换。
  • KEY_CANCEL:发送 LV_EVENT_CANCEL。

20.2 sendCode

cpp 复制代码
static void sendCode(uint8_t key);
cpp 复制代码
NumPad::sendCode(NumPad::KEY_1);
NumPad::sendCode(NumPad::KEY_2);
NumPad::sendCode(NumPad::KEY_Point);
NumPad::sendCode(NumPad::KEY_5);
NumPad::sendCode(NumPad::KEY_SIGN);
NumPad::sendCode(NumPad::KEY_ENTER);

方法把输入发送给全局当前输入框 STTTextAreaInfo::currentInputTa。没有当前输入框时,大多数键静默忽略。

行为说明:

  • 输入可打印 ASCII 字符前,会先删除当前选中文本。
  • KEY_SIGN 在开头的 + 和 - 间切换;没有符号时插入 -,并同步系统变量 CurrentInput。
  • KEY_BACK 删除光标前字符。
  • KEY_DEL 删除光标后字符。
  • KEY_ENTER 和 KEY_CANCEL 发送事件,不直接关闭键盘。
  • 不会检查输入框是否只允许数字、最大长度或数值范围,这些限制由 STTTextArea 自身配置处理。

应在 UI 线程调用,不要保存并跨页面复用旧的 currentInputTa 指针。

21. 推荐的防御性调用清单

  1. JSON、KV、命令结果等输出容器在调用前先 clear(),除非明确需要追加。
  2. socketReadAll 和 uartReadAll 至少提供 512 字节缓冲区,uartRead 至少提供 256 字节。
  3. socket 写入必须处理部分写入;读取的数据不能假定以 \0 结束。
  4. shell、simpleCalc、动态库路径、INI 键和进程名只能来自可信输入。
  5. UI、输入设备、getScrSize 和 NumPad 只在运行时及 LVGL 初始化完成后从 UI 线程调用。
  6. 模拟器中的成功占位返回值不能替代 ARM 真机验证。
  7. setMmaSizeSafe 优先于 setMmaSize,并纳入设备恢复和重启验证流程。
  8. 不直接写 DEBUG、BUTTONER、ENCODER 的公共静态状态字段,使用对应方法维持驱动状态一致。
  9. 对 DEBUG::lastErrorString 不作线程安全和自动清理假设。
  10. rgb、设备列表接口和私有的 DEBUG::CPU 目前不应作为有效功能使用。
上一个
FileUtil.h说明
下一个
工程案例讲解
最近修改: 2026-10-20Powered by