Waze ModWaze Mod
ESP32Tính năngẢnh thực tếTải xuốngCài đặtChangelogLưu ýFAQỦng hộ
Download

Waze Mod

Dành cho tài xế Việt Nam.

© 2026 Waze Mod. Waze Mod Extended không liên kết chính thức với Waze hoặc Google.

Hướng dẫn cài đặtFAQAdmin
Ví dụ SPPĐổi trang ↓
Tổng quanESP32 BLEClassic SPPVí dụ BLEVí dụ SPPHLP/1 protocolCấu hình thiết bịDuy trì kết nốiPhía AndroidPhần cứngXử lý lỗiKiểm thử

Pin map / tài liệu

Bắt đầu

Tổng quan

Kết nối

ESP32 BLEClassic SPP

Code mẫu

Ví dụ BLEVí dụ SPP

Giao thức

HLP/1 protocolCấu hình thiết bịDuy trì kết nối

Tham chiếu

Phía AndroidPhần cứngXử lý lỗiKiểm thử
ESP32HLP/1esp32-hlp-spp/

Code mẫu ESP32 Classic SPP

Project ESP-IDF hoàn chỉnh cho ESP32 dual-mode, gồm RFCOMM SPP và codec HLP dùng chung.

Tải trang Markdown.MDTải bộ tài liệu cho AIDOCS + SOURCE

Ví dụ ESP-IDF 5.5.x này dành cho ESP32 dual-mode đời đầu. Firmware mở RFCOMM SPP service tên WazeHUD, nhận raw state qua codec HLP và in state hợp lệ trong serial monitor. Project không kèm decoder field hay renderer màn hình.

Build và flash

HLP / source
cd esp32-hlp-spp
idf.py set-target esp32
idf.py build
idf.py -p COM5 flash monitor

Thay COM5 bằng cổng serial của board. Sau khi flash, chọn đúng transport và thiết bị trong Cài đặt Mod → Thiết bị → HUD Link.

Cấu trúc project

HLP / source
esp32-hlp-spp/CMakeLists.txt
esp32-hlp-spp/sdkconfig.defaults
esp32-hlp-spp/main/CMakeLists.txt
esp32-hlp-spp/main/main.c
esp32-hlp-spp/shared/hlp_core.h
esp32-hlp-spp/shared/hlp_core.c
esp32-hlp-spp/shared/hlp_messages.h
esp32-hlp-spp/shared/hlp_messages.c
esp32-hlp-spp/shared/hlp_device_config.h
esp32-hlp-spp/shared/hlp_device_config.c

CMakeLists.txt

HLP / source
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(esp32_hlp_spp)

sdkconfig.defaults

HLP / source
# ESP-IDF 5.5.x: enable Bluedroid and Classic Bluetooth SPP.
CONFIG_BT_ENABLED=y
CONFIG_BTDM_CTRL_MODE_BLE_ONLY=n
CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=y
CONFIG_BTDM_CTRL_MODE_BTDM=n
CONFIG_BT_CLASSIC_ENABLED=y
CONFIG_BT_SPP_ENABLED=y
CONFIG_BT_BLE_ENABLED=n

main/CMakeLists.txt

HLP / source
idf_component_register(
    SRCS "main.c" "../shared/hlp_core.c" "../shared/hlp_messages.c"
         "../shared/hlp_device_config.c"
    INCLUDE_DIRS "." "../shared"
    REQUIRES bt nvs_flash json
)

main/main.c

HLP / source
#include "esp_spp_api.h"
#include "esp_bt_device.h"
#include "esp_bt_main.h"
#include "esp_gap_bt_api.h"
#include "esp_log.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include "nvs_flash.h"
#include "hlp_core.h"
#include "hlp_messages.h"
#include "hlp_device_config.h"
#include <string.h>

#define HLP_QUEUE_DEPTH 8
typedef struct { size_t length; char line[HLP_MAX_FRAME]; } hlp_event_t;
typedef struct { size_t length; uint8_t bytes[HLP_MAX_FRAME]; } hlp_tx_event_t;
static const char *TAG = "hlp_spp";
static QueueHandle_t events;
static QueueHandle_t tx_events;
static hlp_rx_t receiver;
static uint32_t spp_handle;
static bool link_up;
static volatile bool spp_congested;
static volatile bool spp_write_pending;
static volatile esp_spp_status_t spp_write_status;
static TaskHandle_t tx_task_handle;
static volatile bool send_dev_pending;

