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.
		
		
		
		
		
			
		
			
				
					
					
						
							210 lines
						
					
					
						
							6.5 KiB
						
					
					
				
			
		
		
	
	
							210 lines
						
					
					
						
							6.5 KiB
						
					
					
				| import asyncio
 | |
| import json
 | |
| import logging
 | |
| import os
 | |
| import ssl
 | |
| import uuid
 | |
| import time
 | |
| 
 | |
| # aiortc and its dependencies have lots of internal warnings :(
 | |
| import warnings
 | |
| warnings.resetwarnings()
 | |
| warnings.simplefilter("always")
 | |
| 
 | |
| 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 typing import Optional
 | |
| 
 | |
| logger = logging.getLogger("pc")
 | |
| logging.basicConfig(level=logging.INFO)
 | |
| 
 | |
| pcs: set[RTCPeerConnection] = set()
 | |
| pm: Optional[messaging.PubMaster] = None
 | |
| sm: Optional[messaging.SubMaster] = 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()
 | |
| 
 |