openpilot v0.9.6 release

date: 2024-01-12T10:13:37
master commit: ba792d576a49a0899b88a753fa1c52956bedf9e6
This commit is contained in:
FrogAi
2024-01-12 22:39:28 -07:00
commit 08e9fb1edc
1881 changed files with 653708 additions and 0 deletions

View File

@@ -0,0 +1,103 @@
<html>
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>commabody</title>
<link rel="stylesheet" href="/static/main.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.3/css/bootstrap.min.css" integrity="sha512-SbiR/eusphKoMVVXysTKG/7VseWii+Y3FdHrt0EpKgpToZeemhqHeZeLWLhJutz/2ut2Vw1uQEj2MbRF+TVBUA==" crossorigin="anonymous" referrerpolicy="no-referrer" /><script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.3/js/bootstrap.min.js" integrity="sha512-1/RvZTcCDEUjY/CypiMz+iqqtaoQfAITmNSJY17Myp4Ms5mdxPS5UV7iOfdZoxcGhzFbOm6sntTKJppjvuhg4g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@^3"></script>
<script src="https://cdn.jsdelivr.net/npm/moment@^2"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment@^1"></script>
</head>
<body>
<div id="main">
<p class="jumbo">comma body</p>
<audio id="audio" autoplay="true"></audio>
<video id="video" playsinline autoplay muted loop poster="/static/poster.png"></video>
<div id="icon-panel" class="row">
<div class="col-sm-12 col-md-6 details">
<div class="icon-sup-panel col-12">
<div class="icon-sub-panel">
<div class="icon-sub-sub-panel">
<i class="bi bi-speaker-fill pre-blob"></i>
<i class="bi bi-mic-fill pre-blob"></i>
<i class="bi bi-camera-video-fill pre-blob"></i>
</div>
<p class="small">body</p>
</div>
<div class="icon-sub-panel">
<div class="icon-sub-sub-panel">
<i class="bi bi-speaker-fill pre-blob"></i>
<i class="bi bi-mic-fill pre-blob"></i>
</div>
<p class="small">you</p>
</div>
</div>
</div>
<div class="col-sm-12 col-md-6 details">
<div class="icon-sup-panel col-12">
<div class="icon-sub-panel">
<div class="icon-sub-sub-panel">
<i id="ping-time" class="pre-blob1">-</i>
</div>
<p class="bi bi-arrow-repeat small"> ping time</p>
</div>
<div class="icon-sub-panel">
<div class="icon-sub-sub-panel">
<i id="battery" class="pre-blob1">-</i>
</div>
<p class="bi bi-battery-half small"> battery</p>
</div>
</div>
</div>
<!-- <div class="icon-sub-panel">
<button type="button" id="start" class="btn btn-light btn-lg">Start</button>
<button type="button" id="stop" class="btn btn-light btn-lg">Stop</button>
</div> -->
</div>
<div class="row" style="width: 100%; padding: 0px 10px 0px 10px;">
<div id="wasd" class="col-md-12 row">
<div class="col-md-6 col-sm-12" style="justify-content: center; display: flex; flex-direction: column;">
<div class="wasd-row">
<div class="keys" id="key-w">W</div>
<div id="key-val"><span id="pos-vals">0,0</span><span>x,y</span></div>
</div>
<div class="wasd-row">
<div class="keys" id="key-a">A</div>
<div class="keys" id="key-s">S</div>
<div class="keys" id="key-d">D</div>
</div>
</div>
<div class="col-md-6 col-sm-12 form-group plan-form">
<label for="plan-text">Plan (w, a, s, d, t)</label>
<label style="font-size: 15px;" for="plan-text">*Extremely Experimental*</label>
<textarea class="form-control" id="plan-text" rows="7" placeholder="1,0,0,0,2"></textarea>
<button type="button" id="plan-button" class="btn btn-light btn-lg">Execute</button>
</div>
</div>
</div>
<div class="row" style="padding: 0px 10px 0px 10px; width: 100%;">
<div class="panel row">
<div class="col-sm-3" style="text-align: center;">
<p>Play Sounds</p>
</div>
<div class="btn-group col-sm-8">
<button type="button" id="sound-engage" class="btn btn-outline-success btn-lg sound">Engage</button>
<button type="button" id="sound-disengage" class="btn btn-outline-warning btn-lg sound">Disengage</button>
<button type="button" id="sound-error" class="btn btn-outline-danger btn-lg sound">Error</button>
</div>
</div>
</div>
<div class="row" style="padding: 0px 10px 0px 10px; width: 100%;">
<div class="panel row">
<div class="col-sm-6"><canvas id="chart-ping"></canvas></div>
<div class="col-sm-6"><canvas id="chart-battery"></canvas></div>
</div>
</div>
</div>
<script src="/static/js/jsmain.js" type="module"></script>
</body>
</html>

