Source: websocket.js

/**
 * @file websocketServer.js
 * @description This file sets up and manages the WebSocket server for handling client communication and broadcasting data.
 * @author Lior Jigalo
 * @license MIT
 */

/** Extract environment variables */
const {maxSpeed} = process.env;

/** Import WebSocket library */
const WebSocket = require("ws");

/** Import sound utility functions */
const {playSound, askForUserConfirmation} = require("./util/sound");

/** Import general utility functions */
const {printToConsole, getSystemData} = require("./util/util");

/** Import car management functions */
const {startSpeedBroadcast, decelerateCar, accelerateCar} = require("./util/carManager");

/** Import drive log management functions */
const {currentDriveObject, updateDriveDataLog} = require("./util/driveLogManager");

/** Import global variables */
const {user_status, locks, sounds} = require("./util/global");

/**
 * @typedef {object} MessageData
 * @property {string} type - Type of the message.
 * @property {object} msgData - Data associated with the message.
 */

/** @type {undefined | object} */
/** Holds data from the detection unit*/
let detectionUnitData = undefined;

/**
 * @function initWebSocket
 * @description Initializes the WebSocket server and sets up event listeners for client communication.
 * @param {object} server - The HTTP server instance to attach the WebSocket server.
 * @returns {WebSocket.Server} The WebSocket server instance.
 */
function initWebSocket(server) {
    const wss = new WebSocket.Server({server});

    // Event listener for new connections
    wss.on("connection", (ws) => {

        // Send a welcome message to the new client
        sendWelcomeMessage(ws);

        // Handle messages from the client
        ws.on("message", (message) => {
            handleClientMessage(ws, message, wss).then(r => printToConsole("Returned from alert message"));
        });

        // Handle client disconnection
        ws.on("close", () => {
           // console.log("WebSocket client disconnected");
        });
    });

    // Start broadcasting speed updates using the WebSocket server instance
    startSpeedBroadcast(wss);
    return wss;
}

/**
 * @function sendWelcomeMessage
 * @description Sends a welcome message to the connected client, including system data and gauge configuration.
 * @param {WebSocket} ws - The WebSocket instance of the connected client.
 */
function sendWelcomeMessage(ws) {
    let tickList = [];
    for (let i = 0; i <= maxSpeed; i += 20)
        tickList.push(i);

    const systemData = getSystemData();

    const welcomeMessage = {
        type: "welcome",
        gaugeConf: {
            tickList: tickList,
            redline: {
                minVal: 160,
                maxVal: 220
            },
        },
        detectionUnitData: detectionUnitData,
        systemData: systemData
    };

    ws.send(JSON.stringify(welcomeMessage));
}

/**
 * @function broadcast
 * @description Sends a message to all connected WebSocket clients.
 * @param {WebSocket.Server} wss - The WebSocket server instance.
 * @param {object} data - The data to broadcast.
 */
function broadcast(wss, data) {
    const message = JSON.stringify(data);
    wss.clients.forEach((client) => {
        if (client.readyState === WebSocket.OPEN)
            client.send(message);
    });
}

/**
 * @function handleClientMessage
 * @description Handles incoming messages from WebSocket clients.
 * @param {WebSocket} ws - The WebSocket instance of the client sending the message.
 * @param {string} message - The received message in JSON format.
 * @param {WebSocket.Server} wss - The WebSocket server instance.
 */
