跳转到内容

定时算法实现

定时算法实现

本节详细介绍投料系统的核心算法——时间差计算和投料决策逻辑。学习完成后,您将能够:

  • 实现基于数据库历史记录的时间差算法
  • 处理首次运行、间隔计算、边界条件等场景
  • 实现灵活的投料周期配置
  • 为多区域投料设计独立的调度算法
输入:
- lastTimestamp: 数据库中的上次投料时间 (Unix 秒)
- currentTime: 当前时间 (Unix 秒)
- dosingInterval: 投料间隔 (秒, 可配置)
算法逻辑:
if lastTimestamp == 0 (首次运行):
结果 = 投料
elif (currentTime - lastTimestamp) >= dosingInterval:
结果 = 投料
elif 固定时间模式 and 当前时间 == 设定时间:
结果 = 投料
else:
结果 = 不投料
输出:
- dosingCommand: "1" (投料) 或 "0" (不投料)
- timeRemaining: 下次投料剩余时间 (秒)
- nextDosing: 下次投料预估时间
// 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 Context
flow.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;
// 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 };
// 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;
}
// 使用该区域的配置执行时间差算法 (见上)
Terminal window
# 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 个投料周期)
# 应触发投料而不是跳过

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;
}
// 工作日: 正常判断

结合液位变化判断:投料前测量液位 → 投料后(下次唤醒)再次测量 → 如果液位上升则投料成功。

// 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 的时间判断 (断电后丢失)
  • 未处理数据库空结果或查询异常
  • 固定时间模式忽略跨天重置
  • 多个区域共用同一个投料状态变量
  1. 核心算法: 比较 lastTimestamp 和 currentTime 的差值
  2. 三种模式: 首次运行 / 固定时间 / 间隔模式
  3. 安全保护: minInterval 防止过于频繁投料
  4. 下次投料预估: 基于算法计算剩余时间并显示
  5. 投料确认: 通过液位变化验证投料执行