54
tools/bodyteleop/static/js/controls.js vendored Normal file
View File

@@ -0,0 +1,54 @@
const keyVals = {w: 0, a: 0, s: 0, d: 0}
export function getXY() {
let x = -keyVals.w + keyVals.s
let y = -keyVals.d + keyVals.a
return {x, y}
}
export const handleKeyX = (key, setValue) => {
if (['w', 'a', 's', 'd'].includes(key)){
keyVals[key] = setValue;
let color = "#333";
if (setValue === 1){
color = "#e74c3c";
}
$("#key-"+key).css('background', color);
const {x, y} = getXY();
$("#pos-vals").text(x+","+y);
}
};
export async function executePlan() {
let plan = $("#plan-text").val();
const planList = [];
plan.split("\n").forEach(function(e){
let line = e.split(",").map(k=>parseInt(k));
if (line.length != 5 || line.slice(0, 4).map(e=>[1, 0].includes(e)).includes(false) || line[4] < 0 || line[4] > 10){
console.log("invalid plan");
}
else{
planList.push(line)
}
});
async function execute() {
for (var i = 0; i < planList.length; i++) {
let [w, a, s, d, t] = planList[i];
while(t > 0){
console.log(w, a, s, d, t);
if(w==1){$("#key-w").mousedown();}
if(a==1){$("#key-a").mousedown();}
if(s==1){$("#key-s").mousedown();}
if(d==1){$("#key-d").mousedown();}
await sleep(50);
$("#key-w").mouseup();
$("#key-a").mouseup();
$("#key-s").mouseup();
$("#key-d").mouseup();
t = t - 0.05;
}
}
}
execute();
}

View File

@@ -0,0 +1,27 @@
import { handleKeyX, executePlan } from "./controls.js";
import { start, stop, lastChannelMessageTime, playSoundRequest } from "./webrtc.js";
export var pc = null;
export var dc = null;
document.addEventListener('keydown', (e)=>(handleKeyX(e.key.toLowerCase(), 1)));
document.addEventListener('keyup', (e)=>(handleKeyX(e.key.toLowerCase(), 0)));
$(".keys").bind("mousedown touchstart", (e)=>handleKeyX($(e.target).attr('id').replace('key-', ''), 1));
$(".keys").bind("mouseup touchend", (e)=>handleKeyX($(e.target).attr('id').replace('key-', ''), 0));
$("#plan-button").click(executePlan);
$(".sound").click((e)=>{
const sound = $(e.target).attr('id').replace('sound-', '')
return playSoundRequest(sound);
});
setInterval( () => {
const dt = new Date().getTime();
if ((dt - lastChannelMessageTime) > 1000) {
$(".pre-blob").removeClass('blob');
$("#battery").text("-");
$("#ping-time").text('-');
$("video")[0].load();
}
}, 5000);
start(pc, dc);

View File

@@ -0,0 +1,53 @@
export const pingPoints = [];
export const batteryPoints = [];
function getChartConfig(pts, color, title, ymax=100) {
return {
type: 'line',
data: {
datasets: [{
label: title,
data: pts,
borderWidth: 1,
borderColor: color,
backgroundColor: color,
fill: 'origin'
}]
},
options: {
scales: {
x: {
type: 'time',
time: {
unit: 'minute',
displayFormats: {
second: 'h:mm a'
}
},
grid: {
color: '#222', // Grid lines color
},
ticks: {
source: 'data',
fontColor: 'rgba(255, 255, 255, 1.0)', // Y-axis label color
}
},
y: {
beginAtZero: true,
max: ymax,
grid: {
color: 'rgba(255, 255, 255, 0.1)', // Grid lines color
},
ticks: {
fontColor: 'rgba(255, 255, 255, 0.7)', // Y-axis label color
}
}
}
}
}
}
const ctxPing = document.getElementById('chart-ping');
const ctxBattery = document.getElementById('chart-battery');
export const chartPing = new Chart(ctxPing, getChartConfig(pingPoints, 'rgba(192, 57, 43, 0.7)', 'Controls Ping Time (ms)', 250));
export const chartBattery = new Chart(ctxBattery, getChartConfig(batteryPoints, 'rgba(41, 128, 185, 0.7)', 'Battery %', 100));

