diff --git a/velxio-deployer-firmware/main/CMakeLists.txt b/velxio-deployer-firmware/main/CMakeLists.txt
index d9bb49a..e20eae6 100644
--- a/velxio-deployer-firmware/main/CMakeLists.txt
+++ b/velxio-deployer-firmware/main/CMakeLists.txt
@@ -10,6 +10,10 @@ idf_component_register(
serial_bridge.c
led_button.c
arduino_reset.c
+ device_name.c
+ wifi_ap.c
INCLUDE_DIRS
.
+ EMBED_TXTFILES
+ config_page.html
)
diff --git a/velxio-deployer-firmware/main/ble_service.c b/velxio-deployer-firmware/main/ble_service.c
index eb0fae7..29c6782 100644
--- a/velxio-deployer-firmware/main/ble_service.c
+++ b/velxio-deployer-firmware/main/ble_service.c
@@ -18,6 +18,7 @@ void ble_store_config_init(void);
#include "state_machine.h"
#include "services/gatt/ble_svc_gatt.h"
#include "ble_service.h"
+#include "device_name.h"
#include "arduino_reset.h"
#include "serial_bridge.h"
#include "state_machine.h"
@@ -148,9 +149,21 @@ static void ble_restart_adv(void)
struct ble_hs_adv_fields fields;
memset(&fields, 0, sizeof(fields));
fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
- fields.name = (uint8_t *)"Velxio";
- fields.name_len = 6;
- fields.name_is_complete = 0;
+ const char *name = device_name_get();
+ size_t name_len = strlen(name);
+ // Check if full name fits in 31-byte adv packet
+ // Adv fields overhead: flags(3) + name header(2) = ~5 bytes overhead for name
+ // So name content needs ≤ 26 bytes max
+ if (name_len <= 24) { // safe margin under 31-byte limit
+ fields.name = (uint8_t *)name;
+ fields.name_len = name_len;
+ fields.name_is_complete = 1;
+ } else {
+ // Truncate to short prefix for advertising
+ fields.name = (uint8_t *)"Velxio";
+ fields.name_len = 6;
+ fields.name_is_complete = 0;
+ }
fields.uuids16 = NULL;
fields.num_uuids16 = 0;
@@ -270,7 +283,7 @@ void ble_service_init(void)
ble_gatts_count_cfg(gatt_svcs);
ble_gatts_add_svcs(gatt_svcs);
- ble_svc_gap_device_name_set("Velxio-Deployer");
+ ble_svc_gap_device_name_set(device_name_get());
ble_svc_gap_device_appearance_set(0x0080);
ble_store_config_init();
diff --git a/velxio-deployer-firmware/main/config_page.html b/velxio-deployer-firmware/main/config_page.html
new file mode 100644
index 0000000..d72ce3c
--- /dev/null
+++ b/velxio-deployer-firmware/main/config_page.html
@@ -0,0 +1,104 @@
+
+
+
+
+
+ Velxio Deployer Setup
+
+
+
+
+
+
Velxio Deployer
+
Konfigurasi nama perangkat
+
+
+
+ Velxio-
+
+
+
+
+
Nama akan tampil di pairing Bluetooth. Maks 24 karakter.
+
+
+
+
+
diff --git a/velxio-deployer-firmware/main/device_name.c b/velxio-deployer-firmware/main/device_name.c
new file mode 100644
index 0000000..269079e
--- /dev/null
+++ b/velxio-deployer-firmware/main/device_name.c
@@ -0,0 +1,107 @@
+#include "device_name.h"
+#include
+#include
+#include "nvs_flash.h"
+#include "nvs.h"
+#include "esp_mac.h"
+#include "esp_log.h"
+
+static const char *TAG = "DEV_NAME";
+static char s_device_name[VELXIO_NAME_MAX_LEN] = {0};
+static bool s_is_custom = false;
+
+static void generate_mac_suffix_name(void)
+{
+ uint8_t mac[6];
+ esp_err_t mac_err = esp_read_mac(mac, ESP_MAC_BT);
+ if (mac_err != ESP_OK) {
+ ESP_LOGW(TAG, "esp_read_mac failed (%d), using fallback suffix", mac_err);
+ snprintf(s_device_name, sizeof(s_device_name),
+ "Velxio-0000");
+ } else {
+ snprintf(s_device_name, sizeof(s_device_name),
+ "Velxio-%02X%02X", mac[4], mac[5]);
+ }
+ s_is_custom = false;
+ ESP_LOGI(TAG, "Generated device name from MAC: %s", s_device_name);
+}
+
+void device_name_init(void)
+{
+ nvs_handle_t h;
+ esp_err_t err = nvs_open(VELXIO_NVS_NAMESPACE, NVS_READONLY, &h);
+ if (err == ESP_OK) {
+ size_t len = sizeof(s_device_name);
+ err = nvs_get_str(h, VELXIO_NVS_KEY_NAME, s_device_name, &len);
+ nvs_close(h);
+ if (err == ESP_OK && strlen(s_device_name) > 0) {
+ s_is_custom = true;
+ ESP_LOGI(TAG, "Loaded custom device name from NVS: %s", s_device_name);
+ return;
+ }
+ ESP_LOGI(TAG, "NVS open OK but no custom name key, using MAC suffix");
+ } else {
+ ESP_LOGI(TAG, "NVS namespace 'velxio' not found (err=%d), using MAC suffix", err);
+ }
+ generate_mac_suffix_name();
+}
+
+const char *device_name_get(void)
+{
+ return s_device_name;
+}
+
+bool device_name_is_custom(void)
+{
+ return s_is_custom;
+}
+
+esp_err_t device_name_set_custom(const char *name)
+{
+ if (!name || strlen(name) == 0) {
+ return ESP_ERR_INVALID_ARG;
+ }
+
+ size_t prefix = (strncmp(name, "Velxio-", 7) == 0) ? 0 : 7;
+ if (strlen(name) + prefix >= VELXIO_NAME_MAX_LEN) {
+ ESP_LOGE(TAG, "Name too long (max %d chars including prefix)", VELXIO_NAME_MAX_LEN - 1);
+ return ESP_ERR_INVALID_SIZE;
+ }
+
+ char full_name[VELXIO_NAME_MAX_LEN];
+ if (prefix == 0) {
+ snprintf(full_name, sizeof(full_name), "%s", name);
+ } else {
+ snprintf(full_name, sizeof(full_name), "Velxio-%s", name);
+ }
+
+ nvs_handle_t h;
+ esp_err_t err = nvs_open(VELXIO_NVS_NAMESPACE, NVS_READWRITE, &h);
+ if (err != ESP_OK) {
+ ESP_LOGE(TAG, "NVS open failed: %d", err);
+ return err;
+ }
+
+ err = nvs_set_str(h, VELXIO_NVS_KEY_NAME, full_name);
+ if (err == ESP_OK) {
+ err = nvs_commit(h);
+ }
+ nvs_close(h);
+
+ if (err == ESP_OK) {
+ ESP_LOGI(TAG, "Saved custom device name: %s", full_name);
+ snprintf(s_device_name, sizeof(s_device_name), "%s", full_name);
+ s_is_custom = true;
+ } else {
+ ESP_LOGE(TAG, "NVS save failed: %d", err);
+ }
+ return err;
+}
+
+void device_name_get_ap_ssid(char *buf, size_t buf_len)
+{
+ const char *name = s_device_name;
+ size_t len = strlen(name);
+ const char *suffix = (len >= 4) ? (name + len - 4) : "0000";
+ snprintf(buf, buf_len, "Velxio-Setup-%s", suffix);
+}
diff --git a/velxio-deployer-firmware/main/device_name.h b/velxio-deployer-firmware/main/device_name.h
new file mode 100644
index 0000000..f31de75
--- /dev/null
+++ b/velxio-deployer-firmware/main/device_name.h
@@ -0,0 +1,21 @@
+/**
+ * @file device_name.h
+ * @brief Device name management for Velxio firmware
+ *
+ * Manages persistent custom device names and MAC-address-derived
+ * fallback names, stored in NVS.
+ */
+#pragma once
+#include
+#include
+#include "esp_err.h"
+
+#define VELXIO_NVS_NAMESPACE "velxio"
+#define VELXIO_NVS_KEY_NAME "dev_name"
+#define VELXIO_NAME_MAX_LEN 32
+
+void device_name_init(void);
+const char *device_name_get(void);
+bool device_name_is_custom(void);
+esp_err_t device_name_set_custom(const char *name);
+void device_name_get_ap_ssid(char *buf, size_t buf_len);
diff --git a/velxio-deployer-firmware/main/led_button.c b/velxio-deployer-firmware/main/led_button.c
index 3106fa8..a2fa34b 100644
--- a/velxio-deployer-firmware/main/led_button.c
+++ b/velxio-deployer-firmware/main/led_button.c
@@ -1,6 +1,8 @@
#include "esp_log.h"
#include "driver/gpio.h"
#include "esp_timer.h"
+#include "nvs_flash.h"
+#include "esp_system.h"
#include "led_button.h"
static const char *TAG = "LED_BTN";
@@ -11,6 +13,10 @@ static bool button_pressed_flag = false;
static int64_t last_toggle_time = 0;
static bool blink_state = false;
+/* Long-press factory reset tracking */
+static int64_t button_press_start_ms = 0;
+static bool button_is_held = false;
+
static void set_led(bool red, bool green, bool blue)
{
gpio_set_level(LED_GPIO_RED, red ? 1 : 0);
@@ -78,6 +84,27 @@ bool button_retry_pressed(void)
void led_button_tick(void)
{
+ /* Factory reset long-press detection (GPIO0 = BOOT button) */
+ bool btn_level = gpio_get_level(BTN_GPIO_RETRY);
+ if (btn_level == 0) { /* button pressed (active low with pull-up) */
+ if (!button_is_held) {
+ button_is_held = true;
+ button_press_start_ms = esp_timer_get_time() / 1000;
+ } else {
+ int64_t held_ms = (esp_timer_get_time() / 1000) - button_press_start_ms;
+ if (held_ms > 5000) {
+ ESP_LOGW(TAG, "BOOT button held %lldms — factory reset!", held_ms);
+ esp_err_t erase_err = nvs_flash_erase();
+ if (erase_err != ESP_OK) {
+ ESP_LOGE(TAG, "NVS erase failed: %d", erase_err);
+ }
+ esp_restart();
+ }
+ }
+ } else {
+ button_is_held = false;
+ }
+
if (current_pattern != LED_GREEN_BLINK &&
current_pattern != LED_RED_BLINK &&
current_pattern != LED_BLUE_BLINK &&
diff --git a/velxio-deployer-firmware/main/main.c b/velxio-deployer-firmware/main/main.c
index 3d4fde3..772b8f1 100644
--- a/velxio-deployer-firmware/main/main.c
+++ b/velxio-deployer-firmware/main/main.c
@@ -14,6 +14,8 @@
#include "state_machine.h"
#include "serial_bridge.h"
#include "led_button.h"
+#include "device_name.h"
+#include "wifi_ap.h"
static const char *TAG = "MAIN";
@@ -67,7 +69,20 @@ void app_main(void)
{
ESP_LOGI(TAG, "Velxio BLE Deployer v1.0");
+ // Initialize NVS
nvs_flash_init();
+
+ // Initialize device name from NVS or MAC suffix
+ device_name_init();
+
+ // AP setup mode: if no custom name, start AP config portal
+ if (!device_name_is_custom()) {
+ ESP_LOGW(TAG, "No custom name — entering AP setup mode");
+ wifi_ap_start_and_block(); // never returns
+ }
+
+ // --- Normal boot: BLE + USB Host mode ---
+
esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
led_button_init();
diff --git a/velxio-deployer-firmware/main/wifi_ap.c b/velxio-deployer-firmware/main/wifi_ap.c
new file mode 100644
index 0000000..fa5a7f3
--- /dev/null
+++ b/velxio-deployer-firmware/main/wifi_ap.c
@@ -0,0 +1,228 @@
+#include "wifi_ap.h"
+#include "device_name.h"
+#include
+#include
+#include
+#include "esp_wifi.h"
+#include "esp_event.h"
+#include "esp_log.h"
+#include "esp_netif.h"
+#include "esp_http_server.h"
+#include "nvs_flash.h"
+#include "esp_system.h"
+
+static const char *TAG = "WIFI_AP";
+
+/* Embedded config_page.html — linked via EMBED_TXTFILES in CMakeLists.txt */
+extern const uint8_t config_page_html_start[] asm("_binary_config_page_html_start");
+extern const uint8_t config_page_html_end[] asm("_binary_config_page_html_end");
+
+#define AP_PASSWORD "velxio123"
+
+/* ---------- Minimal JSON helpers ---------- */
+
+/**
+ * @brief Extract string value for a given key from a flat JSON object.
+ *
+ * Parses {"key":"value"} — no nesting, no escaping support.
+ * Returns pointer to the value text (null-terminated in-place) or NULL.
+ */
+static const char *json_get_string(const char *json, const char *key)
+{
+ if (!json || !key) return NULL;
+
+ /* Build search pattern: "key":" */
+ char pattern[64];
+ int n = snprintf(pattern, sizeof(pattern), "\"%s\":\"", key);
+ if (n < 0 || (size_t)n >= sizeof(pattern)) return NULL;
+
+ const char *p = strstr(json, pattern);
+ if (!p) return NULL;
+
+ p += strlen(pattern); /* point to first char of value */
+ /* Find closing quote */
+ const char *end = strchr(p, '"');
+ if (!end) return NULL;
+
+ /* Return the value as a mutable string (caller gets const, but we
+ write a null to mark the end — safe since json is in a writable buf) */
+ /* Cast away const: the caller owns a writable buffer */
+ char *mutable = (char *)end;
+ *mutable = '\0';
+ return p;
+}
+
+/* ---------- HTTP handlers ---------- */
+
+static esp_err_t get_root_handler(httpd_req_t *req)
+{
+ const size_t html_len = config_page_html_end - config_page_html_start;
+ ESP_LOGI(TAG, "Serving config page (%zu bytes)", html_len);
+ httpd_resp_set_type(req, "text/html");
+ httpd_resp_send(req, (const char *)config_page_html_start, html_len);
+ return ESP_OK;
+}
+
+static esp_err_t get_name_handler(httpd_req_t *req)
+{
+ const char *name = device_name_get();
+ char resp[128];
+ snprintf(resp, sizeof(resp), "{\"name\":\"%s\"}", name);
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+ return ESP_OK;
+}
+
+/**
+ * Validate suffix: only alphanumeric, dash, and underscore allowed.
+ */
+static bool is_valid_suffix(const char *s)
+{
+ if (!s || *s == '\0') return false;
+ for (; *s; s++) {
+ if (!isalnum((unsigned char)*s) && *s != '-' && *s != '_')
+ return false;
+ }
+ return true;
+}
+
+static esp_err_t post_name_handler(httpd_req_t *req)
+{
+ char buf[256];
+ int remaining = req->content_len;
+
+ if (remaining >= (int)sizeof(buf)) {
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_sendstr(req, "{\"success\":false,\"error\":\"Payload too large\"}");
+ return ESP_OK;
+ }
+
+ int ret = httpd_req_recv(req, buf, remaining);
+ if (ret <= 0) {
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_sendstr(req, "{\"success\":false,\"error\":\"Failed to read body\"}");
+ return ESP_OK;
+ }
+ buf[ret] = '\0';
+
+ /* Parse JSON: {"name":"Velxio-XXXX"} */
+ const char *name_val = json_get_string(buf, "name");
+ if (!name_val) {
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_sendstr(req, "{\"success\":false,\"error\":\"Missing 'name' field\"}");
+ return ESP_OK;
+ }
+
+ const char *full_name = name_val;
+
+ /* Extract suffix after "Velxio-" prefix */
+ const char *suffix = full_name;
+ if (strncmp(full_name, "Velxio-", 7) == 0) {
+ suffix = full_name + 7;
+ }
+
+ if (!is_valid_suffix(suffix)) {
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_sendstr(req,
+ "{\"success\":false,\"error\":\"Only alphanumeric, dash, underscore allowed\"}");
+ return ESP_OK;
+ }
+
+ esp_err_t err = device_name_set_custom(full_name);
+
+ if (err != ESP_OK) {
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_sendstr(req, "{\"success\":false,\"error\":\"NVS write failed\"}");
+ return ESP_OK;
+ }
+
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_sendstr(req, "{\"success\":true}");
+
+ ESP_LOGI(TAG, "Name saved, rebooting in 1 second...");
+ vTaskDelay(pdMS_TO_TICKS(1000));
+ esp_restart();
+
+ /* Never reached */
+ return ESP_OK;
+}
+
+/* ---------- Public API ---------- */
+
+void wifi_ap_start_and_block(void)
+{
+ ESP_LOGI(TAG, "Starting WiFi SoftAP + HTTP config server...");
+
+ /* Initialise network interface and event loop */
+ ESP_ERROR_CHECK(esp_netif_init());
+ ESP_ERROR_CHECK(esp_event_loop_create_default());
+ esp_netif_create_default_wifi_ap();
+
+ /* Initialise WiFi in AP mode */
+ wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
+ ESP_ERROR_CHECK(esp_wifi_init(&cfg));
+
+ /* Build AP SSID from device MAC */
+ char ap_ssid[32] = {0};
+ device_name_get_ap_ssid(ap_ssid, sizeof(ap_ssid));
+
+ /* Configure SoftAP */
+ wifi_config_t wifi_config = {
+ .ap = {
+ .ssid_len = 0,
+ .max_connection = 4,
+ .authmode = WIFI_AUTH_WPA_WPA2_PSK,
+ },
+ };
+ snprintf((char *)wifi_config.ap.ssid, sizeof(wifi_config.ap.ssid), "%s", ap_ssid);
+ snprintf((char *)wifi_config.ap.password, sizeof(wifi_config.ap.password), "%s", AP_PASSWORD);
+
+ ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
+ ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
+ ESP_ERROR_CHECK(esp_wifi_start());
+
+ ESP_LOGI(TAG, "SoftAP started — SSID: %s, Password: %s", ap_ssid, AP_PASSWORD);
+
+ /* Start HTTP config server */
+ httpd_handle_t server = NULL;
+ httpd_config_t httpd_conf = HTTPD_DEFAULT_CONFIG();
+
+ if (httpd_start(&server, &httpd_conf) != ESP_OK) {
+ ESP_LOGE(TAG, "Failed to start HTTP server");
+ while (1) {
+ vTaskDelay(pdMS_TO_TICKS(1000));
+ }
+ }
+
+ /* Register URI handlers */
+ httpd_uri_t uri_get_root = {
+ .uri = "/",
+ .method = HTTP_GET,
+ .handler = get_root_handler,
+ .user_ctx = NULL,
+ };
+ httpd_register_uri_handler(server, &uri_get_root);
+
+ httpd_uri_t uri_get_name = {
+ .uri = "/api/name",
+ .method = HTTP_GET,
+ .handler = get_name_handler,
+ .user_ctx = NULL,
+ };
+ httpd_register_uri_handler(server, &uri_get_name);
+
+ httpd_uri_t uri_post_name = {
+ .uri = "/api/name",
+ .method = HTTP_POST,
+ .handler = post_name_handler,
+ .user_ctx = NULL,
+ };
+ httpd_register_uri_handler(server, &uri_post_name);
+
+ ESP_LOGI(TAG, "HTTP config server running at http://192.168.4.1");
+
+ /* Block forever — this function never returns */
+ while (1) {
+ vTaskDelay(pdMS_TO_TICKS(1000));
+ }
+}
diff --git a/velxio-deployer-firmware/main/wifi_ap.h b/velxio-deployer-firmware/main/wifi_ap.h
new file mode 100644
index 0000000..c28cb0c
--- /dev/null
+++ b/velxio-deployer-firmware/main/wifi_ap.h
@@ -0,0 +1,11 @@
+#pragma once
+#include
+
+/**
+ * Start WiFi SoftAP + HTTP config server.
+ * Blocks forever (does not return).
+ * AP SSID: "Velxio-Setup-XXXX" (MAC suffix)
+ * AP Password: "velxio123"
+ * Config page: http://192.168.4.1
+ */
+void wifi_ap_start_and_block(void) __attribute__((noreturn));
diff --git a/velxio-deployer-firmware/sdkconfig.defaults b/velxio-deployer-firmware/sdkconfig.defaults
index a8953c9..547e408 100644
--- a/velxio-deployer-firmware/sdkconfig.defaults
+++ b/velxio-deployer-firmware/sdkconfig.defaults
@@ -10,13 +10,18 @@ CONFIG_FREERTOS_HZ=100
CONFIG_BT_ENABLED=y
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_ROLE_PERIPHERAL=y
-CONFIG_BT_NIMBLE_SVC_GAP_DEVICE_NAME="Velxio-Deployer"
+CONFIG_BT_NIMBLE_SVC_GAP_DEVICE_NAME="Velxio"
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_MAX_BONDS=1
CONFIG_BT_NIMBLE_HS_STOP_TIMEOUT_MS=5000
CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=255
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=24
+# WiFi (for AP setup mode)
+CONFIG_ESP_WIFI_ENABLED=y
+CONFIG_ESP_WIFI_SOFTAP_SUPPORT=y
+CONFIG_ESP_WIFI_NVS_ENABLED=y
+
# PSRAM (octal, N16R8)
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_OCT=y