#!/usr/bin/env python
import os
# check if NEOS update is required
while 1 :
if ( not os . path . isfile ( " /VERSION " ) or int ( open ( " /VERSION " ) . read ( ) ) < 3 ) and not os . path . isfile ( " /sdcard/noupdate " ) :
os . system ( " curl -o /tmp/updater https://openpilot.comma.ai/updater && chmod +x /tmp/updater && /tmp/updater " )
else :
break
import sys
import time
import importlib
import subprocess
import signal
import traceback
import usb1
from multiprocessing import Process
from selfdrive . services import service_list
import hashlib
import zmq
from setproctitle import setproctitle
from selfdrive . swaglog import cloudlog
import selfdrive . messaging as messaging
from selfdrive . thermal import read_thermal
from selfdrive . registration import register
from selfdrive . version import version
import common . crash as crash
from common . params import Params
from selfdrive . loggerd . config import ROOT
# comment out anything you don't want to run
managed_processes = {
" uploader " : " selfdrive.loggerd.uploader " ,
" controlsd " : " selfdrive.controls.controlsd " ,
" plannerd " : " selfdrive.controls.plannerd " ,
" radard " : " selfdrive.controls.radard " ,
" loggerd " : ( " loggerd " , [ " ./loggerd " ] ) ,
" logmessaged " : " selfdrive.logmessaged " ,
" tombstoned " : " selfdrive.tombstoned " ,
" logcatd " : ( " logcatd " , [ " ./logcatd " ] ) ,
" proclogd " : ( " proclogd " , [ " ./proclogd " ] ) ,
" boardd " : ( " boardd " , [ " ./boardd " ] ) , # switch to c++ boardd
" ui " : ( " ui " , [ " ./ui " ] ) ,
" visiond " : ( " visiond " , [ " ./visiond " ] ) ,
" sensord " : ( " sensord " , [ " ./sensord " ] ) , }
running = { }
# due to qualcomm kernel bugs SIGKILLing visiond sometimes causes page table corruption
unkillable_processes = [ ' visiond ' ]
# processes to end with SIGINT instead of SIGTERM
interrupt_processes = [ ]
car_started_processes = [
' controlsd ' ,
' plannerd ' ,
' loggerd ' ,
' sensord ' ,
' radard ' ,
' visiond ' ,
' proclogd ' ,
]
def register_managed_process ( name , desc , car_started = False ) :
global managed_processes , car_started_processes
print " registering " , name
managed_processes [ name ] = desc
if car_started :
car_started_processes . append ( name )
# ****************** process management functions ******************
def launcher ( proc , gctx ) :
try :
# import the process
mod = importlib . import_module ( proc )
# rename the process
setproctitle ( proc )
# exec the process
mod . main ( gctx )
except KeyboardInterrupt :
cloudlog . info ( " child %s got ctrl-c " % proc )
except Exception :
# can't install the crash handler becuase sys.excepthook doesn't play nice
# with threads, so catch it here.
crash . capture_exception ( )
raise
def nativelauncher ( pargs , cwd ) :
# exec the process
os . chdir ( cwd )
# because when extracted from pex zips permissions get lost -_-
os . chmod ( pargs [ 0 ] , 0o700 )
os . execvp ( pargs [ 0 ] , pargs )
def start_managed_process ( name ) :
if name in running or name not in managed_processes :
return
proc = managed_processes [ name ]
if isinstance ( proc , basestring ) :
cloudlog . info ( " starting python %s " % proc )
running [ name ] = Process ( name = name , target = launcher , args = ( proc , gctx ) )
else :
pdir , pargs = proc
cwd = os . path . dirname ( os . path . realpath ( __file__ ) )
if pdir is not None :
cwd = os . path . join ( cwd , pdir )
cloudlog . info ( " starting process %s " % name )
running [ name ] = Process ( name = name , target = nativelauncher , args = ( pargs , cwd ) )
running [ name ] . start ( )
def kill_managed_process ( name ) :
if name not in running or name not in managed_processes :
return
cloudlog . info ( " killing %s " % name )
if running [ name ] . exitcode is None :
if name in interrupt_processes :
os . kill ( running [ name ] . pid , signal . SIGINT )
else :
running [ name ] . terminate ( )
# give it 5 seconds to die
running [ name ] . join ( 5.0 )
if running [ name ] . exitcode is None :
if name in unkillable_processes :
cloudlog . critical ( " unkillable process %s failed to exit! rebooting in 15 if it doesn ' t die " % name )
running [ name ] . join ( 15.0 )
if running [ name ] . exitcode is None :
cloudlog . critical ( " FORCE REBOOTING PHONE! " )
os . system ( " date > /sdcard/unkillable_reboot " )
os . system ( " reboot " )
raise RuntimeError
else :
cloudlog . info ( " killing %s with SIGKILL " % name )
os . kill ( running [ name ] . pid , signal . SIGKILL )
running [ name ] . join ( )
cloudlog . info ( " %s is dead with %d " % ( name , running [ name ] . exitcode ) )
del running [ name ]
def cleanup_all_processes ( signal , frame ) :
cloudlog . info ( " caught ctrl-c %s %s " % ( signal , frame ) )
manage_baseui ( False )
for name in running . keys ( ) :
kill_managed_process ( name )
sys . exit ( 0 )
baseui_running = False
def manage_baseui ( start ) :
global baseui_running
if start and not baseui_running :
os . system ( " am start -n com.baseui/.MainActivity " )
baseui_running = True
elif not start and baseui_running :
os . system ( " am force-stop com.baseui " )
baseui_running = False
# ****************** run loop ******************
def manager_init ( ) :
global gctx
reg_res = register ( )
if reg_res :
dongle_id , dongle_secret = reg_res
else :
raise Exception ( " server registration failed " )
# set dongle id
cloudlog . info ( " dongle id is " + dongle_id )
os . environ [ ' DONGLE_ID ' ] = dongle_id
cloudlog . bind_global ( dongle_id = dongle_id , version = version )
crash . bind_user ( id = dongle_id )
crash . bind_extra ( version = version )
os . system ( " mkdir -p " + ROOT )
# set gctx
gctx = { }
def manager_thread ( ) :
global baseui_running
# now loop
context = zmq . Context ( )
thermal_sock = messaging . pub_sock ( context , service_list [ ' thermal ' ] . port )
health_sock = messaging . sub_sock ( context , service_list [ ' health ' ] . port )
cloudlog . info ( " manager start " )
cloudlog . info ( dict ( os . environ ) )
start_managed_process ( " logmessaged " )
start_managed_process ( " logcatd " )
start_managed_process ( " tombstoned " )
start_managed_process ( " uploader " )
start_managed_process ( " ui " )
manage_baseui ( True )
panda = False
if os . getenv ( " NOBOARD " ) is None :
# *** wait for the board ***
panda = wait_for_device ( ) == 0x2300
# flash the device
if os . getenv ( " NOPROG " ) is None :
# checkout the matching panda repo
rootdir = os . path . dirname ( os . path . abspath ( __file__ ) )
os . system ( " cd %s && git submodule init && git submodule update " % rootdir )
# flash the board
boarddir = os . path . dirname ( os . path . abspath ( __file__ ) ) + " /../panda/board/ "
mkfile = " Makefile " if panda else " Makefile.legacy "
print " using " , mkfile
os . system ( " cd %s && make -f %s " % ( boarddir , mkfile ) )
start_managed_process ( " boardd " )
started = False
logger_dead = False
count = 0
# set 5 second timeout on health socket
# 5x slower than expected
health_sock . RCVTIMEO = 5000
while 1 :
# get health of board, log this in "thermal"
td = messaging . recv_sock ( health_sock , wait = True )
print td
# replace thermald
msg = read_thermal ( )
# loggerd is gated based on free space
statvfs = os . statvfs ( ROOT )
avail = ( statvfs . f_bavail * 1.0 ) / statvfs . f_blocks
# thermal message now also includes free space
msg . thermal . freeSpace = avail
with open ( " /sys/class/power_supply/battery/capacity " ) as f :
msg . thermal . batteryPercent = int ( f . read ( ) )
with open ( " /sys/class/power_supply/battery/status " ) as f :
msg . thermal . batteryStatus = f . read ( ) . strip ( )
thermal_sock . send ( msg . to_bytes ( ) )
print msg
# TODO: add car battery voltage check
max_temp = max ( msg . thermal . cpu0 , msg . thermal . cpu1 ,
msg . thermal . cpu2 , msg . thermal . cpu3 ) / 10.0
# uploader is gated based on the phone temperature
if max_temp > 85.0 :
cloudlog . info ( " over temp: %r " , max_temp )
kill_managed_process ( " uploader " )
elif max_temp < 70.0 :
start_managed_process ( " uploader " )
if avail < 0.05 :
logger_dead = True
# start constellation of processes when the car starts
# with 2% left, we killall, otherwise the phone is bricked
if td is not None and td . health . started and avail > 0.02 :
if not started :
Params ( ) . car_start ( )
started = True
for p in car_started_processes :
if p == " loggerd " and logger_dead :
kill_managed_process ( p )
else :
start_managed_process ( p )
manage_baseui ( False )
else :
manage_baseui ( True )
started = False
logger_dead = False
for p in car_started_processes :
kill_managed_process ( p )
# shutdown if the battery gets lower than 10%, we aren't running, and we are discharging
if msg . thermal . batteryPercent < 5 and msg . thermal . batteryStatus == " Discharging " :
os . system ( ' LD_LIBRARY_PATH= " " svc power shutdown ' )
# check the status of baseui
baseui_running = ' com.baseui ' in subprocess . check_output ( [ " ps " ] )
# check the status of all processes, did any of them die?
for p in running :
cloudlog . debug ( " running %s %s " % ( p , running [ p ] ) )
# report to server once per minute
if ( count % 60 ) == 0 :
cloudlog . event ( " STATUS_PACKET " ,
running = running . keys ( ) ,
count = count ,
health = ( td . to_dict ( ) if td else None ) ,
thermal = msg . to_dict ( ) )
count + = 1
def get_installed_apks ( ) :
dat = subprocess . check_output ( [ " pm " , " list " , " packages " , " -3 " , " -f " ] ) . strip ( ) . split ( " \n " )
ret = { }
for x in dat :
if x . startswith ( " package: " ) :
v , k = x . split ( " package: " ) [ 1 ] . split ( " = " )
ret [ k ] = v
return ret
# optional, build the c++ binaries and preimport the python for speed
def manager_prepare ( ) :
# build cereal first
subprocess . check_call ( [ " make " , " -j4 " ] , cwd = " ../cereal " )
# build all processes
os . chdir ( os . path . dirname ( os . path . abspath ( __file__ ) ) )
for p in managed_processes :
proc = managed_processes [ p ]
if isinstance ( proc , basestring ) :
# import this python
cloudlog . info ( " preimporting %s " % proc )
importlib . import_module ( proc )
else :
# build this process
cloudlog . info ( " building %s " % ( proc , ) )
try :
subprocess . check_call ( [ " make " , " -j4 " ] , cwd = proc [ 0 ] )
except subprocess . CalledProcessError :
# make clean if the build failed
cloudlog . info ( " building %s failed, make clean " % ( proc , ) )
subprocess . check_call ( [ " make " , " clean " ] , cwd = proc [ 0 ] )
subprocess . check_call ( [ " make " , " -j4 " ] , cwd = proc [ 0 ] )
# install apks
installed = get_installed_apks ( )
for app in os . listdir ( " ../apk/ " ) :
if " .apk " in app :
app = app . split ( " .apk " ) [ 0 ]
if app not in installed :
installed [ app ] = None
cloudlog . info ( " installed apks %s " % ( str ( installed ) , ) )
for app in installed :
apk_path = " ../apk/ " + app + " .apk "
if os . path . isfile ( apk_path ) :
h1 = hashlib . sha1 ( open ( apk_path ) . read ( ) ) . hexdigest ( )
h2 = None
if installed [ app ] is not None :
h2 = hashlib . sha1 ( open ( installed [ app ] ) . read ( ) ) . hexdigest ( )
cloudlog . info ( " comparing version of %s %s vs %s " % ( app , h1 , h2 ) )
if h2 is None or h1 != h2 :
cloudlog . info ( " installing %s " % app )
for do_uninstall in [ False , True ] :
if do_uninstall :
cloudlog . info ( " needing to uninstall %s " % app )
os . system ( " pm uninstall %s " % app )
ret = os . system ( " cp %s /sdcard/ %s .apk && pm install -r /sdcard/ %s .apk && rm /sdcard/ %s .apk " % ( apk_path , app , app , app ) )
if ret == 0 :
break
assert ret == 0
def wait_for_device ( ) :
while 1 :
try :
context = usb1 . USBContext ( )
for device in context . getDeviceList ( skip_on_error = True ) :
if ( device . getVendorID ( ) == 0xbbaa and device . getProductID ( ) == 0xddcc ) or \
( device . getVendorID ( ) == 0x0483 and device . getProductID ( ) == 0xdf11 ) :
bcd = device . getbcdDevice ( )
handle = device . open ( )
handle . claimInterface ( 0 )
cloudlog . info ( " found board " )
handle . close ( )
return bcd
except Exception as e :
print " exception " , e ,
print " waiting... "
time . sleep ( 1 )
def main ( ) :
if os . getenv ( " NOLOG " ) is not None :
del managed_processes [ ' loggerd ' ]
del managed_processes [ ' tombstoned ' ]
if os . getenv ( " NOUPLOAD " ) is not None :
del managed_processes [ ' uploader ' ]
if os . getenv ( " NOVISION " ) is not None :
del managed_processes [ ' visiond ' ]
if os . getenv ( " NOBOARD " ) is not None :
del managed_processes [ ' boardd ' ]
if os . getenv ( " LEAN " ) is not None :
del managed_processes [ ' uploader ' ]
del managed_processes [ ' loggerd ' ]
del managed_processes [ ' logmessaged ' ]
del managed_processes [ ' logcatd ' ]
del managed_processes [ ' tombstoned ' ]
del managed_processes [ ' proclogd ' ]
if os . getenv ( " NOCONTROL " ) is not None :
del managed_processes [ ' controlsd ' ]
del managed_processes [ ' radard ' ]
# support additional internal only extensions
try :
import selfdrive . manager_extensions
selfdrive . manager_extensions . register ( register_managed_process )
except ImportError :
pass
params = Params ( )
params . manager_start ( )
manager_init ( )
manager_prepare ( )
if os . getenv ( " PREPAREONLY " ) is not None :
sys . exit ( 0 )
try :
manager_thread ( )
except Exception :
traceback . print_exc ( )
crash . capture_exception ( )
finally :
cleanup_all_processes ( None , None )
if __name__ == " __main__ " :
main ( )