VisualHMI - 分期应用(RW-自定义)

概述

💡 系统方案 :前置密码输入模式(“到期前锁定”)

image-20231205092317319

描述:当前系统时间尚未到达某期截止时间,但 HMI 已提前弹出密码输入画面,要求用户预先输入该期密码才能继续使用。

示例:第一期截止时间:2023-11-01,系统时间:2023-10-01, 2023-10-01 起即锁定,必须输入第一期密码方可操作。

✅ 专业特征:

  • 触发条件当前时间 ≥ 分期启用起始窗口(隐含逻辑:分期一旦配置即“待激活”)

💡 自定义方案 :准时密码输入模式(“到期前锁定”)

若用户需求是 准时密码输入模式(“到期时锁定”),即当本地系统时间达到或超过某期设定时间点时,HMI 才弹出密码输入画面。

示例:第一期截止时间:2023-11-01,系统时间:2023-10-01 ,正常使用。若系统时间:2023-11-01 00:00:00 → 立即锁定,需输入第一期密码。

✅ 专业特征:

  • 触发条件当前时间 ≥ 分期截止时间(Deadline)

区别

维度 系统方案 :前置密码输入 自定义方案 :准时密码输入
触发时机 分期配置后即生效 严格等于/超过分期时间点
授权行为 “先验证,后使用”(前置) “到期即锁,验证续用”
用户感知 “提前被锁” “刚好到期被锁”

使用范围:VisualHMI - HMI&M&DX系列 自定义分期

应用下载:VisualHMI - 自定义分期(点击下载)

1.设计思路

本章节,我们设计“自定义分期”的应用,在Lua采样元表方式,在 HMI 上实现了“时间精准触发 + 动态密码验证 ”的分期授权方案。关键元素有以下几点:

元素 说明
分期数量 1~10 期,可动态配置
分期时间 每期一个 {年,月,日},必须严格递增
动态密码 由外部函数 password_func(date) 实时计算
超级密码 单一 uint32 值,用于全局解锁
使能开关 可远程启用/禁用整个分期功能
已解锁期 记录当前最高已验证通过的期号

💡 自定义特点

此设计避免了静态密码预存的脆弱性,也规避了提前锁定的体验缺陷,精准匹配设备租赁、试用、分阶段交付等典型工业场景。

🔑 1. 到期即锁

通过 get_eligible_stage() 严格比对系统时间与分期截止日,仅当 now ≥ deadline 时才触发锁屏

🔑 2. 动态密码,算法外置

密码由外部传入函数实时计算(_pwd_func(date)),无需预存,支持任意策略(如 y+m+d、哈希、UUID 衍生),灵活且防逆向。

🔑 3. 超级密码兜底

独立超级密码(_super_password)可一键全局解锁,兼顾现场运维与安全控制

🔑 4. 自校验热更新

启动时自动检测日期变更(_check_dates_changed),配置修改后自动重写存储,确保运行逻辑与主程序一致。

2.应用说明

stage.lua 模块采用闭包封装 + 原表(metatable)面向对象实现 HMI 分期授权功能:支持 1~10 期截止日期配置,仅当系统时间 ≥ 某期日期时才锁定界面;密码由外部函数动态计算(如年+月+日),不预存,防泄露;含超级密码应急解锁;所有状态(时间、开关、解锁进度等)持久化存储于 VT_RW 非易失区,掉电不丢;

2.1 初始化

main.luaon_init 函数中,加载stage.lua,通过Stage.new(base_addr, stage_dates, password_func, super_password)加载并实例化分期模块:

参数 类型 作用说明
base_addr number(uint16) VT_RW 起始地址。所有分期配置和状态(如日期、开关、解锁进度等)将从该地址开始连续存储,支持多实例隔离。
stage_dates table 3 期截止日期列表。设备在到达对应日期当天 00:00 起自动锁定,需输入当期密码才能继续使用。日期必须递增。
password_func function 动态密码生成函数。运行时根据当前系统日期计算密码(本例为 年+月+日),不预存,安全灵活。
super_password) number(uint32) 超级密码。由设备唯一 UUID 的 ASCII 码累加生成,实现“一机一密”,用于应急全局解锁。

关键优势

  • 普通密码随日期变化,无法提前破解;
  • 超级密码绑定设备硬件标识,防止通用万能码滥用。