static void send_line(const char *line, void *user) {
    (void)user;
    if (!link_up || !line) return;
    size_t length = strlen(line);
    if (length + 1 > HLP_MAX_FRAME) return;
    hlp_tx_event_t event = { .length = length + 1 };
    memcpy(event.bytes, line, length);
    event.bytes[length] = '\n';
    if (xQueueSend(tx_events, &event, 0) != pdTRUE)
        ESP_LOGW(TAG, "SPP transmit queue is full");
}

static void on_line(const char *line, size_t length, void *user) {
    (void)user;
    hlp_event_t event = { .length = length };
    if (length >= sizeof(event.line)) return;
    memcpy(event.line, line, length);
    event.line[length] = 0;
    (void)xQueueSend(events, &event, 0);
}

static void state_line(const char *line, size_t length, void *user) {
    (void)user;
    ESP_LOGI(TAG, "HLP state: %.*s", (int)length, line);
}

static void tx_task(void *arg) {
    (void)arg;
    hlp_tx_event_t event;
    for (;;) {
        if (xQueueReceive(tx_events, &event, portMAX_DELAY) != pdTRUE) continue;
        while (link_up) {
            while (link_up && spp_congested)
                ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
            if (!link_up) break;

            spp_write_pending = true;
            esp_err_t err = esp_spp_write(spp_handle, event.length, event.bytes);
            if (err != ESP_OK) {
                spp_write_pending = false;
                ESP_LOGW(TAG, "SPP write failed: %s", esp_err_to_name(err));
                vTaskDelay(pdMS_TO_TICKS(20));
                continue;
            }
            while (link_up && spp_write_pending)
                ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
            if (!link_up || spp_write_status == ESP_SPP_SUCCESS) break;
        }
    }
}

static void protocol_task(void *arg) {
    hlp_event_t event;
    for (;;) {
        if (send_dev_pending) {
            send_dev_pending = false;
            hlp_send_dev(send_line, NULL, "spp", "ESP32-Classic", "1.0.0", 8);
        }
        if (xQueueReceive(events, &event, pdMS_TO_TICKS(100)) == pdTRUE)
            hlp_handle_line(event.line, event.length, send_line, state_line, NULL);
    }
}

static void spp_callback(esp_spp_cb_event_t event, esp_spp_cb_param_t *param) {
    switch (event) {
        case ESP_SPP_INIT_EVT:
            if (esp_spp_start_srv(ESP_SPP_SEC_AUTHENTICATE, ESP_SPP_ROLE_SLAVE,
                                  0, "HLP SPP") != ESP_OK
                    || esp_bt_gap_set_scan_mode(
                            ESP_BT_SCAN_MODE_CONNECTABLE_DISCOVERABLE) != ESP_OK)
                ESP_LOGE(TAG, "Cannot start discoverable SPP server");
            break;
        case ESP_SPP_SRV_OPEN_EVT:
            spp_handle = param->srv_open.handle;
            link_up = true;
            spp_congested = false;
            send_dev_pending = true;
            if (tx_task_handle) xTaskNotifyGive(tx_task_handle);
            break;
        case ESP_SPP_DATA_IND_EVT:
            hlp_rx_feed(&receiver, param->data_ind.data, param->data_ind.len);
            break;
        case ESP_SPP_CLOSE_EVT:
            link_up = false;
            spp_handle = 0;
            spp_write_pending = false;
            if (tx_task_handle) xTaskNotifyGive(tx_task_handle);
            break;
        case ESP_SPP_WRITE_EVT:
            spp_write_status = param->write.status;
            spp_congested = param->write.cong;
            spp_write_pending = false;
            if (tx_task_handle) xTaskNotifyGive(tx_task_handle);
            break;
        case ESP_SPP_CONG_EVT:
            spp_congested = param->cong.cong;
            if (tx_task_handle) xTaskNotifyGive(tx_task_handle);
            break;
        default:
            break;
    }
}

