Gaming Controller Setup Help in HTML/Java

  softwareengineering

I’ve been working on an internet controlled rc car based on a rpi zero 2w. My initial idea was to stream video with mjpg-streamer to an html file through http on port 8080 and be able to send controls back to the pi with the html code sending controls from a gamepad through Websockets(hosted by the pi). All of that would in the end go through tailscale to be accessible over the internet. I got the video and websocket connections working, but my issue is that I don’t know how to get the gaming controller values to update constantly.

I’m no expert in html, so I’ll need a little help. As far as my knowledge, some code I found on the internet for using a gaming controller with html was supposed to set it to update the stick values when they change, but it doesn’t seem to update at all. On the pi I have it set to print out the stick controls to the terminal, and I only see a change in the values if I hold the sticks in a different spot and reload the html page. Any help would be appreciated. Thanks!

The gaming controller section is near the bottom. 🙂

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>RC Car Client</title>
    <style>
        /* Reset default margin and padding */
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: Arial, sans-serif;
            background-color: #F5F5F5;
            color: #333;
        }

        .top-bar {
            background-color: #E21E24;
            padding: 10px 20px;
            display: flex;
            justify-content: space-between;
            align-items: center;
            box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
        }

        h1 {
            color: #FFF;
        }

        .container {
            display: flex;
            flex-direction: column;
            align-items: center;
            background-color: #FFF;
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            margin-top: 20px;
        }

        label {
            font-weight: bold;
            margin-bottom: 10px;
        }

        input[type="text"], input[type="number"] {
            padding: 10px;
            margin-bottom: 20px;
            border: 1px solid #CCC;
            border-radius: 5px;
            box-shadow: none;
        }

        button {
            background-color: #FFF;
            color: #E21E24;
            border: 1px solid #E21E24;
            padding: 10px 20px;
            border-radius: 5px;
            cursor: pointer;
            transition: background-color 0.3s ease;
        }

        button:hover {
            background-color: #E21E24;
            color: #FFF;
        }

        video {
            width: 100%;
            max-width: 100%;
            height: auto;
            border-radius: 5px;
            margin-bottom: 20px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            border: 2px solid #333;
        }

        /* Side menu styles */
        .side-menu {
            height: 100%;
            width: 0;
            position: fixed;
            top: 0;
            right: 0;
            background-color: #E21E24;
            overflow-x: hidden;
            transition: 0.5s;
            padding-top: 60px;
            z-index: 1000;
        }

        .side-menu a {
            padding: 10px 15px;
            text-decoration: none;
            font-size: 20px;
            color: #FFF;
            display: block;
            transition: 0.3s;
        }

        .side-menu a:hover {
            background-color: #B10E13;
        }

        .side-menu .close-btn {
            position: absolute;
            top: 10px;
            right: 10px;
            font-size: 36px;
            margin-left: 50px;
        }

        /* Hamburger menu icon */
        .menu-icon {
            cursor: pointer;
            font-size: 24px;
            margin-right: 10px;
        }

        .menu-icon:hover {
            color: #FFF;
        }
    </style>
