Peripheral Execution Business Flow
Peripheral Execution Business Flow
Current Version Notes
- The Lite edition enables PeriphExec by default but disables Command Script and RuleScript; script-related flows apply to Standard, Full, or custom builds.
- Sensor read actions write the latest reading to local cache and produce a
sensor_cachedata event; event triggers can useds:<peripheralID>_<field>as data source, e.g.,ds:dht_01_temperature. /api/periph-execreturnssensorSources; the frontend "Local Sensor" list only shows sensor collection sources already configured by peripheral execution rules.- The default configuration no longer includes a dedicated buzzer peripheral or buzzer preset actions;
actionType=20is retained as a historical placeholder only.
The frontend peripheral execution page displays rule enable status, trigger count, action count, and manual execution entry; the backend business flow corresponds to the page's CRUD, enable/disable, and run-once operations.
The chain diagram helps establish reading order: first see how trigger sources hit rules, then how the action queue executes, and finally how execution results feed back to logs, state, and protocol reporting.
The lifecycle diagram helps understand state changes from disabled draft, config validation, manual execution, enabled running, to exception rollback; the CRUD, scheduling, and WorkerPool details below all map to this chain.
The internal structure diagram connects the following sections: Manager handles rules and config, Scheduler handles trigger matching, Executor handles action dispatch, and WorkerPool handles slow actions and script tasks.
Table of Contents
- 1. Module Overview
- 2. Data Model
- 3. Rule Lifecycle (CRUD)
- 4. Trigger Types In Detail
- 5. Action Types In Detail
- 6. Trigger-Action Execution Flow
- 7. Core Method Analysis
- 8. Async Execution Engine
- 9. Data Conversion Pipeline & Report Control
- 10. Button Event Subsystem
- 11. Config Persistence & Version Migration
- 12. API Route Layer
- 13. Known Issues & Optimization Suggestions
1. Module Overview
PeriphExec (Peripheral Execution) is the rule engine module of FastBee-Arduino IoT devices, implementing "execute actions when conditions are met" automation logic.
Core Architecture
┌─────────────────────────────────────────────────────────────┐
│ Trigger Sources │
│ │
│ MQTT Msg │ Timer │ System Event │ Poll Data │ Button │
└─────┬──────┴────┬─────┴─────┬──────┴─────┬──────┴─────┬─────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ PeriphExecManager (Singleton) │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌────────────────────┐ │
│ │ Rule │ │ Condition │ │ Action Dispatch │ │
│ │ Storage │ │ Evaluation │ │ sync / async │ │
│ │ map<id, │ │ evaluate │ │ dispatch │ │
│ │ rule> │ │ Condition() │ │ │ │
│ └──────────┘ └──────────────┘ └────────────────────┘ │
│ │
│ ┌──────────────────┐ ┌──────────────────────────────┐ │
│ │ Persistence │ │ Data Report │ │
│ │ (LittleFS) │ │ (MQTT/TCP/HTTP/CoAP) │ │
│ └──────────────────┘ └──────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘Key Design Constraints
| Constraint | Value | Description |
|---|---|---|
| Max triggers per rule | 3 | MAX_TRIGGERS_PER_RULE |
| Max actions per rule | 4 | MAX_ACTIONS_PER_RULE |
| Max concurrent async tasks | 3 | MAX_ASYNC_TASKS |
| Min heap for async task | 30000 bytes | MIN_HEAP_FOR_ASYNC |
| Script task stack size | 8192 bytes | SCRIPT_TASK_STACK |
| Normal async task stack | 4096 bytes | SIMPLE_TASK_STACK |
| Async task priority | 0 (lowest) | ASYNC_TASK_PRIORITY |
| Trigger relationship within rule | OR (any match triggers) | — |
| Action relationship within rule | Sequential (array order) | — |
2. Data Model
The rule data model serves as a reading index for this chapter: PeriphExecRule is the core object for persistence and page display; triggers determine data source; actions determine execution target; runtime state handles mutex, queue, results, and logging.
2.1 Enum Definitions
ExecTriggerType - Trigger Types
| Value | Name | Description |
|---|---|---|
| 0 | PLATFORM_TRIGGER | Platform data trigger - fires when MQTT-delivered data meets conditions |
| 1 | TIMER_TRIGGER | Timer trigger - fires at time intervals or daily fixed times |
| 4 | EVENT_TRIGGER | Event trigger - fires on system events (WiFi/MQTT/buttons, etc.) |
| 5 | POLL_TRIGGER | Poll trigger - fires when peripheral poll data meets conditions |
ExecActionType - Action Types
Action enums are defined in include/core/PeripheralExecution.h. Note enum values 11, 12 are deprecated and reserved.
| Value | Enum Name | Category | actionValue Meaning |
|---|---|---|---|
| 0 | ACTION_HIGH | GPIO | Ignored, set target pin HIGH |
| 1 | ACTION_LOW | GPIO | Ignored, set target pin LOW |
| 2 | ACTION_BLINK | GPIO | Blink interval ms (default 500) |
| 3 | ACTION_BREATHE | GPIO | Breathe cycle ms (default 2000) |
| 4 | ACTION_SET_PWM | GPIO | PWM duty cycle 0-255 |
| 5 | ACTION_SET_DAC | GPIO | DAC output value 0-255 |
| 6 | ACTION_SYS_RESTART | System | Ignored, restart device after 500ms delay |
| 7 | ACTION_SYS_FACTORY_RESET | System | Ignored, format LittleFS then restart |
| 8 | ACTION_SYS_NTP_SYNC | System | Ignored, trigger NTP time sync |
| 9 | ACTION_SYS_OTA | System | OTA firmware update URL (empty uses configured default URL) |
| 10 | ACTION_CALL_PERIPHERAL | Linkage | Call other peripheral, actionValue is optional parameter |
| 13 | ACTION_HIGH_INVERTED | GPIO | Ignored, bitwise-inverted then output HIGH (logic HIGH = physical LOW) |
| 14 | ACTION_LOW_INVERTED | GPIO | Ignored, bitwise-inverted then output LOW (logic LOW = physical HIGH) |
| 15 | ACTION_SCRIPT | Script | Command sequence script text, multi-line commands executed line by line |
| 16 | ACTION_MODBUS_COIL_WRITE | Modbus | Coil write (FC05), format slave,addr,value |
| 17 | ACTION_MODBUS_REG_WRITE | Modbus | Register write (FC06), format slave,addr,value |
| 18 | ACTION_MODBUS_POLL | Modbus | Poll slave collection + optional control command, JSON or comma list |
| 19 | ACTION_SENSOR_READ | Collection | JSON, read sensor data and report |
| 20 | ACTION_RESERVED_20 | Reserved | Legacy buzzer preset action placeholder |
| 21 | ACTION_TRIGGER_EVENT | Event | Trigger device event, targetPeriphId is event ID, actionValue is extra data |
| 22 | ACTION_ENABLE_EXEC_RULE | Rule | Enable specified peripheral execution rule, targetPeriphId is rule ID |
| 23 | ACTION_DISABLE_EXEC_RULE | Rule | Disable specified peripheral execution rule, targetPeriphId is rule ID |
| 24 | ACTION_DISPLAY_NUMBER | Display | 7-segment/display shows number, e.g., 12.34 / 12:34 / 1234, supports ${id.field} template |
| 25 | ACTION_DISPLAY_TEXT | Display | 7-segment/display shows text, e.g., PLAY, ON |
| 26 | ACTION_DISPLAY_CLEAR | Display | 7-segment/display clear screen |
| 27 | ACTION_OLED_DISPLAY | Display | OLED custom multi-line display, supports ${id.field} and $value template; first line # prefix = centered title |
ExecOperator - Condition Operators
| Value | Name | Description | Data Type |
|---|---|---|---|
| 0 | OP_EQ | Equals | Numeric/String |
| 1 | OP_NEQ | Not equals | Numeric/String |
| 2 | OP_GT | Greater than | Numeric |
| 3 | OP_LT | Less than | Numeric |
| 4 | OP_GTE | Greater than or equal | Numeric |
| 5 | OP_LTE | Less than or equal | Numeric |
| 6 | OP_BETWEEN | Within range | Numeric (compareValue="min,max") |
| 7 | OP_NOT_BETWEEN | Outside range | Numeric (compareValue="min,max") |
| 8 | OP_CONTAIN | Contains substring | String |
| 9 | OP_NOT_CONTAIN | Does not contain substring | String |
2.2 Core Data Structures
ExecTrigger - Trigger
struct ExecTrigger {
int triggerType; // ExecTriggerType enum value
String triggerPeriphId; // Associated peripheral ID (PLATFORM/POLL trigger) or empty
int operatorType; // ExecOperator enum value
String compareValue; // Comparison value (numeric string/range/substring)
int timerMode; // 0=interval mode, 1=daily fixed time mode
int intervalSec; // Interval seconds (timerMode=0)
String timePoint; // Fixed time "HH:MM" (timerMode=1)
String eventId; // Event ID (EVENT_TRIGGER specific)
int pollResponseTimeout; // Poll response timeout ms (POLL trigger)
int pollMaxRetries; // Poll max retries
int pollInterPollDelay; // Poll interval delay ms
unsigned long lastTriggerTime; // Runtime: last trigger timestamp
int triggerCount; // Runtime: cumulative trigger count
};ExecAction - Action
struct ExecAction {
String targetPeriphId; // Target peripheral ID
int actionType; // ExecActionType enum value
String actionValue; // Action parameter value
bool useReceivedValue; // true=use trigger received value instead of actionValue
int syncDelayMs; // Post-execution delay ms (for inter-action interval, max 10000)
};PeriphExecRule - Rule
struct PeriphExecRule {
String id; // Unique ID "exec_<millis>"
String name; // Rule name
bool enabled; // Whether enabled
int execMode; // 0=async preferred, 1=forced sync
std::vector<ExecTrigger> triggers; // Trigger list (OR logic, max 3)
std::vector<ExecAction> actions; // Action list (sequential, max 4)
int protocolType; // Data conversion protocol type
String scriptContent; // Script content
bool reportAfterExec; // Whether to report device data after execution
};2.3 Event System
The system predefines 37 static events (STATIC_EVENTS[]), categorized as follows:
| Category | Event ID Range | Example Events |
|---|---|---|
| WiFi | 1-3 | CONNECTED, DISCONNECTED, RECONNECTING |
| MQTT | 10-13 | CONNECTED, DISCONNECTED, RECONNECTING, MESSAGE_RECEIVED |
| Network | 20-22 | IP_OBTAINED, DNS_RESOLVED, SIGNAL_CHANGED |
| Protocol | 30-34 | MODBUS_OK/ERROR/TIMEOUT, TCP_CONNECTED/DISCONNECTED |
| System | 40-43, 70-73 | BOOT_COMPLETE, LOW_MEMORY, WATCHDOG_TRIGGERED, TIME_SYNCED, HEAP_LOW/CRITICAL, TASK_WATCHDOG, STACK_OVERFLOW |
| Provision | 50-53 | STARTED, COMPLETED, FAILED, RESET |
| Rule | 60-61 | RULE_TRIGGERED, RULE_ERROR |
| Button | 80-86 | SINGLE_CLICK, DOUBLE_CLICK, LONG_PRESS_{2,5,10}S, RELEASED, STATE_CHANGED |
| PeriphExec | 90 | EXEC_COMPLETED |
| Data | 100-101 | DATA_REPORTED, DATA_REPORT_FAILED |
3. Rule Lifecycle (CRUD)
3.1 Create Rule (addRule)
Request → Parameter Validation → ID Generation → Uniqueness Check → Limit Check → Runtime Field Reset → Store → PersistDetailed Flow:
- Mutex protection: Acquire
rulesMutexfor thread safety - ID generation: If no ID provided, auto-generate
"exec_" + String(millis()) - Uniqueness check: Look up in
rulesmap for existing ID - Trigger count limit: Max
MAX_TRIGGERS_PER_RULE(3), truncate if exceeded - Action count limit: Max
MAX_ACTIONS_PER_RULE(4), truncate if exceeded - Runtime field reset: Each trigger's
lastTriggerTime = 0,triggerCount = 0 - Write to map:
rules[rule.id] = rule - Persist: API layer calls
saveConfiguration()to write to LittleFS
3.2 Update Rule (updateRule)
Request → ID Validation → Existence Check → Field Merge → Runtime State Preservation → Replace → PersistKey Design - Runtime State Preservation:
When updating a rule, each trigger's lastTriggerTime and triggerCount are preserved by index matching:
for (size_t i = 0; i < rule.triggers.size() && i < existingTriggers.size(); i++) {
rule.triggers[i].lastTriggerTime = existingTriggers[i].lastTriggerTime;
rule.triggers[i].triggerCount = existingTriggers[i].triggerCount;
}This ensures that after updating rule configuration, timers do not immediately re-trigger and trigger counts are not lost.
3.3 Delete Rule (removeRule)
Request → ID Validation → Existence Check → Remove from map → Persist3.4 Enable/Disable Rule (enableRule / disableRule)
Directly modifies the rule.enabled flag, then persists. Disabled rules do not participate in any trigger matching.
3.5 Manual Execution (runOnce)
Request → ID Validation → Get Rule → Skip Trigger Matching → Execute All Actions Directly → Trigger Device State ReportManual execution does not check if the rule is enabled and does not check trigger conditions; it directly executes the action list. The API layer additionally triggers MQTT publishDeviceInfo() for state reporting.
4. Trigger Types In Detail
4.1 PLATFORM_TRIGGER (Platform Data Trigger, type=0)
Trigger Source: MQTT downstream messages (platform-delivered commands)
Matching Flow:
MQTT message arrives → handleMqttMessage(periphId, value)
│
├─ Phase 1 (locked): Iterate all enabled rules
│ ├─ Iterate rule triggers
│ │ ├─ triggerType == PLATFORM_TRIGGER?
│ │ ├─ triggerPeriphId matches periphId? (empty ID matches any)
│ │ ├─ evaluateCondition(value, operatorType, compareValue)?
│ │ └─ Debounce check: time since last trigger > 1000ms?
│ │ → Match: update lastTriggerTime, triggerCount++
│ └─ Collect into matchedRules[]
│
└─ Phase 2 (unlocked): Iterate matchedRules[]
└─ dispatchAsync(rule, receivedValue) or executeAllActions(rule, value)Key Details:
- When
triggerPeriphIdis empty, matches any peripheral's messages - Multiple PLATFORM_TRIGGERs in the same rule are OR logic; any match triggers
- 1-second debounce: same trigger will not re-fire within 1 second
4.2 TIMER_TRIGGER (Timer Trigger, type=1)
Trigger Source: checkTimers() function, called by main loop every second
Two Timer Modes:
Interval Mode (timerMode=0)
checkTimers() runs every second
│
├─ Rule enabled && triggerType == TIMER_TRIGGER && timerMode == 0?
├─ lastTriggerTime == 0? → Immediate trigger (first run)
├─ (current time - lastTriggerTime) >= intervalSec * 1000?
│ → Match: lastTriggerTime = now, triggerCount++
└─ Collect then Phase 2 executes actions- Immediate first trigger: When
lastTriggerTime == 0, does not wait for interval, executes immediately - Minimum interval controlled by
intervalSec(default 60 seconds)
Daily Fixed Time Mode (timerMode=1)
checkTimers() runs every second
│
├─ Rule enabled && triggerType == TIMER_TRIGGER && timerMode == 1?
├─ Parse timePoint "HH:MM" → target hour, minute
├─ Current hour:minute == target?
│ ├─ Time since last trigger > 60 seconds? (60s anti-repeat window)
│ │ → Match: lastTriggerTime = now, triggerCount++
│ └─ Otherwise skip (already triggered this minute)
└─ Collect then Phase 2 executes actions- 60-second anti-repeat: Daily fixed time mode uses a 60-second window to prevent re-triggering within the same minute
- Requires NTP time sync to function correctly
4.3 EVENT_TRIGGER (Event Trigger, type=4)
Trigger Source: Internal system events (WiFi/MQTT/buttons/protocol, etc.)
Event Trigger Chain:
System event occurs (e.g., WiFi connection)
│
├─ triggerEvent(EventType type)
│ ├─ Find matching eventId in STATIC_EVENTS[]
│ └─ Call triggerEventById(eventId)
│
├─ triggerEventById(eventId)
│ ├─ Phase 1 (locked): Iterate all enabled rules
│ │ ├─ Iterate triggers: type == EVENT_TRIGGER && eventId matches?
│ │ ├─ Debounce: time since last trigger > 1000ms?
│ │ └─ Match → add to matchedRules[]
│ │
│ └─ Phase 2 (unlocked): Execute matched rule actions
│
└─ triggerPeriphExecEvent(eventId) // For peripheral execution rule-triggered events
└─ Same as above, for chain triggering on rule completion/errorSpecial Event Handling:
- Button events use dedicated
triggerButtonEvent()with debounce window shortened to 100ms EVENT_PERIPH_EXEC_COMPLETED(ID=90) auto-triggers when async tasks complete, supporting rule chain execution
4.4 POLL_TRIGGER (Poll Trigger, type=5)
Trigger Source: Peripheral poll data callback
Matching Flow:
Peripheral poll complete → handlePollData(periphId, value)
│
├─ Phase 1 (locked): Iterate all enabled rules
│ ├─ Iterate triggers: type == POLL_TRIGGER?
│ ├─ triggerPeriphId matches? (empty ID matches any)
│ ├─ evaluateCondition(value, operatorType, compareValue)?
│ ├─ Debounce: time since last trigger > 1000ms?
│ └─ Match → update count and add to list
│
└─ Phase 2 (unlocked): Execute matched rule actionsPoll trigger specific parameters:
pollResponseTimeout: Poll response timeout (default 1000ms)pollMaxRetries: Max retries (default 2)pollInterPollDelay: Poll interval delay (default 100ms)
Timed Poll Initiation:
checkTimers() also checks POLL_TRIGGER triggers, actively initiating poll requests at intervalSec intervals; poll results enter the handlePollData() flow via callback.
5. Action Types In Detail
Action dispatch logic is in PeriphExecExecutor::executeAllActions(), routed by ExecActionType enum value to corresponding internal handler functions. Grouped by function below.
5.1 GPIO Direct Control (0-5, 13, 14)
Operates hardware pins via PeripheralManager (including inverted mode), records results for subsequent reporting.
| Action | Implementation | actionValue |
|---|---|---|
ACTION_HIGH (0) | pm.setPeripheralState(id, true) | Ignored |
ACTION_LOW (1) | pm.setPeripheralState(id, false) | Ignored |
ACTION_BLINK (2) | pm.blinkPeripheral(id, intervalMs) | Blink interval ms (default 500) |
ACTION_BREATHE (3) | pm.breathePeripheral(id, periodMs) | Breathe cycle ms (default 2000) |
ACTION_SET_PWM (4) | pm.setPWMValue(id, duty) | Duty cycle 0-255 |
ACTION_SET_DAC (5) | pm.setDACValue(id, value) | DAC value 0-255 (GPIO25/26) |
ACTION_HIGH_INVERTED (13) | Inverted then setPeripheralState(true) | Logic HIGH maps to physical LOW, for active-low relays |
ACTION_LOW_INVERTED (14) | Inverted then setPeripheralState(false) | Logic LOW maps to physical HIGH |
5.2 System Actions (6-9)
Executed via executeSystemAction(), affecting the entire device:
| Action | Implementation | Async Strategy |
|---|---|---|
ACTION_SYS_RESTART (6) | delay(500); ESP.restart() | Forced sync (async task cannot complete after restart) |
ACTION_SYS_FACTORY_RESET (7) | Format LittleFS + restart | Forced sync |
ACTION_SYS_NTP_SYNC (8) | configTime(gmtOffset, daylightOffset, ntpServer) re-sync | Can be async |
ACTION_SYS_OTA (9) | Trigger OTAManager firmware upgrade, actionValue optional as URL | Can be async |
Important: In dispatchAsync(), ACTION_SYS_RESTART and ACTION_SYS_FACTORY_RESET are automatically downgraded to synchronous execution.
Note: Enum values 11, 12 were originally reserved for ACTION_AP_MODE and ACTION_BLE_TOGGLE; after the project adopted AP+STA dual-mode auto-switching, they were removed. These values are now reserved and unusable.
5.3 Peripheral Linkage (10)
ACTION_CALL_PERIPHERAL (10)
Calls other peripherals' high-level behavior methods; targetPeriphId is the target peripheral ID, actionValue is optional parameter string or JSON. This action does not directly operate GPIO but triggers behaviors defined in the peripheral driver (e.g., trigger LCD redraw, control stepper motor, etc.).
ULN2003 stepper motor (STEPPER_MOTOR) supports the following JSON commands:
| actionValue | Behavior |
|---|---|
{"periphId":"stepper","action":"forward"} | Forward |
{"periphId":"stepper","action":"reverse"} | Reverse |
{"periphId":"stepper","action":"stop"} | Stop and release coils |
{"periphId":"stepper","action":"faster","value":"2"} | Increase by 2 RPM each time |
{"periphId":"stepper","action":"slower","value":"2"} | Decrease by 2 RPM each time |
{"periphId":"stepper","action":"setSpeed","value":"12"} | Set to 12 RPM |
5.4 Script Action (15)
ACTION_SCRIPT (15)
Executes multi-line command scripts via executeScriptAction(). actionValue is the script text (not scriptContent). For script syntax, see script-guide.md command script section:
GPIO 5 HIGH
DELAY 500
PERIPH relay_1 HIGH
MQTT 0 [{"id":"alarm","value":"1"}]Scripts execute in async tasks (SCRIPT_TASK_STACK=8192), supporting PERIPH/GPIO/DELAY/PWM/DAC/LOG/MQTT commands and RANDOM()/RANDOMF() expressions.
5.5 Modbus Communication Actions (16-18)
ACTION_MODBUS_COIL_WRITE (16)
Write single Modbus coil (function code FC05), for boolean output.
actionValue format: slave,addr,value
slave: slave address 1-247
addr: coil address 0-65535
value: 0 or 1ACTION_MODBUS_REG_WRITE (17)
Write single holding register (function code FC06), for numeric output.
actionValue format: slave,addr,value
slave: slave address
addr: register address
value: 0-65535ACTION_MODBUS_POLL (18)
Executed by executeModbusPollAction(), supports batch polling and optional control commands, two data formats:
JSON format (recommended):
{
"poll": [0, 1],
"ctrl": [
{"type":"relay","idx":0,"val":1},
{"type":"pwm","idx":0,"val":128},
{"type":"pid","idx":0,"sp":25.0}
]
}Legacy comma-separated format (poll only, no control):
0,1,2Poll parameters pollResponseTimeout, pollMaxRetries, pollInterPollDelay are configured in the trigger structure.
5.6 Sensor Collection (19)
ACTION_SENSOR_READ (19)
Executed by executeSensorReadAction(), actionValue is a JSON configuration string.
Analog/Digital Sensors:
{
"periphId": "sensor_01",
"sensorCategory": "analog",
"scaleFactor": 1.0,
"offset": 0.0,
"decimalPlaces": 2,
"sensorLabel": "Light",
"unit": "lux"
}DHT11/DHT22:
{
"periphId": "dht_01",
"sensorCategory": "dht11",
"dataField": "temperature",
"scaleFactor": 1.0,
"offset": 0.0,
"decimalPlaces": 1,
"sensorLabel": "Temperature",
"unit": "°C"
}DS18B20:
{
"periphId": "ds18b20_01",
"sensorCategory": "ds18b20",
"deviceIndex": 0,
"scaleFactor": 1.0,
"offset": 0.0,
"decimalPlaces": 2,
"sensorLabel": "Water Temp",
"unit": "°C"
}Supported sensorCategory:
| Value | Sensor Type | Read Method | Characteristics |
|---|---|---|---|
analog | Analog input (ADC) | analogRead() | 0-4095 raw value |
digital | Digital input (GPIO) | digitalRead() | 0/1 |
pulse | Pulse/frequency | Reserved | Not implemented |
dht11 | DHT11 temp/humidity | Single-bus timing | Temp 0-50°C, humidity 20-90% |
dht22 | DHT22/AM2302 | Single-bus timing | Temp -40~80°C, humidity 0-100% |
ds18b20 | DS18B20 digital temp | OneWire protocol | -55~125°C, 12-bit precision |
ultrasonic | HC-SR04 ultrasonic | Trig+Echo | Output distance, unit cm |
current | Current sensor | ADC + linear calibration | Output current, supports ACS712, etc. |
voltage | Voltage sensor | ADC + voltage divider restoration | Output voltage |
SHT31 | SHT31 temp/humidity | I2C lightweight driver | Output temperature, humidity |
AHT20 | AHT20 temp/humidity | I2C lightweight driver | Output temperature, humidity |
BH1750 | BH1750 light | I2C lightweight driver | Output illuminance, unit lx |
BMP280 | BMP280 pressure | I2C advanced driver | Output temperature, pressure, altitude, requires FASTBEE_ENABLE_I2C_SENSORS |
MPU6050 | MPU6050 motion | I2C advanced driver | Output acceleration/temperature/angular velocity, requires FASTBEE_ENABLE_I2C_SENSORS |
Non-blocking guarantee: DHT11 read ~25ms, DS18B20 conversion ~750ms; always executed in independent async tasks, non-blocking to main loop; driver has built-in cache (DHT 2s / DS18B20 1s) to prevent over-frequent reads.
5.7 Peripheral Action (20)
ACTION_RESERVED_20 (20)
Historical reserved slot. Legacy dedicated buzzer preset action removed; new configurations should not use actionType=20.
5.8 Event & Rule Control (21-23)
ACTION_TRIGGER_EVENT (21)
Actively triggers device events, usable for:
- Triggering system built-in events (
targetPeriphIdis event ID, e.g.,sys_breakdown,sys_alarm,low_power) - Triggering custom events defined by
DEVICE_EVENTperipherals
actionValue is passed as extra event data, matchable by other EVENT_TRIGGER rules or reported to the platform via MQTT.
ACTION_ENABLE_EXEC_RULE (22) / ACTION_DISABLE_EXEC_RULE (23)
Enable or disable specified peripheral execution rules at runtime; targetPeriphId is the target rule's ID (format exec_xxxxx). Enables orchestration like "one rule temporarily disables another":
- Disable all timer rules when entering maintenance mode
- Enable collection rules during daytime, disable at night
- Chain protection: after triggering alarm rule, auto-disable actions that would disturb the scene
5.9 Display Actions (24-27)
Display actions are uniformly driven by the display controller (TM1637 7-segment / OLED). All actions support ${periphId.field} placeholders to read sensor cache; some support $value for trigger-received values.
ACTION_DISPLAY_NUMBER (24)
Display numbers on 7-segment or OLED, actionValue format examples:
12.34 # Display 12.34
12:34 # Display 12:34 (colon lit)
1234 # Display 1234
${dht_01.temperature} # Display DHT11 real-time temperature
$value # Display trigger received value (with useReceivedValue)ACTION_DISPLAY_TEXT (25)
Display short text on 7-segment/OLED (7-segment max 4 characters); limited by segment code table, only some letters are recognizable (A/b/C/d/E/F/H/L/o/P/U, etc.).
ON # On
OFF # Off
PLAY # Play
${dht_01.temperature.unit} # Display temperature unitACTION_DISPLAY_CLEAR (26)
Clear display content, turn off all segments. No actionValue needed.
ACTION_OLED_DISPLAY (27)
OLED dedicated multi-line custom display, rendered via LCDManager::showCustomText():
- Content split into multi-line by
\n - First line starting with
#is recognized as centered title with separator line drawn - Other lines are left-aligned by default
- Supports
${periphId.field}and$valuetemplates - Auto-truncated after max lines exceeded
actionValue example:
# Environment Monitor
Temp: ${dht_01.temperature}°C
Humidity: ${dht_01.humidity}%
Water: ${ds18b20_01.temperature}°C
Status: Normal6. Trigger-Action Execution Flow
6.1 Two-Phase Lock Pattern
This is the most core design pattern in PeriphExec; all trigger entry points (handleMqttMessage, handlePollData, triggerEvent, triggerButtonEvent) use this pattern:
The key to the two-phase lock is "hold lock only for matching and snapshot, execute actions after releasing lock". When debugging concurrency issues, first distinguish between rule set lock and peripheral resource lock, avoiding putting time-consuming peripheral operations inside the rule lock scope.
Phase 1 - Locked Phase (Rule Matching)
┌──────────────────────────────────────┐
│ RecursiveMutexGuard lock(mutex) │
│ │
│ for (rule : rules) { │
│ if (!rule.enabled) continue; │
│ for (trigger : rule.triggers) { │
│ if (matchCondition()) { │
│ matchedRules.push_back(rule); │
│ break; // OR logic, one match │
│ } │
│ } │
│ } │
│ │
│ // Lock auto-released │
└──────────────────────────────────────┘
│
▼
Phase 2 - Unlocked Phase (Action Execution)
┌──────────────────────────────────────┐
│ // No lock here, actions may be │
│ // time-consuming │
│ │
│ for (rule : matchedRules) { │
│ dispatchAsync(rule, value); │
│ } │
└──────────────────────────────────────┘Design Purpose: Lock is held only for lightweight condition matching; actions execute after lock release, avoiding long lock holds that block other threads.
6.2 Condition Evaluation (evaluateCondition)
bool evaluateCondition(String& receivedValue, int operatorType, String& compareValue)Evaluation Logic:
receivedValue and compareValue
│
├─ CONTAIN (8): receivedValue.indexOf(compareValue) >= 0
├─ NOT_CONTAIN (9): receivedValue.indexOf(compareValue) < 0
│
├─ Convert to float: recvFloat, compFloat
│
├─ EQ (0): recvFloat == compFloat
├─ NEQ (1): recvFloat != compFloat
├─ GT (2): recvFloat > compFloat
├─ LT (3): recvFloat < compFloat
├─ GTE (4): recvFloat >= compFloat
├─ LTE (5): recvFloat <= compFloat
│
├─ BETWEEN (6): Parse "min,max" → recvFloat >= min && recvFloat <= max
└─ NOT_BETWEEN (7): Parse "min,max" → recvFloat < min || recvFloat > max6.3 Sequential Action Execution (executeAllActions)
executeAllActions(rule, receivedValue)
│
for (action : rule.actions) {
│
├─ useReceivedValue == true?
│ └─ Replace actionValue with receivedValue (data passthrough)
│
├─ Dispatch by actionType to corresponding handler:
│ ├─ 0-5, 13, 14 → executePeripheralAction() // GPIO & inverted output
│ ├─ 6-9 → executeSystemAction() // System actions
│ ├─ 10 → Peripheral linkage (CALL_PERIPHERAL)
│ ├─ 15 → executeScriptAction() // Command sequence script
│ ├─ 16, 17 → executeModbusAction() // FC05/FC06 write
│ ├─ 18 → executeModbusPollAction() // Poll + control
│ ├─ 19 → executeSensorReadAction() // Sensor collection
│ ├─ 20 → reserved // Historical reserved slot
│ ├─ 21 → triggerEvent() // Trigger device event
│ ├─ 22, 23 → enable/disableRule() // Rule enable/disable
│ └─ 24-27 → executeDisplayAction() // 7-segment/OLED display
│
├─ syncDelayMs > 0? (max 10000ms)
│ └─ delay(syncDelayMs) // Inter-action delay
│
} // Next action
│
├─ reportAfterExec == true?
│ └─ reportActionResults() → Report execution results
│
└─ triggerPeriphExecEvent(EVENT_PERIPH_EXEC_COMPLETED)
└─ Trigger chain rules6.4 Complete Rule Execution Lifecycle
Rule Create/Update
└─ addRule() / updateRule() → saveConfiguration() → LittleFS persistence
│
▼
During Device Runtime
│
├─ MQTT message arrives → handleMqttMessage()
│ └─ PLATFORM_TRIGGER match → condition evaluation → debounce → action execution
│
├─ Every second checkTimers()
│ ├─ TIMER_TRIGGER: interval/fixed-time check → action execution
│ └─ POLL_TRIGGER: initiate poll at interval
│
├─ Peripheral data callback → handlePollData()
│ └─ POLL_TRIGGER match → condition evaluation → debounce → action execution
│
├─ System event → triggerEvent()
│ └─ EVENT_TRIGGER match → debounce → action execution
│
└─ Button state machine → triggerButtonEvent()
└─ EVENT_TRIGGER match → 100ms debounce → action execution
│
▼
Action Execution Chain
├─ Sync execution: Sequential on calling thread
└─ Async execution: FreeRTOS task on Core 1
│
▼
Post-Execution Processing
├─ reportAfterExec → Report action results (MQTT)
├─ tryReportDeviceData → Report device overall state (multi-protocol)
└─ Trigger EVENT_PERIPH_EXEC_COMPLETED → May cause chain rules7. Core Method Analysis
7.1 handleDataCommand()
Location: PeriphExecManager.cpp:500-638
Function: Processes platform-delivered data commands, synchronously executes matched rules and builds response.
Difference from handleMqttMessage:
handleMqttMessageis for general MQTT data, executes asynchronouslyhandleDataCommandis for command scenarios requiring synchronous response, caller needs immediate execution result
Detailed Flow:
handleDataCommand(items[], itemCount, response)
│
├─ Step 1: Pre-process "modbus_read" type data
│ ├─ Parse modbus nodes from peripheral registry
│ ├─ Map raw modbus register values to business peripheral IDs
│ └─ Expand into individual (periphId, value) pairs
│
├─ Step 2: Iterate all data items
│ ├─ Locked: Iterate enabled rules' PLATFORM_TRIGGER
│ │ ├─ triggerPeriphId matches current periphId?
│ │ ├─ evaluateCondition(value)?
│ │ └─ Match → Synchronously execute executeAllActions()
│ │
│ ├─ After unlock: Track which data items were consumed by rules
│ └─ Unmatched items preserved as "unmatched"
│
└─ Step 3: Build response JSON
├─ Executed action results
└─ Unmatched data items (returned as-is to caller)Key Characteristics:
- Synchronous execution: Does not use
dispatchAsync(), callsexecuteAllActions()directly - Modbus preprocessing: Auto-maps low-level modbus register addresses to high-level peripheral IDs
- Unmatched tracking: Records which delivered data was not handled by any rule
7.2 checkTimers()
Location: PeriphExecManager.cpp:685-750
Function: Timer trigger check, called by main loop every second.
Detailed Flow:
checkTimers() // Called every second
│
├─ Phase 1 (locked):
│ for (rule : rules) {
│ if (!rule.enabled) continue;
│ for (trigger : rule.triggers) {
│ │
│ ├─ TIMER_TRIGGER (type=1):
│ │ ├─ timerMode == 0 (interval):
│ │ │ ├─ lastTriggerTime == 0 → Immediate match (first time)
│ │ │ └─ now - lastTriggerTime >= intervalSec * 1000 → Match
│ │ │
│ │ └─ timerMode == 1 (daily fixed):
│ │ ├─ Parse timePoint "HH:MM"
│ │ ├─ Current time == target time?
│ │ └─ now - lastTriggerTime > 60000 → Match (60s anti-repeat)
│ │
│ └─ POLL_TRIGGER (type=5):
│ └─ now - lastTriggerTime >= intervalSec * 1000
│ → Mark for poll initiation (does not directly execute actions)
│ }
│ }
│
└─ Phase 2 (unlocked):
├─ TIMER_TRIGGER match → dispatchAsync() / executeAllActions()
└─ POLL_TRIGGER expired → Initiate peripheral poll request (results via callback enter handlePollData)7.3 executeAllActions() (Core Executor)
Location: PeriphExecManager.cpp:771-815
Function: Sequentially executes all actions of a rule.
executeAllActions(rule, receivedValue)
│
├─ Initialize result collector actionResults[]
│
├─ for (i = 0; i < rule.actions.size(); i++) {
│ │
│ ├─ action = rule.actions[i]
│ │
│ ├─ useReceivedValue?
│ │ └─ effectiveValue = receivedValue (data passthrough mode)
│ │ └─ else: effectiveValue = action.actionValue
│ │
│ ├─ switch (action.actionType):
│ │ ├─ 0-5, 13, 14 → executePeripheralAction(action, effectiveValue, results)
│ │ ├─ 6-9 → executeSystemAction(action, effectiveValue)
│ │ ├─ 10 → callPeripheralAction(action)
│ │ ├─ 15 → executeScriptAction(action, effectiveValue)
│ │ ├─ 16, 17 → executeModbusAction(action, effectiveValue, results)
│ │ ├─ 18 → executeModbusPollAction(action, effectiveValue, results)
│ │ ├─ 19 → executeSensorReadAction(action, effectiveValue, results)
│ │ ├─ 20 → reserved
│ │ ├─ 21 → triggerEvent(action.targetPeriphId, effectiveValue)
│ │ ├─ 22/23 → enableRule/disableRule(action.targetPeriphId)
│ │ └─ 24-27 → executeDisplayAction(action, effectiveValue)
│ │
│ └─ syncDelayMs > 0 && syncDelayMs <= 10000?
│ └─ delay(min(syncDelayMs, 10000))
│ }
│
├─ rule.reportAfterExec && results not empty?
│ └─ reportActionResults(results) → MQTT report
│
└─ triggerPeriphExecEvent(EVENT_PERIPH_EXEC_COMPLETED)7.4 evaluateCondition()
Location: PeriphExecManager.cpp:648-681
Function: Evaluates whether trigger conditions are met.
Algorithm:
evaluateCondition(receivedValue, operatorType, compareValue)
│
├─ OP_CONTAIN (8):
│ return receivedValue.indexOf(compareValue) >= 0
│
├─ OP_NOT_CONTAIN (9):
│ return receivedValue.indexOf(compareValue) < 0
│
├─ Numeric conversion: recv = receivedValue.toFloat(), comp = compareValue.toFloat()
│
├─ OP_EQ (0): return recv == comp
├─ OP_NEQ (1): return recv != comp
├─ OP_GT (2): return recv > comp
├─ OP_LT (3): return recv < comp
├─ OP_GTE (4): return recv >= comp
├─ OP_LTE (5): return recv <= comp
│
├─ OP_BETWEEN (6):
│ Parse compareValue split by comma into min, max
│ return recv >= min && recv <= max
│
└─ OP_NOT_BETWEEN (7):
Parse compareValue split by comma into min, max
return recv < min || recv > maxNotes:
- Numeric comparison uses
float, which has floating-point precision issues (e.g., EQ comparison) - CONTAIN/NOT_CONTAIN operations use string
indexOfdirectly - BETWEEN uses comma-separated format
"min,max"
8. Async Execution Engine
8.1 Scheduling Decision (dispatchAsync)
Location: PeriphExecManager.cpp:1270-1354
dispatchAsync(rule, receivedValue)
│
├─ Check 1: Contains system action (RESTART/FACTORY_RESET)?
│ └─ YES → Force sync execution (async task would be terminated on restart)
│
├─ Check 2: User configured execMode == 1 (forced sync)?
│ └─ YES → Sync execution
│
├─ Check 3: Available heap < MIN_HEAP_FOR_ASYNC (30000)?
│ └─ YES → Downgrade to sync (insufficient memory for task creation)
│
├─ Check 4: Semaphore taskSlotSemaphore available? (max 3 concurrent)
│ └─ NO → Downgrade to sync (task slots full)
│
└─ All checks passed:
├─ Create AsyncExecContext (deep copy rule + value)
├─ Calculate stack size: includes script? SCRIPT_TASK_STACK(8192) : SIMPLE_TASK_STACK(4096)
└─ xTaskCreatePinnedToCore(asyncExecTaskFunc, ..., Core 1)8.2 Async Task Lifecycle
asyncExecTaskFunc(context) // Runs in FreeRTOS task (Core 1)
│
├─ Execute all actions: executeAllActions(context.ruleCopy, context.receivedValue)
│
├─ Record execution result: recordResult(ruleId, success, ...)
│ └─ Store in recent results list (for querying)
│
├─ Trigger completion event: triggerPeriphExecEvent(EVENT_PERIPH_EXEC_COMPLETED)
│ └─ May trigger other rules' EVENT_TRIGGER
│
├─ Release task slot: xSemaphoreGive(taskSlotSemaphore)
│
└─ Delete task: vTaskDelete(NULL)8.3 RAII Lock Protection
The project uses RAII pattern for FreeRTOS mutex management:
class MutexGuard {
SemaphoreHandle_t _mutex;
bool _locked;
public:
explicit MutexGuard(SemaphoreHandle_t m, TickType_t timeout = portMAX_DELAY);
~MutexGuard(); // Auto-release on destruction
bool locked() const;
};
class RecursiveMutexGuard {
SemaphoreHandle_t _mutex;
bool _locked;
public:
explicit RecursiveMutexGuard(SemaphoreHandle_t m, TickType_t timeout = portMAX_DELAY);
~RecursiveMutexGuard();
bool locked() const;
};Recursive mutexes are used because some call paths may nest (e.g., rule execution triggers events, events trigger other rules).
9. Data Conversion Pipeline & Report Control
9.1 Data Conversion Pipeline
Peripheral Raw Data
│
├─ useReceivedValue == true?
│ └─ Pass trigger data directly to action (data passthrough)
│
├─ ACTION_SENSOR_READ:
│ └─ rawValue → value * scaleFactor + offset → format(decimals)
│
├─ ACTION_SCRIPT (15):
│ └─ ScriptEngine executes command sequence script
│ action.actionValue → multi-line command text
│ Supports GPIO/PWM/DAC/PERIPH/MQTT/RANDOM commands
│
└─ Modbus data preprocessing (handleDataCommand):
└─ Register address → peripheral ID mapping → business value9.2 Execution Result Reporting (reportActionResults)
Location: PeriphExecManager.cpp:1609-1638
reportActionResults(results[])
│
├─ Build JSON array:
│ [{
│ "id": "periph_01", // Peripheral ID
│ "value": "1", // Execution result value
│ "remark": "GPIO HIGH" // Execution remark
│ }, ...]
│
└─ Publish via MQTT to device report topic9.3 Device State Reporting (tryReportDeviceData)
Location: PeriphExecManager.cpp:1758-1839
tryReportDeviceData()
│
├─ Check available protocols:
│ ├─ MQTT connected?
│ ├─ TCP connected?
│ ├─ HTTP available?
│ └─ CoAP available?
│
├─ Collect device data: collectPeripheralData()
│ ├─ Iterate all enabled GPIO peripherals
│ └─ Get current state (HIGH/LOW/PWM value, etc.)
│
└─ Try reporting by priority:
MQTT (preferred) → TCP → HTTP → CoAP (fallback)Protocol fallback chain: When higher-priority protocol is unavailable, automatically tries the next protocol, ensuring data is reported as much as possible.
9.4 Data Collection (collectPeripheralData)
Location: PeriphExecManager.cpp:1649-1694
collectPeripheralData()
│
├─ Get PeripheralManager instance
├─ Iterate all registered peripherals
│ ├─ Peripheral enabled?
│ ├─ Peripheral type is GPIO?
│ └─ Read current state → Add to data list
│
└─ Return [{periphId, value}] list10. Button Event Subsystem
10.1 Button State Machine
Location: PeriphExecManager.cpp:1843-1961
Button events are implemented via hardware polling and state machine, not dependent on interrupts.
State Machine Configuration:
struct ButtonEventConfig {
String periphId; // Associated GPIO peripheral ID
uint8_t pin; // GPIO pin number
bool activeLow; // Whether active-low
unsigned long debounceMs; // Debounce time (default 20ms)
};
struct ButtonRuntimeState {
bool lastStableState; // Last stable state
bool lastRawState; // Last raw reading
unsigned long lastChangeTime; // Last state change time
unsigned long pressStartTime; // Press start time
uint8_t clickCount; // Click count
unsigned long lastClickTime; // Last click time
bool longPress2sTriggered; // 2s long press triggered
bool longPress5sTriggered; // 5s long press triggered
bool longPress10sTriggered; // 10s long press triggered
};State Machine Flow (checkButtonEvents, called every 20ms):
Read GPIO pin state (digitalRead)
│
├─ State changed?
│ └─ Reset debounce timer lastChangeTime = now
│
├─ Debounce complete? (now - lastChangeTime >= debounceMs)
│ │
│ ├─ Not pressed → Pressed:
│ │ ├─ pressStartTime = now
│ │ ├─ Reset long press flags
│ │ └─ Trigger EVENT_BUTTON_STATE_CHANGED (86)
│ │
│ ├─ Holding pressed:
│ │ ├─ Press duration >= 2s && !longPress2sTriggered?
│ │ │ └─ Trigger EVENT_BUTTON_LONG_PRESS_2S (83)
│ │ ├─ Press duration >= 5s && !longPress5sTriggered?
│ │ │ └─ Trigger EVENT_BUTTON_LONG_PRESS_5S (84)
│ │ └─ Press duration >= 10s && !longPress10sTriggered?
│ │ └─ Trigger EVENT_BUTTON_LONG_PRESS_10S (85)
│ │
│ └─ Pressed → Released:
│ ├─ Trigger EVENT_BUTTON_RELEASED (85)
│ ├─ Trigger EVENT_BUTTON_STATE_CHANGED (86)
│ ├─ clickCount++
│ ├─ lastClickTime = now
│ └─ Delayed judgment:
│ ├─ now - lastClickTime > 300ms? (double-click timeout)
│ │ ├─ clickCount == 1 → Trigger EVENT_BUTTON_SINGLE_CLICK (80)
│ │ └─ clickCount >= 2 → Trigger EVENT_BUTTON_DOUBLE_CLICK (81)
│ └─ Wait for next click...10.2 Button Event Debounce
Button event triggers use 100ms debounce (vs. 1000ms for general events) because button operations typically require faster response.
11. Config Persistence & Version Migration
11.1 Storage Format (v3)
Location: PeriphExecManager.cpp:140-203
{
"version": 3,
"rules": [
{
"id": "exec_12345",
"name": "Temperature Alarm",
"enabled": true,
"execMode": 0,
"protocolType": 0,
"scriptContent": "",
"reportAfterExec": true,
"triggers": [
{
"triggerType": 0,
"triggerPeriphId": "temp_01",
"operatorType": 2,
"compareValue": "35",
"timerMode": 0,
"intervalSec": 60,
"timePoint": "",
"eventId": "",
"pollResponseTimeout": 1000,
"pollMaxRetries": 2,
"pollInterPollDelay": 100
}
],
"actions": [
{
"targetPeriphId": "relay_01",
"actionType": 0,
"actionValue": "",
"useReceivedValue": false,
"syncDelayMs": 0
}
]
}
]
}11.2 Version Migration
loadConfiguration() supports three version formats:
v1 → v3 Migration
v1 format: No version field, uses flat structure
v1 flat fields:
triggerType, triggerPeriphId, operatorType, compareValue,
timerMode, intervalSec, timePoint, eventId,
targetPeriphId, actionType, actionValue, useReceivedValue, syncDelayMs
→ Convert to triggers[single element] + actions[single element]Additional migration:
invertedfield → mapped toACTION_HIGH_INVERTED(13) orACTION_LOW_INVERTED(14) based on original action- Legacy
eventIdformat migrated to new event numbering system
v2 → v3 Migration
v2 format: version: 2, also uses flat structure
v2 same flat fields as v1 → Convert to triggers[]/actions[] arraysv3 Native Load
Directly parse triggers[] and actions[] JSON arrays.
11.3 Persistence Timing
saveConfiguration() is triggered by:
addRule()→ API layer callupdateRule()→ API layer callremoveRule()→ API layer callenableRule()/disableRule()→ API layer call
Note: Runtime state (lastTriggerTime, triggerCount) is not persisted to config file; these values reset to 0 after device restart.
12. API Route Layer
| Page Operation | REST API | Manager Method | Result Target |
|---|---|---|---|
| View rule list | GET /api/periph-exec | listRules() | Web list, runtime state |
| Create rule | POST /api/periph-exec | addRule() | periph_exec.json |
| Edit rule | PUT /api/periph-exec/{id} | updateRule() | periph_exec.json, rule runtime |
| Run once | POST /run-once | runOnce() | Execution log, MQTT report |
| Enable/Disable | POST /enable | enableRule() | Rule runtime, config file |
| Delete rule | DELETE /{id} | removeRule() | Config file, Web list |
API routes can be read in "Page Operation → REST API → Manager Method → Config/Runtime" order. When the page shows success but rules are not persisted, check API response first, then Manager logs, and finally confirm whether /config/periph_exec.json is updated.
12.1 Route Registration
Location: PeriphExecRouteHandler.cpp:14-72
| Method | Path | Handler | Permission |
|---|---|---|---|
| GET | /api/periph-exec | handleGetRules | system.view |
| POST | /api/periph-exec (JSON) | handleAddRuleJson | config.edit |
| POST | /api/periph-exec (form) | handleAddRule | config.edit |
| POST | /api/periph-exec/update (JSON) | handleUpdateRuleJson | config.edit |
| POST | /api/periph-exec/update (form) | handleUpdateRule | config.edit |
| DELETE | /api/periph-exec/ | handleDeleteRule | config.edit |
| POST | /api/periph-exec/enable | handleEnableRule | config.edit |
| POST | /api/periph-exec/disable | handleDisableRule | config.edit |
| POST | /api/periph-exec/run | handleRunOnce | config.edit |
| GET | /api/periph-exec/events/static | handleGetStaticEvents | system.view |
| GET | /api/periph-exec/events/dynamic | handleGetDynamicEvents | system.view |
| GET | /api/periph-exec/events/categories | handleGetEventCategories | system.view |
| GET | /api/periph-exec/trigger-types | handleGetTriggerTypes | system.view |
| GET | /api/periph-exec/results | handleGetRecentResults | system.view |
12.2 Route Registration Order
Route registration order is critical because AsyncCallbackJsonWebHandler uses prefix matching:
- Register specific paths first:
/events/static,/events/dynamic,/events/categories,/trigger-types - JSON handler registers update first:
/api/periph-exec/updatemust come before/api/periph-exec - Register generic paths last:
/api/periph-exec(GET/POST)
If order is reversed, POST requests to /api/periph-exec/update would be intercepted by /api/periph-exec POST handler.
12.3 Dual Format Support
Each write operation supports two request formats:
| Format | Content-Type | Handler | Description |
|---|---|---|---|
| JSON | application/json | handleAddRuleJson / handleUpdateRuleJson | Supports full triggers[]/actions[] arrays |
| Form | application/x-www-form-urlencoded | handleAddRule / handleUpdateRule | Backward compatible, single trigger/action only |
JSON handlers are registered via AsyncCallbackJsonWebHandler with higher priority than form handlers.
12.4 JSON Parsing Helpers
parseRuleFromJson() handles unified JSON to PeriphExecRule parsing, including:
- Safe string extraction: uses
| ""to preventas<String>()returning"null"string - Integer-compatible parsing:
jsonInt()supports both JSON numbers and string format - Runtime field reset:
lastTriggerTime = 0,triggerCount = 0during parsing
12.5 Response Format
Success response:
{"success": true, "message": "Rule added"}List response:
{
"success": true,
"data": [
{
"id": "exec_12345",
"name": "...",
"triggers": [{...}],
"actions": [{...}],
"triggerPeriphName": "Temp Sensor", // Associated peripheral name (extra field)
"targetPeriphName": "Relay", // Associated peripheral name (extra field)
"targetPeriphType": 1 // Peripheral type (extra field)
}
]
}Error response:
{"success": false, "message": "Rule not found"}13. Known Issues & Optimization Suggestions
13.1 Floating-Point Precision
Location: evaluateCondition() (PeriphExecManager.cpp:648-681)
Issue: Using float for OP_EQ comparison, floating-point precision may cause false judgments. E.g., 0.1 + 0.2 != 0.3.
Suggestion: For EQ/NEQ operations, add epsilon tolerance comparison:
bool floatEq(float a, float b, float eps = 0.001) {
return fabs(a - b) < eps;
}13.2 Hardcoded Debounce Times
Issue: Event debounce times are hardcoded:
- General events: 1000ms (
handleMqttMessage,handlePollData,triggerEvent) - Button events: 100ms (
triggerButtonEvent) - Daily timer: 60000ms (
checkTimersdaily fixed mode)
Suggestion: Extract debounce times as configurable parameters, or add debounceMs field to trigger structure for per-trigger configuration.
13.3 TIMER_TRIGGER First Immediate Trigger
Issue: In interval mode, lastTriggerTime == 0 causes all timer rules to fire immediately after device restart, which may not be desired behavior in some scenarios (e.g., triggering alarms immediately after restart).
Suggestion: Add config option skipFirstTrigger to let users choose whether to skip first trigger after restart. Or initialize lastTriggerTime to millis() in addRule().
13.4 No Retry on Async Task Failure
Issue: If action execution fails in asyncExecTaskFunc, results are recorded and completion event is triggered, but there is no retry mechanism.
Suggestion: For communication actions (Modbus/MQTT/HTTP), add configurable retry count and backoff strategy.
13.5 No Rule Count Upper Limit
Issue: The rules map has no maximum capacity limit. On ESP32's limited memory, a large number of rules may cause memory exhaustion.
Suggestion: Add MAX_RULES limit (e.g., 20-50), check in addRule().
13.6 Config Save Frequency
Issue: Every CRUD operation triggers saveConfiguration(), writing to LittleFS flash. Frequent writes shorten flash lifespan.
Suggestion: Implement delayed writing (dirty flag + timed save), or merge saves during batch operations.
13.7 Two-Phase Lock Timing Risk
Issue: After Phase 1 collects matched rules and releases lock, rules may have been deleted or modified during Phase 2 execution. Currently mitigated by copying rule data, but edge cases exist in high-frequency trigger scenarios.
Current Status: Code addresses this by copying rule data to matched list in Phase 1, but for non-async paths calling executeAllActions, rule references are still used, with potential race conditions.
13.8 System Action Safety
Issue: ACTION_FACTORY_RESET can be auto-triggered by rules; if conditions are misconfigured (e.g., event chain circular trigger), it may cause unexpected factory reset.
Suggestion: Add secondary confirmation mechanism or execution count limit for destructive system actions (RESTART/FACTORY_RESET).
13.9 Button Double-Click Detection Delay
Issue: Double-click determination requires waiting for 300ms timeout to confirm single vs. double click, meaning single-click response always has 300ms delay.
Suggestion: Provide config option for users to choose between "single-click response speed" and "double-click support". Disable double-click detection for scenarios that don't need it to get faster single-click response.
13.10 Legacy Reserved Enums & Version Compatibility
Empty slots 11, 12 in ExecActionType were early version reserved for ACTION_AP_MODE and ACTION_BLE_TOGGLE; removed after adopting AP+STA dual-mode auto-switching; definitions removed simultaneously to prevent new actions from reusing these values. Configurations upgraded from older versions with residual actionType=11 or 12 will be warned and skipped in the switch default branch at runtime.
Suggestion: When updating UI and API dropdowns, ensure values 11 and 12 are not exposed; force-map to alternative actions (e.g., EVENT_TRIGGER + WiFi-related event) on update.
13.11 Implemented Performance Optimizations (2024-Q2)
The following optimizations are already implemented in the current version, corresponding to engineering implementation of earlier suggestions:
Timer Trigger timePoint Cache
- Location:
ExecTrigger._cachedHour / _cachedMinute(PeripheralExecution.h) - Location:
sanitizeTriggerForSafety()(PeriphExecManager.cpp) - Issue: Daily fixed mode (timerMode=1) required parsing
HH:MMstring inside lock on every trigger; repeated string operations waste CPU. - Solution: One-time parse and cache to
int8_tfields insanitizeTriggerForSafetyduring rule create/update;checkTimerTriggersreads cache directly, no moresubstring().toInt().
Rule-Level Flag Cache
- Location:
PeriphExecRule._cachedNeedsModbus / _cachedHasPollCollectionAction(PeripheralExecution.h) - Location:
sanitizeRuleForSafety()(PeriphExecManager.cpp) - Issue:
checkTimerTriggersandprocessPollDataMatchtraversed action list on every call to computeruleNeedsModbus()andruleHasPollCollectionAction(); hot path repeated traversal. - Solution: Pre-compute and cache two bool flags in
sanitizeRuleForSafety; hot path reads directly, O(1) replacing O(n).
evaluateCondition BETWEEN Redundancy Removal
- Location:
evaluateCondition()(PeriphExecManager.cpp) - Issue: Original implementation called
toFloat()oncompareValueto get temp variablecmp, but BETWEEN/NOT_BETWEEN needs to parsemin,maxfrom string;cmpwas completely unused. - Solution: Defer
toFloat()call to GT/LT/GTE/LTE branches; BETWEEN branch usessubstringparsing directly, avoiding one useless float conversion.
Appendix A: Source File Index
| File | Lines | Function |
|---|---|---|
include/core/PeripheralExecution.h | ~220 | Enum definitions, data structures, event constants, cache fields |
include/core/AsyncExecTypes.h | ~60 | FreeRTOS async execution types, RAII locks |
include/core/PeriphExecManager.h | ~120 | Manager class interface declaration |
src/core/PeriphExecManager.cpp | ~2550 | Complete business logic implementation (with cache optimizations) |
include/network/handlers/PeriphExecRouteHandler.h | ~45 | API route handler declaration |
src/network/handlers/PeriphExecRouteHandler.cpp | ~570 | API route handler implementation |
Appendix B: Key Method Quick Reference
| Method | File Location (line) | Function |
|---|---|---|
initialize() | PeriphExecManager.cpp:15-25 | Initialize mutexes and semaphores, load config |
addRule() | PeriphExecManager.cpp:29-58 | Create new rule |
updateRule() | PeriphExecManager.cpp:60-89 | Update rule (preserve runtime state) |
saveConfiguration() | PeriphExecManager.cpp:140-203 | v3 format persist to LittleFS |
loadConfiguration() | PeriphExecManager.cpp:205-349 | Load config (supports v1/v2/v3) |
handleMqttMessage() | PeriphExecManager.cpp:353-422 | MQTT message trigger (PLATFORM_TRIGGER) |
handlePollData() | PeriphExecManager.cpp:426-496 | Poll data trigger (POLL_TRIGGER) |
handleDataCommand() | PeriphExecManager.cpp:500-638 | Synchronous data command processing |
evaluateCondition() | PeriphExecManager.cpp:648-681 | Condition expression evaluation |
checkTimers() | PeriphExecManager.cpp:685-750 | Timer trigger check (per second) |
executeAllActions() | PeriphExecManager.cpp:771-815 | Sequential rule action execution |
executePeripheralAction() | PeriphExecManager.cpp:817-891 | GPIO action execution |
executeModbusAction() | PeriphExecManager.cpp:893-952 | Modbus write |
executeModbusPollAction() | PeriphExecManager.cpp:954-1119 | Modbus poll + control |
executeSensorReadAction() | PeriphExecManager.cpp:1121-1174 | Sensor read |
executeSystemAction() | PeriphExecManager.cpp:1176-1227 | System actions |
executeScriptAction() | PeriphExecManager.cpp:1231-1253 | Script execution |
dispatchAsync() | PeriphExecManager.cpp:1270-1354 | Async/sync scheduling decision |
asyncExecTaskFunc() | PeriphExecManager.cpp:1357-1403 | FreeRTOS async task function |
triggerEvent() | PeriphExecManager.cpp:1458-1511 | Event trigger entry |
triggerButtonEvent() | PeriphExecManager.cpp:1964-2009 | Button event trigger |
checkButtonEvents() | PeriphExecManager.cpp:1843-1961 | Button state machine (every 20ms) |
reportActionResults() | PeriphExecManager.cpp:1609-1638 | Report execution results |
tryReportDeviceData() | PeriphExecManager.cpp:1758-1839 | Device state reporting |
collectPeripheralData() | PeriphExecManager.cpp:1649-1694 | Collect peripheral state data |
