Skip to content

Peripheral Execution Business Flow ​

Current Version Notes ​

  • Peripheral Exec is in the release baseline. Script-related paths run only when ruleScript and commandScript are active.
  • Sensor read actions write the latest reading to local cache and produce a sensor_cache data event; event triggers can use ds:<peripheralID>_<field> as data source, e.g., ds:dht_01_temperature.
  • /api/periph-exec returns sensorSources; 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=20 is 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 ​

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)                    │    │
│  └──────────────────┘  └──────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘

Key Design Constraints ​

ConstraintValueDescription
Max triggers per rule3MAX_TRIGGERS_PER_RULE
Max actions per rule4MAX_ACTIONS_PER_RULE
Max concurrent async tasks3MAX_ASYNC_TASKS
Min heap for async task30000 bytesMIN_HEAP_FOR_ASYNC
Script task stack size8192 bytesSCRIPT_TASK_STACK
Normal async task stack4096 bytesSIMPLE_TASK_STACK
Async task priority0 (lowest)ASYNC_TASK_PRIORITY
Trigger relationship within ruleOR (any match triggers)—
Action relationship within ruleSequential (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 ​

ValueNameDescription
0PLATFORM_TRIGGERPlatform data trigger - fires when MQTT-delivered data meets conditions
1TIMER_TRIGGERTimer trigger - fires at time intervals or daily fixed times
4EVENT_TRIGGEREvent trigger - fires on system events (WiFi/MQTT/buttons, etc.)
5POLL_TRIGGERPoll trigger - fires when peripheral poll data meets conditions

ExecActionType - Action Types ​

Action enums are defined in include/core/PeripheralExecution.h. Values 11 and 12 are active NeoPixel and stepper-motor actions; value 20 is the reserved legacy slot.

ValueEnum NameCategoryactionValue Meaning
0ACTION_HIGHGPIOIgnored, set target pin HIGH
1ACTION_LOWGPIOIgnored, set target pin LOW
2ACTION_BLINKGPIOBlink interval ms (default 500)
3ACTION_BREATHEGPIOBreathe cycle ms (default 2000)
4ACTION_SET_PWMGPIOPWM duty cycle 0-255
5ACTION_SET_DACGPIODAC output value 0-255
6ACTION_SYS_RESTARTSystemIgnored, restart device after 500ms delay
7ACTION_SYS_FACTORY_RESETSystemIgnored, format LittleFS then restart
8ACTION_SYS_NTP_SYNCSystemIgnored, trigger NTP time sync
9ACTION_SYS_OTASystemOTA firmware update URL (empty uses configured default URL)
10ACTION_CALL_PERIPHERALLinkageCall other peripheral, actionValue is optional parameter
11ACTION_NEOPIXEL_EFFECTNeoPixelColor or effect command
12ACTION_STEPPER_CONTROLStepperforward, reverse, stop, or step command
13ACTION_HIGH_INVERTEDGPIOIgnored, bitwise-inverted then output HIGH (logic HIGH = physical LOW)
14ACTION_LOW_INVERTEDGPIOIgnored, bitwise-inverted then output LOW (logic LOW = physical HIGH)
15ACTION_SCRIPTScriptCommand sequence script text, multi-line commands executed line by line
16ACTION_MODBUS_COIL_WRITEModbusCoil write (FC05), format slave,addr,value
17ACTION_MODBUS_REG_WRITEModbusRegister write (FC06), format slave,addr,value
18ACTION_MODBUS_POLLModbusPoll slave collection + optional control command, JSON or comma list
19ACTION_SENSOR_READCollectionJSON, read sensor data and report
20ACTION_RESERVED_20ReservedLegacy buzzer preset action placeholder
21ACTION_TRIGGER_EVENTEventTrigger device event, targetPeriphId is event ID, actionValue is extra data
22ACTION_ENABLE_EXEC_RULERuleEnable specified peripheral execution rule, targetPeriphId is rule ID
23ACTION_DISABLE_EXEC_RULERuleDisable specified peripheral execution rule, targetPeriphId is rule ID
24ACTION_DISPLAY_NUMBERDisplay7-segment/display shows number, e.g., 12.34 / 12:34 / 1234, supports ${id.field} template
25ACTION_DISPLAY_TEXTDisplay7-segment/display shows text, e.g., PLAY, ON
26ACTION_DISPLAY_CLEARDisplay7-segment/display clear screen
27ACTION_OLED_DISPLAYDisplayOLED custom multi-line display, supports ${id.field} and $value template; first line # prefix = centered title
28ACTION_RF_SENDRFTransmit an RF code string
29ACTION_UART_SENDUARTSend text or data through the target UART peripheral
30ACTION_GPIO_TOGGLEGPIORead the current level and write the inverse
31ACTION_FEATURE_ENABLESystemEnable runtime feature webServer or mdns, then restart
32ACTION_FEATURE_DISABLESystemDisable runtime feature webServer or mdns, then restart

ExecOperator - Condition Operators ​

ValueNameDescriptionData Type
0OP_EQEqualsNumeric/String
1OP_NEQNot equalsNumeric/String
2OP_GTGreater thanNumeric
3OP_LTLess thanNumeric
4OP_GTEGreater than or equalNumeric
5OP_LTELess than or equalNumeric
6OP_BETWEENWithin rangeNumeric (compareValue="min,max")
7OP_NOT_BETWEENOutside rangeNumeric (compareValue="min,max")
8OP_CONTAINContains substringString
9OP_NOT_CONTAINDoes not contain substringString

2.2 Core Data Structures ​

ExecTrigger - Trigger ​

cpp
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 ​

cpp
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;         // Delay before this action, max 10000 ms
    int execMode;            // 0=async, 1=sync (per-action field persisted by current schema)
};

