Files
iceman-webui/iceman_web_ui_esp32/main/proxarch_main.c

708 lines
31 KiB
C

#include <stdio.h>
#include <string.h>
#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(
<!DOCTYPE HTML><html>
<head>
<title>Proxarch ESP-IDF</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: Arial, sans-serif; background-color: #1e1e1e; color: #d4d4d4; margin: 0; padding: 20px; display: flex; flex-direction: column; align-items: center; }
h1 { color: #569cd6; }
.container { background-color: #252526; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.5); width: 90%; max-width: 800px; }
.button-group { margin-bottom: 20px; display: flex; flex-wrap: wrap; gap: 10px; justify-content: center;}
button { background-color: #0e639c; color: white; border: none; padding: 10px 15px; text-align: center; text-decoration: none; display: inline-block; font-size: 16px; border-radius: 4px; cursor: pointer; transition: background-color 0.3s; }
button:hover { background-color: #1177bb; }
button:active { background-color: #0c5586; }
#output { background-color: #1e1e1e; color: #d4d4d4; border: 1px solid #333; padding: 10px; height: 400px; overflow-y: scroll; white-space: pre-wrap; font-family: 'Courier New', Courier, monospace; margin-top: 20px; border-radius: 4px; }
.status { margin-bottom:15px; font-size:0.9em; text-align:center; }
#pm3Status { font-weight: bold; }
#wifiStatus { font-weight: bold; }
.loader { border: 5px solid #f3f3f3; border-top: 5px solid #569cd6; border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; margin: 10px auto; display: none; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
</style>
</head>
<body>
<h1>Proxarch ESP-IDF Interface</h1>
<div class="container">
<div class="status">
WiFi Status: <span id="wifiStatus">Connecting...</span> | Proxmark3 Status: <span id="pm3Status">Disconnected</span>
</div>
<div class="button-group">
<button onclick="sendCommand('hw version')">Get Version</button>
<button onclick="sendCommand('hw tune')">Tune Antennas</button>
<button onclick="sendCommand('lf search')">LF Search</button>
<button onclick="sendCommand('hf search')">HF Search</button>
<button onclick="sendCommand('data buffclear')">Clear Buffer</button>
</div>
<div id="loader" class="loader"></div>
<h3>Proxmark3 Output:</h3>
<div id="output">Waiting for commands...</div>
</div>
<script>
var gateway = `ws://${window.location.hostname}/ws`;
var websocket;
var outputDiv = document.getElementById('output');
var loaderDiv = document.getElementById('loader');
var pm3StatusSpan = document.getElementById('pm3Status');
var wifiStatusSpan = document.getElementById('wifiStatus');
window.addEventListener('load', onLoad);
function onLoad(event) {
initWebSocket();
// WiFi status will be updated by server push or initial message
}
function initWebSocket() {
console.log('Trying to open a WebSocket connection...');
websocket = new WebSocket(gateway);
websocket.onopen = onOpen;
websocket.onclose = onClose;
websocket.onmessage = onMessage;
}
function onOpen(event) {
console.log('Connection opened');
websocket.send(JSON.stringify({type: "client_hello"}));
updateWifiStatus(true, window.location.hostname);
}
function onClose(event) {
console.log('Connection closed');
pm3StatusSpan.textContent = "Disconnected (WS)";
pm3StatusSpan.style.color = "#ff6347"; // Red
updateWifiStatus(false);
setTimeout(initWebSocket, 2000); // Try to reconnect
}
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function onMessage(event) {
// console.log(event.data);
var data;
try {
data = JSON.parse(event.data);
} catch (e) {
console.error("Error parsing WebSocket JSON:", e);
// If not JSON, treat as raw string for output (e.g. direct PM3 data)
outputDiv.innerHTML += escapeHtml(event.data) + '\\n';
outputDiv.scrollTop = outputDiv.scrollHeight;
return;
}
if(data.type === 'pm3_status') {
if(data.connected) {
pm3StatusSpan.textContent = "Connected";
pm3StatusSpan.style.color = "#32cd32"; // Green
} else {
pm3StatusSpan.textContent = "Disconnected";
pm3StatusSpan.style.color = "#ff6347"; // Red
}
} else if (data.type === 'wifi_status') {
updateWifiStatus(data.connected, data.ip);
} else if (data.type === 'pm3_response_start') {
outputDiv.innerHTML = ''; // Clear previous output
loaderDiv.style.display = 'block';
} else if (data.type === 'pm3_response_chunk') {
outputDiv.innerHTML += escapeHtml(data.chunk);
outputDiv.scrollTop = outputDiv.scrollHeight; // Auto-scroll
} else if (data.type === 'pm3_response_end') {
loaderDiv.style.display = 'none';
outputDiv.scrollTop = outputDiv.scrollHeight;
} else if (data.type === 'pm3_error' || data.type === 'error') {
outputDiv.innerHTML += `<span style="color: #ff6347;">ERROR: ${escapeHtml(data.message)}</span>\\n`;
loaderDiv.style.display = 'none';
}
}
function sendCommand(command) {
if (websocket.readyState === WebSocket.OPEN) {
// outputDiv.innerHTML = ''; // Clear previous output on new command - handled by response_start
loaderDiv.style.display = 'block';
console.log("Sending command: " + command);
websocket.send(JSON.stringify({type: "pm3_command", cmd: command}));
} else {
console.log("WebSocket is not open. Cannot send command.");
outputDiv.innerHTML = "WebSocket connection is closed. Please wait or refresh.";
}
}
function updateWifiStatus(connected, ipAddress = '') {
if(connected) {
wifiStatusSpan.textContent = `Connected (${ipAddress})`;
wifiStatusSpan.style.color = "#32cd32"; // Green
} else {
wifiStatusSpan.textContent = "Disconnected";
wifiStatusSpan.style.color = "#ff6347"; // Red
}
}
</script>
</body>
</html>
)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.");
}