Files
dragonpilot/common/op_params.py
T
2020-03-10 04:24:16 -05:00

200 lines
8.9 KiB
Python

#!/usr/bin/env python3
import os
import json
import time
import string
import random
from selfdrive.swaglog import cloudlog
from common.travis_checker import travis
def write_params(params, params_file):
if not travis:
with open(params_file, "w") as f:
json.dump(params, f, indent=2, sort_keys=True)
os.chmod(params_file, 0o764)
def read_params(params_file, default_params):
try:
with open(params_file, "r") as f:
params = json.load(f)
return params, True
except Exception as e:
print(e)
params = default_params
return params, False
class KeyInfo:
has_allowed_types = False
live = False
has_default = False
has_description = False
hidden = False
class opParams:
def __init__(self):
"""
To add your own parameter to opParams in your fork, simply add a new dictionary entry with the name of your parameter and its default value to save to new users' op_params.json file.
The description, allowed_types, and live keys are no longer required but recommended to help users edit their parameters with opEdit and opTune correctly.
- The description value will be shown to users when they use opEdit or opTune to change the value of the parameter.
- The allowed_types key is used to restrict what kinds of values can be entered with opEdit so that users can't reasonably break the fork with unintended behavior.
Limiting the range of floats or integers is still recommended when `.get`ting the parameter.
When a None value is allowed, use `type(None)` instead of None, as opEdit checks the type against the values in the key with `isinstance()`.
- Finally, the live key tells both opParams and opTune that it's a live parameter that will change. Therefore, you must place the `op_params.get()` call in the update function so that it can update.
Here's an example of the minimum required dictionary:
self.default_params = {'camera_offset': {'default': 0.06}}
"""
self.default_params = {'dynamic_follow': {'default': 'relaxed', 'allowed_types': [str], 'description': "Can be: ('traffic', 'relaxed', 'roadtrip'): Left to right increases in following distance.\n"
"All profiles support dynamic follow so you'll get your preferred distance while\n"
"retaining the smoothness and safety of dynamic follow!", 'live': True},
'op_edit_live_mode': {'default': False, 'description': 'This parameter controls which mode opEdit starts in. It should be hidden from the user with the hide key', 'hide': True}}
self.params = {}
self.params_file = "/data/op_params.json"
self.kegman_file = "/data/kegman.json"
self.last_read_time = time.time()
self.read_frequency = 5.0 # max frequency to read with self.get(...) (sec)
self.force_update = False # replaces values with default params if True, not just add add missing key/value pairs
self.to_delete = ['dynamic_lane_speed', 'longkiV', 'following_distance', 'static_steer_ratio']
self.run_init() # restores, reads, and updates params
def create_id(self): # creates unique identifier to send with sentry errors. please update uniqueID with op_edit.py to your username!
need_id = False
if "uniqueID" not in self.params:
need_id = True
if "uniqueID" in self.params and self.params["uniqueID"] is None:
need_id = True
if need_id:
random_id = ''.join([random.choice(string.ascii_lowercase + string.ascii_uppercase + string.digits) for i in range(15)])
self.params["uniqueID"] = random_id
def add_default_params(self):
prev_params = dict(self.params)
if not travis:
self.create_id()
for key in self.default_params:
if self.force_update:
self.params[key] = self.default_params[key]['default']
elif key not in self.params:
self.params[key] = self.default_params[key]['default']
return prev_params == self.params
def format_default_params(self):
return {key: self.default_params[key]['default'] for key in self.default_params}
def run_init(self): # does first time initializing of default params, and/or restoring from kegman.json
if travis:
self.params = self.format_default_params()
return
self.params = self.format_default_params() # in case any file is corrupted
to_write = False
no_params = False
if os.path.isfile(self.params_file):
self.params, read_status = read_params(self.params_file, self.format_default_params())
if read_status:
to_write = not self.add_default_params() # if new default data has been added
if self.delete_old(): # or if old params have been deleted
to_write = True
else: # don't overwrite corrupted params, just print to screen
print("ERROR: Can't read op_params.json file")
elif os.path.isfile(self.kegman_file):
to_write = True # write no matter what
try:
with open(self.kegman_file, "r") as f: # restore params from kegman
self.params = json.load(f)
self.add_default_params()
except:
print("ERROR: Can't read kegman.json file")
else:
no_params = True # user's first time running a fork with kegman_conf or op_params
if to_write or no_params:
write_params(self.params, self.params_file)
def delete_old(self):
prev_params = dict(self.params)
for i in self.to_delete:
if i in self.params:
del self.params[i]
return prev_params == self.params
def put(self, key, value):
self.params.update({key: value})
write_params(self.params, self.params_file)
def get(self, key=None, default=None, force_update=False): # can specify a default value if key doesn't exist
self.update_params(key, force_update)
if key is None:
return self.get_all()
if key in self.params:
key_info = self.key_info(key)
if key_info.has_allowed_types:
value = self.params[key]
allowed_types = self.default_params[key]['allowed_types']
if type(value) not in allowed_types:
cloudlog.warning('op_params: User\'s value is not valid!')
if key_info.has_default: # invalid value type, try to use default value
default_value = self.default_params[key]['default']
if type(default_value) in allowed_types: # actually check if the default is valid
# return default value because user's value of key is not in the allowed_types to avoid crashing openpilot
return default_value
else: # else use a standard value based on type (last resort to keep openpilot running if user's value is of invalid type)
return self.value_from_types(allowed_types)
else:
return value # all good, returning user's value
else:
return self.params[key] # no defined allowed types, returning user's value
return default # not in params
def get_all(self): # returns all non-hidden params
return {k: v for k, v in self.params.items() if not self.key_info(k).hidden}
def key_info(self, key):
key_info = KeyInfo()
if key is None:
return key_info
if key in self.default_params:
if 'allowed_types' in self.default_params[key]:
allowed_types = self.default_params[key]['allowed_types']
if isinstance(allowed_types, list) and len(allowed_types) > 0:
key_info.has_allowed_types = True
if 'live' in self.default_params[key]:
key_info.live = self.default_params[key]['live']
if 'default' in self.default_params[key]:
key_info.has_default = True
if 'description' in self.default_params[key]:
key_info.has_description = True
if 'hide' in self.default_params[key]:
key_info.hidden = self.default_params[key]['hide']
return key_info
def value_from_types(self, allowed_types):
if list in allowed_types:
return []
elif float in allowed_types or int in allowed_types:
return 0
elif type(None) in allowed_types:
return None
elif str in allowed_types:
return ''
return None # unknown type
def update_params(self, key, force_update):
if force_update or self.key_info(key).live: # if is a live param, we want to get updates while openpilot is running
if not travis and (time.time() - self.last_read_time >= self.read_frequency or force_update): # make sure we aren't reading file too often
self.params, read_status = read_params(self.params_file, self.format_default_params())
if not read_status:
time.sleep(1/100.)
self.params, _ = read_params(self.params_file, self.format_default_params()) # if the file was being written to, retry once
self.last_read_time = time.time()
def delete(self, key):
if key in self.params:
del self.params[key]
write_params(self.params, self.params_file)