PeriphExecRule - Rule ​

cpp
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 currently predefines 38 static events (STATIC_EVENTS[]), categorized as follows:

CategoryEvent ID RangeExample Events
WiFi1-3wifi_connected, wifi_disconnected, wifi_conn_failed
MQTT10-13mqtt_connected, mqtt_disconnected, mqtt_conn_failed, mqtt_enabled
Network20-21net_mode_ap, net_mode_sta
Protocol30modbus_rtu_enabled
System40-43, 70-73, 110-113NTP/OTA lifecycle, boot/ready/error/reset, device alarms
Rule60-61rule_exec_time, rule_exec_error
Button80-86click, double-click, long-press, press and release events
PeriphExec90periph_exec_completed
Data100-101data_receive, data_report
RFID / IR / RF120-130card detected/removed and received-code events

3. Rule Lifecycle (CRUD) ​

3.1 Create Rule (addRule) ​

Request → Parameter Validation → ID Generation → Uniqueness Check → Limit Check → Runtime Field Reset → Store → Persist

Detailed Flow:

  1. Mutex protection: Acquire rulesMutex for thread safety
  2. ID generation: If no ID provided, auto-generate "exec_" + String(millis())
  3. Uniqueness check: Look up in rules map for existing ID
  4. Trigger count limit: Max MAX_TRIGGERS_PER_RULE (3), truncate if exceeded
  5. Action count limit: Max MAX_ACTIONS_PER_RULE (4), truncate if exceeded
  6. Runtime field reset: Each trigger's lastTriggerTime = 0, triggerCount = 0
  7. Write to map: rules[rule.id] = rule
  8. 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 → Persist

Key Design - Runtime State Preservation:

When updating a rule, each trigger's lastTriggerTime and triggerCount are preserved by index matching:

cpp
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 → Persist

3.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 Report

Manual 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 triggerPeriphId is 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/error

Special 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 actions

Poll 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.

ActionImplementationactionValue
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:

ActionImplementationAsync Strategy
ACTION_SYS_RESTART (6)delay(500); ESP.restart()Forced sync (async task cannot complete after restart)
ACTION_SYS_FACTORY_RESET (7)Format LittleFS + restartForced sync
ACTION_SYS_NTP_SYNC (8)configTime(gmtOffset, daylightOffset, ntpServer) re-syncCan be async
ACTION_SYS_OTA (9)Trigger OTAManager firmware upgrade, actionValue optional as URLCan be async

Important: In dispatchAsync(), ACTION_SYS_RESTART and ACTION_SYS_FACTORY_RESET are automatically downgraded to synchronous execution.

Note: Enum values 11 and 12 are active: NeoPixel effects and stepper control. Only value 20 remains a reserved legacy action slot.

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:

actionValueBehavior
{"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 1

ACTION_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-65535

ACTION_MODBUS_POLL (18) ​

Executed by executeModbusPollAction(), supports batch polling and optional control commands, two data formats:

JSON format (recommended):

json
{
  "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,2

Poll 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:

json
{
  "periphId": "sensor_01",
  "sensorCategory": "analog",
  "scaleFactor": 1.0,
  "offset": 0.0,
  "decimalPlaces": 2,
  "sensorLabel": "Light",
  "unit": "lux"
}

DHT11/DHT22:

json
{
  "periphId": "dht_01",
  "sensorCategory": "dht11",
  "dataField": "temperature",
  "scaleFactor": 1.0,
  "offset": 0.0,
  "decimalPlaces": 1,
  "sensorLabel": "Temperature",
  "unit": "°C"
}

DS18B20:

json
{
  "periphId": "ds18b20_01",
  "sensorCategory": "ds18b20",
  "deviceIndex": 0,
  "scaleFactor": 1.0,
  "offset": 0.0,
  "decimalPlaces": 2,
  "sensorLabel": "Water Temp",
  "unit": "°C"
}

Supported sensorCategory:

ValueSensor TypeRead MethodCharacteristics
analogAnalog input (ADC)analogRead()0-4095 raw value
digitalDigital input (GPIO)digitalRead()0/1
pulsePulse/frequencyReservedNot implemented
dht11DHT11 temp/humiditySingle-bus timingTemp 0-50°C, humidity 20-90%
dht22DHT22/AM2302Single-bus timingTemp -40~80°C, humidity 0-100%
ds18b20DS18B20 digital tempOneWire protocol-55~125°C, 12-bit precision
ultrasonicHC-SR04 ultrasonicTrig+EchoOutput distance, unit cm
currentCurrent sensorADC + linear calibrationOutput current, supports ACS712, etc.
voltageVoltage sensorADC + voltage divider restorationOutput voltage
SHT31SHT31 temp/humidityI2C lightweight driverOutput temperature, humidity
AHT20AHT20 temp/humidityI2C lightweight driverOutput temperature, humidity
BH1750BH1750 lightI2C lightweight driverOutput illuminance, unit lx
BMP280BMP280 pressureI2C advanced driverOutput temperature, pressure, altitude, requires FASTBEE_ENABLE_I2C_SENSORS
MPU6050MPU6050 motionI2C advanced driverOutput 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 (targetPeriphId is event ID, e.g., sys_breakdown, sys_alarm, low_power)
  • Triggering custom events defined by DEVICE_EVENT peripherals

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 unit

ACTION_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 $value templates
  • 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: Normal

6. 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) ​

cpp
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 > max

6.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 rules

6.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 rules

7. 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:

  • handleMqttMessage is for general MQTT data, executes asynchronously
  • handleDataCommand is 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(), calls executeAllActions() 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 > max

Notes:

  • Numeric comparison uses float, which has floating-point precision issues (e.g., EQ comparison)
  • CONTAIN/NOT_CONTAIN operations use string indexOf directly
  • 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:

cpp
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 value

9.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 topic

9.3 Device State Reporting (tryReportDeviceData) ​

Location: PeriphExecManager.cpp:1758-1839

tryReportDeviceData()
  │
  ├─ Check whether MQTT/MQTTS is connected
  │
  ├─ Collect device data: collectPeripheralData()
  │   ├─ Iterate all enabled GPIO peripherals
  │   └─ Get current state (HIGH/LOW/PWM value, etc.)
  │
  └─ Report through MQTT/MQTTS; skip or queue a retry while disconnected

Current protocol boundary: Peripheral execution reports only through MQTT/MQTTS. There is no TCP, HTTP, or CoAP fallback chain. Modbus RTU is used for field acquisition and control, not as an alternative cloud-reporting transport.

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}] list