void app_main(void) {
    esp_err_t err = nvs_flash_init();
    if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        err = nvs_flash_init();
    }
    ESP_ERROR_CHECK(err);
    hlp_device_config_init();

    events = xQueueCreate(HLP_QUEUE_DEPTH, sizeof(hlp_event_t));
    tx_events = xQueueCreate(HLP_QUEUE_DEPTH, sizeof(hlp_tx_event_t));
    if (!events || !tx_events) {
        ESP_LOGE(TAG, "Cannot create HLP queues");
        return;
    }
    hlp_rx_init(&receiver, on_line, NULL);
    if (xTaskCreate(protocol_task, "hlp_protocol", 4096, NULL, 5, NULL) != pdPASS
            || xTaskCreate(tx_task, "hlp_spp_tx", 3072, NULL, 5, &tx_task_handle) != pdPASS) {
        ESP_LOGE(TAG, "Cannot create HLP tasks");
        return;
    }

    ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_BLE));
    esp_bt_controller_config_t config = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_bt_controller_init(&config));
    ESP_ERROR_CHECK(esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT));
    ESP_ERROR_CHECK(esp_bluedroid_init());
    ESP_ERROR_CHECK(esp_bluedroid_enable());
    ESP_ERROR_CHECK(esp_bt_dev_set_device_name("WazeHUD"));
    ESP_ERROR_CHECK(esp_spp_register_callback(spp_callback));
    ESP_ERROR_CHECK(esp_spp_init(ESP_SPP_MODE_CB));
}

shared/hlp_core.h

HLP / source
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

#define HLP_MAX_FRAME 512
#define HLP_MAX_PAYLOAD (HLP_MAX_FRAME - 1)

typedef void (*hlp_line_cb_t)(const char *line, size_t length, void *user);

typedef struct {
    uint8_t buffer[HLP_MAX_FRAME]; /* one extra byte for a temporary NUL */
    size_t length;
    bool overflow;
    uint32_t oversized;
    uint32_t malformed_utf8;
    hlp_line_cb_t callback;
    void *user;
} hlp_rx_t;

void hlp_rx_init(hlp_rx_t *rx, hlp_line_cb_t callback, void *user);
void hlp_rx_feed(hlp_rx_t *rx, const uint8_t *data, size_t length);
bool hlp_utf8_valid(const uint8_t *data, size_t length);

shared/hlp_core.c

HLP / source
#include "hlp_core.h"

static void reset(hlp_rx_t *rx) {
    rx->length = 0;
    rx->overflow = false;
}

bool hlp_utf8_valid(const uint8_t *data, size_t length) {
    size_t i = 0;
    while (i < length) {
        uint8_t c = data[i++];
        if (c < 0x80) continue;
        size_t need = c >= 0xF0 ? 3 : c >= 0xE0 ? 2 : c >= 0xC2 ? 1 : 0;
        if (!need || i + need > length) return false;
        uint32_t code = c & ((1u << (6 - need)) - 1u);
        for (size_t j = 0; j < need; ++j) {
            uint8_t part = data[i++];
            if ((part & 0xC0) != 0x80) return false;
            code = (code << 6) | (part & 0x3F);
        }
        if (code > 0x10FFFF || (code >= 0xD800 && code <= 0xDFFF)
                || (need == 1 && code < 0x80)
                || (need == 2 && code < 0x800)
                || (need == 3 && code < 0x10000)) return false;
    }
    return true;
}

void hlp_rx_init(hlp_rx_t *rx, hlp_line_cb_t callback, void *user) {
    *rx = (hlp_rx_t){0};
    rx->callback = callback;
    rx->user = user;
}

void hlp_rx_feed(hlp_rx_t *rx, const uint8_t *data, size_t length) {
    if (!rx || !data) return;
    for (size_t i = 0; i < length; ++i) {
        uint8_t c = data[i];
        if (c == '\n') {
            if (rx->overflow) {
                rx->oversized++;
            } else {
                size_t n = rx->length;
                if (n && rx->buffer[n - 1] == '\r') --n;
                if (!hlp_utf8_valid(rx->buffer, n)) {
                    rx->malformed_utf8++;
                } else if (rx->callback) {
                    rx->buffer[n] = 0;
                    rx->callback((const char *)rx->buffer, n, rx->user);
                }
            }
            reset(rx);
        } else if (!rx->overflow) {
            if (rx->length >= HLP_MAX_PAYLOAD) rx->overflow = true;
            else rx->buffer[rx->length++] = c;
        }
    }
}

shared/hlp_messages.h

HLP / source
#pragma once
#include <stddef.h>

typedef void (*hlp_send_line_t)(const char *line, void *user);
typedef void (*hlp_state_line_t)(const char *line, size_t length, void *user);

void hlp_send_dev(hlp_send_line_t send, void *user, const char *transport,
                  const char *model, const char *firmware, unsigned rate);
void hlp_handle_line(const char *line, size_t length, hlp_send_line_t send,
                     hlp_state_line_t state, void *user);

shared/hlp_messages.c

HLP / source
#include "hlp_messages.h"
#include "hlp_device_config.h"
#include "cJSON.h"
#include <stdio.h>
#include <string.h>

