From 322f7f0699bac16b37942617576ef7d3a0f7eb93 Mon Sep 17 00:00:00 2001 From: pulipakaa24 Date: Tue, 23 Dec 2025 17:21:44 -0600 Subject: [PATCH] CONNECTS TO WEBSOCKET!! --- .gitignore | 5 +- dependencies.lock | 28 +- include/BLE.cpp | 7 +- include/WiFi.cpp | 32 +- include/bmHTTP.cpp | 44 ++- include/setup.cpp | 3 +- include/socketIO.cpp | 119 +++++++ include/socketIO.hpp | 14 + index.js | 599 +++++++++++++++++++++++++++++++++++ sdkconfig.seeed_xiao_esp32c6 | 11 + src/CMakeLists.txt | 2 +- src/idf_component.yml | 2 + src/main.cpp | 98 ++++-- 13 files changed, 903 insertions(+), 61 deletions(-) create mode 100644 include/socketIO.cpp create mode 100644 include/socketIO.hpp create mode 100644 index.js create mode 100644 src/idf_component.yml diff --git a/.gitignore b/.gitignore index e01547b..e9dbd1d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,7 @@ .vscode/launch.json .vscode/ipch -.DS_Store \ No newline at end of file +.DS_Store + +managed_components/ +compile_commands.json \ No newline at end of file diff --git a/dependencies.lock b/dependencies.lock index 78e0bd3..9347bb2 100644 --- a/dependencies.lock +++ b/dependencies.lock @@ -1,8 +1,34 @@ dependencies: + bubblesnake/esp_socketio_client: + component_hash: 6f9d397a583384ca80779af0b2861b8cee99509a308f8b3f25524d8f4ba8f8f9 + dependencies: + - name: espressif/esp_websocket_client + registry_url: https://components.espressif.com + require: public + version: '>=1.2.3' + - name: idf + require: private + version: '>=5.0' + source: + registry_url: https://components.espressif.com/ + type: service + version: 1.0.0 + espressif/esp_websocket_client: + component_hash: 723aba370113196c66321442426cd6452c351eef31c85c83bd1446831ef9f8f4 + dependencies: + - name: idf + require: private + version: '>=5.0' + source: + registry_url: https://components.espressif.com + type: service + version: 1.6.0 idf: source: type: idf version: 5.5.1 -manifest_hash: 24b4d087511378b31aec46ab95615da067c81c6477aaa4515b222e977e29004a +direct_dependencies: +- bubblesnake/esp_socketio_client +manifest_hash: d73c96c5d6ddd24707089a2953e50f36a12ebbc66b5458ada3d4f55c0987ccf1 target: esp32c6 version: 2.0.0 diff --git a/include/BLE.cpp b/include/BLE.cpp index 08b890b..b5b7d7d 100644 --- a/include/BLE.cpp +++ b/include/BLE.cpp @@ -2,6 +2,7 @@ #include "NimBLEDevice.h" #include "WiFi.hpp" #include "nvs_flash.h" +#include "socketIO.hpp" #include "defines.h" #include #include "bmHTTP.hpp" @@ -46,7 +47,7 @@ NimBLEAdvertising* initBLE() { // Create all characteristics with callbacks MyCharCallbacks* charCallbacks = new MyCharCallbacks(); - // 0x0000 - SSID List (READ + NOTIFY) + // 0x0000 - SSID List (READ) ssidListChar = pService->createCharacteristic( "0000", NIMBLE_PROPERTY::READ @@ -75,7 +76,7 @@ NimBLEAdvertising* initBLE() { ); authConfirmChar.load()->createDescriptor("2902"); // Add BLE2902 descriptor for notifications - // 0x0004 - SSID Refresh (WRITE) + // 0x0004 - SSID Refresh ssidRefreshChar = pService->createCharacteristic( "0004", NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY @@ -119,6 +120,7 @@ void notifyAuthStatus(bool success) { } bool BLEtick(NimBLEAdvertising* pAdvertising) { + printf("BleTick\n"); if(flag_scan_requested) { flag_scan_requested = false; if (!scanBlock) { @@ -229,6 +231,7 @@ bool BLEtick(NimBLEAdvertising* pAdvertising) { void reset() { esp_wifi_scan_stop(); + if (!connected) esp_wifi_disconnect(); scanBlock = false; flag_scan_requested = false; credsGiven = false; diff --git a/include/WiFi.cpp b/include/WiFi.cpp index fe3fc1d..22704c7 100644 --- a/include/WiFi.cpp +++ b/include/WiFi.cpp @@ -10,6 +10,7 @@ esp_event_handler_instance_t WiFi::instance_any_id = NULL; esp_event_handler_instance_t WiFi::instance_got_ip = NULL; #define WIFI_CONNECTED_BIT BIT0 +#define WIFI_STARTED_BIT BIT1 WiFi bmWiFi; @@ -17,8 +18,13 @@ WiFi bmWiFi; void WiFi::event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { + // WiFi driver has finished initialization + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { + printf("WiFi initialized and ready\n"); + xEventGroupSetBits(s_wifi_event_group, WIFI_STARTED_BIT); + } // We got disconnected -> Retry automatically - if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { + else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { // 1. Cast the data to the correct struct wifi_event_sta_disconnected_t* event = (wifi_event_sta_disconnected_t*) event_data; @@ -29,11 +35,9 @@ void WiFi::event_handler(void* arg, esp_event_base_t event_base, case WIFI_REASON_AUTH_EXPIRE: // Reason 2 case WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT: // Reason 15 (Most Common for Wrong Pass) case WIFI_REASON_BEACON_TIMEOUT: // Reason 200 - case WIFI_REASON_AUTH_FAIL: // Reason 203 - case WIFI_REASON_ASSOC_FAIL: // Reason 204 - case WIFI_REASON_HANDSHAKE_TIMEOUT: // Reason 205 + case WIFI_REASON_AUTH_FAIL: // Reason 202 + case WIFI_REASON_HANDSHAKE_TIMEOUT: // Reason 204 printf("ERROR: Likely Wrong Password!\n"); - // OPTIONAL: Don't retry immediately if password is wrong authFailed = true; break; @@ -41,10 +45,20 @@ void WiFi::event_handler(void* arg, esp_event_base_t event_base, printf("ERROR: SSID Not Found\n"); authFailed = true; break; + + case WIFI_REASON_ASSOC_LEAVE: // Reason 8 - Manual disconnect + printf("Manual disconnect, not retrying\n"); + break; + + case WIFI_REASON_ASSOC_FAIL: // Reason 203 (Can be AP busy/rate limiting) + printf("Association failed, will retry...\n"); + vTaskDelay(pdMS_TO_TICKS(1000)); // Wait 1 second before retry to avoid rate limiting + esp_wifi_connect(); + break; default: printf("Retrying...\n"); - esp_wifi_connect(); // Retry for other reasons (interference, etc) + esp_wifi_connect(); break; } xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT); @@ -95,6 +109,7 @@ void WiFi::init() { )); ESP_ERROR_CHECK(esp_wifi_start()); + xEventGroupWaitBits(s_wifi_event_group, WIFI_STARTED_BIT, pdFALSE, pdTRUE, portMAX_DELAY); } // --- CHECK STATUS --- @@ -162,11 +177,6 @@ bool WiFi::awaitConnected() { uint8_t attempts = 0; while (!isConnected() && attempts < 20) { - if (!isBLEClientConnected) { - printf("BLE Disconnected: Aborting WiFi connection attempt.\n"); - esp_wifi_disconnect(); - return false; - } if (authFailed) { printf("SSID/Password was wrong! Aborting connection attempt.\n"); return false; diff --git a/include/bmHTTP.cpp b/include/bmHTTP.cpp index e35f6ce..85f00ff 100644 --- a/include/bmHTTP.cpp +++ b/include/bmHTTP.cpp @@ -17,29 +17,47 @@ bool httpGET(std::string endpoint, std::string token, cJSON* &JSONresponse) { std::string authHeader = "Bearer " + token; esp_http_client_set_header(client, "Authorization", authHeader.c_str()); - esp_err_t err = esp_http_client_perform(client); + // Open connection and fetch headers + esp_err_t err = esp_http_client_open(client, 0); bool success = false; if (err == ESP_OK) { + int content_length = esp_http_client_fetch_headers(client); int status_code = esp_http_client_get_status_code(client); - int content_length = esp_http_client_get_content_length(client); printf("HTTP Status = %d, content_length = %d\n", status_code, content_length); - if (status_code == 200) { - std::string responseData = ""; - char buffer[512]; // Read in 512-byte blocks - int read_len; - - // Read until the server stops sending (read_len <= 0) - while ((read_len = esp_http_client_read(client, buffer, sizeof(buffer))) > 0) - responseData.append(buffer, read_len); - - if (!responseData.empty()) { - JSONresponse = cJSON_Parse(responseData.c_str()); + if (status_code == 200 && content_length > 0) { + // Allocate buffer for the response + char *buffer = (char *)malloc(content_length + 1); + if (buffer) { + int total_read = 0; + int read_len; + + // Read the response body + while (total_read < content_length) { + read_len = esp_http_client_read(client, buffer + total_read, content_length - total_read); + if (read_len <= 0) break; + total_read += read_len; + } + + buffer[total_read] = '\0'; + printf("Response body: %s\n", buffer); + + JSONresponse = cJSON_Parse(buffer); success = (JSONresponse != NULL); + + if (!success) { + printf("Failed to parse JSON\n"); + } + + free(buffer); + } else { + printf("Failed to allocate buffer for response\n"); } } + + esp_http_client_close(client); } else printf("HTTP request failed: %s\n", esp_err_to_name(err)); esp_http_client_cleanup(client); diff --git a/include/setup.cpp b/include/setup.cpp index 26c68e3..3f0cc32 100644 --- a/include/setup.cpp +++ b/include/setup.cpp @@ -3,9 +3,10 @@ #include "WiFi.hpp" void initialSetup() { + printf("Entered Setup\n"); NimBLEAdvertising* pAdv = initBLE(); while (!BLEtick(pAdv)) { - vTaskDelay(pdMS_TO_TICKS(10)); + vTaskDelay(pdMS_TO_TICKS(100)); } } \ No newline at end of file diff --git a/include/socketIO.cpp b/include/socketIO.cpp new file mode 100644 index 0000000..4603570 --- /dev/null +++ b/include/socketIO.cpp @@ -0,0 +1,119 @@ +#include "socketIO.hpp" +#include "esp_socketio_client.h" +#include "bmHTTP.hpp" // To access webToken +#include "WiFi.hpp" +#include "setup.hpp" +#include "cJSON.h" + +static esp_socketio_client_handle_t io_client; +static esp_socketio_packet_handle_t tx_packet = NULL; + +std::atomic statusResolved{false}; +std::atomic connected{false}; + +// Event handler for Socket.IO events +static void socketio_event_handler(void *handler_args, esp_event_base_t base, + int32_t event_id, void *event_data) { + esp_socketio_event_data_t *data = (esp_socketio_event_data_t *)event_data; + esp_socketio_packet_handle_t packet = data->socketio_packet; + + switch (event_id) { + case SOCKETIO_EVENT_OPENED: + printf("Socket.IO Received OPEN packet\n"); + // Connect to default namespace "/" + esp_socketio_client_connect_nsp(data->client, NULL, NULL); + break; + + case SOCKETIO_EVENT_NS_CONNECTED: { + printf("Socket.IO Connected to namespace!\n"); + // Check if connected to default namespace + char *nsp = esp_socketio_packet_get_nsp(packet); + if (strcmp(nsp, "/") == 0) { + printf("Connected to default namespace\n"); + } + connected = true; + statusResolved = true; + break; + } + + case SOCKETIO_EVENT_DATA: { + printf("Received Socket.IO data\n"); + // Parse the received packet + cJSON *json = esp_socketio_packet_get_json(packet); + if (json) { + char *json_str = cJSON_Print(json); + printf("Data: %s\n", json_str); + free(json_str); + } + break; + } + + case SOCKETIO_EVENT_ERROR: { + printf("Socket.IO Error!\n"); + + // Check WebSocket error details + esp_websocket_event_data_t *ws_event = data->websocket_event; + if (ws_event && ws_event->error_handle.esp_ws_handshake_status_code != 0) { + printf("HTTP Status: %d\n", ws_event->error_handle.esp_ws_handshake_status_code); + if (ws_event->error_handle.esp_ws_handshake_status_code == 401 || + ws_event->error_handle.esp_ws_handshake_status_code == 403) { + printf("Authentication failed - invalid token\n"); + } + } + + // Disconnect and enter setup mode + printf("Disconnecting and entering setup mode...\n"); + esp_socketio_client_close(io_client, pdMS_TO_TICKS(1000)); + + esp_socketio_client_destroy(io_client); + initialSetup(); + connected = false; + statusResolved = true; + break; + } + } +} + +void initSocketIO() { + // Prepare the Authorization Header (Bearer format) + std::string authHeader = "Authorization: Bearer " + webToken + "\r\n"; + + statusResolved = false; + connected = false; + + esp_socketio_client_config_t config = {}; + config.websocket_config.uri = "ws://192.168.1.190:3000/socket.io/?EIO=4&transport=websocket"; + config.websocket_config.headers = authHeader.c_str(); + + io_client = esp_socketio_client_init(&config); + tx_packet = esp_socketio_client_get_tx_packet(io_client); + + esp_socketio_register_events(io_client, SOCKETIO_EVENT_ANY, socketio_event_handler, NULL); + esp_socketio_client_start(io_client); +} + +// Function to emit 'calib_done' as expected by your server +void emitCalibDone(int port) { + // Set packet header: EIO MESSAGE type, SIO EVENT type, default namespace "/" + if (esp_socketio_packet_set_header(tx_packet, EIO_PACKET_TYPE_MESSAGE, + SIO_PACKET_TYPE_EVENT, NULL, -1) == ESP_OK) { + // Create JSON array with event name and data + cJSON *array = cJSON_CreateArray(); + cJSON_AddItemToArray(array, cJSON_CreateString("calib_done")); + + cJSON *data = cJSON_CreateObject(); + cJSON_AddNumberToObject(data, "port", port); + cJSON_AddItemToArray(array, data); + + // Set the JSON payload + esp_socketio_packet_set_json(tx_packet, array); + + // Send the packet + esp_socketio_client_send_data(io_client, tx_packet); + + // Reset packet for reuse + esp_socketio_packet_reset(tx_packet); + + cJSON_Delete(array); + } +} \ No newline at end of file diff --git a/include/socketIO.hpp b/include/socketIO.hpp new file mode 100644 index 0000000..31a3408 --- /dev/null +++ b/include/socketIO.hpp @@ -0,0 +1,14 @@ +#ifndef SOCKETIO_HPP +#define SOCKETIO_HPP +#include + +extern std::atomic statusResolved; +extern std::atomic connected; + +// Initialize Socket.IO client and connect to server +void initSocketIO(); + +// Emit calibration done event to server +void emitCalibDone(int port); + +#endif // SOCKETIO_HPP \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..28c952b --- /dev/null +++ b/index.js @@ -0,0 +1,599 @@ +const { verify, hash } = require('argon2'); +const express = require('express'); +const { json } = require('express'); +const { Pool } = require('pg'); +require('dotenv').config(); +const jwt = require('jsonwebtoken'); +const http = require('http'); +const socketIo = require('socket.io'); +const connectDB = require('./db'); // Your Mongoose DB connection +const { initializeAgenda } = require('./agenda'); // Agenda setup +const format = require('pg-format'); +const cronParser = require('cron-parser'); + +const app = express(); +const port = 3000; +app.use(json()); +const pool = new Pool(); +const server = http.createServer(app); +const io = socketIo(server, { + pingInterval: 15000, + pingTimeout: 10000, +}); + +let agenda; + +(async () => { + // 1. Connect to MongoDB (for your application data) + await connectDB(); + + agenda = await initializeAgenda('mongodb://localhost:27017/myScheduledApp', pool, io); +})(); + +(async () => { + await pool.query("update user_tokens set connected=FALSE where connected=TRUE"); + await pool.query("update device_tokens set connected=FALSE where connected=TRUE"); +})(); +const JWT_SECRET = process.env.JWT_SECRET; +const TOKEN_EXPIRY = '5d'; + +io.on('connection', async (socket) => { + console.log("periph connected"); + const token = socket.handshake.auth.token ?? socket.handshake.headers['authorization']?.split(' ')[1]; + try { + if (!token) throw new Error("no token!"); + const payload = jwt.verify(token, JWT_SECRET); + let table; + let idCol; + let id; + + if (payload.type === "user") { + table = "user_tokens"; + idCol = "user_id"; + socket.user = true; + id = payload.userId; + } + else if (payload.type === "peripheral") { + table = "device_tokens"; + idCol = "device_id"; + socket.user = false; + id = payload.peripheralId; + } + else { + throw new Error("Unauthorized"); + } + + const query = format(`update %I set connected=TRUE, socket=$1 where %I=$2 and token=$3 and connected=FALSE`, + table, idCol + ); + + const result = await pool.query(query, [socket.id, id, token]); + + if (result.rowCount != 1) { + const errorResponse = { + type: 'error', + code: 404, + message: 'Device not found or already connected' + }; + socket.emit("error", errorResponse); + socket.disconnect(true); + } + + else { + const successResponse = { + type: 'success', + code: 200, + message: 'Device found' + }; + console.log("success"); + socket.emit("success", successResponse); + } + + } catch (error) { + console.error('Error during periph authentication:', error); + + // Send an error message to the client + socket.emit('error', { type: 'error', code: 500 }); + + // Disconnect the client + socket.disconnect(true); + } + + socket.on('calib_done', async (data) => { + console.log(`Received 'calib_done' event from client ${socket.id}:`); + console.log(data); + try { + const {rows} = await pool.query("select device_id from device_tokens where socket=$1 and connected=TRUE", [socket.id]); + if (rows.length != 1) throw new Error("No device with that ID connected to Socket."); + const result = await pool.query("update peripherals set await_calib=FALSE, calibrated=TRUE where device_id=$1 and peripheral_number=$2 returning id, user_id", + [rows[0].device_id, data.port] + ); + if (result.rowCount != 1) throw new Error("No such peripheral in database"); + + const {rows: userRows} = await pool.query("select socket from user_tokens where user_id=$1", [result.rows[0].user_id]); + if (userRows.length == 1) { + if (userRows[0]){ + const userSocket = userRows[0].socket; + io.to(userSocket).emit("calib_done", {periphID: result.rows[0].id}); + } + } + else console.log("No App connected"); + + } catch (error) { + console.error(`Error processing job:`, error); + throw error; + } + }); + + socket.on('pos_hit', async (data) => { + console.log(`Received 'pos_hit' event from client ${socket.id}:`); + console.log(data); + const dateTime = new Date(); + try { + const {rows} = await pool.query("select device_id from device_tokens where socket=$1 and connected=TRUE", [socket.id]); + if (rows.length != 1) throw new Error("No device with that ID connected to Socket."); + const result = await pool.query("update peripherals set last_pos=$1, last_set=$2 where device_id=$3 and peripheral_number=$4 returning id, user_id", + [data.pos, dateTime, rows[0].device_id, data.port] + ); + if (result.rowCount != 1) throw new Error("No such peripheral in database"); + + const {rows: userRows} = await pool.query("select socket from user_tokens where user_id=$1", [result.rows[0].user_id]); + if (userRows.length == 1) { + if (userRows[0]){ + const userSocket = userRows[0].socket; + io.to(userSocket).emit("pos_hit", {periphID: result.rows[0].id}); + } + } + else console.log("No App connected"); + + } catch (error) { + console.error(`Error processing job:`, error); + throw error; + } + }); + + socket.on("disconnect", async () => { + if (socket.user) { + console.log("user disconnect"); + await pool.query("update user_tokens set connected=FALSE where socket=$1", [socket.id]); + } + else { + console.log("device disconnect"); + await pool.query("update device_tokens set connected=FALSE where socket=$1", [socket.id]); + } + }); +}); + +async function createToken(userId) { + const token = jwt.sign({ type: 'user', userId }, JWT_SECRET, { expiresIn: TOKEN_EXPIRY }); + await pool.query("delete from user_tokens where user_id=$1", [userId]); + await pool.query("insert into user_tokens (user_id, token) values ($1, $2)", [userId, token]); + return token; +} + +async function createPeripheralToken(peripheralId) { + const token = jwt.sign({ type: 'peripheral', peripheralId }, JWT_SECRET); + await pool.query("insert into device_tokens (device_id, token) values ($1, $2)", [peripheralId, token]); + return token; +} + +async function createTempPeriphToken(peripheralId) { + const token = jwt.sign({type: 'peripheral', peripheralId}, JWT_SECRET, {expiresIn: '2m'} ); + await pool.query("insert into device_tokens (device_id, token) values ($1, $2)", [peripheralId, token]); + return token; +} + +async function authenticateToken(req, res, next) { + const authHeader = req.headers['authorization']; + const token = authHeader?.split(' ')[1]; + + if (!token) return res.sendStatus(401); + + try { + const payload = jwt.verify(token, JWT_SECRET); + if (payload.type === 'user') { + const {rows} = await pool.query("select user_id from user_tokens where token=$1", [token]); + if (rows.length != 1) throw new Error("Invalid/Expired Token"); + req.user = payload.userId; // make Id accessible in route handlers + } + else if (payload.type === 'peripheral'){ + const {rows} = await pool.query("select device_id from device_tokens where token=$1", [token]); + if (rows.length != 1) throw new Error("Invalid/Expired Token"); + req.peripheral = payload.peripheralId; + } + else { + throw new Error("Invalid/Expired Token"); + } + next(); + } catch { + return res.sendStatus(403); // invalid/expired token + } +} + +app.get('/', (req, res) => { + res.send('Hello World!'); +}); + +app.post('/login', async (req, res) => { + const { email, password } = req.body; + console.log('login'); + if (!email || !password) return res.status(400).json({error: 'email and password required'}); + try { + const {rows} = await pool.query('select id, password_hash_string from users where email = $1', [email]); + if (rows.length === 0) return res.status(401).json({error: 'Invalid Credentials'}); + const user = rows[0] + console.log('user found'); + const verified = await verify(user.password_hash_string, password); + + if (!verified) return res.status(401).json({ error: 'Invalid credentials' }); + console.log("password correct"); + const token = await createToken(user.id); // token is now tied to ID + + res.status(200).json({token}); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +app.post('/create_user', async (req, res) => { + console.log("got post req"); + const {name, email, password} = req.body + try { + + const hashedPass = await hash(password); + + await pool.query("insert into users (name, email, password_hash_string) values (nullif($1, ''), $2, $3)", + [name, email, hashedPass] + ); + return res.sendStatus(201); + } catch (err) { + console.error(err); + if (err.code === '23505') { + return res.status(409).json({ error: 'Email already in use' }); + } + return res.sendStatus(500); + } +}); + +app.get('/verify', authenticateToken, async (req, res) => { + try { + // Issue a new token to extend session + const newToken = await createToken(req.user); + res.status(200).json({token: newToken}); + } catch { + res.status(500).json({ error: 'server error' }); + } +}); + +app.get('/device_list', authenticateToken, async (req, res) => { + try { + console.log("device List request"); + console.log(req.user); + const {rows} = await pool.query('select id, device_name from devices where user_id = $1', [req.user]); + const deviceNames = rows.map(row => row.device_name); + const deviceIds = rows.map(row => row.id); + res.status(200).json({ device_ids: deviceIds, devices: deviceNames }); + } catch { + res.status(500).json({error: 'Internal Server Error'}); + } +}); + +app.get('/device_name', authenticateToken, async (req, res) => { + console.log("deviceName"); + try { + const {deviceId} = req.query; + const {rows} = await pool.query('select device_name from devices where id=$1 and user_id=$2', + [deviceId, req.user]); + if (rows.length != 1) return res.sendStatus(404); + const deviceName = rows[0].device_name; + res.status(200).json({device_name: deviceName}); + } catch { + res.sendStatus(500); + } +}); + +app.get('/peripheral_list', authenticateToken, async (req, res) => { + console.log("periph list") + try { + const {deviceId} = req.query; + const {rows} = await pool.query('select id, peripheral_number, peripheral_name from peripherals where device_id=$1 and user_id=$2', + [deviceId, req.user]); + const peripheralIds = rows.map(row => row.id); + const portNums = rows.map(row => row.peripheral_number); + const peripheralNames = rows.map(row => row.peripheral_name); + res.status(200).json({peripheral_ids: peripheralIds, port_nums: portNums, peripheral_names: peripheralNames}); + } catch { + res.sendStatus(500); + } +}) + +app.post('/add_device', authenticateToken, async (req, res) => { + try { + console.log("add device request"); + console.log(req.user); + console.log(req.peripheral); + const {deviceName} = req.body; + console.log(deviceName); + const {rows} = await pool.query("insert into devices (user_id, device_name) values ($1, $2) returning id", + [req.user, deviceName] + ); // finish token return based on device ID. + const deviceInitToken = await createTempPeriphToken(rows[0].id); + res.status(201).json({token: deviceInitToken}); + } catch (err) { + console.log(err); + if (err.code == '23505') { + return res.status(409).json({ error: 'Device Name in use' }); + } + res.status(500).json({error: 'Internal Server Error'}); + } +}); + +app.post('/add_peripheral', authenticateToken, async (req, res) => { + try { + const {device_id, port_num, peripheral_name} = req.body; + await pool.query("insert into peripherals (device_id, peripheral_number, peripheral_name, user_id) values ($1, $2, $3, $4)", + [device_id, port_num, peripheral_name, req.user] + ); + res.sendStatus(201); + } catch (err){ + if (err.code == '23505') return res.sendStatus(409); + res.sendStatus(500); + } +}); + +app.get('/verify_device', authenticateToken, async (req, res) => { + console.log("device verify"); + try{ + console.log(req.peripheral); + await pool.query("delete from device_tokens where device_id=$1", [req.peripheral]); + const newToken = await createPeripheralToken(req.peripheral); + console.log("New Token", newToken); + res.json({token: newToken, id: req.peripheral}); + } catch { + res.status(500).json({error: "server error"}); + } +}); + +app.get('/position', authenticateToken, async (req, res) => { + console.log("devicepos"); + try { + const {rows} = await pool.query("select last_pos, peripheral_number, await_calib from peripherals where device_id=$1", + [req.peripheral]); + + if (rows.length == 0) { + return res.sendStatus(404); + } + + res.status(200).json(rows); + } catch { + res.status(500).json({error: "server error"}); + } +}); + +app.post('/manual_position_update', authenticateToken, async (req, res) => { + console.log("setpos"); + try { + const {periphId, periphNum, deviceId, newPos} = req.body; + const changedPosList = [{periphNum: periphNum, periphID: periphId, pos: newPos}]; + + // Schedule the job to run immediately + const job = await agenda.now('posChange', {deviceID: deviceId, changedPosList: changedPosList, userID: req.user}); + + res.status(202).json({ // 202 Accepted, as processing happens in background + success: true, + message: 'Request accepted for immediate processing.', + jobId: job.attrs._id + }); + } catch (error) { + console.error('Error triggering immediate action:', error); + res.status(500).json({ success: false, message: 'Failed to trigger immediate action', error: error.message }); + } +}); + +app.post('/calib', authenticateToken, async (req, res) => { + console.log("calibrate"); + try { + const {periphId} = req.body; + // Schedule the job to run immediately + const job = await agenda.now('calib', {periphID: periphId, userID: req.user}); + + res.status(202).json({ // 202 Accepted, as processing happens in background + success: true, + message: 'Request accepted for immediate processing.', + jobId: job.attrs._id + }); + } catch (err) { + console.error('Error triggering immediate action:', err); + res.sendStatus(500); + } +}) + +app.post('/cancel_calib', authenticateToken, async (req, res) => { + console.log("cancelCalib"); + try { + const {periphId} = req.body; + const job = await agenda.now('cancel_calib', {periphID: periphId, userID: req.user}); + + res.status(202).json({ // 202 Accepted, as processing happens in background + success: true, + message: 'Request accepted for immediate processing.', + jobId: job.attrs._id + }); + } catch { + res.sendStatus(500); + } +}); + +app.get('/peripheral_status', authenticateToken, async (req, res) => { + console.log("status"); + try { + const {periphId} = req.query; + const {rows} = await pool.query("select last_pos, last_set, calibrated, await_calib from peripherals where id=$1 and user_id=$2", + [periphId, req.user] + ); + if (rows.length != 1) return res.sendStatus(404); + res.status(200).json({last_pos: rows[0].last_pos, last_set: rows[0].last_set, + calibrated: rows[0].calibrated, await_calib: rows[0].await_calib}); + } catch { + res.sendStatus(500); + } +}); + +app.get('/peripheral_name', authenticateToken, async (req, res) => { + console.log("urmom"); + try { + const {periphId} = req.query; + const {rows} = await pool.query("select peripheral_name from peripherals where id=$1 and user_id=$2", + [periphId, req.user] + ); + if (rows.length != 1) return res.sendStatus(404); + res.status(200).json({name: rows[0].peripheral_name}); + } catch { + res.sendStatus(500); + } +}) + +app.post('/completed_calib', authenticateToken, async (req, res) => { + console.log("calibration complete"); + try { + const {portNum} = req.body; + const result = await pool.query("update peripherals set calibrated=true, await_calib=false where device_id=$1 and peripheral_number=$2", + [req.peripheral, portNum] + ); + if (result.rowCount === 0) return res.sendStatus(404); + res.sendStatus(204); + } catch (error) { + console.error(error); + res.sendStatus(500); + } +}); + +app.post('/rename_device', authenticateToken, async (req, res) => { + console.log("Hub rename"); + try { + const {deviceId, newName} = req.body; + const result = await pool.query("update devices set device_name=$1 where id=$2 and user_id=$3", [newName, deviceId, req.user]); + if (result.rowCount === 0) return res.sendStatus(404); + res.sendStatus(204); + } catch (err) { + if (err.code == '23505') return res.sendStatus(409); + console.error(err); + res.sendStatus(500); + } +}); + +app.post('/rename_peripheral', authenticateToken, async (req, res) => { + console.log("Hub rename"); + try { + const {periphId, newName} = req.body; + const result = await pool.query("update peripherals set peripheral_name=$1 where id=$2 and user_id=$3", [newName, periphId, req.user]); + if (result.rowCount === 0) return res.sendStatus(404); + res.sendStatus(204); + } catch (err) { + if (err.code == '23505') return res.sendStatus(409); + console.error(err); + res.sendStatus(500); + } +}); + +app.post('/delete_device', authenticateToken, async (req, res) => { + console.log("delete device"); + try { + const {deviceId} = req.body; + const {rows} = await pool.query("delete from devices where user_id=$1 and id=$2 returning id", + [req.user, deviceId] + ); + if (rows.length != 1) { + return res.status(404).json({ error: 'Device not found' }); + } + + await pool.query("delete from device_tokens where device_id=$1", [rows[0].id]); + + res.sendStatus(204); + } catch { + res.status(500).json({error: "server error"}); + } +}); + +app.post('/delete_peripheral', authenticateToken, async (req, res) => { + console.log("delete peripheral"); + try { + const {periphId} = req.body; + const {rows} = await pool.query("delete from peripherals where user_id = $1 and id=$2 returning id", + [req.user, periphId] + ); + if (rows.length != 1) { + return res.status(404).json({ error: 'Device not found' }); + } + res.sendStatus(204); + } catch { + res.sendStatus(500); + } +}) + +server.listen(port, () => { + console.log(`Example app listening at http://localhost:${port}`); +}); + +app.post('/periph_schedule_list', authenticateToken, async (req, res) => { + try { + console.log("Schedule List request for user:", req.user); + const { periphId } = req.body; + + if (!periphId) { + return res.status(400).json({ error: 'periphId is required in the request body.' }); + } + + // FIX 1: Assign the returned array directly to a variable (e.g., 'jobs') + const jobs = await agenda.jobs({ + 'name': 'posChangeScheduled', + 'data.changedPosList': { $size: 1 }, + 'data.changedPosList.0.periphID': periphId, + 'data.userID': req.user + }); + + // FIX 2: Use .filter() and .map() to handle all cases cleanly. + // This creates a cleaner, more predictable transformation pipeline. + const details = jobs + // Step 1: Filter out any jobs that are not recurring. + .filter(job => job.attrs.repeatInterval) + // Step 2: Map the remaining recurring jobs to our desired format. + .map(job => { + const { repeatInterval, data, repeatTimezone } = job.attrs; + try { + const interval = cronParser.parseExpression(repeatInterval, { + tz: repeatTimezone || undefined + }); + const fields = interval.fields; + + // Make sure to declare the variable + const parsedSchedule = { + minutes: fields.minute, + hours: fields.hour, + daysOfMonth: fields.dayOfMonth, + months: fields.month, + daysOfWeek: fields.dayOfWeek, + }; + + return { + id: job.attrs._id, // It's good practice to return the job ID + schedule: parsedSchedule, + pos: data.changedPosList[0].pos + }; + } catch (err) { + // If parsing fails for a specific job, log it and filter it out. + console.error(`Could not parse "${repeatInterval}" for job ${job.attrs._id}. Skipping.`); + return null; // Return null for now + } + }) + // Step 3: Filter out any nulls that resulted from a parsing error. + .filter(detail => detail !== null); + + res.status(200).json({ scheduledUpdates: details }); + + } catch (error) { // FIX 3: Capture and log the actual error + console.error("Error in /periph_schedule_list:", error); + res.status(500).json({ error: 'Internal Server Error' }); + } +}); diff --git a/sdkconfig.seeed_xiao_esp32c6 b/sdkconfig.seeed_xiao_esp32c6 index df4ae81..b3de5bf 100644 --- a/sdkconfig.seeed_xiao_esp32c6 +++ b/sdkconfig.seeed_xiao_esp32c6 @@ -2584,6 +2584,17 @@ CONFIG_NIMBLE_CPP_ATT_VALUE_INIT_LENGTH=20 CONFIG_NIMBLE_CPP_FREERTOS_TASK_BLOCK_BIT=31 CONFIG_NIMBLE_CPP_IDF=y # end of ESP-NimBLE-CPP configuration + +# +# ESP Socket.IO client +# + +# +# ESP WebSocket client +# +# CONFIG_ESP_WS_CLIENT_ENABLE_DYNAMIC_BUFFER is not set +# CONFIG_ESP_WS_CLIENT_SEPARATE_TX_LOCK is not set +# end of ESP WebSocket client # end of Component config # CONFIG_IDF_EXPERIMENTAL_FEATURES is not set diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fb0adb6..0848ada 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,4 +5,4 @@ FILE(GLOB_RECURSE app_sources ${CMAKE_SOURCE_DIR}/src/*.* ${CMAKE_SOURCE_DIR}/in idf_component_register(SRCS ${app_sources} INCLUDE_DIRS "." - REQUIRES nvs_flash esp-nimble-cpp) + REQUIRES nvs_flash esp-nimble-cpp esp_socketio_client) diff --git a/src/idf_component.yml b/src/idf_component.yml new file mode 100644 index 0000000..6492a6f --- /dev/null +++ b/src/idf_component.yml @@ -0,0 +1,2 @@ +dependencies: + bubblesnake/esp_socketio_client: "^1.0.0" \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 469a652..0839e77 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,8 +6,12 @@ #include "BLE.hpp" #include "WiFi.hpp" #include "setup.hpp" +#include "bmHTTP.hpp" +#include "socketIO.hpp" +#include "cJSON.h" extern "C" void app_main() { + printf("Hello "); esp_err_t ret = nvs_flash_init(); // change to secure init logic soon!! // 2. If NVS is full or corrupt (common after flashing new code), erase and retry if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { @@ -17,44 +21,76 @@ extern "C" void app_main() { ESP_ERROR_CHECK(ret); bmWiFi.init(); - - nvs_handle_t WiFiHandle; - if (nvs_open(nvsWiFi, NVS_READWRITE, &WiFiHandle) == ESP_OK) { - size_t ssidSize; - esp_err_t WiFiPrefsError = nvs_get_str(WiFiHandle, ssidTag, NULL, &ssidSize); - size_t pwSize; - WiFiPrefsError |= nvs_get_str(WiFiHandle, passTag, NULL, &pwSize); - uint8_t authMode; - WiFiPrefsError |= nvs_get_u8(WiFiHandle, authTag, &authMode); - if (WiFiPrefsError == ESP_ERR_NVS_NOT_FOUND) { - // Make the RGB LED a certain color (Blue?) - nvs_close(WiFiHandle); - initialSetup(); - } else if (WiFiPrefsError == ESP_OK) { - char ssid[ssidSize]; - nvs_get_str(WiFiHandle, ssidTag, ssid, &ssidSize); - char pw[pwSize]; - nvs_get_str(WiFiHandle, passTag, pw, &pwSize); - nvs_close(WiFiHandle); - // TODO: add enterprise support - if (!bmWiFi.attemptConnect(ssid, pw, (wifi_auth_mode_t)authMode)) { + bool initSuccess = false; + + while(!initSuccess) { + nvs_handle_t WiFiHandle; + if (nvs_open(nvsWiFi, NVS_READONLY, &WiFiHandle) == ESP_OK) { + size_t ssidSize; + esp_err_t WiFiPrefsError = nvs_get_str(WiFiHandle, ssidTag, NULL, &ssidSize); + size_t pwSize; + WiFiPrefsError |= nvs_get_str(WiFiHandle, passTag, NULL, &pwSize); + uint8_t authMode; + WiFiPrefsError |= nvs_get_u8(WiFiHandle, authTag, &authMode); + printf("World\n"); + if (WiFiPrefsError == ESP_ERR_NVS_NOT_FOUND) { + printf("Didn't find creds\n"); + // Make the RGB LED a certain color (Blue?) + nvs_close(WiFiHandle); + initialSetup(); + } else if (WiFiPrefsError == ESP_OK) { + char ssid[ssidSize]; + nvs_get_str(WiFiHandle, ssidTag, ssid, &ssidSize); + char pw[pwSize]; + nvs_get_str(WiFiHandle, passTag, pw, &pwSize); + nvs_close(WiFiHandle); + if (!bmWiFi.attemptConnect(ssid, pw, (wifi_auth_mode_t)authMode)) { + // Make RGB LED certain color (Blue?) + printf("Found credentials, failed to connect.\n"); + initialSetup(); + } + else { + printf("Connected to WiFi from NVS credentials\n"); + nvs_handle_t authHandle; + if (nvs_open(nvsAuth, NVS_READONLY, &authHandle) == ESP_OK) { + size_t tokenSize; + if (nvs_get_str(authHandle, tokenTag, NULL, &tokenSize) == ESP_OK) { + char token[tokenSize]; + nvs_get_str(authHandle, tokenTag, token, &tokenSize); + nvs_close(authHandle); + + // Use permanent device token to connect to Socket.IO + // The server will verify the token during connection handshake + webToken = std::string(token); + printf("Connecting to Socket.IO server with saved token...\n"); + initSocketIO(); + while (!statusResolved) vTaskDelay(pdMS_TO_TICKS(100)); + initSuccess = connected; + } + else { + printf("Token read unsuccessful, entering setup.\n"); + initialSetup(); + } + } + else { + printf("Auth NVS segment doesn't exist, entering setup.\n"); + initialSetup(); + } + } + } else { // Make RGB LED certain color (Blue?) + nvs_close(WiFiHandle); + printf("Program error in Wifi Connection\n"); initialSetup(); } - } else { - // Make RGB LED certain color (Blue?) - nvs_close(WiFiHandle); - printf("Program error in Wifi Connection\n"); + } + else { + printf("WiFi NVS segment doesn't exist, entering setup.\n"); initialSetup(); } } - else { - printf("ERROR: Couldn't open wifi NVS segment\nProgram stopped.\n"); - while (1) { - vTaskDelay(pdMS_TO_TICKS(500)); - } - } + // Main loop while (1) {