#include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/event_groups.h" #include "esp_system.h" #include "esp_wifi.h" #include "esp_event.h" #include "esp_log.h" #include "nvs_flash.h" #include "esp_netif.h" #include "esp_http_server.h" #include "esp_vfs.h" // For esp_vfs_spiffs_register if using SPIFFS // For TinyUSB #include "tinyusb.h" #include "tusb_cdc_acm.h" #include "sdkconfig.h" // Required for Kconfig options // WiFi credentials - REPLACE WITH YOURS #define WIFI_SSID "proxarch" #define WIFI_PASS "password" #define WIFI_MAXIMUM_RETRY 5 // Proxmark3 Communication #define PM3_CDC_ITF ITF_NUM_CDC_0 // CDC Interface number for PM3 #define PM3_RESPONSE_BUFFER_SIZE 2048 #define PM3_PROMPT "proxmark3>" #define PM3_CMD_TIMEOUT_MS 10000 // 10 seconds static char pm3_rx_buffer[PM3_RESPONSE_BUFFER_SIZE]; static uint32_t pm3_rx_buffer_len = 0; static bool pm3_connected = false; static bool pm3_command_in_progress = false; static httpd_ws_client_t* ws_clients[CONFIG_LWIP_MAX_SOCKETS - 4]; // Max clients for WebSocket static size_t ws_client_count = 0; // FreeRTOS event group to signal when we are connected static EventGroupHandle_t s_wifi_event_group; #define WIFI_CONNECTED_BIT BIT0 #define WIFI_FAIL_BIT BIT1 static int s_retry_num = 0; static const char *TAG = "PROXARCH"; // --- Forward Declarations --- static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data); void wifi_init_sta(void); static esp_err_t http_get_handler(httpd_req_t *req); static esp_err_t ws_handler(httpd_req_t *req); static void send_pm3_command(const char* command); static void send_ws_message_to_all(const char* message, size_t len); static void send_ws_pm3_status_update(void); static void tinyusb_cdc_rx_callback(int itf, cdcacm_event_t *event); static void tinyusb_cdc_line_state_changed_callback(int itf, cdcacm_event_t *event); // --- Embedded Web Page --- const char index_html_start[] = R"rawliteral( Proxarch ESP-IDF

Proxarch ESP-IDF Interface

WiFi Status: Connecting... | Proxmark3 Status: Disconnected

Proxmark3 Output:

Waiting for commands...
)rawliteral"; // --- WiFi Initialization --- static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { esp_wifi_connect(); } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { if (s_retry_num < WIFI_MAXIMUM_RETRY) { esp_wifi_connect(); s_retry_num++; ESP_LOGI(TAG, "Retrying to connect to the AP"); } else { xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT); // Send WiFi status update via WebSocket if possible char ws_msg[100]; snprintf(ws_msg, sizeof(ws_msg), "{\"type\":\"wifi_status\", \"connected\":false, \"ip\":\"N/A\"}"); send_ws_message_to_all(ws_msg, strlen(ws_msg)); } ESP_LOGI(TAG,"Connect to the AP fail"); } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data; ESP_LOGI(TAG, "Got IP:" IPSTR, IP2STR(&event->ip_info.ip)); s_retry_num = 0; xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); // Send WiFi status update via WebSocket char ws_msg[100]; snprintf(ws_msg, sizeof(ws_msg), "{\"type\":\"wifi_status\", \"connected\":true, \"ip\":\"" IPSTR "\"}", IP2STR(&event->ip_info.ip)); send_ws_message_to_all(ws_msg, strlen(ws_msg)); } } void wifi_init_sta(void) { s_wifi_event_group = xEventGroupCreate(); ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); esp_netif_create_default_wifi_sta(); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); esp_event_handler_instance_t instance_any_id; esp_event_handler_instance_t instance_got_ip; ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, &instance_any_id)); ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL, &instance_got_ip)); wifi_config_t wifi_config = { .sta = { .ssid = WIFI_SSID, .password = WIFI_PASS, .threshold.authmode = WIFI_AUTH_WPA2_PSK, // Adjust if needed }, }; ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) ); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config) ); ESP_ERROR_CHECK(esp_wifi_start() ); ESP_LOGI(TAG, "wifi_init_sta finished."); EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE, portMAX_DELAY); if (bits & WIFI_CONNECTED_BIT) { ESP_LOGI(TAG, "Connected to AP SSID:%s", WIFI_SSID); } else if (bits & WIFI_FAIL_BIT) { ESP_LOGI(TAG, "Failed to connect to SSID:%s", WIFI_SSID); } else { ESP_LOGE(TAG, "UNEXPECTED WIFI EVENT"); } } // --- TinyUSB CDC Host Callbacks & Task --- void tud_mount_cb(void) { ESP_LOGI(TAG, "TinyUSB Device MOUNTED"); } void tud_umount_cb(void) { ESP_LOGI(TAG, "TinyUSB Device UNMOUNTED"); pm3_connected = false; pm3_command_in_progress = false; send_ws_pm3_status_update(); } // Invoked when device is suspended void tud_suspend_cb(bool remote_wakeup_en) { (void) remote_wakeup_en; ESP_LOGI(TAG, "TinyUSB Device Suspended"); } // Invoked when device is resumed void tud_resume_cb(void) { ESP_LOGI(TAG, "TinyUSB Device Resumed"); } // Callback invoked when data is received from CDC host static void tinyusb_cdc_rx_callback(int itf, cdcacm_event_t *event) { size_t rx_size = 0; esp_err_t ret = tud_cdc_n_read(itf, pm3_rx_buffer + pm3_rx_buffer_len, PM3_RESPONSE_BUFFER_SIZE - pm3_rx_buffer_len -1); // -1 for null terminator if (ret == ESP_OK) { rx_size = tud_cdc_n_get_available(itf); // This might not be what we want, tud_cdc_n_read should return bytes read // For now, let's assume ret is the size or use a fixed size from a buffer. // A better way is to use the actual bytes read from tud_cdc_n_read if it returns that. // For now, we'll rely on a different mechanism to know how much was read. // This callback might be tricky with TinyUSB's API for host. // Let's simplify: assume we read into a global buffer and process it elsewhere. } // This callback structure is more for CDC *Device* mode. // For CDC *Host* mode, we typically poll tud_cdc_n_available() and call tud_cdc_n_read() in a task. // Let's adjust the approach. The main loop will handle reads. } // Callback invoked when line state change (DTR/RTS) static void tinyusb_cdc_line_state_changed_callback(int itf, cdcacm_event_t *event) { ESP_LOGI(TAG, "TinyUSB CDC ITF %d Line State Changed: DTR: %d, RTS: %d", itf, event->line_state_changed_data.dtr, event->line_state_changed_data.rts); if (event->line_state_changed_data.dtr && event->line_state_changed_data.rts) { // This is often a signal that the serial device is ready on the other side. // However, for Proxmark3, simply being connected is the main indicator. // We can set pm3_connected here if the device is enumerated. if (tud_cdc_n_mounted(itf)) { ESP_LOGI(TAG, "Proxmark3 (CDC ITF %d) is likely connected and ready.", itf); pm3_connected = true; pm3_rx_buffer_len = 0; // Clear buffer on (re)connect memset(pm3_rx_buffer, 0, PM3_RESPONSE_BUFFER_SIZE); send_ws_pm3_status_update(); } } else { // pm3_connected = false; // Handle disconnection if DTR/RTS go low after being high // send_ws_pm3_status_update(); } } // Invoked when a new CDC Host device is mounted void tud_cdc_mount_cb(uint8_t itf) { ESP_LOGI(TAG, "TinyUSB CDC Host ITF %d MOUNTED", itf); // You might want to check VID/PID here if you have multiple CDC devices // For now, assume any CDC device is the Proxmark3 // The line_state_changed_callback is often a better place to confirm "readiness" // but we can set a preliminary connected status here. cdc_line_coding_t line_coding; tud_cdc_n_get_line_coding(itf, &line_coding); ESP_LOGI(TAG, "Line Coding: %d bps, %d stop, %d parity, %d data", line_coding.bit_rate, line_coding.stop_bits, line_coding.parity, line_coding.data_bits); // Set DTR/RTS to indicate to the device that we are ready tud_cdc_n_set_control_line_state(itf, true, true); ESP_LOGI(TAG, "Set DTR & RTS for ITF %d", itf); // At this point, the device is mounted. The line_state_changed_callback will confirm DTR/RTS. // We can consider the PM3 connected here if we don't rely strictly on DTR/RTS from PM3. pm3_connected = true; pm3_rx_buffer_len = 0; memset(pm3_rx_buffer, 0, PM3_RESPONSE_BUFFER_SIZE); send_ws_pm3_status_update(); } // Invoked when a CDC Host device is unmounted void tud_cdc_umount_cb(uint8_t itf) { ESP_LOGI(TAG, "TinyUSB CDC Host ITF %d UNMOUNTED", itf); pm3_connected = false; pm3_command_in_progress = false; send_ws_pm3_status_update(); } // Task to handle TinyUSB host events and PM3 communication static void tusb_host_task(void *param) { ESP_LOGI(TAG, "TinyUSB Host task started"); tusb_init(); // Initialize TinyUSB stack while (1) { tuh_task(); // TinyUSB host task runner if (pm3_connected && pm3_command_in_progress) { if (tud_cdc_n_available(PM3_CDC_ITF)) { uint32_t count = tud_cdc_n_read(PM3_CDC_ITF, (uint8_t*)pm3_rx_buffer + pm3_rx_buffer_len, PM3_RESPONSE_BUFFER_SIZE - pm3_rx_buffer_len - 1); if (count > 0) { pm3_rx_buffer_len += count; pm3_rx_buffer[pm3_rx_buffer_len] = '\0'; // Null-terminate // Send chunk to WebSocket char chunk_json[PM3_RESPONSE_BUFFER_SIZE + 50]; // Extra space for JSON overhead snprintf(chunk_json, sizeof(chunk_json), "{\"type\":\"pm3_response_chunk\", \"chunk\":\"%.*s\"}", (int)count, pm3_rx_buffer + (pm3_rx_buffer_len - count) ); // Need to escape the chunk before putting into JSON // For simplicity, this is omitted here but is CRITICAL for robust JSON. // The JS side has escapeHtml, but C side needs to escape for JSON string. // Let's assume for now the data is mostly ASCII and doesn't break JSON. // A proper implementation would use a JSON library or manual escaping. // For now, let's send the raw new data. char temp_chunk_buffer[count + 1]; memcpy(temp_chunk_buffer, pm3_rx_buffer + (pm3_rx_buffer_len - count), count); temp_chunk_buffer[count] = '\0'; // Proper escaping for JSON (simplified) char escaped_chunk_buffer[count * 2 + 1]; // Worst case if all chars need escaping int j = 0; for(int i=0; i < count; i++) { char ch = temp_chunk_buffer[i]; if (ch == '"' || ch == '\\' || ch == '/') { escaped_chunk_buffer[j++] = '\\'; escaped_chunk_buffer[j++] = ch; } else if (ch == '\b') { escaped_chunk_buffer[j++] = '\\'; escaped_chunk_buffer[j++] = 'b'; } else if (ch == '\f') { escaped_chunk_buffer[j++] = '\\'; escaped_chunk_buffer[j++] = 'f'; } else if (ch == '\n') { escaped_chunk_buffer[j++] = '\\'; escaped_chunk_buffer[j++] = 'n'; } else if (ch == '\r') { escaped_chunk_buffer[j++] = '\\'; escaped_chunk_buffer[j++] = 'r'; } else if (ch == '\t') { escaped_chunk_buffer[j++] = '\\'; escaped_chunk_buffer[j++] = 't'; } else if (ch < 32) { /* skip control chars or use \uXXXX */ } else { escaped_chunk_buffer[j++] = ch; } } escaped_chunk_buffer[j] = '\0'; snprintf(chunk_json, sizeof(chunk_json), "{\"type\":\"pm3_response_chunk\", \"chunk\":\"%s\"}", escaped_chunk_buffer); send_ws_message_to_all(chunk_json, strlen(chunk_json)); // Check for prompt if (pm3_rx_buffer_len >= strlen(PM3_PROMPT)) { if (strstr(pm3_rx_buffer, PM3_PROMPT) != NULL) { ESP_LOGI(TAG, "PM3 Prompt detected. Command finished."); pm3_command_in_progress = false; pm3_rx_buffer_len = 0; // Clear buffer for next command memset(pm3_rx_buffer, 0, PM3_RESPONSE_BUFFER_SIZE); send_ws_message_to_all("{\"type\":\"pm3_response_end\"}", strlen("{\"type\":\"pm3_response_end\"}")); } } // Buffer full without prompt? Error or very long output. if (pm3_rx_buffer_len >= PM3_RESPONSE_BUFFER_SIZE -1) { ESP_LOGW(TAG, "PM3 RX Buffer full without prompt!"); // Send what we have and signal end/error pm3_command_in_progress = false; pm3_rx_buffer_len = 0; memset(pm3_rx_buffer, 0, PM3_RESPONSE_BUFFER_SIZE); send_ws_message_to_all("{\"type\":\"pm3_error\", \"message\":\"RX buffer full\"}", strlen("{\"type\":\"pm3_error\", \"message\":\"RX buffer full\"}")); } } } else { // Check for timeout if command is in progress // This needs a timer associated with when the command was sent. // For simplicity, this is omitted but important for robustness. } } vTaskDelay(pdMS_TO_TICKS(10)); // Yield for other tasks } } // --- HTTP Server Handlers --- static esp_err_t http_get_handler(httpd_req_t *req) { httpd_resp_set_type(req, "text/html"); httpd_resp_send(req, index_html_start, HTTPD_RESP_USE_STRLEN); return ESP_OK; } static void send_ws_message_to_all(const char* message, size_t len) { for (size_t i = 0; i < ws_client_count; ++i) { httpd_ws_client_t *client = ws_clients[i]; if (client) { httpd_ws_frame_t ws_pkt; memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); ws_pkt.payload = (uint8_t*)message; ws_pkt.len = len; ws_pkt.type = HTTPD_WS_TYPE_TEXT; httpd_ws_send_frame_async(client->hd, client->fd, &ws_pkt); } } } static void send_ws_pm3_status_update(void) { char status_msg[100]; snprintf(status_msg, sizeof(status_msg), "{\"type\":\"pm3_status\", \"connected\":%s}", pm3_connected ? "true" : "false"); send_ws_message_to_all(status_msg, strlen(status_msg)); } static esp_err_t ws_handler(httpd_req_t *req) { if (req->method == HTTP_GET) { // Check if this client is already tracked bool client_exists = false; for(size_t i = 0; i < ws_client_count; ++i) { if(ws_clients[i] && ws_clients[i]->fd == req->handle->fd) { client_exists = true; break; } } if (!client_exists && ws_client_count < (CONFIG_LWIP_MAX_SOCKETS - 4)) { // Find an empty slot or add to the end size_t new_client_idx = ws_client_count; for(size_t i = 0; i < ws_client_count; ++i) { // Reuse slot if any client disconnected if(ws_clients[i] == NULL) { new_client_idx = i; break; } } httpd_ws_client_t *client = calloc(1, sizeof(httpd_ws_client_t)); if (!client) { ESP_LOGE(TAG, "Failed to allocate memory for new WS client"); return ESP_FAIL; } client->hd = req->handle; client->fd = httpd_req_to_sockfd(req); // Get socket descriptor if (new_client_idx == ws_client_count) { // Adding new client ws_clients[ws_client_count++] = client; } else { // Reusing slot ws_clients[new_client_idx] = client; } ESP_LOGI(TAG, "New WebSocket client connected, fd=%d, total_clients=%d", client->fd, ws_client_count); // Send initial status send_ws_pm3_status_update(); // Send PM3 status // Send WiFi status (already done by wifi_event_handler, but good for new client) char ws_msg[100]; if ( (xEventGroupGetBits(s_wifi_event_group) & WIFI_CONNECTED_BIT) ) { esp_netif_ip_info_t ip_info; esp_netif_get_ip_info(esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"), &ip_info); snprintf(ws_msg, sizeof(ws_msg), "{\"type\":\"wifi_status\", \"connected\":true, \"ip\":\"" IPSTR "\"}", IP2STR(&ip_info.ip)); } else { snprintf(ws_msg, sizeof(ws_msg), "{\"type\":\"wifi_status\", \"connected\":false, \"ip\":\"N/A\"}"); } httpd_ws_frame_t pkt; memset(&pkt, 0, sizeof(httpd_ws_frame_t)); pkt.payload = (uint8_t*)ws_msg; pkt.len = strlen(ws_msg); pkt.type = HTTPD_WS_TYPE_TEXT; httpd_ws_send_frame_async(client->hd, client->fd, &pkt); return ESP_OK; // Keep connection open for WebSocket } else { ESP_LOGW(TAG, "Max WebSocket clients reached or client already exists."); return ESP_FAIL; // Or send 429 Too Many Requests } } // Handle WebSocket data frames uint8_t buf[128]; httpd_ws_frame_t ws_pkt; memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); ws_pkt.payload = buf; ws_pkt.type = HTTPD_WS_TYPE_TEXT; // Expecting text esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, sizeof(buf) -1); // -1 for null terminator if (ret != ESP_OK) { ESP_LOGE(TAG, "httpd_ws_recv_frame failed with %d", ret); // Client disconnected or error for(size_t i = 0; i < ws_client_count; ++i) { if(ws_clients[i] && ws_clients[i]->fd == httpd_req_to_sockfd(req)) { ESP_LOGI(TAG, "WebSocket client disconnected, fd=%d", ws_clients[i]->fd); free(ws_clients[i]); ws_clients[i] = NULL; // To properly manage the array, shift elements or mark as NULL and reuse // For simplicity, just NULLing. A robust solution would compact the array. // If it was the last element, decrement count if (i == ws_client_count -1) ws_client_count--; break; } } return ret; } if (ws_pkt.len > 0 && ws_pkt.type == HTTPD_WS_TYPE_TEXT) { buf[ws_pkt.len] = '\0'; // Null-terminate ESP_LOGI(TAG, "Got WS packet with message: %s", buf); // Basic JSON parsing: {"type":"pm3_command", "cmd":"hw version"} // Using simple strstr for this example. A JSON library (e.g. cJSON) is recommended for robust parsing. char* type_str = strstr((char*)buf, "\"type\":\"pm3_command\""); if (type_str) { char* cmd_start_str = strstr((char*)buf, "\"cmd\":\""); if (cmd_start_str) { cmd_start_str += strlen("\"cmd\":\""); // Move pointer to start of command char* cmd_end_str = strchr(cmd_start_str, '"'); if (cmd_end_str) { int cmd_len = cmd_end_str - cmd_start_str; if (cmd_len > 0 && cmd_len < 100) { // Basic sanity check for command length char command[101]; strncpy(command, cmd_start_str, cmd_len); command[cmd_len] = '\0'; ESP_LOGI(TAG, "Parsed PM3 command: %s", command); send_pm3_command(command); } } } } else if (strstr((char*)buf, "\"type\":\"client_hello\"")) { ESP_LOGI(TAG, "Client says hello!"); // Already sent status on connect, can send ack if needed } } return ESP_OK; } static const httpd_uri_t uri_get = { .uri = "/", .method = HTTP_GET, .handler = http_get_handler, .user_ctx = NULL }; static const httpd_uri_t ws = { .uri = "/ws", .method = HTTP_GET, .handler = ws_handler, .user_ctx = NULL, .is_websocket = true }; static httpd_handle_t start_webserver(void) { httpd_handle_t server = NULL; httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.max_open_sockets = 7; // Default is 7. Need at least 1 for HTTP, 1 for WS. config.lru_purge_enable = true; ESP_LOGI(TAG, "Starting httpd server on port: '%d'", config.server_port); if (httpd_start(&server, &config) == ESP_OK) { ESP_LOGI(TAG, "Registering URI handlers"); httpd_register_uri_handler(server, &uri_get); httpd_register_uri_handler(server, &ws); return server; } ESP_LOGI(TAG, "Error starting server!"); return NULL; } // --- Proxmark3 Command Logic --- void send_pm3_command(const char* command) { if (!pm3_connected) { ESP_LOGE(TAG, "Cannot send command: Proxmark3 not connected."); send_ws_message_to_all("{\"type\":\"pm3_error\", \"message\":\"Proxmark3 not connected\"}", strlen("{\"type\":\"pm3_error\", \"message\":\"Proxmark3 not connected\"}")); return; } if (pm3_command_in_progress) { ESP_LOGW(TAG, "Cannot send command: Another command is already in progress."); send_ws_message_to_all("{\"type\":\"pm3_error\", \"message\":\"Command already in progress\"}", strlen("{\"type\":\"pm3_error\", \"message\":\"Command already in progress\"}")); return; } ESP_LOGI(TAG, "Sending to PM3: %s", command); pm3_command_in_progress = true; pm3_rx_buffer_len = 0; // Clear RX buffer memset(pm3_rx_buffer, 0, PM3_RESPONSE_BUFFER_SIZE); send_ws_message_to_all("{\"type\":\"pm3_response_start\"}", strlen("{\"type\":\"pm3_response_start\"}")); // Ensure command ends with newline for Proxmark3 char full_command[128]; snprintf(full_command, sizeof(full_command), "%s\n", command); if (tud_cdc_n_connected(PM3_CDC_ITF)) { tud_cdc_n_write_str(PM3_CDC_ITF, full_command); tud_cdc_n_write_flush(PM3_CDC_ITF); } else { ESP_LOGE(TAG, "PM3 CDC not connected for writing."); pm3_command_in_progress = false; // Reset flag send_ws_message_to_all("{\"type\":\"pm3_error\", \"message\":\"Proxmark3 CDC write error\"}", strlen("{\"type\":\"pm3_error\", \"message\":\"Proxmark3 CDC write error\"}")); } } // --- Main Application --- void app_main(void) { esp_err_t ret = nvs_flash_init(); if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { ESP_ERROR_CHECK(nvs_flash_erase()); ret = nvs_flash_init(); } ESP_ERROR_CHECK(ret); ESP_LOGI(TAG, "ESP_WIFI_MODE_STA"); wifi_init_sta(); // Initialize TinyUSB stack for Host mode // This is now done in the tusb_host_task // tusb_init(); // ESP_LOGI(TAG, "TinyUSB tud_init() done."); // Create a task for TinyUSB host stack and PM3 comms // Stack size for TinyUSB host might need adjustment. // USB operations, especially with potential string processing, can be stack intensive. // Let's increase it from the default. xTaskCreate(tusb_host_task, "tusb_host_task", 4096 * 2, NULL, 5, NULL); ESP_LOGI(TAG, "tusb_host_task created."); // Start Webserver start_webserver(); ESP_LOGI(TAG, "Proxarch ESP-IDF Initialized."); }