void hlp_send_dev(hlp_send_line_t send, void *user, const char *transport,
                  const char *model, const char *firmware, unsigned rate) {
    if (!send) return;
    char line[480];
    snprintf(line, sizeof(line),
             "{\"v\":1,\"t\":\"dev\",\"name\":\"%s\",\"fw\":\"%s\","
             "\"proto\":[1],\"want\":{\"rate\":%u,\"fields\":[\"nav\",\"spd\",\"lim\","
             "\"over\",\"trn\",\"trn2\",\"dst\",\"exit\",\"st\",\"st2\",\"eta\","
             "\"rmin\",\"rkm\",\"avg\",\"avgL\",\"avgR\",\"avgP\",\"alr\",\"alrD\","
             "\"alrV\",\"alrs\"]},\"can\":[\"speed\",\"limit\",\"turn\",\"street\","
             "\"eta\",\"avgzone\",\"alerts\"],\"transport\":\"%s\"}",
             model ? model : "ESP32", firmware ? firmware : "0.0.0", rate,
             transport ? transport : "unknown");
    send(line, user);
}

void hlp_handle_line(const char *line, size_t length, hlp_send_line_t send,
                     hlp_state_line_t state, void *user) {
    (void)length; // hlp_core supplies a NUL-terminated bounded line.
    cJSON *root = cJSON_Parse(line);
    if (!root || !cJSON_IsObject(root)) {
        if (root) cJSON_Delete(root);
        return;
    }
    cJSON *version = cJSON_GetObjectItemCaseSensitive(root, "v");
    cJSON *type = cJSON_GetObjectItemCaseSensitive(root, "t");
    if (!cJSON_IsNumber(version) || version->valueint != 1 || !cJSON_IsString(type)) {
        cJSON_Delete(root);
        return;
    }
    if (hlp_device_config_handle(root, send, user)) {
        /* Dynamic config messages are consumed by the device-config module. */
    } else if (strcmp(type->valuestring, "ping") == 0 && send) {
        send("{\"v\":1,\"t\":\"pong\"}", user);
    } else if (strcmp(type->valuestring, "s") == 0 && state) {
        state(line, length, user);
    }
    cJSON_Delete(root);
}

shared/hlp_device_config.h

HLP / source
#pragma once

#include "hlp_messages.h"
#include "cJSON.h"
#include <stdbool.h>

/* Reference editable device configuration. Replace the five demo fields with product settings. */
void hlp_device_config_init(void);
void hlp_device_config_publish(hlp_send_line_t send, void *user);
bool hlp_device_config_handle(const cJSON *root, hlp_send_line_t send, void *user);

shared/hlp_device_config.c

HLP / source
#include "hlp_device_config.h"
#include "nvs.h"
#include <stdlib.h>
#include <string.h>

#define CFG_COUNT 5
#define CFG_NS "hlp_cfg"

typedef struct {
    bool show_eta;
    int brightness;
    int offset;
    char theme[8];
    char label[21];
} device_config_t;

static device_config_t active = { true, 70, 0, "auto", "Waze HUD" };
static device_config_t draft;
static uint32_t revision = 1;
static uint32_t transaction;
static uint32_t received_mask;
static int received_count;
static int expected_count;

static void send_json(cJSON *object, hlp_send_line_t send, void *user) {
    if (!object || !send) { if (object) cJSON_Delete(object); return; }
    char *line = cJSON_PrintUnformatted(object);
    if (line) { send(line, user); free(line); }
    cJSON_Delete(object);
}

static cJSON *envelope(const char *type) {
    cJSON *root = cJSON_CreateObject();
    if (!root) return NULL;
    cJSON_AddNumberToObject(root, "v", 1);
    cJSON_AddStringToObject(root, "t", type);
    return root;
}

static void load_string(nvs_handle_t nvs, const char *key, char *out, size_t capacity) {
    size_t length = capacity;
    if (nvs_get_str(nvs, key, out, &length) != ESP_OK) out[capacity - 1] = 0;
}

void hlp_device_config_init(void) {
    nvs_handle_t nvs;
    if (nvs_open(CFG_NS, NVS_READONLY, &nvs) != ESP_OK) return;
    uint8_t eta;
    int32_t value;
    if (nvs_get_u8(nvs, "eta", &eta) == ESP_OK) active.show_eta = eta != 0;
    if (nvs_get_i32(nvs, "bright", &value) == ESP_OK) active.brightness = value;
    if (nvs_get_i32(nvs, "offset", &value) == ESP_OK) active.offset = value;
    (void)nvs_get_u32(nvs, "rev", &revision);
    load_string(nvs, "theme", active.theme, sizeof(active.theme));
    load_string(nvs, "label", active.label, sizeof(active.label));
    nvs_close(nvs);
}

