Hi there! I spent the past 3 days trying to make a 4g controlled fpv car. I used chat gpt to help write most of the code, but it seems quite difficult to get it to write a functioning program and not a "template".
My concept was to first make an rc car that is controlled over a wifi network and then use tailscale to make it work over internet. So far I have got mjpg-streamer up and running on startup with low latency over the internet and it works amazingly well! I had chatgpt write a python program that will set up a websocket server that will receive commands from an html page to control a servo motor and a brushless esc with a gaming controller. The html page works great and shows the video, and it's set up to also have a connection indicator for the websocket, which also shows "connected" when I connect to the pi. This is where I got stuck. When everything is up and running, and a servo is connected, the gaming controller does nothing. There is no sign of the commands even getting to the pi. I don't know much about python and html, so I don't know what chatgpt did wrong. I'll put both the html and python code in here if anyone knows how to get this working. Thanks!
https://preview.redd.it/biehs1m52pvc1.png?width=1915&format=png&auto=webp&s=b3200486e887ac58e340a170985bbd1e7116cca7
Here's the Python
import asyncio import websockets import RPi.GPIO as GPIO # Set up GPIO mode and pins GPIO.setmode(GPIO.BCM) SERVO_PIN = 13 # GPIO pin for servo control MOTOR_PIN = 11 # GPIO pin for motor control # Define the maximum and minimum duty cycles for servo and motor SERVO_MAX_DUTY_CYCLE = 12 # Example value, adjust as needed SERVO_MIN_DUTY_CYCLE = 2 # Example value, adjust as needed MOTOR_MAX_DUTY_CYCLE = 100 # Example value, adjust as needed MOTOR_MIN_DUTY_CYCLE = 0 # Example value, adjust as needed # Initialize GPIO pins GPIO.setup(SERVO_PIN, GPIO.OUT) GPIO.setup(MOTOR_PIN, GPIO.OUT) servo_pwm = GPIO.PWM(SERVO_PIN, 50) # PWM frequency for servo motor_pwm = GPIO.PWM(MOTOR_PIN, 50) # PWM frequency for motor servo_pwm.start(50) # Start PWM for servo motor_pwm.start(50) # Start PWM for motor async def control_rc_car(websocket, path): async for message in websocket: data = message.split(',') if len(data) == 2: steering_angle = int(data[0]) motor_speed = int(data[1]) # Map steering angle to duty cycle for servo servo_duty_cycle = map_servo_angle_to_duty_cycle(steering_angle) # Map motor speed to duty cycle for motor motor_duty_cycle = map_motor_speed_to_duty_cycle(motor_speed) # Set servo duty cycle servo_pwm.ChangeDutyCycle(servo_duty_cycle) # Set motor duty cycle motor_pwm.ChangeDutyCycle(motor_duty_cycle) # Output servo and motor controls print(f"Servo Duty Cycle: {servo_duty_cycle}, Motor Duty Cycle: {motor_duty_cycle}") else: print("Invalid message format") def map_servo_angle_to_duty_cycle(angle): # Map the servo angle to a duty cycle within the specified range duty_cycle = SERVO_MIN_DUTY_CYCLE + (angle / 180) * (SERVO_MAX_DUTY_CYCLE - SERVO_MIN_DUTY_CYCLE) return duty_cycle def map_motor_speed_to_duty_cycle(speed): # Map the motor speed to a duty cycle within the specified range duty_cycle = MOTOR_MIN_DUTY_CYCLE + (speed / 100) * (MOTOR_MAX_DUTY_CYCLE - MOTOR_MIN_DUTY_CYCLE) return duty_cycle async def main(): server = await websockets.serve(control_rc_car, "localhost", 8765) await server.wait_closed() asyncio.run(main())
Here's the HTML
<!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"> <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 send settings to WebSocket server function sendSettings() { var servoTrimPos = document.getElementById("servo_trim_pos").value; var servoTrimNeg = document.getElementById("servo_trim_neg").value; var maxSpeed = document.getElementById("max_speed").value; var useController = document.getElementById("controller_checkbox").checked; var settings = { servoTrimPos: servoTrimPos, servoTrimNeg: servoTrimNeg, maxSpeed: maxSpeed, useController: useController }; ws.send(JSON.stringify(settings)); } </script> </body> </html>
submitted by