10. 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:

cpp
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

json
{
  "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:

  • inverted field → mapped to ACTION_HIGH_INVERTED (13) or ACTION_LOW_INVERTED (14) based on original action
  • Legacy eventId format 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[] arrays

v3 Native Load ​

Directly parse triggers[] and actions[] JSON arrays.

11.3 Persistence Timing ​

saveConfiguration() is triggered by:

  • addRule() → API layer call
  • updateRule() → API layer call
  • removeRule() → API layer call
  • enableRule() / 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 OperationREST APIManager MethodResult Target
View rule listGET /api/periph-execlistRules()Web list, runtime state
Create rulePOST /api/periph-execaddRule()periph_exec.json
Edit rulePOST /api/periph-exec/updateupdateRule()periph_exec.json, rule runtime
Run oncePOST /api/periph-exec/runrunOnce()Execution log, MQTT report
EnablePOST /api/periph-exec/enableenableRule()Rule runtime, config file
DisablePOST /api/periph-exec/disabledisableRule()Rule runtime, config file
Delete ruleDELETE /api/periph-exec/?id={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

MethodPathHandlerPermission
GET/api/periph-exechandleGetRulessystem.view
POST/api/periph-exec (JSON)handleAddRuleJsonconfig.edit
POST/api/periph-exec (form)handleAddRuleconfig.edit
POST/api/periph-exec/update (JSON)handleUpdateRuleJsonconfig.edit
POST/api/periph-exec/update (form)handleUpdateRuleconfig.edit
DELETE/api/periph-exec/handleDeleteRuleconfig.edit
POST/api/periph-exec/enablehandleEnableRuleconfig.edit
POST/api/periph-exec/disablehandleDisableRuleconfig.edit
POST/api/periph-exec/runhandleRunOnceconfig.edit
GET/api/periph-exec/events/statichandleGetStaticEventssystem.view
GET/api/periph-exec/events/dynamichandleGetDynamicEventssystem.view
GET/api/periph-exec/events/categorieshandleGetEventCategoriessystem.view
GET/api/periph-exec/trigger-typeshandleGetTriggerTypessystem.view
GET/api/periph-exec/resultshandleGetRecentResultssystem.view

12.2 Route Registration Order ​

Route registration order is critical because AsyncCallbackJsonWebHandler uses prefix matching:

  1. Register specific paths first: /events/static, /events/dynamic, /events/categories, /trigger-types
  2. JSON handler registers update first: /api/periph-exec/update must come before /api/periph-exec
  3. 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:

FormatContent-TypeHandlerDescription
JSONapplication/jsonhandleAddRuleJson / handleUpdateRuleJsonSupports full triggers[]/actions[] arrays
Formapplication/x-www-form-urlencodedhandleAddRule / handleUpdateRuleBackward 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 prevent as<String>() returning "null" string
  • Integer-compatible parsing: jsonInt() supports both JSON numbers and string format
  • Runtime field reset: lastTriggerTime = 0, triggerCount = 0 during parsing

12.5 Response Format ​

Success response:

json
{"success": true, "message": "Rule added"}

List response:

json
{
  "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:

json
{"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:

cpp
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 (checkTimers daily 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 RTU/MQTT), 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 Enum Compatibility ​

actionType=20 is retained as a historical placeholder and should not be emitted by new configurations. Values 11 and 12 are valid current actions for NeoPixel effects and stepper control.

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:MM string inside lock on every trigger; repeated string operations waste CPU.
  • Solution: One-time parse and cache to int8_t fields in sanitizeTriggerForSafety during rule create/update; checkTimerTriggers reads cache directly, no more substring().toInt().

Rule-Level Flag Cache

  • Location: PeriphExecRule._cachedNeedsModbus / _cachedHasPollCollectionAction (PeripheralExecution.h)
  • Location: sanitizeRuleForSafety() (PeriphExecManager.cpp)
  • Issue: checkTimerTriggers and processPollDataMatch traversed action list on every call to compute ruleNeedsModbus() and ruleHasPollCollectionAction(); 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() on compareValue to get temp variable cmp, but BETWEEN/NOT_BETWEEN needs to parse min,max from string; cmp was completely unused.
  • Solution: Defer toFloat() call to GT/LT/GTE/LTE branches; BETWEEN branch uses substring parsing directly, avoiding one useless float conversion.

Appendix A: Source File Index ​

FileLinesFunction
include/core/PeripheralExecution.h~220Enum definitions, data structures, event constants, cache fields
include/core/AsyncExecTypes.h~60FreeRTOS async execution types, RAII locks
include/core/PeriphExecManager.h~120Manager class interface declaration
src/core/PeriphExecManager.cpp~2550Complete business logic implementation (with cache optimizations)
include/network/handlers/PeriphExecRouteHandler.h~45API route handler declaration
src/network/handlers/PeriphExecRouteHandler.cpp~570API route handler implementation

Appendix B: Key Method Quick Reference ​

MethodFile Location (line)Function
initialize()PeriphExecManager.cpp:15-25Initialize mutexes and semaphores, load config
addRule()PeriphExecManager.cpp:29-58Create new rule
updateRule()PeriphExecManager.cpp:60-89Update rule (preserve runtime state)
saveConfiguration()PeriphExecManager.cpp:140-203v3 format persist to LittleFS
loadConfiguration()PeriphExecManager.cpp:205-349Load config (supports v1/v2/v3)
handleMqttMessage()PeriphExecManager.cpp:353-422MQTT message trigger (PLATFORM_TRIGGER)
handlePollData()PeriphExecManager.cpp:426-496Poll data trigger (POLL_TRIGGER)
handleDataCommand()PeriphExecManager.cpp:500-638Synchronous data command processing
evaluateCondition()PeriphExecManager.cpp:648-681Condition expression evaluation
checkTimers()PeriphExecManager.cpp:685-750Timer trigger check (per second)
executeAllActions()PeriphExecManager.cpp:771-815Sequential rule action execution
executePeripheralAction()PeriphExecManager.cpp:817-891GPIO action execution
executeModbusAction()PeriphExecManager.cpp:893-952Modbus write
executeModbusPollAction()PeriphExecManager.cpp:954-1119Modbus poll + control
executeSensorReadAction()PeriphExecManager.cpp:1121-1174Sensor read
executeSystemAction()PeriphExecManager.cpp:1176-1227System actions
executeScriptAction()PeriphExecManager.cpp:1231-1253Script execution
dispatchAsync()PeriphExecManager.cpp:1270-1354Async/sync scheduling decision
asyncExecTaskFunc()PeriphExecManager.cpp:1357-1403FreeRTOS async task function
triggerEvent()PeriphExecManager.cpp:1458-1511Event trigger entry
triggerButtonEvent()PeriphExecManager.cpp:1964-2009Button event trigger
checkButtonEvents()PeriphExecManager.cpp:1843-1961Button state machine (every 20ms)
reportActionResults()PeriphExecManager.cpp:1609-1638Report execution results
tryReportDeviceData()PeriphExecManager.cpp:1758-1839Device state reporting
collectPeripheralData()PeriphExecManager.cpp:1649-1694Collect peripheral state data

FastBee | fastbee.cn