View File

@@ -0,0 +1,209 @@
import { getXY } from "./controls.js";
import { pingPoints, batteryPoints, chartPing, chartBattery } from "./plots.js";
export let controlCommandInterval = null;
export let latencyInterval = null;
export let lastChannelMessageTime = null;
export function offerRtcRequest(sdp, type) {
return fetch('/offer', {
body: JSON.stringify({sdp: sdp, type: type}),
headers: {'Content-Type': 'application/json'},
method: 'POST'
});
}
export function playSoundRequest(sound) {
return fetch('/sound', {
body: JSON.stringify({sound}),
headers: {'Content-Type': 'application/json'},
method: 'POST'
});
}
export function pingHeadRequest() {
return fetch('/', {
method: 'HEAD'
});
}
export function createPeerConnection(pc) {
var config = {
sdpSemantics: 'unified-plan'
};
pc = new RTCPeerConnection(config);
// connect audio / video
pc.addEventListener('track', function(evt) {
console.log("Adding Tracks!")
if (evt.track.kind == 'video')
document.getElementById('video').srcObject = evt.streams[0];
else
document.getElementById('audio').srcObject = evt.streams[0];
});
return pc;
}
export function negotiate(pc) {
return pc.createOffer({offerToReceiveAudio:true, offerToReceiveVideo:true}).then(function(offer) {
return pc.setLocalDescription(offer);
}).then(function() {
return new Promise(function(resolve) {
if (pc.iceGatheringState === 'complete') {
resolve();
}
else {
function checkState() {
if (pc.iceGatheringState === 'complete') {
pc.removeEventListener('icegatheringstatechange', checkState);
resolve();
}
}
pc.addEventListener('icegatheringstatechange', checkState);
}
});
}).then(function() {
var offer = pc.localDescription;
return offerRtcRequest(offer.sdp, offer.type);
}).then(function(response) {
console.log(response);
return response.json();
}).then(function(answer) {
return pc.setRemoteDescription(answer);
}).catch(function(e) {
alert(e);
});
}
function isMobile() {
let check = false;
(function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera);
return check;
};
export const constraints = {
audio: {
autoGainControl: false,
sampleRate: 48000,
sampleSize: 16,
echoCancellation: true,
noiseSuppression: true,
channelCount: 1
},
video: isMobile()
};
export function start(pc, dc) {
pc = createPeerConnection(pc);
// add audio track
navigator.mediaDevices.enumerateDevices()
.then(function(devices) {
const hasAudioInput = devices.find((device) => device.kind === "audioinput");
var modifiedConstraints = {};
modifiedConstraints.video = constraints.video;
modifiedConstraints.audio = hasAudioInput ? constraints.audio : false;
return Promise.resolve(modifiedConstraints);
})
.then(function(constraints) {
if (constraints.audio || constraints.video) {
return navigator.mediaDevices.getUserMedia(constraints);
} else{
return Promise.resolve(null);
}
})
.then(function(stream) {
if (stream) {
stream.getTracks().forEach(function(track) {
pc.addTrack(track, stream);
});
}
return negotiate(pc);
})
.catch(function(err) {
alert('Could not acquire media: ' + err);
});
var parameters = {"ordered": true};
dc = pc.createDataChannel('data', parameters);
dc.onclose = function() {
clearInterval(controlCommandInterval);
clearInterval(latencyInterval);
};
function sendJoystickOverDataChannel() {
const {x, y} = getXY();
var message = JSON.stringify({type: "testJoystick", data: {axes: [x, y], buttons: [false]}})
dc.send(message);
}
function checkLatency() {
const initialTime = new Date().getTime();
pingHeadRequest().then(function() {
const currentTime = new Date().getTime();
if (Math.abs(currentTime - lastChannelMessageTime) < 1000) {
const pingtime = currentTime - initialTime;
pingPoints.push({'x': currentTime, 'y': pingtime});
if (pingPoints.length > 1000) {
pingPoints.shift();
}
chartPing.update();
$("#ping-time").text((pingtime) + "ms");
}
})
}
dc.onopen = function() {
controlCommandInterval = setInterval(sendJoystickOverDataChannel, 50);
latencyInterval = setInterval(checkLatency, 1000);
sendJoystickOverDataChannel();
};
const textDecoder = new TextDecoder();
var carStaterIndex = 0;
dc.onmessage = function(evt) {
const text = textDecoder.decode(evt.data);
const msg = JSON.parse(text);
if (carStaterIndex % 100 == 0 && msg.type === 'carState') {
const batteryLevel = Math.round(msg.data.fuelGauge * 100);
$("#battery").text(batteryLevel + "%");
batteryPoints.push({'x': new Date().getTime(), 'y': batteryLevel});
if (batteryPoints.length > 1000) {
batteryPoints.shift();
}
chartBattery.update();
}
carStaterIndex += 1;
lastChannelMessageTime = new Date().getTime();
$(".pre-blob").addClass('blob');
};
}
export function stop(pc, dc) {
if (dc) {
dc.close();
}
if (pc.getTransceivers) {
pc.getTransceivers().forEach(function(transceiver) {
if (transceiver.stop) {
transceiver.stop();
}
});
}
pc.getSenders().forEach(function(sender) {
sender.track.stop();
});
setTimeout(function() {
pc.close();
}, 500);
}