🛠️Step 1:加载模块

dofile('stage.lua')
  • 执行 stage.lua,定义全局 Stage 表及其 new 方法;
  • 内部通过闭包封装 Stage 类,外部无法访问私有字段或地址。

🛠️Step 2:超级密码

local uuid = get_device_uuid()
set_string(VT_LW, 0x3000, uuid)
  • 读取 HMI 设备的唯一 UUID(如 MAC 或序列号);
  • 将其写入 VT_LW[0x3000] 供调试或显示使用。

🛠️Step 3:调用 Stage.new() 创建实例

▶ 3.1 参数校验(防御性编程)
  • 检查 base_addr 是否为有效数字且 ≤ 0x7FFF
  • 检查 stage_dates 数量(1~10)、格式(三元组)、顺序(递增);
  • 检查 password_func 是函数,super_password 是数字。

若任一校验失败,打印错误并返回 nil,避免后续异常。

▶ 3.2 计算内部存储偏移地址

基于 base_addr = 0x1000,分配如下:

  • 0x1000:初始化标志(0x1234 表示已初始化)
  • 0x1001~0x1014:10 期时间戳(每期占 2 个 uint16)
  • 0x1015:分期总数(=3)
  • 0x1016:使能开关(默认=1)
  • 0x1017:已解锁期(初始=0)
  • 0x1018:超级解锁标志(初始=0)
  • 0x1019~0x101A:超级密码(uint32)
▶ 3.3 检查是否需要初始化 FLASH

调用 _check_dates_changed()

  • 读取 VT_RW[0x1015] 获取已存分期数;
  • 若数量 ≠ 3,或任一期日期 ≠ 存储值 → 判定配置变更
  • VT_RW[0x1000] != 0x1234首次运行

只要满足任一条件,就执行 _init_persistent()

▶ 3.4 执行 _init_persistent()(原子写入)
  • 将 3 个日期转为 Unix 时间戳(00:00:00),补足 10 期(后7期填0);
  • 批量写入 20 个 uint16 到 0x1001~0x1014
  • 写入控制参数(分期数=3、启用=1、未解锁=0、超级未触发=0);
  • 最后写入 0x12340x1000,确保掉电不会导致半初始化。

Step 4:返回可用实例

  • g_stage 成为一个封装好的对象;
  • 外部可通过 g_stage:verify_password(input, stage_idx) 验证密码;
  • 所有底层地址和逻辑被闭包隐藏,安全、防误操作
--main.lua
-- 普通密码,动态计算
local function password_sum_y_m_d(date_table)
    -- 例如:密码 = 年 + 月 + 日
    --local idx = g_stage:set_unlocked_stage(idx)
    y, m, d = get_date_time()
    print('password_sum_y_m_d : '..(string.format('%04d/%02d/%02d', y, m, d))..' = '..( y + m  + d))
    return y + m  + d
end

--超级密码
function calculate_uuid_sum(uuid_str)

    local sum = 0
    -- 遍历字符串中的每一个字符
    for i = 1, #uuid_str do
        local char = uuid_str:sub(i, i) -- 获取第i个字符
        local ascii_val = string.byte(char) -- 获取字符的ASCII码
        sum = sum + ascii_val -- 累加到总和
    end

    return sum
end


function on_init()

    dofile('stage.lua')  -- 加载分期模块(定义 Stage 表)

    local uuid = get_device_uuid()
    set_string(VT_LW, 0x3000, uuid)
    -- 创建分期管理实例
    -- 传入各期解锁日期,
    g_stage = Stage.new(.....)


end

