parent
d270722291
commit
242328f92c
53 changed files with 2122 additions and 387 deletions
@ -1,3 +1,4 @@ |
|||||||
|
### old can parser just used by plant.py for regression tests |
||||||
import os |
import os |
||||||
import opendbc |
import opendbc |
||||||
from collections import defaultdict |
from collections import defaultdict |
@ -1,87 +1,4 @@ |
|||||||
import os |
# functions common among cars |
||||||
|
|
||||||
from common.realtime import sec_since_boot |
def dbc_dict(pt_dbc, radar_dbc, chassis_dbc=None): |
||||||
from common.fingerprints import eliminate_incompatible_cars, all_known_cars |
return {'pt': pt_dbc, 'radar': radar_dbc, 'chassis': chassis_dbc} |
||||||
|
|
||||||
from selfdrive.swaglog import cloudlog |
|
||||||
import selfdrive.messaging as messaging |
|
||||||
from common.fingerprints import HONDA, TOYOTA, GM, FORD |
|
||||||
|
|
||||||
def load_interfaces(x): |
|
||||||
ret = {} |
|
||||||
for interface in x: |
|
||||||
try: |
|
||||||
imp = __import__('selfdrive.car.%s.interface' % interface, fromlist=['CarInterface']).CarInterface |
|
||||||
except ImportError: |
|
||||||
imp = None |
|
||||||
for car in x[interface]: |
|
||||||
ret[car] = imp |
|
||||||
return ret |
|
||||||
|
|
||||||
# imports from directory selfdrive/car/<name>/ |
|
||||||
interfaces = load_interfaces({ |
|
||||||
'honda': [HONDA.CIVIC, HONDA.ACURA_ILX, HONDA.CRV, HONDA.ODYSSEY, HONDA.ACURA_RDX, HONDA.PILOT, HONDA.RIDGELINE], |
|
||||||
'toyota': [TOYOTA.PRIUS, TOYOTA.RAV4, TOYOTA.RAV4H, TOYOTA.COROLLA, TOYOTA.LEXUS_RXH], |
|
||||||
'gm': [GM.VOLT, GM.CADILLAC_CT6], |
|
||||||
'ford': [FORD.FUSION], |
|
||||||
'simulator2': ['simulator2'], |
|
||||||
'mock': ['mock']}) |
|
||||||
|
|
||||||
# **** for use live only **** |
|
||||||
def fingerprint(logcan, timeout): |
|
||||||
if os.getenv("SIMULATOR2") is not None: |
|
||||||
return ("simulator2", None) |
|
||||||
|
|
||||||
finger_st = sec_since_boot() |
|
||||||
|
|
||||||
cloudlog.warning("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) |
|
||||||
|
|
||||||
ts = sec_since_boot() |
|
||||||
# if we only have one car choice and the time_fingerprint since we got our first |
|
||||||
# message has elapsed, exit. Toyota needs higher time_fingerprint, since DSU does not |
|
||||||
# broadcast immediately |
|
||||||
if len(candidate_cars) == 1 and st is not None: |
|
||||||
# TODO: better way to decide to wait more if Toyota |
|
||||||
time_fingerprint = 1.0 if ("TOYOTA" in candidate_cars[0] or "LEXUS" in candidate_cars[0]) else 0.1 |
|
||||||
if (ts-st) > time_fingerprint: |
|
||||||
break |
|
||||||
|
|
||||||
# bail if no cars left or we've been waiting too long |
|
||||||
elif len(candidate_cars) == 0 or (timeout and ts-finger_st > timeout): |
|
||||||
return None, finger |
|
||||||
|
|
||||||
cloudlog.warning("fingerprinted %s", candidate_cars[0]) |
|
||||||
return (candidate_cars[0], finger) |
|
||||||
|
|
||||||
|
|
||||||
def get_car(logcan, sendcan=None, passive=True): |
|
||||||
# TODO: timeout only useful for replays so controlsd can start before unlogger |
|
||||||
timeout = 1. if passive else None |
|
||||||
candidate, fingerprints = fingerprint(logcan, timeout) |
|
||||||
|
|
||||||
if candidate is None: |
|
||||||
cloudlog.warning("car doesn't match any fingerprints: %r", fingerprints) |
|
||||||
if passive: |
|
||||||
candidate = "mock" |
|
||||||
else: |
|
||||||
return None, None |
|
||||||
|
|
||||||
interface_cls = interfaces[candidate] |
|
||||||
if interface_cls is None: |
|
||||||
cloudlog.warning("car matched %s, but interface wasn't available" % candidate) |
|
||||||
return None, None |
|
||||||
|
|
||||||
params = interface_cls.get_params(candidate, fingerprints) |
|
||||||
|
|
||||||
return interface_cls(params, sendcan), params |
|
||||||
|
@ -0,0 +1,101 @@ |
|||||||
|
import os |
||||||
|
from common.basedir import BASEDIR |
||||||
|
from common.realtime import sec_since_boot |
||||||
|
from common.fingerprints import eliminate_incompatible_cars, all_known_cars |
||||||
|
from selfdrive.swaglog import cloudlog |
||||||
|
import selfdrive.messaging as messaging |
||||||
|
|
||||||
|
def load_interfaces(x): |
||||||
|
ret = {} |
||||||
|
for interface in x: |
||||||
|
try: |
||||||
|
imp = __import__('selfdrive.car.%s.interface' % interface, fromlist=['CarInterface']).CarInterface |
||||||
|
except ImportError: |
||||||
|
imp = None |
||||||
|
for car in x[interface]: |
||||||
|
ret[car] = imp |
||||||
|
return ret |
||||||
|
|
||||||
|
|
||||||
|
def _get_interface_names(): |
||||||
|
# read all the folders in selfdrive/car and return a dict where: |
||||||
|
# - keys are all the car names that which we have an interface for |
||||||
|
# - values are lists of spefic car models for a given car |
||||||
|
interface_names = {} |
||||||
|
for car_folder in [x[0] for x in os.walk(BASEDIR + '/selfdrive/car')]: |
||||||
|
try: |
||||||
|
car_name = car_folder.split('/')[-1] |
||||||
|
model_names = __import__('selfdrive.car.%s.values' % car_name, fromlist=['CAR']).CAR |
||||||
|
model_names = [getattr(model_names, c) for c in model_names.__dict__.keys() if not c.startswith("__")] |
||||||
|
interface_names[car_name] = model_names |
||||||
|
except (ImportError, IOError): |
||||||
|
pass |
||||||
|
|
||||||
|
return interface_names |
||||||
|
|
||||||
|
|
||||||
|
# imports from directory selfdrive/car/<name>/ |
||||||
|
interfaces = load_interfaces(_get_interface_names()) |
||||||
|
|
||||||
|
|
||||||
|
# **** for use live only **** |
||||||
|
def fingerprint(logcan, timeout): |
||||||
|
if os.getenv("SIMULATOR2") is not None: |
||||||
|
return ("simulator2", None) |
||||||
|
elif os.getenv("SIMULATOR") is not None: |
||||||
|
return ("simulator", None) |
||||||
|
|
||||||
|
cloudlog.warning("waiting for fingerprint...") |
||||||
|
candidate_cars = all_known_cars() |
||||||
|
finger = {} |
||||||
|
st = None |
||||||
|
while 1: |
||||||
|
for a in messaging.drain_sock(logcan, wait_for_one=True): |
||||||
|
for can in a.can: |
||||||
|
# ignore everything not on bus 0 and with more than 11 bits, |
||||||
|
# which are ussually sporadic and hard to include in fingerprints |
||||||
|
if can.src == 0 and can.address < 0x800: |
||||||
|
finger[can.address] = len(can.dat) |
||||||
|
candidate_cars = eliminate_incompatible_cars(can, candidate_cars) |
||||||
|
|
||||||
|
if st is None: |
||||||
|
st = sec_since_boot() # start time |
||||||
|
st_passive = sec_since_boot() # only relevant when passive |
||||||
|
ts = sec_since_boot() |
||||||
|
# if we only have one car choice and the time_fingerprint since we got our first |
||||||
|
# message has elapsed, exit. Toyota needs higher time_fingerprint, since DSU does not |
||||||
|
# broadcast immediately |
||||||
|
if len(candidate_cars) == 1 and st is not None: |
||||||
|
# TODO: better way to decide to wait more if Toyota |
||||||
|
time_fingerprint = 1.0 if ("TOYOTA" in candidate_cars[0] or "LEXUS" in candidate_cars[0]) else 0.1 |
||||||
|
if (ts-st) > time_fingerprint: |
||||||
|
break |
||||||
|
|
||||||
|
# bail if no cars left or we've been waiting too long |
||||||
|
elif len(candidate_cars) == 0 or (timeout and (ts - st_passive) > timeout): |
||||||
|
return None, finger |
||||||
|
|
||||||
|
cloudlog.warning("fingerprinted %s", candidate_cars[0]) |
||||||
|
return (candidate_cars[0], finger) |
||||||
|
|
||||||
|
|
||||||
|
def get_car(logcan, sendcan=None, passive=True): |
||||||
|
# TODO: timeout only useful for replays so controlsd can start before unlogger |
||||||
|
timeout = 1. if passive else None |
||||||
|
candidate, fingerprints = fingerprint(logcan, timeout) |
||||||
|
|
||||||
|
if candidate is None: |
||||||
|
cloudlog.warning("car doesn't match any fingerprints: %r", fingerprints) |
||||||
|
if passive: |
||||||
|
candidate = "mock" |
||||||
|
else: |
||||||
|
return None, None |
||||||
|
|
||||||
|
interface_cls = interfaces[candidate] |
||||||
|
if interface_cls is None: |
||||||
|
cloudlog.warning("car matched %s, but interface wasn't available" % candidate) |
||||||
|
return None, None |
||||||
|
|
||||||
|
params = interface_cls.get_params(candidate, fingerprints) |
||||||
|
|
||||||
|
return interface_cls(params, sendcan), params |
@ -0,0 +1,105 @@ |
|||||||
|
from selfdrive.can.parser import CANParser |
||||||
|
from selfdrive.config import Conversions as CV |
||||||
|
from selfdrive.car.ford.values import DBC |
||||||
|
from common.kalman.simple_kalman import KF1D |
||||||
|
import numpy as np |
||||||
|
|
||||||
|
WHEEL_RADIUS = 0.33 |
||||||
|
|
||||||
|
def parse_gear_shifter(can_gear, car_fingerprint): |
||||||
|
# TODO: Use values from DBC to parse this field |
||||||
|
if can_gear == 0x0: |
||||||
|
return "park" |
||||||
|
elif can_gear == 0x1: |
||||||
|
return "reverse" |
||||||
|
elif can_gear == 0x2: |
||||||
|
return "neutral" |
||||||
|
elif can_gear == 0x3: |
||||||
|
return "drive" |
||||||
|
elif can_gear == 0x4: |
||||||
|
return "brake" |
||||||
|
return "unknown" |
||||||
|
|
||||||
|
|
||||||
|
def get_can_parser(CP): |
||||||
|
|
||||||
|
signals = [ |
||||||
|
# sig_name, sig_address, default |
||||||
|
("WhlRr_W_Meas", "WheelSpeed_CG1", 0.), |
||||||
|
("WhlRl_W_Meas", "WheelSpeed_CG1", 0.), |
||||||
|
("WhlFr_W_Meas", "WheelSpeed_CG1", 0.), |
||||||
|
("WhlFl_W_Meas", "WheelSpeed_CG1", 0.), |
||||||
|
("SteWhlRelInit_An_Sns", "Steering_Wheel_Data_CG1", 0.), |
||||||
|
("Cruise_State", "Cruise_Status", 0.), |
||||||
|
("Set_Speed", "Cruise_Status", 0.), |
||||||
|
("LaActAvail_D_Actl", "Lane_Keep_Assist_Status", 0), |
||||||
|
("LaHandsOff_B_Actl", "Lane_Keep_Assist_Status", 0), |
||||||
|
("LaActDeny_B_Actl", "Lane_Keep_Assist_Status", 0), |
||||||
|
("ApedPosScal_Pc_Actl", "EngineData_14", 0.), |
||||||
|
("Dist_Incr", "Steering_Buttons", 0.), |
||||||
|
("Brake_Drv_Appl", "Cruise_Status", 0.), |
||||||
|
("Brake_Lights", "BCM_to_HS_Body", 0.), |
||||||
|
] |
||||||
|
|
||||||
|
checks = [ |
||||||
|
] |
||||||
|
|
||||||
|
return CANParser(DBC[CP.carFingerprint]['pt'], signals, checks, 0) |
||||||
|
|
||||||
|
|
||||||
|
class CarState(object): |
||||||
|
def __init__(self, CP): |
||||||
|
|
||||||
|
self.CP = CP |
||||||
|
self.left_blinker_on = 0 |
||||||
|
self.right_blinker_on = 0 |
||||||
|
|
||||||
|
# initialize can parser |
||||||
|
self.car_fingerprint = CP.carFingerprint |
||||||
|
|
||||||
|
# vEgo kalman filter |
||||||
|
dt = 0.01 |
||||||
|
# Q = np.matrix([[10.0, 0.0], [0.0, 100.0]]) |
||||||
|
# R = 1e3 |
||||||
|
self.v_ego_kf = KF1D(x0=np.matrix([[0.0], [0.0]]), |
||||||
|
A=np.matrix([[1.0, dt], [0.0, 1.0]]), |
||||||
|
C=np.matrix([1.0, 0.0]), |
||||||
|
K=np.matrix([[0.12287673], [0.29666309]])) |
||||||
|
self.v_ego = 0.0 |
||||||
|
|
||||||
|
def update(self, cp): |
||||||
|
# copy can_valid |
||||||
|
self.can_valid = cp.can_valid |
||||||
|
|
||||||
|
# update prevs, update must run once per loop |
||||||
|
self.prev_left_blinker_on = self.left_blinker_on |
||||||
|
self.prev_right_blinker_on = self.right_blinker_on |
||||||
|
|
||||||
|
# calc best v_ego estimate, by averaging two opposite corners |
||||||
|
self.v_wheel_fl = cp.vl["WheelSpeed_CG1"]['WhlRr_W_Meas'] * WHEEL_RADIUS |
||||||
|
self.v_wheel_fr = cp.vl["WheelSpeed_CG1"]['WhlRl_W_Meas'] * WHEEL_RADIUS |
||||||
|
self.v_wheel_rl = cp.vl["WheelSpeed_CG1"]['WhlFr_W_Meas'] * WHEEL_RADIUS |
||||||
|
self.v_wheel_rr = cp.vl["WheelSpeed_CG1"]['WhlFl_W_Meas'] * WHEEL_RADIUS |
||||||
|
self.v_wheel = (self.v_wheel_fl + self.v_wheel_fr + self.v_wheel_rl + self.v_wheel_rr) / 4. |
||||||
|
|
||||||
|
# Kalman filter |
||||||
|
if abs(self.v_wheel - self.v_ego) > 2.0: # Prevent large accelerations when car starts at non zero speed |
||||||
|
self.v_ego_x = np.matrix([[self.v_wheel], [0.0]]) |
||||||
|
|
||||||
|
self.v_ego_raw = self.v_wheel |
||||||
|
v_ego_x = self.v_ego_kf.update(self.v_wheel) |
||||||
|
self.v_ego = float(v_ego_x[0]) |
||||||
|
self.a_ego = float(v_ego_x[1]) |
||||||
|
self.standstill = not self.v_wheel > 0.001 |
||||||
|
|
||||||
|
self.angle_steers = cp.vl["Steering_Wheel_Data_CG1"]['SteWhlRelInit_An_Sns'] |
||||||
|
self.v_cruise_pcm = cp.vl["Cruise_Status"]['Set_Speed'] * CV.MPH_TO_MS |
||||||
|
self.pcm_acc_status = cp.vl["Cruise_Status"]['Cruise_State'] |
||||||
|
self.main_on = cp.vl["Cruise_Status"]['Cruise_State'] != 0 |
||||||
|
self.lkas_state = cp.vl["Lane_Keep_Assist_Status"]['LaActAvail_D_Actl'] |
||||||
|
self.steer_override = not cp.vl["Lane_Keep_Assist_Status"]['LaHandsOff_B_Actl'] |
||||||
|
self.steer_error = cp.vl["Lane_Keep_Assist_Status"]['LaActDeny_B_Actl'] |
||||||
|
self.user_gas = cp.vl["EngineData_14"]['ApedPosScal_Pc_Actl'] |
||||||
|
self.brake_pressed = bool(cp.vl["Cruise_Status"]["Brake_Drv_Appl"]) |
||||||
|
self.brake_lights = bool(cp.vl["BCM_to_HS_Body"]["Brake_Lights"]) |
||||||
|
self.generic_toggle = bool(cp.vl["Steering_Buttons"]["Dist_Incr"]) |
@ -0,0 +1,217 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
from common.realtime import sec_since_boot |
||||||
|
from cereal import car |
||||||
|
from selfdrive.config import Conversions as CV |
||||||
|
from selfdrive.controls.lib.drive_helpers import EventTypes as ET, create_event |
||||||
|
from selfdrive.controls.lib.vehicle_model import VehicleModel |
||||||
|
from selfdrive.car.ford.carstate import CarState, get_can_parser |
||||||
|
from selfdrive.car.ford.fordcan import MAX_ANGLE |
||||||
|
|
||||||
|
try: |
||||||
|
from selfdrive.car.ford.carcontroller import CarController |
||||||
|
except ImportError: |
||||||
|
CarController = None |
||||||
|
|
||||||
|
|
||||||
|
class CarInterface(object): |
||||||
|
def __init__(self, CP, sendcan=None): |
||||||
|
self.CP = CP |
||||||
|
self.VM = VehicleModel(CP) |
||||||
|
|
||||||
|
self.frame = 0 |
||||||
|
self.can_invalid_count = 0 |
||||||
|
self.gas_pressed_prev = False |
||||||
|
self.brake_pressed_prev = False |
||||||
|
self.cruise_enabled_prev = False |
||||||
|
|
||||||
|
# *** init the major players *** |
||||||
|
self.CS = CarState(CP) |
||||||
|
|
||||||
|
self.cp = get_can_parser(CP) |
||||||
|
|
||||||
|
# sending if read only is False |
||||||
|
if sendcan is not None: |
||||||
|
self.sendcan = sendcan |
||||||
|
self.CC = CarController(self.cp.dbc_name, CP.enableCamera, self.VM) |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def compute_gb(accel, speed): |
||||||
|
return float(accel) / 3.0 |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def calc_accel_override(a_ego, a_target, v_ego, v_target): |
||||||
|
return 1.0 |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def get_params(candidate, fingerprint): |
||||||
|
|
||||||
|
# kg of standard extra cargo to count for drive, gas, etc... |
||||||
|
std_cargo = 136 |
||||||
|
|
||||||
|
ret = car.CarParams.new_message() |
||||||
|
|
||||||
|
ret.carName = "ford" |
||||||
|
ret.carFingerprint = candidate |
||||||
|
|
||||||
|
ret.safetyModel = car.CarParams.SafetyModels.ford |
||||||
|
|
||||||
|
# pedal |
||||||
|
ret.enableCruise = True |
||||||
|
|
||||||
|
# FIXME: hardcoding honda civic 2016 touring params so they can be used to |
||||||
|
# scale unknown params for other cars |
||||||
|
mass_civic = 2923./2.205 + std_cargo |
||||||
|
wheelbase_civic = 2.70 |
||||||
|
centerToFront_civic = wheelbase_civic * 0.4 |
||||||
|
centerToRear_civic = wheelbase_civic - centerToFront_civic |
||||||
|
rotationalInertia_civic = 2500 |
||||||
|
tireStiffnessFront_civic = 85400 |
||||||
|
tireStiffnessRear_civic = 90000 |
||||||
|
|
||||||
|
ret.steerKiBP, ret.steerKpBP = [[0.], [0.]] |
||||||
|
ret.wheelbase = 2.85 |
||||||
|
ret.steerRatio = 14.8 |
||||||
|
ret.mass = 3045./2.205 + std_cargo |
||||||
|
ret.steerKpV, ret.steerKiV = [[0.01], [0.005]] # TODO: tune this |
||||||
|
ret.steerKf = 1. / MAX_ANGLE # MAX Steer angle to normalize FF |
||||||
|
ret.steerActuatorDelay = 0.1 # Default delay, not measured yet |
||||||
|
ret.steerRateCost = 0.5 |
||||||
|
|
||||||
|
f = 1.2 |
||||||
|
tireStiffnessFront_civic *= f |
||||||
|
tireStiffnessRear_civic *= f |
||||||
|
|
||||||
|
ret.centerToFront = ret.wheelbase * 0.44 |
||||||
|
|
||||||
|
ret.longPidDeadzoneBP = [0., 9.] |
||||||
|
ret.longPidDeadzoneV = [0., .15] |
||||||
|
|
||||||
|
# min speed to enable ACC. if car can do stop and go, then set enabling speed |
||||||
|
# to a negative value, so it won't matter. |
||||||
|
ret.minEnableSpeed = -1. |
||||||
|
|
||||||
|
centerToRear = ret.wheelbase - ret.centerToFront |
||||||
|
# TODO: get actual value, for now starting with reasonable value for |
||||||
|
# civic and scaling by mass and wheelbase |
||||||
|
ret.rotationalInertia = rotationalInertia_civic * \ |
||||||
|
ret.mass * ret.wheelbase**2 / (mass_civic * wheelbase_civic**2) |
||||||
|
|
||||||
|
# TODO: start from empirically derived lateral slip stiffness for the civic and scale by |
||||||
|
# mass and CG position, so all cars will have approximately similar dyn behaviors |
||||||
|
ret.tireStiffnessFront = tireStiffnessFront_civic * \ |
||||||
|
ret.mass / mass_civic * \ |
||||||
|
(centerToRear / ret.wheelbase) / (centerToRear_civic / wheelbase_civic) |
||||||
|
ret.tireStiffnessRear = tireStiffnessRear_civic * \ |
||||||
|
ret.mass / mass_civic * \ |
||||||
|
(ret.centerToFront / ret.wheelbase) / (centerToFront_civic / wheelbase_civic) |
||||||
|
|
||||||
|
# no rear steering, at least on the listed cars above |
||||||
|
ret.steerRatioRear = 0. |
||||||
|
ret.steerControlType = car.CarParams.SteerControlType.angle |
||||||
|
|
||||||
|
# steer, gas, brake limitations VS speed |
||||||
|
ret.steerMaxBP = [0.] # breakpoints at 1 and 40 kph |
||||||
|
ret.steerMaxV = [1.0] # 2/3rd torque allowed above 45 kph |
||||||
|
ret.gasMaxBP = [0.] |
||||||
|
ret.gasMaxV = [0.5] |
||||||
|
ret.brakeMaxBP = [5., 20.] |
||||||
|
ret.brakeMaxV = [1., 0.8] |
||||||
|
|
||||||
|
ret.enableCamera = not any(x for x in [970, 973, 984] if x in fingerprint) |
||||||
|
print "ECU Camera Simulated: ", ret.enableCamera |
||||||
|
|
||||||
|
ret.steerLimitAlert = False |
||||||
|
ret.stoppingControl = False |
||||||
|
ret.startAccel = 0.0 |
||||||
|
|
||||||
|
ret.longitudinalKpBP = [0., 5., 35.] |
||||||
|
ret.longitudinalKpV = [3.6, 2.4, 1.5] |
||||||
|
ret.longitudinalKiBP = [0., 35.] |
||||||
|
ret.longitudinalKiV = [0.54, 0.36] |
||||||
|
|
||||||
|
return ret |
||||||
|
|
||||||
|
# returns a car.CarState |
||||||
|
def update(self, c): |
||||||
|
# ******************* do can recv ******************* |
||||||
|
canMonoTimes = [] |
||||||
|
|
||||||
|
self.cp.update(int(sec_since_boot() * 1e9), False) |
||||||
|
|
||||||
|
self.CS.update(self.cp) |
||||||
|
|
||||||
|
# create message |
||||||
|
ret = car.CarState.new_message() |
||||||
|
|
||||||
|
# speeds |
||||||
|
ret.vEgo = self.CS.v_ego |
||||||
|
ret.vEgoRaw = self.CS.v_ego_raw |
||||||
|
ret.standstill = self.CS.standstill |
||||||
|
ret.wheelSpeeds.fl = self.CS.v_wheel_fl |
||||||
|
ret.wheelSpeeds.fr = self.CS.v_wheel_fr |
||||||
|
ret.wheelSpeeds.rl = self.CS.v_wheel_rl |
||||||
|
ret.wheelSpeeds.rr = self.CS.v_wheel_rr |
||||||
|
|
||||||
|
# steering wheel |
||||||
|
ret.steeringAngle = self.CS.angle_steers |
||||||
|
ret.steeringPressed = self.CS.steer_override |
||||||
|
|
||||||
|
# gas pedal |
||||||
|
ret.gas = self.CS.user_gas / 100. |
||||||
|
ret.gasPressed = self.CS.user_gas > 0.0001 |
||||||
|
ret.brakePressed = self.CS.brake_pressed |
||||||
|
ret.brakeLights = self.CS.brake_lights |
||||||
|
|
||||||
|
ret.cruiseState.enabled = not (self.CS.pcm_acc_status in [0, 3]) |
||||||
|
ret.cruiseState.speed = self.CS.v_cruise_pcm |
||||||
|
ret.cruiseState.available = self.CS.pcm_acc_status != 0 |
||||||
|
|
||||||
|
ret.genericToggle = self.CS.generic_toggle |
||||||
|
|
||||||
|
# events |
||||||
|
events = [] |
||||||
|
if not self.CS.can_valid: |
||||||
|
self.can_invalid_count += 1 |
||||||
|
if self.can_invalid_count >= 5: |
||||||
|
events.append(create_event('commIssue', [ET.NO_ENTRY, ET.IMMEDIATE_DISABLE])) |
||||||
|
else: |
||||||
|
self.can_invalid_count = 0 |
||||||
|
|
||||||
|
if self.CS.steer_error: |
||||||
|
events.append(create_event('steerUnavailable', [ET.NO_ENTRY, ET.IMMEDIATE_DISABLE, ET.PERMANENT])) |
||||||
|
|
||||||
|
# enable request in prius is simple, as we activate when Toyota is active (rising edge) |
||||||
|
if ret.cruiseState.enabled and not self.cruise_enabled_prev: |
||||||
|
events.append(create_event('pcmEnable', [ET.ENABLE])) |
||||||
|
elif not ret.cruiseState.enabled: |
||||||
|
events.append(create_event('pcmDisable', [ET.USER_DISABLE])) |
||||||
|
|
||||||
|
# disable on pedals rising edge or when brake is pressed and speed isn't zero |
||||||
|
if (ret.gasPressed and not self.gas_pressed_prev) or \ |
||||||
|
(ret.brakePressed and (not self.brake_pressed_prev or ret.vEgo > 0.001)): |
||||||
|
events.append(create_event('pedalPressed', [ET.NO_ENTRY, ET.USER_DISABLE])) |
||||||
|
|
||||||
|
if ret.gasPressed: |
||||||
|
events.append(create_event('pedalPressed', [ET.PRE_ENABLE])) |
||||||
|
|
||||||
|
if self.CS.lkas_state not in [2, 3] and ret.vEgo > 13.* CV.MPH_TO_MS and ret.cruiseState.enabled: |
||||||
|
events.append(create_event('steerTempUnavailableMute', [ET.WARNING])) |
||||||
|
|
||||||
|
ret.events = events |
||||||
|
ret.canMonoTimes = canMonoTimes |
||||||
|
|
||||||
|
self.gas_pressed_prev = ret.gasPressed |
||||||
|
self.brake_pressed_prev = ret.brakePressed |
||||||
|
self.cruise_enabled_prev = ret.cruiseState.enabled |
||||||
|
|
||||||
|
return ret.as_reader() |
||||||
|
|
||||||
|
# pass in a car.CarControl |
||||||
|
# to be called @ 100hz |
||||||
|
def apply(self, c): |
||||||
|
|
||||||
|
self.CC.update(self.sendcan, c.enabled, self.CS, self.frame, c.actuators, |
||||||
|
c.hudControl.visualAlert, c.cruiseControl.cancel) |
||||||
|
|
||||||
|
self.frame += 1 |
||||||
|
return False |
@ -0,0 +1,93 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
import os |
||||||
|
import numpy as np |
||||||
|
from selfdrive.can.parser import CANParser |
||||||
|
from cereal import car |
||||||
|
from common.realtime import sec_since_boot |
||||||
|
import zmq |
||||||
|
from selfdrive.services import service_list |
||||||
|
import selfdrive.messaging as messaging |
||||||
|
|
||||||
|
|
||||||
|
RADAR_MSGS = range(0x500, 0x540) |
||||||
|
|
||||||
|
def _create_radard_can_parser(): |
||||||
|
dbc_f = 'ford_fusion_2018_adas.dbc' |
||||||
|
msg_n = len(RADAR_MSGS) |
||||||
|
signals = zip(['X_Rel'] * msg_n + ['Angle'] * msg_n + ['V_Rel'] * msg_n, |
||||||
|
RADAR_MSGS * 3, |
||||||
|
[0] * msg_n + [0] * msg_n + [0] * msg_n) |
||||||
|
checks = zip(RADAR_MSGS, [20]*msg_n) |
||||||
|
|
||||||
|
return CANParser(os.path.splitext(dbc_f)[0], signals, checks, 1) |
||||||
|
|
||||||
|
class RadarInterface(object): |
||||||
|
def __init__(self, CP): |
||||||
|
# radar |
||||||
|
self.pts = {} |
||||||
|
self.validCnt = {key: 0 for key in RADAR_MSGS} |
||||||
|
self.track_id = 0 |
||||||
|
|
||||||
|
self.delay = 0.0 # Delay of radar |
||||||
|
|
||||||
|
# Nidec |
||||||
|
self.rcp = _create_radard_can_parser() |
||||||
|
|
||||||
|
context = zmq.Context() |
||||||
|
self.logcan = messaging.sub_sock(context, service_list['can'].port) |
||||||
|
|
||||||
|
def update(self): |
||||||
|
canMonoTimes = [] |
||||||
|
|
||||||
|
updated_messages = set() |
||||||
|
while 1: |
||||||
|
tm = int(sec_since_boot() * 1e9) |
||||||
|
updated_messages.update(self.rcp.update(tm, True)) |
||||||
|
# TODO: do not hardcode last msg |
||||||
|
if 0x53f in updated_messages: |
||||||
|
break |
||||||
|
|
||||||
|
ret = car.RadarState.new_message() |
||||||
|
errors = [] |
||||||
|
if not self.rcp.can_valid: |
||||||
|
errors.append("commIssue") |
||||||
|
ret.errors = errors |
||||||
|
ret.canMonoTimes = canMonoTimes |
||||||
|
|
||||||
|
for ii in updated_messages: |
||||||
|
cpt = self.rcp.vl[ii] |
||||||
|
|
||||||
|
if cpt['X_Rel'] > 0.00001: |
||||||
|
self.validCnt[ii] = 0 # reset counter |
||||||
|
|
||||||
|
if cpt['X_Rel'] > 0.00001: |
||||||
|
self.validCnt[ii] += 1 |
||||||
|
else: |
||||||
|
self.validCnt[ii] = max(self.validCnt[ii] -1, 0) |
||||||
|
#print ii, self.validCnt[ii], cpt['VALID'], cpt['X_Rel'], cpt['Angle'] |
||||||
|
|
||||||
|
# radar point only valid if there have been enough valid measurements |
||||||
|
if self.validCnt[ii] > 0: |
||||||
|
if ii not in self.pts: |
||||||
|
self.pts[ii] = car.RadarState.RadarPoint.new_message() |
||||||
|
self.pts[ii].trackId = self.track_id |
||||||
|
self.track_id += 1 |
||||||
|
self.pts[ii].dRel = cpt['X_Rel'] # from front of car |
||||||
|
self.pts[ii].yRel = cpt['X_Rel'] * cpt['Angle'] * np.pi / 180. # in car frame's y axis, left is positive |
||||||
|
self.pts[ii].vRel = cpt['V_Rel'] |
||||||
|
self.pts[ii].aRel = float('nan') |
||||||
|
self.pts[ii].yvRel = float('nan') |
||||||
|
self.pts[ii].measured = True |
||||||
|
else: |
||||||
|
if ii in self.pts: |
||||||
|
del self.pts[ii] |
||||||
|
|
||||||
|
ret.points = self.pts.values() |
||||||
|
return ret |
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
RI = RadarInterface(None) |
||||||
|
while 1: |
||||||
|
ret = RI.update() |
||||||
|
print(chr(27) + "[2J") |
||||||
|
print ret |
@ -0,0 +1,14 @@ |
|||||||
|
from selfdrive.car import dbc_dict |
||||||
|
|
||||||
|
class CAR: |
||||||
|
FUSION = "FORD FUSION 2018" |
||||||
|
|
||||||
|
FINGERPRINTS = { |
||||||
|
CAR.FUSION: [{ |
||||||
|
71: 8, 74: 8, 75: 8, 76: 8, 90: 8, 92: 8, 93: 8, 118: 8, 119: 8, 120: 8, 125: 8, 129: 8, 130: 8, 131: 8, 132: 8, 133: 8, 145: 8, 146: 8, 357: 8, 359: 8, 360: 8, 361: 8, 376: 8, 390: 8, 391: 8, 392: 8, 394: 8, 512: 8, 514: 8, 516: 8, 531: 8, 532: 8, 534: 8, 535: 8, 560: 8, 578: 8, 604: 8, 613: 8, 673: 8, 827: 8, 848: 8, 934: 8, 935: 8, 936: 8, 947: 8, 963: 8, 970: 8, 972: 8, 973: 8, 984: 8, 992: 8, 994: 8, 997: 8, 998: 8, 1003: 8, 1034: 8, 1045: 8, 1046: 8, 1053: 8, 1054: 8, 1058: 8, 1059: 8, 1068: 8, 1072: 8, 1073: 8, 1082: 8, 1107: 8, 1108: 8, 1109: 8, 1110: 8, 1200: 8, 1427: 8, 1430: 8, 1438: 8, 1459: 8 |
||||||
|
}], |
||||||
|
} |
||||||
|
|
||||||
|
DBC = { |
||||||
|
CAR.FUSION: dbc_dict('ford_fusion_2018_pt', 'ford_fusion_2018_adas'), |
||||||
|
} |
@ -0,0 +1,192 @@ |
|||||||
|
from common.numpy_fast import clip, interp |
||||||
|
from common.realtime import sec_since_boot |
||||||
|
from selfdrive.config import Conversions as CV |
||||||
|
from selfdrive.boardd.boardd import can_list_to_can_capnp |
||||||
|
from selfdrive.car.gm import gmcan |
||||||
|
from selfdrive.car.gm.values import CAR, DBC |
||||||
|
from selfdrive.can.packer import CANPacker |
||||||
|
|
||||||
|
|
||||||
|
class CarControllerParams(): |
||||||
|
def __init__(self, car_fingerprint): |
||||||
|
if car_fingerprint == CAR.VOLT: |
||||||
|
self.STEER_MAX = 255 |
||||||
|
self.STEER_STEP = 2 # how often we update the steer cmd |
||||||
|
self.STEER_DELTA_UP = 7 # ~0.75s time to peak torque (255/50hz/0.75s) |
||||||
|
self.STEER_DELTA_DOWN = 17 # ~0.3s from peak torque to zero |
||||||
|
elif car_fingerprint == CAR.CADILLAC_CT6: |
||||||
|
self.STEER_MAX = 150 |
||||||
|
self.STEER_STEP = 1 # how often we update the steer cmd |
||||||
|
self.STEER_DELTA_UP = 2 # 0.75s time to peak torque |
||||||
|
self.STEER_DELTA_DOWN = 5 # 0.3s from peak torque to zero |
||||||
|
|
||||||
|
self.STEER_DRIVER_ALLOWANCE = 50 # allowed driver torque before start limiting |
||||||
|
self.STEER_DRIVER_MULTIPLIER = 4 # weight driver torque heavily |
||||||
|
self.STEER_DRIVER_FACTOR = 100 # from dbc |
||||||
|
self.NEAR_STOP_BRAKE_PHASE = 0.5 # m/s, more aggressive braking near full stop |
||||||
|
|
||||||
|
self.ADAS_KEEPALIVE_STEP = 10 |
||||||
|
# pedal lookups, only for Volt |
||||||
|
MAX_GAS = 3072 # Only a safety limit |
||||||
|
ZERO_GAS = 2048 |
||||||
|
MAX_BRAKE = 350 # Should be around 3.5m/s^2, including regen |
||||||
|
self.MAX_ACC_REGEN = 1404 # ACC Regen braking is slightly less powerful than max regen paddle |
||||||
|
self.GAS_LOOKUP_BP = [-0.25, 0., 0.5] |
||||||
|
self.GAS_LOOKUP_V = [self.MAX_ACC_REGEN, ZERO_GAS, MAX_GAS] |
||||||
|
self.BRAKE_LOOKUP_BP = [-1., -0.25] |
||||||
|
self.BRAKE_LOOKUP_V = [MAX_BRAKE, 0] |
||||||
|
|
||||||
|
|
||||||
|
def actuator_hystereses(final_pedal, pedal_steady): |
||||||
|
# hyst params... TODO: move these to VehicleParams |
||||||
|
pedal_hyst_gap = 0.01 # don't change pedal command for small oscilalitons within this value |
||||||
|
|
||||||
|
# for small pedal oscillations within pedal_hyst_gap, don't change the pedal command |
||||||
|
if final_pedal == 0.: |
||||||
|
pedal_steady = 0. |
||||||
|
elif final_pedal > pedal_steady + pedal_hyst_gap: |
||||||
|
pedal_steady = final_pedal - pedal_hyst_gap |
||||||
|
elif final_pedal < pedal_steady - pedal_hyst_gap: |
||||||
|
pedal_steady = final_pedal + pedal_hyst_gap |
||||||
|
final_pedal = pedal_steady |
||||||
|
|
||||||
|
return final_pedal, pedal_steady |
||||||
|
|
||||||
|
|
||||||
|
class CarController(object): |
||||||
|
def __init__(self, canbus, car_fingerprint): |
||||||
|
self.pedal_steady = 0. |
||||||
|
self.start_time = sec_since_boot() |
||||||
|
self.chime = 0 |
||||||
|
self.lkas_active = False |
||||||
|
self.inhibit_steer_for = 0 |
||||||
|
self.steer_idx = 0 |
||||||
|
self.apply_steer_last = 0 |
||||||
|
self.car_fingerprint = car_fingerprint |
||||||
|
|
||||||
|
# Setup detection helper. Routes commands to |
||||||
|
# an appropriate CAN bus number. |
||||||
|
self.canbus = canbus |
||||||
|
self.params = CarControllerParams(car_fingerprint) |
||||||
|
|
||||||
|
self.packer_pt = CANPacker(DBC[car_fingerprint]['pt']) |
||||||
|
self.packer_ch = CANPacker(DBC[car_fingerprint]['chassis']) |
||||||
|
|
||||||
|
def update(self, sendcan, enabled, CS, frame, actuators, \ |
||||||
|
hud_v_cruise, hud_show_lanes, hud_show_car, chime, chime_cnt): |
||||||
|
""" Controls thread """ |
||||||
|
|
||||||
|
P = self.params |
||||||
|
|
||||||
|
# Send CAN commands. |
||||||
|
can_sends = [] |
||||||
|
canbus = self.canbus |
||||||
|
|
||||||
|
### STEER ### |
||||||
|
|
||||||
|
if (frame % P.STEER_STEP) == 0: |
||||||
|
final_steer = actuators.steer if enabled else 0. |
||||||
|
apply_steer = final_steer * P.STEER_MAX |
||||||
|
|
||||||
|
# limits due to driver torque |
||||||
|
driver_max_torque = P.STEER_MAX + (P.STEER_DRIVER_ALLOWANCE + CS.steer_torque_driver * P.STEER_DRIVER_FACTOR) * P.STEER_DRIVER_MULTIPLIER |
||||||
|
driver_min_torque = -P.STEER_MAX + (-P.STEER_DRIVER_ALLOWANCE + CS.steer_torque_driver * P.STEER_DRIVER_FACTOR) * P.STEER_DRIVER_MULTIPLIER |
||||||
|
max_steer_allowed = max(min(P.STEER_MAX, driver_max_torque), 0) |
||||||
|
min_steer_allowed = min(max(-P.STEER_MAX, driver_min_torque), 0) |
||||||
|
apply_steer = clip(apply_steer, min_steer_allowed, max_steer_allowed) |
||||||
|
|
||||||
|
# slow rate if steer torque increases in magnitude |
||||||
|
if self.apply_steer_last > 0: |
||||||
|
apply_steer = clip(apply_steer, max(self.apply_steer_last - P.STEER_DELTA_DOWN, -P.STEER_DELTA_UP), |
||||||
|
self.apply_steer_last + P.STEER_DELTA_UP) |
||||||
|
else: |
||||||
|
apply_steer = clip(apply_steer, self.apply_steer_last - P.STEER_DELTA_UP, |
||||||
|
min(self.apply_steer_last + P.STEER_DELTA_DOWN, P.STEER_DELTA_UP)) |
||||||
|
|
||||||
|
lkas_enabled = enabled and not CS.steer_not_allowed and CS.v_ego > 3. |
||||||
|
|
||||||
|
if not lkas_enabled: |
||||||
|
apply_steer = 0 |
||||||
|
|
||||||
|
apply_steer = int(round(apply_steer)) |
||||||
|
self.apply_steer_last = apply_steer |
||||||
|
idx = (frame / P.STEER_STEP) % 4 |
||||||
|
|
||||||
|
if self.car_fingerprint == CAR.VOLT: |
||||||
|
can_sends.append(gmcan.create_steering_control(self.packer_pt, |
||||||
|
canbus.powertrain, apply_steer, idx, lkas_enabled)) |
||||||
|
if self.car_fingerprint == CAR.CADILLAC_CT6: |
||||||
|
can_sends += gmcan.create_steering_control_ct6(self.packer_pt, |
||||||
|
canbus, apply_steer, CS.v_ego, idx, lkas_enabled) |
||||||
|
|
||||||
|
### GAS/BRAKE ### |
||||||
|
|
||||||
|
if self.car_fingerprint == CAR.VOLT: |
||||||
|
# no output if not enabled, but keep sending keepalive messages |
||||||
|
# threat pedals as one |
||||||
|
final_pedal = actuators.gas - actuators.brake |
||||||
|
|
||||||
|
# *** apply pedal hysteresis *** |
||||||
|
final_brake, self.brake_steady = actuator_hystereses( |
||||||
|
final_pedal, self.pedal_steady) |
||||||
|
|
||||||
|
if not enabled: |
||||||
|
apply_gas = P.MAX_ACC_REGEN # TODO: do we really need to send max regen when not enabled? |
||||||
|
apply_brake = 0 |
||||||
|
else: |
||||||
|
apply_gas = int(round(interp(final_pedal, P.GAS_LOOKUP_BP, P.GAS_LOOKUP_V))) |
||||||
|
apply_brake = int(round(interp(final_pedal, P.BRAKE_LOOKUP_BP, P.BRAKE_LOOKUP_V))) |
||||||
|
|
||||||
|
# Gas/regen and brakes - all at 25Hz |
||||||
|
if (frame % 4) == 0: |
||||||
|
idx = (frame / 4) % 4 |
||||||
|
|
||||||
|
at_full_stop = enabled and CS.standstill |
||||||
|
near_stop = enabled and (CS.v_ego < P.NEAR_STOP_BRAKE_PHASE) |
||||||
|
can_sends.append(gmcan.create_friction_brake_command(self.packer_ch, canbus.chassis, apply_brake, idx, near_stop, at_full_stop)) |
||||||
|
|
||||||
|
at_full_stop = enabled and CS.standstill |
||||||
|
can_sends.append(gmcan.create_gas_regen_command(self.packer_pt, canbus.powertrain, apply_gas, idx, enabled, at_full_stop)) |
||||||
|
|
||||||
|
# Send dashboard UI commands (ACC status), 25hz |
||||||
|
if (frame % 4) == 0: |
||||||
|
can_sends.append(gmcan.create_acc_dashboard_command(canbus.powertrain, enabled, hud_v_cruise / CV.MS_TO_KPH, hud_show_car)) |
||||||
|
|
||||||
|
# Radar needs to know current speed and yaw rate (50hz), |
||||||
|
# and that ADAS is alive (10hz) |
||||||
|
time_and_headlights_step = 10 |
||||||
|
tt = sec_since_boot() |
||||||
|
|
||||||
|
if frame % time_and_headlights_step == 0: |
||||||
|
idx = (frame / time_and_headlights_step) % 4 |
||||||
|
can_sends.append(gmcan.create_adas_time_status(canbus.obstacle, int((tt - self.start_time) * 60), idx)) |
||||||
|
can_sends.append(gmcan.create_adas_headlights_status(canbus.obstacle)) |
||||||
|
|
||||||
|
speed_and_accelerometer_step = 2 |
||||||
|
if frame % speed_and_accelerometer_step == 0: |
||||||
|
idx = (frame / speed_and_accelerometer_step) % 4 |
||||||
|
can_sends.append(gmcan.create_adas_steering_status(canbus.obstacle, idx)) |
||||||
|
can_sends.append(gmcan.create_adas_accelerometer_speed_status(canbus.obstacle, CS.v_ego, idx)) |
||||||
|
|
||||||
|
# Send ADAS keepalive, 10hz |
||||||
|
if frame % P.ADAS_KEEPALIVE_STEP == 0: |
||||||
|
can_sends += gmcan.create_adas_keepalive(canbus.powertrain) |
||||||
|
|
||||||
|
# Send chimes |
||||||
|
if self.chime != chime: |
||||||
|
duration = 0x3c |
||||||
|
|
||||||
|
# There is no 'repeat forever' chime command |
||||||
|
# TODO: Manage periodic re-issuing of chime command |
||||||
|
# and chime cancellation |
||||||
|
if chime_cnt == -1: |
||||||
|
chime_cnt = 10 |
||||||
|
|
||||||
|
if chime != 0: |
||||||
|
can_sends.append(gmcan.create_chime_command(canbus.sw_gmlan, chime, duration, chime_cnt)) |
||||||
|
|
||||||
|
# If canceling a repeated chime, cancel command must be |
||||||
|
# issued for the same chime type and duration |
||||||
|
self.chime = chime |
||||||
|
|
||||||
|
sendcan.send(can_list_to_can_capnp(can_sends, msgtype='sendcan').to_bytes()) |
@ -0,0 +1,144 @@ |
|||||||
|
import numpy as np |
||||||
|
from cereal import car |
||||||
|
from common.kalman.simple_kalman import KF1D |
||||||
|
from selfdrive.config import Conversions as CV |
||||||
|
from selfdrive.can.parser import CANParser |
||||||
|
from selfdrive.car.gm.values import DBC, CAR, parse_gear_shifter, \ |
||||||
|
CruiseButtons, is_eps_status_ok |
||||||
|
|
||||||
|
def get_powertrain_can_parser(CP, canbus): |
||||||
|
# this function generates lists for signal, messages and initial values |
||||||
|
signals = [ |
||||||
|
# sig_name, sig_address, default |
||||||
|
("BrakePedalPosition", "EBCMBrakePedalPosition", 0), |
||||||
|
("FrontLeftDoor", "BCMDoorBeltStatus", 0), |
||||||
|
("FrontRightDoor", "BCMDoorBeltStatus", 0), |
||||||
|
("RearLeftDoor", "BCMDoorBeltStatus", 0), |
||||||
|
("RearRightDoor", "BCMDoorBeltStatus", 0), |
||||||
|
("LeftSeatBelt", "BCMDoorBeltStatus", 0), |
||||||
|
("RightSeatBelt", "BCMDoorBeltStatus", 0), |
||||||
|
("TurnSignals", "BCMTurnSignals", 0), |
||||||
|
("AcceleratorPedal", "AcceleratorPedal", 0), |
||||||
|
("ACCButtons", "ASCMSteeringButton", CruiseButtons.UNPRESS), |
||||||
|
("SteeringWheelAngle", "PSCMSteeringAngle", 0), |
||||||
|
("FLWheelSpd", "EBCMWheelSpdFront", 0), |
||||||
|
("FRWheelSpd", "EBCMWheelSpdFront", 0), |
||||||
|
("RLWheelSpd", "EBCMWheelSpdRear", 0), |
||||||
|
("RRWheelSpd", "EBCMWheelSpdRear", 0), |
||||||
|
("PRNDL", "ECMPRDNL", 0), |
||||||
|
("LKADriverAppldTrq", "PSCMStatus", 0), |
||||||
|
("LKATorqueDeliveredStatus", "PSCMStatus", 0), |
||||||
|
] |
||||||
|
|
||||||
|
if CP.carFingerprint == CAR.VOLT: |
||||||
|
signals += [ |
||||||
|
("RegenPaddle", "EBCMRegenPaddle", 0), |
||||||
|
("TractionControlOn", "ESPStatus", 0), |
||||||
|
("EPBClosed", "EPBStatus", 0), |
||||||
|
("CruiseMainOn", "ECMEngineStatus", 0), |
||||||
|
("CruiseState", "AcceleratorPedal2", 0), |
||||||
|
] |
||||||
|
elif CP.carFingerprint == CAR.CADILLAC_CT6: |
||||||
|
signals += [ |
||||||
|
("ACCCmdActive", "ASCMActiveCruiseControlStatus", 0) |
||||||
|
] |
||||||
|
|
||||||
|
return CANParser(DBC[CP.carFingerprint]['pt'], signals, [], canbus.powertrain) |
||||||
|
|
||||||
|
class CarState(object): |
||||||
|
def __init__(self, CP, canbus): |
||||||
|
# initialize can parser |
||||||
|
|
||||||
|
self.car_fingerprint = CP.carFingerprint |
||||||
|
self.cruise_buttons = CruiseButtons.UNPRESS |
||||||
|
self.left_blinker_on = False |
||||||
|
self.prev_left_blinker_on = False |
||||||
|
self.right_blinker_on = False |
||||||
|
self.prev_right_blinker_on = False |
||||||
|
|
||||||
|
# vEgo kalman filter |
||||||
|
dt = 0.01 |
||||||
|
self.v_ego_kf = KF1D(x0=np.matrix([[0.], [0.]]), |
||||||
|
A=np.matrix([[1., dt], [0., 1.]]), |
||||||
|
C=np.matrix([1., 0.]), |
||||||
|
K=np.matrix([[0.12287673], [0.29666309]])) |
||||||
|
self.v_ego = 0. |
||||||
|
|
||||||
|
def update(self, pt_cp): |
||||||
|
|
||||||
|
self.can_valid = pt_cp.can_valid |
||||||
|
self.prev_cruise_buttons = self.cruise_buttons |
||||||
|
self.cruise_buttons = pt_cp.vl["ASCMSteeringButton"]['ACCButtons'] |
||||||
|
|
||||||
|
self.v_wheel_fl = pt_cp.vl["EBCMWheelSpdFront"]['FLWheelSpd'] * CV.KPH_TO_MS |
||||||
|
self.v_wheel_fr = pt_cp.vl["EBCMWheelSpdFront"]['FRWheelSpd'] * CV.KPH_TO_MS |
||||||
|
self.v_wheel_rl = pt_cp.vl["EBCMWheelSpdRear"]['RLWheelSpd'] * CV.KPH_TO_MS |
||||||
|
self.v_wheel_rr = pt_cp.vl["EBCMWheelSpdRear"]['RRWheelSpd'] * CV.KPH_TO_MS |
||||||
|
speed_estimate = (self.v_wheel_fl + self.v_wheel_fr + |
||||||
|
self.v_wheel_rl + self.v_wheel_rr) / 4.0 |
||||||
|
|
||||||
|
self.v_ego_raw = speed_estimate |
||||||
|
v_ego_x = self.v_ego_kf.update(speed_estimate) |
||||||
|
self.v_ego = float(v_ego_x[0]) |
||||||
|
self.a_ego = float(v_ego_x[1]) |
||||||
|
|
||||||
|
self.standstill = self.v_ego_raw < 0.01 |
||||||
|
|
||||||
|
self.angle_steers = pt_cp.vl["PSCMSteeringAngle"]['SteeringWheelAngle'] |
||||||
|
self.gear_shifter = parse_gear_shifter(pt_cp.vl["ECMPRDNL"]['PRNDL']) |
||||||
|
self.user_brake = pt_cp.vl["EBCMBrakePedalPosition"]['BrakePedalPosition'] |
||||||
|
|
||||||
|
self.pedal_gas = pt_cp.vl["AcceleratorPedal"]['AcceleratorPedal'] |
||||||
|
self.user_gas_pressed = self.pedal_gas > 0 |
||||||
|
|
||||||
|
self.steer_torque_driver = pt_cp.vl["PSCMStatus"]['LKADriverAppldTrq'] |
||||||
|
self.steer_override = abs(self.steer_torque_driver) > 1.0 |
||||||
|
|
||||||
|
# 0 - inactive, 1 - active, 2 - temporary limited, 3 - failed |
||||||
|
self.lkas_status = pt_cp.vl["PSCMStatus"]['LKATorqueDeliveredStatus'] |
||||||
|
self.steer_not_allowed = not is_eps_status_ok(self.lkas_status, self.car_fingerprint) |
||||||
|
|
||||||
|
# 1 - open, 0 - closed |
||||||
|
self.door_all_closed = (pt_cp.vl["BCMDoorBeltStatus"]['FrontLeftDoor'] == 0 and |
||||||
|
pt_cp.vl["BCMDoorBeltStatus"]['FrontRightDoor'] == 0 and |
||||||
|
pt_cp.vl["BCMDoorBeltStatus"]['RearLeftDoor'] == 0 and |
||||||
|
pt_cp.vl["BCMDoorBeltStatus"]['RearRightDoor'] == 0) |
||||||
|
|
||||||
|
# 1 - latched |
||||||
|
self.seatbelt = pt_cp.vl["BCMDoorBeltStatus"]['LeftSeatBelt'] == 1 |
||||||
|
|
||||||
|
self.steer_error = False |
||||||
|
|
||||||
|
self.brake_error = False |
||||||
|
self.can_valid = True |
||||||
|
|
||||||
|
self.prev_left_blinker_on = self.left_blinker_on |
||||||
|
self.prev_right_blinker_on = self.right_blinker_on |
||||||
|
self.left_blinker_on = pt_cp.vl["BCMTurnSignals"]['TurnSignals'] == 1 |
||||||
|
self.right_blinker_on = pt_cp.vl["BCMTurnSignals"]['TurnSignals'] == 2 |
||||||
|
|
||||||
|
if self.car_fingerprint == CAR.VOLT: |
||||||
|
self.park_brake = pt_cp.vl["EPBStatus"]['EPBClosed'] |
||||||
|
self.main_on = pt_cp.vl["ECMEngineStatus"]['CruiseMainOn'] |
||||||
|
self.acc_active = False |
||||||
|
self.esp_disabled = pt_cp.vl["ESPStatus"]['TractionControlOn'] != 1 |
||||||
|
self.regen_pressed = bool(pt_cp.vl["EBCMRegenPaddle"]['RegenPaddle']) |
||||||
|
self.pcm_acc_status = pt_cp.vl["AcceleratorPedal2"]['CruiseState'] |
||||||
|
else: |
||||||
|
self.park_brake = False |
||||||
|
self.main_on = False |
||||||
|
self.acc_active = pt_cp.vl["ASCMActiveCruiseControlStatus"]['ACCCmdActive'] |
||||||
|
self.esp_disabled = False |
||||||
|
self.regen_pressed = False |
||||||
|
self.pcm_acc_status = int(self.acc_active) |
||||||
|
|
||||||
|
# Brake pedal's potentiometer returns near-zero reading |
||||||
|
# even when pedal is not pressed. |
||||||
|
if self.user_brake < 10: |
||||||
|
self.user_brake = 0 |
||||||
|
|
||||||
|
# Regen braking is braking |
||||||
|
self.brake_pressed = self.user_brake > 10 or self.regen_pressed |
||||||
|
|
||||||
|
self.gear_shifter_valid = self.gear_shifter == car.CarState.GearShifter.drive |
||||||
|
|
@ -0,0 +1,173 @@ |
|||||||
|
def create_steering_control(packer, bus, apply_steer, idx, lkas_active): |
||||||
|
|
||||||
|
values = { |
||||||
|
"LKASteeringCmdActive": lkas_active, |
||||||
|
"LKASteeringCmd": apply_steer, |
||||||
|
"RollingCounter": idx, |
||||||
|
"LKASteeringCmdChecksum": 0x1000 - (lkas_active << 11) - (apply_steer & 0x7ff) - idx |
||||||
|
} |
||||||
|
|
||||||
|
return packer.make_can_msg("ASCMLKASteeringCmd", bus, values) |
||||||
|
|
||||||
|
def create_steering_control_ct6(packer, canbus, apply_steer, v_ego, idx, enabled): |
||||||
|
|
||||||
|
values = { |
||||||
|
"LKASteeringCmdActive": 1 if enabled else 0, |
||||||
|
"LKASteeringCmd": apply_steer, |
||||||
|
"RollingCounter": idx, |
||||||
|
"SetMe1": 1, |
||||||
|
"LKASVehicleSpeed": abs(v_ego * 3.6), |
||||||
|
"LKASMode": 2 if enabled else 0, |
||||||
|
"LKASteeringCmdChecksum": 0 # assume zero and then manually compute it |
||||||
|
} |
||||||
|
|
||||||
|
dat = packer.make_can_msg("ASCMLKASteeringCmd", 0, values)[2] |
||||||
|
# the checksum logic is weird |
||||||
|
values['LKASteeringCmdChecksum'] = (0x2a + |
||||||
|
sum([ord(i) for i in dat][:4]) + |
||||||
|
values['LKASMode']) & 0x3ff |
||||||
|
# pack again with checksum |
||||||
|
dat = packer.make_can_msg("ASCMLKASteeringCmd", 0, values)[2] |
||||||
|
|
||||||
|
return [0x152, 0, dat, canbus.powertrain], \ |
||||||
|
[0x154, 0, dat, canbus.powertrain], \ |
||||||
|
[0x151, 0, dat, canbus.chassis], \ |
||||||
|
[0x153, 0, dat, canbus.chassis] |
||||||
|
|
||||||
|
|
||||||
|
def create_adas_keepalive(bus): |
||||||
|
dat = "\x00\x00\x00\x00\x00\x00\x00" |
||||||
|
return [[0x409, 0, dat, bus], [0x40a, 0, dat, bus]] |
||||||
|
|
||||||
|
def create_gas_regen_command(packer, bus, throttle, idx, acc_engaged, at_full_stop): |
||||||
|
values = { |
||||||
|
"GasRegenCmdActive": acc_engaged, |
||||||
|
"RollingCounter": idx, |
||||||
|
"GasRegenCmdActiveInv": 1 - acc_engaged, |
||||||
|
"GasRegenCmd": throttle, |
||||||
|
"GasRegenFullStopActive": at_full_stop, |
||||||
|
"GasRegenAlwaysOne": 1, |
||||||
|
"GasRegenAlwaysOne2": 1, |
||||||
|
"GasRegenAlwaysOne3": 1, |
||||||
|
} |
||||||
|
|
||||||
|
dat = packer.make_can_msg("ASCMGasRegenCmd", bus, values)[2] |
||||||
|
values["GasRegenChecksum"] = (((0xff - ord(dat[1])) & 0xff) << 16) | \ |
||||||
|
(((0xff - ord(dat[2])) & 0xff) << 8) | \ |
||||||
|
((0x100 - ord(dat[3]) - idx) & 0xff) |
||||||
|
|
||||||
|
return packer.make_can_msg("ASCMGasRegenCmd", bus, values) |
||||||
|
|
||||||
|
def create_friction_brake_command(packer, bus, apply_brake, idx, near_stop, at_full_stop): |
||||||
|
|
||||||
|
if apply_brake == 0: |
||||||
|
mode = 0x1 |
||||||
|
else: |
||||||
|
mode = 0xa |
||||||
|
|
||||||
|
if at_full_stop: |
||||||
|
mode = 0xd |
||||||
|
# TODO: this is to have GM bringing the car to complete stop, |
||||||
|
# but currently it conflicts with OP controls, so turned off. |
||||||
|
#elif near_stop: |
||||||
|
# mode = 0xb |
||||||
|
|
||||||
|
brake = (0x1000 - apply_brake) & 0xfff |
||||||
|
checksum = (0x10000 - (mode << 12) - brake - idx) & 0xffff |
||||||
|
|
||||||
|
values = { |
||||||
|
"RollingCounter" : idx, |
||||||
|
"FrictionBrakeMode" : mode, |
||||||
|
"FrictionBrakeChecksum": checksum, |
||||||
|
"FrictionBrakeCmd" : -apply_brake |
||||||
|
} |
||||||
|
|
||||||
|
return packer.make_can_msg("EBCMFrictionBrakeCmd", bus, values) |
||||||
|
|
||||||
|
def create_acc_dashboard_command(bus, acc_engaged, target_speed_ms, lead_car_in_sight): |
||||||
|
engaged = 0x90 if acc_engaged else 0 |
||||||
|
lead_car = 0x10 if lead_car_in_sight else 0 |
||||||
|
target_speed = int(target_speed_ms * 208) & 0xfff |
||||||
|
speed_high = target_speed >> 8 |
||||||
|
speed_low = target_speed & 0xff |
||||||
|
dat = [0x01, 0x00, engaged | speed_high, speed_low, 0x01, lead_car] |
||||||
|
return [0x370, 0, "".join(map(chr, dat)), bus] |
||||||
|
|
||||||
|
def create_adas_time_status(bus, tt, idx): |
||||||
|
dat = [(tt >> 20) & 0xff, (tt >> 12) & 0xff, (tt >> 4) & 0xff, |
||||||
|
((tt & 0xf) << 4) + (idx << 2)] |
||||||
|
chksum = 0x1000 - dat[0] - dat[1] - dat[2] - dat[3] |
||||||
|
chksum = chksum & 0xfff |
||||||
|
dat += [0x40 + (chksum >> 8), chksum & 0xff, 0x12] |
||||||
|
return [0xa1, 0, "".join(map(chr, dat)), bus] |
||||||
|
|
||||||
|
def create_adas_steering_status(bus, idx): |
||||||
|
dat = [idx << 6, 0xf0, 0x20, 0, 0, 0] |
||||||
|
chksum = 0x60 + sum(dat) |
||||||
|
dat += [chksum >> 8, chksum & 0xff] |
||||||
|
return [0x306, 0, "".join(map(chr, dat)), bus] |
||||||
|
|
||||||
|
def create_adas_accelerometer_speed_status(bus, speed_ms, idx): |
||||||
|
spd = int(speed_ms * 16) & 0xfff |
||||||
|
accel = 0 & 0xfff |
||||||
|
# 0 if in park/neutral, 0x10 if in reverse, 0x08 for D/L |
||||||
|
#stick = 0x08 |
||||||
|
near_range_cutoff = 0x27 |
||||||
|
near_range_mode = 1 if spd <= near_range_cutoff else 0 |
||||||
|
far_range_mode = 1 - near_range_mode |
||||||
|
dat = [0x08, spd >> 4, ((spd & 0xf) << 4) | (accel >> 8), accel & 0xff, 0] |
||||||
|
chksum = 0x62 + far_range_mode + (idx << 2) + dat[0] + dat[1] + dat[2] + dat[3] + dat[4] |
||||||
|
dat += [(idx << 5) + (far_range_mode << 4) + (near_range_mode << 3) + (chksum >> 8), chksum & 0xff] |
||||||
|
return [0x308, 0, "".join(map(chr, dat)), bus] |
||||||
|
|
||||||
|
def create_adas_headlights_status(bus): |
||||||
|
return [0x310, 0, "\x42\x04", bus] |
||||||
|
|
||||||
|
def create_chime_command(bus, chime_type, duration, repeat_cnt): |
||||||
|
dat = [chime_type, duration, repeat_cnt, 0xff, 0] |
||||||
|
return [0x10400060, 0, "".join(map(chr, dat)), bus] |
||||||
|
|
||||||
|
# TODO: WIP |
||||||
|
''' |
||||||
|
def create_friction_brake_command_ct6(packer, bus, apply_brake, idx, near_stop, at_full_stop): |
||||||
|
|
||||||
|
# counters loops across [0, 29, 42, 55] but checksum only considers 0, 1, 2, 3 |
||||||
|
cntrs = [0, 29, 42, 55] |
||||||
|
if apply_brake == 0: |
||||||
|
mode = 0x1 |
||||||
|
else: |
||||||
|
mode = 0xa |
||||||
|
|
||||||
|
if at_full_stop: |
||||||
|
mode = 0xd |
||||||
|
elif near_stop: |
||||||
|
mode = 0xb |
||||||
|
|
||||||
|
brake = (0x1000 - apply_brake) & 0xfff |
||||||
|
checksum = (0x10000 - (mode << 12) - brake - idx) & 0xffff |
||||||
|
|
||||||
|
values = { |
||||||
|
"RollingCounter" : cntrs[idx], |
||||||
|
"FrictionBrakeMode" : mode, |
||||||
|
"FrictionBrakeChecksum": checksum, |
||||||
|
"FrictionBrakeCmd" : -apply_brake |
||||||
|
} |
||||||
|
|
||||||
|
dat = packer.make_can_msg("EBCMFrictionBrakeCmd", 0, values)[2] |
||||||
|
# msg is 0x315 but we are doing the panda forwarding |
||||||
|
return [0x314, 0, dat, 2] |
||||||
|
|
||||||
|
def create_gas_regen_command_ct6(bus, throttle, idx, acc_engaged, at_full_stop): |
||||||
|
cntrs = [0, 7, 10, 13] |
||||||
|
eng_bit = 1 if acc_engaged else 0 |
||||||
|
gas_high = (throttle >> 8) | 0x80 |
||||||
|
gas_low = (throttle) & 0xff |
||||||
|
full_stop = 0x20 if at_full_stop else 0 |
||||||
|
|
||||||
|
chk1 = (0x100 - gas_high - 1) & 0xff |
||||||
|
chk2 = (0x100 - gas_low - idx) & 0xff |
||||||
|
dat = [(idx << 6) | eng_bit, 0xc2 | full_stop, gas_high, gas_low, |
||||||
|
(1 - eng_bit) | (cntrs[idx] << 1), 0x5d - full_stop, chk1, chk2] |
||||||
|
return [0x2cb, 0, "".join(map(chr, dat)), bus] |
||||||
|
|
||||||
|
''' |
@ -0,0 +1,332 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
from cereal import car |
||||||
|
from common.realtime import sec_since_boot |
||||||
|
from selfdrive.config import Conversions as CV |
||||||
|
from selfdrive.controls.lib.drive_helpers import create_event, EventTypes as ET |
||||||
|
from selfdrive.controls.lib.vehicle_model import VehicleModel |
||||||
|
from selfdrive.car.gm.values import DBC, CAR |
||||||
|
from selfdrive.car.gm.carstate import CarState, CruiseButtons, get_powertrain_can_parser |
||||||
|
|
||||||
|
try: |
||||||
|
from selfdrive.car.gm.carcontroller import CarController |
||||||
|
except ImportError: |
||||||
|
CarController = None |
||||||
|
|
||||||
|
# Car chimes, beeps, blinker sounds etc |
||||||
|
class CM: |
||||||
|
TOCK = 0x81 |
||||||
|
TICK = 0x82 |
||||||
|
LOW_BEEP = 0x84 |
||||||
|
HIGH_BEEP = 0x85 |
||||||
|
LOW_CHIME = 0x86 |
||||||
|
HIGH_CHIME = 0x87 |
||||||
|
|
||||||
|
# GM cars have 4 CAN buses, which creates many ways |
||||||
|
# of how the car can be connected to. |
||||||
|
# This ia a helper class for the interface to be setup-agnostic. |
||||||
|
# Supports single Panda setup (connected to OBDII port), |
||||||
|
# and a CAN forwarding setup (connected to camera module connector). |
||||||
|
|
||||||
|
class CanBus(object): |
||||||
|
def __init__(self): |
||||||
|
self.powertrain = 0 |
||||||
|
self.obstacle = 1 |
||||||
|
self.chassis = 2 |
||||||
|
self.sw_gmlan = 3 |
||||||
|
|
||||||
|
class CarInterface(object): |
||||||
|
def __init__(self, CP, sendcan=None): |
||||||
|
self.CP = CP |
||||||
|
|
||||||
|
self.frame = 0 |
||||||
|
self.gas_pressed_prev = False |
||||||
|
self.brake_pressed_prev = False |
||||||
|
self.can_invalid_count = 0 |
||||||
|
self.acc_active_prev = 0 |
||||||
|
|
||||||
|
# *** init the major players *** |
||||||
|
canbus = CanBus() |
||||||
|
self.CS = CarState(CP, canbus) |
||||||
|
self.VM = VehicleModel(CP) |
||||||
|
self.pt_cp = get_powertrain_can_parser(CP, canbus) |
||||||
|
self.ch_cp_dbc_name = DBC[CP.carFingerprint]['chassis'] |
||||||
|
|
||||||
|
# sending if read only is False |
||||||
|
if sendcan is not None: |
||||||
|
self.sendcan = sendcan |
||||||
|
self.CC = CarController(canbus, CP.carFingerprint) |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def compute_gb(accel, speed): |
||||||
|
return float(accel) / 4.0 |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def calc_accel_override(a_ego, a_target, v_ego, v_target): |
||||||
|
return 1.0 |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def get_params(candidate, fingerprint): |
||||||
|
ret = car.CarParams.new_message() |
||||||
|
|
||||||
|
ret.carName = "gm" |
||||||
|
ret.carFingerprint = candidate |
||||||
|
|
||||||
|
ret.enableCruise = False |
||||||
|
|
||||||
|
# TODO: gate this on detection |
||||||
|
ret.enableCamera = True |
||||||
|
std_cargo = 136 |
||||||
|
|
||||||
|
if candidate == CAR.VOLT: |
||||||
|
# supports stop and go, but initial engage must be above 18mph (which include conservatism) |
||||||
|
ret.minEnableSpeed = 18 * CV.MPH_TO_MS |
||||||
|
# kg of standard extra cargo to count for drive, gas, etc... |
||||||
|
ret.mass = 1607 + std_cargo |
||||||
|
ret.safetyModel = car.CarParams.SafetyModels.gm |
||||||
|
ret.wheelbase = 2.69 |
||||||
|
ret.steerRatio = 15.7 |
||||||
|
ret.steerRatioRear = 0. |
||||||
|
ret.centerToFront = ret.wheelbase * 0.4 # wild guess |
||||||
|
|
||||||
|
elif candidate == CAR.CADILLAC_CT6: |
||||||
|
# engage speed is decided by pcm |
||||||
|
ret.minEnableSpeed = -1 |
||||||
|
# kg of standard extra cargo to count for drive, gas, etc... |
||||||
|
ret.mass = 4016/2.205 + std_cargo |
||||||
|
ret.safetyModel = car.CarParams.SafetyModels.cadillac |
||||||
|
ret.wheelbase = 3.11 |
||||||
|
ret.steerRatio = 14.6 # it's 16.3 without rear active steering |
||||||
|
ret.steerRatioRear = 0. # TODO: there is RAS on this car! |
||||||
|
ret.centerToFront = ret.wheelbase * 0.465 |
||||||
|
|
||||||
|
|
||||||
|
# hardcoding honda civic 2016 touring params so they can be used to |
||||||
|
# scale unknown params for other cars |
||||||
|
mass_civic = 2923./2.205 + std_cargo |
||||||
|
wheelbase_civic = 2.70 |
||||||
|
centerToFront_civic = wheelbase_civic * 0.4 |
||||||
|
centerToRear_civic = wheelbase_civic - centerToFront_civic |
||||||
|
rotationalInertia_civic = 2500 |
||||||
|
tireStiffnessFront_civic = 85400 |
||||||
|
tireStiffnessRear_civic = 90000 |
||||||
|
|
||||||
|
centerToRear = ret.wheelbase - ret.centerToFront |
||||||
|
# TODO: get actual value, for now starting with reasonable value for |
||||||
|
# civic and scaling by mass and wheelbase |
||||||
|
ret.rotationalInertia = rotationalInertia_civic * \ |
||||||
|
ret.mass * ret.wheelbase**2 / (mass_civic * wheelbase_civic**2) |
||||||
|
|
||||||
|
# TODO: start from empirically derived lateral slip stiffness for the civic and scale by |
||||||
|
# mass and CG position, so all cars will have approximately similar dyn behaviors |
||||||
|
ret.tireStiffnessFront = tireStiffnessFront_civic * \ |
||||||
|
ret.mass / mass_civic * \ |
||||||
|
(centerToRear / ret.wheelbase) / (centerToRear_civic / wheelbase_civic) |
||||||
|
ret.tireStiffnessRear = tireStiffnessRear_civic * \ |
||||||
|
ret.mass / mass_civic * \ |
||||||
|
(ret.centerToFront / ret.wheelbase) / (centerToFront_civic / wheelbase_civic) |
||||||
|
|
||||||
|
|
||||||
|
# same tuning for Volt and CT6 for now |
||||||
|
ret.steerKiBP, ret.steerKpBP = [[0.], [0.]] |
||||||
|
ret.steerKpV, ret.steerKiV = [[0.2], [0.00]] |
||||||
|
ret.steerKf = 0.00004 # full torque for 20 deg at 80mph means 0.00007818594 |
||||||
|
|
||||||
|
ret.steerMaxBP = [0.] # m/s |
||||||
|
ret.steerMaxV = [1.] |
||||||
|
ret.gasMaxBP = [0.] |
||||||
|
ret.gasMaxV = [.5] |
||||||
|
ret.brakeMaxBP = [0.] |
||||||
|
ret.brakeMaxV = [1.] |
||||||
|
ret.longPidDeadzoneBP = [0.] |
||||||
|
ret.longPidDeadzoneV = [0.] |
||||||
|
|
||||||
|
ret.longitudinalKpBP = [5., 35.] |
||||||
|
ret.longitudinalKpV = [2.4, 1.5] |
||||||
|
ret.longitudinalKiBP = [0.] |
||||||
|
ret.longitudinalKiV = [0.36] |
||||||
|
|
||||||
|
ret.steerLimitAlert = True |
||||||
|
|
||||||
|
ret.stoppingControl = True |
||||||
|
ret.startAccel = 0.8 |
||||||
|
|
||||||
|
ret.steerActuatorDelay = 0.1 # Default delay, not measured yet |
||||||
|
ret.steerRateCost = 0.5 |
||||||
|
ret.steerControlType = car.CarParams.SteerControlType.torque |
||||||
|
|
||||||
|
return ret |
||||||
|
|
||||||
|
# returns a car.CarState |
||||||
|
def update(self, c): |
||||||
|
|
||||||
|
self.pt_cp.update(int(sec_since_boot() * 1e9), False) |
||||||
|
self.CS.update(self.pt_cp) |
||||||
|
|
||||||
|
# create message |
||||||
|
ret = car.CarState.new_message() |
||||||
|
|
||||||
|
# speeds |
||||||
|
ret.vEgo = self.CS.v_ego |
||||||
|
ret.aEgo = self.CS.a_ego |
||||||
|
ret.vEgoRaw = self.CS.v_ego_raw |
||||||
|
ret.yawRate = self.VM.yaw_rate(self.CS.angle_steers * CV.DEG_TO_RAD, self.CS.v_ego) |
||||||
|
ret.standstill = self.CS.standstill |
||||||
|
ret.wheelSpeeds.fl = self.CS.v_wheel_fl |
||||||
|
ret.wheelSpeeds.fr = self.CS.v_wheel_fr |
||||||
|
ret.wheelSpeeds.rl = self.CS.v_wheel_rl |
||||||
|
ret.wheelSpeeds.rr = self.CS.v_wheel_rr |
||||||
|
|
||||||
|
# gas pedal information. |
||||||
|
ret.gas = self.CS.pedal_gas / 254.0 |
||||||
|
ret.gasPressed = self.CS.user_gas_pressed |
||||||
|
|
||||||
|
# brake pedal |
||||||
|
ret.brake = self.CS.user_brake / 0xd0 |
||||||
|
ret.brakePressed = self.CS.brake_pressed |
||||||
|
|
||||||
|
# steering wheel |
||||||
|
ret.steeringAngle = self.CS.angle_steers |
||||||
|
|
||||||
|
# torque and user override. Driver awareness |
||||||
|
# timer resets when the user uses the steering wheel. |
||||||
|
ret.steeringPressed = self.CS.steer_override |
||||||
|
ret.steeringTorque = self.CS.steer_torque_driver |
||||||
|
|
||||||
|
# cruise state |
||||||
|
ret.cruiseState.available = bool(self.CS.main_on) |
||||||
|
ret.cruiseState.enabled = self.CS.pcm_acc_status != 0 |
||||||
|
ret.cruiseState.standstill = self.CS.pcm_acc_status == 4 |
||||||
|
|
||||||
|
ret.leftBlinker = self.CS.left_blinker_on |
||||||
|
ret.rightBlinker = self.CS.right_blinker_on |
||||||
|
ret.doorOpen = not self.CS.door_all_closed |
||||||
|
ret.seatbeltUnlatched = not self.CS.seatbelt |
||||||
|
ret.gearShifter = self.CS.gear_shifter |
||||||
|
|
||||||
|
buttonEvents = [] |
||||||
|
|
||||||
|
# blinkers |
||||||
|
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 |
||||||
|
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 |
||||||
|
buttonEvents.append(be) |
||||||
|
|
||||||
|
if self.CS.cruise_buttons != self.CS.prev_cruise_buttons: |
||||||
|
be = car.CarState.ButtonEvent.new_message() |
||||||
|
be.type = 'unknown' |
||||||
|
if self.CS.cruise_buttons != CruiseButtons.UNPRESS: |
||||||
|
be.pressed = True |
||||||
|
but = self.CS.cruise_buttons |
||||||
|
else: |
||||||
|
be.pressed = False |
||||||
|
but = self.CS.prev_cruise_buttons |
||||||
|
if but == CruiseButtons.RES_ACCEL: |
||||||
|
be.type = 'accelCruise' |
||||||
|
elif but == CruiseButtons.DECEL_SET: |
||||||
|
be.type = 'decelCruise' |
||||||
|
elif but == CruiseButtons.CANCEL: |
||||||
|
be.type = 'cancel' |
||||||
|
elif but == CruiseButtons.MAIN: |
||||||
|
be.type = 'altButton3' |
||||||
|
buttonEvents.append(be) |
||||||
|
|
||||||
|
ret.buttonEvents = buttonEvents |
||||||
|
|
||||||
|
events = [] |
||||||
|
if not self.CS.can_valid: |
||||||
|
self.can_invalid_count += 1 |
||||||
|
if self.can_invalid_count >= 5: |
||||||
|
events.append(create_event('commIssue', [ET.NO_ENTRY, ET.IMMEDIATE_DISABLE])) |
||||||
|
else: |
||||||
|
self.can_invalid_count = 0 |
||||||
|
if self.CS.steer_error: |
||||||
|
events.append(create_event('steerUnavailable', [ET.NO_ENTRY, ET.IMMEDIATE_DISABLE, ET.PERMANENT])) |
||||||
|
if self.CS.steer_not_allowed: |
||||||
|
events.append(create_event('steerTempUnavailable', [ET.NO_ENTRY, ET.WARNING])) |
||||||
|
if ret.doorOpen: |
||||||
|
events.append(create_event('doorOpen', [ET.NO_ENTRY, ET.SOFT_DISABLE])) |
||||||
|
if ret.seatbeltUnlatched: |
||||||
|
events.append(create_event('seatbeltNotLatched', [ET.NO_ENTRY, ET.SOFT_DISABLE])) |
||||||
|
|
||||||
|
if self.CS.car_fingerprint == CAR.VOLT: |
||||||
|
|
||||||
|
if self.CS.brake_error: |
||||||
|
events.append(create_event('brakeUnavailable', [ET.NO_ENTRY, ET.IMMEDIATE_DISABLE, ET.PERMANENT])) |
||||||
|
if not self.CS.gear_shifter_valid: |
||||||
|
events.append(create_event('wrongGear', [ET.NO_ENTRY, ET.SOFT_DISABLE])) |
||||||
|
if self.CS.esp_disabled: |
||||||
|
events.append(create_event('espDisabled', [ET.NO_ENTRY, ET.SOFT_DISABLE])) |
||||||
|
if not self.CS.main_on: |
||||||
|
events.append(create_event('wrongCarMode', [ET.NO_ENTRY, ET.USER_DISABLE])) |
||||||
|
if self.CS.gear_shifter == 3: |
||||||
|
events.append(create_event('reverseGear', [ET.NO_ENTRY, ET.IMMEDIATE_DISABLE])) |
||||||
|
if ret.vEgo < self.CP.minEnableSpeed: |
||||||
|
events.append(create_event('speedTooLow', [ET.NO_ENTRY])) |
||||||
|
if self.CS.park_brake: |
||||||
|
events.append(create_event('parkBrake', [ET.NO_ENTRY, ET.USER_DISABLE])) |
||||||
|
# disable on pedals rising edge or when brake is pressed and speed isn't zero |
||||||
|
if (ret.gasPressed and not self.gas_pressed_prev) or \ |
||||||
|
(ret.brakePressed): # and (not self.brake_pressed_prev or ret.vEgo > 0.001)): |
||||||
|
events.append(create_event('pedalPressed', [ET.NO_ENTRY, ET.USER_DISABLE])) |
||||||
|
if ret.gasPressed: |
||||||
|
events.append(create_event('pedalPressed', [ET.PRE_ENABLE])) |
||||||
|
if ret.cruiseState.standstill: |
||||||
|
events.append(create_event('resumeRequired', [ET.WARNING])) |
||||||
|
|
||||||
|
# handle button presses |
||||||
|
for b in ret.buttonEvents: |
||||||
|
# do enable on both accel and decel buttons |
||||||
|
if b.type in ["accelCruise", "decelCruise"] and not b.pressed: |
||||||
|
events.append(create_event('buttonEnable', [ET.ENABLE])) |
||||||
|
# do disable on button down |
||||||
|
if b.type == "cancel" and b.pressed: |
||||||
|
events.append(create_event('buttonCancel', [ET.USER_DISABLE])) |
||||||
|
|
||||||
|
if self.CS.car_fingerprint == CAR.CADILLAC_CT6: |
||||||
|
|
||||||
|
if self.CS.acc_active and not self.acc_active_prev: |
||||||
|
events.append(create_event('pcmEnable', [ET.ENABLE])) |
||||||
|
if not self.CS.acc_active: |
||||||
|
events.append(create_event('pcmDisable', [ET.USER_DISABLE])) |
||||||
|
|
||||||
|
ret.events = events |
||||||
|
|
||||||
|
# update previous brake/gas pressed |
||||||
|
self.acc_active_prev = self.CS.acc_active |
||||||
|
self.gas_pressed_prev = ret.gasPressed |
||||||
|
self.brake_pressed_prev = ret.brakePressed |
||||||
|
|
||||||
|
# cast to reader so it can't be modified |
||||||
|
return ret.as_reader() |
||||||
|
|
||||||
|
# pass in a car.CarControl |
||||||
|
# to be called @ 100hz |
||||||
|
def apply(self, c): |
||||||
|
hud_v_cruise = c.hudControl.setSpeed |
||||||
|
if hud_v_cruise > 70: |
||||||
|
hud_v_cruise = 0 |
||||||
|
|
||||||
|
chime, chime_count = { |
||||||
|
"none": (0, 0), |
||||||
|
"beepSingle": (CM.HIGH_CHIME, 1), |
||||||
|
"beepTriple": (CM.HIGH_CHIME, 3), |
||||||
|
"beepRepeated": (CM.LOW_CHIME, -1), |
||||||
|
"chimeSingle": (CM.LOW_CHIME, 1), |
||||||
|
"chimeDouble": (CM.LOW_CHIME, 2), |
||||||
|
"chimeRepeated": (CM.LOW_CHIME, -1), |
||||||
|
"chimeContinuous": (CM.LOW_CHIME, -1)}[str(c.hudControl.audibleAlert)] |
||||||
|
|
||||||
|
self.CC.update(self.sendcan, c.enabled, self.CS, self.frame, \ |
||||||
|
c.actuators, |
||||||
|
hud_v_cruise, c.hudControl.lanesVisible, \ |
||||||
|
c.hudControl.leadVisible, \ |
||||||
|
chime, chime_count) |
||||||
|
|
||||||
|
self.frame += 1 |
@ -0,0 +1,122 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
import zmq |
||||||
|
import math |
||||||
|
import time |
||||||
|
import numpy as np |
||||||
|
from cereal import car |
||||||
|
from selfdrive.can.parser import CANParser |
||||||
|
from selfdrive.car.gm.interface import CanBus |
||||||
|
from selfdrive.car.gm.values import DBC, CAR |
||||||
|
from common.realtime import sec_since_boot |
||||||
|
from selfdrive.services import service_list |
||||||
|
import selfdrive.messaging as messaging |
||||||
|
|
||||||
|
NUM_TARGETS_MSG = 1120 |
||||||
|
SLOT_1_MSG = NUM_TARGETS_MSG + 1 |
||||||
|
NUM_SLOTS = 20 |
||||||
|
|
||||||
|
# Actually it's 0x47f, but can parser only reports |
||||||
|
# messages that are present in DBC |
||||||
|
LAST_RADAR_MSG = NUM_TARGETS_MSG + NUM_SLOTS |
||||||
|
|
||||||
|
def create_radard_can_parser(canbus, car_fingerprint): |
||||||
|
|
||||||
|
dbc_f = DBC[car_fingerprint]['radar'] |
||||||
|
if car_fingerprint == CAR.VOLT: |
||||||
|
# C1A-ARS3-A by Continental |
||||||
|
radar_targets = range(SLOT_1_MSG, SLOT_1_MSG + NUM_SLOTS) |
||||||
|
signals = zip(['LRRNumObjects'] + |
||||||
|
['TrkRange'] * NUM_SLOTS + ['TrkRangeRate'] * NUM_SLOTS + |
||||||
|
['TrkRangeAccel'] * NUM_SLOTS + ['TrkAzimuth'] * NUM_SLOTS + |
||||||
|
['TrkWidth'] * NUM_SLOTS + ['TrkObjectID'] * NUM_SLOTS, |
||||||
|
[NUM_TARGETS_MSG] + radar_targets * 6, |
||||||
|
[0] + [0.0] * NUM_SLOTS + [0.0] * NUM_SLOTS + |
||||||
|
[0.0] * NUM_SLOTS + [0.0] * NUM_SLOTS + |
||||||
|
[0.0] * NUM_SLOTS + [0] * NUM_SLOTS) |
||||||
|
|
||||||
|
checks = [] |
||||||
|
|
||||||
|
return CANParser(dbc_f, signals, checks, canbus.obstacle) |
||||||
|
else: |
||||||
|
return None |
||||||
|
|
||||||
|
class RadarInterface(object): |
||||||
|
def __init__(self, CP): |
||||||
|
# radar |
||||||
|
self.pts = {} |
||||||
|
self.track_id = 0 |
||||||
|
self.num_targets = 0 |
||||||
|
|
||||||
|
self.delay = 0.0 # Delay of radar |
||||||
|
|
||||||
|
canbus = CanBus() |
||||||
|
print "Using %d as obstacle CAN bus ID" % canbus.obstacle |
||||||
|
self.rcp = create_radard_can_parser(canbus, CP.carFingerprint) |
||||||
|
|
||||||
|
context = zmq.Context() |
||||||
|
self.logcan = messaging.sub_sock(context, service_list['can'].port) |
||||||
|
|
||||||
|
def update(self): |
||||||
|
updated_messages = set() |
||||||
|
ret = car.RadarState.new_message() |
||||||
|
while 1: |
||||||
|
|
||||||
|
if self.rcp is None: |
||||||
|
time.sleep(0.05) # nothing to do |
||||||
|
return ret |
||||||
|
|
||||||
|
tm = int(sec_since_boot() * 1e9) |
||||||
|
updated_messages.update(self.rcp.update(tm, True)) |
||||||
|
if LAST_RADAR_MSG in updated_messages: |
||||||
|
break |
||||||
|
|
||||||
|
errors = [] |
||||||
|
if not self.rcp.can_valid: |
||||||
|
errors.append("notValid") |
||||||
|
ret.errors = errors |
||||||
|
|
||||||
|
currentTargets = set() |
||||||
|
if self.rcp.vl[NUM_TARGETS_MSG]['LRRNumObjects'] != self.num_targets: |
||||||
|
self.num_targets = self.rcp.vl[NUM_TARGETS_MSG]['LRRNumObjects'] |
||||||
|
|
||||||
|
# Not all radar messages describe targets, |
||||||
|
# no need to monitor all of the sself.rcp.msgs_upd |
||||||
|
for ii in updated_messages: |
||||||
|
if ii == NUM_TARGETS_MSG: |
||||||
|
continue |
||||||
|
|
||||||
|
if self.num_targets == 0: |
||||||
|
break |
||||||
|
|
||||||
|
cpt = self.rcp.vl[ii] |
||||||
|
# Zero distance means it's an empty target slot |
||||||
|
if cpt['TrkRange'] > 0.0: |
||||||
|
targetId = cpt['TrkObjectID'] |
||||||
|
currentTargets.add(targetId) |
||||||
|
if targetId not in self.pts: |
||||||
|
self.pts[targetId] = car.RadarState.RadarPoint.new_message() |
||||||
|
self.pts[targetId].trackId = targetId |
||||||
|
distance = cpt['TrkRange'] |
||||||
|
self.pts[targetId].dRel = distance # from front of car |
||||||
|
# From driver's pov, left is positive |
||||||
|
deg_to_rad = np.pi/180. |
||||||
|
self.pts[targetId].yRel = math.sin(deg_to_rad * cpt['TrkAzimuth']) * distance |
||||||
|
self.pts[targetId].vRel = cpt['TrkRangeRate'] |
||||||
|
self.pts[targetId].aRel = float('nan') |
||||||
|
self.pts[targetId].yvRel = float('nan') |
||||||
|
|
||||||
|
for oldTarget in self.pts.keys(): |
||||||
|
if not oldTarget in currentTargets: |
||||||
|
del self.pts[oldTarget] |
||||||
|
|
||||||
|
ret.points = self.pts.values() |
||||||
|
return ret |
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
RI = RadarInterface(None) |
||||||
|
while 1: |
||||||
|
ret = RI.update() |
||||||
|
print(chr(27) + "[2J") |
||||||
|
print ret |
||||||
|
|
||||||
|
|
@ -0,0 +1,48 @@ |
|||||||
|
from cereal import car |
||||||
|
from selfdrive.car import dbc_dict |
||||||
|
|
||||||
|
class CAR: |
||||||
|
VOLT = "CHEVROLET VOLT PREMIER 2017" |
||||||
|
CADILLAC_CT6 = "CADILLAC CT6 SUPERCRUISE 2018" |
||||||
|
|
||||||
|
class CruiseButtons: |
||||||
|
UNPRESS = 1 |
||||||
|
RES_ACCEL = 2 |
||||||
|
DECEL_SET = 3 |
||||||
|
MAIN = 5 |
||||||
|
CANCEL = 6 |
||||||
|
|
||||||
|
def is_eps_status_ok(eps_status, car_fingerprint): |
||||||
|
valid_eps_status = [] |
||||||
|
if car_fingerprint == CAR.VOLT: |
||||||
|
valid_eps_status += [0, 1] |
||||||
|
elif car_fingerprint == CAR.CADILLAC_CT6: |
||||||
|
valid_eps_status += [0, 1, 4, 5, 6] |
||||||
|
return eps_status in valid_eps_status |
||||||
|
|
||||||
|
def parse_gear_shifter(can_gear): |
||||||
|
if can_gear == 0: |
||||||
|
return car.CarState.GearShifter.park |
||||||
|
elif can_gear == 1: |
||||||
|
return car.CarState.GearShifter.neutral |
||||||
|
elif can_gear == 2: |
||||||
|
return car.CarState.GearShifter.drive |
||||||
|
elif can_gear == 3: |
||||||
|
return car.CarState.GearShifter.reverse |
||||||
|
else: |
||||||
|
return car.CarState.GearShifter.unknown |
||||||
|
|
||||||
|
FINGERPRINTS = { |
||||||
|
CAR.VOLT: [{ |
||||||
|
170: 8, 171: 8, 189: 7, 190: 6, 193: 8, 197: 8, 199: 4, 201: 8, 209: 7, 211: 2, 241: 6, 288: 5, 289: 8, 298: 8, 304: 1, 308: 4, 309: 8, 311: 8, 313: 8, 320: 3, 328: 1, 352: 5, 381: 6, 386: 8, 388: 8, 389: 2, 390: 7, 417: 7, 419: 1, 426: 7, 451: 8, 452: 8, 453: 6, 454: 8, 456: 8, 479: 3, 481: 7, 485: 8, 489: 8, 493: 8, 495: 4, 497: 8, 499: 3, 500: 6, 501: 8, 508: 8, 528: 4, 532: 6, 546: 7, 550: 8, 554: 3, 558: 8, 560: 8, 562: 8, 563: 5, 564: 5, 565: 5, 566: 5, 567: 3, 568: 1, 573: 1, 577: 8, 647: 3, 707: 8, 711: 6, 761: 7, 810: 8, 840: 5, 842: 5, 844: 8, 866: 4, 961: 8, 969: 8, 977: 8, 979: 7, 988: 6, 989: 8, 995: 7, 1001: 8, 1005: 6, 1009: 8, 1017: 8, 1019: 2, 1020: 8, 1105: 6, 1187: 4, 1217: 8, 1221: 5, 1223: 3, 1225: 7, 1227: 4, 1233: 8, 1249: 8, 1257: 6, 1265: 8, 1267: 1, 1273: 3, 1275: 3, 1280: 4, 1300: 8, 1322: 6, 1323: 4, 1328: 4, 1417: 8, 1601: 8, 1905: 7, 1906: 7, 1907: 7, 1910: 7, 1912: 7, 1922: 7, 1927: 7, 1928: 7, 2016: 8, 2020: 8, 2024: 8, 2028: 8 |
||||||
|
}], |
||||||
|
CAR.CADILLAC_CT6: [{ |
||||||
|
190: 6, 193: 8, 197: 8, 199: 4, 201: 8, 209: 7, 211: 2, 241: 6, 249: 8, 288: 5, 298: 8, 304: 1, 309: 8, 313: 8, 320: 3, 322: 7, 328: 1, 336: 1, 338: 6, 340: 6, 352: 5, 354: 5, 356: 8, 368: 3, 372: 5, 381: 8, 386: 8, 393: 7, 398: 8, 407: 7, 413: 8, 417: 7, 419: 1, 422: 4, 426: 7, 431: 8, 442: 8, 451: 8, 452: 8, 453: 6, 455: 7, 456: 8, 458: 5, 460: 5, 462: 4, 463: 3, 479: 3, 481: 7, 485: 8, 487: 8, 489: 8, 495: 4, 497: 8, 499: 3, 500: 6, 501: 8, 508: 8, 528: 5, 532: 6, 534: 2, 554: 3, 560: 8, 562: 8, 563: 5, 564: 5, 565: 5, 567: 5, 569: 3, 573: 1, 577: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 647: 6, 707: 8, 715: 8, 717: 5, 719: 5, 723: 2, 753: 5, 761: 7, 800: 6, 801: 8, 804: 3, 810: 8, 832: 8, 833: 8, 834: 8, 835: 6, 836: 5, 837: 8, 838: 8, 839: 8, 840: 5, 842: 5, 844: 8, 866: 4, 869: 4, 880: 6, 884: 8, 961: 8, 969: 8, 977: 8, 979: 8, 985: 5, 1001: 8, 1005: 6, 1009: 8, 1011: 6, 1013: 1, 1017: 8, 1019: 2, 1020: 8, 1105: 6, 1217: 8, 1221: 5, 1223: 3, 1225: 7, 1233: 8, 1249: 8, 1257: 6, 1259: 8, 1261: 7, 1263: 4, 1265: 8, 1267: 1, 1280: 4, 1296: 4, 1300: 8, 1322: 6, 1417: 8, 1601: 8, 1906: 7, 1907: 7, 1912: 7, 1914: 7, 1918: 7, 1919: 7, 1934: 7, 2016: 8, 2024: 8 |
||||||
|
}], |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
DBC = { |
||||||
|
CAR.VOLT: dbc_dict('gm_global_a_powertrain', 'gm_global_a_object', chassis_dbc='gm_global_a_chassis'), |
||||||
|
CAR.CADILLAC_CT6: dbc_dict('cadillac_ct6_powertrain', 'cadillac_ct6_object', chassis_dbc='cadillac_ct6_chassis'), |
||||||
|
} |
@ -0,0 +1,2 @@ |
|||||||
|
class CAR: |
||||||
|
MOCK = 'mock' |
@ -1 +1 @@ |
|||||||
#define COMMA_VERSION "0.4.6-release" |
#define COMMA_VERSION "0.4.7-release" |
||||||
|
@ -1,3 +1,3 @@ |
|||||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||||
oid sha256:64f33bda4befb1625175d6bce1499fffe88e7fd6b0aae45f1fbeae10c294c9bf |
oid sha256:c5332d30ff64fee5f06b67b2f2f3ac547f135cb112bc4301f7928ba4b58ffb52 |
||||||
size 1622712 |
size 1622712 |
||||||
|
@ -1,3 +1,3 @@ |
|||||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||||
oid sha256:dde86f27dead8dc5b8e404bfc6291a2b002fdb6a7866c6705021c5e92e4ea6cc |
oid sha256:f190a1f3c82d1b4e7bbbbf43a4546c5f3bd7e97a02e51f726dae60164265aa12 |
||||||
size 1171536 |
size 1171544 |
||||||
|
@ -1,3 +1,3 @@ |
|||||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||||
oid sha256:994fb6530dd9afeff691d06137346239ce4dd1e0eeb42f3468ee133f7c41ddba |
oid sha256:dc0facd8b75e340d37d6b911a1674da73b047a268d246932b945631f84860b00 |
||||||
size 1159008 |
size 1159016 |
||||||
|
@ -1,3 +1,3 @@ |
|||||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||||
oid sha256:44325092b1355c2e078671b8d7c63459bb2a902ce55981c1709a23ecb955203d |
oid sha256:f768d0d4189e6acc7e84fbecdcce2d06f1373db40f42d6be5a76a6692891b68d |
||||||
size 13508968 |
size 13522344 |
||||||
|
Loading…
Reference in new issue