View File

@@ -0,0 +1,185 @@
body {
background: #333 !important;
color: #fff !important;
display: flex;
justify-content: center;
align-items: start;
}
p {
margin: 0px !important;
}
i {
font-style: normal;
}
.small {
font-size: 1em !important
}
.jumbo {
font-size: 8rem;
}
@media (max-width: 600px) {
.small {
font-size: 0.5em !important
}
.jumbo {
display: none;
}
}
#main {
display: flex;
flex-direction: column;
align-content: center;
justify-content: center;
align-items: center;
font-size: 30px;
width: 100%;
max-width: 1200px;
}
video {
width: 95%;
}
.pre-blob {
display: flex;
background: #333;
border-radius: 50%;
margin: 10px;
height: 45px;
width: 45px;
justify-content: center;
align-items: center;
font-size: 1rem;
}
.blob {
background: rgba(231, 76, 60,1.0);
box-shadow: 0 0 0 0 rgba(231, 76, 60,1.0);
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0px rgba(192, 57, 43, 1);
}
100% {
box-shadow: 0 0 0 20px rgba(192, 57, 43, 0);
}
}
.icon-sup-panel {
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
background: #222;
border-radius: 10px;
padding: 5px;
margin: 5px 0px 5px 0px;
}
.icon-sub-panel {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
}
#icon-panel {
display: flex;
width: 100%;
justify-content: space-between;
margin-top: 5px;
}
.icon-sub-sub-panel {
display: flex;
flex-direction: row;
}
.keys, #key-val {
background: #333;
padding: 2rem;
margin: 5px;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
border-radius: 10px;
cursor: pointer;
}
#key-val {
pointer-events: none;
background: #fff;
color: #333;
line-height: 1;
font-size: 20px;
flex-direction: column;
}
.wasd-row {
display: flex;
flex-direction: row;
justify-content: center;
align-items: stretch;
}
#wasd {
margin: 5px 0px 5px 0px;
background: #222;
border-radius: 10px;
width: 100%;
padding: 20px;
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: stretch;
user-select: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
touch-action: manipulation;
}
.panel {
display: flex;
justify-content: center;
margin: 5px 0px 5px 0px !important;
background: #222;
border-radius: 10px;
width: 100%;
padding: 10px;
}
#ping-time, #battery {
font-size: 25px;
}
#stop {
display: none;
}
.plan-form {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
}
.details {
display: flex;
padding: 0px 10px 0px 10px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

126
tools/bodyteleop/web.py Normal file
View File

@@ -0,0 +1,126 @@
import asyncio
import dataclasses
import json
import logging
import os
import ssl
import subprocess
from aiohttp import web, ClientSession
import pyaudio
import wave
from openpilot.common.basedir import BASEDIR
from openpilot.system.webrtc.webrtcd import StreamRequestBody
from openpilot.common.params import Params
logger = logging.getLogger("bodyteleop")
logging.basicConfig(level=logging.INFO)
TELEOPDIR = f"{BASEDIR}/tools/bodyteleop"
WEBRTCD_HOST, WEBRTCD_PORT = "localhost", 5001
## UTILS
async def play_sound(sound):
SOUNDS = {
"engage": "selfdrive/assets/sounds/engage.wav",
"disengage": "selfdrive/assets/sounds/disengage.wav",
"error": "selfdrive/assets/sounds/warning_immediate.wav",
}
assert sound in SOUNDS
chunk = 5120
with wave.open(os.path.join(BASEDIR, SOUNDS[sound]), "rb") as wf:
def callback(in_data, frame_count, time_info, status):
data = wf.readframes(frame_count)
return data, pyaudio.paContinue
p = pyaudio.PyAudio()
stream = p.open(format=p.get_format_from_width(wf.getsampwidth()),
channels=wf.getnchannels(),
rate=wf.getframerate(),
output=True,
frames_per_buffer=chunk,
stream_callback=callback)
stream.start_stream()
while stream.is_active():
await asyncio.sleep(0)
stream.stop_stream()
stream.close()
p.terminate()
## SSL
def create_ssl_cert(cert_path, key_path):
try:
proc = subprocess.run(f'openssl req -x509 -newkey rsa:4096 -nodes -out {cert_path} -keyout {key_path} \
-days 365 -subj "/C=US/ST=California/O=commaai/OU=comma body"',
stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
proc.check_returncode()
except subprocess.CalledProcessError as ex:
raise ValueError(f"Error creating SSL certificate:\n[stdout]\n{proc.stdout.decode()}\n[stderr]\n{proc.stderr.decode()}") from ex
def create_ssl_context():
cert_path = os.path.join(TELEOPDIR, "cert.pem")
key_path = os.path.join(TELEOPDIR, "key.pem")
if not os.path.exists(cert_path) or not os.path.exists(key_path):
logger.info("Creating certificate...")
create_ssl_cert(cert_path, key_path)
else:
logger.info("Certificate exists!")
ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_SERVER)
ssl_context.load_cert_chain(cert_path, key_path)
return ssl_context
## ENDPOINTS
async def index(request):
with open(os.path.join(TELEOPDIR, "static", "index.html"), "r") as f:
content = f.read()
return web.Response(content_type="text/html", text=content)
async def ping(request):
return web.Response(text="pong")
async def sound(request):
params = await request.json()
sound_to_play = params["sound"]
await play_sound(sound_to_play)
return web.json_response({"status": "ok"})
async def offer(request):
params = await request.json()
body = StreamRequestBody(params["sdp"], ["driver"], ["testJoystick"], ["carState"])
body_json = json.dumps(dataclasses.asdict(body))
logger.info("Sending offer to webrtcd...")
webrtcd_url = f"http://{WEBRTCD_HOST}:{WEBRTCD_PORT}/stream"
async with ClientSession() as session, session.post(webrtcd_url, data=body_json) as resp:
assert resp.status == 200
answer = await resp.json()
return web.json_response(answer)
def main():
# Enable joystick debug mode
Params().put_bool("JoystickDebugMode", True)
# App needs to be HTTPS for microphone and audio autoplay to work on the browser
ssl_context = create_ssl_context()
app = web.Application()
app.router.add_get("/", index)
app.router.add_get("/ping", ping, allow_head=True)
app.router.add_post("/offer", offer)
app.router.add_post("/sound", sound)
app.router.add_static('/static', os.path.join(TELEOPDIR, 'static'))
web.run_app(app, access_log=None, host="0.0.0.0", port=5000, ssl_context=ssl_context)
if __name__ == "__main__":
main()