--stage.lua

    -- 构造函数:创建分期管理实例
    -- 参数 base_addr: uint16,VT_RW 中的起始寄存器地址(例如 0x1000)
    -- 参数 stage_dates: 表,格式为 {...},最多10期,且日期必须递增
    -- 参数 password_func: 函数,接收 {year, month, day} 返回密码(如 function(d) return d[1]+d[2]+d[3] end)
    -- 参数 super_password: uint32,初始的超级密码
    function Stage.new(base_addr, stage_dates, password_func, super_password)
        -- 校验基地址类型
        if type(base_addr) ~= "number" then
            --print("错误:base_addr 必须是一个数字")
            return nil
        end

        if base_addr > 0x7FFF then
            --print("base_addr 必须在0x0000~0x7FFF")
            return nil
        end

        -- 校验分期数量:必须在 1~10 之间
        if #stage_dates < 1 or #stage_dates > 10 then
            --print("错误:分期数量必须为 1~10")
            return nil
        end

        -- 校验每期日期格式:必须是 {y, m, d} 三元组
        for i, d in ipairs(stage_dates) do
            if #d ~= 3 then
                --print("错误:日期格式错误,必须为 {年,月,日}")
                return nil
            end
        end

        -- 校验日期是否递增
        for i = 2, #stage_dates do
            local prev_y, prev_m, prev_d = stage_dates[i-1][1], stage_dates[i-1][2], stage_dates[i-1][3]
            local curr_y, curr_m, curr_d = stage_dates[i][1], stage_dates[i][2], stage_dates[i][3]

            local prev_ts = make_timestamp(prev_y, prev_m, prev_d, 0, 0, 0)
            local curr_ts = make_timestamp(curr_y, curr_m, curr_d, 0, 0, 0)

            if prev_ts >= curr_ts then
                --print(string.format("错误:日期必须递增,第%d期 (%04d/%02d/%02d) 不能大于或等于 第%d期 (%04d/%02d/%02d)",i-1, prev_y, prev_m, prev_d, i, curr_y, curr_m, curr_d))
                return nil
            end
        end

        -- 校验密码函数是否存在
        if type(password_func) ~= "function" then
            --print("错误:password_func 必须是一个函数")
            return nil
        end

        -- 校验超级密码类型
        if type(super_password) ~= "number" then
            --print("错误:super_password 必须是一个数字")
            return nil
        end

        -- 计算实际存储地址
        local addr_init_flag       = base_addr + 0x0000 -- 初始化完成标志,值为 0x1234 表示已初始化
        local addr_timestamp_base  = base_addr + 0x0001 -- 时间戳数组起始地址
                                                         -- 每期时间戳为 uint32(4字节),拆分为 2 个 uint16
                                                         -- 10 期 × 2 = 20 个寄存器(+0x0001 ~ +0x0014)
        local addr_stage_count     = base_addr + 0x0015 -- 当前启用的分期数量(1~10)
        local addr_enable_flag     = base_addr + 0x0016 -- 分期功能使能开关(1=启用,0=禁用)
        local addr_unlocked_stage  = base_addr + 0x0017 -- 当前已解锁的分期编号(0=未解锁,1~10=对应期数)
        local addr_super_unlock    = base_addr + 0x0018 -- 超级密码是否已触发(1=已解锁全部功能)
        local addr_super_pwd       = base_addr + 0x0019 -- 超级密码

        -- 创建新对象并绑定元表,支持 self:method() 调用
        local self = setmetatable({ 
            _dates = stage_dates,
            _pwd_func = password_func,  -- 保存密码计算函数
            _super_password = super_password, -- 保存初始超级密码
            -- 将计算好的地址存储在实例中,方便后续方法使用
            _addr_init_flag       = addr_init_flag,
            _addr_timestamp_base  = addr_timestamp_base,
            _addr_stage_count     = addr_stage_count,
            _addr_enable_flag     = addr_enable_flag,
            _addr_unlocked_stage  = addr_unlocked_stage,
            _addr_super_unlock    = addr_super_unlock,
            _addr_super_pwd       = addr_super_pwd
        }, Stage)

        -- 检查是否首次运行 或 日期数据不一致:若 INIT_FLAG 不等于 0x1234 或者 日期数量/内容不同,则需初始化 FLASH
        if  self:_check_dates_changed()  or get_uint16(VT_RW, self._addr_init_flag) ~= 0x1234  then
            self:_init_persistent()

        end

        return self
    end

 -- 私有方法:检查当前日期数据是否与存储在Flash中的不一致
    function Stage:_check_dates_changed()
        -- 读取已存储的分期数量
        local stored_count = get_uint16(VT_RW, self._addr_stage_count)

        -- 数量不同,直接返回 true
        if stored_count ~= #self._dates then
            print("检测到分期数量改变,需要重新初始化")
            return true
        end

        -- 逐个比较日期(由于时间戳是以 uint32 存储,拆成两个 uint16)
        for i = 1, #self._dates do
            local stored_timestamp = get_uint32(VT_RW, self._addr_timestamp_base + (i - 1) * 2)

            local y, m, d = self._dates[i][1], self._dates[i][2], self._dates[i][3]
            local calculated_timestamp = make_timestamp(y, m, d, 0, 0, 0)

            if stored_timestamp ~= calculated_timestamp then
                print("检测到第 "..i.." 期日期改变,需要重新初始化")
                return true
            end
        end

        -- 所有日期都一致,返回 false
        return false
    end

    -- 私有方法:首次运行时将配置写入 VT_RW(FLASH)
    function Stage:_init_persistent()
        local ts_data = {}  -- 用于批量写入时间戳的数组

        -- 循环处理 10 期(不足10期的补0)
        for i = 1, 10 do
            if self._dates[i] then
                local y, m, d = self._dates[i][1], self._dates[i][2], self._dates[i][3]-- 提取年、月、日
                local ts = make_timestamp(y, m, d, 0, 0, 0)-- 转换为 Unix 时间戳(秒),时分秒设为 00:00:00

                table.insert(ts_data, (ts >> 16) & 0xFFFF)  -- 高16位
                table.insert(ts_data, ts & 0xFFFF)          -- 低16位
            else
                -- 未定义的期数填充 0
                table.insert(ts_data, 0)
                table.insert(ts_data, 0)
            end
        end

        -- 批量写入 20 个 uint16(10 期时间戳)
        set_array(VT_RW, self._addr_timestamp_base, ts_data)

        -- 写入控制参数(按紧凑顺序)
        set_uint16(VT_RW, self._addr_stage_count, #self._dates) -- 实际分期数
        set_uint16(VT_RW, self._addr_enable_flag, 1)            -- 默认启用功能
        set_uint16(VT_RW, self._addr_unlocked_stage, 0)         -- 初始未解锁
        set_uint16(VT_RW, self._addr_super_unlock, 0)           -- 超级密码未触发
        set_uint32(VT_RW, self._addr_super_pwd, self._super_password) -- 初始超级密码(uint32)

        -- 最后写入初始化标志,防止掉电导致半初始化状态(原子性保障)
        set_uint16(VT_RW, self._addr_init_flag, 0x1234)
    end

2.2 分期”锁“

run_stage() 函数是分期授权系统的核心检测逻辑,通常由 HMI 的 系统定时器(如 on_systick,每 1 秒调用一次) 触发。其目标是:在设备时间到达某期截止日时,自动跳转到密码输入界面(锁屏)

以下是该函数的分步执行流程与检测逻辑说明

🛠️Step 1:安全前置检查

if not g_stage then return end
  • 如果 g_stage 实例未创建(如初始化失败),直接退出,避免空指针错误。

🛠️ Step 2:跳过锁屏的豁免条件

if g_stage:is_super_unlocked() or not g_stage:is_enabled() then return end
  • 超级密码已触发 → 全局解锁,无需再锁;

  • 分期功能被禁用

    满足任一条件即退出,不进入锁屏。

🛠️ Step 3:获取“当前应解锁的最高分期编号”

local eligible = g_stage:get_eligible_stage(y, m, d)
  • get_eligible_stage 内部
    • 将当前日期转为时间戳(00:00:00);
    • 遍历所有分期截止日,找到最后一个 ≤ 当前时间的期号
    • 返回该期号(如今天是 2026-02-20,则返回 2,因为第 2 期截止日 2026-02-15 已过,第 3 期 2026-03-15 未到);
    • 若所有期都未到,返回 0

📌 关键逻辑:只有当 eligible > 0 时,才表示“已有到期未解锁的期”。


🛠️ Step 4:获取当前实际已解锁的期号

local current = g_stage:get_unlocked_stage()
  • VT_RW 读取当前已通过密码验证的最高期号(初始为 0);
  • 例如:用户已输入第 1 期密码 → current = 1

🛠️ Step 5:判断是否需要触发锁屏

if eligible > 0 and eligible ~= current and get_screen() ~= LOCK_SCREEN_ID then

同时满足以下三个条件才跳转:

  1. 有应解锁的期eligible > 0(说明至少有一期已到期)
  2. 尚未解锁该期eligible ~= current(例如:应解锁第 2 期,但当前只解锁到第 1 期)
  3. 当前不在锁屏页get_screen() ~= LOCK_SCREEN_ID (防止重复跳转或循环)

⚠️ 注意:这里 不要求逐期解锁。只要当前时间跨过多期(如跳过第 1、2 期直接到第 3 期),eligible = 3,而 current = 0,就会要求输入第 3 期密码(而非逐期输入)。这是典型“到期即锁”行为。


🛠️ Step 6:执行锁屏跳转

set_uint16(VT_LW, 0x1100, eligible)  -- 通知 HMI 画面:当前需解锁第几期set_screen(LOCK_SCREEN_ID)           -- 切换到密码输入界面
  • 将目标期号写入 VT_LW[0x1100],供锁屏画面显示提示(如“请输入第 2 期密码”);
  • 强制切换到锁屏页面,阻止用户操作主功能。

🔚 总结:检测逻辑本质

每秒检查一次:如果“当前日期 ≥ 某期截止日”,且“该期尚未解锁”,就立即锁屏要求输入对应动态密码。

function run_stage()

    -- 安全检查:若分期未初始化,直接退出
    if not g_stage then return end 

    -- 若已通过超级密码解锁,或功能被禁用,则无需锁屏
    if g_stage:is_super_unlocked() or not g_stage:is_enabled() then return end

    -- 获取当前时间应解锁的最高分期编号(基于系统日期)
    local test = get_uint16(VT_LW, 0x2003)
    local y, m, d  = nil, nil, nil
    if test == 1
    then
        y, m, d = get_uint16(VT_LW, 0x2000), get_uint16(VT_LW, 0x2001), get_uint16(VT_LW, 0x2002)
    end
    local eligible = g_stage:get_eligible_stage(y, m, d )


    -- 获取当前已解锁的分期编号(0 表示未解锁)
    local current = g_stage:get_unlocked_stage()

    -- 判断是否需要触发锁屏:
    -- 1. 有应解锁的期(eligible > 0)
    -- 2. 当前未解锁该期(eligible ~= current)
    -- 3. 当前不在锁屏页(防止重复跳转)
    if eligible > 0 and eligible ~= current and get_screen() ~= LOCK_SCREEN_ID 
    then
        set_uint16(VT_LW, 0x1100, eligible)  -- 通知 HMI 画面:当前应解锁第几期(用于显示提示)
        -- 跳转到锁屏页面(用户需输入密码)
        set_screen(LOCK_SCREEN_ID)
    end
end

-- 系统定时器回调(通常每 100ms 调用一次)
function on_systick()
    run_stage() 
end

2.3 分期“解”

🛠️Step 1:读取用户输入与目标分期

local pwd = get_uint32(VT_LW, addr)           -- 读取用户输入的 32 位密码
local target = get_uint16(VT_LW, 0x1100)      -- 读取当前应解锁的期号(由 run_stage 设置)
  • pwd:用户在 HMI 密码框中输入的值;
  • target:此前由 run_stage() 写入,表示当前因时间到期需验证的分期编号(如第 2 期)。

🛠️Step 2:多种密码验证

if g_stage:verify_password(pwd, target) or g_stage:verify_super_password(pwd) then
  • 普通密码验证:调用动态函数(如 年+月+日)计算第 target 期的正确密码,比对用户输入;
  • 超级密码验证:比对是否等于基于设备 UUID 生成的唯一万能密码;

验证成功后:

  • 普通密码 → 自动更新已解锁分期;
  • 超级密码 → 全局解锁并禁用分期功能。

🛠️ Step 3:解锁后跳转主界面

set_screen(3)
  • 立即退出锁屏页面,返回主操作界面(ID=3);
  • 后续定时检测将不再触发锁屏(因状态已同步)。

🛠️Step 4:协同逻辑

该函数与 run_stage() 形成闭环:

  • run_stage() 负责何时锁(基于系统时间);
  • do_stage() 负责如何解(响应用户输入);
  • 两者通过 VT_LW[0x1100]VT_LW[0x1110] 与 HMI 画面交互,实现全自动授权控制。
Copyright ©Dacai all right reserved,powered by Gitbook该文件修订时间: 2026-02-05 14:29:29

results matching ""

    No results matching ""