定时算法实现
定时算法实现
本节详细介绍投料系统的核心算法——时间差计算和投料决策逻辑。学习完成后,您将能够:
- 实现基于数据库历史记录的时间差算法
- 处理首次运行、间隔计算、边界条件等场景
- 实现灵活的投料周期配置
- 为多区域投料设计独立的调度算法
Core Algorithm
Section titled “Core Algorithm”输入: - lastTimestamp: 数据库中的上次投料时间 (Unix 秒) - currentTime: 当前时间 (Unix 秒) - dosingInterval: 投料间隔 (秒, 可配置)
算法逻辑:
if lastTimestamp == 0 (首次运行): 结果 = 投料
elif (currentTime - lastTimestamp) >= dosingInterval: 结果 = 投料
elif 固定时间模式 and 当前时间 == 设定时间: 结果 = 投料
else: 结果 = 不投料
输出: - dosingCommand: "1" (投料) 或 "0" (不投料) - timeRemaining: 下次投料剩余时间 (秒) - nextDosing: 下次投料预估时间Node-RED Implementation
Section titled “Node-RED Implementation”Function Node: 时间差算法
Section titled “Function Node: 时间差算法”// Function: 投料时间差核心算法// 输入: msg.payload = SELECT 查询结果 (数组)// 输出: MQTT 控制指令
// ===== 配置参数 (可以从 Dashboard 动态设置) =====var DEFAULT_INTERVAL = 86400; // 秒, 默认 24 小时var config = flow.get("dosingConfig") || { interval: DEFAULT_INTERVAL, fixedHour: -1, // -1 表示不使用固定时间模式 minInterval: 3600, // 最小间隔 1 小时 (安全保护) maxInterval: 604800 // 最大间隔 7 天};
// ===== 从数据库获取上次投料时间 =====var lastTimestamp = 0;if (msg.payload && Array.isArray(msg.payload) && msg.payload.length > 0) { lastTimestamp = msg.payload[0].timestamp;}
// ===== 当前时间 =====var currentTime = Math.floor(Date.now() / 1000);var currentDate = new Date();var currentHour = currentDate.getHours();var currentMinute = currentDate.getMinutes();
// ===== 算法: 计算时间差 =====var timeDiff = currentTime - lastTimestamp;var shouldDose = false;var decisionReason = "";
// 情况 1: 首次运行 (数据库无记录)if (lastTimestamp === 0) { shouldDose = true; decisionReason = "first_run";
// 情况 2: 固定时间模式 (例如每天 8:00)} else if (config.fixedHour >= 0) { var lastDate = new Date(lastTimestamp * 1000); var lastDosingHour = lastDate.getHours();
// 检查是否已经过了今天的设定时间 var todayDosed = flow.get("todayDosed_" + config.zone) || false;
if (currentHour === config.fixedHour && !todayDosed && timeDiff >= config.minInterval) { shouldDose = true; decisionReason = "fixed_time"; flow.set("todayDosed_" + config.zone, true); }
// 跨天重置 if (currentHour === 0 && currentMinute === 0) { flow.set("todayDosed_" + config.zone, false); }
// 情况 3: 间隔模式 (例如每 24 小时)} else { if (timeDiff >= config.interval) { shouldDose = true; decisionReason = "interval_elapsed"; } else { decisionReason = "interval_not_met"; }}
// ===== 计算下次投料信息 =====var remainingSeconds = 0;var nextDosingTime = null;
if (config.fixedHour >= 0) { // 固定时间模式: 下次投料 = 明天 8:00 var next = new Date(); if (currentHour >= config.fixedHour) { next.setDate(next.getDate() + 1); // 明天 } next.setHours(config.fixedHour, 0, 0, 0); remainingSeconds = Math.floor((next.getTime() - Date.now()) / 1000); nextDosingTime = next.toLocaleString();} else { // 间隔模式: 剩余时间 = 间隔 - 已过时间 remainingSeconds = Math.max(0, config.interval - timeDiff); nextDosingTime = new Date((currentTime + remainingSeconds) * 1000).toLocaleString();}
// ===== 输出 =====var result = { command: shouldDose ? "1" : "0", reason: decisionReason, metadata: { lastTimestamp: lastTimestamp, lastDosingTime: lastTimestamp > 0 ? new Date(lastTimestamp * 1000).toLocaleString() : "never", currentTime: currentTime, timeDiff: timeDiff, remaining: remainingSeconds, remainingHours: Math.round(remainingSeconds / 3600 * 10) / 10, nextDosing: nextDosingTime, zone: config.zone || "default" }};
// 保存决策到 Flow Contextflow.set("lastDosingResult", result);
// 输出: 第一个输出发送 MQTT 指令node.log("Decision: " + (shouldDose ? "DOSE" : "SKIP") + " | Reason: " + decisionReason + " | Next: " + nextDosingTime);
msg.payload = result.command;msg.topic = "esp32/dosing/control";msg.qos = 1;msg.metadata = result.metadata;
return msg;Dashboard: 下次投料显示
Section titled “Dashboard: 下次投料显示”// Function: 格式化下次投料时间用于 Dashboard 显示
var result = flow.get("lastDosingResult");
if (!result) { return { payload: "等待首次唤醒..." };}
var remaining = result.metadata.remaining;var hours = Math.floor(remaining / 3600);var minutes = Math.floor((remaining % 3600) / 60);
var display = "";
if (result.command === "1") { display = "🟢 正在投料中...";} else if (remaining <= 0) { display = "🟡 等待 ESP32 下次唤醒触发";} else { display = "⏳ 下次投料: " + result.metadata.nextDosing + "\n 还剩 " + hours + " 小时 " + minutes + " 分钟";}
return { payload: display };Extended Algorithm: Multi-Zone Scheduling
Section titled “Extended Algorithm: Multi-Zone Scheduling”// Function: 多区域独立调度// 每个容器有自己的投料计划
var zonesConfig = { container_A: { interval: 86400, // 每天 duration: 3000, // 3 秒 enabled: true }, container_B: { interval: 172800, // 每 2 天 duration: 5000, // 5 秒 enabled: true }, container_C: { interval: 604800, // 每周 duration: 3000, // 3 秒 enabled: false // 暂时禁用 }};
// 根据 MQTT 消息中的区域标识动态选择配置var zone = flow.get("activeZone") || "container_A";var config = zonesConfig[zone];
if (!config || !config.enabled) { node.warn("Zone " + zone + " is disabled or not configured"); return null;}
// 使用该区域的配置执行时间差算法 (见上)# 1. 模拟首次运行 (数据库无记录)# 删除表记录后重启# 应显示: Decision: DOSE | Reason: first_run
# 2. 测试间隔模式# 设置 interval = 60 (秒)# 每分钟唤醒检查一次# 60 秒内: Decision: SKIP | Reason: interval_not_met# 60 秒后: Decision: DOSE | Reason: interval_elapsed
# 3. 测试固定时间模式# 设置 fixedHour = 当前小时 + 1# 等到下一小时查看是否触发投料
# 4. 测试边界条件# ESP32 长时间未唤醒 (超过 2 个投料周期)# 应触发投料而不是跳过Common Customer Questions
Section titled “Common Customer Questions”Q1: 如果 ESP32 因故跳过了一次投料怎么办?
Section titled “Q1: 如果 ESP32 因故跳过了一次投料怎么办?”算法会自动补偿:timeDiff 会大于 interval,下次唤醒时立即触发投料。但为了防止连续跳过,可以在 Dashboard 中监控唤醒间隔。
Q2: 如何实现”工作日投料,周末不投料”?
Section titled “Q2: 如何实现”工作日投料,周末不投料”?”// Function: 工作日判断var dayOfWeek = new Date().getDay(); // 0=周日, 1-6=周一至周六
if (dayOfWeek === 0 || dayOfWeek === 6) { // 周末: 不投料 msg.payload = "0"; return msg;}// 工作日: 正常判断Q3: 如何确定投料是否成功?
Section titled “Q3: 如何确定投料是否成功?”结合液位变化判断:投料前测量液位 → 投料后(下次唤醒)再次测量 → 如果液位上升则投料成功。
// Function: 投料确认var beforeDosing = flow.get("levelBeforeDosing"); // 投料前液位var afterDosing = msg.payload.level; // 下次唤醒液位
var levelChange = afterDosing - beforeDosing;if (levelChange > 5) { node.log("Dosing confirmed: Level increased by " + levelChange + "%");} else { node.warn("Dosing may have failed: Level only changed by " + levelChange + "%");}✅ 推荐做法:
- 最小间隔保护 (minInterval) 防止误操作频繁投料
- 数据库查询失败时使用默认值(间隔到了就投料)
- 所有时间计算使用 Unix 时间戳 (秒),避免时区问题
- 每次决策记录到 Flow Context,便于 Dashboard 显示
- 固定时间模式需处理跨天切换逻辑
❌ 避免做法:
- 仅依赖 ESP32 的时间判断 (断电后丢失)
- 未处理数据库空结果或查询异常
- 固定时间模式忽略跨天重置
- 多个区域共用同一个投料状态变量
Summary
Section titled “Summary”- 核心算法: 比较 lastTimestamp 和 currentTime 的差值
- 三种模式: 首次运行 / 固定时间 / 间隔模式
- 安全保护: minInterval 防止过于频繁投料
- 下次投料预估: 基于算法计算剩余时间并显示
- 投料确认: 通过液位变化验证投料执行