03-remote-command

Example of the component nguyenduchoai/innoedge v0.1.4
<a id="top"></a>
# 03 — Remote Command: Điều khiển từ xa (Two-Way Commands)

> 🇻🇳 **Tài liệu Tiếng Việt** (toàn bộ nội dung bên dưới) | [🇬🇧 English Documentation](#english)

Cloud gửi lệnh xuống máy, máy thực thi và trả kết quả về.

## Phần cứng
Devkit + 1 LED ở GPIO2 (đổi `LED_GPIO` cho bo của bạn). Không có LED vẫn chạy được.

## Giao thức (SDK lo, để tham khảo)
```jsonc
// cloud → máy
{"type":"command","commandId":91,"action":"led","params":{"on":true}}
// máy → cloud
{"type":"command_ack","commandId":91,"status":"ok","message":"LED da bat","result":{"on":true}}
```

## Viết một lệnh mới
```c
static esp_err_t cmd_xyz(cJSON *params, char *result, size_t result_len,
                         char *msg, size_t msg_len)
{
    // ... làm việc ...
    snprintf(result, result_len, "{\"count\":%d}", n); // vào field "result"
    snprintf(msg, msg_len, "xong");                    // vào field "message"
    return ESP_OK;                                     // lỗi khác → ack "error"
}

innoedge_register_command("xyz", cmd_xyz);   // 1 dòng, xong
```

## Ba luật bắt buộc

**1. Không block lâu.** Handler chạy trên task WebSocket. Việc > vài giây (tải
file, chờ khách bấm) → `xTaskCreate` rồi trả `ESP_OK` ngay.

**2. Không reboot thẳng trong handler.** `esp_restart()` chạy trước khi ack kịp
gửi → cloud tưởng lệnh hỏng và gửi lại. Dùng `innoedge_reboot_after_ack()`.

**3. `action` phải là string literal.** Registry giữ con trỏ, không copy chuỗi.

## Chống trùng (quan trọng nhất khi lệnh có tiền)

Cloud gửi lại lệnh sau khi mạng rớt là chuyện bình thường. SDK giữ
**high-watermark `commandId` trong NVS**: mọi `commandId ≤` watermark bị coi là
đã xử lý → chỉ ack lại, **không gọi handler lần hai**. Watermark sống qua
reboot, nên mất điện giữa lúc nhả tiền cũng không nhả hai lần.

Đánh dấu xảy ra **trước** khi handler chạy — cố ý: thà bỏ sót một lệnh còn hơn
nhả tiền hai lần.

Kiểm chứng: gửi cùng `commandId` hai lần → lần hai trả
`{"status":"ok","message":"duplicate"}` và LED không đổi.

## Troubleshooting
| Triệu chứng | Nguyên nhân |
|---|---|
| `unknown action` | Quên `innoedge_register_command`, hoặc gõ sai tên action |
| Lệnh chạy nhưng cloud báo timeout | Handler block quá lâu → đẩy sang task riêng |
| Lệnh chạy hai lần | Đang gọi `gtek_command_bus_dispatch` thủ công thay vì để SDK gọi |
| `registry đầy` | Quá 24 action — sửa `GTEK_CMD_REGISTRY_MAX` |

---

<a id="english"></a>
## 🇬🇧 English Documentation

> [🇻🇳 Quay lại Tiếng Việt](#top)

Receive remote commands from the cloud, execute hardware actions, and report status back.

## Hardware Requirements
Devkit board + 1 LED on GPIO2 (adjust `LED_GPIO` for your specific board). Works without an LED by observing logs.

## Protocol Structure (Handled automatically by SDK)
```jsonc
// cloud → device
{"type":"command","commandId":91,"action":"led","params":{"on":true}}
// device → cloud
{"type":"command_ack","commandId":91,"status":"ok","message":"LED is turned on","result":{"on":true}}
```

## Registering a New Command Handler
```c
static esp_err_t cmd_xyz(cJSON *params, char *result, size_t result_len,
                         char *msg, size_t msg_len)
{
    // ... execute hardware actuation ...
    snprintf(result, result_len, "{\"count\":%d}", n); // Populates the "result" field
    snprintf(msg, msg_len, "done");                    // Populates the "message" field
    return ESP_OK;                                     // Non-OK returns status "error"
}

innoedge_register_command("xyz", cmd_xyz);   // Single line registration
```

## Three Essential Rules

1. **Never block the WebSocket task.** Handlers execute on the WebSocket thread. For long-running operations (>1 second, such as dispensing 50 items or waiting for sensors), spawn a FreeRTOS task via `xTaskCreate` and return `ESP_OK` immediately.
2. **Never call `esp_restart()` directly inside a handler.** Calling restart before the ACK frame is flushed causes the cloud to assume a network timeout and resend the command. Always call `innoedge_reboot_after_ack()`.
3. **`action` must be a string literal.** The command registry stores pointers without string duplication.

## Idempotency: Never Dispense Twice
Cloud servers frequently retry commands when cellular signals jitter. The SDK stores a persistent **high-watermark `commandId` in NVS**: any command where `commandId ≤ watermark` is recognized as already executed → immediately ACKed with `{"status":"ok","message":"duplicate"}`, **without invoking the hardware handler a second time**.

The watermark persists across sudden power cuts and reboots. If power is lost mid-dispense, subsequent retry commands upon reboot will not dispense twice.

*Verification:* Send the same `commandId` twice via the Web Dashboard → The second execution returns duplicate status and does not actuate the hardware.

## Troubleshooting
| Symptom | Cause & Solution |
|---|---|
| `unknown action` | Forgot `innoedge_register_command`, or action name mismatch. |
| Command runs but cloud reports timeout | Handler blocked the WebSocket task too long → offload to a separate FreeRTOS task. |
| Command executes twice | Manually calling dispatcher instead of letting the SDK handle it. |
| Registry full | Exceeded maximum number of actions — increase `GTEK_CMD_REGISTRY_MAX`. |

To create a project from this example, run:

idf.py create-project-from-example "nguyenduchoai/innoedge=0.1.4:03-remote-command"

or download archive (~6.93 KB)