static bool save_active(void) {
    nvs_handle_t nvs;
    if (nvs_open(CFG_NS, NVS_READWRITE, &nvs) != ESP_OK) return false;
    esp_err_t err = nvs_set_u8(nvs, "eta", active.show_eta ? 1 : 0);
    if (err == ESP_OK) err = nvs_set_i32(nvs, "bright", active.brightness);
    if (err == ESP_OK) err = nvs_set_i32(nvs, "offset", active.offset);
    if (err == ESP_OK) err = nvs_set_str(nvs, "theme", active.theme);
    if (err == ESP_OK) err = nvs_set_str(nvs, "label", active.label);
    if (err == ESP_OK) err = nvs_set_u32(nvs, "rev", revision);
    if (err == ESP_OK) err = nvs_commit(nvs);
    nvs_close(nvs);
    return err == ESP_OK;
}

static cJSON *item(const char *id, const char *kind, const char *label) {
    cJSON *root = envelope("cfg_item");
    if (!root) return NULL;
    cJSON_AddNumberToObject(root, "rev", revision);
    cJSON_AddStringToObject(root, "id", id);
    cJSON_AddStringToObject(root, "kind", kind);
    cJSON_AddStringToObject(root, "label", label);
    return root;
}

void hlp_device_config_publish(hlp_send_line_t send, void *user) {
    cJSON *root = envelope("cfg_begin");
    cJSON_AddNumberToObject(root, "rev", revision);
    cJSON_AddNumberToObject(root, "count", CFG_COUNT);
    cJSON_AddStringToObject(root, "title", "Cấu hình thiết bị HUD");
    send_json(root, send, user);

    root = item("show_eta", "toggle", "Hiện thời gian đến");
    cJSON_AddBoolToObject(root, "value", active.show_eta);
    send_json(root, send, user);

    root = item("brightness", "slider", "Độ sáng");
    cJSON_AddNumberToObject(root, "value", active.brightness);
    cJSON_AddNumberToObject(root, "min", 10);
    cJSON_AddNumberToObject(root, "max", 100);
    cJSON_AddNumberToObject(root, "step", 5);
    send_json(root, send, user);

    root = item("theme", "selection", "Giao diện");
    cJSON_AddStringToObject(root, "value", active.theme);
    cJSON *options = cJSON_AddArrayToObject(root, "options");
    const char *values[] = { "auto", "day", "night" };
    const char *labels[] = { "Tự động", "Ban ngày", "Ban đêm" };
    for (int i = 0; i < 3; ++i) {
        cJSON *option = cJSON_CreateObject();
        cJSON_AddStringToObject(option, "value", values[i]);
        cJSON_AddStringToObject(option, "label", labels[i]);
        cJSON_AddItemToArray(options, option);
    }
    send_json(root, send, user);

    root = item("offset", "integer", "Hiệu chỉnh ngang");
    cJSON_AddNumberToObject(root, "value", active.offset);
    cJSON_AddNumberToObject(root, "min", -20);
    cJSON_AddNumberToObject(root, "max", 20);
    send_json(root, send, user);

    root = item("label", "text", "Tên hiển thị");
    cJSON_AddStringToObject(root, "value", active.label);
    cJSON_AddNumberToObject(root, "maxLength", 20);
    send_json(root, send, user);

    root = envelope("cfg_end");
    cJSON_AddNumberToObject(root, "rev", revision);
    send_json(root, send, user);
}

static void ack(hlp_send_line_t send, void *user, bool ok,
                const char *field, const char *error) {
    cJSON *root = envelope("cfg_ack");
    cJSON_AddNumberToObject(root, "tx", transaction);
    cJSON_AddBoolToObject(root, "ok", ok);
    if (ok) cJSON_AddNumberToObject(root, "rev", revision);
    if (field) cJSON_AddStringToObject(root, "field", field);
    if (error) cJSON_AddStringToObject(root, "error", error);
    send_json(root, send, user);
}

static bool string_value(const cJSON *value, char *out, size_t capacity) {
    if (!cJSON_IsString(value) || !value->valuestring
            || strlen(value->valuestring) >= capacity) return false;
    strcpy(out, value->valuestring);
    return true;
}