async function handleClientMessage(ws, message, wss) {
    try {
        const data = JSON.parse(message);
        const response = {
            type: null,
            data: data,
            event: null
        };

        const {type, msgData} = data;

        printToConsole("Received type: " + type)

        switch (type) {

            case "detection_feed":
                // Decode the image frame (base64 to buffer)
                if (!("face_tensors" in msgData) || !("frame" in msgData)) return;

                const {frame, face_tensors, face_landmark_tensors, commands} = msgData;

                if (typeof commands !== 'undefined')
                    detectionUnitData = commands

                // Broadcast detection data to all connected clients
                broadcast(wss, {
                    type: "detection_feed",
                    msgData: {
                        frame: frame,
                        ...(typeof face_tensors !== 'undefined' && { face_tensors }),
                        ...(typeof face_landmark_tensors !== 'undefined' && { face_landmark_tensors }),
                    }
                });
                break;

            case "manual_user_confirmation":
            case "accelerate":
                printToConsole(msgData);
                break;

            case "decelerate":
                printToConsole(msgData);
                break;

            case "cruise":
                printToConsole(msgData);
                break;

            case "alert": {
                // 1. Check if alert messages are locked (simple semaphore logic)
                if (locks.alert_lock) {
                    printToConsole("alert messages are locked");
                    return;
                }
                locks.alert_lock = true;

                console.log(msgData);

                switch (msgData) {
                    case "low_average_ear":
                        await playSound(sounds.takeABreak);
                        break;

                    case "high_blink_rate":
                        // not sure if this will be used yet
                        break;

                    case "Prolonged_eye_closure":
                        // 3. If we’ve triggered a “medium alert” before, play attentionTest
                        //    and loop until user responds or alert is escalated
                        if (currentDriveObject.medium_alert_num > 0) {
                            await playSound(sounds.attentionTest);

                            try {
                                // 3A. Wait for user confirmation in a loop
                                while (true) {
                                    const isConfirmedAlert = await askForUserConfirmation();

                                    if (isConfirmedAlert === user_status.userResponded) {
                                        // User confirmed alert
                                        printToConsole("User has confirmed being alert.");
                                        await playSound(sounds.gotIt);
                                        currentDriveObject.consecutive_alert_num = 0;
                                        break;
                                    } else if (isConfirmedAlert === user_status.noResponse) {
                                        // User did not confirm → increment counter
                                        currentDriveObject.consecutive_alert_num += 1;
                                        if (currentDriveObject.consecutive_alert_num >= 1)
                                            break;

                                        printToConsole("User did not confirm being alert.");
                                        await playSound(sounds.noResponse);
                                    } else if (isConfirmedAlert === user_status.failedToParse)
                                        // Retry if parse failed
                                        await playSound(sounds.failedToParse);
                                }

                                // 3B. If user never responded, escalate: play decelerateWarning
                                if (currentDriveObject.consecutive_alert_num >= 1) {
                                    printToConsole("debug 5");
                                    currentDriveObject.consecutive_alert_num = 0;

                                    await playSound(sounds.failedToParse);
                                    const isConfirmedAlert = await askForUserConfirmation();

                                    if (isConfirmedAlert === user_status.userResponded) {
                                        await playSound(sounds.gotIt);
                                        currentDriveObject.consecutive_alert_num = 0;
                                    } else if (isConfirmedAlert === user_status.failedToParse) {
                                        // Should ideally loop until response is valid, ill do that later...
                                        await playSound(sounds.failedToParse);
                                    } else {
                                        // TODO: find a way to interrupt this process
                                        await playSound(sounds.decelerating);
                                        decelerateCar();
                                    }
                                }

                            } catch (err) {
                                // In case anything goes wrong
                                console.error("Error:", err.message);
                                locks.alert_lock = true;
                            }
                            break; // End of "alert" logic when medium_alert_num > 0
                        }
                            // 4. If this is the first time medium_alert_num = 0, just play "takeABreak"
                        //    and increment the alert count
                        else {
                            await playSound(sounds.takeABreak);
                            currentDriveObject.medium_alert_num += 1;
                        }
                        break;

                    default:
                        return
                }
                break; // End of case "alert"
            }

            default:
                printToConsole(`unknown type: ${data}`)
                response.type = "unknown";
                locks.alert_lock = false;

        }
        locks.alert_lock = false;

        ws.send(JSON.stringify(response));
    } catch (error) {
        console.error("Invalid JSON received:", error);
    }
}

/** Export WebSocket functions */
module.exports = {initWebSocket, broadcast};

















// case "alert":
//     // Some simple "semaphore" logic
//     if (locks.alert_lock){
//         printToConsole("alert messages are locked");
//         return;
//     }
//     locks.alert_lock = true;
//
//     const {event} = data
//
//     let takeABreak       = path.join(__dirname, 'assets/sounds', "takeABreak.wav");
//     let attentionTest    = path.join(__dirname, 'assets/sounds', 'attentionTest.wav');
//     let failedToParse    = path.join(__dirname, 'assets/sounds', 'failedToParse.wav');
//     let noResponse       = path.join(__dirname, 'assets/sounds', 'noResponse.wav');
//     let decelerateWarning= path.join(__dirname, 'assets/sounds', 'decelerateWarning.wav');
//     let decelerating     = path.join(__dirname, 'assets/sounds', 'decelerating.wav');
//
//     if (currentDriveObject.medium_alert_num > 0) {
//         await playSound(attentionTest);
//
//         try {
//             while ( true ){
//                 // check user response
//                 const isConfirmedAlert = await askForUserConfirmation();
//
//                 if (isConfirmedAlert === user_status.user_responded) {
//                     printToConsole("User has confirmed being alert.");
//                     await playSound(takeABreak);
//                     currentDriveObject.consecutive_alert_num = 0;
//                     break;
//                 } else if (isConfirmedAlert === user_status.no_response) {
//                     currentDriveObject.consecutive_alert_num += 1;
//                     if (currentDriveObject.consecutive_alert_num >= 1)
//                         break;
//
//                     printToConsole("User did not confirm being alert.");
//                     await playSound(noResponse);
//                 } else if (isConfirmedAlert === user_status.failed_to_parse)
//                     await playSound(failedToParse);
//             }
//
//             if (currentDriveObject.consecutive_alert_num >= 1){
//                 printToConsole("debug 5")
//
//                 currentDriveObject.consecutive_alert_num = 0;
//                 await playSound(decelerateWarning);
//                 const isConfirmedAlert = await askForUserConfirmation();
//                  if (isConfirmedAlert === user_status.user_responded) {
//                      await playSound(takeABreak);
//                      currentDriveObject.consecutive_alert_num = 0;
//                  } else if (isConfirmedAlert === user_status.failed_to_parse)
//                      await playSound(failedToParse); // should also  be like a while loop until we get a response.
//                  else
//                      await playSound(decelerating);
//
//             }
//         } catch (err) {
//             console.error("Error:", err.message);
//             locks.alert_lock = true;
//         }
//         break;
//     } else {
//         await playSound(takeABreak);
//         currentDriveObject.medium_alert_num += 1;
//     }
//     break;