[commabody] Add new body teleop ui (#29119)
* add new ui * move body tele op ui to new dir * fix codespell errors * resolve mediablackhole pylint error * fix import error * style fixes * use logging, not print * fix js styling * resolve commentspull/29235/head
parent
7949dfe796
commit
a7304d059c
13 changed files with 1000 additions and 80 deletions
@ -0,0 +1,4 @@ |
|||||||
|
av |
||||||
|
av-10.0.0/* |
||||||
|
key.pem |
||||||
|
cert.pem |
@ -0,0 +1,158 @@ |
|||||||
|
import asyncio |
||||||
|
import io |
||||||
|
import numpy as np |
||||||
|
import pyaudio |
||||||
|
import wave |
||||||
|
|
||||||
|
from aiortc.contrib.media import MediaBlackhole |
||||||
|
from aiortc.mediastreams import AudioStreamTrack, MediaStreamError, MediaStreamTrack |
||||||
|
from aiortc.mediastreams import VIDEO_CLOCK_RATE, VIDEO_TIME_BASE |
||||||
|
from aiortc.rtcrtpsender import RTCRtpSender |
||||||
|
from av import CodecContext, Packet |
||||||
|
from pydub import AudioSegment |
||||||
|
import cereal.messaging as messaging |
||||||
|
|
||||||
|
AUDIO_RATE = 16000 |
||||||
|
SOUNDS = { |
||||||
|
'engage': '../../selfdrive/assets/sounds/engage.wav', |
||||||
|
'disengage': '../../selfdrive/assets/sounds/disengage.wav', |
||||||
|
'error': '../../selfdrive/assets/sounds/warning_immediate.wav', |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
def force_codec(pc, sender, forced_codec='video/VP9', stream_type="video"): |
||||||
|
codecs = RTCRtpSender.getCapabilities(stream_type).codecs |
||||||
|
codec = [codec for codec in codecs if codec.mimeType == forced_codec] |
||||||
|
transceiver = next(t for t in pc.getTransceivers() if t.sender == sender) |
||||||
|
transceiver.setCodecPreferences(codec) |
||||||
|
|
||||||
|
|
||||||
|
class EncodedBodyVideo(MediaStreamTrack): |
||||||
|
kind = "video" |
||||||
|
|
||||||
|
_start: float |
||||||
|
_timestamp: int |
||||||
|
|
||||||
|
def __init__(self): |
||||||
|
super().__init__() |
||||||
|
sock_name = 'livestreamDriverEncodeData' |
||||||
|
messaging.context = messaging.Context() |
||||||
|
self.sock = messaging.sub_sock(sock_name, None, conflate=True) |
||||||
|
self.pts = 0 |
||||||
|
|
||||||
|
async def recv(self) -> Packet: |
||||||
|
while True: |
||||||
|
msg = messaging.recv_one_or_none(self.sock) |
||||||
|
if msg is not None: |
||||||
|
break |
||||||
|
await asyncio.sleep(0.005) |
||||||
|
|
||||||
|
evta = getattr(msg, msg.which()) |
||||||
|
self.last_idx = evta.idx.encodeId |
||||||
|
|
||||||
|
packet = Packet(evta.header + evta.data) |
||||||
|
packet.time_base = VIDEO_TIME_BASE |
||||||
|
packet.pts = self.pts |
||||||
|
self.pts += 0.05 * VIDEO_CLOCK_RATE |
||||||
|
return packet |
||||||
|
|
||||||
|
|
||||||
|
class WebClientSpeaker(MediaBlackhole): |
||||||
|
def __init__(self): |
||||||
|
super().__init__() |
||||||
|
self.p = pyaudio.PyAudio() |
||||||
|
self.buffer = io.BytesIO() |
||||||
|
self.channels = 2 |
||||||
|
self.stream = self.p.open(format=pyaudio.paInt16, channels=self.channels, rate=48000, frames_per_buffer=9600, output=True, stream_callback=self.pyaudio_callback) |
||||||
|
|
||||||
|
def pyaudio_callback(self, in_data, frame_count, time_info, status): |
||||||
|
if self.buffer.getbuffer().nbytes < frame_count * self.channels * 2: |
||||||
|
buff = np.zeros((frame_count, 2), dtype=np.int16).tobytes() |
||||||
|
elif self.buffer.getbuffer().nbytes > 115200: # 3x the usual read size |
||||||
|
self.buffer.seek(0) |
||||||
|
buff = self.buffer.read(frame_count * self.channels * 4) |
||||||
|
buff = buff[:frame_count * self.channels * 2] |
||||||
|
self.buffer.seek(2) |
||||||
|
else: |
||||||
|
self.buffer.seek(0) |
||||||
|
buff = self.buffer.read(frame_count * self.channels * 2) |
||||||
|
self.buffer.seek(2) |
||||||
|
return (buff, pyaudio.paContinue) |
||||||
|
|
||||||
|
async def consume(self, track): |
||||||
|
while True: |
||||||
|
try: |
||||||
|
frame = await track.recv() |
||||||
|
except MediaStreamError: |
||||||
|
return |
||||||
|
bio = bytes(frame.planes[0]) |
||||||
|
self.buffer.write(bio) |
||||||
|
|
||||||
|
async def start(self): |
||||||
|
for track, task in self._MediaBlackhole__tracks.items(): # pylint: disable=access-member-before-definition |
||||||
|
if task is None: |
||||||
|
self._MediaBlackhole__tracks[track] = asyncio.ensure_future(self.consume(track)) |
||||||
|
|
||||||
|
async def stop(self): |
||||||
|
for task in self._MediaBlackhole__tracks.values(): # pylint: disable=access-member-before-definition |
||||||
|
if task is not None: |
||||||
|
task.cancel() |
||||||
|
self._MediaBlackhole__tracks = {} |
||||||
|
self.stream.stop_stream() |
||||||
|
self.stream.close() |
||||||
|
self.p.terminate() |
||||||
|
|
||||||
|
|
||||||
|
class BodyMic(AudioStreamTrack): |
||||||
|
def __init__(self): |
||||||
|
super().__init__() |
||||||
|
|
||||||
|
self.sample_rate = AUDIO_RATE |
||||||
|
self.AUDIO_PTIME = 0.020 # 20ms audio packetization |
||||||
|
self.samples = int(self.AUDIO_PTIME * self.sample_rate) |
||||||
|
self.FORMAT = pyaudio.paInt16 |
||||||
|
self.CHANNELS = 2 |
||||||
|
self.RATE = self.sample_rate |
||||||
|
self.CHUNK = int(AUDIO_RATE * 0.020) |
||||||
|
self.p = pyaudio.PyAudio() |
||||||
|
self.mic_stream = self.p.open(format=self.FORMAT, channels=1, rate=self.RATE, input=True, frames_per_buffer=self.CHUNK) |
||||||
|
|
||||||
|
self.codec = CodecContext.create('pcm_s16le', 'r') |
||||||
|
self.codec.sample_rate = self.RATE |
||||||
|
self.codec.channels = 2 |
||||||
|
self.audio_samples = 0 |
||||||
|
self.chunk_number = 0 |
||||||
|
|
||||||
|
async def recv(self): |
||||||
|
mic_data = self.mic_stream.read(self.CHUNK) |
||||||
|
mic_sound = AudioSegment(mic_data, sample_width=2, channels=1, frame_rate=self.RATE) |
||||||
|
mic_sound = AudioSegment.from_mono_audiosegments(mic_sound, mic_sound) |
||||||
|
mic_sound += 3 # increase volume by 3db |
||||||
|
packet = Packet(mic_sound.raw_data) |
||||||
|
frame = self.codec.decode(packet)[0] |
||||||
|
frame.pts = self.audio_samples |
||||||
|
self.audio_samples += frame.samples |
||||||
|
self.chunk_number = self.chunk_number + 1 |
||||||
|
return frame |
||||||
|
|
||||||
|
|
||||||
|
async def play_sound(sound): |
||||||
|
chunk = 5120 |
||||||
|
with wave.open(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() |
@ -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> |
@ -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(); |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
import { handleKeyX, executePlan } from "./controls.js"; |
||||||
|
import { start, stop, last_ping } 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); |
||||||
|
|
||||||
|
setInterval( () => { |
||||||
|
const dt = new Date().getTime(); |
||||||
|
if ((dt - last_ping) > 1000) { |
||||||
|
$(".pre-blob").removeClass('blob'); |
||||||
|
$("#battery").text("-"); |
||||||
|
$("#ping-time").text('-'); |
||||||
|
$("video")[0].load(); |
||||||
|
} |
||||||
|
}, 5000); |
||||||
|
|
||||||
|
start(pc, dc); |
@ -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)); |
@ -0,0 +1,217 @@ |
|||||||
|
import { getXY } from "./controls.js"; |
||||||
|
import { pingPoints, batteryPoints, chartPing, chartBattery } from "./plots.js"; |
||||||
|
|
||||||
|
export let dcInterval = null; |
||||||
|
export let batteryInterval = null; |
||||||
|
export let last_ping = null; |
||||||
|
|
||||||
|
|
||||||
|
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 fetch('/offer', { |
||||||
|
body: JSON.stringify({ |
||||||
|
sdp: offer.sdp, |
||||||
|
type: offer.type, |
||||||
|
}), |
||||||
|
headers: { |
||||||
|
'Content-Type': 'application/json' |
||||||
|
}, |
||||||
|
method: 'POST' |
||||||
|
}); |
||||||
|
}).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 createDummyVideoTrack() { |
||||||
|
const canvas = document.createElement('canvas'); |
||||||
|
const context = canvas.getContext('2d'); |
||||||
|
|
||||||
|
const frameWidth = 5; // Set the width of the frame
|
||||||
|
const frameHeight = 5; // Set the height of the frame
|
||||||
|
canvas.width = frameWidth; |
||||||
|
canvas.height = frameHeight; |
||||||
|
|
||||||
|
context.fillStyle = 'black'; |
||||||
|
context.fillRect(0, 0, frameWidth, frameHeight); |
||||||
|
|
||||||
|
const stream = canvas.captureStream(); |
||||||
|
const videoTrack = stream.getVideoTracks()[0]; |
||||||
|
|
||||||
|
return videoTrack; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
export function start(pc, dc) { |
||||||
|
pc = createPeerConnection(pc); |
||||||
|
|
||||||
|
if (constraints.audio || constraints.video) { |
||||||
|
// add audio track
|
||||||
|
navigator.mediaDevices.getUserMedia(constraints).then(function(stream) { |
||||||
|
stream.getTracks().forEach(function(track) { |
||||||
|
pc.addTrack(track, stream); |
||||||
|
// only audio?
|
||||||
|
// if (track.kind === 'audio'){
|
||||||
|
// pc.addTrack(track, stream);
|
||||||
|
// }
|
||||||
|
}); |
||||||
|
return negotiate(pc); |
||||||
|
}, function(err) { |
||||||
|
alert('Could not acquire media: ' + err); |
||||||
|
}); |
||||||
|
|
||||||
|
// add a fake video?
|
||||||
|
// const dummyVideoTrack = createDummyVideoTrack();
|
||||||
|
// const dummyMediaStream = new MediaStream();
|
||||||
|
// dummyMediaStream.addTrack(dummyVideoTrack);
|
||||||
|
// pc.addTrack(dummyVideoTrack, dummyMediaStream);
|
||||||
|
|
||||||
|
} else { |
||||||
|
negotiate(pc); |
||||||
|
} |
||||||
|
|
||||||
|
// setInterval(() => {pc.getStats(null).then((stats) => {stats.forEach((report) => console.log(report))})}, 10000)
|
||||||
|
// var video = document.querySelector('video');
|
||||||
|
// var print = function (e, f){console.log(e, f); video.requestVideoFrameCallback(print);};
|
||||||
|
// video.requestVideoFrameCallback(print);
|
||||||
|
|
||||||
|
|
||||||
|
var parameters = {"ordered": true}; |
||||||
|
dc = pc.createDataChannel('data', parameters); |
||||||
|
dc.onclose = function() { |
||||||
|
console.log("data channel closed"); |
||||||
|
clearInterval(dcInterval); |
||||||
|
clearInterval(batteryInterval); |
||||||
|
}; |
||||||
|
function controlCommand() { |
||||||
|
const {x, y} = getXY(); |
||||||
|
const dt = new Date().getTime(); |
||||||
|
var message = JSON.stringify({type: 'control_command', x, y, dt}); |
||||||
|
dc.send(message); |
||||||
|
} |
||||||
|
|
||||||
|
function batteryLevel() { |
||||||
|
var message = JSON.stringify({type: 'battery_level'}); |
||||||
|
dc.send(message); |
||||||
|
} |
||||||
|
|
||||||
|
dc.onopen = function() { |
||||||
|
dcInterval = setInterval(controlCommand, 50); |
||||||
|
batteryInterval = setInterval(batteryLevel, 10000); |
||||||
|
controlCommand(); |
||||||
|
batteryLevel(); |
||||||
|
$(".sound").click((e)=>{ |
||||||
|
const sound = $(e.target).attr('id').replace('sound-', '') |
||||||
|
dc.send(JSON.stringify({type: 'play_sound', sound})); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
let val_print_idx = 0; |
||||||
|
dc.onmessage = function(evt) { |
||||||
|
const data = JSON.parse(evt.data); |
||||||
|
if(val_print_idx == 0 && data.type === 'ping_time') { |
||||||
|
const dt = new Date().getTime(); |
||||||
|
const pingtime = dt - data.incoming_time; |
||||||
|
pingPoints.push({'x': dt, 'y': pingtime}); |
||||||
|
if (pingPoints.length > 1000) { |
||||||
|
pingPoints.shift(); |
||||||
|
} |
||||||
|
chartPing.update(); |
||||||
|
$("#ping-time").text((pingtime) + "ms"); |
||||||
|
last_ping = dt; |
||||||
|
$(".pre-blob").addClass('blob'); |
||||||
|
} |
||||||
|
val_print_idx = (val_print_idx + 1 ) % 20; |
||||||
|
if(data.type === 'battery_level') { |
||||||
|
$("#battery").text(data.value + "%"); |
||||||
|
batteryPoints.push({'x': new Date().getTime(), 'y': data.value}); |
||||||
|
if (batteryPoints.length > 1000) { |
||||||
|
batteryPoints.shift(); |
||||||
|
} |
||||||
|
chartBattery.update(); |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
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); |
||||||
|
} |
@ -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; |
||||||
|
} |
After Width: | Height: | Size: 8.1 KiB |
@ -0,0 +1,201 @@ |
|||||||
|
import asyncio |
||||||
|
import json |
||||||
|
import logging |
||||||
|
import os |
||||||
|
import ssl |
||||||
|
import uuid |
||||||
|
import time |
||||||
|
|
||||||
|
from common.basedir import BASEDIR |
||||||
|
from aiohttp import web |
||||||
|
from aiortc import RTCPeerConnection, RTCSessionDescription |
||||||
|
|
||||||
|
import cereal.messaging as messaging |
||||||
|
from tools.bodyteleop.bodyav import BodyMic, WebClientSpeaker, force_codec, play_sound, MediaBlackhole, EncodedBodyVideo |
||||||
|
|
||||||
|
logger = logging.getLogger("pc") |
||||||
|
logging.basicConfig(level=logging.INFO) |
||||||
|
|
||||||
|
pcs = set() |
||||||
|
pm, sm = None, None |
||||||
|
TELEOPDIR = f"{BASEDIR}/tools/bodyteleop" |
||||||
|
|
||||||
|
|
||||||
|
async def index(request): |
||||||
|
content = open(TELEOPDIR + "/static/index.html", "r").read() |
||||||
|
now = time.monotonic() |
||||||
|
request.app['mutable_vals']['last_send_time'] = now |
||||||
|
request.app['mutable_vals']['last_override_time'] = now |
||||||
|
request.app['mutable_vals']['prev_command'] = [] |
||||||
|
request.app['mutable_vals']['find_person'] = False |
||||||
|
|
||||||
|
return web.Response(content_type="text/html", text=content) |
||||||
|
|
||||||
|
|
||||||
|
async def control_body(data, app): |
||||||
|
now = time.monotonic() |
||||||
|
if (data['type'] == 'dummy_controls') and (now < (app['mutable_vals']['last_send_time'] + 0.2)): |
||||||
|
return |
||||||
|
if (data['type'] == 'control_command') and (app['mutable_vals']['prev_command'] == [data['x'], data['y']] and data['x'] == 0 and data['y'] == 0): |
||||||
|
return |
||||||
|
|
||||||
|
logger.info(str(data)) |
||||||
|
x = max(-1.0, min(1.0, data['x'])) |
||||||
|
y = max(-1.0, min(1.0, data['y'])) |
||||||
|
dat = messaging.new_message('testJoystick') |
||||||
|
dat.testJoystick.axes = [x, y] |
||||||
|
dat.testJoystick.buttons = [False] |
||||||
|
pm.send('testJoystick', dat) |
||||||
|
app['mutable_vals']['last_send_time'] = now |
||||||
|
if (data['type'] == 'control_command'): |
||||||
|
app['mutable_vals']['last_override_time'] = now |
||||||
|
app['mutable_vals']['prev_command'] = [data['x'], data['y']] |
||||||
|
|
||||||
|
|
||||||
|
async def dummy_controls_msg(app): |
||||||
|
while True: |
||||||
|
if 'last_send_time' in app['mutable_vals']: |
||||||
|
this_time = time.monotonic() |
||||||
|
if (app['mutable_vals']['last_send_time'] + 0.2) < this_time: |
||||||
|
await control_body({'type': 'dummy_controls', 'x': 0, 'y': 0}, app) |
||||||
|
await asyncio.sleep(0.2) |
||||||
|
|
||||||
|
|
||||||
|
async def start_background_tasks(app): |
||||||
|
app['bgtask_dummy_controls_msg'] = asyncio.create_task(dummy_controls_msg(app)) |
||||||
|
|
||||||
|
|
||||||
|
async def stop_background_tasks(app): |
||||||
|
app['bgtask_dummy_controls_msg'].cancel() |
||||||
|
await app['bgtask_dummy_controls_msg'] |
||||||
|
|
||||||
|
|
||||||
|
async def offer(request): |
||||||
|
logger.info("\n\n\nnewoffer!\n\n") |
||||||
|
|
||||||
|
params = await request.json() |
||||||
|
offer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) |
||||||
|
speaker = WebClientSpeaker() |
||||||
|
blackhole = MediaBlackhole() |
||||||
|
|
||||||
|
pc = RTCPeerConnection() |
||||||
|
pc_id = "PeerConnection(%s)" % uuid.uuid4() |
||||||
|
pcs.add(pc) |
||||||
|
|
||||||
|
def log_info(msg, *args): |
||||||
|
logger.info(pc_id + " " + msg, *args) |
||||||
|
|
||||||
|
log_info("Created for %s", request.remote) |
||||||
|
|
||||||
|
@pc.on("datachannel") |
||||||
|
def on_datachannel(channel): |
||||||
|
request.app['mutable_vals']['remote_channel'] = channel |
||||||
|
|
||||||
|
@channel.on("message") |
||||||
|
async def on_message(message): |
||||||
|
data = json.loads(message) |
||||||
|
if data['type'] == 'control_command': |
||||||
|
await control_body(data, request.app) |
||||||
|
times = { |
||||||
|
'type': 'ping_time', |
||||||
|
'incoming_time': data['dt'], |
||||||
|
'outgoing_time': int(time.time() * 1000), |
||||||
|
} |
||||||
|
channel.send(json.dumps(times)) |
||||||
|
if data['type'] == 'battery_level': |
||||||
|
sm.update(timeout=0) |
||||||
|
if sm.updated['carState']: |
||||||
|
channel.send(json.dumps({'type': 'battery_level', 'value': int(sm['carState'].fuelGauge * 100)})) |
||||||
|
if data['type'] == 'play_sound': |
||||||
|
logger.info(f"Playing sound: {data['sound']}") |
||||||
|
await play_sound(data['sound']) |
||||||
|
if data['type'] == 'find_person': |
||||||
|
request.app['mutable_vals']['find_person'] = data['value'] |
||||||
|
|
||||||
|
@pc.on("connectionstatechange") |
||||||
|
async def on_connectionstatechange(): |
||||||
|
log_info("Connection state is %s", pc.connectionState) |
||||||
|
if pc.connectionState == "failed": |
||||||
|
await pc.close() |
||||||
|
pcs.discard(pc) |
||||||
|
|
||||||
|
@pc.on('track') |
||||||
|
def on_track(track): |
||||||
|
logger.info(f"Track received: {track.kind}") |
||||||
|
if track.kind == "audio": |
||||||
|
speaker.addTrack(track) |
||||||
|
elif track.kind == "video": |
||||||
|
blackhole.addTrack(track) |
||||||
|
|
||||||
|
@track.on("ended") |
||||||
|
async def on_ended(): |
||||||
|
log_info("Remote %s track ended", track.kind) |
||||||
|
if track.kind == "audio": |
||||||
|
await speaker.stop() |
||||||
|
elif track.kind == "video": |
||||||
|
await blackhole.stop() |
||||||
|
|
||||||
|
video_sender = pc.addTrack(EncodedBodyVideo()) |
||||||
|
force_codec(pc, video_sender, forced_codec='video/H264') |
||||||
|
_ = pc.addTrack(BodyMic()) |
||||||
|
|
||||||
|
await pc.setRemoteDescription(offer) |
||||||
|
await speaker.start() |
||||||
|
await blackhole.start() |
||||||
|
answer = await pc.createAnswer() |
||||||
|
await pc.setLocalDescription(answer) |
||||||
|
|
||||||
|
return web.Response( |
||||||
|
content_type="application/json", |
||||||
|
text=json.dumps( |
||||||
|
{"sdp": pc.localDescription.sdp, "type": pc.localDescription.type} |
||||||
|
), |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
async def on_shutdown(app): |
||||||
|
coros = [pc.close() for pc in pcs] |
||||||
|
await asyncio.gather(*coros) |
||||||
|
pcs.clear() |
||||||
|
|
||||||
|
|
||||||
|
async def run(cmd): |
||||||
|
proc = await asyncio.create_subprocess_shell( |
||||||
|
cmd, |
||||||
|
stdout=asyncio.subprocess.PIPE, |
||||||
|
stderr=asyncio.subprocess.PIPE |
||||||
|
) |
||||||
|
stdout, stderr = await proc.communicate() |
||||||
|
logger.info("Created key and cert!") |
||||||
|
if stdout: |
||||||
|
logger.info(f'[stdout]\n{stdout.decode()}') |
||||||
|
if stderr: |
||||||
|
logger.info(f'[stderr]\n{stderr.decode()}') |
||||||
|
|
||||||
|
|
||||||
|
def main(): |
||||||
|
global pm, sm |
||||||
|
pm = messaging.PubMaster(['testJoystick']) |
||||||
|
sm = messaging.SubMaster(['carState', 'logMessage']) |
||||||
|
# App needs to be HTTPS for microphone and audio autoplay to work on the browser |
||||||
|
cert_path = TELEOPDIR + '/cert.pem' |
||||||
|
key_path = TELEOPDIR + '/key.pem' |
||||||
|
if (not os.path.exists(cert_path)) or (not os.path.exists(key_path)): |
||||||
|
asyncio.run(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"')) |
||||||
|
else: |
||||||
|
logger.info("Certificate exists!") |
||||||
|
ssl_context = ssl.SSLContext() |
||||||
|
ssl_context.load_cert_chain(cert_path, key_path) |
||||||
|
app = web.Application() |
||||||
|
app['mutable_vals'] = {} |
||||||
|
app.on_shutdown.append(on_shutdown) |
||||||
|
app.router.add_post("/offer", offer) |
||||||
|
app.router.add_get("/", index) |
||||||
|
app.router.add_static('/static', TELEOPDIR + '/static') |
||||||
|
app.on_startup.append(start_background_tasks) |
||||||
|
app.on_cleanup.append(stop_background_tasks) |
||||||
|
web.run_app(app, access_log=None, host="0.0.0.0", port=5000, ssl_context=ssl_context) |
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
main() |
@ -1,78 +0,0 @@ |
|||||||
#!/usr/bin/env python3 |
|
||||||
import time |
|
||||||
import threading |
|
||||||
from flask import Flask |
|
||||||
|
|
||||||
import cereal.messaging as messaging |
|
||||||
|
|
||||||
app = Flask(__name__) |
|
||||||
pm = messaging.PubMaster(['testJoystick']) |
|
||||||
|
|
||||||
index = """ |
|
||||||
<html> |
|
||||||
<head> |
|
||||||
<script src="https://github.com/bobboteck/JoyStick/releases/download/v1.1.6/joy.min.js"></script> |
|
||||||
</head> |
|
||||||
<body> |
|
||||||
<div id="joyDiv" style="width:100%;height:100%"></div> |
|
||||||
<script type="text/javascript"> |
|
||||||
// Set up gamepad handlers |
|
||||||
let gamepad = null; |
|
||||||
window.addEventListener("gamepadconnected", function(e) { |
|
||||||
gamepad = e.gamepad; |
|
||||||
}); |
|
||||||
window.addEventListener("gamepaddisconnected", function(e) { |
|
||||||
gamepad = null; |
|
||||||
}); |
|
||||||
// Create JoyStick object into the DIV 'joyDiv' |
|
||||||
var joy = new JoyStick('joyDiv'); |
|
||||||
setInterval(function(){ |
|
||||||
var x = -joy.GetX()/100; |
|
||||||
var y = joy.GetY()/100; |
|
||||||
if (x === 0 && y === 0 && gamepad !== null) { |
|
||||||
let gamepadstate = navigator.getGamepads()[gamepad.index]; |
|
||||||
x = -gamepadstate.axes[0]; |
|
||||||
y = -gamepadstate.axes[1]; |
|
||||||
} |
|
||||||
let xhr = new XMLHttpRequest(); |
|
||||||
xhr.open("GET", "/control/"+x+"/"+y); |
|
||||||
xhr.send(); |
|
||||||
}, 50); |
|
||||||
</script> |
|
||||||
""" |
|
||||||
|
|
||||||
@app.route("/") |
|
||||||
def hello_world(): |
|
||||||
return index |
|
||||||
|
|
||||||
last_send_time = time.monotonic() |
|
||||||
@app.route("/control/<x>/<y>") |
|
||||||
def control(x, y): |
|
||||||
global last_send_time |
|
||||||
x,y = float(x), float(y) |
|
||||||
x = max(-1, min(1, x)) |
|
||||||
y = max(-1, min(1, y)) |
|
||||||
dat = messaging.new_message('testJoystick') |
|
||||||
dat.testJoystick.axes = [y,x] |
|
||||||
dat.testJoystick.buttons = [False] |
|
||||||
pm.send('testJoystick', dat) |
|
||||||
last_send_time = time.monotonic() |
|
||||||
return "" |
|
||||||
|
|
||||||
def handle_timeout(): |
|
||||||
while 1: |
|
||||||
this_time = time.monotonic() |
|
||||||
if (last_send_time+0.5) < this_time: |
|
||||||
#print("timeout, no web in %.2f s" % (this_time-last_send_time)) |
|
||||||
dat = messaging.new_message('testJoystick') |
|
||||||
dat.testJoystick.axes = [0,0] |
|
||||||
dat.testJoystick.buttons = [False] |
|
||||||
pm.send('testJoystick', dat) |
|
||||||
time.sleep(0.1) |
|
||||||
|
|
||||||
def main(): |
|
||||||
threading.Thread(target=handle_timeout, daemon=True).start() |
|
||||||
app.run(host="0.0.0.0") |
|
||||||
|
|
||||||
if __name__ == '__main__': |
|
||||||
main() |
|
Loading…
Reference in new issue