bool hlp_device_config_handle(const cJSON *root, hlp_send_line_t send, void *user) {
    cJSON *type = cJSON_GetObjectItemCaseSensitive(root, "t");
    if (!cJSON_IsString(type)) return false;
    if (strcmp(type->valuestring, "hi") == 0) {
        hlp_device_config_publish(send, user);
        return true;
    }
    if (strcmp(type->valuestring, "cfg_set_begin") == 0) {
        cJSON *tx = cJSON_GetObjectItemCaseSensitive(root, "tx");
        cJSON *rev = cJSON_GetObjectItemCaseSensitive(root, "rev");
        cJSON *count = cJSON_GetObjectItemCaseSensitive(root, "count");
        if (!cJSON_IsNumber(tx) || !cJSON_IsNumber(rev) || !cJSON_IsNumber(count)) return true;
        transaction = (uint32_t)tx->valuedouble;
        expected_count = count->valueint;
        received_mask = 0;
        received_count = 0;
        draft = active;
        if ((uint32_t)rev->valuedouble != revision || expected_count != CFG_COUNT) {
            ack(send, user, false, NULL, "schema revision/count mismatch");
            transaction = 0;
        }
        return true;
    }
    if (strcmp(type->valuestring, "cfg_set") == 0) {
        cJSON *tx = cJSON_GetObjectItemCaseSensitive(root, "tx");
        cJSON *id = cJSON_GetObjectItemCaseSensitive(root, "id");
        cJSON *value = cJSON_GetObjectItemCaseSensitive(root, "value");
        if (!transaction || !cJSON_IsNumber(tx) || (uint32_t)tx->valuedouble != transaction
                || !cJSON_IsString(id) || !value) return true;
        const char *field = id->valuestring;
        bool valid = true;
        uint32_t mask_before = received_mask;
        if (strcmp(field, "show_eta") == 0) {
            valid = cJSON_IsBool(value); if (valid) draft.show_eta = cJSON_IsTrue(value);
            received_mask |= valid ? 1u << 0 : 0;
        } else if (strcmp(field, "brightness") == 0) {
            valid = cJSON_IsNumber(value) && value->valueint >= 10 && value->valueint <= 100
                    && ((value->valueint - 10) % 5) == 0;
            if (valid) draft.brightness = value->valueint;
            received_mask |= valid ? 1u << 1 : 0;
        } else if (strcmp(field, "theme") == 0) {
            valid = string_value(value, draft.theme, sizeof(draft.theme))
                    && (!strcmp(draft.theme, "auto") || !strcmp(draft.theme, "day")
                        || !strcmp(draft.theme, "night"));
            received_mask |= valid ? 1u << 2 : 0;
        } else if (strcmp(field, "offset") == 0) {
            valid = cJSON_IsNumber(value) && value->valueint >= -20 && value->valueint <= 20;
            if (valid) draft.offset = value->valueint;
            received_mask |= valid ? 1u << 3 : 0;
        } else if (strcmp(field, "label") == 0) {
            valid = string_value(value, draft.label, sizeof(draft.label));
            received_mask |= valid ? 1u << 4 : 0;
        } else valid = false;
        if (valid && received_mask == mask_before) valid = false; /* duplicate id */
        if (valid) received_count++;
        if (!valid) { ack(send, user, false, field, "invalid value"); transaction = 0; }
        return true;
    }
    if (strcmp(type->valuestring, "cfg_set_commit") == 0) {
        cJSON *tx = cJSON_GetObjectItemCaseSensitive(root, "tx");
        if (!transaction || !cJSON_IsNumber(tx) || (uint32_t)tx->valuedouble != transaction)
            return true;
        if (received_count != expected_count
                || received_mask != ((1u << CFG_COUNT) - 1u)) {
            ack(send, user, false, NULL, "incomplete transaction");
        } else {
            device_config_t previous = active;
            active = draft;
            revision++;
            if (save_active()) ack(send, user, true, NULL, NULL);
            else {
                active = previous;
                revision--;
                ack(send, user, false, NULL, "NVS write failed");
            }
        }
        transaction = 0;
        return true;
    }
    return false;
}
TrướcVí dụ BLETiếp HLP/1 protocol

Trên trang này

Build và flashCấu trúc projectCMakeLists.txtsdkconfig.defaultsmain/CMakeLists.txtmain/main.cshared/hlpcore.hshared/hlpcore.cshared/hlpmessages.hshared/hlpmessages.cshared/hlpdeviceconfig.hshared/hlpdeviceconfig.c