From db83bc847bcabc17d8bb6fcbdd5cd9c3afff5f56 Mon Sep 17 00:00:00 2001 From: Vehicle Researcher <user@comma.ai> Date: Sat, 28 Oct 2017 13:08:20 -0700 Subject: [PATCH] Tesla Model S support --- common/dbc-0.35.py | 195 ++++++++++++++++ common/dbc.py | 29 ++- common/fingerprints.py | 35 +-- selfdrive/car/__init__.py | 2 + selfdrive/car/tesla/__init__.py | 0 selfdrive/car/tesla/can_parser.py | 131 +++++++++++ selfdrive/car/tesla/carcontroller.py | 209 ++++++++++++++++++ selfdrive/car/tesla/carstate.py | 254 +++++++++++++++++++++ selfdrive/car/tesla/interface.py | 293 +++++++++++++++++++++++++ selfdrive/car/tesla/teslacan.py | 113 ++++++++++ selfdrive/controls/controlsd.py | 31 +-- selfdrive/controls/lib/alertmanager.py | 6 +- selfdrive/manager.py | 4 +- 13 files changed, 1253 insertions(+), 49 deletions(-) create mode 100755 common/dbc-0.35.py create mode 100644 selfdrive/car/tesla/__init__.py create mode 100644 selfdrive/car/tesla/can_parser.py create mode 100644 selfdrive/car/tesla/carcontroller.py create mode 100644 selfdrive/car/tesla/carstate.py create mode 100644 selfdrive/car/tesla/interface.py create mode 100644 selfdrive/car/tesla/teslacan.py diff --git a/common/dbc-0.35.py b/common/dbc-0.35.py new file mode 100755 index 00000000000000..518890326b2bca --- /dev/null +++ b/common/dbc-0.35.py @@ -0,0 +1,195 @@ +import re +import os +import struct +import bitstring +from collections import namedtuple + +def int_or_float(s): + # return number, trying to maintain int format + try: + return int(s) + except ValueError: + return float(s) + +DBCSignal = namedtuple( + "DBCSignal", ["name", "start_bit", "size", "is_little_endian", "is_signed", + "factor", "offset", "tmin", "tmax", "units"]) + +class dbc(object): + def __init__(self, fn): + self.name, _ = os.path.splitext(os.path.basename(fn)) + with open(fn) as f: + self.txt = f.read().split("\n") + self._warned_addresses = set() + + # regexps from https://github.com/ebroecker/canmatrix/blob/master/canmatrix/importdbc.py + bo_regexp = re.compile("^BO\_ (\w+) (\w+) *: (\w+) (\w+)") + sg_regexp = re.compile("^SG\_ (\w+) : (\d+)\|(\d+)@(\d+)([\+|\-]) \(([0-9.+\-eE]+),([0-9.+\-eE]+)\) \[([0-9.+\-eE]+)\|([0-9.+\-eE]+)\] \"(.*)\" (.*)") + sgm_regexp = re.compile("^SG\_ (\w+) (\w+) *: (\d+)\|(\d+)@(\d+)([\+|\-]) \(([0-9.+\-eE]+),([0-9.+\-eE]+)\) \[([0-9.+\-eE]+)\|([0-9.+\-eE]+)\] \"(.*)\" (.*)") + + # A dictionary which maps message ids to tuples ((name, size), signals). + # name is the ASCII name of the message. + # size is the size of the message in bytes. + # signals is a list signals contained in the message. + # signals is a list of DBCSignal in order of increasing start_bit. + self.msgs = {} + + # lookup to bit reverse each byte + self.bits_index = [(i & ~0b111) + ((-i-1) & 0b111) for i in xrange(64)] + + for l in self.txt: + l = l.strip() + + if l.startswith("BO_ "): + # new group + dat = bo_regexp.match(l) + + if dat is None: + print "bad BO", l + name = dat.group(2) + size = int(dat.group(3)) + ids = int(dat.group(1), 0) # could be hex + + self.msgs[ids] = ((name, size), []) + + if l.startswith("SG_ "): + # new signal + dat = sg_regexp.match(l) + go = 0 + if dat is None: + dat = sgm_regexp.match(l) + go = 1 + if dat is None: + print "bad SG", l + + sgname = dat.group(1) + start_bit = int(dat.group(go+2)) + signal_size = int(dat.group(go+3)) + is_little_endian = int(dat.group(go+4))==1 + is_signed = dat.group(go+5)=='-' + factor = int_or_float(dat.group(go+6)) + offset = int_or_float(dat.group(go+7)) + tmin = int_or_float(dat.group(go+8)) + tmax = int_or_float(dat.group(go+9)) + units = dat.group(go+10) + + self.msgs[ids][1].append( + DBCSignal(sgname, start_bit, signal_size, is_little_endian, + is_signed, factor, offset, tmin, tmax, units)) + + for msg in self.msgs.viewvalues(): + msg[1].sort(key=lambda x: x.start_bit) + + def encode(self, msg_id, dd): + """Encode a CAN message using the dbc. + + Inputs: + msg_id: The message ID. + dd: A dictionary mapping signal name to signal data. + """ + # TODO: Stop using bitstring, which is super slow. + msg_def = self.msgs[msg_id] + size = msg_def[0][1] + + bsf = bitstring.Bits(hex="00"*size) + for s in msg_def[1]: + ival = dd.get(s.name) + if ival is not None: + ival = (ival / s.factor) - s.offset + ival = int(round(ival)) + + # should pack this + if s.is_little_endian: + ss = s.start_bit + else: + ss = self.bits_index[s.start_bit] + + + if s.is_signed: + tbs = bitstring.Bits(int=ival, length=s.size) + else: + tbs = bitstring.Bits(uint=ival, length=s.size) + + lpad = bitstring.Bits(bin="0b"+"0"*ss) + rpad = bitstring.Bits(bin="0b"+"0"*(8*size-(ss+s.size))) + tbs = lpad+tbs+rpad + + bsf |= tbs + return bsf.tobytes() + + def decode(self, x, arr=None, debug=False): + """Decode a CAN message using the dbc. + + Inputs: + x: A collection with elements (address, time, data), where address is + the CAN address, time is the bus time, and data is the CAN data as a + hex string. + arr: Optional list of signals which should be decoded and returned. + debug: True to print debugging statements. + + Returns: + A tuple (name, data), where name is the name of the CAN message and data + is the decoded result. If arr is None, data is a dict of properties. + Otherwise data is a list of the same length as arr. + + Returns (None, None) if the message could not be decoded. + """ + + if arr is None: + out = {} + else: + out = [None]*len(arr) + + msg = self.msgs.get(x[0]) + if msg is None: + if x[0] not in self._warned_addresses: + print("WARNING: Unknown message address {}".format(x[0])) + self._warned_addresses.add(x[0]) + return None, None + + name = msg[0][0] + if debug: + print name + + blen = 8*len(x[2]) + + st = x[2].rjust(8, '\x00') + le, be = None, None + + for s in msg[1]: + if arr is not None and s[0] not in arr: + continue + + # big or little endian? + # see http://vi-firmware.openxcplatform.com/en/master/config/bit-numbering.html + if s[3] is False: + ss = self.bits_index[s[1]] + if be is None: + be = struct.unpack(">Q", st)[0] + x2_int = be + data_bit_pos = (blen - (ss + s[2])) + else: + if le is None: + le = struct.unpack("<Q", st)[0] + x2_int = le + ss = s[1] + data_bit_pos = ss + + if data_bit_pos < 0: + continue + ival = (x2_int >> data_bit_pos) & ((1 << (s[2])) - 1) + + if s[4] and (ival & (1<<(s[2]-1))): # signed + ival -= (1<<s[2]) + + # control the offset + ival = (ival * s[5]) + s[6] + #if debug: + # print "%40s %2d %2d %7.2f %s" % (s[0], s[1], s[2], ival, s[-1]) + + if arr is None: + out[s[0]] = ival + else: + out[arr.index(s[0])] = ival + return name, out + diff --git a/common/dbc.py b/common/dbc.py index 518890326b2bca..054da288ef98b5 100755 --- a/common/dbc.py +++ b/common/dbc.py @@ -1,7 +1,6 @@ import re -import os -import struct import bitstring +from binascii import hexlify from collections import namedtuple def int_or_float(s): @@ -17,7 +16,6 @@ def int_or_float(s): class dbc(object): def __init__(self, fn): - self.name, _ = os.path.splitext(os.path.basename(fn)) with open(fn) as f: self.txt = f.read().split("\n") self._warned_addresses = set() @@ -34,8 +32,10 @@ def __init__(self, fn): # signals is a list of DBCSignal in order of increasing start_bit. self.msgs = {} - # lookup to bit reverse each byte - self.bits_index = [(i & ~0b111) + ((-i-1) & 0b111) for i in xrange(64)] + self.bits = [] + for i in range(0, 64, 8): + for j in range(7, -1, -1): + self.bits.append(i+j) for l in self.txt: l = l.strip() @@ -102,7 +102,7 @@ def encode(self, msg_id, dd): if s.is_little_endian: ss = s.start_bit else: - ss = self.bits_index[s.start_bit] + ss = self.bits.index(s.start_bit) if s.is_signed: @@ -135,6 +135,9 @@ def decode(self, x, arr=None, debug=False): Returns (None, None) if the message could not be decoded. """ + def swap_order(d, wsz=4, gsz=2 ): + return "".join(["".join([m[i:i+gsz] for i in range(wsz-gsz,-gsz,-gsz)]) for m in [d[i:i+wsz] for i in range(0,len(d),wsz)]]) + if arr is None: out = {} else: @@ -153,9 +156,6 @@ def decode(self, x, arr=None, debug=False): blen = 8*len(x[2]) - st = x[2].rjust(8, '\x00') - le, be = None, None - for s in msg[1]: if arr is not None and s[0] not in arr: continue @@ -163,18 +163,15 @@ def decode(self, x, arr=None, debug=False): # big or little endian? # see http://vi-firmware.openxcplatform.com/en/master/config/bit-numbering.html if s[3] is False: - ss = self.bits_index[s[1]] - if be is None: - be = struct.unpack(">Q", st)[0] - x2_int = be + ss = self.bits.index(s[1]) + x2_int = int(hexlify(x[2]), 16) data_bit_pos = (blen - (ss + s[2])) else: - if le is None: - le = struct.unpack("<Q", st)[0] - x2_int = le + x2_int = int(swap_order(hexlify(x[2]), 16, 2), 16) ss = s[1] data_bit_pos = ss + if data_bit_pos < 0: continue ival = (x2_int >> data_bit_pos) & ((1 << (s[2])) - 1) diff --git a/common/fingerprints.py b/common/fingerprints.py index 298424940b5131..be19e9f158a520 100644 --- a/common/fingerprints.py +++ b/common/fingerprints.py @@ -61,22 +61,25 @@ def fingerprint(logcan): print "waiting for fingerprint..." candidate_cars = all_known_cars() finger = {} - st = None - while 1: - for a in messaging.drain_sock(logcan, wait_for_one=True): - if st is None: - st = sec_since_boot() - for can in a.can: - if can.src == 0: - finger[can.address] = len(can.dat) - candidate_cars = eliminate_incompatible_cars(can, candidate_cars) + + #st = None + #while 1: + # for a in messaging.drain_sock(logcan, wait_for_one=True): + # if st is None: + # st = sec_since_boot() + # for can in a.can: + # if can.src == 0: + # finger[can.address] = len(can.dat) + # candidate_cars = eliminate_incompatible_cars(can, candidate_cars) # if we only have one car choice and it's been 100ms since we got our first message, exit - if len(candidate_cars) == 1 and st is not None and (sec_since_boot()-st) > 0.1: - break - elif len(candidate_cars) == 0: - print map(hex, finger.keys()) - raise Exception("car doesn't match any fingerprints") + # if len(candidate_cars) == 1 and st is not None and (sec_since_boot()-st) > 0.1: + # break + # elif len(candidate_cars) == 0: + # print map(hex, finger.keys()) + # raise Exception("car doesn't match any fingerprints") - print "fingerprinted", candidate_cars[0] - return (candidate_cars[0], finger) + #print "fingerprinted", candidate_cars[0] + + #return (candidate_cars[0], finger) + return ("TESLA CLASSIC MODEL S", finger) diff --git a/selfdrive/car/__init__.py b/selfdrive/car/__init__.py index e18afe12da9c34..0408398c03c010 100644 --- a/selfdrive/car/__init__.py +++ b/selfdrive/car/__init__.py @@ -1,6 +1,7 @@ from common.fingerprints import fingerprint from .honda.interface import CarInterface as HondaInterface +from .tesla.interface import CarInterface as TeslaInterface try: from .simulator.interface import CarInterface as SimInterface @@ -17,6 +18,7 @@ "ACURA ILX 2016 ACURAWATCH PLUS": HondaInterface, "HONDA ACCORD 2016 TOURING": HondaInterface, "HONDA CR-V 2016 TOURING": HondaInterface, + "TESLA CLASSIC MODEL S": TeslaInterface, "simulator": SimInterface, "simulator2": Sim2Interface diff --git a/selfdrive/car/tesla/__init__.py b/selfdrive/car/tesla/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/selfdrive/car/tesla/can_parser.py b/selfdrive/car/tesla/can_parser.py new file mode 100644 index 00000000000000..bfd2552225acb6 --- /dev/null +++ b/selfdrive/car/tesla/can_parser.py @@ -0,0 +1,131 @@ +import os +import opendbc +from collections import defaultdict + +from selfdrive.car.tesla.teslacan import fix +from common.realtime import sec_since_boot +from common.dbc import dbc + +class CANParser(object): + def __init__(self, dbc_f, signals, checks=[]): + ### input: + # dbc_f : dbc file + # signals : List of tuples (name, address, ival) where + # - name is the signal name. + # - address is the corresponding message address. + # - ival is the initial value. + # checks : List of pairs (address, frequency) where + # - address is the message address of a message for which health should be + # monitored. + # - frequency is the frequency at which health should be monitored. + + self.msgs_ck = set([check[0] for check in checks]) + self.frqs = dict(checks) + self.can_valid = False # start with False CAN assumption + # list of received msg we want to monitor counter and checksum for + # read dbc file + self.can_dbc = dbc(os.path.join(opendbc.DBC_PATH, dbc_f)) + # initialize variables to initial values + self.vl = {} # signal values + self.ts = {} # time stamp recorded in log + self.ct = {} # current time stamp + self.ok = {} # valid message? + self.cn = {} # message counter + self.cn_vl = {} # message counter mismatch value + self.ck = {} # message checksum status + + for _, addr, _ in signals: + self.vl[addr] = {} + self.ts[addr] = 0 + self.ct[addr] = sec_since_boot() + self.ok[addr] = False + self.cn[addr] = 0 + self.cn_vl[addr] = 0 + self.ck[addr] = False + + for name, addr, ival in signals: + self.vl[addr][name] = ival + + self._msgs = [s[1] for s in signals] + self._sgs = [s[0] for s in signals] + + self._message_indices = defaultdict(list) + for i, x in enumerate(self._msgs): + self._message_indices[x].append(i) + + def update_can(self, can_recv): + msgs_upd = [] + cn_vl_max = 5 # no more than 5 wrong counter checks + + self.sec_since_boot_cached = sec_since_boot() + + # we are subscribing to PID_XXX, else data from USB + for msg, ts, cdat, _ in can_recv: + idxs = self._message_indices[msg] + if idxs: + msgs_upd.append(msg) + # read the entire message + out = self.can_dbc.decode((msg, 0, cdat))[1] + # checksum check + self.ck[msg] = True + if "CHECKSUM" in out.keys() and msg in self.msgs_ck: + # remove checksum (half byte) + ck_portion = cdat[:-1] + chr(ord(cdat[-1]) & 0xF0) + # recalculate checksum + msg_vl = fix(ck_portion, msg) + # compare recalculated vs received checksum + if msg_vl != cdat: + print "CHECKSUM FAIL: " + hex(msg) + self.ck[msg] = False + self.ok[msg] = False + # counter check + cn = 0 + if "COUNTER" in out.keys(): + cn = out["COUNTER"] + # check counter validity if it's a relevant message + if cn != ((self.cn[msg] + 1) % 4) and msg in self.msgs_ck and "COUNTER" in out.keys(): + #print hex(msg), "FAILED COUNTER!" + self.cn_vl[msg] += 1 # counter check failed + else: + self.cn_vl[msg] -= 1 # counter check passed + # message status is invalid if we received too many wrong counter values + if self.cn_vl[msg] >= cn_vl_max: + print "COUNTER WRONG: " + hex(msg) + self.ok[msg] = False + + # update msg time stamps and counter value + self.ts[msg] = ts + self.ct[msg] = self.sec_since_boot_cached + self.cn[msg] = cn + self.cn_vl[msg] = min(max(self.cn_vl[msg], 0), cn_vl_max) + + # set msg valid status if checksum is good and wrong counter counter is zero + if self.ck[msg] and self.cn_vl[msg] == 0: + self.ok[msg] = True + + #robertc (from JCT) TEMPORARY remove counter and checksum check + #robertc (from JCT) This needs to be re-done before openpilot is activated to steer the car + self.ok[msg] = True + + # update value of signals in the + for ii in idxs: + sg = self._sgs[ii] + self.vl[msg][sg] = out[sg] + + # for each message, check if it's too long since last time we received it + self._check_dead_msgs() + + # assess overall can validity: if there is one relevant message invalid, then set can validity flag to False + self.can_valid = True + if False in self.ok.values(): + #print "CAN INVALID!" + self.can_valid = False + + return msgs_upd + + def _check_dead_msgs(self): + ### input: + ## simple stuff for now: msg is not valid if a message isn't received for 10 consecutive steps + for msg in set(self._msgs): + if msg in self.msgs_ck and self.sec_since_boot_cached - self.ct[msg] > 10./self.frqs[msg]: + self.ok[msg] = False diff --git a/selfdrive/car/tesla/carcontroller.py b/selfdrive/car/tesla/carcontroller.py new file mode 100644 index 00000000000000..4c5cdfdbee0786 --- /dev/null +++ b/selfdrive/car/tesla/carcontroller.py @@ -0,0 +1,209 @@ +from collections import namedtuple + +import common.numpy_fast as np +from common.numpy_fast import clip, interp +from common.realtime import sec_since_boot + +from selfdrive.config import CruiseButtons +from selfdrive.boardd.boardd import can_list_to_can_capnp +from selfdrive.controls.lib.drive_helpers import rate_limit + +from . import teslacan + +def actuator_hystereses(final_brake, braking, brake_steady, v_ego): + # hyst params... TODO: move these to VehicleParams + brake_hyst_on = 0.1 # to activate brakes exceed this value + brake_hyst_off = 0.005 # to deactivate brakes below this value + brake_hyst_gap = 0.01 # don't change brake command for small ocilalitons within this value + + final_brake = 0. + braking = False + brake_steady = 0. + + + #*** histeresys logic to avoid brake blinking. go above 0.1 to trigger + #if (final_brake < brake_hyst_on and not braking) or final_brake < brake_hyst_off: + # final_brake = 0. + #braking = final_brake > 0. + + # for small brake oscillations within brake_hyst_gap, don't change the brake command + #if final_brake == 0.: + # brake_steady = 0. + #elif final_brake > brake_steady + brake_hyst_gap: + # brake_steady = final_brake - brake_hyst_gap + #elif final_brake < brake_steady - brake_hyst_gap: + # brake_steady = final_brake + brake_hyst_gap + #final_brake = brake_steady + + #if not civic: + # brake_on_offset_v = [.25, .15] # min brake command on brake activation. below this no decel is perceived + # brake_on_offset_bp = [15., 30.] # offset changes VS speed to not have too abrupt decels at high speeds + # offset the brake command for threshold in the brake system. no brake torque perceived below it + # brake_on_offset = interp(v_ego, brake_on_offset_bp, brake_on_offset_v) + # brake_offset = brake_on_offset - brake_hyst_on + # if final_brake > 0.0: + # final_brake += brake_offset + + return final_brake, braking, brake_steady + +class AH: + #[alert_idx, value] + # See dbc files for info on values" + NONE = [0, 0] + FCW = [1, 0x8] + STEER = [2, 1] + BRAKE_PRESSED = [3, 10] + GEAR_NOT_D = [4, 6] + SEATBELT = [5, 5] + SPEED_TOO_HIGH = [6, 8] + +def process_hud_alert(hud_alert): + # initialize to no alert + fcw_display = 0 + steer_required = 0 + acc_alert = 0 + if hud_alert == AH.NONE: # no alert + pass + elif hud_alert == AH.FCW: # FCW + fcw_display = hud_alert[1] + elif hud_alert == AH.STEER: # STEER + steer_required = hud_alert[1] + else: # any other ACC alert + acc_alert = hud_alert[1] + + return fcw_display, steer_required, acc_alert + +#HUDData = namedtuple("HUDData", +# ["pcm_accel", "v_cruise", "X2", "car", "X4", "X5", +# "lanes", "beep", "X8", "chime", "acc_alert"]) + +class CarController(object): + def __init__(self): + self.braking = False + self.brake_steady = 0. + self.final_brake_last = 0. + + # redundant safety check with the board + self.controls_allowed = False + + def update(self, sendcan, enabled, CS, frame, final_gas, final_brake, final_steer, \ + pcm_speed, pcm_override, pcm_cancel_cmd, pcm_accel, \ + hud_v_cruise, hud_show_lanes, hud_show_car, hud_alert, \ + snd_beep, snd_chime): + """ Controls thread """ + + # *** apply brake hysteresis *** + #final_brake, self.braking, self.brake_steady = actuator_hystereses(final_brake, self.braking, self.brake_steady, CS.v_ego) + + # *** no output if not enabled *** + if not enabled: + final_gas = 0. + final_brake = 0. + final_steer = 0. + + #Override for now + final_gas = 0. + final_brake = 0. + + # send pcm acc cancel cmd if drive is disabled but pcm is still on, or if the system can't be activated + #if CS.pcm_acc_status: + # pcm_cancel_cmd = True + + # *** rate limit after the enable check *** + #final_brake = rate_limit(final_brake, self.final_brake_last, -2., 1./100) + self.final_brake_last = final_brake + + # vehicle hud display, wait for one update from 10Hz 0x304 msg + #TODO: use enum!! + if hud_show_lanes: + hud_lanes = 0x04 + else: + hud_lanes = 0x00 + + # TODO: factor this out better + if enabled: + if hud_show_car: + hud_car = 0xe0 + else: + hud_car = 0xd0 + else: + hud_car = 0xc0 + + # **** process the car messages **** + + # *** compute control surfaces *** + tt = sec_since_boot() + GAS_MAX = 1004 + BRAKE_MAX = 1024/4 + SIGNAL_STEER_MAX = 16384 + GAS_OFFSET = 328 + + if CS.v_ego < 16.7: #60.12 km/h divided by 3.6 = 16.7 meter per sec + USER_STEER_MAX = 180 # 180 degrees + elif CS.v_ego < 28: # 100.8 km/h + USER_STEER_MAX = 900 # 90 degrees + else: + USER_STEER_MAX = 300 # 30 degrees + + apply_gas = 0 #int(clip(final_gas*GAS_MAX, 0, GAS_MAX-1)) + apply_brake = 0 #int(clip(final_brake*BRAKE_MAX, 0, BRAKE_MAX-1)) + + #final_steer is between -1 and 1 + if not enabled: + final_steer = 0 + else: + # steer torque is converted back to CAN reference (positive when steering right) + final_steer = final_steer * -1 + apply_steer = int(clip((final_steer * 100) + SIGNAL_STEER_MAX - (CS.angle_steers * 10), SIGNAL_STEER_MAX - USER_STEER_MAX, SIGNAL_STEER_MAX + USER_STEER_MAX)) + + # no gas if you are hitting the brake or the user is + if apply_gas > 0 and (apply_brake != 0 or CS.brake_pressed): + print "CANCELLING ACCELERATOR", apply_brake + apply_gas = 0 + + # no computer brake if the gas is being pressed + if CS.car_gas > 0 and apply_brake != 0: + print "CANCELLING BRAKE" + apply_brake = 0 + + # any other cp.vl[0x18F]['STEER_STATUS'] is common and can happen during user override. sending 0 torque to avoid EPS sending error 5 + if CS.steer_not_allowed: + print "STEER ALERT, TORQUE INHIBITED" + apply_steer = 0 + + # *** entry into controls state *** + #if (CS.prev_cruise_buttons == 2) and CS.cruise_buttons == 0 and not self.controls_allowed: + if CS.cruise_buttons == 2 and not self.controls_allowed: + print "CONTROLS ARE LIVE" + self.controls_allowed = True + + # *** exit from controls state on cancel, gas, or brake *** + if (CS.cruise_buttons == 1 or CS.brake_pressed ) and self.controls_allowed: + print "CONTROLS ARE DEAD (Cruise CANCEL or BRAKE pressed" + self.controls_allowed = False + + # *** controls fail on steer error, brake error, or invalid can *** + if CS.steer_error: + print "STEER ERROR" + self.controls_allowed = False + + if CS.brake_error: + print "BRAKE ERROR" + self.controls_allowed = False + + #if not CS.can_valid and self.controls_allowed: # 200 ms + # print "CAN INVALID" + # self.controls_allowed = False + + # Send CAN commands. + can_sends = [] + + # Send steering command. + if (frame % 5) == 0: + idx = (frame / 5) % 16 + enable_steer_control = (not CS.right_blinker_on and not CS.left_blinker_on and self.controls_allowed and enabled) + can_sends.append(teslacan.create_steering_control(apply_steer, idx, enable_steer_control)) + #if idx == 0: print "carcontroller.py: CS.angle_steers: %.2f CS.steer_override: %.2f final_steer: %.2f apply_steer: %.2f" % (CS.angle_steers, CS.steer_override, final_steer, apply_steer) + #print "carcontroller.py: apply_steer = " + str(apply_steer) + ", idx = " + str(idx) + sendcan.send(can_list_to_can_capnp(can_sends, msgtype='sendcan').to_bytes()) + diff --git a/selfdrive/car/tesla/carstate.py b/selfdrive/car/tesla/carstate.py new file mode 100644 index 00000000000000..1689f5fd3f9167 --- /dev/null +++ b/selfdrive/car/tesla/carstate.py @@ -0,0 +1,254 @@ +import os +import time + +import common.numpy_fast as np +from common.realtime import sec_since_boot + +import selfdrive.messaging as messaging + +from common.realtime import sec_since_boot + +from selfdrive.car.tesla.can_parser import CANParser +from selfdrive.can.parser import CANParser as CANParserC + +from selfdrive.services import service_list + +import zmq + +NEW_CAN = os.getenv("OLD_CAN") is None +NEW_CAN = False + +def get_can_parser(CP): + # this function generates lists for signal, messages and initial values + if CP.carFingerprint == "TESLA CLASSIC MODEL S": + dbc_f = 'tesla_can.dbc' + signals = [ + #("XMISSION_SPEED", 0x158, 0), + #("WHEEL_SPEED_FL", 0x1d0, 0), + #("WHEEL_SPEED_FR", 0x1d0, 0), + #("WHEEL_SPEED_RL", 0x1d0, 0), + #("WHEEL_SPEED_RR", 0x1d0, 0), #robertc (from JCT) -- might add later + #("STEER_ANGLE", 0x156, 0), + #("STEER_TORQUE_SENSOR", 0x18f, 0), + #("MCU_gpsHDOP", 760, 0), + ("MCU_gpsVehicleHeading", 760, 0), + ("MCU_gpsVehicleSpeed", 760, 0), + #("MCU_mppSpeedLimit", 760, 0), + #("MCU_speedLimitUnits", 760, 0), + #("MCU_userSpeedOffset", 760, 0), + #("MCU_userSpeedOffsetUnits", 760, 0), + #("MCU_clusterBrightnessLevel", 904, 0), + #("LgtSens_Tunnel", 643, 0), + #("LgtSens_Night", 643, 0), + #("LgtSens_Twlgt", 643, 0), + ("MCU_gpsAccuracy", 984, 0), + #("StW_Angl", 3, 0), + #("MC_STW_ANGL_STAT", 3, 0), + #("SECOND", 792,0), + #("MINUTE", 792,0), + ("MCU_latitude", 984, 0), + ("MCU_longitude", 984, 0), + ("DI_vehicleSpeed", 280, 0), + ("StW_AnglHP", 14, 0), + ("EPAS_torsionBarTorque", 880, 0), # Used in interface.py + ("EPAS_eacStatus", 880, 0), + ("EPAS_handsOnLevel", 880, 0), + ("DI_gear", 280, 3), + ("DOOR_STATE_FL", 792, 1), + ("DOOR_STATE_FR", 792, 1), + ("DOOR_STATE_RL", 792, 1), + ("DOOR_STATE_RR", 792, 1), + ("DI_stateCounter", 872, 0), + ("GTW_driverPresent", 840, 0), + ("DI_brakePedal", 280, 0), + ("SpdCtrlLvr_Stat", 69, 0), + #("EPAS_eacErrorCode", 880, 5), + #("ESP_brakeLamp", 309, 0), + #("ESP_espOffLamp", 309, 0), + #("ESP_tcOffLamp", 309, 0), + #("ESP_tcLampFlash", 309, 0), + #("ESP_espFaultLamp", 309, 0), + ("DI_cruiseSet", 872, 0), + ("DI_cruiseState", 872, 0), + #("DI_pedalPos", 264, 0), + ("VSL_Enbl_Rq", 69, 0), # Used in interface.py + ("TSL_RND_Posn_StW",109 , 0), + ("TSL_P_Psd_StW",109 , 0), + ("TurnIndLvr_Stat", 69, 0), + #("GTW_statusCounter", 840, 0), + ("DI_motorRPM", 264, 0) + ] + # robertc: all frequencies must be integers with new C CAN parser (2.5 => 2) + checks = [ + (3, 175), #JCT Actual message freq is 35 Hz (0.0286 sec) + (14, 200), #JCT Actual message freq is 40 Hz (0.025 sec) + (69, 17), #JCT Actual message freq is 3.5 Hz (0.285 sec) + (109, 175), #JCT Actual message freq is 35 Hz (0.0286 sec) + (264, 59), #JCT Actual message freq is 11.8 Hz (0.084 sec) + (280, 18), #JCT Actual message freq is 3.7 Hz (0.275 sec) + (309, 2), #JCT Actual message freq is 0.487 Hz (2.05 sec) + (760, 2), #JCT Actual message freq is 0.487 Hz (2.05 sec) + (792, 16), #JCT Actual message freq is 3.3 Hz (0.3 sec) + (840, 2), #JCT Actual message freq is 0.5 Hz (2 sec) + (872, 5), #JCT Actual message freq is 1 Hz (1 sec) + (643, 5), #JCT Actual message freq is 1.3 Hz (0.76 sec) + (880, 5), #JCT Actual message freq is 1.3 Hz (0.76 sec) + (904, 5), #JCT Actual message freq is 1.3 Hz (0.76 sec) + (984, 5), #JCT Actual message freq is 1.3 Hz (0.76 sec) + ] + + # add gas interceptor reading if we are using it + # robertc: What is this exactly? + #if CP.enableGas: + # signals.append(("INTERCEPTOR_GAS", 0x201, 0)) + # checks.append((0x201, 50)) + + if NEW_CAN: + return CANParserC(os.path.splitext(dbc_f)[0], signals, checks, 0) + else: + return CANParser(dbc_f, signals, checks) + +class CarState(object): + def __init__(self, CP, logcan): + # robertc: got rid of all the Honda/Acura-specific stuff + self.brake_only = CP.enableCruise + self.CP = CP + + # initialize can parser + self.cp = get_can_parser(CP) + + self.user_gas, self.user_gas_pressed = 0., 0 + + self.cruise_buttons = 0 + self.cruise_setting = 0 + self.blinker_on = 0 + + self.log_interval = sec_since_boot() + + #self.prev_user_brightness = 50 + #self.user_brightness = 50 + #self.display_brightness = 50 + #self.day_or_night = 0 + + self.left_blinker_on = 0 + self.right_blinker_on = 0 + + context = zmq.Context() + self.gps_sock = messaging.pub_sock(context, service_list['gpsLocationExternal'].port) + + # TODO: actually make this work + self.a_ego = 0. + + def update(self, can_pub_main=None): + cp = self.cp + + if NEW_CAN: + cp.update(int(sec_since_boot() * 1e9), False) + else: + cp.update_can(can_pub_main) + + # copy can_valid + self.can_valid = cp.can_valid + + # car params + v_weight_v = [0., 1. ] # don't trust smooth speed at low values to avoid premature zero snapping + v_weight_bp = [1., 6.] # smooth blending, below ~0.6m/s the smooth speed snaps to zero + + # update prevs, update must run once per loop + self.prev_cruise_buttons = self.cruise_buttons + self.prev_cruise_setting = self.cruise_setting + self.prev_blinker_on = self.blinker_on + + self.prev_left_blinker_on = self.left_blinker_on + self.prev_right_blinker_on = self.right_blinker_on + + self.rpm = cp.vl[264]['DI_motorRPM'] + + # ******************* parse out can ******************* + self.door_all_closed = not any([cp.vl[792]['DOOR_STATE_FL'], cp.vl[792]['DOOR_STATE_FR'], + cp.vl[792]['DOOR_STATE_RL'], cp.vl[792]['DOOR_STATE_RR']]) #JCT + self.seatbelt = cp.vl[840]['GTW_driverPresent'] + self.steer_error = cp.vl[880]['EPAS_eacStatus'] not in [2,1] + self.steer_not_allowed = 0 #cp.vl[880]['EPAS_eacErrorCode'] != 0 + self.brake_error = 0 #NOT WORKINGcp.vl[309]['ESP_brakeLamp'] #JCT + # JCT More ESP errors available, these needs to be added once car steers on its own to disable / alert driver + # JCT More ESP errors available, these needs to be added once car steers on its own to disable / alert driver + self.esp_disabled = 0 #NEED TO CORRECT DBC cp.vl[309]['ESP_espOffLamp'] or cp.vl[309]['ESP_tcOffLamp'] or cp.vl[309]['ESP_tcLampFlash'] or cp.vl[309]['ESP_espFaultLamp'] #JCT + # calc best v_ego estimate, by averaging two opposite corners + self.v_wheel = 0 #JCT + self.v_weight = 0 #JCT + self.v_ego = (cp.vl[280]['DI_vehicleSpeed'])*1.609/3.6 #JCT MPH_TO_MS. Tesla is in MPH, v_ego is expected in M/S + self.user_gas = 0 #for now + self.user_gas_pressed = 0 #for now + self.angle_steers = -(cp.vl[14]['StW_AnglHP']) #JCT polarity reversed from Honda/Acura + self.gear = 0 + #JCT removed for now, will be used for cruise ON/OFF aka yellow led: self.cruise_setting = cp.vl[69]['VSL_Enbl_Rq'] + self.cruise_buttons = cp.vl[69]['SpdCtrlLvr_Stat'] + self.main_on = 1 #0 #JCT Need to find the Cruise ON/OFF switch that illuminate the yellow led on the cruise stalk + self.gear_shifter = cp.vl[280]['DI_gear'] + self.gear_shifter_valid = (cp.vl[109]['TSL_RND_Posn_StW'] == 0) and (cp.vl[109]['TSL_P_Psd_StW'] ==0) and (cp.vl[280]['DI_gear'] ==4) #JCT PND stalk idle and Park button not pressed and in DRIVE + self.blinker_on = 0 #cp.vl[69]['TurnIndLvr_Stat'] in [1,2] + self.left_blinker_on = cp.vl[69]['TurnIndLvr_Stat'] == 1 + self.right_blinker_on = cp.vl[69]['TurnIndLvr_Stat'] == 2 + self.car_gas = 0 #cp.vl[264]['DI_pedalPos'] + self.steer_override = abs(cp.vl[880]['EPAS_handsOnLevel']) > 0 #JCT need to be tested and adjusted. 8=38.7% of range as george uses. Range -20.4 to 20.4 Nm + self.brake_pressed = cp.vl[280]['DI_brakePedal'] + self.user_brake = cp.vl[280]['DI_brakePedal'] + self.standstill = cp.vl[280]['DI_vehicleSpeed'] == 0 + self.v_cruise_pcm = cp.vl[872]['DI_cruiseSet'] + self.pcm_acc_status = cp.vl[872]['DI_cruiseState'] + self.pedal_gas = 0 #cp.vl[264]['DI_pedalPos'] + self.hud_lead = 0 #JCT TBD + self.counter_pcm = cp.vl[872]['DI_stateCounter'] + #self.display_brightness = cp.vl[904]['MCU_clusterBrightnessLevel'] + #self.day_or_night = cp.vl[643]['LgtSens_Night'] + #self.tunnel = cp.vl[643]['LgtSens_Tunnel'] + #self.LgtSens_Twlgt = cp.vl[643]['LgtSens_Twlgt'] + + + ## JCT Action every 1 sec + if (sec_since_boot() - self.log_interval) > 1: + + # GPS Addon + dat = messaging.new_message() + dat.init('gpsLocation') + dat.gpsLocation.source = 2 # getting gps from car (on CAN bus) + dat.gpsLocation.flags = 0x1D # see line 89 of https://source.android.com/devices/halref/gps_8h_source.html#l00531 + dat.gpsLocation.latitude = cp.vl[984]['MCU_latitude'] # in degrees + dat.gpsLocation.longitude = cp.vl[984]['MCU_longitude'] # in degrees + dat.gpsLocation.altitude = 0 # Flags += 2 once available (will give flags=0x1F) in meters above the WGS 84 reference ellipsoid + dat.gpsLocation.speed = (cp.vl[760]['MCU_gpsVehicleSpeed'])*1./3.6 # in meters per second + dat.gpsLocation.bearing = cp.vl[760]['MCU_gpsVehicleHeading'] # in degrees + dat.gpsLocation.accuracy = cp.vl[984]['MCU_gpsAccuracy'] # in meters + dat.gpsLocation.timestamp = int(time.time()) # Milliseconds since January 1, 1970. (GPS time N/A for now) + self.gps_sock.send(dat.to_bytes()) + + #if not self.day_or_night and not self.tunnel: + # self.user_brightness = self.display_brightness if self.display_brightness > 50 else 50 # During day anything bellow 50% is too dim + #else: + # self.user_brightness = self.display_brightness # Full brightness range (0-100%) if night or tunnel + # + #if self.user_brightness != self.prev_user_brightness: + # f2 = open('/sdcard/brightness', 'w') + # s = "%3.1f" % (self.user_brightness) + # f2.write(s) + # f2.close() + + #self.prev_user_brightness = self.user_brightness + + #if self.gear_shifter == 4: + # gear = 'D' + #elif self.gear_shifter == 1: + # gear = 'P' + #elif self.gear_shifter == 2: + # gear = 'R' + #else: + # gear = '?' + #f = open('/sdcard/candiac.log', 'a') + #s = "SPD=%3.1f RPM=%5d ANGLE=%5.1f GEAR=%s DOORS=%2d Standstill=%d Gas=%3.1f Brake=%1d GearValid=%1d D=%3.1f%% Night=%d Tunnel=%d Twlgt=%d\n" % (self.v_ego*3.6, self.rpm, self.angle_steers, gear, self.door_all_closed, self.standstill, self.car_gas, self.user_brake, self.gear_shifter_valid, self.display_brightness, self.day_or_night, self.tunnel, self.LgtSens_Twlgt) + #f.write(s) + #f.close() + + self.log_interval = sec_since_boot() + + diff --git a/selfdrive/car/tesla/interface.py b/selfdrive/car/tesla/interface.py new file mode 100644 index 00000000000000..f63e0c4914f8bd --- /dev/null +++ b/selfdrive/car/tesla/interface.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python +import os +import time +import common.numpy_fast as np + +from selfdrive.config import Conversions as CV +from .carstate import CarState +from .carcontroller import CarController, AH +from selfdrive.boardd.boardd import can_capnp_to_can_list + +from cereal import car + +from selfdrive.services import service_list +import selfdrive.messaging as messaging + +NEW_CAN = os.getenv("OLD_CAN") is None +NEW_CAN = False + +# Car button codes +#JCT from dbc: VAL_ 69 SpdCtrlLvr_Stat 32 "DN_1ST" 16 "UP_1ST" 8 "DN_2ND" 4 "UP_2ND" 2 "RWD" 1 "FWD" 0 "IDLE" ; +class CruiseButtons: + DN_1ST = 32 # Set / remove 1 km/h if already set + UP_1ST = 16 # Set / add 1 km/h if already set + DN_2ND = 8 # Remove 10 km/h if already set + UP_2ND = 4 # Add 10 km/h if already set + RWD = 2 # Resume (twice would eventually be enable openpilot) + FWD = 1 # Cancel + IDLE = 0 + +#car chimes: enumeration from dbc file. Chimes are for alerts and warnings +class CM: + MUTE = 0 + SINGLE = 3 + DOUBLE = 4 + REPEATED = 1 + CONTINUOUS = 2 + +#car beepss: enumeration from dbc file. Beeps are for activ and deactiv +class BP: + MUTE = 0 + SINGLE = 3 + TRIPLE = 2 + REPEATED = 1 + +class AH: + #[alert_idx, value] + # See dbc files for info on values" + NONE = [0, 0] + FCW = [1, 0x8] + STEER = [2, 1] + BRAKE_PRESSED = [3, 10] + GEAR_NOT_D = [4, 6] + SEATBELT = [5, 5] + SPEED_TOO_HIGH = [6, 8] + +class CarInterface(object): + def __init__(self, CP, logcan, sendcan=None): + self.logcan = logcan + self.CP = CP + + self.frame = 0 + self.can_invalid_count = 0 + + # *** init the major players *** + self.CS = CarState(CP, self.logcan) + + # sending if read only is False + if sendcan is not None: + self.sendcan = sendcan + self.CC = CarController() + + @staticmethod + def get_params(candidate, fingerprint): + + # pedal + brake_only = True + + ret = car.CarParams.new_message() + + ret.carName = "tesla" + ret.radarName = "bosch" + ret.carFingerprint = candidate + + ret.enableSteer = True + ret.enableBrake = True + ret.enableGas = not brake_only + ret.enableCruise = brake_only + + if candidate == "TESLA CLASSIC MODEL S": + ret.wheelBase = 2.959 + ret.slipFactor = 0.000713841 + ret.steerRatio = 12 + #ret.steerKp, ret.steerKi = 2.5, 0.15 # 2017-07-11am was at 2.5/0.25 + ret.steerKp, ret.steerKi = 1.25, 0.2 # 2017-07-11am was at 2.5/0.25 + else: + raise ValueError("unsupported car %s" % candidate) + + return ret + + # returns a car.CarState + def update(self): + # ******************* do can recv ******************* + can_pub_main = [] + canMonoTimes = [] + + if NEW_CAN: + self.CS.update(can_pub_main) + else: + for a in messaging.drain_sock(self.logcan): + canMonoTimes.append(a.logMonoTime) + can_pub_main.extend(can_capnp_to_can_list(a.can, [0,2])) + self.CS.update(can_pub_main) + + # create message + ret = car.CarState.new_message() + + # speeds + ret.vEgo = self.CS.v_ego + ret.wheelSpeeds.fl = self.CS.v_ego #JCT TODO while we find these self.CS.cp.vl[0x1D0]['WHEEL_SPEED_FL'] + ret.wheelSpeeds.fr = self.CS.v_ego #JCT TODO while we find these self.CS.cp.vl[0x1D0]['WHEEL_SPEED_FR'] + ret.wheelSpeeds.rl = self.CS.v_ego #JCT TODO while we find these self.CS.cp.vl[0x1D0]['WHEEL_SPEED_RL'] + ret.wheelSpeeds.rr = self.CS.v_ego #JCT TODO while we find these self.CS.cp.vl[0x1D0]['WHEEL_SPEED_RR'] + + # gas pedal + ret.gas = 0 #self.CS.car_gas / 100.0 #JCT Tesla scaling is 0-100, Honda was 0-255 + if not self.CP.enableGas: + ret.gasPressed = self.CS.pedal_gas > 0 + else: + ret.gasPressed = self.CS.user_gas_pressed + + # brake pedal + ret.brake = self.CS.user_brake + ret.brakePressed = self.CS.brake_pressed != 0 + + # steering wheel + # TODO: units + ret.steeringAngle = self.CS.angle_steers #JCT degree range -819.2|819 + ret.steeringTorque = self.CS.cp.vl[880]['EPAS_torsionBarTorque'] + ret.steeringPressed = self.CS.steer_override + + # cruise state + #ret.cruiseState.enabled = self.CS.pcm_acc_status == 2 + #ret.cruiseState.enabled = self.CS.cruise_buttons == 2 #CruiseButtons.RWD + ret.cruiseState.enabled = True #(self.CS.cp.vl[69]['VSL_Enbl_Rq'] == 0) #cruise light off on stalk + #print "VSL_Enbl_Rq: " + str(self.CS.cp.vl[69]['VSL_Enbl_Rq']) + #print "cruiseState.enabled: " + str(ret.cruiseState.enabled) + ret.cruiseState.speed = self.CS.v_cruise_pcm * CV.KPH_TO_MS + #ret.right_blinker = self.CS.right_blinker_on + #ret.left_blinker = self.CS.left_blinker_on + # TODO: for now, set to same as cruise light, check if car is in D? + ret.cruiseState.available = True #(self.CS.cp.vl[69]['VSL_Enbl_Rq'] == 0) + + # TODO: button presses + buttonEvents = [] + + if self.CS.left_blinker_on != self.CS.prev_left_blinker_on: + be = car.CarState.ButtonEvent.new_message() + be.type = 'leftBlinker' + be.pressed = self.CS.left_blinker_on != 0 + buttonEvents.append(be) + + if self.CS.right_blinker_on != self.CS.prev_right_blinker_on: + be = car.CarState.ButtonEvent.new_message() + be.type = 'rightBlinker' + be.pressed = self.CS.right_blinker_on != 0 + buttonEvents.append(be) + + if self.CS.cruise_buttons != self.CS.prev_cruise_buttons: + #print "Cruise Buttons Debug: " + str(self.CS.cruise_buttons) + " Prev: " + str(self.CS.prev_cruise_buttons) + be = car.CarState.ButtonEvent.new_message() + be.type = 'unknown' + if self.CS.cruise_buttons != 0: + be.pressed = True + but = self.CS.cruise_buttons + else: + be.pressed = False + but = self.CS.prev_cruise_buttons + if but == CruiseButtons.UP_1ST: + be.type = 'accelCruise' + elif but == CruiseButtons.DN_1ST: + be.type = 'decelCruise' + elif but == CruiseButtons.FWD: + be.type = 'cancel' + elif but == 2: + be.type = 'altButton1' + #print "altButton1" + elif but == CruiseButtons.IDLE: + be.type = 'altButton3' + buttonEvents.append(be) + + # JCT TODO This will be used to select openpilot or just regular cruise control via cruise ON/OFF button + # on cruise OFF (yellow led off) openpilot will be used if we can make it work even with led off, we'll see... + #if self.CS.cruise_setting != self.CS.prev_cruise_setting: + # be = car.CarState.ButtonEvent.new_message() + # be.type = 'unknown' + # if self.CS.cruise_setting != 0: + # be.pressed = True + # but = self.CS.cruise_setting + # else: + # be.pressed = False + # but = self.CS.prev_cruise_setting + # if but == 1: + # be.type = 'altButton1' + # # TODO: more buttons? + # buttonEvents.append(be) + ret.buttonEvents = buttonEvents + + # errors + # TODO: I don't like the way capnp does enums + # These strings aren't checked at compile time + errors = [] + if not self.CS.can_valid: + self.can_invalid_count =0#+= 1 + #print "interface.py line 165, can_invalid_count = " + str(self.can_invalid_count) + #if self.can_invalid_count >= 5: + # errors.append('commIssue') + else: + self.can_invalid_count = 0 + if self.CS.steer_error: + errors.append('steerUnavailable') + elif self.CS.steer_not_allowed: + errors.append('steerTempUnavailable') + if self.CS.brake_error: + errors.append('brakeUnavailable') + if not self.CS.gear_shifter_valid: + errors.append('wrongGear') + if not self.CS.door_all_closed: + errors.append('doorOpen') + if not self.CS.seatbelt: + errors.append('seatbeltNotLatched') + if self.CS.esp_disabled: + errors.append('espDisabled') + if not self.CS.main_on: + errors.append('wrongCarMode') + if self.CS.gear_shifter == 2: + errors.append('reverseGear') + + ret.errors = errors + ret.canMonoTimes = canMonoTimes + + # cast to reader so it can't be modified + #print ret + return ret.as_reader() + + # pass in a car.CarControl + # to be called @ 100hz + def apply(self, c): + #print c + + if c.hudControl.speedVisible: + hud_v_cruise = c.hudControl.setSpeed * CV.MS_TO_KPH + else: + hud_v_cruise = 255 + + hud_alert = { + "none": AH.NONE, + "fcw": AH.FCW, + "steerRequired": AH.STEER, + "brakePressed": AH.BRAKE_PRESSED, + "wrongGear": AH.GEAR_NOT_D, + "seatbeltUnbuckled": AH.SEATBELT, + "speedTooHigh": AH.SPEED_TOO_HIGH}[str(c.hudControl.visualAlert)] + + snd_beep, snd_chime = { + "none": (BP.MUTE, CM.MUTE), + "beepSingle": (BP.SINGLE, CM.MUTE), + "beepTriple": (BP.TRIPLE, CM.MUTE), + "beepRepeated": (BP.REPEATED, CM.MUTE), + "chimeSingle": (BP.MUTE, CM.SINGLE), + "chimeDouble": (BP.MUTE, CM.DOUBLE), + "chimeRepeated": (BP.MUTE, CM.REPEATED), + "chimeContinuous": (BP.MUTE, CM.CONTINUOUS)}[str(c.hudControl.audibleAlert)] + + pcm_accel = int(np.clip(c.cruiseControl.accelOverride/1.4,0,1)*0xc6) + + read_only = False + # sending if read only is False + if not read_only: + self.CC.update(self.sendcan, c.enabled, self.CS, self.frame, \ + c.gas, c.brake, c.steeringTorque, \ + c.cruiseControl.speedOverride, \ + c.cruiseControl.override, \ + c.cruiseControl.cancel, \ + pcm_accel, \ + hud_v_cruise, c.hudControl.lanesVisible, \ + hud_show_car = c.hudControl.leadVisible, \ + hud_alert = hud_alert, \ + snd_beep = snd_beep, \ + snd_chime = snd_chime) + + self.frame += 1 + return not (c.enabled and not self.CC.controls_allowed) + diff --git a/selfdrive/car/tesla/teslacan.py b/selfdrive/car/tesla/teslacan.py new file mode 100644 index 00000000000000..64d2b87e6c2efd --- /dev/null +++ b/selfdrive/car/tesla/teslacan.py @@ -0,0 +1,113 @@ +import struct + +import common.numpy_fast as np +from selfdrive.config import Conversions as CV + + +# *** Honda specific *** +def can_cksum(mm): + s = 0 + for c in mm: + c = ord(c) + s += (c>>4) + s += c & 0xF + s = 8-s + s %= 0x10 + return s + +def fix(msg, addr): + msg2 = msg[0:-1] + chr(ord(msg[-1]) | can_cksum(struct.pack("I", addr)+msg)) + return msg2 + +def make_can_msg(addr, dat, idx, alt): + if idx is not None: + dat += chr(idx << 4) + dat = fix(dat, addr) + return [addr, 0, dat, alt] + +def create_brake_command(apply_brake, pcm_override, pcm_cancel_cmd, chime, idx): + """Creates a CAN message for the Honda DBC BRAKE_COMMAND.""" + pump_on = apply_brake > 0 + brakelights = apply_brake > 0 + brake_rq = apply_brake > 0 + + pcm_fault_cmd = False + amount = struct.pack("!H", (apply_brake << 6) + pump_on) + msg = amount + struct.pack("BBB", (pcm_override << 4) | + (pcm_fault_cmd << 2) | + (pcm_cancel_cmd << 1) | brake_rq, 0x80, + brakelights << 7) + chr(chime) + "\x00" + return make_can_msg(0x1fa, msg, idx, 0) + +def create_gas_command(gas_amount, idx): + """Creates a CAN message for the Tesla DBC acceleration command.""" + msg = struct.pack("!H", gas_amount) + return make_can_msg(0x200, msg, idx, 0) + +def create_sound_packet(command): + msg = struct.pack("B", command) + return [0x720, 0, msg, 0] + +def create_steering_control(apply_steer, idx, controls_allowed): + """Creates a CAN message for the Tesla EPAS STEERING_CONTROL.""" + """BO_ 1160 DAS_steeringControl: 4 NEO + SG_ DAS_steeringControlType : 23|2@0+ (1,0) [0|0] "" EPAS + SG_ DAS_steeringControlChecksum : 31|8@0+ (1,0) [0|0] "" EPAS + SG_ DAS_steeringControlCounter : 19|4@0+ (1,0) [0|0] "" EPAS + SG_ DAS_steeringAngleRequest : 6|15@0+ (0.1,-1638.35) [-1638.35|1638.35] "deg" EPAS + SG_ DAS_steeringHapticRequest : 7|1@0+ (1,0) [0|0] "" EPAS""" + if controls_allowed == False: + steering_type = 0 + else: + steering_type = 1 + type_counter = steering_type << 6 + type_counter += idx + checksum = ((apply_steer & 0xFF) + ((apply_steer >> 8) & 0xFF) + type_counter + 0x8C) & 0xFF + msg = struct.pack("!hBB", apply_steer, type_counter, checksum) + #TODO: change 0x489 to 0x488 for production + return [0x488, 0, msg, 1] + +def create_ui_commands(pcm_speed, hud, idx): + """Creates an iterable of CAN messages for the UIs.""" + commands = [] + pcm_speed_real = np.clip(int(round(pcm_speed / 0.002759506)), 0, + 64000) # conversion factor from dbc file + msg_0x30c = struct.pack("!HBBBBB", pcm_speed_real, hud.pcm_accel, + hud.v_cruise, hud.X2, hud.car, hud.X4) + commands.append(make_can_msg(0x30c, msg_0x30c, idx, 0)) + + msg_0x33d = chr(hud.X5) + chr(hud.lanes) + chr(hud.beep) + chr(hud.X8) + commands.append(make_can_msg(0x33d, msg_0x33d, idx, 0)) + #if civic: # 2 more msgs + # msg_0x35e = chr(0) * 7 + # commands.append(make_can_msg(0x35e, msg_0x35e, idx, 0)) + #if civic or accord: + # msg_0x39f = ( + # chr(0) * 2 + chr(hud.acc_alert) + chr(0) + chr(0xff) + chr(0x7f) + chr(0) + # ) + # commands.append(make_can_msg(0x39f, msg_0x39f, idx, 0)) + return commands + +def create_radar_commands(v_ego, idx): + """Creates an iterable of CAN messages for the radar system.""" + commands = [] + v_ego_kph = np.clip(int(round(v_ego * CV.MS_TO_KPH)), 0, 255) + speed = struct.pack('!B', v_ego_kph) + msg_0x300 = ("\xf9" + speed + "\x8a\xd0" +\ + ("\x20" if idx == 0 or idx == 3 else "\x00") +\ + "\x00\x00") + #if civic: + # msg_0x301 = "\x02\x38\x44\x32\x4f\x00\x00" + # add 8 on idx. + # commands.append(make_can_msg(0x300, msg_0x300, idx + 8, 1)) + #elif accord: + # 0300( 768)( 69) f9008ad0100000ef + # 0301( 769)( 69) 0ed8522256000029 + # msg_0x301 = "\x0e\xd8\x52\x22\x56\x00\x00" + # add 0xc on idx? WTF is this? + # commands.append(make_can_msg(0x300, msg_0x300, idx + 0xc, 1)) + #else: + # msg_0x301 = "\x0f\x18\x51\x02\x5a\x00\x00" + # commands.append(make_can_msg(0x300, msg_0x300, idx, 1)) + commands.append(make_can_msg(0x301, msg_0x301, idx, 1)) + return commands diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 9d447954b806b1..ac49d5fa6f87ff 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -92,7 +92,8 @@ def __init__(self, gctx, rate=100): # rear view camera state self.rear_view_toggle = False self.rear_view_allowed = bool(params.get("IsRearViewMirror")) - + self.rear_view_allowed = False + self.v_cruise_kph = 255 # 0.0 - 1.0 @@ -165,8 +166,11 @@ def state_control(self): else: self.rear_view_toggle = False + # if b.type == "altButton1" and b.pressed: + # self.rear_view_toggle = not self.rear_view_toggle if b.type == "altButton1" and b.pressed: - self.rear_view_toggle = not self.rear_view_toggle + print("Activating openpilot.") + enable_request = True if not self.CP.enableCruise and self.enabled and not b.pressed: if b.type == "accelCruise": @@ -175,8 +179,8 @@ def state_control(self): self.v_cruise_kph -= (self.v_cruise_kph % V_CRUISE_DELTA) + V_CRUISE_DELTA self.v_cruise_kph = clip(self.v_cruise_kph, V_CRUISE_MIN, V_CRUISE_MAX) - if not self.enabled and b.type in ["accelCruise", "decelCruise"] and not b.pressed: - enable_request = True + #if not self.enabled and b.type in ["accelCruise", "decelCruise"] and not b.pressed: + # enable_request = True # do disable on button down if b.type == "cancel" and b.pressed: @@ -185,16 +189,17 @@ def state_control(self): self.prof.checkpoint("Buttons") # *** health checking logic *** - hh = messaging.recv_sock(self.health) - if hh is not None: + #hh = messaging.recv_sock(self.health) + #if hh is not None: # if the board isn't allowing controls but somehow we are enabled! # TODO: this should be in state transition with a function follower logic - if not hh.health.controlsAllowed and self.enabled: - self.AM.add("controlsMismatch", self.enabled) + #if not hh.health.controlsAllowed and self.enabled: + # self.AM.add("controlsMismatch", self.enabled) # disable if the pedals are pressed while engaged, this is a user disable if self.enabled: - if self.CS.gasPressed or self.CS.brakePressed or not self.CS.cruiseState.available: + #if self.CS.gasPressed or self.CS.brakePressed or not self.CS.cruiseState.available: + if self.CS.brakePressed or not self.CS.cruiseState.available: self.AM.add("disable", self.enabled) # it can happen that car cruise disables while comma system is enabled: need to @@ -223,8 +228,8 @@ def state_control(self): else: enable_condition = enable_request - if self.CP.enableCruise and self.CS.cruiseState.enabled: - self.v_cruise_kph = self.CS.cruiseState.speed * CV.MS_TO_KPH + #if self.CP.enableCruise and self.CS.cruiseState.enabled: + self.v_cruise_kph = self.CS.cruiseState.speed * CV.MS_TO_KPH self.prof.checkpoint("AdaptiveCruise") @@ -243,8 +248,8 @@ def state_control(self): for alert in self.CS.errors: self.AM.add(alert, self.enabled) - if not self.plan.longitudinalValid: - self.AM.add("radarCommIssue", self.enabled) + #if not self.plan.longitudinalValid: + # self.AM.add("radarCommIssue", self.enabled) if self.cal_status != Calibration.CALIBRATED: if self.cal_status == Calibration.UNCALIBRATED: diff --git a/selfdrive/controls/lib/alertmanager.py b/selfdrive/controls/lib/alertmanager.py index 162a1977ed2168..bad49d6870cbdc 100644 --- a/selfdrive/controls/lib/alertmanager.py +++ b/selfdrive/controls/lib/alertmanager.py @@ -40,7 +40,8 @@ class AlertManager(object): "pedalPressed": alert("Comma Unavailable", "Pedal Pressed", ET.NO_ENTRY, "brakePressed", "chimeDouble", .4, 2., 3.), "preDriverDistracted": alert("Take Control ", "User Distracted", ET.WARNING, "steerRequired", "chimeDouble", .4, 2., 3.), "driverDistracted": alert("Take Control to Regain Speed", "User Distracted", ET.WARNING, "steerRequired", "chimeRepeated", .5, .5, .5), - "steerSaturated": alert("Take Control", "Turn Exceeds Limit", ET.WARNING, "steerRequired", "chimeSingle", 1., 2., 3.), + #"steerSaturated": alert("Take Control", "Turn Exceeds Limit", ET.WARNING, "steerRequired", "chimeSingle", 1., 2., 3.), + "steerSaturated": alert("Control on standby", "Lane change in progress", ET.WARNING, None, "chimeSingle", .2, 0., 0.), "overheat": alert("Take Control Immediately", "System Overheated", ET.SOFT_DISABLE, "steerRequired", "chimeRepeated", 1., 3., 3.), "controlsMismatch": alert("Take Control Immediately", "Controls Mismatch", ET.IMMEDIATE_DISABLE, "steerRequired", "chimeRepeated", 1., 3., 3.), "radarCommIssue": alert("Take Control Immediately", "Radar Error: Restart the Car",ET.IMMEDIATE_DISABLE, "steerRequired", "chimeRepeated", 1., 3., 3.), @@ -51,7 +52,8 @@ class AlertManager(object): "fcw": alert("", "", ET.WARNING, None, None, .1, .1, .1), # car errors "commIssue": alert("Take Control Immediately","CAN Error: Restart the Car", ET.IMMEDIATE_DISABLE, "steerRequired", "chimeRepeated", 1., 3., 3.), - "steerUnavailable": alert("Take Control Immediately","Steer Error: Restart the Car", ET.IMMEDIATE_DISABLE, "steerRequired", "chimeRepeated", 1., 3., 3.), + #"steerUnavailable": alert("Take Control Immediately","Steer Error: Restart the Car", ET.IMMEDIATE_DISABLE, "steerRequired", "chimeRepeated", 1., 3., 3.), + "steerUnavailable": alert("Control soft disable","User has taken over control", ET.IMMEDIATE_DISABLE, "steerRequired", "chimeRepeated", 1., 3., 3.), "steerTempUnavailable": alert("Take Control", "Steer Temporarily Unavailable", ET.WARNING, "steerRequired", "chimeDouble", .4, 2., 3.), "brakeUnavailable": alert("Take Control Immediately","Brake Error: Restart the Car", ET.IMMEDIATE_DISABLE, "steerRequired", "chimeRepeated", 1., 3., 3.), "gasUnavailable": alert("Take Control Immediately","Gas Error: Restart the Car", ET.IMMEDIATE_DISABLE, "steerRequired", "chimeRepeated", 1., 3., 3.), diff --git a/selfdrive/manager.py b/selfdrive/manager.py index 136cd7345707a2..0857f0deef4788 100755 --- a/selfdrive/manager.py +++ b/selfdrive/manager.py @@ -42,7 +42,7 @@ managed_processes = { "uploader": "selfdrive.loggerd.uploader", "controlsd": "selfdrive.controls.controlsd", - "radard": "selfdrive.controls.radard", +# "radard": "selfdrive.controls.radard", "loggerd": ("loggerd", ["./loggerd"]), "logmessaged": "selfdrive.logmessaged", "tombstoned": "selfdrive.tombstoned", @@ -67,7 +67,7 @@ def get_running(): 'controlsd', 'loggerd', 'sensord', - 'radard', +# 'radard', 'visiond', 'proclogd', ]