├── _config.yml ├── log └── .gitignore ├── .gitignore ├── default-bashrcs ├── README.md └── .bashrc-community ├── aliases.sh ├── .editorconfig ├── powerline.sh ├── .config └── powerline-shell │ └── config.json ├── commands ├── update │ └── __init__.py ├── uninstall │ └── __init__.py ├── __init__.py ├── panda │ └── __init__.py ├── README.md ├── debug │ └── __init__.py ├── device │ └── __init__.py ├── base.py └── fork │ └── __init__.py ├── uninstall.sh ├── CONTRIBUTING.md ├── py_utils ├── colors.py └── emu_utils.py ├── emu.sh ├── emu.py ├── update.sh ├── check-for-updates.sh ├── README.md ├── CHANGELOG.md ├── install.sh └── LICENSE /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /log/.gitignore: -------------------------------------------------------------------------------- 1 | **/** 2 | ** 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #IDE Related 2 | .idea/ 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | -------------------------------------------------------------------------------- /default-bashrcs/README.md: -------------------------------------------------------------------------------- 1 | A source to /data/community/.bashrc is made in the system .bashrc file, depending on the system (AGNOS or NEOS). 2 | 3 | To edit your own bashrc after installation: 4 | ``` 5 | cd /data/community/ 6 | ls -al 7 | vi .bashrc 8 | ``` 9 | -------------------------------------------------------------------------------- /aliases.sh: -------------------------------------------------------------------------------- 1 | alias ll="ls -lAh" 2 | alias pf="emu panda flash" 3 | alias controlsd="emu debug controlsd" 4 | alias reload="emu debug reload" 5 | alias battery="emu device battery" 6 | alias shutdown="emu device shutdown" 7 | alias settings="emu device settings" 8 | alias fork="emu fork switch" 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | 5 | # Matches multiple files with brace expansion notation 6 | # Set default charset 7 | [*.{js,py}] 8 | charset = utf-8 9 | 10 | [*.{py,sh,bashrc-community,bashrc-system}] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /powerline.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ -x "$(command -v powerline-shell)" ]; then 3 | function _update_ps1() { 4 | PS1=$(powerline-shell $?) 5 | } 6 | 7 | if [[ $TERM != linux && ! $PROMPT_COMMAND =~ _update_ps1 ]]; then 8 | PROMPT_COMMAND="_update_ps1; $PROMPT_COMMAND" 9 | fi 10 | fi 11 | -------------------------------------------------------------------------------- /.config/powerline-shell/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "segments": [ 3 | "virtual_env", 4 | "cwd", 5 | "git", 6 | "git_stash", 7 | "set_term_title", 8 | "root" 9 | ], 10 | "mode": "patched", 11 | "cwd": { 12 | "mode": "flat", 13 | "max_depth": 4 14 | }, 15 | "theme": "default" 16 | } 17 | -------------------------------------------------------------------------------- /commands/update/__init__.py: -------------------------------------------------------------------------------- 1 | from commands.base import CommandBase 2 | from py_utils.emu_utils import run, error 3 | from py_utils.emu_utils import UPDATE_PATH 4 | 5 | 6 | class Update(CommandBase): 7 | def __init__(self): 8 | super().__init__() 9 | self.name = 'update' 10 | self.description = '🎉 Updates this tool' 11 | 12 | @staticmethod 13 | def _update(): 14 | if not run(['sh', UPDATE_PATH]): 15 | error('Error updating!') 16 | -------------------------------------------------------------------------------- /commands/uninstall/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from commands.base import CommandBase 4 | from py_utils.emu_utils import run, error, input_with_options, UNINSTALL_PATH 5 | 6 | 7 | class Uninstall(CommandBase): 8 | def __init__(self): 9 | super().__init__() 10 | self.name = 'uninstall' 11 | self.description = '👋 Uninstalls emu' 12 | 13 | @staticmethod 14 | def _uninstall(): 15 | print('Are you sure you want to uninstall emu?') 16 | if input_with_options(['Y', 'n'], 'n')[0] == 0: 17 | run(['sh', UNINSTALL_PATH]) 18 | else: 19 | error('Not uninstalling!') 20 | -------------------------------------------------------------------------------- /commands/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import importlib 5 | from py_utils.emu_utils import error 6 | 7 | EMU_COMMANDS = [] 8 | basedir = os.path.dirname(__file__) 9 | for module_name in os.listdir(basedir): 10 | if module_name.endswith('.py') or module_name == '__pycache__' or not os.path.isdir(os.path.join(basedir, module_name)): 11 | continue 12 | try: 13 | module = importlib.import_module('commands.{}'.format(module_name)) 14 | module = getattr(module, module_name.title()) 15 | EMU_COMMANDS.append(module()) 16 | except Exception as e: 17 | error('Error loading {} command, please try updating!'.format(module_name)) 18 | error(e) 19 | -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | COMMUNITY_BASHRC_PATH=/data/community/.bashrc 4 | OH_MY_COMMA_PATH=/data/community/.oh-my-comma 5 | 6 | echo "Deleting $COMMUNITY_BASHRC_PATH" 7 | rm $COMMUNITY_BASHRC_PATH 8 | 9 | # only applicable on NEOS for now 10 | if [ -f /EON ] && [ -x "$(command -v powerline-shell)" ]; then 11 | echo "It's recommended to uninstall powerline if you uninstall emu. Uninstall powerline?" 12 | read -p "[Y/n] > " choices 13 | case ${choices} in 14 | n|N ) echo "Skipping...";; 15 | * ) mount -o rw,remount /system && pip uninstall powerline-shell && mount -o r,remount /system;; 16 | esac 17 | fi 18 | 19 | echo "Deleting $OH_MY_COMMA_PATH" 20 | rm -rf $OH_MY_COMMA_PATH 21 | 22 | echo "Uninstalled!" 23 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | See First: 2 | https://github.com/AskAlice/.oh-my-comma/projects 3 | https://github.com/AskAlice/.oh-my-comma/issues 4 | 5 | # Contributing 6 | This is a project that will benefit greatly from mass-collaberation, and is somewhat unique in that most users of comma.ai are tech-saavy enough to make a pull request. 7 | Please try to keep your contributions to the core of this repository generic for neos/openpilot, but plugins for .oh-my-comma can be tailored for specific forks if you'd like. (could be cool to see a kegman configuration tool, for example) 8 | Check out the projects and issues on this repo! Feel free to contribute your own ideas as issues, and tag them with labels and projects. 9 | 10 | ## Pull Requests 11 | Ensure that it works locally if possible. I will validate PRs, and make changes to your PRs in the 'devel' branch, if need be. 12 | I will squash and merge all PRs to keep the git log down. 13 | 14 | 15 | -------------------------------------------------------------------------------- /commands/panda/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | import importlib 4 | from commands.base import CommandBase, Command 5 | from py_utils.emu_utils import run, error 6 | from py_utils.emu_utils import OPENPILOT_PATH 7 | 8 | 9 | class Panda(CommandBase): 10 | def __init__(self): 11 | super().__init__() 12 | self.name = 'panda' 13 | self.description = '🐼 panda interfacing tools' 14 | 15 | self.commands = {'flash': Command(description='🐼 flashes panda with make recover (usually works with the C2)'), 16 | 'flash2': Command(description='🎍 flashes panda using Panda module (usually works with the EON)')} 17 | 18 | @staticmethod 19 | def _flash(): 20 | r = run('make -C {}/panda/board recover'.format(OPENPILOT_PATH)) 21 | if not r: 22 | error('Error running make command!') 23 | 24 | @staticmethod 25 | def _flash2(): 26 | if not run('pkill -f boardd'): 27 | error('Error killing boardd! Is it running? (continuing...)') 28 | importlib.import_module('panda', 'Panda').Panda().flash() 29 | -------------------------------------------------------------------------------- /py_utils/colors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | class COLORS: 4 | def __init__(self): 5 | self.HEADER = '\033[95m' 6 | self.OKBLUE = '\033[94m' 7 | self.CBLUE = '\33[44m' 8 | self.BOLD = '\033[1m' 9 | self.CITALIC = '\33[3m' 10 | self.OKGREEN = '\033[92m' 11 | self.CWHITE = '\33[37m' 12 | self.ENDC = '\033[0m' + self.CWHITE 13 | self.UNDERLINE = '\033[4m' 14 | self.PINK = '\33[38;5;207m' 15 | self.PRETTY_YELLOW = self.BASE(220) 16 | 17 | self.RED = '\033[91m' 18 | self.PURPLE_BG = '\33[45m' 19 | self.YELLOW = '\033[93m' 20 | self.BLUE_GREEN = self.BASE(85) 21 | 22 | self.FAIL = self.RED 23 | self.INFO = self.PURPLE_BG 24 | self.SUCCESS = self.OKGREEN 25 | self.PROMPT = self.YELLOW 26 | self.DBLUE = '\033[36m' 27 | self.CYAN = self.BASE(39) 28 | self.WARNING = '\033[33m' 29 | 30 | def BASE(self, col): # seems to support more colors 31 | return '\33[38;5;{}m'.format(col) 32 | 33 | def BASEBG(self, col): # seems to support more colors 34 | return '\33[48;5;{}m'.format(col) 35 | 36 | 37 | COLORS = COLORS() 38 | -------------------------------------------------------------------------------- /emu.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export COMMUNITY_PATH=/data/community 3 | export COMMUNITY_BASHRC_PATH=${COMMUNITY_PATH}/.bashrc 4 | export OH_MY_COMMA_PATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 5 | 6 | source ${OH_MY_COMMA_PATH}/powerline.sh 7 | source ${OH_MY_COMMA_PATH}/aliases.sh 8 | 9 | function _updateohmycomma(){ # good to keep a backup in case python CLI is broken 10 | source ${OH_MY_COMMA_PATH}/update.sh 11 | source ${OH_MY_COMMA_PATH}/emu.sh 12 | } 13 | 14 | function emu(){ # main wrapper function 15 | if $(python -c 'import sys; print(".".join(map(str, sys.version_info[:3])))' | grep -q -e '^2') 16 | then 17 | python3 ${OH_MY_COMMA_PATH}/emu.py "$@" 18 | else 19 | python ${OH_MY_COMMA_PATH}/emu.py "$@" 20 | fi 21 | 22 | if [ $? = 1 ] && [ "$1" = "update" ]; then # fallback to updating immediately if CLI crashed updating 23 | printf "\033[91mAn error occurred in the Python CLI, attempting to manually update .oh-my-comma...\n" 24 | printf "Press Ctrl+C to cancel!\033[0m\n" 25 | sleep 5 26 | _updateohmycomma 27 | fi 28 | } 29 | 30 | source ${OH_MY_COMMA_PATH}/check-for-updates.sh 31 | -------------------------------------------------------------------------------- /emu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | import sys 4 | if __package__ is None: 5 | import sys 6 | from os import path 7 | sys.path.append(path.abspath(path.join(path.dirname(__file__), 'py_utils'))) 8 | sys.path.append(path.abspath(path.join(path.dirname(__file__), 'commands'))) 9 | 10 | from py_utils.emu_utils import BaseFunctions 11 | from py_utils.emu_utils import OPENPILOT_PATH 12 | from commands import EMU_COMMANDS 13 | 14 | sys.path.append(OPENPILOT_PATH) # for importlib 15 | DEBUG = not path.exists('/data/params/d') 16 | 17 | 18 | class Emu(BaseFunctions): 19 | def __init__(self, args): 20 | self.name = 'emu' 21 | self.args = args 22 | self.commands = {cmd.name: cmd for cmd in EMU_COMMANDS} 23 | self.parse() 24 | 25 | def parse(self): 26 | cmd = self.next_arg() 27 | if cmd is None: 28 | self.print_commands(error_msg='You must specify a command for emu. Some options are:', ascii_art=True) 29 | return 30 | if cmd not in self.commands: 31 | self.print_commands(error_msg='Unknown command! Try one of these:') 32 | return 33 | self.commands[cmd].main(self.args, cmd) 34 | 35 | 36 | if __name__ == "__main__": 37 | args = sys.argv[1:] 38 | if DEBUG: 39 | args = input().split(' ') 40 | if '' in args: 41 | args.remove('') 42 | emu = Emu(args) 43 | -------------------------------------------------------------------------------- /default-bashrcs/.bashrc-community: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## BEGIN .oh-my-comma magic ### 4 | 5 | # If not running interactively, don't do anything 6 | case $- in 7 | *i*) ;; 8 | *) return;; 9 | esac 10 | 11 | # enable color support of ls and also add handy aliases 12 | if [ -x /usr/bin/dircolors ]; then 13 | test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)" 14 | alias ls='ls --color=auto' 15 | #alias dir='dir --color=auto' 16 | #alias vdir='vdir --color=auto' 17 | 18 | alias grep='grep --color=auto' 19 | alias fgrep='fgrep --color=auto' 20 | alias egrep='egrep --color=auto' 21 | fi 22 | 23 | # colored GCC warnings and errors 24 | export GCC_COLORS='error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01' 25 | 26 | source /data/community/.oh-my-comma/emu.sh 27 | 28 | # Change active histfile to a writable directory outside of the /system partition 29 | HISTFILE="/data/community/.bash_history" 30 | 31 | # don't put duplicate lines or lines starting with space in the history. 32 | # See bash(1) for more options 33 | HISTCONTROL=ignoreboth 34 | 35 | # append to the history file, don't overwrite it 36 | shopt -s histappend 37 | 38 | ### End of .oh-my-comma magic ### 39 | 40 | # This is your space to configure your terminal to your liking 41 | 42 | # Change default working directory 43 | # Just in case openpilot is missing, default to /data 44 | cd /data 45 | if [ -d "/data/openpilot" ]; then 46 | cd /data/openpilot 47 | fi 48 | -------------------------------------------------------------------------------- /update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | OH_MY_COMMA_PATH=/data/community/.oh-my-comma 3 | PREV_BRANCH=$(cd ${OH_MY_COMMA_PATH} && git rev-parse --abbrev-ref HEAD) 4 | PREV_VERSION=$(cd ${OH_MY_COMMA_PATH} && git describe --tags | grep -Po "^v(\d+\.)?(\d+\.)?(\*|\d+)") 5 | PREV_REMOTE=$(cd ${OH_MY_COMMA_PATH} && git config --get remote.origin.url) 6 | git -C ${OH_MY_COMMA_PATH} pull 7 | 8 | sh ${OH_MY_COMMA_PATH}/install.sh 'update' 9 | 10 | CURRENT_BRANCH=$(cd ${OH_MY_COMMA_PATH} && git rev-parse --abbrev-ref HEAD) 11 | CURRENT_VERSION=$(cd ${OH_MY_COMMA_PATH} && git describe --tags | grep -Po "^v(\d+\.)?(\d+\.)?(\*|\d+)") 12 | CURRENT_REMOTE=$(cd ${OH_MY_COMMA_PATH} && git config --get remote.origin.url) 13 | 14 | echo "Updated ${OH_MY_COMMA_PATH}" 15 | echo "====== FROM ======" 16 | printf "Version \033[92m${PREV_VERSION}\033[0m | branch \033[92m${PREV_BRANCH}\033[0m | remote \033[92m${PREV_REMOTE}\033[0m\n" 17 | printf "Version \033[92m${CURRENT_VERSION}\033[0m | branch \033[92m${CURRENT_BRANCH}\033[0m | remote \033[92m${CURRENT_REMOTE}\033[0m\n" 18 | echo "======= TO =======" 19 | 20 | if git -C $OH_MY_COMMA_PATH log --stat -1 | grep -q 'default-bashrcs/.bashrc-community'; then 21 | if [ "${PREV_VERSION}" != "${CURRENT_VERSION}" ]; then 22 | printf "\n\33[38;5;190mThe default .bashrc has been updated!\033[0m The update has not been applied to retain your custom changes.\nTo update and reset your .bashrc, run the command:\n" 23 | printf "\033[92mcp -fr ${OH_MY_COMMA_PATH}/default-bashrcs/.bashrc-community /data/community/.bashrc\033[0m\n\n" 24 | printf "This will wipe any custom changes you've made!\n" 25 | fi 26 | fi 27 | -------------------------------------------------------------------------------- /commands/README.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | 3 | #### `emu fork`: 4 | 🍴 Manage installed forks, or install a new one 5 | - `emu fork switch`: 🍴 Switch between any openpilot fork 6 | - Arguments 💢: 7 | - username (optional): 👤 The username of the fork's owner to switch to, will use current fork if not provided 8 | - -b, --branch (optional): 🌿 Branch to switch to, will use fork's default branch if not provided 9 | - *New Behavior:* If a branch is provided with `-b` and username is not supplied, it will use the current fork switched to 10 | - Example 📚: 11 | - `emu fork switch stock devel` 12 | - `emu fork list`: 📜 See a list of installed forks and branches 13 | - Arguments 💢: 14 | - fork (optional): 🌿 See branches of specified fork 15 | - Example 📚: 16 | - `emu fork list stock` 17 | 18 | #### `emu panda`: 19 | 🐼 panda interfacing tools 20 | - `emu panda flash`: 🐼 flashes panda with make recover (usually works with the C2) 21 | - `emu panda flash2`: 🎍 flashes panda using Panda module (usually works with the EON) 22 | 23 | #### `emu debug`: 24 | de-🐛-ing tools 25 | - `emu debug controlsd`: logs controlsd to /data/output.log by default 26 | - Arguments 💢: 27 | - -o, --output: Name of file to save log to 28 | - Example 📚: 29 | - `emu debug controlsd /data/controlsd_log` 30 | 31 | #### `emu device`: 32 | 📈 Statistics about your device 33 | - `emu device battery`: 🔋 see information about the state of your battery 34 | - `emu device reboot`: ⚡ safely reboot your device 35 | - `emu device shutdown`: 🔌 safely shutdown your device 36 | - Arguments 💢: 37 | - -r, --reboot: ♻️ An alternate way to reboot your device 38 | - `emu device settings`: ⚙ open the Settings app 39 | - Arguments 💢: 40 | - -c, --close: ❌ Closes the settings application 41 | -------------------------------------------------------------------------------- /commands/debug/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from commands.base import CommandBase, Command, Flag 4 | from py_utils.colors import COLORS 5 | from py_utils.emu_utils import run, kill, warning, check_output, is_affirmative, error, info, success 6 | from py_utils.emu_utils import OPENPILOT_PATH 7 | 8 | 9 | class Debug(CommandBase): 10 | def __init__(self): 11 | super().__init__() 12 | self.name = 'debug' 13 | self.description = 'de-🐛-ing tools' 14 | 15 | self.commands = {'controlsd': Command(description='🔬 logs controlsd to /data/output.log by default', 16 | flags=[Flag(['-o', '--output'], 'Name of file to save log to', dtype='str')]), 17 | 'reload': Command(description='🔄 kills the current openpilot session and restarts it (all without rebooting)')} 18 | 19 | @staticmethod 20 | def _reload(): 21 | info('This will kill the current openpilot tmux session, set up a new one, and relaunch openpilot.') 22 | info('Confirm you would like to continue') 23 | if not is_affirmative(): 24 | error('Aborting!') 25 | return 26 | 27 | r = check_output('tmux kill-session -t comma') 28 | if r.success: 29 | info('Killed the current openpilot session') 30 | else: 31 | warning('Error killing current openpilot session, continuing...') 32 | 33 | # Command below thanks to mlp 34 | r = check_output(['tmux', 'new', '-s', 'comma', '-d', 35 | "echo $$ > /dev/cpuset/app/tasks;" # add pid of current shell to app cpuset 36 | "echo $PPID > /dev/cpuset/app/tasks;" # (our parent, tmux, also gets all the cores) 37 | "/data/openpilot/launch_openpilot.sh"]) 38 | if r.success: 39 | success('Succesfully started a new tmux session for openpilot!') 40 | success('Type {}tmux a{} to attach to it'.format(COLORS.FAIL, COLORS.SUCCESS)) 41 | 42 | def _controlsd(self): 43 | out_file = '/data/output.log' 44 | flags = self.get_flags('controlsd') 45 | if flags.output is not None: 46 | out_file = flags.output 47 | # r = run('pkill -f controlsd') # terminates file for some reason # todo: remove me if not needed 48 | r = kill('selfdrive.controls.controlsd') # seems to work, some process names are weird 49 | if r is None: 50 | warning('controlsd is already dead! (continuing...)') 51 | run('python {}/selfdrive/controls/controlsd.py'.format(OPENPILOT_PATH), out_file=out_file) 52 | -------------------------------------------------------------------------------- /commands/device/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from commands.base import CommandBase, Command, Flag 4 | from py_utils.emu_utils import error, check_output, COLORS, success 5 | 6 | 7 | class Device(CommandBase): 8 | def __init__(self): 9 | super().__init__() 10 | self.name = 'device' 11 | self.description = '📈 Statistics about your device' 12 | 13 | self.commands = {'battery': Command(description='🔋 see information about the state of your battery'), 14 | 'reboot': Command(description='⚡ safely reboot your device'), 15 | 'shutdown': Command(description='🔌 safely shutdown your device', 16 | flags=[Flag(['-r', '--reboot'], 'An alternate way to reboot your device', dtype='bool')]), 17 | 'settings': Command(description='⚙️ open the Settings app', 18 | flags=[Flag(['-c', '--close'], 'Closes the settings application', dtype='bool')])} 19 | 20 | def _settings(self): 21 | flags = self.get_flags('settings') 22 | if flags.close: 23 | check_output('kill $(pgrep com.android.settings)', shell=True) 24 | success('⚙️ Closed settings!') 25 | else: 26 | check_output('am start -a android.settings.SETTINGS') 27 | success('⚙️ Opened settings!') 28 | 29 | def _shutdown(self): 30 | flags = self.get_flags('shutdown') 31 | if flags.reboot: 32 | self._reboot() 33 | return 34 | check_output('am start -n android/com.android.internal.app.ShutdownActivity') 35 | success('🌙 Goodnight!') 36 | 37 | @staticmethod 38 | def _reboot(): 39 | check_output('am start -a android.intent.action.REBOOT') 40 | success('👋 See you in a bit!') 41 | 42 | @staticmethod 43 | def _battery(): 44 | r = check_output('dumpsys batterymanager') 45 | if not r: 46 | error('Unable to get battery status!') 47 | return 48 | r = r.output.split('\n') 49 | r = [i.strip() for i in r if i != ''][1:] 50 | battery_idxs = {'level': 7, 'temperature': 10} 51 | success('Battery info:') 52 | for name in battery_idxs: 53 | idx = battery_idxs[name] 54 | info = r[idx] 55 | 56 | value = float(info.split(': ')[1]) 57 | if name == 'temperature': 58 | value /= 10 59 | value = str(value) + '°C' 60 | else: 61 | value = str(value) + '%' 62 | 63 | value = COLORS.SUCCESS + str(value) 64 | name = COLORS.WARNING + name.title() 65 | print('- {}: {}{}'.format(name, value, COLORS.ENDC)) 66 | -------------------------------------------------------------------------------- /check-for-updates.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #Based on https://github.com/ohmyzsh/ohmyzsh/blob/master/tools/check_for_upgrade.sh 4 | if [[ "$OMC_DISABLE_AUTO_UPDATE" = true ]] \ 5 | || ! command -v git &>/dev/null; then 6 | return 7 | fi 8 | OMC_EPOCH=$(date +%s) 9 | set +x 10 | function current_epoch() { 11 | echo $(($OMC_EPOCH/60/60/24)) 12 | } 13 | 14 | function update_last_updated_file() { 15 | echo "LAST_EPOCH=$(current_epoch)" > "${OH_MY_COMMA_PATH}/log/.omc-update" 16 | } 17 | 18 | function omc_delete_update_lock() { 19 | rm -rf '$OH_MY_COMMA_PATH/log/update.lock' || return 1 20 | } 21 | # Remove lock directory if older than a day 22 | if mtime=$(date +%s -r "$OH_MY_COMMA_PATH/log/update.lock" 2>/dev/null); then 23 | if (( (mtime + 3600 * 24) < OMC_EPOCH )); then 24 | command rm -rf "$OH_MY_COMMA_PATH/log/update.lock" 25 | fi 26 | fi 27 | 28 | # Check for lock directory 29 | if ! command mkdir "$OH_MY_COMMA_PATH/log/update.lock" 2>/dev/null; then 30 | return 31 | fi 32 | 33 | # Remove lock directory on exit. `return 1` is important for when trapping a SIGINT: 34 | # The return status from the function is handled specially. If it is zero, the signal is 35 | # assumed to have been handled, and execution continues normally. Otherwise, the shell 36 | # will behave as interrupted except that the return status of the trap is retained. 37 | omc_delete_update_lock 38 | 39 | 40 | # Create or update .omc-update file if missing or malformed 41 | if ! source "${OH_MY_COMMA_PATH}/log/.omc-update" 2>/dev/null || [[ -z "$LAST_EPOCH" ]]; then 42 | touch ${OH_MY_COMMA_PATH}/log/.omc-update 43 | update_last_updated_file 44 | fi 45 | 46 | # Number of days before trying to update again 47 | epoch_target=${OMC_AUTOUPDATE_DAYS:-7} 48 | # Test if enough time has passed until the next update 49 | if (( ( $(current_epoch) - LAST_EPOCH ) < $epoch_target )); then 50 | return 51 | fi 52 | 53 | cd ${OH_MY_COMMA_PATH} 54 | 55 | git fetch 56 | OMC_UPSTREAM=${1:-'@{u}'} 57 | OMC_LOCAL=$(git rev-parse @) 58 | OMC_REMOTE=$(git rev-parse "$OMC_UPSTREAM") 59 | 60 | if [ $OMC_LOCAL != $OMC_REMOTE ]; then 61 | # Ask for confirmation before updating unless disabled 62 | if [[ "$OMC_DISABLE_UPDATE_PROMPT" = true ]]; then 63 | emu update 64 | else 65 | echo "[emu.sh] Current OMC branch:" 66 | echo "$(git branch | head -n 1)" 67 | echo "$(git status | head -n 2 | tail -n 1)" 68 | # input sink to swallow all characters typed before the prompt 69 | # and add a newline if there wasn't one after characters typed 70 | read -r -p "[emu.sh] Update .oh-my-comma? [Y/n] " option 71 | [[ "$option" != $'\n' ]] && echo 72 | case "$option" in 73 | [yY$'\n']) emu update && update_last_updated_file ;; 74 | [nN]) update_last_updated_file ;; 75 | *) emu update ;; 76 | esac 77 | fi 78 | fi 79 | cd - 80 | unset -f current_epoch update_last_updated_file 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## comma.ai command-line additions and practical tooling for all 2 | 3 | improving the dev workflow friction is paramount to innovating [openpilot](https://github.com/commaai/openpilot) 4 | 5 | ***PRs accepted!** What cool shit do you do to your ssh session with your car??* 6 | 7 | This tool was created by [Alice Knag](https://github.com/AskAlice) - `@emu#6969` on Discord, and is widely contributed to by [ShaneSmiskol](https://github.com/sshane) - `Shane#6175` 8 | If you have any questions about the development process, or have any ideas you want to see happen, check out [CONTRIBUTING.md](CONTRIBUTING.md) and/or DM one of us on Discord or ask in #custom-forks 9 |

10 | 11 | 12 | click for full size 13 |
Click for Full Size
14 |

15 | 16 | # Getting Started 17 | 18 | To install these utilities, SSH into your comma device running neos (ie Comma 2, Eon, etc), and paste in the following: 19 | ```bash 20 | bash <(curl -fsSL install.emu.sh) # the brain of the bird 21 | source /data/community/.bashrc 22 | ``` 23 | 24 | 25 | 26 | --- 27 | Read the README for . You can optionally [install the fonts on the computer/terminal emulator that you SSH from](https://github.com/powerline/fonts) 28 | 29 | Alternately, you can install [Nerd Fonts](https://github.com/ryanoasis/nerd-fonts), as it provides more icons than powerline fonts, and is more maintained. 30 | 31 | Once NEOS 15 comes out, zsh will be used and [powerlevel10k](https://github.com/romkatv/powerlevel10k) will be the optimal powerline 32 | 33 | The default directory of your bash/ssh session is now `/data/openpilot`. Much easier to git pull after shelling in. 34 | 35 | # welcome to the family 36 | 37 | 38 | 39 | Emu my neo! 40 | You should now be able to use the `emu` command. 41 | 42 | # Updating 43 | 44 | Once you've installed, you can update via the utility 45 | 46 | ```bash 47 | emu update 48 | ``` 49 | 50 | This will essentially perform a git pull and replace all current files in the `/data/community/.oh-my-comma` directory with new ones, if an update is available, as well as check the integrity of the files that must remain elsewhere on the filesystem such as the .bashrc and powerline configs 51 | 52 | # Commands 53 | 54 | ### General 55 | - `emu update`: 🎉 Updates this tool, recommended to restart ssh session 56 | - `emu uninstall`: 👋 Uninstalls emu 57 | ### [Forks](#fork-management) 58 | - `emu fork`: 🍴 Manage installed forks, or install a new one 59 | - `emu fork switch`: 🍴 Switch between any openpilot fork 60 | - `emu fork list`: 📜 See a list of installed forks and branches 61 | ### Panda 62 | - `emu panda`: 🐼 panda interfacing tools 63 | - `emu panda flash`: 🐼 flashes panda with make recover (usually works with the C2) 64 | - `emu panda flash2`: 🎍 flashes panda using Panda module (usually works with the EON) 65 | ### Debugging 66 | - `emu debug`: de-🐛-ing tools 67 | - `emu debug controlsd`: 🔬 logs controlsd to /data/output.log by default 68 | - `emu device`: 📈 Statistics about your device 69 | - `emu device battery`: 🔋 see information about the state of your battery 70 | - `emu device reboot`: ⚡ safely reboot your device 71 | - `emu device shutdown`: 🔌 safely shutdown your device 72 | - `emu device settings`: ⚙ open the Settings app 73 | 74 | To see more information about each command and its arguments, checkout the full [command documentation here.](/commands/README.md) 75 | 76 | --- 77 | 78 | # Fork management 79 | When you first run any `emu fork` command, `emu` will ask you to perform a one-time setup of cloning the base repository of openpilot from commaai. This may take a while, but upon finishing the setup you will be able to switch to any openpilot fork much quicker than the time it usually takes to full-clone a new fork the old fashioned way. 80 | 81 | For each new fork you install with the `emu fork switch` command, Git is able to re-use blobs already downloaded from commaai/openpilot and other similar installed forks, enabling quicker install times. For a shorter command, `fork` is automatically aliased to `emu fork switch`. 82 | 83 | # Git config 84 | 85 | In a rw filesystem, you can edit your git config so you can push your changes up easily. 86 | 87 | ```bash 88 | mount -o rw,remount /system 89 | git config --global user.name "your_username" 90 | git config --global user.email "your_email_address@example.com" 91 | git config --global credential.helper store 92 | git pull 93 | mount -o r,remount /system 94 | ``` 95 | 96 | if the git pull fails, just do some action on git that requires authentication, and you should be good to go 97 | -------------------------------------------------------------------------------- /commands/base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from py_utils.colors import COLORS 4 | from py_utils.emu_utils import ArgumentParser, BaseFunctions, success, error 5 | 6 | 7 | class CommandBase(BaseFunctions): 8 | def __init__(self): 9 | self.name = '' 10 | self.commands = {} 11 | 12 | def main(self, args, cmd_name): 13 | self.args = args 14 | cmd = self.next_arg() 15 | if len(self.commands) > 0: 16 | if cmd is None: 17 | self.print_commands(error_msg='You must specify a command for emu {}. Some options are:'.format(cmd_name)) 18 | return 19 | if cmd not in self.commands: 20 | self.print_commands(error_msg='Unknown command! Try one of these:') 21 | return 22 | self.start_function_from_str(cmd) 23 | else: 24 | self.start_function_from_str(cmd_name) # eg. update and uninstall 25 | 26 | def start_function_from_str(self, cmd): 27 | cmd = '_' + cmd 28 | if not hasattr(self, cmd): 29 | error('Command has not been implemented yet, please try updating.') 30 | return 31 | getattr(self, cmd)() # call command's function 32 | 33 | def get_flags(self, cmd_name): 34 | try: 35 | return self.commands[cmd_name].parser.parse_args(self.args) 36 | except Exception as e: 37 | error(e) 38 | self._help(cmd_name) 39 | exit(1) 40 | 41 | def _help(self, cmd, show_description=True, leading=''): 42 | has_extra_info = False 43 | description = self.commands[cmd].description 44 | if show_description: 45 | print('{}>> Description 📚: {}{}'.format(COLORS.CYAN, description, COLORS.ENDC)) 46 | 47 | flags = self.commands[cmd].flags 48 | 49 | flags_to_print = [] 50 | if flags is not None and len(flags) > 0: 51 | has_extra_info = True 52 | usage_req = [f.aliases[0] for f in flags if f.required and len(f.aliases) == 1] # if required 53 | usage_non_req = [f.aliases[0] for f in flags if not f.required and len(f.aliases) == 1] # if non-required non-positional 54 | usage_flags = [f.aliases for f in flags if not f.required and len(f.aliases) > 1 or f.aliases[0].startswith('-')] # if flag 55 | if len(usage_req) > 0 or len(usage_non_req) > 0: # print usage with proper braces 56 | usage_req = ['[{}]'.format(u) for u in usage_req] 57 | usage_non_req = ['({})'.format(u) for u in usage_non_req] 58 | if len(usage_flags): 59 | # formats flags to: "[-b BRANCH, -o OUTPUT]" 60 | usage_flags = ['{} {}'.format(min(u, key=len), max(u, key=len).upper()[2:]) for u in usage_flags] 61 | usage_flags = ['[{}]'.format(', '.join(usage_flags))] 62 | 63 | usage = ['emu', self.name, cmd] + usage_req + usage_non_req + usage_flags 64 | print(leading + COLORS.WARNING + '>> Usage:{} {}'.format(COLORS.OKGREEN, ' '.join(usage)) + COLORS.ENDC) 65 | 66 | print(leading + COLORS.WARNING + '>> Arguments 💢:' + COLORS.ENDC) 67 | for flag in flags: 68 | aliases = COLORS.SUCCESS + ', '.join(flag.aliases) + COLORS.WARNING 69 | if not flag.required and '-' not in aliases: 70 | aliases += COLORS.RED + ' (optional)' + COLORS.WARNING 71 | flags_to_print.append(leading + COLORS.WARNING + ' - {}: {}'.format(aliases, flag.description) + COLORS.ENDC) 72 | print('\n'.join(flags_to_print)) 73 | 74 | commands = self.commands[cmd].commands 75 | cmds_to_print = [] 76 | if commands is not None and len(commands) > 0: 77 | print(leading + '{}>> Commands 💻:{}'.format(COLORS.OKGREEN, COLORS.ENDC)) 78 | for cmd in commands: 79 | cmds_to_print.append(leading + COLORS.FAIL + ' - {}: {}'.format(cmd, success(commands[cmd].description, ret=True)) + COLORS.ENDC) 80 | print('\n'.join(cmds_to_print)) 81 | return has_extra_info 82 | 83 | 84 | class Flag: 85 | def __init__(self, aliases, description, required=False, dtype='bool'): 86 | if isinstance(aliases, str): 87 | self.aliases = [aliases] 88 | else: 89 | self.aliases = aliases 90 | self.description = description 91 | self.required = required 92 | self.dtype = dtype 93 | if self.required and self.aliases[0][0] == '-': 94 | raise Exception('Positional arguments cannot be required!') 95 | 96 | 97 | class Command: 98 | def __init__(self, description=None, commands=None, flags=None): 99 | self.parser = ArgumentParser() 100 | self.description = description 101 | self.commands = commands 102 | self.has_flags = False 103 | self.flags = flags 104 | if flags is not None: 105 | self.has_flags = True 106 | for flag in flags: 107 | # for each flag, add it as argument with aliases. 108 | parser_args = {} # handle various conditions 109 | if not flag.required and flag.dtype not in ['bool']: 110 | parser_args['nargs'] = '?' 111 | 112 | if flag.dtype != 'bool': 113 | parser_args['action'] = 'store' 114 | elif flag.dtype == 'bool': 115 | parser_args['action'] = 'store_true' 116 | 117 | if flag.dtype == 'bool': # type bool is not required when store_true 118 | pass 119 | elif flag.dtype == 'str': 120 | parser_args['type'] = str 121 | elif flag.dtype == 'int': 122 | parser_args['type'] = int 123 | else: 124 | error('Unsupported dtype: {}'.format(flag.dtype)) 125 | return 126 | self.parser.add_argument(*flag.aliases, help=flag.description, **parser_args) 127 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Release 0.1.17 (2021-11-17) 2 | ===== 3 | 4 | * Support comma three, which has its .bashrc located elsewhere. 5 | * Nicer user-facing installation process 6 | * Clean up installation logic: 7 | * Minimal modification to the system .bashrc file, only one `source` line is appended to the community .bashrc 8 | * Much safer: the previous installer moved the system .bashrc file to a permanently rw partition 9 | 10 | Release 0.1.16 (2021-04-18) 11 | ===== 12 | 13 | * Add `emu debug reload` (or simply `reload`) command to restart openpilot without needing to reboot your device. 14 | 15 | 16 | Release 0.1.15 (2021-04-11) 17 | ===== 18 | 19 | * Add `repo` flag to `emu fork switch` command: if a repository's name isn't openpilot and isn't a GitHub fork (no name redirection), you can use this option the first time you switch to the fork (remembers URL after that). 20 | 21 | 22 | Release 0.1.14 (2021-04-02) 23 | ===== 24 | 25 | * Remember user's GitHub credentials for 1 day (for pushing) 26 | * When `git push`ing, Git will no longer bother you about your local branch's name not matching the remote's 27 | * You need to re-set up fork management for these two improvements 28 | * Fix issue where it would lock user out from `ssh` if a `/openpilot` directory doesn't exist 29 | * You need to apply the changes manually in your `/data/community/.bashrc` file from [this commit](https://github.com/emu-sh/.oh-my-comma/commit/ea67a5960cf3e4aeb93627060ca4ed990a71f595) 30 | 31 | 32 | Release 0.1.13 (2021-02-22) 33 | ===== 34 | 35 | * Add flag to `emu device settings -c` to close settings app 36 | * Use most similar remote branch (using difflib) if user types unknown close branch 37 | * Use existing function in CommandBase for getting flags, exits if fails so no need to catch errors in each command that has flags 38 | * Alias `fork` to `emu fork switch`. Ex.: `fork stock -b devel` 39 | 40 | 41 | Release 0.1.12 (2020-10-22) 42 | ===== 43 | 44 | * Force reinitializes when submodules detected on the branch we're switching to 45 | * Fixes openpilot not starting when switching away and back to a branch with submodule 46 | * Show what git is doing when switching branches (make checkout verbose) 47 | * Speed up switching by ~300ms by only pruning once every 24 hrs. 48 | * Add `--force` flag to switch command, same as `git checkout -f` 49 | * Fix `shutdown` command happily taking any argument without error, defaulting to shutdown when not `-r` 50 | * Add x emoji (❌) prepended to all errors using error function, makes errors stand out more. 51 | 52 | 53 | Release 0.1.11 (2020-10-05) 54 | ===== 55 | 56 | * Don't fetch in new sessions while cloning 57 | * Clean up update screen 58 | * Exception catching: 59 | * When user enters branch for fork switch without the -b flag 60 | * KeyboardInterrupt exception while cloning 61 | 62 | Release 0.1.10 (2020-08-28) 63 | ===== 64 | 65 | * On first setup we now rename local branch `release2` to `commaai_release2` (removes dangling `release2` branch after setup and switching to stock) 66 | * We also now add the `release2` branch to `installed_branches` for the fork so `emu fork list` now shows current branch after immediate set up with no switching 67 | * Add `dragonpilot` as an alias to `dragonpilot-community/dragonpilot` 68 | 69 | Release 0.1.9 (2020-08-12) 70 | ===== 71 | 72 | * Fix "branch you specified does not exist" error due to deleted remote branches 73 | * Also add a prompt to prune the local branches that have deleted on the remote 74 | 75 | Release 0.1.8 (2020-07-29) 76 | ===== 77 | 78 | * Run user's switch command after fork management setup 79 | * More verbose fork switching, shows git output 80 | * Only print newline when more information about command is available to better differentiate between commands 81 | 82 | Release 0.1.7 (2020-07-24) 83 | ===== 84 | 85 | * Auto updater will check for updates. This is based on .oh-my-zsh's [check_for_upgrade.sh](https://github.com/ohmyzsh/ohmyzsh/blob/master/tools/check_for_upgrade.sh) 86 | 87 | Release 0.1.6 (2020-07-23) 88 | ===== 89 | 90 | * Add device command aliases: battery, settings, shutdown 91 | 92 | Release 0.1.5 (2020-07-22) 93 | ===== 94 | 95 | * Make the username argument under `emu fork switch` optional. If not specified, it will use the current fork switched to 96 | * Add `--branch` (`-b`) flag for specifying the branch 97 | * This means you must supply `-b` or `--branch` when switching branches, even when supplying the username: 98 | 99 | Old syntax: emu fork switch another_fork branch 100 | New syntax: emu fork switch another_fork -b branch 101 | 102 | Old syntax: emu fork switch same_fork new_branch 103 | New syntax: emu fork switch -b new_branch 104 | 105 | Release 0.1.4 (2020-07-12) 106 | ===== 107 | 108 | * Add `emu device settings` command to open the settings app 109 | 110 | Release 0.1.3 (2020-07-06) 111 | ===== 112 | 113 | * Make flags/arguments more robust. Optional non-positional arguments are now supported, as long as they are the last arguments. 114 | * `emu fork switch` and `emu fork list` commands added. Uses one singular git repo and adds remotes of forks so that the time to install a new fork is reduced significantly since git is able to re-use blobs. 115 | * A one-time setup is required when using the fork command, this full clones commaai/openpilot which may take a bit of time on first use. 116 | * Change remote of `origin` to `commaai` so that no additional logic is required. Aliases of stock openpilot are: `['stock', 'commaai', 'origin']` 117 | * Stores all installed forks and forks' branches in `/data/community/forks.json` so that the forks command can easily identify when it needs to track and create a branch or just check it out. 118 | * You should still run `git pull` to make sure you get the latest updates from the fork you're currently switched to. 119 | * Dynamic loading of commands. If a command has an exception loading, it won't crash the CLI. Instead you will see an error when you try to call `emu` 120 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | # _ 4 | # -=(') 5 | # ;; 6 | # // 7 | # // 8 | # : '.---.__ 9 | # | --_-_)__) 10 | # `.____,' 11 | # \ \ ___ ._ _ _ _ _ 12 | # ___\ \ / ._>| ' ' || | | 13 | # ( \ \___.|_|_|_|`___| 14 | # \ 15 | # / 16 | 17 | # This is the install script for https://emu.sh/ 18 | # Located on git at https://github.com/emu-sh/.oh-my-comma 19 | # To install this, ssh into your comma device and paste: 20 | # bash <(curl -fsSL install.emu.sh) # the brain of the bird 21 | # source $SYSTEM_BASHRC_PATH depending on system 22 | 23 | if [ ! -f /EON ] && [ ! -f /TICI ]; then 24 | echo "Attempting to install on an unsupported platform" 25 | echo "emu only supports comma.ai devices at this time" 26 | exit 1 27 | fi 28 | 29 | SYSTEM_BASHRC_PATH=$([ -f /EON ] && echo "/home/.bashrc" || echo "/etc/bash.bashrc") 30 | COMMUNITY_PATH=/data/community 31 | COMMUNITY_BASHRC_PATH=/data/community/.bashrc 32 | OH_MY_COMMA_PATH=/data/community/.oh-my-comma 33 | GIT_BRANCH_NAME=master 34 | GIT_REMOTE_URL=https://github.com/emu-sh/.oh-my-comma.git 35 | OMC_VERSION=0.1.17 36 | 37 | install_echo() { # only prints if not updating 38 | if [ "$update" != true ]; then 39 | # shellcheck disable=SC2059 40 | printf -- "$1\n" 41 | fi 42 | } 43 | 44 | install_community_bashrc() { 45 | # Copies default-bashrcs/.bashrc-community to /data/community/.bashrc 46 | cp "${OH_MY_COMMA_PATH}/default-bashrcs/.bashrc-community" $COMMUNITY_BASHRC_PATH 47 | chmod 755 ${COMMUNITY_BASHRC_PATH} 48 | echo "✅ Copied ${OH_MY_COMMA_PATH}/default-bashrcs/.bashrc-community to ${COMMUNITY_BASHRC_PATH}" 49 | } 50 | 51 | remount_system() { 52 | # Mounts the correct partition at which each OS's .bashrc is located 53 | writable_str=$([ "$1" = "rw" ] && echo "writable" || echo "read-only") 54 | if [ -f /EON ]; then 55 | permission=$([ "$1" = "ro" ] && echo "r" || echo "rw") # just maps ro to r on EON 56 | install_echo "ℹ️ Remounting /system partition as ${writable_str}" 57 | mount -o "$permission",remount /system || exit 1 58 | else 59 | install_echo "ℹ️ Remounting / partition as ${writable_str}" 60 | sudo mount -o "$1",remount / || exit 1 61 | fi 62 | } 63 | 64 | # System .bashrc should exist 65 | if [ ! -f "$SYSTEM_BASHRC_PATH" ]; then 66 | echo "Your .bashrc file does not exist at ${SYSTEM_BASHRC_PATH}" 67 | exit 1 68 | fi 69 | 70 | update=false 71 | if [ $# -ge 1 ] && [ $1 = "update" ]; then 72 | update=true 73 | fi 74 | 75 | if [ ! -d "/data/community" ]; then 76 | mkdir /data/community 77 | chmod 755 /data/community 78 | fi 79 | 80 | if [ ! -d "$OH_MY_COMMA_PATH" ]; then 81 | echo "Cloning .oh-my-comma" 82 | git clone -b ${GIT_BRANCH_NAME} ${GIT_REMOTE_URL} ${OH_MY_COMMA_PATH} 83 | fi 84 | 85 | # FIXME: figure out how to install pip packages in AGNOS 86 | if [ -f /EON ] && [ ! -x "$(command -v powerline-shell)" ] && [ $update = false ]; then 87 | echo "Do you want to install powerline? [You will also need to install the fonts on your local terminal.]" 88 | read -p "[Y/n] > " choices 89 | case ${choices} in 90 | y|Y ) remount_system rw && pip install powerline-shell && remount_system ro;; 91 | * ) echo "Skipping...";; 92 | esac 93 | fi 94 | 95 | install_echo "ℹ️ Installing emu utilities\n" 96 | # If community .bashrc is already sourced, do nothing, else merely append source line to system .bashrc 97 | if grep -q "$SYSTEM_BASHRC_PATH" -e "source ${COMMUNITY_BASHRC_PATH}"; then 98 | install_echo "✅ Community .bashrc is sourced in system .bashrc, skipping" 99 | else 100 | # Append community .bashrc source onto system .bashrc 101 | remount_system rw 102 | echo "ℹ️ Sourcing community .bashrc in system .bashrc" 103 | msg="\n# automatically added by .oh-my-comma:\nif [ -f ${COMMUNITY_BASHRC_PATH} ]; then\n source ${COMMUNITY_BASHRC_PATH}\nfi\n" 104 | if [ -f /TICI ]; then # need to sudo on AGNOS 105 | printf "$msg" | sudo tee -a "$SYSTEM_BASHRC_PATH" > /dev/null || exit 1 106 | else 107 | printf "$msg" | tee -a "$SYSTEM_BASHRC_PATH" > /dev/null || exit 1 108 | fi 109 | remount_system ro 110 | printf "✅ Success!\n\n" 111 | fi 112 | 113 | # FIXME: not applicable on TICI 114 | if [ -f /EON ]; then 115 | install_echo "Checking /home/.config symlink..." 116 | if [ "$(readlink -f /home/.config/powerline-shell)" != "$OH_MY_COMMA_PATH/.config/powerline-shell" ]; then 117 | remount_system rw # FIXME: do we need /system rw to access /home on NEOS? 118 | echo "Creating a symlink of ${OH_MY_COMMA_PATH}/.config/powerline-shell to /home/.config/powerline-shell" 119 | ln -s ${OH_MY_COMMA_PATH}/.config/powerline-shell /home/.config/powerline-shell 120 | remount_system ro 121 | else 122 | install_echo "Symlink check passed" 123 | fi 124 | fi 125 | 126 | # If community .bashrc file doesn't exist, copy from .bashrc-community 127 | if [ ! -f "$COMMUNITY_BASHRC_PATH" ]; then 128 | echo "ℹ️ Creating your community .bashrc at ${COMMUNITY_BASHRC_PATH}" 129 | install_community_bashrc 130 | elif [ $update = false ]; then 131 | printf "\n❗ A .bashrc file already exists at ${COMMUNITY_BASHRC_PATH}, but you're installing .oh-my.comma\n" 132 | printf "Would you like to overwrite it with the default to make sure it's up to date?\n\n" 133 | read -p "[Y/n]: " overwrite 134 | case ${overwrite} in 135 | n|N ) printf "Skipping...\n";; 136 | * ) install_community_bashrc;; 137 | esac 138 | fi 139 | 140 | touch ${COMMUNITY_PATH}/.bash_history 141 | chmod 775 ${COMMUNITY_PATH}/.bash_history 142 | 143 | printf "\n\033[92m" 144 | if [ $update = true ]; then 145 | echo "✅ Successfully updated emu utilities!" 146 | else 147 | echo "✅ Successfully installed emu utilities!" 148 | fi 149 | 150 | CURRENT_BRANCH=$(cd ${OH_MY_COMMA_PATH} && git rev-parse --abbrev-ref HEAD) 151 | if [ "${CURRENT_BRANCH}" != "master" ]; then 152 | printf "\n❗ \033[0;31mWarning:\033[0m your current .oh-my-comma git branch is %s. If this is unintentional, run:\n\033[92mgit -C /data/community/.oh-my-comma checkout master\033[0m\n\n" "${CURRENT_BRANCH}" 153 | fi 154 | 155 | install_echo "Current version: $OMC_VERSION" # prints in update.sh 156 | if [ $update = false ]; then 157 | printf "\033[0mYou may want to exit out of this bash instance to automatically source emu\n" 158 | fi 159 | 160 | printf "\033[0m\n" # reset color 161 | 162 | if [ $update = false ]; then 163 | set +x 164 | fi 165 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /py_utils/emu_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | import sys 4 | import psutil 5 | import difflib 6 | import argparse 7 | import subprocess 8 | import time 9 | 10 | if __package__ is None: 11 | from os import path 12 | 13 | sys.path.append(path.abspath(path.join(path.dirname(__file__), '../py_utils'))) 14 | from py_utils.colors import COLORS 15 | else: 16 | from py_utils.colors import COLORS 17 | 18 | COMMUNITY_PATH = '/data/community' 19 | COMMUNITY_BASHRC_PATH = '/data/community/.bashrc' 20 | OH_MY_COMMA_PATH = '/data/community/.oh-my-comma' 21 | UPDATE_PATH = '{}/update.sh'.format(OH_MY_COMMA_PATH) 22 | UNINSTALL_PATH = '{}/uninstall.sh'.format(OH_MY_COMMA_PATH) 23 | OPENPILOT_PATH = '/data/openpilot' 24 | 25 | FORK_PARAM_PATH = '/data/community/forks.json' 26 | 27 | 28 | class ArgumentParser(argparse.ArgumentParser): 29 | def error(self, message): 30 | raise Exception('error: {}'.format(message)) 31 | 32 | 33 | class TimeDebugger: 34 | def __init__(self, convention='s', round_to=4, silent=False): 35 | assert convention in ['s', 'ms'], 'Must be "s" or "ms"!' 36 | self.convention = convention 37 | self.round_to = round_to 38 | self.silent = silent 39 | self.reset(full=True) 40 | 41 | def reset(self, full=False): 42 | self.last_time = time.time() 43 | if full: 44 | self.start_time = self.last_time 45 | 46 | def print(self, msg=None, total=False): 47 | if self.silent: 48 | return 49 | if not total: 50 | elapsed = time.time() - self.last_time 51 | elapsed *= 1000 if self.convention == 'ms' else 1 52 | if msg is not None: 53 | msg = 'Time to {}'.format(msg) 54 | else: 55 | msg = 'Time elapsed' 56 | print('{}: {} {}'.format(msg, round(elapsed, self.round_to), self.convention)) 57 | else: 58 | elapsed = time.time() - self.start_time 59 | elapsed *= 1000 if self.convention == 'ms' else 1 60 | print('Total: {} {}'.format(round(elapsed, self.round_to), self.convention)) 61 | self.reset(total) 62 | 63 | 64 | class BaseFunctions: 65 | def print_commands(self, error_msg=None, ascii_art=False): 66 | if ascii_art: 67 | print(EMU_ART) 68 | 69 | if error_msg is not None: 70 | error(error_msg) 71 | max_cmd = max([len(_c) for _c in self.commands]) + 1 72 | for idx, cmd in enumerate(self.commands): 73 | desc = COLORS.CYAN + self.commands[cmd].description 74 | print_cmd = '{} {}'.format(self.name, cmd) 75 | if self.name != 'emu': 76 | print_cmd = 'emu {}'.format(print_cmd) 77 | print(COLORS.OKGREEN + ('- {:<%d} {}' % max_cmd).format(print_cmd + ':', desc)) 78 | if hasattr(self, '_help'): 79 | # leading is for better differentiating between the different commands 80 | if self._help(cmd, show_description=False, leading=' '): 81 | print() # only add newline when there's more information to sift through 82 | print(COLORS.ENDC, end='') 83 | 84 | def next_arg(self, lower=True, ingest=True): 85 | """ 86 | Returns next arg and deletes arg from self.args if ingest=True 87 | :param lower: Returns arg.lower() 88 | :param ingest: Deletes returned arg from self.arg 89 | :return: 90 | """ 91 | if len(self.args): 92 | arg = self.args[0] 93 | if lower: 94 | arg = arg.lower() 95 | if ingest: 96 | del self.args[0] 97 | else: 98 | arg = None 99 | return arg 100 | 101 | 102 | def str_sim(a, b): 103 | return difflib.SequenceMatcher(a=a, b=b).ratio() 104 | 105 | 106 | def input_with_options(options, default=None): 107 | """ 108 | Takes in a list of options and asks user to make a choice. 109 | The most similar option list index is returned along with the similarity percentage from 0 to 1 110 | """ 111 | 112 | user_input = input('[{}]: '.format('/'.join(options))).lower().strip() 113 | if not user_input: 114 | return default, 0.0 115 | sims = [str_sim(i.lower().strip(), user_input) for i in options] 116 | argmax = sims.index(max(sims)) 117 | return argmax, sims[argmax] 118 | 119 | 120 | def most_similar(find, options): 121 | sims = [[str_sim(i.lower().strip(), find.lower().strip()), i] for i in options] 122 | sims = sorted(sims, reverse=True) 123 | return [[o[1], o[0]] for o in sims] 124 | 125 | 126 | def check_output(cmd, cwd=None, shell=False): 127 | class Output: 128 | def __init__(self, output='', s=True): 129 | self.output = output 130 | self.success = s 131 | 132 | if isinstance(cmd, str) and not shell: 133 | cmd = cmd.split() 134 | try: 135 | return Output(subprocess.check_output(cmd, cwd=cwd, stderr=subprocess.STDOUT, encoding='utf8', shell=shell)) 136 | except subprocess.CalledProcessError as e: 137 | if e.output is None: 138 | return Output(e, s=False) # command failed to execute 139 | return Output(e.output) # command executed but it resulted in error 140 | 141 | 142 | def run(cmd, out_file=None): # todo: return output with same format as check_output, but also output to user (current behavior) 143 | """ 144 | If cmd is a string, it is split into a list, otherwise it doesn't modify cmd. 145 | The status is returned, True being success, False for failure 146 | """ 147 | if isinstance(cmd, str): 148 | cmd = cmd.split() 149 | 150 | f = None 151 | if isinstance(out_file, str): 152 | f = open(out_file, 'a') 153 | 154 | try: 155 | r = subprocess.call(cmd, stdout=f) 156 | return not r 157 | except (Exception, KeyboardInterrupt) as e: 158 | # print(e) 159 | return False 160 | 161 | 162 | def kill(procname): 163 | for proc in psutil.process_iter(): 164 | # check whether the process name matches 165 | if proc.name() == procname: 166 | proc.kill() 167 | return True 168 | return None 169 | 170 | 171 | def is_affirmative(): 172 | i = None 173 | print(COLORS.PROMPT, end='') 174 | while i not in ['y', 'n', 'yes', 'no', 'sure', '']: 175 | i = input('[Y/n]: ').lower().strip() 176 | print(COLORS.ENDC) 177 | return i in ['y', 'yes', 'sure', ''] 178 | 179 | 180 | def error(msg, end='\n', ret=False, start=''): 181 | """ 182 | The following applies to error, warning, and success methods 183 | :param msg: The message to display 184 | :param end: The ending char, default is \n 185 | :param ret: Whether to return the formatted string, or print it 186 | :return: The formatted string if ret is True 187 | """ 188 | e = start + '❌ {}{}{}'.format(COLORS.FAIL, msg, COLORS.ENDC) 189 | if ret: 190 | return e 191 | print(e, end=end) 192 | 193 | 194 | def warning(msg, end='\n', ret=False): 195 | w = '{}{}{}'.format(COLORS.PROMPT, msg, COLORS.ENDC) 196 | if ret: 197 | return w 198 | print(w, end=end) 199 | 200 | 201 | def success(msg, end='\n', ret=False): 202 | s = '{}{}{}'.format(COLORS.SUCCESS, msg, COLORS.ENDC) 203 | if ret: 204 | return s 205 | print(s, end=end) 206 | 207 | 208 | def info(msg, end='\n', ret=False): 209 | s = '{}{}{}'.format(COLORS.WARNING, msg, COLORS.ENDC) 210 | if ret: 211 | return s 212 | print(s, end=end) 213 | 214 | 215 | EMU_ART = r""" _ 216 | -=(""" + COLORS.RED + """'""" + COLORS.CWHITE + """) 217 | ;; 218 | // 219 | // 220 | : '.---.__ 221 | | --_-_)__) 222 | `.____,' 223 | \ \ """ + COLORS.OKGREEN + """ ___ ._ _ _ _ _ """ + COLORS.CWHITE + """ 224 | ___\ \ """ + COLORS.OKGREEN + """/ ._>| ' ' || | |""" + COLORS.CWHITE + """ 225 | ( \ """ + COLORS.OKGREEN + """\___.|_|_|_|`___|""" + COLORS.CWHITE + """ 226 | \ 227 | /""" + '\n' 228 | -------------------------------------------------------------------------------- /commands/fork/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | import shutil 4 | import os 5 | import json 6 | from datetime import datetime 7 | from commands.base import CommandBase, Command, Flag 8 | from py_utils.emu_utils import run, error, success, warning, info, is_affirmative, check_output, most_similar 9 | from py_utils.emu_utils import OPENPILOT_PATH, FORK_PARAM_PATH, COLORS, OH_MY_COMMA_PATH 10 | 11 | GIT_OPENPILOT_URL = 'https://github.com/commaai/openpilot' 12 | REMOTE_ALREADY_EXISTS = 'already exists' 13 | DEFAULT_BRANCH_START = 'HEAD branch: ' 14 | REMOTE_BRANCHES_START = 'Remote branches:\n' 15 | REMOTE_BRANCH_START = 'Remote branch:' 16 | DEFAULT_REPO_NAME = 'openpilot' 17 | 18 | 19 | def valid_fork_url(url): 20 | import urllib.request 21 | try: 22 | request = urllib.request.Request(url) 23 | request.get_method = lambda: 'HEAD' 24 | urllib.request.urlopen(request) 25 | return True 26 | except Exception as e: 27 | return False 28 | 29 | 30 | class ForkParams: 31 | def __init__(self): 32 | self.default_params = {'current_fork': None, 33 | 'current_branch': None, 34 | 'installed_forks': {}, 35 | 'last_prune': None, 36 | 'setup_complete': False} 37 | self._init() 38 | 39 | def _init(self): 40 | if os.path.exists(FORK_PARAM_PATH): 41 | try: 42 | self._read() 43 | for param in self.default_params: 44 | if param not in self.params: 45 | self.params[param] = self.default_params[param] 46 | return 47 | except: 48 | pass 49 | 50 | self.params = self.default_params # default params 51 | self._write() # failed to read, just write default 52 | 53 | def get(self, key): 54 | return self.params[key] 55 | 56 | def put(self, key, value): 57 | self.params.update({key: value}) 58 | self._write() 59 | 60 | def reset(self): 61 | self.params = self.default_params 62 | self._write() 63 | 64 | def _read(self): 65 | with open(FORK_PARAM_PATH, "r") as f: 66 | self.params = json.loads(f.read()) 67 | 68 | def _write(self): 69 | with open(FORK_PARAM_PATH, "w") as f: 70 | f.write(json.dumps(self.params, indent=2)) 71 | 72 | 73 | class RemoteInfo: 74 | def __init__(self, fork_name, username_aliases, default_branch): 75 | self.username = None # to be added by Fork.__get_remote_info function 76 | self.fork_name = fork_name 77 | self.username_aliases = username_aliases 78 | self.default_branch = default_branch 79 | 80 | 81 | class Fork(CommandBase): 82 | def __init__(self): 83 | super().__init__() 84 | self.name = 'fork' 85 | self.description = '🍴 Manage installed forks, or install a new one' 86 | 87 | self.fork_params = ForkParams() 88 | self.remote_defaults = {'commaai': RemoteInfo('openpilot', ['stock', 'origin'], 'release2'), 89 | 'dragonpilot-community': RemoteInfo('dragonpilot', ['dragonpilot'], 'devel-i18n')} # devel-i18n isn't most stable, but its name remains the same 90 | 91 | self.comma_origin_name = 'commaai' 92 | self.comma_default_branch = self.remote_defaults['commaai'].default_branch 93 | 94 | self.commands = {'switch': Command(description='🍴 Switch between any openpilot fork', 95 | flags=[Flag('username', '👤 The username of the fork\'s owner to switch to, will use current fork if not provided', required=False, dtype='str'), 96 | Flag(['-b', '--branch'], '🌿 Branch to switch to, will use default branch if not provided', required=False, dtype='str'), 97 | Flag(['-r', '--repo'], 'The repository name of the fork, if its name isn\'t openpilot', required=False, dtype='str'), 98 | Flag(['-f', '--force'], '💪 Similar to checkout -f, force checks out new branch overwriting any changes')]), 99 | 'list': Command(description='📜 See a list of installed forks and branches', 100 | flags=[Flag('fork', '🌿 See branches of specified fork', dtype='str')])} 101 | 102 | def _list(self): 103 | if not self._init(): 104 | return 105 | flags = self.get_flags('list') 106 | specified_fork = flags.fork 107 | 108 | installed_forks = self.fork_params.get('installed_forks') 109 | if specified_fork is None: 110 | max_branches = 4 # max branches to display per fork when listing all forks 111 | success('Installed forks:') 112 | for idi, fork in enumerate(installed_forks): 113 | print('- {}{}{}'.format(COLORS.OKBLUE, fork, COLORS.ENDC), end='') 114 | current_fork = self.fork_params.get('current_fork') 115 | if current_fork == fork: 116 | print(' (current)') 117 | else: 118 | print() 119 | branches = installed_forks[fork]['installed_branches'] 120 | current_branch = self.fork_params.get('current_branch') 121 | if current_branch in branches: 122 | branches.remove(current_branch) 123 | branches.insert(0, current_branch) # move cur_branch to beginning 124 | 125 | if len(branches) > 0: 126 | success(' Branches:') 127 | for idx, branch in enumerate(branches): 128 | if idx < max_branches: 129 | print(' - {}{}{}'.format(COLORS.RED, branch, COLORS.ENDC), end='') 130 | if branch == current_branch and fork == current_fork: 131 | print(' (current)') 132 | else: 133 | print() 134 | else: 135 | print(' - {}...see more branches: {}emu fork list {}{}'.format(COLORS.RED, COLORS.CYAN, fork, COLORS.ENDC)) 136 | break 137 | print() 138 | else: 139 | specified_fork = specified_fork.lower() 140 | remote_info = self.__get_remote_info(specified_fork) 141 | if remote_info is not None: # there's an overriding default username available 142 | specified_fork = remote_info.username 143 | if specified_fork not in installed_forks: 144 | error('{} not an installed fork! Try installing it with the {}switch{} command'.format(specified_fork, COLORS.CYAN, COLORS.RED)) 145 | return 146 | installed_branches = installed_forks[specified_fork]['installed_branches'] 147 | success('Installed branches for {}:'.format(specified_fork)) 148 | for branch in installed_branches: 149 | print(' - {}{}{}'.format(COLORS.RED, branch, COLORS.ENDC)) 150 | 151 | def _switch(self): 152 | if not self._init(): 153 | return 154 | flags = self.get_flags('switch') 155 | if flags.username is flags.branch is None: # since both are non-required we need custom logic to check user supplied sufficient args/flags 156 | error('You must supply either username or branch or both') 157 | self._help('switch') 158 | return 159 | 160 | username = flags.username 161 | branch = flags.branch 162 | repo_name = flags.repo 163 | force_switch = flags.force 164 | if username is None: # branch is specified, so use current checked out fork/username 165 | _current_fork = self.fork_params.get('current_fork') 166 | if _current_fork is not None: # ...if available 167 | info('Assuming current fork for username: {}'.format(COLORS.SUCCESS + _current_fork + COLORS.ENDC)) 168 | username = _current_fork 169 | else: 170 | error('Current fork is unknown, please switch to a fork first before switching between branches!') 171 | return 172 | 173 | username = username.lower() 174 | remote_info = self.__get_remote_info(username) 175 | if remote_info is not None: # user entered an alias (ex. stock, dragonpilot) 176 | username = remote_info.username 177 | 178 | installed_forks = self.fork_params.get('installed_forks') 179 | fork_in_params = True 180 | if username not in installed_forks: 181 | fork_in_params = False 182 | if remote_info is not None: 183 | remote_url = f'https://github.com/{username}/{remote_info.fork_name}' # dragonpilot doesn't have a GH redirect 184 | else: # for most forks, GH will redirect from /openpilot if user renames fork 185 | if repo_name is None: 186 | repo_name = DEFAULT_REPO_NAME # openpilot 187 | remote_url = f'https://github.com/{username}/{repo_name}' 188 | 189 | if not valid_fork_url(remote_url): 190 | error('Invalid username{}! {} does not exist'.format('' if flags.repo is None else ' or repository name', remote_url)) 191 | return 192 | 193 | r = check_output(['git', '-C', OPENPILOT_PATH, 'remote', 'add', username, remote_url]) 194 | if r.success and r.output == '': 195 | success('Remote added successfully!') 196 | elif r.success and REMOTE_ALREADY_EXISTS in r.output: 197 | # remote already added, update params 198 | info('Fork exists but wasn\'t in params, updating now...') 199 | self.__add_fork(username) 200 | else: 201 | error(r.output) 202 | return 203 | 204 | # fork has been added as a remote, switch to it 205 | if fork_in_params: 206 | info('Fetching {}\'s latest changes...'.format(COLORS.SUCCESS + username + COLORS.WARNING)) 207 | else: 208 | info('Fetching {}\'s fork, this may take a sec...'.format(COLORS.SUCCESS + username + COLORS.WARNING)) 209 | 210 | r = run(['git', '-C', OPENPILOT_PATH, 'fetch', username]) 211 | if not r: 212 | error('Error while fetching remote, please try again') 213 | return 214 | 215 | self.__add_fork(username) 216 | self.__prune_remote_branches(username) 217 | r = check_output(['git', '-C', OPENPILOT_PATH, 'remote', 'show', username]) 218 | remote_branches, default_remote_branch = self.__get_remote_branches(r) 219 | if remote_branches is None: 220 | return 221 | 222 | if DEFAULT_BRANCH_START not in r.output: 223 | error('Error: Cannot find default branch from fork!') 224 | return 225 | 226 | if branch is None: # user hasn't specified a branch, use remote's default branch 227 | if remote_info is not None: # there's an overriding default branch specified 228 | remote_branch = remote_info.default_branch 229 | local_branch = '{}_{}'.format(remote_info.username, remote_branch) 230 | else: 231 | remote_branch = default_remote_branch # for command to checkout correct branch from remote, branch is previously None since user didn't specify 232 | local_branch = '{}_{}'.format(username, default_remote_branch) 233 | else: 234 | if branch not in remote_branches: 235 | close_branches = most_similar(branch, remote_branches) # remote_branches is gauranteed to have at least 1 branch 236 | if close_branches[0][1] > 0.5: 237 | branch = close_branches[0][0] 238 | info('Unknown branch, checking out most similar: {}'.format(COLORS.SUCCESS + branch + COLORS.WARNING)) 239 | else: 240 | error('The branch you specified does not exist!') 241 | self.__show_similar_branches(branch, remote_branches) # if possible 242 | return 243 | remote_branch = branch # branch is now gauranteed to be in remote_branches 244 | local_branch = f'{username}_{branch}' 245 | 246 | # checkout remote branch and prepend username so we can have multiple forks with same branch names locally 247 | if remote_branch not in installed_forks[username]['installed_branches']: 248 | info('New branch! Tracking and checking out {} from {}'.format(local_branch, f'{username}/{remote_branch}')) 249 | command = ['git', '-C', OPENPILOT_PATH, 'checkout', '--track', '-b', local_branch, f'{username}/{remote_branch}'] 250 | else: # already installed branch, checking out fork_branch from f'{username}/{branch}' 251 | command = ['git', '-C', OPENPILOT_PATH, 'checkout', local_branch] 252 | 253 | if force_switch: 254 | command.append('-f') 255 | r = run(command) 256 | if not r: 257 | error('Error while checking out branch, please try again or use flag --force') 258 | return 259 | self.__add_branch(username, remote_branch) # we can deduce fork branch from username and original branch f({username}_{branch}) 260 | 261 | # reset to remote/branch just to ensure we checked out fully. if remote branch has been force pushed, this will also reset local to remote 262 | r = check_output(['git', '-C', OPENPILOT_PATH, 'reset', '--hard', f'{username}/{remote_branch}']) 263 | if not r.success: 264 | error(r.output) 265 | return 266 | 267 | reinit_subs = self.__init_submodules() 268 | self.fork_params.put('current_fork', username) 269 | self.fork_params.put('current_branch', remote_branch) 270 | info('\n✅ Successfully checked out {}/{} as {}'.format(COLORS.SUCCESS + username, remote_branch + COLORS.WARNING, COLORS.SUCCESS + local_branch)) 271 | if reinit_subs: 272 | success('✅ Successfully reinitialized submodules!') 273 | 274 | def __add_fork(self, username, branch=None): 275 | installed_forks = self.fork_params.get('installed_forks') 276 | if username not in installed_forks: 277 | installed_forks[username] = {'installed_branches': []} 278 | if branch is not None: 279 | installed_forks[username]['installed_branches'].append(branch) 280 | self.fork_params.put('installed_forks', installed_forks) 281 | 282 | def __add_branch(self, username, branch): # assumes fork exists in params, doesn't add branch if exists 283 | installed_forks = self.fork_params.get('installed_forks') 284 | if branch not in installed_forks[username]['installed_branches']: 285 | installed_forks[username]['installed_branches'].append(branch) 286 | self.fork_params.put('installed_forks', installed_forks) 287 | 288 | @staticmethod 289 | def __show_similar_branches(branch, branches): 290 | if len(branches) > 0: 291 | info('Did you mean:') 292 | close_branches = most_similar(branch, branches)[:5] 293 | for idx in range(len(close_branches)): 294 | cb = close_branches[idx][0] 295 | if idx == 0: 296 | cb = COLORS.OKGREEN + cb 297 | else: 298 | cb = COLORS.CYAN + cb 299 | print(' - {}{}'.format(cb, COLORS.ENDC)) 300 | 301 | def __prune_remote_branches(self, username): # remove deleted remote branches locally 302 | last_prune = self.fork_params.get('last_prune') 303 | if isinstance(last_prune, str) and datetime.now().strftime("%d") == last_prune: 304 | return 305 | self.fork_params.put('last_prune', datetime.now().strftime("%d")) 306 | 307 | r = check_output(['git', '-C', OPENPILOT_PATH, 'remote', 'prune', username, '--dry-run']) 308 | if r.output == '': # nothing to prune 309 | return 310 | branches_to_prune = [b.strip() for b in r.output.split('\n') if 'would prune' in b] 311 | branches_to_prune = [b[b.index(username):] for b in branches_to_prune] 312 | 313 | error('Deleted remote branches detected:', start='\n') 314 | for b in branches_to_prune: 315 | print(COLORS.CYAN + ' - {}'.format(b) + COLORS.ENDC) 316 | warning('\nWould you like to delete them locally?') 317 | if is_affirmative(): 318 | r = check_output(['git', '-C', OPENPILOT_PATH, 'remote', 'prune', username]) 319 | if r.success: 320 | success('Pruned local branches successfully!') 321 | else: 322 | error('Please try again, something went wrong:') 323 | print(r.output) 324 | 325 | def __get_remote_info(self, username): 326 | for default_username in self.remote_defaults: 327 | remote_info = self.remote_defaults[default_username] 328 | remote_info.username = default_username # add dict key to class instance so we don't have to return a tuple 329 | remote_info.username_aliases.append(default_username) # so default branch works when user enters the actual name 330 | if username in remote_info.username_aliases: 331 | return remote_info 332 | return None 333 | 334 | @staticmethod 335 | def __get_remote_branches(r): 336 | # get remote's branches to verify from output of command in parent function 337 | if not r.success: 338 | error(r.output) 339 | return None, None 340 | if REMOTE_BRANCHES_START in r.output: 341 | start_remote_branches = r.output.index(REMOTE_BRANCHES_START) 342 | remote_branches_txt = r.output[start_remote_branches + len(REMOTE_BRANCHES_START):].split('\n') 343 | remote_branches = [] 344 | for b in remote_branches_txt: 345 | b = b.replace('tracked', '').strip() 346 | if 'stale' in b: # support stale/to-be-pruned branches 347 | b = b.split(' ')[0].split('/')[-1] 348 | if ' ' in b or b == '': # end of branches 349 | break 350 | remote_branches.append(b) 351 | elif REMOTE_BRANCH_START in r.output: # remote has single branch, shouldn't need to handle stale here 352 | start_remote_branch = r.output.index(REMOTE_BRANCH_START) 353 | remote_branches = r.output[start_remote_branch + len(REMOTE_BRANCH_START):].split('\n') 354 | remote_branches = [b.replace('tracked', '').strip() for b in remote_branches if b.strip() != '' and 'tracked' in b] 355 | else: 356 | error('Unable to parse remote branches!') 357 | return None, None 358 | 359 | if len(remote_branches) == 0: 360 | error('Error getting remote branches!') 361 | return None, None 362 | 363 | start_default_branch = r.output.index(DEFAULT_BRANCH_START) # get default branch to return 364 | default_branch = r.output[start_default_branch + len(DEFAULT_BRANCH_START):] 365 | end_default_branch = default_branch.index('\n') 366 | default_branch = default_branch[:end_default_branch] 367 | return remote_branches, default_branch 368 | 369 | @staticmethod 370 | def __init_submodules(): 371 | r = check_output(['git', '-C', OPENPILOT_PATH, 'submodule', 'status']) 372 | if len(r.output): 373 | info('Submodules detected, reinitializing!') 374 | r0 = check_output(['git', '-C', OPENPILOT_PATH, 'submodule', 'deinit', '--all', '-f']) 375 | r1 = check_output(['git', '-C', OPENPILOT_PATH, 'submodule', 'update', '--init', '--recursive']) 376 | if not r0.success or not r1.success: 377 | error('Error reinitializing submodules for this branch!') 378 | else: 379 | return True 380 | return False 381 | 382 | def _init(self): 383 | if os.path.isdir('/data/community/forks'): 384 | shutil.rmtree('/data/community/forks') # remove to save space 385 | if self.fork_params.get('setup_complete'): 386 | if os.path.exists(OPENPILOT_PATH): 387 | r = check_output(['git', '-C', OPENPILOT_PATH, 'remote', 'show']) 388 | if self.comma_origin_name in r.output.split('\n'): # sign that we're set up correctly todo: check all forks exist as remotes 389 | return True 390 | self.fork_params.put('setup_complete', False) # renamed origin -> commaai does not exist, restart setup 391 | self.fork_params.reset() 392 | warning('There was an error with your clone of commaai/openpilot, restarting initialization!') 393 | 394 | info('To set up emu fork management we will clone commaai/openpilot into {}'.format(OPENPILOT_PATH)) 395 | info('Confirm you would like to continue') 396 | if not is_affirmative(): 397 | error('Stopping initialization!') 398 | return 399 | 400 | # backup openpilot here to free up /data/openpilot 401 | if os.path.exists(OPENPILOT_PATH): 402 | bak_dir = '{}.bak'.format(OPENPILOT_PATH) 403 | idx = 0 404 | while os.path.exists(bak_dir): 405 | bak_dir = '{}{}'.format(bak_dir, idx) 406 | idx += 1 407 | shutil.move(OPENPILOT_PATH, bak_dir) 408 | success('Backed up your current openpilot install to {}'.format(bak_dir)) 409 | 410 | info('Cloning commaai/openpilot into {}, please wait...'.format(OPENPILOT_PATH)) 411 | r = run(['git', 'clone', '-b', self.comma_default_branch, GIT_OPENPILOT_URL, OPENPILOT_PATH]) # default to stock/release2 for setup 412 | if not r: 413 | error('Error while cloning, please try again') 414 | return 415 | 416 | # rename origin to commaai so it's easy to switch to stock without any extra logic for url checking, etc 417 | r = check_output(['git', '-C', OPENPILOT_PATH, 'remote', 'rename', 'origin', self.comma_origin_name]) 418 | if not r.success: 419 | error(r.output) 420 | return 421 | 422 | # rename release2 to commaai_release2 to align with emu fork standards 423 | r = check_output(['git', '-C', OPENPILOT_PATH, 'branch', '-m', f'{self.comma_origin_name}_{self.comma_default_branch}']) 424 | if not r.success: 425 | error(r.output) 426 | return 427 | 428 | # set git config push.default to `upstream` to remove differently named remote branch warning when pushing 429 | check_output(['git', '-C', OPENPILOT_PATH, 'config', 'push.default', 'upstream']) # not game breaking if this fails 430 | 431 | # remember username and password of user for pushing 432 | check_output(['git', '-C', OPENPILOT_PATH, 'config', 'credential.helper', 'cache --timeout=1440']) # cache for a day 433 | 434 | success('Fork management set up successfully! You\'re on {}/{}'.format(self.comma_origin_name, self.comma_default_branch)) 435 | success('To get started, try running: {}emu fork switch (username) [-b BRANCH]{}'.format(COLORS.RED, COLORS.ENDC)) 436 | self.__add_fork(self.comma_origin_name, self.comma_default_branch) 437 | self.fork_params.put('setup_complete', True) 438 | self.fork_params.put('current_fork', self.comma_origin_name) 439 | self.fork_params.put('current_branch', self.comma_default_branch) 440 | return True 441 | --------------------------------------------------------------------------------