</head>
<body>
    <div class="top-bar">
        <h1>RC Car Client</h1>
        <span id="status" class="status-indicator">Disconnected</span>
        <span id="fps" class="fps-meter">FPS: 0</span>
        <input type="text" id="pi_ip" placeholder="Raspberry Pi IP" style="width: auto;">
        <button onclick="connect()">Connect</button>
        <div class="menu-icon" onclick="openSideMenu()">☰</div>
    </div>
    <div id="side-menu" class="side-menu">
        <a href="javascript:void(0)" class="close-btn" onclick="closeSideMenu()">×</a>
        <label for="servo_trim_pos">Servo Trim Positive:</label>
        <input type="number" id="servo_trim_pos" min="0" max="100" value="0">
        <label for="servo_trim_neg">Servo Trim Negative:</label>
        <input type="number" id="servo_trim_neg" min="0" max="100" value="0">
        <label for="max_speed">Max Speed:</label>
        <input type="number" id="max_speed" min="0" max="100" value="100">
        <div class="checkbox">
            <input type="checkbox" id="controller_checkbox" checked> <!-- Default to checked -->
            <label for="controller_checkbox">Use Gaming Controller</label>
        </div>
        <button onclick="sendSettings()">Apply Settings</button>
        <button onclick="toggleFullScreen()">Fullscreen</button>
    </div>
    <div class="container">
        <!-- Video element will be dynamically initialized after connection -->
    </div>

    <script>
        var ws;
        var lastIpAddress = localStorage.getItem("lastIpAddress");
        if (lastIpAddress) {
            document.getElementById("pi_ip").value = lastIpAddress;
        }

        function openWebSocket(ip) {
            if (ws && ws.readyState === WebSocket.OPEN) {
                return;
            }

            ws = new WebSocket('ws://' + ip + ':8765');

            ws.onopen = function() {
                updateStatus(true);
                initializeVideo();
                startFPSCounter();
            };

            ws.onclose = function() {
                updateStatus(false);
            };
        }

        function closeWebSocket() {
            if (ws) {
                ws.close();
            }
        }

        function openSideMenu() {
            document.getElementById("side-menu").style.width = "250px";
        }

        function closeSideMenu() {
            document.getElementById("side-menu").style.width = "0";
        }

        function toggleFullScreen() {
            var video = document.getElementById("video");
            if (video.requestFullscreen) {
                video.requestFullscreen();
            } else if (video.mozRequestFullScreen) {
                video.mozRequestFullScreen();
            } else if (video.webkitRequestFullscreen) {
                video.webkitRequestFullscreen();
            } else if (video.msRequestFullscreen) {
                video.msRequestFullscreen();
            }
        }

        function connect() {
            var ip = document.getElementById("pi_ip").value;
            localStorage.setItem("lastIpAddress", ip);
            openWebSocket(ip);
        }

        function initializeVideo() {
            var container = document.querySelector(".container");
            var existingVideo = document.getElementById("video");
            if (existingVideo) {
                container.removeChild(existingVideo);
            }
            var video = document.createElement("img"); // Use <img> element for MJPG Streamer
            video.id = "video";
            video.src = "http://" + document.getElementById("pi_ip").value + ":8080/?action=stream"; // MJPG Streamer URL
            video.style.maxWidth = "100%";
            container.appendChild(video);
        }

        function startFPSCounter() {
            var video = document.getElementById("video");
            var fpsElement = document.getElementById("fps");

            var fps = 0;
            var start = Date.now();

            function countFPS() {
                fps++;
                var elapsed = Date.now() - start;
                if (elapsed >= 1000) {
                    fpsElement.textContent = "FPS: " + fps;
                    fps = 0;
                    start = Date.now();
                }
                requestAnimationFrame(countFPS);
            }

            countFPS();
        }

        function updateStatus(connected) {
            var statusElement = document.getElementById("status");
            if (connected) {
                statusElement.textContent = "Connected";
                statusElement.style.color = "green";
            } else {
                statusElement.textContent = "Disconnected";
                statusElement.style.color = "red";
            }
        }

    

        // Function to map stick movements to servo angles and throttle values
        function updateControls(e) {
            var throttle = (e.axes[1] + 1) * 50; // Map vertical movement to throttle (0-100)
            var steering = e.axes[2] * 90; // Map horizontal movement to steering (-90 to 90)

            // Send throttle and steering values to WebSocket server
            var controls = throttle.toFixed(0) + "," + steering.toFixed(0);
            ws.send(controls);
        }

        // Initialize the controller
        window.addEventListener("gamepadconnected", function(e) {
            console.log("Gamepad connected:", e.gamepad.id);
            var controller = e.gamepad;
            // Update controls when controller input changes
            setInterval(function() {
                updateControls(controller);
            }, 100);
        });
    </script>
</body>
</html>

New contributor

Logan Colón is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.

LEAVE A COMMENT