CONNECTS TO WEBSOCKET!!

This commit is contained in:
2025-12-23 17:21:44 -06:00
parent c8f95463a5
commit 322f7f0699
13 changed files with 903 additions and 61 deletions

5
.gitignore vendored
View File

@@ -4,4 +4,7 @@
.vscode/launch.json
.vscode/ipch
.DS_Store
.DS_Store
managed_components/
compile_commands.json

View File

@@ -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

View File

@@ -2,6 +2,7 @@
#include "NimBLEDevice.h"
#include "WiFi.hpp"
#include "nvs_flash.h"
#include "socketIO.hpp"
#include "defines.h"
#include <mutex>
#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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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));
}
}

119
include/socketIO.cpp Normal file
View File

@@ -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<bool> statusResolved{false};
std::atomic<bool> 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);
}
}

14
include/socketIO.hpp Normal file
View File

@@ -0,0 +1,14 @@
#ifndef SOCKETIO_HPP
#define SOCKETIO_HPP
#include <atomic>
extern std::atomic<bool> statusResolved;
extern std::atomic<bool> connected;
// Initialize Socket.IO client and connect to server
void initSocketIO();
// Emit calibration done event to server
void emitCalibDone(int port);
#endif // SOCKETIO_HPP

599
index.js Normal file
View File

@@ -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' });
}
});

View File

@@ -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

View File

@@ -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)

2
src/idf_component.yml Normal file
View File

@@ -0,0 +1,2 @@
dependencies:
bubblesnake/esp_socketio_client: "^1.0.0"

View File

@@ -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) {