You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							123 lines
						
					
					
						
							3.8 KiB
						
					
					
				
			
		
		
	
	
							123 lines
						
					
					
						
							3.8 KiB
						
					
					
				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
 | 
						|
 | 
						|
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():
 | 
						|
  # App needs to be HTTPS for microphone and audio autoplay to work on the browser
 | 
						|
  ssl_context = create_ssl_context()
 | 
						|
 | 
						|
  app = web.Application()
 | 
						|
  app['mutable_vals'] = {}
 | 
						|
  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()
 | 
						|
 |