mirror of
https://github.com/dragonpilot/dragonpilot.git
synced 2026-08-22 16:53:49 +08:00
dragonpilot beta3
date: 2023-12-23T21:19:29 commit: 38612b14f1a8aa49d1c6ef61bd67f5a095abb3f9
This commit is contained in:
@@ -1,159 +0,0 @@
|
||||
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():
|
||||
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():
|
||||
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()
|
||||
@@ -1,5 +1,5 @@
|
||||
import { handleKeyX, executePlan } from "./controls.js";
|
||||
import { start, stop, last_ping } from "./webrtc.js";
|
||||
import { start, stop, lastChannelMessageTime, playSoundRequest } from "./webrtc.js";
|
||||
|
||||
export var pc = null;
|
||||
export var dc = null;
|
||||
@@ -9,10 +9,14 @@ 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 - last_ping) > 1000) {
|
||||
if ((dt - lastChannelMessageTime) > 1000) {
|
||||
$(".pre-blob").removeClass('blob');
|
||||
$("#battery").text("-");
|
||||
$("#ping-time").text('-');
|
||||
@@ -20,4 +24,4 @@ setInterval( () => {
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
start(pc, dc);
|
||||
start(pc, dc);
|
||||
|
||||
@@ -1,9 +1,34 @@
|
||||
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 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) {
|
||||
@@ -45,16 +70,7 @@ export function negotiate(pc) {
|
||||
});
|
||||
}).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'
|
||||
});
|
||||
return offerRtcRequest(offer.sdp, offer.type);
|
||||
}).then(function(response) {
|
||||
console.log(response);
|
||||
return response.json();
|
||||
@@ -86,25 +102,6 @@ export const constraints = {
|
||||
};
|
||||
|
||||
|
||||
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);
|
||||
|
||||
@@ -138,71 +135,56 @@ export function start(pc, dc) {
|
||||
alert('Could not acquire media: ' + err);
|
||||
});
|
||||
|
||||
// add a fake video?
|
||||
// const dummyVideoTrack = createDummyVideoTrack();
|
||||
// const dummyMediaStream = new MediaStream();
|
||||
// dummyMediaStream.addTrack(dummyVideoTrack);
|
||||
// pc.addTrack(dummyVideoTrack, dummyMediaStream);
|
||||
|
||||
// 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);
|
||||
clearInterval(controlCommandInterval);
|
||||
clearInterval(latencyInterval);
|
||||
};
|
||||
function controlCommand() {
|
||||
|
||||
function sendJoystickOverDataChannel() {
|
||||
const {x, y} = getXY();
|
||||
const dt = new Date().getTime();
|
||||
var message = JSON.stringify({type: 'control_command', x, y, dt});
|
||||
var message = JSON.stringify({type: "testJoystick", data: {axes: [x, y], buttons: [false]}})
|
||||
dc.send(message);
|
||||
}
|
||||
|
||||
function batteryLevel() {
|
||||
var message = JSON.stringify({type: 'battery_level'});
|
||||
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() {
|
||||
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}));
|
||||
});
|
||||
controlCommandInterval = setInterval(sendJoystickOverDataChannel, 50);
|
||||
latencyInterval = setInterval(checkLatency, 1000);
|
||||
sendJoystickOverDataChannel();
|
||||
};
|
||||
|
||||
let val_print_idx = 0;
|
||||
const textDecoder = new TextDecoder();
|
||||
var carStaterIndex = 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) {
|
||||
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');
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+90
-171
@@ -1,205 +1,124 @@
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import ssl
|
||||
import uuid
|
||||
import time
|
||||
import subprocess
|
||||
|
||||
# aiortc and its dependencies have lots of internal warnings :(
|
||||
import warnings
|
||||
warnings.resetwarnings()
|
||||
warnings.simplefilter("always")
|
||||
from aiohttp import web, ClientSession
|
||||
import pyaudio
|
||||
import wave
|
||||
|
||||
from aiohttp import web
|
||||
from aiortc import RTCPeerConnection, RTCSessionDescription
|
||||
|
||||
import cereal.messaging as messaging
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.tools.bodyteleop.bodyav import BodyMic, WebClientSpeaker, force_codec, play_sound, MediaBlackhole, EncodedBodyVideo
|
||||
from openpilot.system.webrtc.webrtcd import StreamRequestBody
|
||||
from openpilot.common.params import Params
|
||||
|
||||
logger = logging.getLogger("pc")
|
||||
logger = logging.getLogger("bodyteleop")
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
pcs = set()
|
||||
pm, sm = None, None
|
||||
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):
|
||||
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)
|
||||
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 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 ping(request):
|
||||
return web.Response(text="pong")
|
||||
|
||||
|
||||
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 sound(request):
|
||||
params = await request.json()
|
||||
sound_to_play = params["sound"]
|
||||
|
||||
|
||||
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']
|
||||
await play_sound(sound_to_play)
|
||||
return web.json_response({"status": "ok"})
|
||||
|
||||
|
||||
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()
|
||||
body = StreamRequestBody(params["sdp"], ["driver"], ["testJoystick"], ["carState"])
|
||||
body_json = json.dumps(dataclasses.asdict(body))
|
||||
|
||||
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()}')
|
||||
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():
|
||||
global pm, sm
|
||||
pm = messaging.PubMaster(['testJoystick'])
|
||||
sm = messaging.SubMaster(['carState', 'logMessage'])
|
||||
# Enable joystick debug mode
|
||||
Params().put_bool("JoystickDebugMode", True)
|
||||
|
||||
# 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)
|
||||
ssl_context = create_ssl_context()
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
|
||||
@@ -45,17 +45,6 @@ In order to use a joystick over the network, we need to run joystickd locally fr
|
||||
tools/joystick/joystickd.py
|
||||
```
|
||||
|
||||
### Web joystick on your mobile device
|
||||
|
||||
A browser-based virtual joystick designed for touch screens. Starts automatically when installed on comma body (non-car robotics platform).
|
||||
For cars, start the web joystick service manually via SSH before starting the car.
|
||||
|
||||
```shell
|
||||
tools/joystick/web.py
|
||||
```
|
||||
|
||||
After starting the car/body, open the web joystick app at this URL: `http://[comma three IP address]:5000`
|
||||
|
||||
---
|
||||
Now start your car and openpilot should go into joystick mode with an alert on startup! The status of the axes will display on the alert, while button statuses print in the shell.
|
||||
|
||||
|
||||
@@ -82,9 +82,6 @@ def send_thread(joystick):
|
||||
dat.testJoystick.buttons = [joystick.cancel]
|
||||
joystick_sock.send(dat.to_bytes())
|
||||
print('\n' + ', '.join(f'{name}: {round(v, 3)}' for name, v in joystick.axes_values.items()))
|
||||
if "WEB" in os.environ:
|
||||
import requests
|
||||
requests.get("http://"+os.environ["WEB"]+":5000/control/%f/%f" % tuple([joystick.axes_values[a] for a in joystick.axes_order][::-1]), timeout=None)
|
||||
rk.keep_time()
|
||||
|
||||
def joystick_thread(joystick):
|
||||
@@ -101,7 +98,7 @@ if __name__ == '__main__':
|
||||
parser.add_argument('--gamepad', action='store_true', help='Use gamepad configuration instead of joystick')
|
||||
args = parser.parse_args()
|
||||
|
||||
if not Params().get_bool("IsOffroad") and "ZMQ" not in os.environ and "WEB" not in os.environ:
|
||||
if not Params().get_bool("IsOffroad") and "ZMQ" not in os.environ:
|
||||
print("The car must be off before running joystickd.")
|
||||
exit()
|
||||
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
import json
|
||||
import os
|
||||
from openpilot.common.file_helpers import mkdirs_exists_ok
|
||||
from openpilot.system.hardware import PC
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
|
||||
|
||||
class MissingAuthConfigError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
if PC:
|
||||
CONFIG_DIR = os.path.expanduser('~/.comma')
|
||||
else:
|
||||
CONFIG_DIR = "/tmp/.comma"
|
||||
|
||||
def get_token():
|
||||
try:
|
||||
with open(os.path.join(CONFIG_DIR, 'auth.json')) as f:
|
||||
with open(os.path.join(Paths.config_root(), 'auth.json')) as f:
|
||||
auth = json.load(f)
|
||||
return auth['access_token']
|
||||
except Exception:
|
||||
@@ -23,13 +17,13 @@ def get_token():
|
||||
|
||||
|
||||
def set_token(token):
|
||||
mkdirs_exists_ok(CONFIG_DIR)
|
||||
with open(os.path.join(CONFIG_DIR, 'auth.json'), 'w') as f:
|
||||
os.makedirs(Paths.config_root(), exist_ok=True)
|
||||
with open(os.path.join(Paths.config_root(), 'auth.json'), 'w') as f:
|
||||
json.dump({'access_token': token}, f)
|
||||
|
||||
|
||||
def clear_token():
|
||||
try:
|
||||
os.unlink(os.path.join(CONFIG_DIR, 'auth.json'))
|
||||
os.unlink(os.path.join(Paths.config_root(), 'auth.json'))
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
+1
-2
@@ -1,12 +1,11 @@
|
||||
import os
|
||||
import urllib.parse
|
||||
from openpilot.common.file_helpers import mkdirs_exists_ok
|
||||
|
||||
DEFAULT_CACHE_DIR = os.getenv("CACHE_ROOT", os.path.expanduser("~/.commacache"))
|
||||
|
||||
def cache_path_for_file_path(fn, cache_dir=DEFAULT_CACHE_DIR):
|
||||
dir_ = os.path.join(cache_dir, "local")
|
||||
mkdirs_exists_ok(dir_)
|
||||
os.makedirs(dir_, exist_ok=True)
|
||||
fn_parsed = urllib.parse.urlparse(fn)
|
||||
if fn_parsed.scheme == '':
|
||||
cache_fn = os.path.abspath(fn).replace("/", "_")
|
||||
|
||||
@@ -5,7 +5,7 @@ import pycurl
|
||||
from hashlib import sha256
|
||||
from io import BytesIO
|
||||
from tenacity import retry, wait_random_exponential, stop_after_attempt
|
||||
from openpilot.common.file_helpers import mkdirs_exists_ok, atomic_write_in_dir
|
||||
from openpilot.common.file_helpers import atomic_write_in_dir
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
# Cache chunk size
|
||||
K = 1000
|
||||
@@ -40,7 +40,7 @@ class URLFile:
|
||||
except AttributeError:
|
||||
self._curl = self._tlocal.curl = pycurl.Curl()
|
||||
if not self._force_download:
|
||||
mkdirs_exists_ok(Paths.download_cache_root())
|
||||
os.makedirs(Paths.download_cache_root(), exist_ok=True)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
@@ -16,6 +16,7 @@ class ConsoleUI : public QObject {
|
||||
public:
|
||||
ConsoleUI(Replay *replay, QObject *parent = 0);
|
||||
~ConsoleUI();
|
||||
inline static const std::array speed_array = {0.2f, 0.5f, 1.0f, 2.0f, 3.0f};
|
||||
|
||||
private:
|
||||
void initWindows();
|
||||
|
||||
@@ -6,13 +6,11 @@
|
||||
#endif
|
||||
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "cereal/gen/cpp/log.capnp.h"
|
||||
#include "system/camerad/cameras/camera_common.h"
|
||||
#include "tools/replay/filereader.h"
|
||||
|
||||
const CameraType ALL_CAMERAS[] = {RoadCam, DriverCam, WideRoadCam};
|
||||
const int MAX_CAMERAS = std::size(ALL_CAMERAS);
|
||||
@@ -55,13 +53,13 @@ class LogReader {
|
||||
public:
|
||||
LogReader(size_t memory_pool_block_size = DEFAULT_EVENT_MEMORY_POOL_BLOCK_SIZE);
|
||||
~LogReader();
|
||||
bool load(const std::string &url, std::atomic<bool> *abort = nullptr, const std::set<cereal::Event::Which> &allow = {},
|
||||
bool load(const std::string &url, std::atomic<bool> *abort = nullptr,
|
||||
bool local_cache = false, int chunk_size = -1, int retries = 0);
|
||||
bool load(const std::byte *data, size_t size, std::atomic<bool> *abort = nullptr);
|
||||
std::vector<Event*> events;
|
||||
|
||||
private:
|
||||
bool parse(const std::set<cereal::Event::Which> &allow, std::atomic<bool> *abort);
|
||||
bool parse(std::atomic<bool> *abort);
|
||||
std::string raw_;
|
||||
#ifdef HAS_MEMORY_RESOURCE
|
||||
std::unique_ptr<std::pmr::monotonic_buffer_resource> mbr_;
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
@@ -28,7 +27,6 @@ enum REPLAY_FLAGS {
|
||||
REPLAY_FLAG_NO_FILE_CACHE = 0x0020,
|
||||
REPLAY_FLAG_QCAMERA = 0x0040,
|
||||
REPLAY_FLAG_NO_HW_DECODER = 0x0100,
|
||||
REPLAY_FLAG_FULL_SPEED = 0x0200,
|
||||
REPLAY_FLAG_NO_VIPC = 0x0400,
|
||||
REPLAY_FLAG_ALL_SERVICES = 0x0800,
|
||||
};
|
||||
@@ -50,8 +48,8 @@ class Replay : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Replay(QString route, QStringList allow, QStringList block, QStringList base_blacklist, SubMaster *sm = nullptr,
|
||||
uint32_t flags = REPLAY_FLAG_NONE, QString data_dir = "", QObject *parent = 0);
|
||||
Replay(QString route, QStringList allow, QStringList block, SubMaster *sm = nullptr,
|
||||
uint32_t flags = REPLAY_FLAG_NONE, QString data_dir = "", QObject *parent = 0);
|
||||
~Replay();
|
||||
bool load();
|
||||
void start(int seconds = 0);
|
||||
@@ -114,8 +112,6 @@ protected:
|
||||
}
|
||||
|
||||
QThread *stream_thread_ = nullptr;
|
||||
|
||||
// logs
|
||||
std::mutex stream_lock_;
|
||||
std::condition_variable stream_cv_;
|
||||
std::atomic<bool> updating_events_ = false;
|
||||
@@ -142,9 +138,8 @@ protected:
|
||||
std::mutex timeline_lock;
|
||||
QFuture<void> timeline_future;
|
||||
std::vector<std::tuple<double, double, TimelineType>> timeline;
|
||||
std::set<cereal::Event::Which> allow_list;
|
||||
std::string car_fingerprint_;
|
||||
float speed_ = 1.0;
|
||||
std::atomic<float> speed_ = 1.0;
|
||||
replayEventFilter event_filter = nullptr;
|
||||
void *filter_opaque = nullptr;
|
||||
int segment_cache_limit = MIN_SEGMENTS_CACHE;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
#include <QDateTime>
|
||||
@@ -55,7 +54,7 @@ class Segment : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Segment(int n, const SegmentFile &files, uint32_t flags, const std::set<cereal::Event::Which> &allow = {});
|
||||
Segment(int n, const SegmentFile &files, uint32_t flags);
|
||||
~Segment();
|
||||
inline bool isLoaded() const { return !loading_ && !abort_; }
|
||||
|
||||
@@ -73,5 +72,4 @@ protected:
|
||||
std::atomic<int> loading_ = 0;
|
||||
QFutureSynchronizer<void> synchronizer_;
|
||||
uint32_t flags;
|
||||
std::set<cereal::Event::Which> allow;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user