parent
							
								
									ea02548652
								
							
						
					
					
						commit
						dda315bcc8
					
				
				 46 changed files with 6157 additions and 0 deletions
			
			
		| @ -0,0 +1,4 @@ | ||||
| ubloxd | ||||
| ubloxd_test | ||||
| params_learner | ||||
| paramsd | ||||
| @ -0,0 +1,25 @@ | ||||
| Import('env', 'common', 'messaging') | ||||
| loc_objs = [ | ||||
|   "locationd_yawrate.cc", | ||||
|   "params_learner.cc", | ||||
|   "paramsd.cc"] | ||||
| loc_libs = [messaging, 'zmq', common, 'capnp', 'kj', 'json', 'json11', 'pthread'] | ||||
| 
 | ||||
| env.Program("paramsd", loc_objs, LIBS=loc_libs) | ||||
| env.SharedLibrary("locationd", loc_objs, LIBS=loc_libs) | ||||
| 
 | ||||
| env.Program("ubloxd", [ | ||||
|   "ubloxd.cc", | ||||
|   "ublox_msg.cc", | ||||
|   "ubloxd_main.cc"], | ||||
|   LIBS=loc_libs) | ||||
| 
 | ||||
| env.Program("ubloxd_test", [ | ||||
|   "ubloxd_test.cc", | ||||
|   "ublox_msg.cc", | ||||
|   "ubloxd_main.cc"], | ||||
|   LIBS=loc_libs) | ||||
|    | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| @ -0,0 +1,11 @@ | ||||
| import math | ||||
| 
 | ||||
| class Filter: | ||||
|   MIN_SPEED = 7 # m/s  (~15.5mph) | ||||
|   MAX_YAW_RATE = math.radians(3) # per second | ||||
| 
 | ||||
| class Calibration: | ||||
|   UNCALIBRATED = 0 | ||||
|   CALIBRATED = 1 | ||||
|   INVALID = 2 | ||||
| 
 | ||||
| @ -0,0 +1,164 @@ | ||||
| #!/usr/bin/env python3 | ||||
| 
 | ||||
| import os | ||||
| import copy | ||||
| import json | ||||
| import numpy as np | ||||
| import cereal.messaging as messaging | ||||
| from selfdrive.locationd.calibration_helpers import Calibration | ||||
| from selfdrive.swaglog import cloudlog | ||||
| from common.params import Params, put_nonblocking | ||||
| from common.transformations.model import model_height | ||||
| from common.transformations.camera import view_frame_from_device_frame, get_view_frame_from_road_frame, \ | ||||
|                                           get_calib_from_vp, H, W, FOCAL | ||||
| 
 | ||||
| MPH_TO_MS = 0.44704 | ||||
| MIN_SPEED_FILTER = 15 * MPH_TO_MS | ||||
| MAX_SPEED_STD = 1.5 | ||||
| MAX_YAW_RATE_FILTER = np.radians(2)  # per second | ||||
| 
 | ||||
| # This is all 20Hz, blocks needed for efficiency | ||||
| BLOCK_SIZE = 100 | ||||
| INPUTS_NEEDED = 5   # allow to update VP every so many frames | ||||
| INPUTS_WANTED = 20   # We want a little bit more than we need for stability | ||||
| WRITE_CYCLES = 10  # write every 1000 cycles | ||||
| VP_INIT = np.array([W/2., H/2.]) | ||||
| 
 | ||||
| # These validity corners were chosen by looking at 1000 | ||||
| # and taking most extreme cases with some margin. | ||||
| VP_VALIDITY_CORNERS = np.array([[W//2 - 120, 300], [W//2 + 120, 520]]) | ||||
| DEBUG = os.getenv("DEBUG") is not None | ||||
| 
 | ||||
| 
 | ||||
| def is_calibration_valid(vp): | ||||
|   return vp[0] > VP_VALIDITY_CORNERS[0,0] and vp[0] < VP_VALIDITY_CORNERS[1,0] and \ | ||||
|          vp[1] > VP_VALIDITY_CORNERS[0,1] and vp[1] < VP_VALIDITY_CORNERS[1,1] | ||||
| 
 | ||||
| 
 | ||||
| def sanity_clip(vp): | ||||
|   if np.isnan(vp).any(): | ||||
|     vp = VP_INIT | ||||
|   return [np.clip(vp[0], VP_VALIDITY_CORNERS[0,0] - 20, VP_VALIDITY_CORNERS[1,0] + 20), | ||||
|           np.clip(vp[1], VP_VALIDITY_CORNERS[0,1] - 20, VP_VALIDITY_CORNERS[1,1] + 20)] | ||||
| 
 | ||||
| 
 | ||||
| def intrinsics_from_vp(vp): | ||||
|   return np.array([ | ||||
|     [FOCAL,   0.,   vp[0]], | ||||
|     [  0.,  FOCAL,  vp[1]], | ||||
|     [  0.,    0.,     1.]]) | ||||
| 
 | ||||
| 
 | ||||
| class Calibrator(): | ||||
|   def __init__(self, param_put=False): | ||||
|     self.param_put = param_put | ||||
|     self.vp = copy.copy(VP_INIT) | ||||
|     self.vps = np.zeros((INPUTS_WANTED, 2)) | ||||
|     self.idx = 0 | ||||
|     self.block_idx = 0 | ||||
|     self.valid_blocks = 0 | ||||
|     self.cal_status = Calibration.UNCALIBRATED | ||||
|     self.just_calibrated = False | ||||
| 
 | ||||
|     # Read calibration | ||||
|     calibration_params = Params().get("CalibrationParams") | ||||
|     if calibration_params: | ||||
|       try: | ||||
|         calibration_params = json.loads(calibration_params) | ||||
|         self.vp = np.array(calibration_params["vanishing_point"]) | ||||
|         if not np.isfinite(self.vp).all(): | ||||
|           self.vp = copy.copy(VP_INIT) | ||||
|         self.vps = np.tile(self.vp, (INPUTS_WANTED, 1)) | ||||
|         self.valid_blocks = calibration_params['valid_blocks'] | ||||
|         if not np.isfinite(self.valid_blocks) or self.valid_blocks < 0: | ||||
|           self.valid_blocks = 0 | ||||
|         self.update_status() | ||||
|       except Exception: | ||||
|         cloudlog.exception("CalibrationParams file found but error encountered") | ||||
| 
 | ||||
|   def update_status(self): | ||||
|     start_status = self.cal_status | ||||
|     if self.valid_blocks < INPUTS_NEEDED: | ||||
|       self.cal_status = Calibration.UNCALIBRATED | ||||
|     else: | ||||
|       self.cal_status = Calibration.CALIBRATED if is_calibration_valid(self.vp) else Calibration.INVALID | ||||
|     end_status = self.cal_status | ||||
| 
 | ||||
|     self.just_calibrated = False | ||||
|     if start_status == Calibration.UNCALIBRATED and end_status == Calibration.CALIBRATED: | ||||
|       self.just_calibrated = True | ||||
| 
 | ||||
|   def handle_cam_odom(self, trans, rot, trans_std, rot_std): | ||||
|     if ((trans[0] > MIN_SPEED_FILTER) and | ||||
|         (trans_std[0] < MAX_SPEED_STD) and | ||||
|          (abs(rot[2]) < MAX_YAW_RATE_FILTER)): | ||||
|       # intrinsics are not eon intrinsics, since this is calibrated frame | ||||
|       intrinsics = intrinsics_from_vp(self.vp) | ||||
|       new_vp = intrinsics.dot(view_frame_from_device_frame.dot(trans)) | ||||
|       new_vp = new_vp[:2]/new_vp[2] | ||||
| 
 | ||||
|       self.vps[self.block_idx] = (self.idx*self.vps[self.block_idx] + (BLOCK_SIZE - self.idx) * new_vp) / float(BLOCK_SIZE) | ||||
|       self.idx = (self.idx + 1) % BLOCK_SIZE | ||||
|       if self.idx == 0: | ||||
|         self.block_idx += 1 | ||||
|         self.valid_blocks = max(self.block_idx, self.valid_blocks) | ||||
|         self.block_idx = self.block_idx % INPUTS_WANTED | ||||
|       raw_vp = np.mean(self.vps[:max(1, self.valid_blocks)], axis=0) | ||||
|       self.vp = sanity_clip(raw_vp) | ||||
|       self.update_status() | ||||
| 
 | ||||
|       if self.param_put and ((self.idx == 0 and self.block_idx == 0) or self.just_calibrated): | ||||
|         cal_params = {"vanishing_point": list(self.vp), | ||||
|                       "valid_blocks": self.valid_blocks} | ||||
|         put_nonblocking("CalibrationParams", json.dumps(cal_params).encode('utf8')) | ||||
|       return new_vp | ||||
|     else: | ||||
|       return None | ||||
| 
 | ||||
|   def send_data(self, pm): | ||||
|     calib = get_calib_from_vp(self.vp) | ||||
|     extrinsic_matrix = get_view_frame_from_road_frame(0, calib[1], calib[2], model_height) | ||||
| 
 | ||||
|     cal_send = messaging.new_message() | ||||
|     cal_send.init('liveCalibration') | ||||
|     cal_send.liveCalibration.calStatus = self.cal_status | ||||
|     cal_send.liveCalibration.calPerc = min(100 * (self.valid_blocks * BLOCK_SIZE + self.idx) // (INPUTS_NEEDED * BLOCK_SIZE), 100) | ||||
|     cal_send.liveCalibration.extrinsicMatrix = [float(x) for x in extrinsic_matrix.flatten()] | ||||
|     cal_send.liveCalibration.rpyCalib = [float(x) for x in calib] | ||||
| 
 | ||||
|     pm.send('liveCalibration', cal_send) | ||||
| 
 | ||||
| 
 | ||||
| def calibrationd_thread(sm=None, pm=None): | ||||
|   if sm is None: | ||||
|     sm = messaging.SubMaster(['cameraOdometry']) | ||||
| 
 | ||||
|   if pm is None: | ||||
|     pm = messaging.PubMaster(['liveCalibration']) | ||||
| 
 | ||||
|   calibrator = Calibrator(param_put=True) | ||||
| 
 | ||||
|   send_counter = 0 | ||||
|   while 1: | ||||
|     sm.update() | ||||
| 
 | ||||
|     if sm.updated['cameraOdometry']: | ||||
|       new_vp = calibrator.handle_cam_odom(sm['cameraOdometry'].trans, | ||||
|                                           sm['cameraOdometry'].rot, | ||||
|                                           sm['cameraOdometry'].transStd, | ||||
|                                           sm['cameraOdometry'].rotStd) | ||||
|     if DEBUG and new_vp is not None: | ||||
|       print('got new vp', new_vp) | ||||
| 
 | ||||
|     # decimate outputs for efficiency | ||||
|     if (send_counter % 5) == 0: | ||||
|       calibrator.send_data(pm) | ||||
|     send_counter += 1 | ||||
| 
 | ||||
| 
 | ||||
| def main(sm=None, pm=None): | ||||
|   calibrationd_thread(sm, pm) | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|   main() | ||||
| @ -0,0 +1,4 @@ | ||||
| lane.cpp | ||||
| gnss.cpp | ||||
| loc*.cpp | ||||
| pos_computer*.cpp | ||||
| @ -0,0 +1,21 @@ | ||||
| import numpy as np | ||||
| import os | ||||
| 
 | ||||
| 
 | ||||
| def gen_chi2_ppf_lookup(max_dim=200): | ||||
|   from scipy.stats import chi2 | ||||
|   table = np.zeros((max_dim, 98)) | ||||
|   for dim in range(1,max_dim): | ||||
|     table[dim] = chi2.ppf(np.arange(.01, .99, .01), dim) | ||||
|   #outfile = open('chi2_lookup_table', 'w') | ||||
|   np.save('chi2_lookup_table', table) | ||||
| 
 | ||||
| 
 | ||||
| def chi2_ppf(p, dim): | ||||
|   table = np.load(os.path.dirname(os.path.realpath(__file__)) + '/chi2_lookup_table.npy') | ||||
|   result = np.interp(p, np.arange(.01, .99, .01), table[dim]) | ||||
|   return result | ||||
| 
 | ||||
| 
 | ||||
| if __name__== "__main__": | ||||
|   gen_chi2_ppf_lookup() | ||||
| @ -0,0 +1,3 @@ | ||||
| version https://git-lfs.github.com/spec/v1 | ||||
| oid sha256:0cc301b3a918cce76bd119a219e31722c6a03fa837927eb10fd84e877966cc3e | ||||
| size 156928 | ||||
| @ -0,0 +1,51 @@ | ||||
| #include <eigen3/Eigen/QR>     | ||||
| #include <eigen3/Eigen/Dense>     | ||||
| #include <iostream>     | ||||
| typedef Eigen::Matrix<double, KDIM*2, 3, Eigen::RowMajor> R3M; | ||||
| typedef Eigen::Matrix<double, KDIM*2, 1> R1M; | ||||
| typedef Eigen::Matrix<double, 3, 1> O1M; | ||||
| typedef Eigen::Matrix<double, 3, 3, Eigen::RowMajor> M3D; | ||||
| 
 | ||||
| void gauss_newton(double *in_x, double *in_poses, double *in_img_positions) { | ||||
|   
 | ||||
|   double res[KDIM*2] = {0}; | ||||
|   double jac[KDIM*6] = {0}; | ||||
| 
 | ||||
|   O1M x(in_x); | ||||
|   O1M delta; | ||||
|   int counter = 0; | ||||
|   while ((delta.squaredNorm() > 0.0001 and counter < 30) or counter == 0){ | ||||
|     res_fun(in_x, in_poses, in_img_positions, res); | ||||
|     jac_fun(in_x, in_poses, in_img_positions, jac); | ||||
|     R1M E(res); R3M J(jac); | ||||
|     delta = (J.transpose()*J).inverse() * J.transpose() * E; | ||||
|     x = x - delta; | ||||
|     memcpy(in_x, x.data(), 3 * sizeof(double)); | ||||
|     counter = counter + 1; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| void compute_pos(double *to_c, double *poses, double *img_positions, double *param, double *pos) { | ||||
|     param[0] = img_positions[KDIM*2-2]; | ||||
|     param[1] = img_positions[KDIM*2-1]; 
 | ||||
|     param[2] = 0.1; | ||||
|     gauss_newton(param, poses, img_positions); | ||||
| 
 | ||||
|     Eigen::Quaterniond q; | ||||
|     q.w() = poses[KDIM*7-4]; | ||||
|     q.x() = poses[KDIM*7-3]; | ||||
|     q.y() = poses[KDIM*7-2]; | ||||
|     q.z() = poses[KDIM*7-1]; | ||||
|     M3D RC(to_c); | ||||
|     Eigen::Matrix3d R = q.normalized().toRotationMatrix(); | ||||
|     Eigen::Matrix3d rot = R * RC.transpose(); | ||||
| 
 | ||||
|     pos[0] = param[0]/param[2]; | ||||
|     pos[1] = param[1]/param[2]; | ||||
|     pos[2] = 1.0/param[2]; | ||||
|     O1M ecef_offset(poses + KDIM*7-7); | ||||
|     O1M ecef_output(pos); | ||||
|     ecef_output = rot*ecef_output + ecef_offset; | ||||
|     memcpy(pos, ecef_output.data(), 3 * sizeof(double)); | ||||
| } | ||||
| @ -0,0 +1,123 @@ | ||||
| #include <eigen3/Eigen/Dense> | ||||
| #include <iostream> | ||||
| 
 | ||||
| typedef Eigen::Matrix<double, DIM, DIM, Eigen::RowMajor> DDM; | ||||
| typedef Eigen::Matrix<double, EDIM, EDIM, Eigen::RowMajor> EEM; | ||||
| typedef Eigen::Matrix<double, DIM, EDIM, Eigen::RowMajor> DEM; | ||||
| 
 | ||||
| void predict(double *in_x, double *in_P, double *in_Q, double dt) { | ||||
|   typedef Eigen::Matrix<double, MEDIM, MEDIM, Eigen::RowMajor> RRM; | ||||
|   
 | ||||
|   double nx[DIM] = {0}; | ||||
|   double in_F[EDIM*EDIM] = {0}; | ||||
| 
 | ||||
|   // functions from sympy
 | ||||
|   f_fun(in_x, dt, nx); | ||||
|   F_fun(in_x, dt, in_F); | ||||
| 
 | ||||
| 
 | ||||
|   EEM F(in_F); | ||||
|   EEM P(in_P); | ||||
|   EEM Q(in_Q); | ||||
| 
 | ||||
|   RRM F_main = F.topLeftCorner(MEDIM, MEDIM); | ||||
|   P.topLeftCorner(MEDIM, MEDIM) = (F_main * P.topLeftCorner(MEDIM, MEDIM)) * F_main.transpose(); | ||||
|   P.topRightCorner(MEDIM, EDIM - MEDIM) = F_main * P.topRightCorner(MEDIM, EDIM - MEDIM); | ||||
|   P.bottomLeftCorner(EDIM - MEDIM, MEDIM) = P.bottomLeftCorner(EDIM - MEDIM, MEDIM) * F_main.transpose(); | ||||
| 
 | ||||
|   P = P + dt*Q; | ||||
| 
 | ||||
|   // copy out state
 | ||||
|   memcpy(in_x, nx, DIM * sizeof(double)); | ||||
|   memcpy(in_P, P.data(), EDIM * EDIM * sizeof(double)); | ||||
| } | ||||
| 
 | ||||
| // note: extra_args dim only correct when null space projecting
 | ||||
| // otherwise 1
 | ||||
| template <int ZDIM, int EADIM, bool MAHA_TEST> | ||||
| void update(double *in_x, double *in_P, Hfun h_fun, Hfun H_fun, Hfun Hea_fun, double *in_z, double *in_R, double *in_ea, double MAHA_THRESHOLD) { | ||||
|   typedef Eigen::Matrix<double, ZDIM, ZDIM, Eigen::RowMajor> ZZM; | ||||
|   typedef Eigen::Matrix<double, ZDIM, DIM, Eigen::RowMajor> ZDM; | ||||
|   typedef Eigen::Matrix<double, Eigen::Dynamic, EDIM, Eigen::RowMajor> XEM; | ||||
|   //typedef Eigen::Matrix<double, EDIM, ZDIM, Eigen::RowMajor> EZM;
 | ||||
|   typedef Eigen::Matrix<double, Eigen::Dynamic, 1> X1M; | ||||
|   typedef Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor> XXM; | ||||
| 
 | ||||
|   double in_hx[ZDIM] = {0}; | ||||
|   double in_H[ZDIM * DIM] = {0}; | ||||
|   double in_H_mod[EDIM * DIM] = {0}; | ||||
|   double delta_x[EDIM] = {0}; | ||||
|   double x_new[DIM] = {0}; | ||||
| 
 | ||||
| 
 | ||||
|   // state x, P
 | ||||
|   Eigen::Matrix<double, ZDIM, 1> z(in_z); | ||||
|   EEM P(in_P); | ||||
|   ZZM pre_R(in_R); | ||||
|   
 | ||||
|   // functions from sympy
 | ||||
|   h_fun(in_x, in_ea, in_hx); | ||||
|   H_fun(in_x, in_ea, in_H); | ||||
|   ZDM pre_H(in_H); 
 | ||||
|   
 | ||||
|   // get y (y = z - hx)
 | ||||
|   Eigen::Matrix<double, ZDIM, 1> pre_y(in_hx); pre_y = z - pre_y; | ||||
|   X1M y; XXM H; XXM R; | ||||
|   if (Hea_fun){ | ||||
|     typedef Eigen::Matrix<double, ZDIM, EADIM, Eigen::RowMajor> ZAM; | ||||
|     double in_Hea[ZDIM * EADIM] = {0}; | ||||
|     Hea_fun(in_x, in_ea, in_Hea); | ||||
|     ZAM Hea(in_Hea); | ||||
|     XXM A = Hea.transpose().fullPivLu().kernel(); | ||||
|    
 | ||||
| 
 | ||||
|     y = A.transpose() * pre_y; | ||||
|     H = A.transpose() * pre_H; | ||||
|     R = A.transpose() * pre_R * A; | ||||
|   } else { | ||||
|     y = pre_y; | ||||
|     H = pre_H; | ||||
|     R = pre_R; | ||||
|   } | ||||
|   // get modified H
 | ||||
|   H_mod_fun(in_x, in_H_mod); | ||||
|   DEM H_mod(in_H_mod); | ||||
|   XEM H_err = H * H_mod; | ||||
|   
 | ||||
|   // Do mahalobis distance test
 | ||||
|   if (MAHA_TEST){ | ||||
|     XXM a = (H_err * P * H_err.transpose() + R).inverse(); | ||||
|     double maha_dist = y.transpose() * a * y; | ||||
|     if (maha_dist > MAHA_THRESHOLD){ | ||||
|       R = 1.0e16 * R; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Outlier resilient weighting
 | ||||
|   double weight = 1;//(1.5)/(1 + y.squaredNorm()/R.sum());
 | ||||
| 
 | ||||
|   // kalman gains and I_KH
 | ||||
|   XXM S = ((H_err * P) * H_err.transpose()) + R/weight; | ||||
|   XEM KT = S.fullPivLu().solve(H_err * P.transpose()); | ||||
|   //EZM K = KT.transpose(); TODO: WHY DOES THIS NOT COMPILE?
 | ||||
|   //EZM K = S.fullPivLu().solve(H_err * P.transpose()).transpose();
 | ||||
|   //std::cout << "Here is the matrix rot:\n" << K << std::endl;
 | ||||
|   EEM I_KH = Eigen::Matrix<double, EDIM, EDIM>::Identity() - (KT.transpose() * H_err); | ||||
| 
 | ||||
|   // update state by injecting dx
 | ||||
|   Eigen::Matrix<double, EDIM, 1> dx(delta_x); | ||||
|   dx  = (KT.transpose() * y); | ||||
|   memcpy(delta_x, dx.data(), EDIM * sizeof(double)); | ||||
|   err_fun(in_x, delta_x, x_new); | ||||
|   Eigen::Matrix<double, DIM, 1> x(x_new); | ||||
|  
 | ||||
|   // update cov 
 | ||||
|   P = ((I_KH * P) * I_KH.transpose()) + ((KT.transpose() * R) * KT); | ||||
| 
 | ||||
|   // copy out state
 | ||||
|   memcpy(in_x, x.data(), DIM * sizeof(double)); | ||||
|   memcpy(in_P, P.data(), EDIM * EDIM * sizeof(double)); | ||||
|   memcpy(in_z, y.data(), y.rows() * sizeof(double)); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| @ -0,0 +1,562 @@ | ||||
| import os | ||||
| from bisect import bisect_right | ||||
| import sympy as sp | ||||
| import numpy as np | ||||
| from numpy import dot | ||||
| from common.ffi_wrapper import compile_code, wrap_compiled | ||||
| from common.sympy_helpers import sympy_into_c | ||||
| from .chi2_lookup import chi2_ppf | ||||
| 
 | ||||
| 
 | ||||
| EXTERNAL_PATH = os.path.dirname(os.path.abspath(__file__)) | ||||
| 
 | ||||
| def solve(a, b): | ||||
|   if a.shape[0] == 1 and a.shape[1] == 1: | ||||
|     #assert np.allclose(b/a[0][0], np.linalg.solve(a, b)) | ||||
|     return b/a[0][0] | ||||
|   else: | ||||
|     return np.linalg.solve(a, b) | ||||
| 
 | ||||
| def null(H, eps=1e-12): | ||||
|   u, s, vh = np.linalg.svd(H) | ||||
|   padding = max(0,np.shape(H)[1]-np.shape(s)[0]) | ||||
|   null_mask = np.concatenate(((s <= eps), np.ones((padding,),dtype=bool)),axis=0) | ||||
|   null_space = np.compress(null_mask, vh, axis=0) | ||||
|   return np.transpose(null_space) | ||||
| 
 | ||||
| def gen_code(name, f_sym, dt_sym, x_sym, obs_eqs, dim_x, dim_err, eskf_params=None, msckf_params=None, maha_test_kinds=[]): | ||||
|   # optional state transition matrix, H modifier | ||||
|   # and err_function if an error-state kalman filter (ESKF) | ||||
|   # is desired. Best described in "Quaternion kinematics | ||||
|   # for the error-state Kalman filter" by Joan Sola | ||||
| 
 | ||||
|   if eskf_params: | ||||
|     err_eqs = eskf_params[0] | ||||
|     inv_err_eqs = eskf_params[1] | ||||
|     H_mod_sym = eskf_params[2] | ||||
|     f_err_sym = eskf_params[3] | ||||
|     x_err_sym = eskf_params[4] | ||||
|   else: | ||||
|     nom_x = sp.MatrixSymbol('nom_x',dim_x,1) | ||||
|     true_x = sp.MatrixSymbol('true_x',dim_x,1) | ||||
|     delta_x = sp.MatrixSymbol('delta_x',dim_x,1) | ||||
|     err_function_sym = sp.Matrix(nom_x + delta_x) | ||||
|     inv_err_function_sym = sp.Matrix(true_x - nom_x) | ||||
|     err_eqs = [err_function_sym, nom_x, delta_x] | ||||
|     inv_err_eqs = [inv_err_function_sym, nom_x, true_x] | ||||
| 
 | ||||
|     H_mod_sym = sp.Matrix(np.eye(dim_x)) | ||||
|     f_err_sym = f_sym | ||||
|     x_err_sym = x_sym | ||||
| 
 | ||||
|   # This configures the multi-state augmentation | ||||
|   # needed for EKF-SLAM with MSCKF (Mourikis et al 2007) | ||||
|   if msckf_params: | ||||
|     msckf = True | ||||
|     dim_main = msckf_params[0]      # size of the main state | ||||
|     dim_augment = msckf_params[1]   # size of one augment state chunk | ||||
|     dim_main_err = msckf_params[2] | ||||
|     dim_augment_err = msckf_params[3] | ||||
|     N = msckf_params[4] | ||||
|     feature_track_kinds = msckf_params[5] | ||||
|     assert dim_main + dim_augment*N == dim_x | ||||
|     assert dim_main_err + dim_augment_err*N == dim_err | ||||
|   else: | ||||
|     msckf = False | ||||
|     dim_main = dim_x | ||||
|     dim_augment = 0 | ||||
|     dim_main_err = dim_err | ||||
|     dim_augment_err = 0 | ||||
|     N = 0 | ||||
| 
 | ||||
|   # linearize with jacobians | ||||
|   F_sym = f_err_sym.jacobian(x_err_sym) | ||||
|   for sym in x_err_sym: | ||||
|     F_sym = F_sym.subs(sym, 0) | ||||
|   for i in range(len(obs_eqs)): | ||||
|     obs_eqs[i].append(obs_eqs[i][0].jacobian(x_sym)) | ||||
|     if msckf and obs_eqs[i][1] in feature_track_kinds: | ||||
|       obs_eqs[i].append(obs_eqs[i][0].jacobian(obs_eqs[i][2])) | ||||
|     else: | ||||
|       obs_eqs[i].append(None) | ||||
| 
 | ||||
|   # collect sympy functions | ||||
|   sympy_functions = [] | ||||
| 
 | ||||
|   # error functions | ||||
|   sympy_functions.append(('err_fun', err_eqs[0], [err_eqs[1], err_eqs[2]])) | ||||
|   sympy_functions.append(('inv_err_fun', inv_err_eqs[0], [inv_err_eqs[1], inv_err_eqs[2]])) | ||||
| 
 | ||||
|   # H modifier for ESKF updates | ||||
|   sympy_functions.append(('H_mod_fun', H_mod_sym, [x_sym])) | ||||
| 
 | ||||
|   # state propagation function | ||||
|   sympy_functions.append(('f_fun', f_sym, [x_sym, dt_sym])) | ||||
|   sympy_functions.append(('F_fun', F_sym, [x_sym, dt_sym])) | ||||
| 
 | ||||
|   # observation functions | ||||
|   for h_sym, kind, ea_sym, H_sym, He_sym in obs_eqs: | ||||
|     sympy_functions.append(('h_%d' % kind, h_sym, [x_sym, ea_sym])) | ||||
|     sympy_functions.append(('H_%d' % kind, H_sym, [x_sym, ea_sym])) | ||||
|     if msckf and kind in feature_track_kinds: | ||||
|       sympy_functions.append(('He_%d' % kind, He_sym, [x_sym, ea_sym])) | ||||
| 
 | ||||
|   # Generate and wrap all th c code | ||||
|   header, code = sympy_into_c(sympy_functions) | ||||
|   extra_header = "#define DIM %d\n" % dim_x | ||||
|   extra_header += "#define EDIM %d\n" % dim_err | ||||
|   extra_header += "#define MEDIM %d\n" % dim_main_err | ||||
|   extra_header += "typedef void (*Hfun)(double *, double *, double *);\n" | ||||
| 
 | ||||
|   extra_header += "\nvoid predict(double *x, double *P, double *Q, double dt);" | ||||
| 
 | ||||
|   extra_post = "" | ||||
| 
 | ||||
|   for h_sym, kind, ea_sym, H_sym, He_sym in obs_eqs: | ||||
|     if msckf and kind in feature_track_kinds: | ||||
|       He_str = 'He_%d' % kind | ||||
|       # ea_dim = ea_sym.shape[0] | ||||
|     else: | ||||
|       He_str = 'NULL' | ||||
|       # ea_dim = 1 # not really dim of ea but makes c function work | ||||
|     maha_thresh = chi2_ppf(0.95, int(h_sym.shape[0])) # mahalanobis distance for outlier detection | ||||
|     maha_test = kind in maha_test_kinds | ||||
|     extra_post += """ | ||||
|       void update_%d(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea) { | ||||
|         update<%d,%d,%d>(in_x, in_P, h_%d, H_%d, %s, in_z, in_R, in_ea, MAHA_THRESH_%d); | ||||
|       } | ||||
|     """ % (kind, h_sym.shape[0], 3, maha_test, kind, kind, He_str, kind) | ||||
|     extra_header += "\nconst static double MAHA_THRESH_%d = %f;" % (kind, maha_thresh) | ||||
|     extra_header += "\nvoid update_%d(double *, double *, double *, double *, double *);" % kind | ||||
| 
 | ||||
|   code += "\n" + extra_header | ||||
|   code += "\n" + open(os.path.join(EXTERNAL_PATH, "ekf_c.c")).read() | ||||
|   code += "\n" + extra_post | ||||
|   header += "\n" + extra_header | ||||
|   compile_code(name, code, header, EXTERNAL_PATH) | ||||
| 
 | ||||
| class EKF_sym(): | ||||
|   def __init__(self, name, Q, x_initial, P_initial, dim_main, dim_main_err, | ||||
|                      N=0, dim_augment=0, dim_augment_err=0, maha_test_kinds=[]): | ||||
|     ''' | ||||
|     Generates process function and all | ||||
|     observation functions for the kalman | ||||
|     filter. | ||||
|     ''' | ||||
|     if N > 0: | ||||
|       self.msckf = True | ||||
|     else: | ||||
|       self.msckf = False | ||||
|     self.N = N | ||||
|     self.dim_augment = dim_augment | ||||
|     self.dim_augment_err = dim_augment_err | ||||
|     self.dim_main = dim_main | ||||
|     self.dim_main_err = dim_main_err | ||||
| 
 | ||||
|     # state | ||||
|     x_initial = x_initial.reshape((-1, 1)) | ||||
|     self.dim_x = x_initial.shape[0] | ||||
|     self.dim_err = P_initial.shape[0] | ||||
|     assert dim_main + dim_augment*N == self.dim_x | ||||
|     assert dim_main_err + dim_augment_err*N == self.dim_err | ||||
| 
 | ||||
|     # kinds that should get mahalanobis distance | ||||
|     # tested for outlier rejection | ||||
|     self.maha_test_kinds = maha_test_kinds | ||||
| 
 | ||||
|     # process noise | ||||
|     self.Q = Q | ||||
| 
 | ||||
|     # rewind stuff | ||||
|     self.rewind_t = [] | ||||
|     self.rewind_states = [] | ||||
|     self.rewind_obscache = [] | ||||
|     self.init_state(x_initial, P_initial, None) | ||||
| 
 | ||||
|     ffi, lib = wrap_compiled(name, EXTERNAL_PATH) | ||||
|     kinds, self.feature_track_kinds = [], [] | ||||
|     for func in dir(lib): | ||||
|       if func[:2] == 'h_': | ||||
|         kinds.append(int(func[2:])) | ||||
|       if func[:3] == 'He_': | ||||
|         self.feature_track_kinds.append(int(func[3:])) | ||||
| 
 | ||||
|     # wrap all the sympy functions | ||||
|     def wrap_1lists(name): | ||||
|       func = eval("lib.%s" % name, {"lib":lib}) | ||||
|       def ret(lst1, out): | ||||
|         func(ffi.cast("double *", lst1.ctypes.data), | ||||
|           ffi.cast("double *", out.ctypes.data)) | ||||
|       return ret | ||||
|     def wrap_2lists(name): | ||||
|       func = eval("lib.%s" % name, {"lib":lib}) | ||||
|       def ret(lst1, lst2, out): | ||||
|         func(ffi.cast("double *", lst1.ctypes.data), | ||||
|           ffi.cast("double *", lst2.ctypes.data), | ||||
|           ffi.cast("double *", out.ctypes.data)) | ||||
|       return ret | ||||
|     def wrap_1list_1float(name): | ||||
|       func = eval("lib.%s" % name, {"lib":lib}) | ||||
|       def ret(lst1, fl, out): | ||||
|         func(ffi.cast("double *", lst1.ctypes.data), | ||||
|           ffi.cast("double", fl), | ||||
|           ffi.cast("double *", out.ctypes.data)) | ||||
|       return ret | ||||
| 
 | ||||
|     self.f = wrap_1list_1float("f_fun") | ||||
|     self.F = wrap_1list_1float("F_fun") | ||||
| 
 | ||||
|     self.err_function = wrap_2lists("err_fun") | ||||
|     self.inv_err_function = wrap_2lists("inv_err_fun") | ||||
|     self.H_mod = wrap_1lists("H_mod_fun") | ||||
| 
 | ||||
|     self.hs, self.Hs, self.Hes = {}, {}, {} | ||||
|     for kind in kinds: | ||||
|       self.hs[kind] = wrap_2lists("h_%d" % kind) | ||||
|       self.Hs[kind] = wrap_2lists("H_%d" % kind) | ||||
|       if self.msckf  and kind in self.feature_track_kinds: | ||||
|         self.Hes[kind] = wrap_2lists("He_%d" % kind) | ||||
| 
 | ||||
|     # wrap the C++ predict function | ||||
|     def _predict_blas(x, P, dt): | ||||
|       lib.predict(ffi.cast("double *", x.ctypes.data), | ||||
|                   ffi.cast("double *", P.ctypes.data), | ||||
|                   ffi.cast("double *", self.Q.ctypes.data), | ||||
|                   ffi.cast("double", dt)) | ||||
|       return x, P | ||||
| 
 | ||||
|     # wrap the C++ update function | ||||
|     def fun_wrapper(f, kind): | ||||
|       f = eval("lib.%s" % f, {"lib": lib}) | ||||
|       def _update_inner_blas(x, P, z, R, extra_args): | ||||
|         f(ffi.cast("double *", x.ctypes.data), | ||||
|           ffi.cast("double *", P.ctypes.data), | ||||
|           ffi.cast("double *", z.ctypes.data), | ||||
|           ffi.cast("double *", R.ctypes.data), | ||||
|           ffi.cast("double *", extra_args.ctypes.data)) | ||||
|         if self.msckf and kind in self.feature_track_kinds: | ||||
|           y = z[:-len(extra_args)] | ||||
|         else: | ||||
|           y = z | ||||
|         return x, P, y | ||||
|       return _update_inner_blas | ||||
| 
 | ||||
|     self._updates = {} | ||||
|     for kind in kinds: | ||||
|       self._updates[kind] = fun_wrapper("update_%d" % kind, kind) | ||||
| 
 | ||||
|     def _update_blas(x, P, kind, z, R, extra_args=[]): | ||||
|         return self._updates[kind](x, P, z, R, extra_args) | ||||
| 
 | ||||
|     # assign the functions | ||||
|     self._predict = _predict_blas | ||||
|     #self._predict = self._predict_python | ||||
|     self._update = _update_blas | ||||
|     #self._update = self._update_python | ||||
| 
 | ||||
| 
 | ||||
|   def init_state(self, state, covs, filter_time): | ||||
|     self.x = np.array(state.reshape((-1, 1))).astype(np.float64) | ||||
|     self.P = np.array(covs).astype(np.float64) | ||||
|     self.filter_time = filter_time | ||||
|     self.augment_times = [0]*self.N | ||||
|     self.rewind_obscache = [] | ||||
|     self.rewind_t = [] | ||||
|     self.rewind_states = [] | ||||
| 
 | ||||
|   def augment(self): | ||||
|     # TODO this is not a generalized way of doing | ||||
|     # this and implies that the augmented states | ||||
|     # are simply the first (dim_augment_state) | ||||
|     # elements of the main state. | ||||
|     assert self.msckf | ||||
|     d1 = self.dim_main | ||||
|     d2 = self.dim_main_err | ||||
|     d3 = self.dim_augment | ||||
|     d4 = self.dim_augment_err | ||||
|     # push through augmented states | ||||
|     self.x[d1:-d3] = self.x[d1+d3:] | ||||
|     self.x[-d3:] = self.x[:d3] | ||||
|     assert self.x.shape == (self.dim_x, 1) | ||||
|     # push through augmented covs | ||||
|     assert self.P.shape == (self.dim_err, self.dim_err) | ||||
|     P_reduced = self.P | ||||
|     P_reduced = np.delete(P_reduced, np.s_[d2:d2+d4], axis=1) | ||||
|     P_reduced = np.delete(P_reduced, np.s_[d2:d2+d4], axis=0) | ||||
|     assert P_reduced.shape == (self.dim_err -d4, self.dim_err -d4) | ||||
|     to_mult = np.zeros((self.dim_err, self.dim_err - d4)) | ||||
|     to_mult[:-d4,:] = np.eye(self.dim_err - d4) | ||||
|     to_mult[-d4:,:d4] = np.eye(d4) | ||||
|     self.P = to_mult.dot(P_reduced.dot(to_mult.T)) | ||||
|     self.augment_times = self.augment_times[1:] | ||||
|     self.augment_times.append(self.filter_time) | ||||
|     assert self.P.shape == (self.dim_err, self.dim_err) | ||||
| 
 | ||||
|   def state(self): | ||||
|     return np.array(self.x).flatten() | ||||
| 
 | ||||
|   def covs(self): | ||||
|     return self.P | ||||
| 
 | ||||
|   def rewind(self, t): | ||||
|     # find where we are rewinding to | ||||
|     idx = bisect_right(self.rewind_t, t) | ||||
|     assert self.rewind_t[idx-1] <= t | ||||
|     assert self.rewind_t[idx] > t    # must be true, or rewind wouldn't be called | ||||
| 
 | ||||
|     # set the state to the time right before that | ||||
|     self.filter_time = self.rewind_t[idx-1] | ||||
|     self.x[:] = self.rewind_states[idx-1][0] | ||||
|     self.P[:] = self.rewind_states[idx-1][1] | ||||
| 
 | ||||
|     # return the observations we rewound over for fast forwarding | ||||
|     ret = self.rewind_obscache[idx:] | ||||
| 
 | ||||
|     # throw away the old future | ||||
|     # TODO: is this making a copy? | ||||
|     self.rewind_t = self.rewind_t[:idx] | ||||
|     self.rewind_states = self.rewind_states[:idx] | ||||
|     self.rewind_obscache = self.rewind_obscache[:idx] | ||||
| 
 | ||||
|     return ret | ||||
| 
 | ||||
|   def checkpoint(self, obs): | ||||
|     # push to rewinder | ||||
|     self.rewind_t.append(self.filter_time) | ||||
|     self.rewind_states.append((np.copy(self.x), np.copy(self.P))) | ||||
|     self.rewind_obscache.append(obs) | ||||
| 
 | ||||
|     # only keep a certain number around | ||||
|     REWIND_TO_KEEP = 512 | ||||
|     self.rewind_t = self.rewind_t[-REWIND_TO_KEEP:] | ||||
|     self.rewind_states = self.rewind_states[-REWIND_TO_KEEP:] | ||||
|     self.rewind_obscache = self.rewind_obscache[-REWIND_TO_KEEP:] | ||||
| 
 | ||||
|   def predict_and_update_batch(self, t, kind, z, R, extra_args=[[]], augment=False): | ||||
|     # TODO handle rewinding at this level" | ||||
| 
 | ||||
|     # rewind | ||||
|     if self.filter_time is not None and t < self.filter_time: | ||||
|       if len(self.rewind_t) == 0 or t < self.rewind_t[0] or t < self.rewind_t[-1] -1.0: | ||||
|         print("observation too old at %.3f with filter at %.3f, ignoring" % (t, self.filter_time)) | ||||
|         return None | ||||
|       rewound = self.rewind(t) | ||||
|     else: | ||||
|       rewound = [] | ||||
| 
 | ||||
|     ret = self._predict_and_update_batch(t, kind, z, R, extra_args, augment) | ||||
| 
 | ||||
|     # optional fast forward | ||||
|     for r in rewound: | ||||
|       self._predict_and_update_batch(*r) | ||||
| 
 | ||||
|     return ret | ||||
| 
 | ||||
|   def _predict_and_update_batch(self, t, kind, z, R, extra_args, augment=False): | ||||
|     """The main kalman filter function | ||||
|     Predicts the state and then updates a batch of observations | ||||
| 
 | ||||
|     dim_x: dimensionality of the state space | ||||
|     dim_z: dimensionality of the observation and depends on kind | ||||
|     n: number of observations | ||||
| 
 | ||||
|     Args: | ||||
|       t                 (float): Time of observation | ||||
|       kind                (int): Type of observation | ||||
|       z         (vec [n,dim_z]): Measurements | ||||
|       R  (mat [n,dim_z, dim_z]): Measurement Noise | ||||
|       extra_args    (list, [n]): Values used in H computations | ||||
|     """ | ||||
|     # initialize time | ||||
|     if self.filter_time is None: | ||||
|       self.filter_time = t | ||||
| 
 | ||||
|     # predict | ||||
|     dt = t - self.filter_time | ||||
|     assert dt >= 0 | ||||
|     self.x, self.P = self._predict(self.x, self.P, dt) | ||||
|     self.filter_time = t | ||||
|     xk_km1, Pk_km1 = np.copy(self.x).flatten(), np.copy(self.P) | ||||
| 
 | ||||
|     # update batch | ||||
|     y = [] | ||||
|     for i in range(len(z)): | ||||
|       # these are from the user, so we canonicalize them | ||||
|       z_i = np.array(z[i], dtype=np.float64, order='F') | ||||
|       R_i = np.array(R[i], dtype=np.float64, order='F') | ||||
|       extra_args_i = np.array(extra_args[i], dtype=np.float64, order='F') | ||||
|       # update | ||||
|       self.x, self.P, y_i = self._update(self.x, self.P, kind, z_i, R_i, extra_args=extra_args_i) | ||||
|       y.append(y_i) | ||||
|     xk_k, Pk_k = np.copy(self.x).flatten(), np.copy(self.P) | ||||
| 
 | ||||
|     if augment: | ||||
|       self.augment() | ||||
| 
 | ||||
|     # checkpoint | ||||
|     self.checkpoint((t, kind, z, R, extra_args)) | ||||
| 
 | ||||
|     return xk_km1, xk_k, Pk_km1, Pk_k, t, kind, y, z, extra_args | ||||
| 
 | ||||
|   def _predict_python(self, x, P, dt): | ||||
|     x_new = np.zeros(x.shape, dtype=np.float64) | ||||
|     self.f(x, dt, x_new) | ||||
| 
 | ||||
|     F = np.zeros(P.shape, dtype=np.float64) | ||||
|     self.F(x, dt, F) | ||||
| 
 | ||||
|     if not self.msckf: | ||||
|       P = dot(dot(F, P), F.T) | ||||
|     else: | ||||
|       # Update the predicted state covariance: | ||||
|       #  Pk+1|k   =  |F*Pii*FT + Q*dt   F*Pij | | ||||
|       #              |PijT*FT           Pjj   | | ||||
|       # Where F is the jacobian of the main state | ||||
|       # predict function, Pii is the main state's | ||||
|       # covariance and Q its process noise. Pij | ||||
|       # is the covariance between the augmented | ||||
|       # states and the main state. | ||||
|       # | ||||
|       d2 = self.dim_main_err    # known at compile time | ||||
|       F_curr = F[:d2, :d2] | ||||
|       P[:d2, :d2] = (F_curr.dot(P[:d2, :d2])).dot(F_curr.T) | ||||
|       P[:d2, d2:] = F_curr.dot(P[:d2, d2:]) | ||||
|       P[d2:, :d2] = P[d2:, :d2].dot(F_curr.T) | ||||
| 
 | ||||
|     P += dt*self.Q | ||||
|     return x_new, P | ||||
| 
 | ||||
|   def _update_python(self, x, P, kind, z, R, extra_args=[]): | ||||
|     # init vars | ||||
|     z = z.reshape((-1, 1)) | ||||
|     h = np.zeros(z.shape, dtype=np.float64) | ||||
|     H = np.zeros((z.shape[0], self.dim_x), dtype=np.float64) | ||||
| 
 | ||||
|     # C functions | ||||
|     self.hs[kind](x, extra_args, h) | ||||
|     self.Hs[kind](x, extra_args, H) | ||||
| 
 | ||||
|     # y is the "loss" | ||||
|     y = z - h | ||||
| 
 | ||||
|     # *** same above this line *** | ||||
| 
 | ||||
|     if self.msckf and kind in self.Hes: | ||||
|       # Do some algebraic magic to decorrelate | ||||
|       He = np.zeros((z.shape[0], len(extra_args)), dtype=np.float64) | ||||
|       self.Hes[kind](x, extra_args, He) | ||||
| 
 | ||||
|       # TODO: Don't call a function here, do projection locally | ||||
|       A = null(He.T) | ||||
| 
 | ||||
|       y = A.T.dot(y) | ||||
|       H = A.T.dot(H) | ||||
|       R = A.T.dot(R.dot(A)) | ||||
| 
 | ||||
|       # TODO If nullspace isn't the dimension we want | ||||
|       if A.shape[1] + He.shape[1] != A.shape[0]: | ||||
|         print('Warning: null space projection failed, measurement ignored') | ||||
|         return x, P, np.zeros(A.shape[0] - He.shape[1]) | ||||
| 
 | ||||
|     # if using eskf | ||||
|     H_mod = np.zeros((x.shape[0], P.shape[0]), dtype=np.float64) | ||||
|     self.H_mod(x, H_mod) | ||||
|     H = H.dot(H_mod) | ||||
| 
 | ||||
|     # Do mahalobis distance test | ||||
|     # currently just runs on msckf observations | ||||
|     # could run on anything if needed | ||||
|     if self.msckf and kind in self.maha_test_kinds: | ||||
|       a = np.linalg.inv(H.dot(P).dot(H.T) + R) | ||||
|       maha_dist = y.T.dot(a.dot(y)) | ||||
|       if maha_dist > chi2_ppf(0.95, y.shape[0]): | ||||
|         R = 10e16*R | ||||
| 
 | ||||
|     # *** same below this line *** | ||||
| 
 | ||||
|     # Outlier resilient weighting as described in: | ||||
|     # "A Kalman Filter for Robust Outlier Detection - Jo-Anne Ting, ..." | ||||
|     weight = 1 #(1.5)/(1 + np.sum(y**2)/np.sum(R)) | ||||
| 
 | ||||
|     S = dot(dot(H, P), H.T) + R/weight | ||||
|     K = solve(S, dot(H, P.T)).T | ||||
|     I_KH = np.eye(P.shape[0]) - dot(K, H) | ||||
| 
 | ||||
|     # update actual state | ||||
|     delta_x = dot(K, y) | ||||
|     P = dot(dot(I_KH, P), I_KH.T) + dot(dot(K, R), K.T) | ||||
| 
 | ||||
|     # inject observed error into state | ||||
|     x_new = np.zeros(x.shape, dtype=np.float64) | ||||
|     self.err_function(x, delta_x, x_new) | ||||
|     return x_new, P, y.flatten() | ||||
| 
 | ||||
|   def maha_test(self, x, P, kind, z, R, extra_args=[], maha_thresh=0.95): | ||||
|     # init vars | ||||
|     z = z.reshape((-1, 1)) | ||||
|     h = np.zeros(z.shape, dtype=np.float64) | ||||
|     H = np.zeros((z.shape[0], self.dim_x), dtype=np.float64) | ||||
| 
 | ||||
|     # C functions | ||||
|     self.hs[kind](x, extra_args, h) | ||||
|     self.Hs[kind](x, extra_args, H) | ||||
| 
 | ||||
|     # y is the "loss" | ||||
|     y = z - h | ||||
| 
 | ||||
|     # if using eskf | ||||
|     H_mod = np.zeros((x.shape[0], P.shape[0]), dtype=np.float64) | ||||
|     self.H_mod(x, H_mod) | ||||
|     H = H.dot(H_mod) | ||||
| 
 | ||||
|     a = np.linalg.inv(H.dot(P).dot(H.T) + R) | ||||
|     maha_dist = y.T.dot(a.dot(y)) | ||||
|     if maha_dist > chi2_ppf(maha_thresh, y.shape[0]): | ||||
|       return False | ||||
|     else: | ||||
|       return True | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|   def rts_smooth(self, estimates, norm_quats=False): | ||||
|     ''' | ||||
|     Returns rts smoothed results of | ||||
|     kalman filter estimates | ||||
| 
 | ||||
|     If the kalman state is augmented with | ||||
|     old states only the main state is smoothed | ||||
|     ''' | ||||
|     xk_n = estimates[-1][0] | ||||
|     Pk_n = estimates[-1][2] | ||||
|     Fk_1 = np.zeros(Pk_n.shape, dtype=np.float64) | ||||
| 
 | ||||
|     states_smoothed = [xk_n] | ||||
|     covs_smoothed = [Pk_n] | ||||
|     for k in range(len(estimates) - 2, -1, -1): | ||||
|       xk1_n = xk_n | ||||
|       if norm_quats: | ||||
|         xk1_n[3:7] /= np.linalg.norm(xk1_n[3:7]) | ||||
|       Pk1_n = Pk_n | ||||
| 
 | ||||
|       xk1_k, _, Pk1_k, _, t2, _, _, _, _ = estimates[k + 1] | ||||
|       _, xk_k, _, Pk_k, t1, _, _, _, _ = estimates[k] | ||||
|       dt = t2 - t1 | ||||
|       self.F(xk_k, dt, Fk_1) | ||||
| 
 | ||||
|       d1 = self.dim_main | ||||
|       d2 = self.dim_main_err | ||||
|       Ck = np.linalg.solve(Pk1_k[:d2,:d2], Fk_1[:d2,:d2].dot(Pk_k[:d2,:d2].T)).T | ||||
|       xk_n = xk_k | ||||
|       delta_x = np.zeros((Pk_n.shape[0], 1), dtype=np.float64) | ||||
|       self.inv_err_function(xk1_k, xk1_n, delta_x) | ||||
|       delta_x[:d2] = Ck.dot(delta_x[:d2]) | ||||
|       x_new = np.zeros((xk_n.shape[0], 1), dtype=np.float64) | ||||
|       self.err_function(xk_k, delta_x, x_new) | ||||
|       xk_n[:d1] = x_new[:d1,0] | ||||
|       Pk_n = Pk_k | ||||
|       Pk_n[:d2,:d2] = Pk_k[:d2,:d2] + Ck.dot(Pk1_n[:d2,:d2] - Pk1_k[:d2,:d2]).dot(Ck.T) | ||||
|       states_smoothed.append(xk_n) | ||||
|       covs_smoothed.append(Pk_n) | ||||
| 
 | ||||
|     return np.flipud(np.vstack(states_smoothed)), np.stack(covs_smoothed, 0)[::-1] | ||||
| @ -0,0 +1,56 @@ | ||||
| bool sane(double track [K + 1][5]) { | ||||
|   double diffs_x [K-1]; | ||||
|   double diffs_y [K-1]; | ||||
|   int i; | ||||
|   for (i = 0; i < K-1; i++) { | ||||
|     diffs_x[i] = fabs(track[i+2][2] - track[i+1][2]); | ||||
|     diffs_y[i] = fabs(track[i+2][3] - track[i+1][3]); | ||||
|   } | ||||
|   for (i = 1; i < K-1; i++) { | ||||
|     if (((diffs_x[i] > 0.05 or diffs_x[i-1] > 0.05) and | ||||
|          (diffs_x[i] > 2*diffs_x[i-1] or | ||||
|           diffs_x[i] < .5*diffs_x[i-1])) or | ||||
|         ((diffs_y[i] > 0.05 or diffs_y[i-1] > 0.05) and | ||||
| 	 (diffs_y[i] > 2*diffs_y[i-1] or | ||||
|           diffs_y[i] < .5*diffs_y[i-1]))){ | ||||
|       return false; | ||||
|     } 
 | ||||
|   } | ||||
|   return true; | ||||
| } | ||||
| 
 | ||||
| void merge_features(double *tracks, double *features, long long *empty_idxs) { | ||||
|   double feature_arr [3000][5]; | ||||
|   memcpy(feature_arr, features, 3000 * 5 * sizeof(double)); | ||||
|   double track_arr [6000][K + 1][5]; | ||||
|   memcpy(track_arr, tracks, (K+1) * 6000 * 5 * sizeof(double)); | ||||
|   int match; | ||||
|   int empty_idx = 0; | ||||
|   int idx; | ||||
|   for (int i = 0; i < 3000; i++) { | ||||
|     match = feature_arr[i][4]; | ||||
|     if (track_arr[match][0][1] == match and track_arr[match][0][2] == 0){ | ||||
|       track_arr[match][0][0] = track_arr[match][0][0] + 1; | ||||
|       track_arr[match][0][1] = feature_arr[i][1]; | ||||
|       track_arr[match][0][2] = 1; | ||||
|       idx = track_arr[match][0][0]; | ||||
|       memcpy(track_arr[match][idx], feature_arr[i], 5 * sizeof(double)); | ||||
|       if (idx == K){ | ||||
|         // label complete
 | ||||
|         track_arr[match][0][3] = 1; | ||||
| 	if (sane(track_arr[match])){ | ||||
|           // label valid
 | ||||
|           track_arr[match][0][4] = 1; 
 | ||||
| 	} | ||||
|       }		
 | ||||
|     } else { | ||||
|       // gen new track with this feature
 | ||||
|       track_arr[empty_idxs[empty_idx]][0][0] = 1; | ||||
|       track_arr[empty_idxs[empty_idx]][0][1] = feature_arr[i][1]; | ||||
|       track_arr[empty_idxs[empty_idx]][0][2] = 1; | ||||
|       memcpy(track_arr[empty_idxs[empty_idx]][1], feature_arr[i], 5 * sizeof(double)); | ||||
|       empty_idx = empty_idx + 1; | ||||
|     } 
 | ||||
|   } | ||||
|   memcpy(tracks, track_arr, (K+1) * 6000 * 5 * sizeof(double)); | ||||
| } | ||||
| @ -0,0 +1,323 @@ | ||||
| import common.transformations.orientation as orient | ||||
| import numpy as np | ||||
| import scipy.optimize as opt | ||||
| import time | ||||
| import os | ||||
| from bisect import bisect_left | ||||
| from common.sympy_helpers import sympy_into_c, quat_matrix_l | ||||
| from common.ffi_wrapper import ffi_wrap, wrap_compiled, compile_code | ||||
| 
 | ||||
| EXTERNAL_PATH = os.path.dirname(os.path.abspath(__file__)) | ||||
| 
 | ||||
| 
 | ||||
| def sane(track): | ||||
|   img_pos = track[1:,2:4] | ||||
|   diffs_x = abs(img_pos[1:,0] - img_pos[:-1,0]) | ||||
|   diffs_y = abs(img_pos[1:,1] - img_pos[:-1,1]) | ||||
|   for i in range(1, len(diffs_x)): | ||||
|     if ((diffs_x[i] > 0.05 or diffs_x[i-1] > 0.05) and \ | ||||
|        (diffs_x[i] > 2*diffs_x[i-1] or \ | ||||
|         diffs_x[i] < .5*diffs_x[i-1])) or \ | ||||
|        ((diffs_y[i] > 0.05 or diffs_y[i-1] > 0.05) and \ | ||||
|        (diffs_y[i] > 2*diffs_y[i-1] or \ | ||||
|         diffs_y[i] < .5*diffs_y[i-1])): | ||||
|       return False | ||||
|   return True | ||||
| 
 | ||||
| class FeatureHandler(): | ||||
|   def __init__(self, K): | ||||
|     self.MAX_TRACKS=6000 | ||||
|     self.K = K | ||||
|     #Array of tracks, each track | ||||
|     #has K 5D features preceded | ||||
|     #by 5 params that inidicate | ||||
|     #[f_idx, last_idx, updated, complete, valid] | ||||
|     # f_idx: idx of current last feature in track | ||||
|     # idx of of last feature in frame | ||||
|     # bool for whether this track has been update | ||||
|     # bool for whether this track is complete | ||||
|     # bool for whether this track is valid | ||||
|     self.tracks = np.zeros((self.MAX_TRACKS, K+1, 5)) | ||||
|     self.tracks[:] = np.nan | ||||
| 
 | ||||
|     # Wrap c code for slow matching | ||||
|     c_header = "\nvoid merge_features(double *tracks, double *features, long long *empty_idxs);" | ||||
|     c_code = "#define K %d\n" % K | ||||
|     c_code += "\n" + open(os.path.join(EXTERNAL_PATH, "feature_handler.c")).read() | ||||
|     ffi, lib = ffi_wrap('feature_handler', c_code, c_header) | ||||
|     def merge_features_c(tracks, features, empty_idxs): | ||||
|       lib.merge_features(ffi.cast("double *", tracks.ctypes.data), | ||||
|                     ffi.cast("double *", features.ctypes.data), | ||||
|                     ffi.cast("long long *", empty_idxs.ctypes.data)) | ||||
| 
 | ||||
|     #self.merge_features = self.merge_features_python | ||||
|     self.merge_features = merge_features_c | ||||
| 
 | ||||
|   def reset(self): | ||||
|     self.tracks[:] = np.nan | ||||
| 
 | ||||
|   def merge_features_python(self, tracks, features, empty_idxs): | ||||
|     empty_idx = 0 | ||||
|     for f in features: | ||||
|       match_idx = int(f[4]) | ||||
|       if tracks[match_idx, 0, 1] == match_idx and tracks[match_idx, 0 ,2] == 0: | ||||
|         tracks[match_idx, 0, 0] += 1 | ||||
|         tracks[match_idx, 0, 1] = f[1] | ||||
|         tracks[match_idx, 0, 2] = 1 | ||||
|         tracks[match_idx, int(tracks[match_idx, 0, 0])] = f | ||||
|         if tracks[match_idx, 0, 0] == self.K: | ||||
|           tracks[match_idx, 0, 3] = 1 | ||||
|           if sane(tracks[match_idx]): | ||||
|             tracks[match_idx, 0, 4] = 1 | ||||
|       else: | ||||
|         if empty_idx == len(empty_idxs): | ||||
|           print('need more empty space') | ||||
|           continue | ||||
|         tracks[empty_idxs[empty_idx], 0, 0] = 1 | ||||
|         tracks[empty_idxs[empty_idx], 0, 1] = f[1] | ||||
|         tracks[empty_idxs[empty_idx], 0, 2] = 1 | ||||
|         tracks[empty_idxs[empty_idx], 1] = f | ||||
|         empty_idx += 1 | ||||
| 
 | ||||
|   def update_tracks(self, features): | ||||
|     t0 = time.time() | ||||
|     last_idxs = np.copy(self.tracks[:,0,1]) | ||||
|     real = np.isfinite(last_idxs) | ||||
|     self.tracks[last_idxs[real].astype(int)] = self.tracks[real] | ||||
|     mask = np.ones(self.MAX_TRACKS, np.bool) | ||||
|     mask[last_idxs[real].astype(int)] = 0 | ||||
|     empty_idxs = np.arange(self.MAX_TRACKS)[mask] | ||||
|     self.tracks[empty_idxs] = np.nan | ||||
|     self.tracks[:,0,2] = 0 | ||||
|     self.merge_features(self.tracks, features, empty_idxs) | ||||
| 
 | ||||
|   def handle_features(self, features): | ||||
|     self.update_tracks(features) | ||||
|     valid_idxs = self.tracks[:,0,4] == 1 | ||||
|     complete_idxs = self.tracks[:,0,3] == 1 | ||||
|     stale_idxs = self.tracks[:,0,2] == 0 | ||||
|     valid_tracks = self.tracks[valid_idxs] | ||||
|     self.tracks[complete_idxs] = np.nan | ||||
|     self.tracks[stale_idxs] = np.nan | ||||
|     return valid_tracks[:,1:,:4].reshape((len(valid_tracks), self.K*4)) | ||||
| 
 | ||||
| def generate_residual(K): | ||||
|   import sympy as sp | ||||
|   from common.sympy_helpers import quat_rotate | ||||
|   x_sym = sp.MatrixSymbol('abr', 3,1) | ||||
|   poses_sym = sp.MatrixSymbol('poses', 7*K,1) | ||||
|   img_pos_sym = sp.MatrixSymbol('img_positions', 2*K,1) | ||||
|   alpha, beta, rho = x_sym | ||||
|   to_c = sp.Matrix(orient.rot_matrix(-np.pi/2, -np.pi/2, 0)) | ||||
|   pos_0 = sp.Matrix(np.array(poses_sym[K*7-7:K*7-4])[:,0]) | ||||
|   q = poses_sym[K*7-4:K*7] | ||||
|   quat_rot = quat_rotate(*q) | ||||
|   rot_g_to_0 = to_c*quat_rot.T | ||||
|   rows = [] | ||||
|   for i in range(K): | ||||
|     pos_i = sp.Matrix(np.array(poses_sym[i*7:i*7+3])[:,0]) | ||||
|     q = poses_sym[7*i+3:7*i+7] | ||||
|     quat_rot = quat_rotate(*q) | ||||
|     rot_g_to_i = to_c*quat_rot.T | ||||
|     rot_0_to_i = rot_g_to_i*(rot_g_to_0.T) | ||||
|     trans_0_to_i = rot_g_to_i*(pos_0 - pos_i) | ||||
|     funct_vec = rot_0_to_i*sp.Matrix([alpha, beta, 1]) + rho*trans_0_to_i | ||||
|     h1, h2, h3 = funct_vec | ||||
|     rows.append(h1/h3 - img_pos_sym[i*2 +0]) | ||||
|     rows.append(h2/h3 - img_pos_sym[i*2 + 1]) | ||||
|   img_pos_residual_sym = sp.Matrix(rows) | ||||
| 
 | ||||
|   # sympy into c | ||||
|   sympy_functions = [] | ||||
|   sympy_functions.append(('res_fun', img_pos_residual_sym, [x_sym, poses_sym, img_pos_sym])) | ||||
|   sympy_functions.append(('jac_fun', img_pos_residual_sym.jacobian(x_sym), [x_sym, poses_sym, img_pos_sym])) | ||||
| 
 | ||||
|   return sympy_functions | ||||
| 
 | ||||
| 
 | ||||
| def generate_orient_error_jac(K): | ||||
|   import sympy as sp | ||||
|   from common.sympy_helpers import quat_rotate | ||||
|   x_sym = sp.MatrixSymbol('abr', 3,1) | ||||
|   dtheta = sp.MatrixSymbol('dtheta', 3,1) | ||||
|   delta_quat = sp.Matrix(np.ones(4)) | ||||
|   delta_quat[1:,:] = sp.Matrix(0.5*dtheta[0:3,:]) | ||||
|   poses_sym = sp.MatrixSymbol('poses', 7*K,1) | ||||
|   img_pos_sym = sp.MatrixSymbol('img_positions', 2*K,1) | ||||
|   alpha, beta, rho = x_sym | ||||
|   to_c = sp.Matrix(orient.rot_matrix(-np.pi/2, -np.pi/2, 0)) | ||||
|   pos_0 = sp.Matrix(np.array(poses_sym[K*7-7:K*7-4])[:,0]) | ||||
|   q = quat_matrix_l(poses_sym[K*7-4:K*7])*delta_quat | ||||
|   quat_rot = quat_rotate(*q) | ||||
|   rot_g_to_0 = to_c*quat_rot.T | ||||
|   rows = [] | ||||
|   for i in range(K): | ||||
|     pos_i = sp.Matrix(np.array(poses_sym[i*7:i*7+3])[:,0]) | ||||
|     q = quat_matrix_l(poses_sym[7*i+3:7*i+7])*delta_quat | ||||
|     quat_rot = quat_rotate(*q) | ||||
|     rot_g_to_i = to_c*quat_rot.T | ||||
|     rot_0_to_i = rot_g_to_i*(rot_g_to_0.T) | ||||
|     trans_0_to_i = rot_g_to_i*(pos_0 - pos_i) | ||||
|     funct_vec = rot_0_to_i*sp.Matrix([alpha, beta, 1]) + rho*trans_0_to_i | ||||
|     h1, h2, h3 = funct_vec | ||||
|     rows.append(h1/h3 - img_pos_sym[i*2 +0]) | ||||
|     rows.append(h2/h3 - img_pos_sym[i*2 + 1]) | ||||
|   img_pos_residual_sym = sp.Matrix(rows) | ||||
| 
 | ||||
|   # sympy into c | ||||
|   sympy_functions = [] | ||||
|   sympy_functions.append(('orient_error_jac', img_pos_residual_sym.jacobian(dtheta), [x_sym, poses_sym, img_pos_sym, dtheta])) | ||||
| 
 | ||||
|   return sympy_functions | ||||
| 
 | ||||
| 
 | ||||
| class LstSqComputer(): | ||||
|   def __init__(self, K, MIN_DEPTH=2, MAX_DEPTH=500, debug=False): | ||||
|     self.to_c = orient.rot_matrix(-np.pi/2, -np.pi/2, 0) | ||||
|     self.MAX_DEPTH = MAX_DEPTH | ||||
|     self.MIN_DEPTH = MIN_DEPTH | ||||
|     self.debug = debug | ||||
|     self.name = 'pos_computer_' + str(K) | ||||
|     if debug: | ||||
|       self.name += '_debug' | ||||
| 
 | ||||
|     try: | ||||
|       dir_path = os.path.dirname(__file__) | ||||
|       deps = [dir_path + '/' + 'feature_handler.py', | ||||
|               dir_path + '/' + 'compute_pos.c'] | ||||
| 
 | ||||
|       outs = [dir_path + '/' + self.name + '.o', | ||||
|               dir_path + '/' + self.name + '.so', | ||||
|               dir_path + '/' + self.name + '.cpp'] | ||||
|       out_times = list(map(os.path.getmtime, outs)) | ||||
|       dep_times = list(map(os.path.getmtime, deps)) | ||||
|       rebuild = os.getenv("REBUILD", False) | ||||
|       if min(out_times) < max(dep_times) or rebuild: | ||||
|         list(map(os.remove, outs)) | ||||
|         # raise the OSError if removing didnt | ||||
|         # raise one to start the compilation | ||||
|         raise OSError() | ||||
|     except OSError as e: | ||||
|       # gen c code for sympy functions | ||||
|       sympy_functions = generate_residual(K) | ||||
|       #if debug: | ||||
|       #  sympy_functions.extend(generate_orient_error_jac(K)) | ||||
|       header, code = sympy_into_c(sympy_functions) | ||||
| 
 | ||||
|       # ffi wrap c code | ||||
|       extra_header = "\nvoid compute_pos(double *to_c, double *in_poses, double *in_img_positions, double *param, double *pos);" | ||||
|       code += "\n#define KDIM %d\n" % K | ||||
|       header += "\n" + extra_header | ||||
|       code += "\n" + open(os.path.join(EXTERNAL_PATH, "compute_pos.c")).read() | ||||
|       compile_code(self.name, code, header, EXTERNAL_PATH) | ||||
|     ffi, lib = wrap_compiled(self.name, EXTERNAL_PATH) | ||||
| 
 | ||||
|     # wrap c functions | ||||
|     #if debug: | ||||
|       #def orient_error_jac(x, poses, img_positions, dtheta): | ||||
|       #  out = np.zeros(((K*2, 3)), dtype=np.float64) | ||||
|       #  lib.orient_error_jac(ffi.cast("double *", x.ctypes.data), | ||||
|       #    ffi.cast("double *", poses.ctypes.data), | ||||
|       #    ffi.cast("double *", img_positions.ctypes.data), | ||||
|       #    ffi.cast("double *", dtheta.ctypes.data), | ||||
|       #    ffi.cast("double *", out.ctypes.data)) | ||||
|       #  return out | ||||
|       #self.orient_error_jac = orient_error_jac | ||||
|     def residual_jac(x, poses, img_positions): | ||||
|       out = np.zeros(((K*2, 3)), dtype=np.float64) | ||||
|       lib.jac_fun(ffi.cast("double *", x.ctypes.data), | ||||
|         ffi.cast("double *", poses.ctypes.data), | ||||
|         ffi.cast("double *", img_positions.ctypes.data), | ||||
|         ffi.cast("double *", out.ctypes.data)) | ||||
|       return out | ||||
|     def residual(x, poses, img_positions): | ||||
|       out = np.zeros((K*2), dtype=np.float64) | ||||
|       lib.res_fun(ffi.cast("double *", x.ctypes.data), | ||||
|           ffi.cast("double *", poses.ctypes.data), | ||||
|           ffi.cast("double *", img_positions.ctypes.data), | ||||
|           ffi.cast("double *", out.ctypes.data)) | ||||
|       return out | ||||
|     self.residual = residual | ||||
|     self.residual_jac = residual_jac | ||||
| 
 | ||||
|     def compute_pos_c(poses, img_positions): | ||||
|       pos = np.zeros(3, dtype=np.float64) | ||||
|       param = np.zeros(3, dtype=np.float64) | ||||
|       # Can't be a view for the ctype | ||||
|       img_positions = np.copy(img_positions) | ||||
|       lib.compute_pos(ffi.cast("double *", self.to_c.ctypes.data), | ||||
|           ffi.cast("double *", poses.ctypes.data), | ||||
|           ffi.cast("double *", img_positions.ctypes.data), | ||||
|           ffi.cast("double *", param.ctypes.data), | ||||
|           ffi.cast("double *", pos.ctypes.data)) | ||||
|       return pos, param | ||||
|     self.compute_pos_c = compute_pos_c | ||||
| 
 | ||||
|   def compute_pos(self, poses, img_positions, debug=False): | ||||
|     pos, param =  self.compute_pos_c(poses, img_positions) | ||||
|     #pos, param =  self.compute_pos_python(poses, img_positions) | ||||
|     depth = 1/param[2] | ||||
|     if debug: | ||||
|       if not self.debug: | ||||
|         raise NotImplementedError("This is not a debug computer") | ||||
|       #orient_err_jac = self.orient_error_jac(param, poses, img_positions, np.zeros(3)).reshape((-1,2,3)) | ||||
|       jac = self.residual_jac(param, poses, img_positions).reshape((-1,2,3)) | ||||
|       res = self.residual(param, poses, img_positions).reshape((-1,2)) | ||||
|       return pos, param, res, jac #, orient_err_jac | ||||
|     elif (self.MIN_DEPTH < depth < self.MAX_DEPTH): | ||||
|       return pos | ||||
|     else: | ||||
|       return None | ||||
| 
 | ||||
|   def gauss_newton(self, fun, jac, x, args): | ||||
|     poses, img_positions = args | ||||
|     delta = 1 | ||||
|     counter = 0 | ||||
|     while abs(np.linalg.norm(delta)) > 1e-4 and counter < 30: | ||||
|       delta = np.linalg.pinv(jac(x, poses, img_positions)).dot(fun(x, poses, img_positions)) | ||||
|       x = x - delta | ||||
|       counter += 1 | ||||
|     return [x] | ||||
| 
 | ||||
|   def compute_pos_python(self, poses, img_positions, check_quality=False): | ||||
|     # This procedure is also described | ||||
|     # in the MSCKF paper (Mourikis et al. 2007) | ||||
|     x = np.array([img_positions[-1][0], | ||||
|                   img_positions[-1][1], 0.1]) | ||||
|     res = opt.leastsq(self.residual, x, Dfun=self.residual_jac, args=(poses, img_positions)) # scipy opt | ||||
|     #res = self.gauss_newton(self.residual, self.residual_jac, x, (poses, img_positions)) # diy gauss_newton | ||||
| 
 | ||||
|     alpha, beta, rho = res[0] | ||||
|     rot_0_to_g = (orient.rotations_from_quats(poses[-1,3:])).dot(self.to_c.T) | ||||
|     return (rot_0_to_g.dot(np.array([alpha, beta, 1])))/rho + poses[-1,:3] | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| ''' | ||||
| EXPERIMENTAL CODE | ||||
| ''' | ||||
| def unroll_shutter(img_positions, poses, v, rot_rates, ecef_pos): | ||||
|   # only speed correction for now | ||||
|   t_roll = 0.016 # 16ms rolling shutter? | ||||
|   vroll, vpitch, vyaw = rot_rates | ||||
|   A = 0.5*np.array([[-1, -vroll, -vpitch, -vyaw], | ||||
|                  [vroll, 0, vyaw, -vpitch], | ||||
|                  [vpitch, -vyaw, 0, vroll], | ||||
|                  [vyaw, vpitch, -vroll, 0]]) | ||||
|   q_dot = A.dot(poses[-1][3:7]) | ||||
|   v = np.append(v, q_dot) | ||||
|   v = np.array([v[0], v[1], v[2],0,0,0,0]) | ||||
|   current_pose = poses[-1] + v*0.05 | ||||
|   poses = np.vstack((current_pose, poses)) | ||||
|   dt = -img_positions[:,1]*t_roll/0.48 | ||||
|   errs = project(poses, ecef_pos) - project(poses + np.atleast_2d(dt).T.dot(np.atleast_2d(v)), ecef_pos) | ||||
|   return img_positions - errs | ||||
| 
 | ||||
| def project(poses, ecef_pos): | ||||
|   img_positions = np.zeros((len(poses), 2)) | ||||
|   for i, p in enumerate(poses): | ||||
|     cam_frame = orient.rotations_from_quats(p[3:]).T.dot(ecef_pos - p[:3]) | ||||
|     img_positions[i] = np.array([cam_frame[1]/cam_frame[0], cam_frame[2]/cam_frame[0]]) | ||||
|   return img_positions | ||||
| 
 | ||||
| @ -0,0 +1,105 @@ | ||||
| #!/usr/bin/env python3 | ||||
| import numpy as np | ||||
| from . import gnss_model | ||||
| 
 | ||||
| from .kalman_helpers import ObservationKind | ||||
| from .ekf_sym import EKF_sym | ||||
| from selfdrive.locationd.kalman.loc_kf import parse_pr, parse_prr | ||||
| 
 | ||||
| class States(): | ||||
|   ECEF_POS = slice(0,3) # x, y and z in ECEF in meters | ||||
|   ECEF_VELOCITY = slice(3,6) | ||||
|   CLOCK_BIAS = slice(6, 7) # clock bias in light-meters, | ||||
|   CLOCK_DRIFT = slice(7, 8) # clock drift in light-meters/s, | ||||
|   CLOCK_ACCELERATION = slice(8, 9) # clock acceleration in light-meters/s**2 | ||||
|   GLONASS_BIAS = slice(9, 10) # clock drift in light-meters/s, | ||||
|   GLONASS_FREQ_SLOPE = slice(10, 11) # GLONASS bias in m expressed as bias + freq_num*freq_slope | ||||
| 
 | ||||
| 
 | ||||
| class GNSSKalman(): | ||||
|   def __init__(self, N=0, max_tracks=3000): | ||||
|     x_initial = np.array([-2712700.6008, -4281600.6679, 3859300.1830, | ||||
|                           0, 0, 0, | ||||
|                           0, 0, 0, | ||||
|                           0, 0]) | ||||
| 
 | ||||
|     # state covariance | ||||
|     P_initial = np.diag([10000**2, 10000**2, 10000**2, | ||||
|                          10**2, 10**2, 10**2, | ||||
|                          (2000000)**2, (100)**2, (0.5)**2, | ||||
|                          (10)**2, (1)**2]) | ||||
| 
 | ||||
|     # process noise | ||||
|     Q = np.diag([0.3**2, 0.3**2, 0.3**2, | ||||
|                  3**2, 3**2, 3**2, | ||||
|                  (.1)**2, (0)**2, (0.01)**2, | ||||
|                  .1**2, (.01)**2]) | ||||
| 
 | ||||
|     self.dim_state = x_initial.shape[0] | ||||
| 
 | ||||
|     # mahalanobis outlier rejection | ||||
|     maha_test_kinds = []#ObservationKind.PSEUDORANGE_RATE, ObservationKind.PSEUDORANGE, ObservationKind.PSEUDORANGE_GLONASS] | ||||
| 
 | ||||
|     name = 'gnss' | ||||
|     gnss_model.gen_model(name, self.dim_state, maha_test_kinds) | ||||
| 
 | ||||
|     # init filter | ||||
|     self.filter = EKF_sym(name, Q, x_initial, P_initial, self.dim_state, self.dim_state, maha_test_kinds=maha_test_kinds) | ||||
| 
 | ||||
|   @property | ||||
|   def x(self): | ||||
|     return self.filter.state() | ||||
| 
 | ||||
|   @property | ||||
|   def P(self): | ||||
|     return self.filter.covs() | ||||
| 
 | ||||
|   def predict(self, t): | ||||
|     return self.filter.predict(t) | ||||
| 
 | ||||
|   def rts_smooth(self, estimates): | ||||
|     return self.filter.rts_smooth(estimates, norm_quats=False) | ||||
| 
 | ||||
|   def init_state(self, state, covs_diag=None, covs=None, filter_time=None): | ||||
|     if covs_diag is not None: | ||||
|       P = np.diag(covs_diag) | ||||
|     elif covs is not None: | ||||
|       P = covs | ||||
|     else: | ||||
|       P = self.filter.covs() | ||||
|     self.filter.init_state(state, P, filter_time) | ||||
| 
 | ||||
|   def predict_and_observe(self, t, kind, data): | ||||
|     if len(data) > 0: | ||||
|       data = np.atleast_2d(data) | ||||
|     if kind == ObservationKind.PSEUDORANGE_GPS or kind == ObservationKind.PSEUDORANGE_GLONASS: | ||||
|       r = self.predict_and_update_pseudorange(data, t, kind) | ||||
|     elif kind == ObservationKind.PSEUDORANGE_RATE_GPS or kind == ObservationKind.PSEUDORANGE_RATE_GLONASS: | ||||
|       r = self.predict_and_update_pseudorange_rate(data, t, kind) | ||||
|     return r | ||||
| 
 | ||||
|   def predict_and_update_pseudorange(self, meas, t, kind): | ||||
|     R = np.zeros((len(meas), 1, 1)) | ||||
|     sat_pos_freq = np.zeros((len(meas), 4)) | ||||
|     z = np.zeros((len(meas), 1)) | ||||
|     for i, m in enumerate(meas): | ||||
|       z_i, R_i, sat_pos_freq_i = parse_pr(m) | ||||
|       sat_pos_freq[i,:] = sat_pos_freq_i | ||||
|       z[i,:] = z_i | ||||
|       R[i,:,:] = R_i | ||||
|     return self.filter.predict_and_update_batch(t, kind, z, R, sat_pos_freq) | ||||
| 
 | ||||
|   def predict_and_update_pseudorange_rate(self, meas, t, kind): | ||||
|     R = np.zeros((len(meas), 1, 1)) | ||||
|     z = np.zeros((len(meas), 1)) | ||||
|     sat_pos_vel = np.zeros((len(meas), 6)) | ||||
|     for i, m in enumerate(meas): | ||||
|       z_i, R_i, sat_pos_vel_i = parse_prr(m) | ||||
|       sat_pos_vel[i] = sat_pos_vel_i | ||||
|       R[i,:,:] = R_i | ||||
|       z[i, :] = z_i | ||||
|     return self.filter.predict_and_update_batch(t, kind, z, R, sat_pos_vel) | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|   GNSSKalman() | ||||
| @ -0,0 +1,93 @@ | ||||
| import numpy as np | ||||
| import sympy as sp | ||||
| 
 | ||||
| import os | ||||
| from .kalman_helpers import ObservationKind | ||||
| from .ekf_sym import gen_code | ||||
| from common.sympy_helpers import cross, euler_rotate, quat_rotate, quat_matrix_l, quat_matrix_r | ||||
| 
 | ||||
| def gen_model(name, dim_state, maha_test_kinds): | ||||
| 
 | ||||
|   # check if rebuild is needed | ||||
|   try: | ||||
|     dir_path = os.path.dirname(__file__) | ||||
|     deps = [dir_path + '/' + 'ekf_c.c', | ||||
|             dir_path + '/' + 'ekf_sym.py', | ||||
|             dir_path + '/' + 'gnss_model.py', | ||||
|             dir_path + '/' + 'gnss_kf.py'] | ||||
| 
 | ||||
|     outs = [dir_path + '/' + name + '.o', | ||||
|             dir_path + '/' + name + '.so', | ||||
|             dir_path + '/' + name + '.cpp'] | ||||
|     out_times = list(map(os.path.getmtime, outs)) | ||||
|     dep_times = list(map(os.path.getmtime, deps)) | ||||
|     rebuild = os.getenv("REBUILD", False) | ||||
|     if min(out_times) > max(dep_times) and not rebuild: | ||||
|       return | ||||
|     list(map(os.remove, outs)) | ||||
|   except OSError as e: | ||||
|     pass | ||||
| 
 | ||||
|   # make functions and jacobians with sympy | ||||
|   # state variables | ||||
|   state_sym = sp.MatrixSymbol('state', dim_state, 1) | ||||
|   state = sp.Matrix(state_sym) | ||||
|   x,y,z = state[0:3,:] | ||||
|   v = state[3:6,:] | ||||
|   vx, vy, vz = v | ||||
|   cb, cd, ca = state[6:9,:] | ||||
|   glonass_bias, glonass_freq_slope = state[9:11,:] | ||||
| 
 | ||||
|   dt = sp.Symbol('dt') | ||||
| 
 | ||||
|   state_dot = sp.Matrix(np.zeros((dim_state, 1))) | ||||
|   state_dot[:3,:] = v | ||||
|   state_dot[6,0] = cd | ||||
|   state_dot[7,0] = ca | ||||
| 
 | ||||
|   # Basic descretization, 1st order intergrator | ||||
|   # Can be pretty bad if dt is big | ||||
|   f_sym = state + dt*state_dot | ||||
| 
 | ||||
| 
 | ||||
|   # | ||||
|   # Observation functions | ||||
|   # | ||||
| 
 | ||||
|   # extra args | ||||
|   sat_pos_freq_sym = sp.MatrixSymbol('sat_pos', 4, 1) | ||||
|   sat_pos_vel_sym = sp.MatrixSymbol('sat_pos_vel', 6, 1) | ||||
|   sat_los_sym = sp.MatrixSymbol('sat_los', 3, 1) | ||||
|   orb_epos_sym = sp.MatrixSymbol('orb_epos_sym', 3, 1) | ||||
| 
 | ||||
|   # expand extra args | ||||
|   sat_x, sat_y, sat_z, glonass_freq = sat_pos_freq_sym | ||||
|   sat_vx, sat_vy, sat_vz = sat_pos_vel_sym[3:] | ||||
|   los_x, los_y, los_z = sat_los_sym | ||||
|   orb_x, orb_y, orb_z = orb_epos_sym | ||||
| 
 | ||||
|   h_pseudorange_sym = sp.Matrix([sp.sqrt( | ||||
|                                   (x - sat_x)**2 + | ||||
|                                   (y - sat_y)**2 + | ||||
|                                   (z - sat_z)**2) + | ||||
|                                   cb]) | ||||
| 
 | ||||
|   h_pseudorange_glonass_sym = sp.Matrix([sp.sqrt( | ||||
|                                   (x - sat_x)**2 + | ||||
|                                   (y - sat_y)**2 + | ||||
|                                   (z - sat_z)**2) + | ||||
|                                   cb + glonass_bias + glonass_freq_slope*glonass_freq]) | ||||
| 
 | ||||
|   los_vector = (sp.Matrix(sat_pos_vel_sym[0:3]) - sp.Matrix([x, y, z])) | ||||
|   los_vector = los_vector / sp.sqrt(los_vector[0]**2 + los_vector[1]**2 + los_vector[2]**2) | ||||
|   h_pseudorange_rate_sym = sp.Matrix([los_vector[0]*(sat_vx - vx) + | ||||
|                                          los_vector[1]*(sat_vy - vy) + | ||||
|                                          los_vector[2]*(sat_vz - vz) + | ||||
|                                          cd]) | ||||
| 
 | ||||
|   obs_eqs = [[h_pseudorange_sym, ObservationKind.PSEUDORANGE_GPS, sat_pos_freq_sym], | ||||
|              [h_pseudorange_glonass_sym, ObservationKind.PSEUDORANGE_GLONASS, sat_pos_freq_sym], | ||||
|              [h_pseudorange_rate_sym, ObservationKind.PSEUDORANGE_RATE_GPS, sat_pos_vel_sym], | ||||
|              [h_pseudorange_rate_sym, ObservationKind.PSEUDORANGE_RATE_GLONASS, sat_pos_vel_sym]] | ||||
| 
 | ||||
|   gen_code(name, f_sym, dt, state_sym, obs_eqs, dim_state, dim_state, maha_test_kinds=maha_test_kinds) | ||||
| @ -0,0 +1,165 @@ | ||||
| import numpy as np | ||||
| import os | ||||
| from bisect import bisect | ||||
| from tqdm import tqdm | ||||
| 
 | ||||
| 
 | ||||
| class ObservationKind(): | ||||
|   UNKNOWN = 0 | ||||
|   NO_OBSERVATION = 1 | ||||
|   GPS_NED = 2 | ||||
|   ODOMETRIC_SPEED = 3 | ||||
|   PHONE_GYRO = 4 | ||||
|   GPS_VEL = 5 | ||||
|   PSEUDORANGE_GPS = 6 | ||||
|   PSEUDORANGE_RATE_GPS = 7 | ||||
|   SPEED = 8 | ||||
|   NO_ROT = 9 | ||||
|   PHONE_ACCEL = 10 | ||||
|   ORB_POINT = 11 | ||||
|   ECEF_POS = 12 | ||||
|   CAMERA_ODO_TRANSLATION = 13 | ||||
|   CAMERA_ODO_ROTATION = 14 | ||||
|   ORB_FEATURES = 15 | ||||
|   MSCKF_TEST = 16 | ||||
|   FEATURE_TRACK_TEST = 17 | ||||
|   LANE_PT = 18 | ||||
|   IMU_FRAME = 19 | ||||
|   PSEUDORANGE_GLONASS = 20 | ||||
|   PSEUDORANGE_RATE_GLONASS = 21 | ||||
|   PSEUDORANGE = 22 | ||||
|   PSEUDORANGE_RATE = 23 | ||||
| 
 | ||||
|   names = ['Unknown', | ||||
|            'No observation', | ||||
|            'GPS NED', | ||||
|            'Odometric speed', | ||||
|            'Phone gyro', | ||||
|            'GPS velocity', | ||||
|            'GPS pseudorange', | ||||
|            'GPS pseudorange rate', | ||||
|            'Speed', | ||||
|            'No rotation', | ||||
|            'Phone acceleration', | ||||
|            'ORB point', | ||||
|            'ECEF pos', | ||||
|            'camera odometric translation', | ||||
|            'camera odometric rotation', | ||||
|            'ORB features', | ||||
|            'MSCKF test', | ||||
|            'Feature track test', | ||||
|            'Lane ecef point', | ||||
|            'imu frame eulers', | ||||
|            'GLONASS pseudorange', | ||||
|            'GLONASS pseudorange rate'] | ||||
| 
 | ||||
|   @classmethod | ||||
|   def to_string(cls, kind): | ||||
|     return cls.names[kind] | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| SAT_OBS = [ObservationKind.PSEUDORANGE_GPS, | ||||
|            ObservationKind.PSEUDORANGE_RATE_GPS, | ||||
|            ObservationKind.PSEUDORANGE_GLONASS, | ||||
|            ObservationKind.PSEUDORANGE_RATE_GLONASS] | ||||
| 
 | ||||
| 
 | ||||
| def run_car_ekf_offline(kf, observations_by_kind): | ||||
|   from laika.raw_gnss import GNSSMeasurement | ||||
|   observations = [] | ||||
|   # create list of observations with element format: [kind, time, data] | ||||
|   for kind in observations_by_kind: | ||||
|     for t, data in zip(observations_by_kind[kind][0], observations_by_kind[kind][1]): | ||||
|       observations.append([t, kind, data]) | ||||
|   observations.sort(key=lambda obs: obs[0]) | ||||
| 
 | ||||
|   times, estimates = run_observations_through_filter(kf, observations) | ||||
| 
 | ||||
|   forward_states = np.stack(e[1] for e in estimates) | ||||
|   forward_covs = np.stack(e[3] for e in estimates) | ||||
|   smoothed_states, smoothed_covs = kf.rts_smooth(estimates) | ||||
| 
 | ||||
|   observations_dict = {} | ||||
|   # TODO assuming observations and estimates | ||||
|   # are same length may not work with VO | ||||
|   for e in estimates: | ||||
|     t = e[4] | ||||
|     kind = str(int(e[5])) | ||||
|     res = e[6] | ||||
|     z = e[7] | ||||
|     ea = e[8] | ||||
|     if len(z) == 0: | ||||
|       continue | ||||
|     if kind not in observations_dict: | ||||
|       observations_dict[kind] = {} | ||||
|       observations_dict[kind]['t'] = np.array(len(z)*[t]) | ||||
|       observations_dict[kind]['z'] = np.array(z) | ||||
|       observations_dict[kind]['ea'] = np.array(ea) | ||||
|       observations_dict[kind]['residual'] = np.array(res) | ||||
|     else: | ||||
|       observations_dict[kind]['t'] = np.append(observations_dict[kind]['t'], np.array(len(z)*[t])) | ||||
|       observations_dict[kind]['z'] = np.vstack((observations_dict[kind]['z'], np.array(z))) | ||||
|       observations_dict[kind]['ea'] = np.vstack((observations_dict[kind]['ea'], np.array(ea))) | ||||
|       observations_dict[kind]['residual'] = np.vstack((observations_dict[kind]['residual'], np.array(res))) | ||||
| 
 | ||||
|   # add svIds to gnss data | ||||
|   for kind in map(str, SAT_OBS): | ||||
|     if int(kind) in observations_by_kind and kind in observations_dict: | ||||
|       observations_dict[kind]['svIds'] = np.array([]) | ||||
|       observations_dict[kind]['CNO'] = np.array([]) | ||||
|       observations_dict[kind]['std'] = np.array([]) | ||||
|       for obs in observations_by_kind[int(kind)][1]: | ||||
|         observations_dict[kind]['svIds'] = np.append(observations_dict[kind]['svIds'], | ||||
|                                                      np.array([obs[:,GNSSMeasurement.PRN]])) | ||||
|         observations_dict[kind]['std'] = np.append(observations_dict[kind]['std'], | ||||
|                                                    np.array([obs[:,GNSSMeasurement.PR_STD]])) | ||||
|   return smoothed_states, smoothed_covs, forward_states, forward_covs, times, observations_dict | ||||
| 
 | ||||
| 
 | ||||
| def run_observations_through_filter(kf, observations, filter_time=None): | ||||
|   estimates = [] | ||||
| 
 | ||||
|   for obs in tqdm(observations): | ||||
|     t = obs[0] | ||||
|     kind = obs[1] | ||||
|     data = obs[2] | ||||
|     estimates.append(kf.predict_and_observe(t, kind, data)) | ||||
|   times = [x[4] for x in estimates] | ||||
|   return times, estimates | ||||
| 
 | ||||
| 
 | ||||
| def save_residuals_plot(obs, save_path, data_name): | ||||
|   import matplotlib.pyplot as plt | ||||
|   import mpld3 | ||||
|   fig = plt.figure(figsize=(10,20)) | ||||
|   fig.suptitle('Residuals of ' + data_name, fontsize=24) | ||||
|   n = len(list(obs.keys())) | ||||
|   start_times = [obs[kind]['t'][0] for kind in obs] | ||||
|   start_time = min(start_times) | ||||
|   xlims = [start_time + 3, start_time + 60] | ||||
| 
 | ||||
|   for i, kind in enumerate(obs): | ||||
|     ax = fig.add_subplot(n, 1, i+1) | ||||
|     ax.set_xlim(xlims) | ||||
|     t = obs[kind]['t'] | ||||
|     res = obs[kind]['residual'] | ||||
|     start_idx = bisect(t, xlims[0]) | ||||
|     if len(res) == start_idx: | ||||
|       continue | ||||
|     ylim = max(np.linalg.norm(res[start_idx:], axis=1)) | ||||
|     ax.set_ylim([-ylim, ylim]) | ||||
|     if int(kind) in SAT_OBS: | ||||
|       svIds = obs[kind]['svIds'] | ||||
|       for svId in set(svIds): | ||||
|         svId_idx = (svIds == svId) | ||||
|         t = obs[kind]['t'][svId_idx] | ||||
|         res = obs[kind]['residual'][svId_idx] | ||||
|         ax.plot(t, res, label='SV ' + str(int(svId))) | ||||
|         ax.legend(loc='right') | ||||
|     else: | ||||
|       ax.plot(t, res) | ||||
|     plt.title('Residual of kind ' + ObservationKind.to_string(int(kind)), fontsize=20) | ||||
|   plt.tight_layout() | ||||
|   os.makedirs(save_path) | ||||
|   mpld3.save_html(fig, save_path + 'residuals_plot.html') | ||||
| @ -0,0 +1,323 @@ | ||||
| #!/usr/bin/env python3 | ||||
| import numpy as np | ||||
| from . import loc_model | ||||
| 
 | ||||
| from .kalman_helpers import ObservationKind | ||||
| from .ekf_sym import EKF_sym | ||||
| from .feature_handler import LstSqComputer, unroll_shutter | ||||
| from laika.raw_gnss import GNSSMeasurement | ||||
| 
 | ||||
| 
 | ||||
| def parse_prr(m): | ||||
|   sat_pos_vel_i = np.concatenate((m[GNSSMeasurement.SAT_POS], | ||||
|                                   m[GNSSMeasurement.SAT_VEL])) | ||||
|   R_i = np.atleast_2d(m[GNSSMeasurement.PRR_STD]**2) | ||||
|   z_i = m[GNSSMeasurement.PRR] | ||||
|   return z_i, R_i, sat_pos_vel_i | ||||
| 
 | ||||
| 
 | ||||
| def parse_pr(m): | ||||
|   pseudorange = m[GNSSMeasurement.PR] | ||||
|   pseudorange_stdev = m[GNSSMeasurement.PR_STD] | ||||
|   sat_pos_freq_i = np.concatenate((m[GNSSMeasurement.SAT_POS], | ||||
|                                    np.array([m[GNSSMeasurement.GLONASS_FREQ]]))) | ||||
|   z_i = np.atleast_1d(pseudorange) | ||||
|   R_i = np.atleast_2d(pseudorange_stdev**2) | ||||
|   return z_i, R_i, sat_pos_freq_i | ||||
| 
 | ||||
| 
 | ||||
| class States(): | ||||
|   ECEF_POS = slice(0,3) # x, y and z in ECEF in meters | ||||
|   ECEF_ORIENTATION = slice(3,7) # quat for pose of phone in ecef | ||||
|   ECEF_VELOCITY = slice(7,10) # ecef velocity in m/s | ||||
|   ANGULAR_VELOCITY = slice(10, 13) # roll, pitch and yaw rates in device frame in radians/s | ||||
|   CLOCK_BIAS = slice(13, 14) # clock bias in light-meters, | ||||
|   CLOCK_DRIFT = slice(14, 15) # clock drift in light-meters/s, | ||||
|   GYRO_BIAS = slice(15, 18) # roll, pitch and yaw biases | ||||
|   ODO_SCALE = slice(18, 19) # odometer scale | ||||
|   ACCELERATION = slice(19, 22) # Acceleration in device frame in m/s**2 | ||||
|   FOCAL_SCALE = slice(22, 23) # focal length scale | ||||
|   IMU_OFFSET = slice(23,26) # imu offset angles in radians | ||||
|   GLONASS_BIAS = slice(26,27) # GLONASS bias in m expressed as bias + freq_num*freq_slope | ||||
|   GLONASS_FREQ_SLOPE = slice(27, 28) # GLONASS bias in m expressed as bias + freq_num*freq_slope | ||||
|   CLOCK_ACCELERATION = slice(28, 29) # clock acceleration in light-meters/s**2, | ||||
| 
 | ||||
| 
 | ||||
| class LocKalman(): | ||||
|   def __init__(self, N=0, max_tracks=3000): | ||||
|     x_initial = np.array([-2.7e6, 4.2e6, 3.8e6, | ||||
|                           1, 0, 0, 0, | ||||
|                           0, 0, 0, | ||||
|                           0, 0, 0, | ||||
|                           0, 0, | ||||
|                           0, 0, 0, | ||||
|                           1, | ||||
|                           0, 0, 0, | ||||
|                           1, | ||||
|                           0, 0, 0, | ||||
|                           0, 0, | ||||
|                           0]) | ||||
| 
 | ||||
|     # state covariance | ||||
|     P_initial = np.diag([10000**2, 10000**2, 10000**2, | ||||
|                          10**2, 10**2, 10**2, | ||||
|                          10**2, 10**2, 10**2, | ||||
|                          1**2, 1**2, 1**2, | ||||
|                          (200000)**2, (100)**2, | ||||
|                          0.05**2, 0.05**2, 0.05**2, | ||||
|                          0.02**2, | ||||
|                          1**2, 1**2, 1**2, | ||||
|                          0.01**2, | ||||
|                          (0.01)**2, (0.01)**2, (0.01)**2, | ||||
|                          10**2, 1**2, | ||||
|                          0.05**2]) | ||||
| 
 | ||||
|     # process noise | ||||
|     Q = np.diag([0.03**2, 0.03**2, 0.03**2, | ||||
|                  0.0**2, 0.0**2, 0.0**2, | ||||
|                  0.0**2, 0.0**2, 0.0**2, | ||||
|                  0.1**2, 0.1**2, 0.1**2, | ||||
|                  (.1)**2, (0.0)**2, | ||||
|                  (0.005/100)**2, (0.005/100)**2, (0.005/100)**2, | ||||
|                  (0.02/100)**2, | ||||
|                  3**2, 3**2, 3**2, | ||||
|                  0.001**2, | ||||
|                  (0.05/60)**2, (0.05/60)**2, (0.05/60)**2, | ||||
|                  (.1)**2, (.01)**2, | ||||
|                  0.005**2]) | ||||
| 
 | ||||
|     self.obs_noise = {ObservationKind.ODOMETRIC_SPEED: np.atleast_2d(0.2**2), | ||||
|                       ObservationKind.PHONE_GYRO: np.diag([0.025**2, 0.025**2, 0.025**2]), | ||||
|                       ObservationKind.PHONE_ACCEL:  np.diag([.5**2, .5**2, .5*2]), | ||||
|                       ObservationKind.CAMERA_ODO_ROTATION:  np.diag([0.05**2, 0.05**2, 0.05**2]), | ||||
|                       ObservationKind.IMU_FRAME:  np.diag([0.05**2, 0.05**2, 0.05**2]), | ||||
|                       ObservationKind.NO_ROT: np.diag([0.00025**2, 0.00025**2, 0.00025**2]), | ||||
|                       ObservationKind.ECEF_POS: np.diag([5**2, 5**2, 5**2])} | ||||
| 
 | ||||
|     # MSCKF stuff | ||||
|     self.N = N | ||||
|     self.dim_main = x_initial.shape[0] | ||||
|     self.dim_augment = 7 | ||||
|     self.dim_main_err = P_initial.shape[0] | ||||
|     self.dim_augment_err = 6 | ||||
|     self.dim_state = self.dim_main + self.dim_augment*self.N | ||||
|     self.dim_state_err = self.dim_main_err + self.dim_augment_err*self.N | ||||
| 
 | ||||
|     # mahalanobis outlier rejection | ||||
|     maha_test_kinds = [ObservationKind.ORB_FEATURES] #, ObservationKind.PSEUDORANGE, ObservationKind.PSEUDORANGE_RATE] | ||||
| 
 | ||||
|     name = 'loc_%d' % N | ||||
|     loc_model.gen_model(name, N, self.dim_main, self.dim_main_err, | ||||
|                         self.dim_augment, self.dim_augment_err, | ||||
|                         self.dim_state, self.dim_state_err, | ||||
|                         maha_test_kinds) | ||||
| 
 | ||||
|     if self.N > 0: | ||||
|       x_initial, P_initial, Q = self.pad_augmented(x_initial, P_initial, Q) | ||||
|       self.computer = LstSqComputer(N) | ||||
|       self.max_tracks = max_tracks | ||||
| 
 | ||||
|     # init filter | ||||
|     self.filter = EKF_sym(name, Q, x_initial, P_initial, self.dim_main, self.dim_main_err, | ||||
|                           N, self.dim_augment, self.dim_augment_err, maha_test_kinds) | ||||
| 
 | ||||
|   @property | ||||
|   def x(self): | ||||
|     return self.filter.state() | ||||
| 
 | ||||
|   @property | ||||
|   def t(self): | ||||
|     return self.filter.filter_time | ||||
| 
 | ||||
|   @property | ||||
|   def P(self): | ||||
|     return self.filter.covs() | ||||
| 
 | ||||
|   def predict(self, t): | ||||
|     return self.filter.predict(t) | ||||
| 
 | ||||
|   def rts_smooth(self, estimates): | ||||
|     return self.filter.rts_smooth(estimates, norm_quats=True) | ||||
| 
 | ||||
|   def pad_augmented(self, x, P, Q=None): | ||||
|     if x.shape[0] == self.dim_main and self.N > 0: | ||||
|       x = np.pad(x, (0, self.N*self.dim_augment), mode='constant') | ||||
|       x[self.dim_main+3::7] = 1 | ||||
|     if P.shape[0] == self.dim_main_err and self.N > 0: | ||||
|       P = np.pad(P, [(0, self.N*self.dim_augment_err), (0, self.N*self.dim_augment_err)], mode='constant') | ||||
|       P[self.dim_main_err:, self.dim_main_err:] = 10e20*np.eye(self.dim_augment_err *self.N) | ||||
|     if Q is None: | ||||
|       return x, P | ||||
|     else: | ||||
|       Q = np.pad(Q, [(0, self.N*self.dim_augment_err), (0, self.N*self.dim_augment_err)], mode='constant') | ||||
|       return x, P, Q | ||||
| 
 | ||||
|   def init_state(self, state, covs_diag=None, covs=None, filter_time=None): | ||||
|     if covs_diag is not None: | ||||
|       P = np.diag(covs_diag) | ||||
|     elif covs is not None: | ||||
|       P = covs | ||||
|     else: | ||||
|       P = self.filter.covs() | ||||
|     state, P = self.pad_augmented(state, P) | ||||
|     self.filter.init_state(state, P, filter_time) | ||||
| 
 | ||||
|   def predict_and_observe(self, t, kind, data): | ||||
|     if len(data) > 0: | ||||
|       data = np.atleast_2d(data) | ||||
|     if kind == ObservationKind.CAMERA_ODO_TRANSLATION: | ||||
|       r = self.predict_and_update_odo_trans(data, t, kind) | ||||
|     elif kind == ObservationKind.CAMERA_ODO_ROTATION: | ||||
|       r = self.predict_and_update_odo_rot(data, t, kind) | ||||
|     elif kind == ObservationKind.PSEUDORANGE_GPS or kind == ObservationKind.PSEUDORANGE_GLONASS: | ||||
|       r = self.predict_and_update_pseudorange(data, t, kind) | ||||
|     elif kind == ObservationKind.PSEUDORANGE_RATE_GPS or kind == ObservationKind.PSEUDORANGE_RATE_GLONASS: | ||||
|       r = self.predict_and_update_pseudorange_rate(data, t, kind) | ||||
|     elif kind == ObservationKind.ORB_POINT: | ||||
|       r = self.predict_and_update_orb(data, t, kind) | ||||
|     elif kind == ObservationKind.ORB_FEATURES: | ||||
|       r = self.predict_and_update_orb_features(data, t, kind) | ||||
|     elif kind == ObservationKind.MSCKF_TEST: | ||||
|       r = self.predict_and_update_msckf_test(data, t, kind) | ||||
|     elif kind == ObservationKind.FEATURE_TRACK_TEST: | ||||
|       r = self.predict_and_update_feature_track_test(data, t, kind) | ||||
|     elif kind == ObservationKind.ODOMETRIC_SPEED: | ||||
|       r = self.predict_and_update_odo_speed(data, t, kind) | ||||
|     else: | ||||
|       r = self.filter.predict_and_update_batch(t, kind, data, self.get_R(kind, len(data))) | ||||
|     # Normalize quats | ||||
|     quat_norm = np.linalg.norm(self.filter.x[3:7,0]) | ||||
|     # Should not continue if the quats behave this weirdly | ||||
|     if not 0.1 < quat_norm < 10: | ||||
|       raise RuntimeError("Sir! The filter's gone all wobbly!") | ||||
|     self.filter.x[3:7,0] = self.filter.x[3:7,0]/quat_norm | ||||
|     for i in range(self.N): | ||||
|       d1 = self.dim_main | ||||
|       d3 = self.dim_augment | ||||
|       self.filter.x[d1+d3*i+3:d1+d3*i+7] /= np.linalg.norm(self.filter.x[d1+i*d3 + 3:d1+i*d3 + 7,0]) | ||||
|     return r | ||||
| 
 | ||||
|   def get_R(self, kind, n): | ||||
|     obs_noise = self.obs_noise[kind] | ||||
|     dim = obs_noise.shape[0] | ||||
|     R = np.zeros((n, dim, dim)) | ||||
|     for i in range(n): | ||||
|       R[i,:,:] = obs_noise | ||||
|     return R | ||||
| 
 | ||||
|   def predict_and_update_pseudorange(self, meas, t, kind): | ||||
|     R = np.zeros((len(meas), 1, 1)) | ||||
|     sat_pos_freq = np.zeros((len(meas), 4)) | ||||
|     z = np.zeros((len(meas), 1)) | ||||
|     for i, m in enumerate(meas): | ||||
|       z_i, R_i, sat_pos_freq_i = parse_pr(m) | ||||
|       sat_pos_freq[i,:] = sat_pos_freq_i | ||||
|       z[i,:] = z_i | ||||
|       R[i,:,:] = R_i | ||||
|     return self.filter.predict_and_update_batch(t, kind, z, R, sat_pos_freq) | ||||
| 
 | ||||
| 
 | ||||
|   def predict_and_update_pseudorange_rate(self, meas, t, kind): | ||||
|     R = np.zeros((len(meas), 1, 1)) | ||||
|     z = np.zeros((len(meas), 1)) | ||||
|     sat_pos_vel = np.zeros((len(meas), 6)) | ||||
|     for i, m in enumerate(meas): | ||||
|       z_i, R_i, sat_pos_vel_i = parse_prr(m) | ||||
|       sat_pos_vel[i] = sat_pos_vel_i | ||||
|       R[i,:,:] = R_i | ||||
|       z[i, :] = z_i | ||||
|     return self.filter.predict_and_update_batch(t, kind, z, R, sat_pos_vel) | ||||
| 
 | ||||
|   def predict_and_update_orb(self, orb, t, kind): | ||||
|     true_pos = orb[:,2:] | ||||
|     z = orb[:,:2] | ||||
|     R = np.zeros((len(orb), 2, 2)) | ||||
|     for i, _ in enumerate(z): | ||||
|       R[i,:,:] = np.diag([10**2, 10**2]) | ||||
|     return self.filter.predict_and_update_batch(t, kind, z, R, true_pos) | ||||
| 
 | ||||
|   def predict_and_update_odo_speed(self, speed, t, kind): | ||||
|     z = np.array(speed) | ||||
|     R = np.zeros((len(speed), 1, 1)) | ||||
|     for i, _ in enumerate(z): | ||||
|       R[i,:,:] = np.diag([0.2**2]) | ||||
|     return self.filter.predict_and_update_batch(t, kind, z, R) | ||||
| 
 | ||||
|   def predict_and_update_odo_trans(self, trans, t, kind): | ||||
|     z = trans[:,:3] | ||||
|     R = np.zeros((len(trans), 3, 3)) | ||||
|     for i, _ in enumerate(z): | ||||
|         R[i,:,:] = np.diag(trans[i,3:]**2) | ||||
|     return self.filter.predict_and_update_batch(t, kind, z, R) | ||||
| 
 | ||||
|   def predict_and_update_odo_rot(self, rot, t, kind): | ||||
|     z = rot[:,:3] | ||||
|     R = np.zeros((len(rot), 3, 3)) | ||||
|     for i, _ in enumerate(z): | ||||
|         R[i,:,:] = np.diag(rot[i,3:]**2) | ||||
|     return self.filter.predict_and_update_batch(t, kind, z, R) | ||||
| 
 | ||||
|   def predict_and_update_orb_features(self, tracks, t, kind): | ||||
|     k = 2*(self.N+1) | ||||
|     R = np.zeros((len(tracks), k, k)) | ||||
|     z = np.zeros((len(tracks), k)) | ||||
|     ecef_pos = np.zeros((len(tracks), 3)) | ||||
|     ecef_pos[:] = np.nan | ||||
|     poses = self.x[self.dim_main:].reshape((-1,7)) | ||||
|     times = tracks.reshape((len(tracks),self.N+1, 4))[:,:,0] | ||||
|     good_counter = 0 | ||||
|     if times.any() and np.allclose(times[0,:-1], self.filter.augment_times, rtol=1e-6): | ||||
|       for i, track in enumerate(tracks): | ||||
|         img_positions = track.reshape((self.N+1, 4))[:,2:] | ||||
|         # TODO not perfect as last pose not used | ||||
|         #img_positions = unroll_shutter(img_positions, poses, self.filter.state()[7:10], self.filter.state()[10:13], ecef_pos[i]) | ||||
|         ecef_pos[i] = self.computer.compute_pos(poses, img_positions[:-1]) | ||||
|         z[i] = img_positions.flatten() | ||||
|         R[i,:,:] = np.diag([0.005**2]*(k)) | ||||
|         if np.isfinite(ecef_pos[i][0]): | ||||
|           good_counter += 1 | ||||
|           if good_counter > self.max_tracks: | ||||
|             break | ||||
|     good_idxs = np.all(np.isfinite(ecef_pos),axis=1) | ||||
|     # have to do some weird stuff here to keep | ||||
|     # to have the observations input from mesh3d | ||||
|     # consistent with the outputs of the filter | ||||
|     # Probably should be replaced, not sure how. | ||||
|     ret = self.filter.predict_and_update_batch(t, kind, z[good_idxs], R[good_idxs], ecef_pos[good_idxs], augment=True) | ||||
|     if ret is None: | ||||
|       return | ||||
|     y_full = np.zeros((z.shape[0], z.shape[1] - 3)) | ||||
|     #print sum(good_idxs), len(tracks) | ||||
|     if sum(good_idxs) > 0: | ||||
|         y_full[good_idxs] = np.array(ret[6]) | ||||
|     ret = ret[:6] + (y_full, z, ecef_pos) | ||||
|     return ret | ||||
| 
 | ||||
|   def predict_and_update_msckf_test(self, test_data, t, kind): | ||||
|     assert self.N > 0 | ||||
|     z = test_data | ||||
|     R = np.zeros((len(test_data), len(z[0]), len(z[0]))) | ||||
|     ecef_pos = [self.x[:3]] | ||||
|     for i, _ in enumerate(z): | ||||
|       R[i,:,:] = np.diag([0.1**2]*len(z[0])) | ||||
|     ret = self.filter.predict_and_update_batch(t, kind, z, R, ecef_pos) | ||||
|     self.filter.augment() | ||||
|     return ret | ||||
| 
 | ||||
|   def maha_test_pseudorange(self, x, P, meas, kind, maha_thresh=.3): | ||||
|     bools = [] | ||||
|     for i, m in enumerate(meas): | ||||
|       z, R, sat_pos_freq = parse_pr(m) | ||||
|       bools.append(self.filter.maha_test(x, P, kind, z, R, extra_args=sat_pos_freq, maha_thresh=maha_thresh)) | ||||
|     return np.array(bools) | ||||
| 
 | ||||
|   def maha_test_pseudorange_rate(self, x, P, meas, kind, maha_thresh=.999): | ||||
|     bools = [] | ||||
|     for i, m in enumerate(meas): | ||||
|       z, R, sat_pos_vel = parse_prr(m) | ||||
|       bools.append(self.filter.maha_test(x, P, kind, z, R, extra_args=sat_pos_vel, maha_thresh=maha_thresh)) | ||||
|     return np.array(bools) | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|   LocKalman(N=4) | ||||
| @ -0,0 +1,128 @@ | ||||
| #!/usr/bin/env python3 | ||||
| import numpy as np | ||||
| from selfdrive.locationd.kalman import loc_local_model | ||||
| 
 | ||||
| from selfdrive.locationd.kalman.kalman_helpers import ObservationKind | ||||
| from selfdrive.locationd.kalman.ekf_sym import EKF_sym | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| class States(): | ||||
|   VELOCITY = slice(0,3) # device frame velocity in m/s | ||||
|   ANGULAR_VELOCITY = slice(3, 6) # roll, pitch and yaw rates in device frame in radians/s | ||||
|   GYRO_BIAS = slice(6, 9) # roll, pitch and yaw biases | ||||
|   ODO_SCALE = slice(9, 10) # odometer scale | ||||
|   ACCELERATION = slice(10, 13) # Acceleration in device frame in m/s**2 | ||||
| 
 | ||||
| 
 | ||||
| class LocLocalKalman(): | ||||
|   def __init__(self): | ||||
|     x_initial = np.array([0, 0, 0, | ||||
|                           0, 0, 0, | ||||
|                           0, 0, 0, | ||||
|                           1, | ||||
|                           0, 0, 0]) | ||||
| 
 | ||||
|     # state covariance | ||||
|     P_initial = np.diag([10**2, 10**2, 10**2, | ||||
|                          1**2, 1**2, 1**2, | ||||
|                          0.05**2, 0.05**2, 0.05**2, | ||||
|                          0.02**2, | ||||
|                          1**2, 1**2, 1**2]) | ||||
| 
 | ||||
|     # process noise | ||||
|     Q = np.diag([0.0**2, 0.0**2, 0.0**2, | ||||
|                  .01**2, .01**2, .01**2, | ||||
|                  (0.005/100)**2, (0.005/100)**2, (0.005/100)**2, | ||||
|                  (0.02/100)**2, | ||||
|                  3**2, 3**2, 3**2]) | ||||
| 
 | ||||
|     self.obs_noise = {ObservationKind.ODOMETRIC_SPEED: np.atleast_2d(0.2**2), | ||||
|                       ObservationKind.PHONE_GYRO: np.diag([0.025**2, 0.025**2, 0.025**2])} | ||||
| 
 | ||||
|     # MSCKF stuff | ||||
|     self.dim_state = len(x_initial) | ||||
|     self.dim_main = self.dim_state | ||||
| 
 | ||||
|     name = 'loc_local' | ||||
|     loc_local_model.gen_model(name, self.dim_state) | ||||
| 
 | ||||
|     # init filter | ||||
|     self.filter = EKF_sym(name, Q, x_initial, P_initial, self.dim_main, self.dim_main) | ||||
| 
 | ||||
|   @property | ||||
|   def x(self): | ||||
|     return self.filter.state() | ||||
| 
 | ||||
|   @property | ||||
|   def t(self): | ||||
|     return self.filter.filter_time | ||||
| 
 | ||||
|   @property | ||||
|   def P(self): | ||||
|     return self.filter.covs() | ||||
| 
 | ||||
|   def predict(self, t): | ||||
|     if self.t: | ||||
|       # Does NOT modify filter state | ||||
|       return self.filter._predict(self.x, self.P, t - self.t)[0] | ||||
|     else: | ||||
|       raise RuntimeError("Request predict on filter with uninitialized time") | ||||
| 
 | ||||
|   def rts_smooth(self, estimates): | ||||
|     return self.filter.rts_smooth(estimates, norm_quats=True) | ||||
| 
 | ||||
| 
 | ||||
|   def init_state(self, state, covs_diag=None, covs=None, filter_time=None): | ||||
|     if covs_diag is not None: | ||||
|       P = np.diag(covs_diag) | ||||
|     elif covs is not None: | ||||
|       P = covs | ||||
|     else: | ||||
|       P = self.filter.covs() | ||||
|     self.filter.init_state(state, P, filter_time) | ||||
| 
 | ||||
|   def predict_and_observe(self, t, kind, data): | ||||
|     if len(data) > 0: | ||||
|       data = np.atleast_2d(data) | ||||
|     if kind == ObservationKind.CAMERA_ODO_TRANSLATION: | ||||
|       r = self.predict_and_update_odo_trans(data, t, kind) | ||||
|     elif kind == ObservationKind.CAMERA_ODO_ROTATION: | ||||
|       r = self.predict_and_update_odo_rot(data, t, kind) | ||||
|     elif kind == ObservationKind.ODOMETRIC_SPEED: | ||||
|       r = self.predict_and_update_odo_speed(data, t, kind) | ||||
|     else: | ||||
|       r = self.filter.predict_and_update_batch(t, kind, data, self.get_R(kind, len(data))) | ||||
|     return r | ||||
| 
 | ||||
|   def get_R(self, kind, n): | ||||
|     obs_noise = self.obs_noise[kind] | ||||
|     dim = obs_noise.shape[0] | ||||
|     R = np.zeros((n, dim, dim)) | ||||
|     for i in range(n): | ||||
|       R[i,:,:] = obs_noise | ||||
|     return R | ||||
| 
 | ||||
|   def predict_and_update_odo_speed(self, speed, t, kind): | ||||
|     z = np.array(speed) | ||||
|     R = np.zeros((len(speed), 1, 1)) | ||||
|     for i, _ in enumerate(z): | ||||
|       R[i,:,:] = np.diag([0.2**2]) | ||||
|     return self.filter.predict_and_update_batch(t, kind, z, R) | ||||
| 
 | ||||
|   def predict_and_update_odo_trans(self, trans, t, kind): | ||||
|     z = trans[:,:3] | ||||
|     R = np.zeros((len(trans), 3, 3)) | ||||
|     for i, _ in enumerate(z): | ||||
|         R[i,:,:] = np.diag(trans[i,3:]**2) | ||||
|     return self.filter.predict_and_update_batch(t, kind, z, R) | ||||
| 
 | ||||
|   def predict_and_update_odo_rot(self, rot, t, kind): | ||||
|     z = rot[:,:3] | ||||
|     R = np.zeros((len(rot), 3, 3)) | ||||
|     for i, _ in enumerate(z): | ||||
|         R[i,:,:] = np.diag(rot[i,3:]**2) | ||||
|     return self.filter.predict_and_update_batch(t, kind, z, R) | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|   LocLocalKalman() | ||||
| @ -0,0 +1,80 @@ | ||||
| import numpy as np | ||||
| import sympy as sp | ||||
| import os | ||||
| 
 | ||||
| from selfdrive.locationd.kalman.kalman_helpers import ObservationKind | ||||
| from selfdrive.locationd.kalman.ekf_sym import gen_code | ||||
| 
 | ||||
| 
 | ||||
| def gen_model(name, dim_state): | ||||
| 
 | ||||
|   # check if rebuild is needed | ||||
|   try: | ||||
|     dir_path = os.path.dirname(__file__) | ||||
|     deps = [dir_path + '/' + 'ekf_c.c', | ||||
|             dir_path + '/' + 'ekf_sym.py', | ||||
|             dir_path + '/' + 'loc_local_model.py', | ||||
|             dir_path + '/' + 'loc_local_kf.py'] | ||||
| 
 | ||||
|     outs = [dir_path + '/' + name + '.o', | ||||
|             dir_path + '/' + name + '.so', | ||||
|             dir_path + '/' + name + '.cpp'] | ||||
|     out_times = list(map(os.path.getmtime, outs)) | ||||
|     dep_times = list(map(os.path.getmtime, deps)) | ||||
|     rebuild = os.getenv("REBUILD", False) | ||||
|     if min(out_times) > max(dep_times) and not rebuild: | ||||
|       return | ||||
|     list(map(os.remove, outs)) | ||||
|   except OSError: | ||||
|     pass | ||||
| 
 | ||||
|   # make functions and jacobians with sympy | ||||
|   # state variables | ||||
|   state_sym = sp.MatrixSymbol('state', dim_state, 1) | ||||
|   state = sp.Matrix(state_sym) | ||||
|   v = state[0:3,:] | ||||
|   omega = state[3:6,:] | ||||
|   vroll, vpitch, vyaw = omega | ||||
|   vx, vy, vz = v | ||||
|   roll_bias, pitch_bias, yaw_bias = state[6:9,:] | ||||
|   odo_scale = state[9,:] | ||||
|   accel = state[10:13,:] | ||||
| 
 | ||||
|   dt = sp.Symbol('dt') | ||||
| 
 | ||||
|   # Time derivative of the state as a function of state | ||||
|   state_dot = sp.Matrix(np.zeros((dim_state, 1))) | ||||
|   state_dot[:3,:] = accel | ||||
| 
 | ||||
|   # Basic descretization, 1st order intergrator | ||||
|   # Can be pretty bad if dt is big | ||||
|   f_sym = sp.Matrix(state + dt*state_dot) | ||||
| 
 | ||||
|   # | ||||
|   # Observation functions | ||||
|   # | ||||
| 
 | ||||
|   # extra args | ||||
|   #imu_rot = euler_rotate(*imu_angles) | ||||
|   #h_gyro_sym = imu_rot*sp.Matrix([vroll + roll_bias, | ||||
|   #                               vpitch + pitch_bias, | ||||
|   #                               vyaw + yaw_bias]) | ||||
|   h_gyro_sym = sp.Matrix([vroll + roll_bias, | ||||
|                           vpitch + pitch_bias, | ||||
|                           vyaw + yaw_bias]) | ||||
| 
 | ||||
|   speed = vx**2 + vy**2 + vz**2 | ||||
|   h_speed_sym = sp.Matrix([sp.sqrt(speed)*odo_scale]) | ||||
| 
 | ||||
|   h_relative_motion = sp.Matrix(v) | ||||
|   h_phone_rot_sym = sp.Matrix([vroll, | ||||
|                                vpitch, | ||||
|                                vyaw]) | ||||
| 
 | ||||
| 
 | ||||
|   obs_eqs = [[h_speed_sym, ObservationKind.ODOMETRIC_SPEED, None], | ||||
|              [h_gyro_sym, ObservationKind.PHONE_GYRO, None], | ||||
|              [h_phone_rot_sym, ObservationKind.NO_ROT, None], | ||||
|              [h_relative_motion, ObservationKind.CAMERA_ODO_TRANSLATION, None], | ||||
|              [h_phone_rot_sym, ObservationKind.CAMERA_ODO_ROTATION, None]] | ||||
|   gen_code(name, f_sym, dt, state_sym, obs_eqs, dim_state, dim_state) | ||||
| @ -0,0 +1,254 @@ | ||||
| import numpy as np | ||||
| import sympy as sp | ||||
| import os | ||||
| 
 | ||||
| from laika.constants import EARTH_GM | ||||
| from .kalman_helpers import ObservationKind | ||||
| from .ekf_sym import gen_code | ||||
| from common.sympy_helpers import cross, euler_rotate, quat_rotate, quat_matrix_l, quat_matrix_r | ||||
| 
 | ||||
| def gen_model(name, N, dim_main, dim_main_err, | ||||
|                  dim_augment, dim_augment_err, | ||||
|                  dim_state, dim_state_err, | ||||
|                  maha_test_kinds): | ||||
| 
 | ||||
| 
 | ||||
|   # check if rebuild is needed | ||||
|   try: | ||||
|     dir_path = os.path.dirname(__file__) | ||||
|     deps = [dir_path + '/' + 'ekf_c.c', | ||||
|             dir_path + '/' + 'ekf_sym.py', | ||||
|             dir_path + '/' + 'loc_model.py', | ||||
|             dir_path + '/' + 'loc_kf.py'] | ||||
| 
 | ||||
|     outs = [dir_path + '/' + name + '.o', | ||||
|             dir_path + '/' + name + '.so', | ||||
|             dir_path + '/' + name + '.cpp'] | ||||
|     out_times = list(map(os.path.getmtime, outs)) | ||||
|     dep_times = list(map(os.path.getmtime, deps)) | ||||
|     rebuild = os.getenv("REBUILD", False) | ||||
|     if min(out_times) > max(dep_times) and not rebuild: | ||||
|       return | ||||
|     list(map(os.remove, outs)) | ||||
|   except OSError as e: | ||||
|     pass | ||||
| 
 | ||||
|   # make functions and jacobians with sympy | ||||
|   # state variables | ||||
|   state_sym = sp.MatrixSymbol('state', dim_state, 1) | ||||
|   state = sp.Matrix(state_sym) | ||||
|   x,y,z = state[0:3,:] | ||||
|   q = state[3:7,:] | ||||
|   v = state[7:10,:] | ||||
|   vx, vy, vz = v | ||||
|   omega = state[10:13,:] | ||||
|   vroll, vpitch, vyaw = omega | ||||
|   cb, cd = state[13:15,:] | ||||
|   roll_bias, pitch_bias, yaw_bias = state[15:18,:] | ||||
|   odo_scale = state[18,:] | ||||
|   acceleration = state[19:22,:] | ||||
|   focal_scale = state[22,:] | ||||
|   imu_angles= state[23:26,:] | ||||
|   glonass_bias, glonass_freq_slope = state[26:28,:] | ||||
|   ca = state[28,0] | ||||
| 
 | ||||
|   dt = sp.Symbol('dt') | ||||
| 
 | ||||
|   # calibration and attitude rotation matrices | ||||
|   quat_rot = quat_rotate(*q) | ||||
| 
 | ||||
|   # Got the quat predict equations from here | ||||
|   # A New Quaternion-Based Kalman Filter for | ||||
|   # Real-Time Attitude Estimation Using the Two-Step | ||||
|   # Geometrically-Intuitive Correction Algorithm | ||||
|   A = 0.5*sp.Matrix([[0, -vroll, -vpitch, -vyaw], | ||||
|                  [vroll, 0, vyaw, -vpitch], | ||||
|                  [vpitch, -vyaw, 0, vroll], | ||||
|                  [vyaw, vpitch, -vroll, 0]]) | ||||
|   q_dot = A * q | ||||
| 
 | ||||
|   # Time derivative of the state as a function of state | ||||
|   state_dot = sp.Matrix(np.zeros((dim_state, 1))) | ||||
|   state_dot[:3,:] = v | ||||
|   state_dot[3:7,:] = q_dot | ||||
|   state_dot[7:10,0] = quat_rot * acceleration | ||||
|   state_dot[13,0] = cd | ||||
|   state_dot[14,0] = ca | ||||
| 
 | ||||
|   # Basic descretization, 1st order intergrator | ||||
|   # Can be pretty bad if dt is big | ||||
|   f_sym = state + dt*state_dot | ||||
| 
 | ||||
|   state_err_sym = sp.MatrixSymbol('state_err',dim_state_err,1) | ||||
|   state_err = sp.Matrix(state_err_sym) | ||||
|   quat_err = state_err[3:6,:] | ||||
|   v_err = state_err[6:9,:] | ||||
|   omega_err = state_err[9:12,:] | ||||
|   cd_err = state_err[13,:] | ||||
|   acceleration_err = state_err[18:21,:] | ||||
|   ca_err = state_err[27,:] | ||||
| 
 | ||||
|   # Time derivative of the state error as a function of state error and state | ||||
|   quat_err_matrix = euler_rotate(quat_err[0], quat_err[1], quat_err[2]) | ||||
|   q_err_dot = quat_err_matrix * quat_rot * (omega + omega_err) | ||||
|   state_err_dot = sp.Matrix(np.zeros((dim_state_err, 1))) | ||||
|   state_err_dot[:3,:] = v_err | ||||
|   state_err_dot[3:6,:] = q_err_dot | ||||
|   state_err_dot[6:9,:] = quat_err_matrix * quat_rot * (acceleration + acceleration_err) | ||||
|   state_err_dot[12,:] = cd_err | ||||
|   state_err_dot[13,:] = ca_err | ||||
|   f_err_sym = state_err + dt*state_err_dot | ||||
| 
 | ||||
|   # convenient indexing | ||||
|   # q idxs are for quats and p idxs are for other | ||||
|   q_idxs = [[3, dim_augment]] + [[dim_main + n*dim_augment + 3, dim_main + (n+1)*dim_augment] for n in range(N)] | ||||
|   q_err_idxs = [[3, dim_augment_err]] + [[dim_main_err + n*dim_augment_err + 3, dim_main_err + (n+1)*dim_augment_err] for n in range(N)] | ||||
|   p_idxs = [[0, 3]] + [[dim_augment, dim_main]] + [[dim_main + n*dim_augment , dim_main + n*dim_augment + 3] for n in range(N)] | ||||
|   p_err_idxs = [[0, 3]] + [[dim_augment_err, dim_main_err]] + [[dim_main_err + n*dim_augment_err, dim_main_err + n*dim_augment_err + 3] for n in range(N)] | ||||
| 
 | ||||
|   # Observation matrix modifier | ||||
|   H_mod_sym = sp.Matrix(np.zeros((dim_state, dim_state_err))) | ||||
|   for p_idx, p_err_idx in zip(p_idxs, p_err_idxs): | ||||
|     H_mod_sym[p_idx[0]:p_idx[1],p_err_idx[0]:p_err_idx[1]] = np.eye(p_idx[1]-p_idx[0]) | ||||
|   for q_idx, q_err_idx in zip(q_idxs, q_err_idxs): | ||||
|     H_mod_sym[q_idx[0]:q_idx[1],q_err_idx[0]:q_err_idx[1]] = 0.5*quat_matrix_r(state[q_idx[0]:q_idx[1]])[:,1:] | ||||
| 
 | ||||
| 
 | ||||
|   # these error functions are defined so that say there | ||||
|   # is a nominal x and true x: | ||||
|   # true x = err_function(nominal x, delta x) | ||||
|   # delta x = inv_err_function(nominal x, true x) | ||||
|   nom_x = sp.MatrixSymbol('nom_x',dim_state,1) | ||||
|   true_x = sp.MatrixSymbol('true_x',dim_state,1) | ||||
|   delta_x = sp.MatrixSymbol('delta_x',dim_state_err,1) | ||||
| 
 | ||||
|   err_function_sym = sp.Matrix(np.zeros((dim_state,1))) | ||||
|   for q_idx, q_err_idx in zip(q_idxs, q_err_idxs): | ||||
|     delta_quat = sp.Matrix(np.ones((4))) | ||||
|     delta_quat[1:,:] = sp.Matrix(0.5*delta_x[q_err_idx[0]: q_err_idx[1],:]) | ||||
|     err_function_sym[q_idx[0]:q_idx[1],0] = quat_matrix_r(nom_x[q_idx[0]:q_idx[1],0])*delta_quat | ||||
|   for p_idx, p_err_idx in zip(p_idxs, p_err_idxs): | ||||
|     err_function_sym[p_idx[0]:p_idx[1],:] = sp.Matrix(nom_x[p_idx[0]:p_idx[1],:] + delta_x[p_err_idx[0]:p_err_idx[1],:]) | ||||
| 
 | ||||
|   inv_err_function_sym = sp.Matrix(np.zeros((dim_state_err,1))) | ||||
|   for p_idx, p_err_idx in zip(p_idxs, p_err_idxs): | ||||
|     inv_err_function_sym[p_err_idx[0]:p_err_idx[1],0] = sp.Matrix(-nom_x[p_idx[0]:p_idx[1],0] + true_x[p_idx[0]:p_idx[1],0]) | ||||
|   for q_idx, q_err_idx in zip(q_idxs, q_err_idxs): | ||||
|     delta_quat = quat_matrix_r(nom_x[q_idx[0]:q_idx[1],0]).T*true_x[q_idx[0]:q_idx[1],0] | ||||
|     inv_err_function_sym[q_err_idx[0]:q_err_idx[1],0] = sp.Matrix(2*delta_quat[1:]) | ||||
| 
 | ||||
|   eskf_params = [[err_function_sym, nom_x, delta_x], | ||||
|                  [inv_err_function_sym, nom_x, true_x], | ||||
|                  H_mod_sym, f_err_sym, state_err_sym] | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|   # | ||||
|   # Observation functions | ||||
|   # | ||||
| 
 | ||||
|   # extra args | ||||
|   sat_pos_freq_sym = sp.MatrixSymbol('sat_pos', 4, 1) | ||||
|   sat_pos_vel_sym = sp.MatrixSymbol('sat_pos_vel', 6, 1) | ||||
|   sat_los_sym = sp.MatrixSymbol('sat_los', 3, 1) | ||||
|   orb_epos_sym = sp.MatrixSymbol('orb_epos_sym', 3, 1) | ||||
| 
 | ||||
|   # expand extra args | ||||
|   sat_x, sat_y, sat_z, glonass_freq = sat_pos_freq_sym | ||||
|   sat_vx, sat_vy, sat_vz = sat_pos_vel_sym[3:] | ||||
|   los_x, los_y, los_z = sat_los_sym | ||||
|   orb_x, orb_y, orb_z = orb_epos_sym | ||||
| 
 | ||||
|   h_pseudorange_sym = sp.Matrix([sp.sqrt( | ||||
|                                   (x - sat_x)**2 + | ||||
|                                   (y - sat_y)**2 + | ||||
|                                   (z - sat_z)**2) + | ||||
|                                   cb]) | ||||
| 
 | ||||
|   h_pseudorange_glonass_sym = sp.Matrix([sp.sqrt( | ||||
|                                   (x - sat_x)**2 + | ||||
|                                   (y - sat_y)**2 + | ||||
|                                   (z - sat_z)**2) + | ||||
|                                   cb + glonass_bias + glonass_freq_slope*glonass_freq]) | ||||
| 
 | ||||
|   los_vector = (sp.Matrix(sat_pos_vel_sym[0:3]) - sp.Matrix([x, y, z])) | ||||
|   los_vector = los_vector / sp.sqrt(los_vector[0]**2 + los_vector[1]**2 + los_vector[2]**2) | ||||
|   h_pseudorange_rate_sym = sp.Matrix([los_vector[0]*(sat_vx - vx) + | ||||
|                                          los_vector[1]*(sat_vy - vy) + | ||||
|                                          los_vector[2]*(sat_vz - vz) + | ||||
|                                          cd]) | ||||
| 
 | ||||
|   imu_rot = euler_rotate(*imu_angles) | ||||
|   h_gyro_sym = imu_rot*sp.Matrix([vroll + roll_bias, | ||||
|                                  vpitch + pitch_bias, | ||||
|                                  vyaw + yaw_bias]) | ||||
| 
 | ||||
|   pos = sp.Matrix([x, y, z]) | ||||
|   gravity = quat_rot.T * ((EARTH_GM/((x**2 + y**2 + z**2)**(3.0/2.0)))*pos) | ||||
|   h_acc_sym = imu_rot*(gravity + acceleration) | ||||
|   h_phone_rot_sym = sp.Matrix([vroll, | ||||
|                                vpitch, | ||||
|                                vyaw]) | ||||
|   speed = vx**2 + vy**2 + vz**2 | ||||
|   h_speed_sym = sp.Matrix([sp.sqrt(speed)*odo_scale]) | ||||
| 
 | ||||
|   # orb stuff | ||||
|   orb_pos_sym = sp.Matrix([orb_x - x, orb_y - y, orb_z - z]) | ||||
|   orb_pos_rot_sym = quat_rot.T * orb_pos_sym | ||||
|   s = orb_pos_rot_sym[0] | ||||
|   h_orb_point_sym = sp.Matrix([(1/s)*(orb_pos_rot_sym[1]), | ||||
|                                (1/s)*(orb_pos_rot_sym[2])]) | ||||
| 
 | ||||
|   h_pos_sym = sp.Matrix([x, y, z]) | ||||
|   h_imu_frame_sym = sp.Matrix(imu_angles) | ||||
| 
 | ||||
|   h_relative_motion = sp.Matrix(quat_rot.T * v) | ||||
| 
 | ||||
| 
 | ||||
|   obs_eqs = [[h_speed_sym, ObservationKind.ODOMETRIC_SPEED, None], | ||||
|              [h_gyro_sym, ObservationKind.PHONE_GYRO, None], | ||||
|              [h_phone_rot_sym, ObservationKind.NO_ROT, None], | ||||
|              [h_acc_sym, ObservationKind.PHONE_ACCEL, None], | ||||
|              [h_pseudorange_sym, ObservationKind.PSEUDORANGE_GPS, sat_pos_freq_sym], | ||||
|              [h_pseudorange_glonass_sym, ObservationKind.PSEUDORANGE_GLONASS, sat_pos_freq_sym], | ||||
|              [h_pseudorange_rate_sym, ObservationKind.PSEUDORANGE_RATE_GPS, sat_pos_vel_sym], | ||||
|              [h_pseudorange_rate_sym, ObservationKind.PSEUDORANGE_RATE_GLONASS, sat_pos_vel_sym], | ||||
|              [h_pos_sym, ObservationKind.ECEF_POS, None], | ||||
|              [h_relative_motion, ObservationKind.CAMERA_ODO_TRANSLATION, None], | ||||
|              [h_phone_rot_sym, ObservationKind.CAMERA_ODO_ROTATION, None], | ||||
|              [h_imu_frame_sym, ObservationKind.IMU_FRAME, None], | ||||
|              [h_orb_point_sym, ObservationKind.ORB_POINT, orb_epos_sym]] | ||||
| 
 | ||||
|   # MSCKF configuration | ||||
|   if N > 0: | ||||
|     focal_scale =1 | ||||
|     # Add observation functions for orb feature tracks | ||||
|     track_epos_sym = sp.MatrixSymbol('track_epos_sym', 3, 1) | ||||
|     track_x, track_y, track_z = track_epos_sym | ||||
|     h_track_sym = sp.Matrix(np.zeros(((1 + N)*2, 1))) | ||||
|     track_pos_sym = sp.Matrix([track_x - x, track_y - y, track_z - z]) | ||||
|     track_pos_rot_sym = quat_rot.T * track_pos_sym | ||||
|     h_track_sym[-2:,:] = sp.Matrix([focal_scale*(track_pos_rot_sym[1]/track_pos_rot_sym[0]), | ||||
|                                      focal_scale*(track_pos_rot_sym[2]/track_pos_rot_sym[0])]) | ||||
| 
 | ||||
|     h_msckf_test_sym = sp.Matrix(np.zeros(((1 + N)*3, 1))) | ||||
|     h_msckf_test_sym[-3:,:] = sp.Matrix([track_x - x,track_y - y , track_z - z]) | ||||
| 
 | ||||
|     for n in range(N): | ||||
|       idx = dim_main + n*dim_augment | ||||
|       err_idx = dim_main_err + n*dim_augment_err | ||||
|       x, y, z = state[idx:idx+3] | ||||
|       q = state[idx+3:idx+7] | ||||
|       quat_rot = quat_rotate(*q) | ||||
|       track_pos_sym = sp.Matrix([track_x - x, track_y - y, track_z - z]) | ||||
|       track_pos_rot_sym = quat_rot.T * track_pos_sym | ||||
|       h_track_sym[n*2:n*2+2,:] = sp.Matrix([focal_scale*(track_pos_rot_sym[1]/track_pos_rot_sym[0]), | ||||
|                                              focal_scale*(track_pos_rot_sym[2]/track_pos_rot_sym[0])]) | ||||
|       h_msckf_test_sym[n*3:n*3+3,:] = sp.Matrix([track_x - x, track_y - y, track_z - z]) | ||||
|     obs_eqs.append([h_msckf_test_sym, ObservationKind.MSCKF_TEST, track_epos_sym]) | ||||
|     obs_eqs.append([h_track_sym, ObservationKind.ORB_FEATURES, track_epos_sym]) | ||||
|     obs_eqs.append([h_track_sym, ObservationKind.FEATURE_TRACK_TEST, track_epos_sym]) | ||||
|     msckf_params = [dim_main, dim_augment, dim_main_err, dim_augment_err, N, [ObservationKind.MSCKF_TEST, ObservationKind.ORB_FEATURES]] | ||||
|   else: | ||||
|     msckf_params = None | ||||
|   gen_code(name, f_sym, dt, state_sym, obs_eqs, dim_state, dim_state_err, eskf_params, msckf_params, maha_test_kinds) | ||||
| @ -0,0 +1,31 @@ | ||||
| import os | ||||
| import subprocess | ||||
| from common.basedir import BASEDIR | ||||
| 
 | ||||
| from cffi import FFI | ||||
| from ctypes import cdll | ||||
| 
 | ||||
| locationd_dir = os.path.dirname(os.path.abspath(__file__)) | ||||
| liblocationd_fn = os.path.join(locationd_dir, "liblocationd.so") | ||||
| 
 | ||||
| 
 | ||||
| ffi = FFI() | ||||
| ffi.cdef(""" | ||||
| void *localizer_init(void); | ||||
| void localizer_handle_log(void * localizer, const unsigned char * data, size_t len); | ||||
| double localizer_get_yaw(void * localizer); | ||||
| double localizer_get_bias(void * localizer); | ||||
| double localizer_get_t(void * localizer); | ||||
| void *params_learner_init(size_t len, char * params, double angle_offset, double stiffness_factor, double steer_ratio, double learning_rate); | ||||
| bool params_learner_update(void * params_learner, double psi, double u, double sa); | ||||
| double params_learner_get_ao(void * params_learner); | ||||
| double params_learner_get_slow_ao(void * params_learner); | ||||
| double params_learner_get_x(void * params_learner); | ||||
| double params_learner_get_sR(void * params_learner); | ||||
| double * localizer_get_P(void * localizer); | ||||
| void localizer_set_P(void * localizer, double * P); | ||||
| double * localizer_get_state(void * localizer); | ||||
| void localizer_set_state(void * localizer, double * state); | ||||
| """) | ||||
| 
 | ||||
| liblocationd = ffi.dlopen(liblocationd_fn) | ||||
| @ -0,0 +1,232 @@ | ||||
| #!/usr/bin/env python3 | ||||
| import os | ||||
| import zmq | ||||
| import numpy as np | ||||
| from bisect import bisect_right | ||||
| import cereal.messaging as messaging | ||||
| from selfdrive.swaglog import cloudlog | ||||
| from cereal.services import service_list | ||||
| from common.transformations.orientation import rotations_from_quats, ecef_euler_from_ned, euler2quat, ned_euler_from_ecef, quat2euler | ||||
| import common.transformations.coordinates as coord | ||||
| 
 | ||||
| import laika.raw_gnss as gnss | ||||
| from laika.astro_dog import AstroDog | ||||
| 
 | ||||
| from selfdrive.locationd.kalman.loc_kf import LocKalman | ||||
| from selfdrive.locationd.kalman.kalman_helpers import ObservationKind | ||||
| os.environ["OMP_NUM_THREADS"] = "1" | ||||
| 
 | ||||
| 
 | ||||
| class Localizer(): | ||||
|   def __init__(self, disabled_logs=[], dog=None): | ||||
|     self.kf = LocKalman(0) | ||||
|     self.reset_kalman() | ||||
|     if dog: | ||||
|       self.dog = dog | ||||
|     else: | ||||
|       self.dog = AstroDog(auto_update=True) | ||||
|     self.max_age = .2  # seconds | ||||
|     self.disabled_logs = disabled_logs | ||||
|     self.week = None | ||||
| 
 | ||||
|   def liveLocationMsg(self, time): | ||||
|     fix = messaging.log.LiveLocationData.new_message() | ||||
|     predicted_state = self.kf.x | ||||
|     fix_ecef = predicted_state[0:3] | ||||
|     fix_pos_geo = coord.ecef2geodetic(fix_ecef) | ||||
|     fix.lat = float(fix_pos_geo[0]) | ||||
|     fix.lon = float(fix_pos_geo[1]) | ||||
|     fix.alt = float(fix_pos_geo[2]) | ||||
| 
 | ||||
|     fix.speed = float(np.linalg.norm(predicted_state[7:10])) | ||||
| 
 | ||||
|     orientation_ned_euler = ned_euler_from_ecef(fix_ecef, quat2euler(predicted_state[3:7])) | ||||
|     fix.roll = float(orientation_ned_euler[0]*180/np.pi) | ||||
|     fix.pitch = float(orientation_ned_euler[1]*180/np.pi) | ||||
|     fix.heading = float(orientation_ned_euler[2]*180/np.pi) | ||||
| 
 | ||||
|     fix.gyro = [float(predicted_state[10]), float(predicted_state[11]), float(predicted_state[12])] | ||||
|     fix.accel = [float(predicted_state[19]), float(predicted_state[20]), float(predicted_state[21])] | ||||
|     local_vel = rotations_from_quats(predicted_state[3:7]).T.dot(predicted_state[7:10]) | ||||
|     fix.pitchCalibration = float((np.arctan2(local_vel[2], local_vel[0]))*180/np.pi) | ||||
|     fix.yawCalibration = float((np.arctan2(local_vel[1], local_vel[0]))*180/np.pi) | ||||
|     fix.imuFrame = [(180/np.pi)*float(predicted_state[23]), (180/np.pi)*float(predicted_state[24]), (180/np.pi)*float(predicted_state[25])] | ||||
|     return fix | ||||
| 
 | ||||
| 
 | ||||
|   def update_kalman(self, time, kind, meas): | ||||
|     idx = bisect_right([x[0] for x in self.observation_buffer], time) | ||||
|     self.observation_buffer.insert(idx, (time, kind, meas)) | ||||
|     #print len(self.observation_buffer), idx, self.kf.filter.filter_time, time | ||||
|     while self.observation_buffer[-1][0] - self.observation_buffer[0][0] > self.max_age: | ||||
|       if self.filter_ready: | ||||
|         self.kf.predict_and_observe(*self.observation_buffer.pop(0)) | ||||
|       else: | ||||
|         self.observation_buffer.pop(0) | ||||
| 
 | ||||
|   def handle_gps(self, log, current_time): | ||||
|     self.converter = coord.LocalCoord.from_geodetic([log.gpsLocationExternal.latitude, log.gpsLocationExternal.longitude, log.gpsLocationExternal.altitude]) | ||||
|     fix_ecef = self.converter.ned2ecef([0,0,0]) | ||||
|     # initing with bad bearing allowed, maybe bad? | ||||
|     if not self.filter_ready and len(list(self.dog.orbits.keys())) >6:  # and log.gpsLocationExternal.speed > 5: | ||||
|       self.filter_ready = True | ||||
|       initial_ecef = fix_ecef | ||||
|       initial_state = np.zeros(29) | ||||
|       gps_bearing = log.gpsLocationExternal.bearing*(np.pi/180) | ||||
|       initial_pose_ecef = ecef_euler_from_ned(initial_ecef, [0, 0, gps_bearing]) | ||||
|       initial_pose_ecef_quat = euler2quat(initial_pose_ecef) | ||||
|       gps_speed = log.gpsLocationExternal.speed | ||||
|       quat_uncertainty = 0.2**2 | ||||
|       initial_pose_ecef_quat = euler2quat(initial_pose_ecef) | ||||
|       initial_state[:3] = initial_ecef | ||||
|       initial_state[3:7] = initial_pose_ecef_quat | ||||
|       initial_state[7:10] = rotations_from_quats(initial_pose_ecef_quat).dot(np.array([gps_speed, 0, 0])) | ||||
|       initial_state[18] = 1 | ||||
|       initial_state[22] = 1 | ||||
|       covs_diag = np.array([10**2,10**2,10**2, | ||||
|                      quat_uncertainty, quat_uncertainty, quat_uncertainty, | ||||
|                      2**2, 2**2, 2**2, | ||||
|                      1, 1, 1, | ||||
|                      20000000**2, 100**2, | ||||
|                      0.01**2, 0.01**2, 0.01**2, | ||||
|                      0.02**2, | ||||
|                      2**2, 2**2, 2**2, | ||||
|                      .01**2, | ||||
|                      0.01**2, 0.01**2, 0.01**2, | ||||
|                      10**2, 1**2, | ||||
|                      0.2**2]) | ||||
|       self.kf.init_state(initial_state, covs=np.diag(covs_diag), filter_time=current_time) | ||||
|       print("Filter initialized") | ||||
|     elif self.filter_ready: | ||||
|       #self.update_kalman(current_time, ObservationKind.ECEF_POS, fix_ecef) | ||||
|       gps_est_error = np.sqrt((self.kf.x[0] - fix_ecef[0])**2 + | ||||
|                               (self.kf.x[1] - fix_ecef[1])**2 + | ||||
|                               (self.kf.x[2] - fix_ecef[2])**2) | ||||
|       if gps_est_error > 50: | ||||
|         cloudlog.info("Locationd vs ubloxLocation difference too large, kalman reset") | ||||
|         self.reset_kalman() | ||||
| 
 | ||||
|   def handle_car_state(self, log, current_time): | ||||
|     self.speed_counter += 1 | ||||
|     if self.speed_counter % 5==0: | ||||
|       self.update_kalman(current_time, ObservationKind.ODOMETRIC_SPEED, log.carState.vEgo) | ||||
|       if log.carState.vEgo == 0: | ||||
|         self.update_kalman(current_time, ObservationKind.NO_ROT, [0, 0, 0]) | ||||
| 
 | ||||
|   def handle_ublox_gnss(self, log, current_time): | ||||
|     if hasattr(log.ubloxGnss, 'measurementReport'): | ||||
|       self.raw_gnss_counter += 1 | ||||
|       if True or self.raw_gnss_counter % 3==0: | ||||
|         processed_raw = gnss.process_measurements(gnss.read_raw_ublox(log.ubloxGnss.measurementReport), dog=self.dog) | ||||
|         corrected_raw = gnss.correct_measurements(processed_raw, self.kf.x[:3], dog=self.dog) | ||||
|         corrected_raw = np.array([c.as_array() for c in corrected_raw]).reshape((-1,14)) | ||||
|         self.update_kalman(current_time, ObservationKind.PSEUDORANGE_GPS, corrected_raw) | ||||
|         self.update_kalman(current_time, ObservationKind.PSEUDORANGE_RATE_GPS, corrected_raw) | ||||
|     #elif hasattr(log.ubloxGnss, 'ephemeris'): | ||||
|     #  self.dog.add_ublox_ephems([log]) | ||||
|     #  if len(self.dog.orbits.keys()) < 6: | ||||
|     #    print 'Added ublox ephem now has ', len(self.dog.orbits.keys()) | ||||
| 
 | ||||
|   def handle_qcom_gnss(self, log, current_time): | ||||
|     if hasattr(log.qcomGnss, 'drSvPoly') and self.week is not None: | ||||
|       self.dog.add_qcom_ephems([log], self.week) | ||||
|       if len(list(self.dog.orbits.keys())) < 6: | ||||
|         print('Added qcom ephem now has ', len(list(self.dog.orbits.keys()))) | ||||
|     if hasattr(log.qcomGnss, 'drMeasurementReport') and log.qcomGnss.drMeasurementReport.source == "gps": | ||||
|       self.week = log.qcomGnss.drMeasurementReport.gpsWeek | ||||
| 
 | ||||
|   def handle_cam_odo(self, log, current_time): | ||||
|     self.update_kalman(current_time, ObservationKind.CAMERA_ODO_ROTATION, np.concatenate([log.cameraOdometry.rot, | ||||
|                                                                                           log.cameraOdometry.rotStd])) | ||||
|     self.update_kalman(current_time, ObservationKind.CAMERA_ODO_TRANSLATION, np.concatenate([log.cameraOdometry.trans, | ||||
|                                                                                              log.cameraOdometry.transStd])) | ||||
|     pass | ||||
| 
 | ||||
|   def handle_sensors(self, log, current_time): | ||||
|     for sensor_reading in log.sensorEvents: | ||||
|       # TODO does not yet account for double sensor readings in the log | ||||
|       if sensor_reading.type == 4: | ||||
|         self.gyro_counter += 1 | ||||
|         if True or self.gyro_counter % 5==0: | ||||
|           if max(abs(self.kf.x[23:26])) > 0.07: | ||||
|             print('imu frame angles exceeded, correcting') | ||||
|             self.update_kalman(current_time, ObservationKind.IMU_FRAME, [0, 0, 0]) | ||||
|           self.update_kalman(current_time, ObservationKind.PHONE_GYRO, [-sensor_reading.gyro.v[2], -sensor_reading.gyro.v[1], -sensor_reading.gyro.v[0]]) | ||||
|       if sensor_reading.type == 1: | ||||
|         self.acc_counter += 1 | ||||
|         if True or self.acc_counter % 5==0: | ||||
|           self.update_kalman(current_time, ObservationKind.PHONE_ACCEL, [-sensor_reading.acceleration.v[2], -sensor_reading.acceleration.v[1], -sensor_reading.acceleration.v[0]]) | ||||
| 
 | ||||
|   def handle_log(self, log): | ||||
|     current_time = 1e-9*log.logMonoTime | ||||
|     typ = log.which | ||||
|     if typ in self.disabled_logs: | ||||
|       return | ||||
|     if typ == "sensorEvents": | ||||
|       self.handle_sensors(log, current_time) | ||||
|     elif typ == "gpsLocationExternal": | ||||
|       self.handle_gps(log, current_time) | ||||
|     elif typ == "carState": | ||||
|       self.handle_car_state(log, current_time) | ||||
|     elif typ == "ubloxGnss": | ||||
|       self.handle_ublox_gnss(log, current_time) | ||||
|     elif typ == "qcomGnss": | ||||
|       self.handle_qcom_gnss(log, current_time) | ||||
|     elif typ == "cameraOdometry": | ||||
|       self.handle_cam_odo(log, current_time) | ||||
| 
 | ||||
|   def reset_kalman(self): | ||||
|     self.filter_time = None | ||||
|     self.filter_ready = False | ||||
|     self.observation_buffer = [] | ||||
|     self.converter = None | ||||
|     self.gyro_counter = 0 | ||||
|     self.acc_counter = 0 | ||||
|     self.raw_gnss_counter = 0 | ||||
|     self.speed_counter = 0 | ||||
| 
 | ||||
| 
 | ||||
| def locationd_thread(gctx, addr, disabled_logs): | ||||
|   poller = zmq.Poller() | ||||
| 
 | ||||
|   #carstate = messaging.sub_sock('carState', poller, addr=addr, conflate=True) | ||||
|   gpsLocationExternal = messaging.sub_sock('gpsLocationExternal', poller, addr=addr, conflate=True) | ||||
|   ubloxGnss = messaging.sub_sock('ubloxGnss', poller, addr=addr, conflate=True) | ||||
|   qcomGnss = messaging.sub_sock('qcomGnss', poller, addr=addr, conflate=True) | ||||
|   sensorEvents = messaging.sub_sock('sensorEvents', poller, addr=addr, conflate=True) | ||||
| 
 | ||||
|   liveLocation = messaging.pub_sock('liveLocation') | ||||
| 
 | ||||
|   localizer = Localizer(disabled_logs=disabled_logs) | ||||
|   print("init done") | ||||
| 
 | ||||
|   # buffer with all the messages that still need to be input into the kalman | ||||
|   while 1: | ||||
|     polld = poller.poll(timeout=1000) | ||||
|     for sock, mode in polld: | ||||
|       if mode != zmq.POLLIN: | ||||
|         continue | ||||
|       logs = messaging.drain_sock(sock) | ||||
|       for log in logs: | ||||
|         localizer.handle_log(log) | ||||
| 
 | ||||
|     if localizer.filter_ready and log.which == 'ubloxGnss': | ||||
|       msg = messaging.new_message() | ||||
|       msg.logMonoTime = log.logMonoTime | ||||
|       msg.init('liveLocation') | ||||
|       msg.liveLocation = localizer.liveLocationMsg(log.logMonoTime*1e-9) | ||||
|       liveLocation.send(msg.to_bytes()) | ||||
| 
 | ||||
| 
 | ||||
| def main(gctx=None, addr="127.0.0.1"): | ||||
|   IN_CAR = os.getenv("IN_CAR", False) | ||||
|   disabled_logs = os.getenv("DISABLED_LOGS", "").split(",") | ||||
|   # No speed for now | ||||
|   disabled_logs.append('carState') | ||||
|   if IN_CAR: | ||||
|     addr = "192.168.5.11" | ||||
|   locationd_thread(gctx, addr, disabled_logs) | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|   main() | ||||
| @ -0,0 +1,206 @@ | ||||
| #!/usr/bin/env python3 | ||||
| import os | ||||
| import zmq | ||||
| import math | ||||
| import json | ||||
| 
 | ||||
| import numpy as np | ||||
| from bisect import bisect_right | ||||
| 
 | ||||
| from cereal import car | ||||
| from common.params import Params | ||||
| from common.numpy_fast import clip | ||||
| import cereal.messaging as messaging | ||||
| from selfdrive.swaglog import cloudlog | ||||
| from selfdrive.controls.lib.vehicle_model import VehicleModel | ||||
| from cereal.services import service_list | ||||
| from selfdrive.locationd.kalman.loc_local_kf import LocLocalKalman | ||||
| from selfdrive.locationd.kalman.kalman_helpers import ObservationKind | ||||
| from selfdrive.locationd.params_learner import ParamsLearner | ||||
| 
 | ||||
| DEBUG = False | ||||
| kf = LocLocalKalman()  # Make sure that model is generated on import time | ||||
| 
 | ||||
| 
 | ||||
| LEARNING_RATE = 3 | ||||
| 
 | ||||
| 
 | ||||
| class Localizer(): | ||||
|   def __init__(self, disabled_logs=None, dog=None): | ||||
|     self.kf = LocLocalKalman() | ||||
|     self.reset_kalman() | ||||
| 
 | ||||
|     self.sensor_data_t = 0.0 | ||||
|     self.max_age = .2  # seconds | ||||
|     self.calibration_valid = False | ||||
| 
 | ||||
|     if disabled_logs is None: | ||||
|       self.disabled_logs = list() | ||||
|     else: | ||||
|       self.disabled_logs = disabled_logs | ||||
| 
 | ||||
|   def reset_kalman(self): | ||||
|     self.filter_time = None | ||||
|     self.observation_buffer = [] | ||||
|     self.converter = None | ||||
|     self.speed_counter = 0 | ||||
|     self.sensor_counter = 0 | ||||
| 
 | ||||
|   def liveLocationMsg(self, time): | ||||
|     fix = messaging.log.KalmanOdometry.new_message() | ||||
| 
 | ||||
|     predicted_state = self.kf.x | ||||
|     fix.trans = [float(predicted_state[0]), float(predicted_state[1]), float(predicted_state[2])] | ||||
|     fix.rot = [float(predicted_state[3]), float(predicted_state[4]), float(predicted_state[5])] | ||||
| 
 | ||||
|     return fix | ||||
| 
 | ||||
|   def update_kalman(self, time, kind, meas): | ||||
|     idx = bisect_right([x[0] for x in self.observation_buffer], time) | ||||
|     self.observation_buffer.insert(idx, (time, kind, meas)) | ||||
|     while self.observation_buffer[-1][0] - self.observation_buffer[0][0] > self.max_age: | ||||
|       self.kf.predict_and_observe(*self.observation_buffer.pop(0)) | ||||
| 
 | ||||
|   def handle_cam_odo(self, log, current_time): | ||||
|     self.update_kalman(current_time, ObservationKind.CAMERA_ODO_ROTATION, np.concatenate([log.cameraOdometry.rot, | ||||
|                                                                                           log.cameraOdometry.rotStd])) | ||||
|     self.update_kalman(current_time, ObservationKind.CAMERA_ODO_TRANSLATION, np.concatenate([log.cameraOdometry.trans, | ||||
|                                                                                              log.cameraOdometry.transStd])) | ||||
| 
 | ||||
|   def handle_controls_state(self, log, current_time): | ||||
|     self.speed_counter += 1 | ||||
|     if self.speed_counter % 5 == 0: | ||||
|       self.update_kalman(current_time, ObservationKind.ODOMETRIC_SPEED, np.array([log.controlsState.vEgo])) | ||||
| 
 | ||||
|   def handle_sensors(self, log, current_time): | ||||
|     for sensor_reading in log.sensorEvents: | ||||
|       # TODO does not yet account for double sensor readings in the log | ||||
|       if sensor_reading.type == 4: | ||||
|         self.sensor_counter += 1 | ||||
|         if self.sensor_counter % LEARNING_RATE == 0: | ||||
|           self.update_kalman(current_time, ObservationKind.PHONE_GYRO, [-sensor_reading.gyro.v[2], -sensor_reading.gyro.v[1], -sensor_reading.gyro.v[0]]) | ||||
| 
 | ||||
|   def handle_log(self, log): | ||||
|     current_time = 1e-9 * log.logMonoTime | ||||
|     typ = log.which | ||||
|     if typ in self.disabled_logs: | ||||
|       return | ||||
|     if typ == "sensorEvents": | ||||
|       self.sensor_data_t = current_time | ||||
|       self.handle_sensors(log, current_time) | ||||
|     elif typ == "controlsState": | ||||
|       self.handle_controls_state(log, current_time) | ||||
|     elif typ == "cameraOdometry": | ||||
|       self.handle_cam_odo(log, current_time) | ||||
| 
 | ||||
| 
 | ||||
| def locationd_thread(gctx, addr, disabled_logs): | ||||
|   poller = zmq.Poller() | ||||
| 
 | ||||
|   controls_state_socket = messaging.sub_sock('controlsState', poller, addr=addr, conflate=True) | ||||
|   sensor_events_socket = messaging.sub_sock('sensorEvents', poller, addr=addr, conflate=True) | ||||
|   camera_odometry_socket = messaging.sub_sock('cameraOdometry', poller, addr=addr, conflate=True) | ||||
| 
 | ||||
|   kalman_odometry_socket = messaging.pub_sock('kalmanOdometry') | ||||
|   live_parameters_socket = messaging.pub_sock('liveParameters') | ||||
| 
 | ||||
|   params_reader = Params() | ||||
|   cloudlog.info("Parameter learner is waiting for CarParams") | ||||
|   CP = car.CarParams.from_bytes(params_reader.get("CarParams", block=True)) | ||||
|   VM = VehicleModel(CP) | ||||
|   cloudlog.info("Parameter learner got CarParams: %s" % CP.carFingerprint) | ||||
| 
 | ||||
|   params = params_reader.get("LiveParameters") | ||||
| 
 | ||||
|   # Check if car model matches | ||||
|   if params is not None: | ||||
|     params = json.loads(params) | ||||
|     if (params.get('carFingerprint', None) != CP.carFingerprint) or (params.get('carVin', CP.carVin) != CP.carVin): | ||||
|       cloudlog.info("Parameter learner found parameters for wrong car.") | ||||
|       params = None | ||||
| 
 | ||||
|   if params is None: | ||||
|     params = { | ||||
|       'carFingerprint': CP.carFingerprint, | ||||
|       'carVin': CP.carVin, | ||||
|       'angleOffsetAverage': 0.0, | ||||
|       'stiffnessFactor': 1.0, | ||||
|       'steerRatio': VM.sR, | ||||
|     } | ||||
|     params_reader.put("LiveParameters", json.dumps(params)) | ||||
|     cloudlog.info("Parameter learner resetting to default values") | ||||
| 
 | ||||
|   cloudlog.info("Parameter starting with: %s" % str(params)) | ||||
|   localizer = Localizer(disabled_logs=disabled_logs) | ||||
| 
 | ||||
|   learner = ParamsLearner(VM, | ||||
|                           angle_offset=params['angleOffsetAverage'], | ||||
|                           stiffness_factor=params['stiffnessFactor'], | ||||
|                           steer_ratio=params['steerRatio'], | ||||
|                           learning_rate=LEARNING_RATE) | ||||
| 
 | ||||
|   i = 1 | ||||
|   while True: | ||||
|     for socket, event in poller.poll(timeout=1000): | ||||
|       log = messaging.recv_one(socket) | ||||
|       localizer.handle_log(log) | ||||
| 
 | ||||
|       if socket is controls_state_socket: | ||||
|         if not localizer.kf.t: | ||||
|           continue | ||||
| 
 | ||||
|         if i % LEARNING_RATE == 0: | ||||
|           # controlsState is not updating the Kalman Filter, so update KF manually | ||||
|           localizer.kf.predict(1e-9 * log.logMonoTime) | ||||
| 
 | ||||
|           predicted_state = localizer.kf.x | ||||
|           yaw_rate = -float(predicted_state[5]) | ||||
| 
 | ||||
|           steering_angle = math.radians(log.controlsState.angleSteers) | ||||
|           params_valid = learner.update(yaw_rate, log.controlsState.vEgo, steering_angle) | ||||
| 
 | ||||
|           log_t = 1e-9 * log.logMonoTime | ||||
|           sensor_data_age = log_t - localizer.sensor_data_t | ||||
| 
 | ||||
|           params = messaging.new_message() | ||||
|           params.init('liveParameters') | ||||
|           params.liveParameters.valid = bool(params_valid) | ||||
|           params.liveParameters.sensorValid = bool(sensor_data_age < 5.0) | ||||
|           params.liveParameters.angleOffset = float(math.degrees(learner.ao)) | ||||
|           params.liveParameters.angleOffsetAverage = float(math.degrees(learner.slow_ao)) | ||||
|           params.liveParameters.stiffnessFactor = float(learner.x) | ||||
|           params.liveParameters.steerRatio = float(learner.sR) | ||||
|           live_parameters_socket.send(params.to_bytes()) | ||||
| 
 | ||||
|         if i % 6000 == 0:   # once a minute | ||||
|           params = learner.get_values() | ||||
|           params['carFingerprint'] = CP.carFingerprint | ||||
|           params['carVin'] = CP.carVin | ||||
|           params_reader.put("LiveParameters", json.dumps(params)) | ||||
|           params_reader.put("ControlsParams", json.dumps({'angle_model_bias': log.controlsState.angleModelBias})) | ||||
| 
 | ||||
|         i += 1 | ||||
|       elif socket is camera_odometry_socket: | ||||
|         msg = messaging.new_message() | ||||
|         msg.init('kalmanOdometry') | ||||
|         msg.logMonoTime = log.logMonoTime | ||||
|         msg.kalmanOdometry = localizer.liveLocationMsg(log.logMonoTime * 1e-9) | ||||
|         kalman_odometry_socket.send(msg.to_bytes()) | ||||
|       elif socket is sensor_events_socket: | ||||
|         pass | ||||
| 
 | ||||
| 
 | ||||
| def main(gctx=None, addr="127.0.0.1"): | ||||
|   IN_CAR = os.getenv("IN_CAR", False) | ||||
|   disabled_logs = os.getenv("DISABLED_LOGS", "").split(",") | ||||
| 
 | ||||
|   # No speed for now | ||||
|   disabled_logs.append('controlsState') | ||||
|   if IN_CAR: | ||||
|     addr = "192.168.5.11" | ||||
| 
 | ||||
|   locationd_thread(gctx, addr, disabled_logs) | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|   main() | ||||
| @ -0,0 +1,163 @@ | ||||
| #include <iostream> | ||||
| #include <cmath> | ||||
| 
 | ||||
| #include <capnp/message.h> | ||||
| #include <capnp/serialize-packed.h> | ||||
| #include <eigen3/Eigen/Dense> | ||||
| 
 | ||||
| #include "cereal/gen/cpp/log.capnp.h" | ||||
| 
 | ||||
| #include "locationd_yawrate.h" | ||||
| 
 | ||||
| 
 | ||||
| void Localizer::update_state(const Eigen::Matrix<double, 1, 4> &C, const double R, double current_time, double meas) { | ||||
|   double dt = current_time - prev_update_time; | ||||
| 
 | ||||
|   if (dt < 0) { | ||||
|     dt = 0; | ||||
|   } else { | ||||
|     prev_update_time = current_time; | ||||
|   } | ||||
| 
 | ||||
|   x = A * x; | ||||
|   P = A * P * A.transpose() + dt * Q; | ||||
| 
 | ||||
|   double y = meas - C * x; | ||||
|   double S = R + C * P * C.transpose(); | ||||
|   Eigen::Vector4d K = P * C.transpose() * (1.0 / S); | ||||
|   x = x + K * y; | ||||
|   P = (I - K * C) * P; | ||||
| } | ||||
| 
 | ||||
| void Localizer::handle_sensor_events(capnp::List<cereal::SensorEventData>::Reader sensor_events, double current_time) { | ||||
|   for (cereal::SensorEventData::Reader sensor_event : sensor_events){ | ||||
|     if (sensor_event.getSensor() == 5 && sensor_event.getType() == 16) { | ||||
|       sensor_data_time = current_time; | ||||
|       double meas = -sensor_event.getGyroUncalibrated().getV()[0]; | ||||
|       update_state(C_gyro, R_gyro, current_time, meas); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| void Localizer::handle_camera_odometry(cereal::CameraOdometry::Reader camera_odometry, double current_time) { | ||||
|   double R = pow(30.0 *camera_odometry.getRotStd()[2], 2); | ||||
|   double meas = camera_odometry.getRot()[2]; | ||||
|   update_state(C_posenet, R, current_time, meas); | ||||
| 
 | ||||
|   auto trans = camera_odometry.getTrans(); | ||||
|   posenet_speed = sqrt(trans[0]*trans[0] + trans[1]*trans[1] + trans[2]*trans[2]); | ||||
|   camera_odometry_time = current_time; | ||||
| } | ||||
| 
 | ||||
| void Localizer::handle_controls_state(cereal::ControlsState::Reader controls_state, double current_time) { | ||||
|   steering_angle = controls_state.getAngleSteers() * DEGREES_TO_RADIANS; | ||||
|   car_speed = controls_state.getVEgo(); | ||||
|   controls_state_time = current_time; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| Localizer::Localizer() { | ||||
|   // States: [yaw rate, yaw rate diff, gyro bias, gyro bias diff]
 | ||||
|   A << | ||||
|     1, 1, 0, 0, | ||||
|     0, 1, 0, 0, | ||||
|     0, 0, 1, 1, | ||||
|     0, 0, 0, 1; | ||||
|   I << | ||||
|     1, 0, 0, 0, | ||||
|     0, 1, 0, 0, | ||||
|     0, 0, 1, 0, | ||||
|     0, 0, 0, 1; | ||||
| 
 | ||||
|   Q << | ||||
|     0, 0, 0, 0, | ||||
|     0, pow(0.1, 2.0), 0, 0, | ||||
|     0, 0, 0, 0, | ||||
|     0, 0, pow(0.005 / 100.0, 2.0), 0; | ||||
|   P << | ||||
|     pow(100.0, 2.0), 0, 0, 0, | ||||
|     0, pow(100.0, 2.0), 0, 0, | ||||
|     0, 0, pow(100.0, 2.0), 0, | ||||
|     0, 0, 0, pow(100.0, 2.0); | ||||
| 
 | ||||
|   C_posenet << 1, 0, 0, 0; | ||||
|   C_gyro << 1, 0, 1, 0; | ||||
|   x << 0, 0, 0, 0; | ||||
| 
 | ||||
|   R_gyro = pow(0.25, 2.0); | ||||
| } | ||||
| 
 | ||||
| void Localizer::handle_log(cereal::Event::Reader event) { | ||||
|   double current_time = event.getLogMonoTime() / 1.0e9; | ||||
| 
 | ||||
|   // Initialize update_time on first update
 | ||||
|   if (prev_update_time < 0) { | ||||
|     prev_update_time = current_time; | ||||
|   } | ||||
| 
 | ||||
|   auto type = event.which(); | ||||
|   switch(type) { | ||||
|   case cereal::Event::CONTROLS_STATE: | ||||
|     handle_controls_state(event.getControlsState(), current_time); | ||||
|     break; | ||||
|   case cereal::Event::CAMERA_ODOMETRY: | ||||
|     handle_camera_odometry(event.getCameraOdometry(), current_time); | ||||
|     break; | ||||
|   case cereal::Event::SENSOR_EVENTS: | ||||
|     handle_sensor_events(event.getSensorEvents(), current_time); | ||||
|     break; | ||||
|   default: | ||||
|     break; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| extern "C" { | ||||
|   void *localizer_init(void) { | ||||
|     Localizer * localizer = new Localizer; | ||||
|     return (void*)localizer; | ||||
|   } | ||||
| 
 | ||||
|   void localizer_handle_log(void * localizer, const unsigned char * data, size_t len) { | ||||
|     const kj::ArrayPtr<const capnp::word> view((const capnp::word*)data, len); | ||||
|     capnp::FlatArrayMessageReader msg(view); | ||||
|     cereal::Event::Reader event = msg.getRoot<cereal::Event>(); | ||||
| 
 | ||||
|     Localizer * loc = (Localizer*) localizer; | ||||
|     loc->handle_log(event); | ||||
|   } | ||||
| 
 | ||||
|   double localizer_get_yaw(void * localizer) { | ||||
|     Localizer * loc = (Localizer*) localizer; | ||||
|     return loc->x[0]; | ||||
|   } | ||||
|   double localizer_get_bias(void * localizer) { | ||||
|     Localizer * loc = (Localizer*) localizer; | ||||
|     return loc->x[2]; | ||||
|   } | ||||
| 
 | ||||
|   double * localizer_get_state(void * localizer) { | ||||
|     Localizer * loc = (Localizer*) localizer; | ||||
|     return loc->x.data(); | ||||
|   } | ||||
| 
 | ||||
|   void localizer_set_state(void * localizer, double * state) { | ||||
|     Localizer * loc = (Localizer*) localizer; | ||||
|     memcpy(loc->x.data(), state, 4 * sizeof(double)); | ||||
|   } | ||||
| 
 | ||||
|   double localizer_get_t(void * localizer) { | ||||
|     Localizer * loc = (Localizer*) localizer; | ||||
|     return loc->prev_update_time; | ||||
|   } | ||||
| 
 | ||||
|   double * localizer_get_P(void * localizer) { | ||||
|     Localizer * loc = (Localizer*) localizer; | ||||
|     return loc->P.data(); | ||||
|   } | ||||
| 
 | ||||
|   void localizer_set_P(void * localizer, double * P) { | ||||
|     Localizer * loc = (Localizer*) localizer; | ||||
|     memcpy(loc->P.data(), P, 16 * sizeof(double)); | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,37 @@ | ||||
| #pragma once | ||||
| 
 | ||||
| #include <eigen3/Eigen/Dense> | ||||
| #include "cereal/gen/cpp/log.capnp.h" | ||||
| 
 | ||||
| #define DEGREES_TO_RADIANS 0.017453292519943295 | ||||
| 
 | ||||
| class Localizer | ||||
| { | ||||
|   Eigen::Matrix4d A; | ||||
|   Eigen::Matrix4d I; | ||||
|   Eigen::Matrix4d Q; | ||||
|   Eigen::Matrix<double, 1, 4> C_posenet; | ||||
|   Eigen::Matrix<double, 1, 4> C_gyro; | ||||
| 
 | ||||
|   double R_gyro; | ||||
| 
 | ||||
|   void update_state(const Eigen::Matrix<double, 1, 4> &C, const double R, double current_time, double meas); | ||||
|   void handle_sensor_events(capnp::List<cereal::SensorEventData>::Reader sensor_events, double current_time); | ||||
|   void handle_camera_odometry(cereal::CameraOdometry::Reader camera_odometry, double current_time); | ||||
|   void handle_controls_state(cereal::ControlsState::Reader controls_state, double current_time); | ||||
| 
 | ||||
| public: | ||||
|   Eigen::Vector4d x; | ||||
|   Eigen::Matrix4d P; | ||||
|   double steering_angle = 0; | ||||
|   double car_speed = 0; | ||||
|   double posenet_speed = 0; | ||||
|   double prev_update_time = -1; | ||||
|   double controls_state_time = -1; | ||||
|   double sensor_data_time = -1; | ||||
|   double camera_odometry_time = -1; | ||||
| 
 | ||||
|   Localizer(); | ||||
|   void handle_log(cereal::Event::Reader event); | ||||
| 
 | ||||
| }; | ||||
| @ -0,0 +1,118 @@ | ||||
| #include <algorithm> | ||||
| #include <cmath> | ||||
| #include <iostream> | ||||
| 
 | ||||
| #include <capnp/message.h> | ||||
| #include <capnp/serialize-packed.h> | ||||
| #include "cereal/gen/cpp/log.capnp.h" | ||||
| #include "cereal/gen/cpp/car.capnp.h" | ||||
| #include "params_learner.h" | ||||
| 
 | ||||
| // #define DEBUG
 | ||||
| 
 | ||||
| template <typename T> | ||||
| T clip(const T& n, const T& lower, const T& upper) { | ||||
|   return std::max(lower, std::min(n, upper)); | ||||
| } | ||||
| 
 | ||||
| ParamsLearner::ParamsLearner(cereal::CarParams::Reader car_params, | ||||
|                              double angle_offset, | ||||
|                              double stiffness_factor, | ||||
|                              double steer_ratio, | ||||
|                              double learning_rate) : | ||||
|   ao(angle_offset * DEGREES_TO_RADIANS), | ||||
|   slow_ao(angle_offset * DEGREES_TO_RADIANS), | ||||
|   x(stiffness_factor), | ||||
|   sR(steer_ratio) { | ||||
| 
 | ||||
|   cF0 = car_params.getTireStiffnessFront(); | ||||
|   cR0 = car_params.getTireStiffnessRear(); | ||||
| 
 | ||||
|   l = car_params.getWheelbase(); | ||||
|   m = car_params.getMass(); | ||||
| 
 | ||||
|   aF = car_params.getCenterToFront(); | ||||
|   aR = l - aF; | ||||
| 
 | ||||
|   min_sr = MIN_SR * car_params.getSteerRatio(); | ||||
|   max_sr = MAX_SR * car_params.getSteerRatio(); | ||||
|   min_sr_th = MIN_SR_TH * car_params.getSteerRatio(); | ||||
|   max_sr_th = MAX_SR_TH * car_params.getSteerRatio(); | ||||
|   alpha1 = 0.01 * learning_rate; | ||||
|   alpha2 = 0.0005 * learning_rate; | ||||
|   alpha3 = 0.1 * learning_rate; | ||||
|   alpha4 = 1.0 * learning_rate; | ||||
| } | ||||
| 
 | ||||
| bool ParamsLearner::update(double psi, double u, double sa) { | ||||
|   if (u > 10.0 && fabs(sa) < (DEGREES_TO_RADIANS * 90.)) { | ||||
|     double ao_diff = 2.0*cF0*cR0*l*u*x*(1.0*cF0*cR0*l*u*x*(ao - sa) + psi*sR*(cF0*cR0*pow(l, 2)*x - m*pow(u, 2)*(aF*cF0 - aR*cR0)))/(pow(sR, 2)*pow(cF0*cR0*pow(l, 2)*x - m*pow(u, 2)*(aF*cF0 - aR*cR0), 2)); | ||||
|     double new_ao = ao - alpha1 * ao_diff; | ||||
| 
 | ||||
|     double slow_ao_diff = 2.0*cF0*cR0*l*u*x*(1.0*cF0*cR0*l*u*x*(slow_ao - sa) + psi*sR*(cF0*cR0*pow(l, 2)*x - m*pow(u, 2)*(aF*cF0 - aR*cR0)))/(pow(sR, 2)*pow(cF0*cR0*pow(l, 2)*x - m*pow(u, 2)*(aF*cF0 - aR*cR0), 2)); | ||||
|     double new_slow_ao = slow_ao - alpha2 * slow_ao_diff; | ||||
| 
 | ||||
|     double new_x = x - alpha3 * (-2.0*cF0*cR0*l*m*pow(u, 3)*(slow_ao - sa)*(aF*cF0 - aR*cR0)*(1.0*cF0*cR0*l*u*x*(slow_ao - sa) + psi*sR*(cF0*cR0*pow(l, 2)*x - m*pow(u, 2)*(aF*cF0 - aR*cR0)))/(pow(sR, 2)*pow(cF0*cR0*pow(l, 2)*x - m*pow(u, 2)*(aF*cF0 - aR*cR0), 3))); | ||||
|     double new_sR = sR - alpha4 * (-2.0*cF0*cR0*l*u*x*(slow_ao - sa)*(1.0*cF0*cR0*l*u*x*(slow_ao - sa) + psi*sR*(cF0*cR0*pow(l, 2)*x - m*pow(u, 2)*(aF*cF0 - aR*cR0)))/(pow(sR, 3)*pow(cF0*cR0*pow(l, 2)*x - m*pow(u, 2)*(aF*cF0 - aR*cR0), 2))); | ||||
| 
 | ||||
|     ao = new_ao; | ||||
|     slow_ao = new_slow_ao; | ||||
|     x = new_x; | ||||
|     sR = new_sR; | ||||
|   } | ||||
| 
 | ||||
| #ifdef DEBUG | ||||
|   std::cout << "Instant AO: " << (RADIANS_TO_DEGREES * ao) << "\tAverage AO: " << (RADIANS_TO_DEGREES * slow_ao); | ||||
|   std::cout << "\tStiffness: " << x << "\t sR: " << sR << std::endl; | ||||
| #endif | ||||
| 
 | ||||
|   ao = clip(ao, -MAX_ANGLE_OFFSET, MAX_ANGLE_OFFSET); | ||||
|   slow_ao = clip(slow_ao, -MAX_ANGLE_OFFSET, MAX_ANGLE_OFFSET); | ||||
|   x = clip(x, MIN_STIFFNESS, MAX_STIFFNESS); | ||||
|   sR = clip(sR, min_sr, max_sr); | ||||
| 
 | ||||
|   bool valid = fabs(slow_ao) < MAX_ANGLE_OFFSET_TH; | ||||
|   valid = valid && sR > min_sr_th; | ||||
|   valid = valid && sR < max_sr_th; | ||||
|   return valid; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| extern "C" { | ||||
|   void *params_learner_init(size_t len, char * params, double angle_offset, double stiffness_factor, double steer_ratio, double learning_rate) { | ||||
| 
 | ||||
|     auto amsg = kj::heapArray<capnp::word>((len / sizeof(capnp::word)) + 1); | ||||
|     memcpy(amsg.begin(), params, len); | ||||
| 
 | ||||
|     capnp::FlatArrayMessageReader cmsg(amsg); | ||||
|     cereal::CarParams::Reader car_params = cmsg.getRoot<cereal::CarParams>(); | ||||
| 
 | ||||
|     ParamsLearner * p = new ParamsLearner(car_params, angle_offset, stiffness_factor, steer_ratio, learning_rate); | ||||
|     return (void*)p; | ||||
|   } | ||||
| 
 | ||||
|   bool params_learner_update(void * params_learner, double psi, double u, double sa) { | ||||
|     ParamsLearner * p = (ParamsLearner*) params_learner; | ||||
|     return p->update(psi, u, sa); | ||||
|   } | ||||
| 
 | ||||
|   double params_learner_get_ao(void * params_learner){ | ||||
|     ParamsLearner * p = (ParamsLearner*) params_learner; | ||||
|     return p->ao; | ||||
|   } | ||||
| 
 | ||||
|   double params_learner_get_x(void * params_learner){ | ||||
|     ParamsLearner * p = (ParamsLearner*) params_learner; | ||||
|     return p->x; | ||||
|   } | ||||
| 
 | ||||
|   double params_learner_get_slow_ao(void * params_learner){ | ||||
|     ParamsLearner * p = (ParamsLearner*) params_learner; | ||||
|     return p->slow_ao; | ||||
|   } | ||||
| 
 | ||||
|   double params_learner_get_sR(void * params_learner){ | ||||
|     ParamsLearner * p = (ParamsLearner*) params_learner; | ||||
|     return p->sR; | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,35 @@ | ||||
| #pragma once | ||||
| 
 | ||||
| #define DEGREES_TO_RADIANS 0.017453292519943295 | ||||
| #define RADIANS_TO_DEGREES (1.0 / DEGREES_TO_RADIANS) | ||||
| 
 | ||||
| #define MAX_ANGLE_OFFSET (10.0 * DEGREES_TO_RADIANS) | ||||
| #define MAX_ANGLE_OFFSET_TH  (9.0 * DEGREES_TO_RADIANS) | ||||
| #define MIN_STIFFNESS  0.5 | ||||
| #define MAX_STIFFNESS  2.0 | ||||
| #define MIN_SR  0.5 | ||||
| #define MAX_SR  2.0 | ||||
| #define MIN_SR_TH  0.55 | ||||
| #define MAX_SR_TH  1.9 | ||||
| 
 | ||||
| class ParamsLearner { | ||||
|   double cF0, cR0; | ||||
|   double aR, aF; | ||||
|   double l, m; | ||||
| 
 | ||||
|   double min_sr, max_sr, min_sr_th, max_sr_th; | ||||
|   double alpha1, alpha2, alpha3, alpha4; | ||||
| 
 | ||||
| public: | ||||
|   double ao; | ||||
|   double slow_ao; | ||||
|   double x, sR; | ||||
| 
 | ||||
|   ParamsLearner(cereal::CarParams::Reader car_params, | ||||
|                 double angle_offset, | ||||
|                 double stiffness_factor, | ||||
|                 double steer_ratio, | ||||
|                 double learning_rate); | ||||
| 
 | ||||
|   bool update(double psi, double u, double sa); | ||||
| }; | ||||
| @ -0,0 +1,84 @@ | ||||
| import math | ||||
| from common.numpy_fast import clip | ||||
| 
 | ||||
| MAX_ANGLE_OFFSET = math.radians(10.) | ||||
| MAX_ANGLE_OFFSET_TH = math.radians(9.) | ||||
| MIN_STIFFNESS = 0.5 | ||||
| MAX_STIFFNESS = 2.0 | ||||
| MIN_SR = 0.5 | ||||
| MAX_SR = 2.0 | ||||
| MIN_SR_TH = 0.55 | ||||
| MAX_SR_TH = 1.9 | ||||
| 
 | ||||
| DEBUG = False | ||||
| 
 | ||||
| 
 | ||||
| class ParamsLearner(): | ||||
|   def __init__(self, VM, angle_offset=0., stiffness_factor=1.0, steer_ratio=None, learning_rate=1.0): | ||||
|     self.VM = VM | ||||
| 
 | ||||
|     self.ao = math.radians(angle_offset) | ||||
|     self.slow_ao = math.radians(angle_offset) | ||||
|     self.x = stiffness_factor | ||||
|     self.sR = VM.sR if steer_ratio is None else steer_ratio | ||||
|     self.MIN_SR = MIN_SR * self.VM.sR | ||||
|     self.MAX_SR = MAX_SR * self.VM.sR | ||||
|     self.MIN_SR_TH = MIN_SR_TH * self.VM.sR | ||||
|     self.MAX_SR_TH = MAX_SR_TH * self.VM.sR | ||||
| 
 | ||||
|     self.alpha1 = 0.01 * learning_rate | ||||
|     self.alpha2 = 0.0005 * learning_rate | ||||
|     self.alpha3 = 0.1 * learning_rate | ||||
|     self.alpha4 = 1.0 * learning_rate | ||||
| 
 | ||||
|   def get_values(self): | ||||
|     return { | ||||
|       'angleOffsetAverage': math.degrees(self.slow_ao), | ||||
|       'stiffnessFactor': self.x, | ||||
|       'steerRatio': self.sR, | ||||
|     } | ||||
| 
 | ||||
|   def update(self, psi, u, sa): | ||||
|     cF0 = self.VM.cF | ||||
|     cR0 = self.VM.cR | ||||
|     aR = self.VM.aR | ||||
|     aF = self.VM.aF | ||||
|     l = self.VM.l | ||||
|     m = self.VM.m | ||||
| 
 | ||||
|     x = self.x | ||||
|     ao = self.ao | ||||
|     sR = self.sR | ||||
| 
 | ||||
|     # Gradient descent:  learn angle offset, tire stiffness and steer ratio. | ||||
|     if u > 10.0 and abs(math.degrees(sa)) < 15.: | ||||
|       self.ao -= self.alpha1 * 2.0*cF0*cR0*l*u*x*(1.0*cF0*cR0*l*u*x*(ao - sa) + psi*sR*(cF0*cR0*l**2*x - m*u**2*(aF*cF0 - aR*cR0)))/(sR**2*(cF0*cR0*l**2*x - m*u**2*(aF*cF0 - aR*cR0))**2) | ||||
| 
 | ||||
|       ao = self.slow_ao | ||||
|       self.slow_ao -= self.alpha2 * 2.0*cF0*cR0*l*u*x*(1.0*cF0*cR0*l*u*x*(ao - sa) + psi*sR*(cF0*cR0*l**2*x - m*u**2*(aF*cF0 - aR*cR0)))/(sR**2*(cF0*cR0*l**2*x - m*u**2*(aF*cF0 - aR*cR0))**2) | ||||
| 
 | ||||
|       self.x -= self.alpha3 * -2.0*cF0*cR0*l*m*u**3*(ao - sa)*(aF*cF0 - aR*cR0)*(1.0*cF0*cR0*l*u*x*(ao - sa) + psi*sR*(cF0*cR0*l**2*x - m*u**2*(aF*cF0 - aR*cR0)))/(sR**2*(cF0*cR0*l**2*x - m*u**2*(aF*cF0 - aR*cR0))**3) | ||||
| 
 | ||||
|       self.sR -= self.alpha4 * -2.0*cF0*cR0*l*u*x*(ao - sa)*(1.0*cF0*cR0*l*u*x*(ao - sa) + psi*sR*(cF0*cR0*l**2*x - m*u**2*(aF*cF0 - aR*cR0)))/(sR**3*(cF0*cR0*l**2*x - m*u**2*(aF*cF0 - aR*cR0))**2) | ||||
| 
 | ||||
|     if DEBUG: | ||||
|       # s1 = "Measured yaw rate % .6f" % psi | ||||
|       # ao = 0. | ||||
|       # s2 = "Uncompensated yaw % .6f" % (1.0*u*(-ao + sa)/(l*sR*(1 - m*u**2*(aF*cF0*x - aR*cR0*x)/(cF0*cR0*l**2*x**2)))) | ||||
|       # instant_ao = aF*m*psi*sR*u/(cR0*l*x) - aR*m*psi*sR*u/(cF0*l*x) - l*psi*sR/u + sa | ||||
|       s4 = "Instant AO: % .6f Avg. AO % .6f" % (math.degrees(self.ao), math.degrees(self.slow_ao)) | ||||
|       s5 = "Stiffnes: % .6f x" % self.x | ||||
|       s6 = "sR: %.4f" % self.sR | ||||
|       print("{0} {1} {2}".format(s4, s5, s6)) | ||||
| 
 | ||||
| 
 | ||||
|     self.ao = clip(self.ao, -MAX_ANGLE_OFFSET, MAX_ANGLE_OFFSET) | ||||
|     self.slow_ao = clip(self.slow_ao, -MAX_ANGLE_OFFSET, MAX_ANGLE_OFFSET) | ||||
|     self.x = clip(self.x, MIN_STIFFNESS, MAX_STIFFNESS) | ||||
|     self.sR = clip(self.sR, self.MIN_SR, self.MAX_SR) | ||||
| 
 | ||||
|     # don't check stiffness for validity, as it can change quickly if sR is off | ||||
|     valid = abs(self.slow_ao) < MAX_ANGLE_OFFSET_TH and \ | ||||
|       self.sR > self.MIN_SR_TH and self.sR < self.MAX_SR_TH | ||||
| 
 | ||||
|     return valid | ||||
| @ -0,0 +1,186 @@ | ||||
| #include <future> | ||||
| #include <iostream> | ||||
| #include <cassert> | ||||
| #include <csignal> | ||||
| #include <unistd.h> | ||||
| 
 | ||||
| 
 | ||||
| #include <capnp/message.h> | ||||
| #include <capnp/serialize-packed.h> | ||||
| 
 | ||||
| #include "json11.hpp" | ||||
| #include "cereal/gen/cpp/log.capnp.h" | ||||
| 
 | ||||
| #include "common/swaglog.h" | ||||
| #include "common/messaging.h" | ||||
| #include "common/params.h" | ||||
| #include "common/timing.h" | ||||
| 
 | ||||
| #include "messaging.hpp" | ||||
| #include "locationd_yawrate.h" | ||||
| #include "params_learner.h" | ||||
| 
 | ||||
| 
 | ||||
| void sigpipe_handler(int sig) { | ||||
|   LOGE("SIGPIPE received"); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| int main(int argc, char *argv[]) { | ||||
|   signal(SIGPIPE, (sighandler_t)sigpipe_handler); | ||||
| 
 | ||||
|   Context * c = Context::create(); | ||||
|   SubSocket * controls_state_sock = SubSocket::create(c, "controlsState"); | ||||
|   SubSocket * sensor_events_sock = SubSocket::create(c, "sensorEvents"); | ||||
|   SubSocket * camera_odometry_sock = SubSocket::create(c, "cameraOdometry"); | ||||
|   PubSocket * live_parameters_sock = PubSocket::create(c, "liveParameters"); | ||||
| 
 | ||||
|   assert(controls_state_sock != NULL); | ||||
|   assert(sensor_events_sock != NULL); | ||||
|   assert(camera_odometry_sock != NULL); | ||||
|   assert(live_parameters_sock != NULL); | ||||
| 
 | ||||
|   Poller * poller = Poller::create({controls_state_sock, sensor_events_sock, camera_odometry_sock}); | ||||
| 
 | ||||
|   Localizer localizer; | ||||
| 
 | ||||
|   // Read car params
 | ||||
|   char *value; | ||||
|   size_t value_sz = 0; | ||||
| 
 | ||||
|   LOGW("waiting for params to set vehicle model"); | ||||
|   while (true) { | ||||
|     read_db_value(NULL, "CarParams", &value, &value_sz); | ||||
|     if (value_sz > 0) break; | ||||
|     usleep(100*1000); | ||||
|   } | ||||
|   LOGW("got %d bytes CarParams", value_sz); | ||||
| 
 | ||||
|   // make copy due to alignment issues
 | ||||
|   auto amsg = kj::heapArray<capnp::word>((value_sz / sizeof(capnp::word)) + 1); | ||||
|   memcpy(amsg.begin(), value, value_sz); | ||||
|   free(value); | ||||
| 
 | ||||
|   capnp::FlatArrayMessageReader cmsg(amsg); | ||||
|   cereal::CarParams::Reader car_params = cmsg.getRoot<cereal::CarParams>(); | ||||
| 
 | ||||
|   // Read params from previous run
 | ||||
|   const int result = read_db_value(NULL, "LiveParameters", &value, &value_sz); | ||||
| 
 | ||||
|   std::string fingerprint = car_params.getCarFingerprint(); | ||||
|   std::string vin = car_params.getCarVin(); | ||||
|   double sR = car_params.getSteerRatio(); | ||||
|   double x = 1.0; | ||||
|   double ao = 0.0; | ||||
|   double posenet_invalid_count = 0; | ||||
| 
 | ||||
|   if (result == 0){ | ||||
|     auto str = std::string(value, value_sz); | ||||
|     free(value); | ||||
| 
 | ||||
|     std::string err; | ||||
|     auto json = json11::Json::parse(str, err); | ||||
|     if (json.is_null() || !err.empty()) { | ||||
|       std::string log = "Error parsing json: " + err; | ||||
|       LOGW(log.c_str()); | ||||
|     } else { | ||||
|       std::string new_fingerprint = json["carFingerprint"].string_value(); | ||||
|       std::string new_vin = json["carVin"].string_value(); | ||||
| 
 | ||||
|       if (fingerprint == new_fingerprint && vin == new_vin) { | ||||
|         std::string log = "Parameter starting with: " + str; | ||||
|         LOGW(log.c_str()); | ||||
| 
 | ||||
|         sR = json["steerRatio"].number_value(); | ||||
|         x = json["stiffnessFactor"].number_value(); | ||||
|         ao = json["angleOffsetAverage"].number_value(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   ParamsLearner learner(car_params, ao, x, sR, 1.0); | ||||
| 
 | ||||
|   // Main loop
 | ||||
|   int save_counter = 0; | ||||
|   while (true){ | ||||
|     for (auto s : poller->poll(100)){ | ||||
|       Message * msg = s->receive(); | ||||
| 
 | ||||
|       auto amsg = kj::heapArray<capnp::word>((msg->getSize() / sizeof(capnp::word)) + 1); | ||||
|       memcpy(amsg.begin(), msg->getData(), msg->getSize()); | ||||
| 
 | ||||
|       capnp::FlatArrayMessageReader capnp_msg(amsg); | ||||
|       cereal::Event::Reader event = capnp_msg.getRoot<cereal::Event>(); | ||||
| 
 | ||||
|       localizer.handle_log(event); | ||||
| 
 | ||||
|       auto which = event.which(); | ||||
|       // Throw vision failure if posenet and odometric speed too different
 | ||||
|       if (which == cereal::Event::CAMERA_ODOMETRY){ | ||||
|         if (std::abs(localizer.posenet_speed - localizer.car_speed) > std::max(0.4 * localizer.car_speed, 5.0)) { | ||||
|           posenet_invalid_count++; | ||||
|         } else { | ||||
|           posenet_invalid_count = 0; | ||||
|         } | ||||
|       } else if (which == cereal::Event::CONTROLS_STATE){ | ||||
|         save_counter++; | ||||
| 
 | ||||
|         double yaw_rate = -localizer.x[0]; | ||||
|         bool valid = learner.update(yaw_rate, localizer.car_speed, localizer.steering_angle); | ||||
| 
 | ||||
|         // TODO: Fix in replay
 | ||||
|         double sensor_data_age = localizer.controls_state_time - localizer.sensor_data_time; | ||||
|         double camera_odometry_age = localizer.controls_state_time - localizer.camera_odometry_time; | ||||
| 
 | ||||
|         double angle_offset_degrees = RADIANS_TO_DEGREES * learner.ao; | ||||
|         double angle_offset_average_degrees = RADIANS_TO_DEGREES * learner.slow_ao; | ||||
| 
 | ||||
|         capnp::MallocMessageBuilder msg; | ||||
|         cereal::Event::Builder event = msg.initRoot<cereal::Event>(); | ||||
|         event.setLogMonoTime(nanos_since_boot()); | ||||
|         auto live_params = event.initLiveParameters(); | ||||
|         live_params.setValid(valid); | ||||
|         live_params.setYawRate(localizer.x[0]); | ||||
|         live_params.setGyroBias(localizer.x[2]); | ||||
|         live_params.setSensorValid(sensor_data_age < 5.0); | ||||
|         live_params.setAngleOffset(angle_offset_degrees); | ||||
|         live_params.setAngleOffsetAverage(angle_offset_average_degrees); | ||||
|         live_params.setStiffnessFactor(learner.x); | ||||
|         live_params.setSteerRatio(learner.sR); | ||||
|         live_params.setPosenetSpeed(localizer.posenet_speed); | ||||
|         live_params.setPosenetValid((posenet_invalid_count < 4) && (camera_odometry_age < 5.0)); | ||||
| 
 | ||||
|         auto words = capnp::messageToFlatArray(msg); | ||||
|         auto bytes = words.asBytes(); | ||||
|         live_parameters_sock->send((char*)bytes.begin(), bytes.size()); | ||||
| 
 | ||||
|         // Save parameters every minute
 | ||||
|         if (save_counter % 6000 == 0) { | ||||
|           json11::Json json = json11::Json::object { | ||||
|                                                     {"carVin", vin}, | ||||
|                                                     {"carFingerprint", fingerprint}, | ||||
|                                                     {"steerRatio", learner.sR}, | ||||
|                                                     {"stiffnessFactor", learner.x}, | ||||
|                                                     {"angleOffsetAverage", angle_offset_average_degrees}, | ||||
|           }; | ||||
| 
 | ||||
|           std::string out = json.dump(); | ||||
|           std::async(std::launch::async, | ||||
|                      [out]{ | ||||
|                        write_db_value(NULL, "LiveParameters", out.c_str(), out.length()); | ||||
|                      }); | ||||
|         } | ||||
|       } | ||||
|       delete msg; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   delete live_parameters_sock; | ||||
|   delete controls_state_sock; | ||||
|   delete camera_odometry_sock; | ||||
|   delete sensor_events_sock; | ||||
|   delete poller; | ||||
|   delete c; | ||||
| 
 | ||||
|   return 0; | ||||
| } | ||||
| @ -0,0 +1 @@ | ||||
| out/ | ||||
| @ -0,0 +1,79 @@ | ||||
| #!/usr/bin/env python3 | ||||
| import subprocess | ||||
| import os | ||||
| import sys | ||||
| import argparse | ||||
| import tempfile | ||||
| 
 | ||||
| from selfdrive.locationd.test.ubloxd_py_test import parser_test | ||||
| from selfdrive.locationd.test.ubloxd_regression_test import compare_results | ||||
| 
 | ||||
| 
 | ||||
| def mkdirs_exists_ok(path): | ||||
|   try: | ||||
|     os.makedirs(path) | ||||
|   except OSError: | ||||
|     if not os.path.isdir(path): | ||||
|       raise | ||||
| 
 | ||||
| 
 | ||||
| def main(args): | ||||
|   cur_dir = os.path.dirname(os.path.realpath(__file__)) | ||||
|   ubloxd_dir = os.path.join(cur_dir, '../') | ||||
| 
 | ||||
|   cc_output_dir = os.path.join(args.output_dir, 'cc') | ||||
|   mkdirs_exists_ok(cc_output_dir) | ||||
| 
 | ||||
|   py_output_dir = os.path.join(args.output_dir, 'py') | ||||
|   mkdirs_exists_ok(py_output_dir) | ||||
| 
 | ||||
|   archive_file = os.path.join(cur_dir, args.stream_gz_file) | ||||
| 
 | ||||
|   try: | ||||
|     print('Extracting stream file') | ||||
|     subprocess.check_call(['tar', 'zxf', archive_file], cwd=tempfile.gettempdir()) | ||||
|     stream_file_path = os.path.join(tempfile.gettempdir(), 'ubloxRaw.stream') | ||||
| 
 | ||||
|     if not os.path.isfile(stream_file_path): | ||||
|       print('Extract file failed') | ||||
|       sys.exit(-3) | ||||
| 
 | ||||
|     print('Run regression test - CC parser...') | ||||
|     if args.valgrind: | ||||
|       subprocess.check_call(["valgrind", "--leak-check=full", os.path.join(ubloxd_dir, 'ubloxd_test'), stream_file_path, cc_output_dir]) | ||||
|     else: | ||||
|       subprocess.check_call([os.path.join(ubloxd_dir, 'ubloxd_test'), stream_file_path, cc_output_dir]) | ||||
| 
 | ||||
|     print('Running regression test - py parser...') | ||||
|     parser_test(stream_file_path, py_output_dir) | ||||
| 
 | ||||
|     print('Running regression test - compare result...') | ||||
|     r = compare_results(cc_output_dir, py_output_dir) | ||||
| 
 | ||||
|     print('All done!') | ||||
| 
 | ||||
|     subprocess.check_call(["rm", stream_file_path]) | ||||
|     subprocess.check_call(["rm", '-rf', cc_output_dir]) | ||||
|     subprocess.check_call(["rm", '-rf', py_output_dir]) | ||||
|     sys.exit(r) | ||||
| 
 | ||||
|   except subprocess.CalledProcessError as e: | ||||
|     print('CI test failed with {}'.format(e.returncode)) | ||||
|     sys.exit(e.returncode) | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|   parser = argparse.ArgumentParser(description="Ubloxd CI test", | ||||
|                                    formatter_class=argparse.ArgumentDefaultsHelpFormatter) | ||||
| 
 | ||||
|   parser.add_argument("stream_gz_file", nargs='?', default='ubloxRaw.tar.gz', | ||||
|                       help="UbloxRaw data stream zip file") | ||||
| 
 | ||||
|   parser.add_argument("output_dir", nargs='?', default='out', | ||||
|                       help="Output events temp directory") | ||||
| 
 | ||||
|   parser.add_argument("--valgrind", default=False, action='store_true', | ||||
|                       help="Run in valgrind") | ||||
| 
 | ||||
|   args = parser.parse_args() | ||||
|   main(args) | ||||
| @ -0,0 +1,137 @@ | ||||
| def GET_FIELD_U(w, nb, pos): | ||||
|   return (((w) >> (pos)) & ((1 << (nb)) - 1)) | ||||
| 
 | ||||
| 
 | ||||
| def twos_complement(v, nb): | ||||
|   sign = v >> (nb - 1) | ||||
|   value = v | ||||
|   if sign != 0: | ||||
|     value = value - (1 << nb) | ||||
|   return value | ||||
| 
 | ||||
| 
 | ||||
| def GET_FIELD_S(w, nb, pos): | ||||
|   v = GET_FIELD_U(w, nb, pos) | ||||
|   return twos_complement(v, nb) | ||||
| 
 | ||||
| 
 | ||||
| def extract_uint8(v, b): | ||||
|   return (v >> (8 * (3 - b))) & 255 | ||||
| 
 | ||||
| def extract_int8(v, b): | ||||
|   value = extract_uint8(v, b) | ||||
|   if value > 127: | ||||
|     value -= 256 | ||||
|   return value | ||||
| 
 | ||||
| class EphemerisData: | ||||
|   '''container for parsing a AID_EPH message | ||||
|     Thanks to Sylvain Munaut <tnt@246tNt.com> | ||||
|     http://cgit.osmocom.org/cgit/osmocom-lcs/tree/gps.c | ||||
| on of this parser | ||||
| 
 | ||||
|     See IS-GPS-200F.pdf Table 20-III for the field meanings, scaling factors and | ||||
|     field widths | ||||
|     ''' | ||||
| 
 | ||||
|   def __init__(self, svId, subframes): | ||||
|     from math import pow | ||||
|     self.svId = svId | ||||
|     week_no = GET_FIELD_U(subframes[1][2+0], 10, 20) | ||||
|     # code_on_l2 = GET_FIELD_U(subframes[1][0],  2, 12) | ||||
|     # sv_ura     = GET_FIELD_U(subframes[1][0],  4,  8) | ||||
|     # sv_health  = GET_FIELD_U(subframes[1][0],  6,  2) | ||||
|     # l2_p_flag  = GET_FIELD_U(subframes[1][1],  1, 23) | ||||
|     t_gd = GET_FIELD_S(subframes[1][2+4], 8, 6) | ||||
|     iodc = (GET_FIELD_U(subframes[1][2+0], 2, 6) << 8) | GET_FIELD_U( | ||||
|       subframes[1][2+5], 8, 22) | ||||
| 
 | ||||
|     t_oc = GET_FIELD_U(subframes[1][2+5], 16, 6) | ||||
|     a_f2 = GET_FIELD_S(subframes[1][2+6], 8, 22) | ||||
|     a_f1 = GET_FIELD_S(subframes[1][2+6], 16, 6) | ||||
|     a_f0 = GET_FIELD_S(subframes[1][2+7], 22, 8) | ||||
| 
 | ||||
|     c_rs = GET_FIELD_S(subframes[2][2+0], 16, 6) | ||||
|     delta_n = GET_FIELD_S(subframes[2][2+1], 16, 14) | ||||
|     m_0 = (GET_FIELD_S(subframes[2][2+1], 8, 6) << 24) | GET_FIELD_U( | ||||
|       subframes[2][2+2], 24, 6) | ||||
|     c_uc = GET_FIELD_S(subframes[2][2+3], 16, 14) | ||||
|     e = (GET_FIELD_U(subframes[2][2+3], 8, 6) << 24) | GET_FIELD_U(subframes[2][2+4], 24, 6) | ||||
|     c_us = GET_FIELD_S(subframes[2][2+5], 16, 14) | ||||
|     a_powhalf = (GET_FIELD_U(subframes[2][2+5], 8, 6) << 24) | GET_FIELD_U( | ||||
|       subframes[2][2+6], 24, 6) | ||||
|     t_oe = GET_FIELD_U(subframes[2][2+7], 16, 14) | ||||
|     # fit_flag   = GET_FIELD_U(subframes[2][7],  1,  7) | ||||
| 
 | ||||
|     c_ic = GET_FIELD_S(subframes[3][2+0], 16, 14) | ||||
|     omega_0 = (GET_FIELD_S(subframes[3][2+0], 8, 6) << 24) | GET_FIELD_U( | ||||
|       subframes[3][2+1], 24, 6) | ||||
|     c_is = GET_FIELD_S(subframes[3][2+2], 16, 14) | ||||
|     i_0 = (GET_FIELD_S(subframes[3][2+2], 8, 6) << 24) | GET_FIELD_U( | ||||
|       subframes[3][2+3], 24, 6) | ||||
|     c_rc = GET_FIELD_S(subframes[3][2+4], 16, 14) | ||||
|     w = (GET_FIELD_S(subframes[3][2+4], 8, 6) << 24) | GET_FIELD_U(subframes[3][5], 24, 6) | ||||
|     omega_dot = GET_FIELD_S(subframes[3][2+6], 24, 6) | ||||
|     idot = GET_FIELD_S(subframes[3][2+7], 14, 8) | ||||
| 
 | ||||
|     self._rsvd1 = GET_FIELD_U(subframes[1][2+1], 23, 6) | ||||
|     self._rsvd2 = GET_FIELD_U(subframes[1][2+2], 24, 6) | ||||
|     self._rsvd3 = GET_FIELD_U(subframes[1][2+3], 24, 6) | ||||
|     self._rsvd4 = GET_FIELD_U(subframes[1][2+4], 16, 14) | ||||
|     self.aodo = GET_FIELD_U(subframes[2][2+7], 5, 8) | ||||
| 
 | ||||
|     # Definition of Pi used in the GPS coordinate system | ||||
|     gpsPi = 3.1415926535898 | ||||
| 
 | ||||
|     # now form variables in radians, meters and seconds etc | ||||
|     self.Tgd = t_gd * pow(2, -31) | ||||
|     self.A = pow(a_powhalf * pow(2, -19), 2.0) | ||||
|     self.cic = c_ic * pow(2, -29) | ||||
|     self.cis = c_is * pow(2, -29) | ||||
|     self.crc = c_rc * pow(2, -5) | ||||
|     self.crs = c_rs * pow(2, -5) | ||||
|     self.cuc = c_uc * pow(2, -29) | ||||
|     self.cus = c_us * pow(2, -29) | ||||
|     self.deltaN = delta_n * pow(2, -43) * gpsPi | ||||
|     self.ecc = e * pow(2, -33) | ||||
|     self.i0 = i_0 * pow(2, -31) * gpsPi | ||||
|     self.idot = idot * pow(2, -43) * gpsPi | ||||
|     self.M0 = m_0 * pow(2, -31) * gpsPi | ||||
|     self.omega = w * pow(2, -31) * gpsPi | ||||
|     self.omega_dot = omega_dot * pow(2, -43) * gpsPi | ||||
|     self.omega0 = omega_0 * pow(2, -31) * gpsPi | ||||
|     self.toe = t_oe * pow(2, 4) | ||||
| 
 | ||||
|     # clock correction information | ||||
|     self.toc = t_oc * pow(2, 4) | ||||
|     self.gpsWeek = week_no | ||||
|     self.af0 = a_f0 * pow(2, -31) | ||||
|     self.af1 = a_f1 * pow(2, -43) | ||||
|     self.af2 = a_f2 * pow(2, -55) | ||||
| 
 | ||||
|     iode1 = GET_FIELD_U(subframes[2][2+0], 8, 22) | ||||
|     iode2 = GET_FIELD_U(subframes[3][2+7], 8, 22) | ||||
|     self.valid = (iode1 == iode2) and (iode1 == (iodc & 0xff)) | ||||
|     self.iode = iode1 | ||||
| 
 | ||||
|     if GET_FIELD_U(subframes[4][2+0], 6, 22) == 56 and \ | ||||
|        GET_FIELD_U(subframes[4][2+0], 2, 28) == 1 and \ | ||||
|        GET_FIELD_U(subframes[5][2+0], 2, 28) == 1: | ||||
|       a0 = GET_FIELD_S(subframes[4][2], 8, 14) * pow(2, -30) | ||||
|       a1 = GET_FIELD_S(subframes[4][2], 8, 6) * pow(2, -27) | ||||
|       a2 = GET_FIELD_S(subframes[4][3], 8, 22) * pow(2, -24) | ||||
|       a3 = GET_FIELD_S(subframes[4][3], 8, 14) * pow(2, -24) | ||||
|       b0 = GET_FIELD_S(subframes[4][3], 8, 6) * pow(2, 11) | ||||
|       b1 = GET_FIELD_S(subframes[4][4], 8, 22) * pow(2, 14) | ||||
|       b2 = GET_FIELD_S(subframes[4][4], 8, 14) * pow(2, 16) | ||||
|       b3 = GET_FIELD_S(subframes[4][4], 8, 6) * pow(2, 16) | ||||
| 
 | ||||
|       self.ionoAlpha = [a0, a1, a2, a3] | ||||
|       self.ionoBeta = [b0, b1, b2, b3] | ||||
|       self.ionoCoeffsValid = True | ||||
|     else: | ||||
|       self.ionoAlpha = [] | ||||
|       self.ionoBeta = [] | ||||
|       self.ionoCoeffsValid = False | ||||
| 
 | ||||
| 
 | ||||
| @ -0,0 +1,53 @@ | ||||
| #!/usr/bin/env python3 | ||||
| 
 | ||||
| import numpy as np | ||||
| import unittest | ||||
| 
 | ||||
| from selfdrive.car.honda.interface import CarInterface | ||||
| from selfdrive.car.honda.values import CAR | ||||
| from selfdrive.controls.lib.vehicle_model import VehicleModel | ||||
| from selfdrive.locationd.liblocationd_py import liblocationd # pylint: disable=no-name-in-module, import-error | ||||
| 
 | ||||
| 
 | ||||
| class TestParamsLearner(unittest.TestCase): | ||||
|   def setUp(self): | ||||
| 
 | ||||
|     self.CP = CarInterface.get_params(CAR.CIVIC) | ||||
|     bts = self.CP.to_bytes() | ||||
| 
 | ||||
|     self.params_learner = liblocationd.params_learner_init(len(bts), bts, 0.0, 1.0, self.CP.steerRatio, 1.0) | ||||
| 
 | ||||
|   def test_convergence(self): | ||||
|     # Setup vehicle model with wrong parameters | ||||
|     VM_sim = VehicleModel(self.CP) | ||||
|     x_target = 0.75 | ||||
|     sr_target = self.CP.steerRatio - 0.5 | ||||
|     ao_target = -1.0 | ||||
|     VM_sim.update_params(x_target, sr_target) | ||||
| 
 | ||||
|     # Run simulation | ||||
|     times = np.arange(0, 15*3600, 0.01) | ||||
|     angle_offset = np.radians(ao_target) | ||||
|     steering_angles = np.radians(10 * np.sin(2 * np.pi * times / 100.)) + angle_offset | ||||
|     speeds = 10 * np.sin(2 * np.pi * times / 1000.) + 25 | ||||
| 
 | ||||
|     for i, t in enumerate(times): | ||||
|       u = speeds[i] | ||||
|       sa = steering_angles[i] | ||||
|       psi = VM_sim.yaw_rate(sa - angle_offset, u) | ||||
|       liblocationd.params_learner_update(self.params_learner, psi, u, sa) | ||||
| 
 | ||||
|     # Verify learned parameters | ||||
|     sr = liblocationd.params_learner_get_sR(self.params_learner) | ||||
|     ao_slow = np.degrees(liblocationd.params_learner_get_slow_ao(self.params_learner)) | ||||
|     x = liblocationd.params_learner_get_x(self.params_learner) | ||||
|     self.assertAlmostEqual(x_target, x, places=1) | ||||
|     self.assertAlmostEqual(ao_target, ao_slow, places=1) | ||||
|     self.assertAlmostEqual(sr_target, sr, places=1) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|   unittest.main() | ||||
| @ -0,0 +1,996 @@ | ||||
| #!/usr/bin/env python3 | ||||
| ''' | ||||
| UBlox binary protocol handling | ||||
| 
 | ||||
| Copyright Andrew Tridgell, October 2012 | ||||
| Released under GNU GPL version 3 or later | ||||
| 
 | ||||
| WARNING: This code has originally intended for | ||||
| ublox version 7, it has been adapted to work | ||||
| for ublox version 8, not all functions may work. | ||||
| ''' | ||||
| 
 | ||||
| 
 | ||||
| import struct | ||||
| import time, os | ||||
| 
 | ||||
| # protocol constants | ||||
| PREAMBLE1 = 0xb5 | ||||
| PREAMBLE2 = 0x62 | ||||
| 
 | ||||
| # message classes | ||||
| CLASS_NAV = 0x01 | ||||
| CLASS_RXM = 0x02 | ||||
| CLASS_INF = 0x04 | ||||
| CLASS_ACK = 0x05 | ||||
| CLASS_CFG = 0x06 | ||||
| CLASS_MON = 0x0A | ||||
| CLASS_AID = 0x0B | ||||
| CLASS_TIM = 0x0D | ||||
| CLASS_ESF = 0x10 | ||||
| 
 | ||||
| # ACK messages | ||||
| MSG_ACK_NACK = 0x00 | ||||
| MSG_ACK_ACK = 0x01 | ||||
| 
 | ||||
| # NAV messages | ||||
| MSG_NAV_POSECEF = 0x1 | ||||
| MSG_NAV_POSLLH = 0x2 | ||||
| MSG_NAV_STATUS = 0x3 | ||||
| MSG_NAV_DOP = 0x4 | ||||
| MSG_NAV_SOL = 0x6 | ||||
| MSG_NAV_PVT = 0x7 | ||||
| MSG_NAV_POSUTM = 0x8 | ||||
| MSG_NAV_VELNED = 0x12 | ||||
| MSG_NAV_VELECEF = 0x11 | ||||
| MSG_NAV_TIMEGPS = 0x20 | ||||
| MSG_NAV_TIMEUTC = 0x21 | ||||
| MSG_NAV_CLOCK = 0x22 | ||||
| MSG_NAV_SVINFO = 0x30 | ||||
| MSG_NAV_AOPSTATUS = 0x60 | ||||
| MSG_NAV_DGPS = 0x31 | ||||
| MSG_NAV_DOP = 0x04 | ||||
| MSG_NAV_EKFSTATUS = 0x40 | ||||
| MSG_NAV_SBAS = 0x32 | ||||
| MSG_NAV_SOL = 0x06 | ||||
| 
 | ||||
| # RXM messages | ||||
| MSG_RXM_RAW = 0x15 | ||||
| MSG_RXM_SFRB = 0x11 | ||||
| MSG_RXM_SFRBX = 0x13 | ||||
| MSG_RXM_SVSI = 0x20 | ||||
| MSG_RXM_EPH = 0x31 | ||||
| MSG_RXM_ALM = 0x30 | ||||
| MSG_RXM_PMREQ = 0x41 | ||||
| 
 | ||||
| # AID messages | ||||
| MSG_AID_ALM = 0x30 | ||||
| MSG_AID_EPH = 0x31 | ||||
| MSG_AID_ALPSRV = 0x32 | ||||
| MSG_AID_AOP = 0x33 | ||||
| MSG_AID_DATA = 0x10 | ||||
| MSG_AID_ALP = 0x50 | ||||
| MSG_AID_DATA = 0x10 | ||||
| MSG_AID_HUI = 0x02 | ||||
| MSG_AID_INI = 0x01 | ||||
| MSG_AID_REQ = 0x00 | ||||
| 
 | ||||
| # CFG messages | ||||
| MSG_CFG_PRT = 0x00 | ||||
| MSG_CFG_ANT = 0x13 | ||||
| MSG_CFG_DAT = 0x06 | ||||
| MSG_CFG_EKF = 0x12 | ||||
| MSG_CFG_ESFGWT = 0x29 | ||||
| MSG_CFG_CFG = 0x09 | ||||
| MSG_CFG_USB = 0x1b | ||||
| MSG_CFG_RATE = 0x08 | ||||
| MSG_CFG_SET_RATE = 0x01 | ||||
| MSG_CFG_NAV5 = 0x24 | ||||
| MSG_CFG_FXN = 0x0E | ||||
| MSG_CFG_INF = 0x02 | ||||
| MSG_CFG_ITFM = 0x39 | ||||
| MSG_CFG_MSG = 0x01 | ||||
| MSG_CFG_NAVX5 = 0x23 | ||||
| MSG_CFG_NMEA = 0x17 | ||||
| MSG_CFG_NVS = 0x22 | ||||
| MSG_CFG_PM2 = 0x3B | ||||
| MSG_CFG_PM = 0x32 | ||||
| MSG_CFG_RINV = 0x34 | ||||
| MSG_CFG_RST = 0x04 | ||||
| MSG_CFG_RXM = 0x11 | ||||
| MSG_CFG_SBAS = 0x16 | ||||
| MSG_CFG_TMODE2 = 0x3D | ||||
| MSG_CFG_TMODE = 0x1D | ||||
| MSG_CFG_TPS = 0x31 | ||||
| MSG_CFG_TP = 0x07 | ||||
| MSG_CFG_GNSS = 0x3E | ||||
| MSG_CFG_ODO = 0x1E | ||||
| 
 | ||||
| # ESF messages | ||||
| MSG_ESF_MEAS = 0x02 | ||||
| MSG_ESF_STATUS = 0x10 | ||||
| 
 | ||||
| # INF messages | ||||
| MSG_INF_DEBUG = 0x04 | ||||
| MSG_INF_ERROR = 0x00 | ||||
| MSG_INF_NOTICE = 0x02 | ||||
| MSG_INF_TEST = 0x03 | ||||
| MSG_INF_WARNING = 0x01 | ||||
| 
 | ||||
| # MON messages | ||||
| MSG_MON_SCHD = 0x01 | ||||
| MSG_MON_HW = 0x09 | ||||
| MSG_MON_HW2 = 0x0B | ||||
| MSG_MON_IO = 0x02 | ||||
| MSG_MON_MSGPP = 0x06 | ||||
| MSG_MON_RXBUF = 0x07 | ||||
| MSG_MON_RXR = 0x21 | ||||
| MSG_MON_TXBUF = 0x08 | ||||
| MSG_MON_VER = 0x04 | ||||
| 
 | ||||
| # TIM messages | ||||
| MSG_TIM_TP = 0x01 | ||||
| MSG_TIM_TM2 = 0x03 | ||||
| MSG_TIM_SVIN = 0x04 | ||||
| MSG_TIM_VRFY = 0x06 | ||||
| 
 | ||||
| # port IDs | ||||
| PORT_DDC = 0 | ||||
| PORT_SERIAL1 = 1 | ||||
| PORT_SERIAL2 = 2 | ||||
| PORT_USB = 3 | ||||
| PORT_SPI = 4 | ||||
| 
 | ||||
| # dynamic models | ||||
| DYNAMIC_MODEL_PORTABLE = 0 | ||||
| DYNAMIC_MODEL_STATIONARY = 2 | ||||
| DYNAMIC_MODEL_PEDESTRIAN = 3 | ||||
| DYNAMIC_MODEL_AUTOMOTIVE = 4 | ||||
| DYNAMIC_MODEL_SEA = 5 | ||||
| DYNAMIC_MODEL_AIRBORNE1G = 6 | ||||
| DYNAMIC_MODEL_AIRBORNE2G = 7 | ||||
| DYNAMIC_MODEL_AIRBORNE4G = 8 | ||||
| 
 | ||||
| #reset items | ||||
| RESET_HOT = 0 | ||||
| RESET_WARM = 1 | ||||
| RESET_COLD = 0xFFFF | ||||
| 
 | ||||
| RESET_HW = 0 | ||||
| RESET_SW = 1 | ||||
| RESET_SW_GPS = 2 | ||||
| RESET_HW_GRACEFUL = 4 | ||||
| RESET_GPS_STOP = 8 | ||||
| RESET_GPS_START = 9 | ||||
| 
 | ||||
| 
 | ||||
| class UBloxError(Exception): | ||||
|   '''Ublox error class''' | ||||
| 
 | ||||
|   def __init__(self, msg): | ||||
|     Exception.__init__(self, msg) | ||||
|     self.message = msg | ||||
| 
 | ||||
| 
 | ||||
| class UBloxAttrDict(dict): | ||||
|   '''allow dictionary members as attributes''' | ||||
| 
 | ||||
|   def __init__(self): | ||||
|     dict.__init__(self) | ||||
| 
 | ||||
|   def __getattr__(self, name): | ||||
|     try: | ||||
|       return self.__getitem__(name) | ||||
|     except KeyError: | ||||
|       raise AttributeError(name) | ||||
| 
 | ||||
|   def __setattr__(self, name, value): | ||||
|     if name in self.__dict__: | ||||
|       # allow set on normal attributes | ||||
|       dict.__setattr__(self, name, value) | ||||
|     else: | ||||
|       self.__setitem__(name, value) | ||||
| 
 | ||||
| 
 | ||||
| def ArrayParse(field): | ||||
|   '''parse an array descriptor''' | ||||
|   arridx = field.find('[') | ||||
|   if arridx == -1: | ||||
|     return (field, -1) | ||||
|   alen = int(field[arridx + 1:-1]) | ||||
|   fieldname = field[:arridx] | ||||
|   return (fieldname, alen) | ||||
| 
 | ||||
| 
 | ||||
| class UBloxDescriptor: | ||||
|   '''class used to describe the layout of a UBlox message''' | ||||
| 
 | ||||
|   def __init__(self, | ||||
|                name, | ||||
|                msg_format, | ||||
|                fields=None, | ||||
|                count_field=None, | ||||
|                format2=None, | ||||
|                fields2=None): | ||||
|     if fields is None: | ||||
|       fields = [] | ||||
| 
 | ||||
|     self.name = name | ||||
|     self.msg_format = msg_format | ||||
|     self.fields = fields | ||||
|     self.count_field = count_field | ||||
|     self.format2 = format2 | ||||
|     self.fields2 = fields2 | ||||
| 
 | ||||
|   def unpack(self, msg): | ||||
|     '''unpack a UBloxMessage, creating the .fields and ._recs attributes in msg''' | ||||
|     msg._fields = {} | ||||
| 
 | ||||
|     # unpack main message blocks. A comm | ||||
|     formats = self.msg_format.split(',') | ||||
|     buf = msg._buf[6:-2] | ||||
|     count = 0 | ||||
|     msg._recs = [] | ||||
|     fields = self.fields[:] | ||||
| 
 | ||||
|     for fmt in formats: | ||||
|       size1 = struct.calcsize(fmt) | ||||
|       if size1 > len(buf): | ||||
|         raise UBloxError("%s INVALID_SIZE1=%u" % (self.name, len(buf))) | ||||
|       f1 = list(struct.unpack(fmt, buf[:size1])) | ||||
|       i = 0 | ||||
|       while i < len(f1): | ||||
|         field = fields.pop(0) | ||||
|         (fieldname, alen) = ArrayParse(field) | ||||
|         if alen == -1: | ||||
|           msg._fields[fieldname] = f1[i] | ||||
|           if self.count_field == fieldname: | ||||
|             count = int(f1[i]) | ||||
|           i += 1 | ||||
|         else: | ||||
|           msg._fields[fieldname] = [0] * alen | ||||
|           for a in range(alen): | ||||
|             msg._fields[fieldname][a] = f1[i] | ||||
|             i += 1 | ||||
|       buf = buf[size1:] | ||||
|       if len(buf) == 0: | ||||
|         break | ||||
| 
 | ||||
|     if self.count_field == '_remaining': | ||||
|       count = len(buf) // struct.calcsize(self.format2) | ||||
| 
 | ||||
|     if count == 0: | ||||
|       msg._unpacked = True | ||||
|       if len(buf) != 0: | ||||
|         raise UBloxError("EXTRA_BYTES=%u" % len(buf)) | ||||
|       return | ||||
| 
 | ||||
|     size2 = struct.calcsize(self.format2) | ||||
|     for c in range(count): | ||||
|       r = UBloxAttrDict() | ||||
|       if size2 > len(buf): | ||||
|         raise UBloxError("INVALID_SIZE=%u, " % len(buf)) | ||||
|       f2 = list(struct.unpack(self.format2, buf[:size2])) | ||||
|       for i in range(len(self.fields2)): | ||||
|         r[self.fields2[i]] = f2[i] | ||||
|       buf = buf[size2:] | ||||
|       msg._recs.append(r) | ||||
|     if len(buf) != 0: | ||||
|       raise UBloxError("EXTRA_BYTES=%u" % len(buf)) | ||||
|     msg._unpacked = True | ||||
| 
 | ||||
|   def pack(self, msg, msg_class=None, msg_id=None): | ||||
|     '''pack a UBloxMessage from the .fields and ._recs attributes in msg''' | ||||
|     f1 = [] | ||||
|     if msg_class is None: | ||||
|       msg_class = msg.msg_class() | ||||
|     if msg_id is None: | ||||
|       msg_id = msg.msg_id() | ||||
|     msg._buf = '' | ||||
| 
 | ||||
|     fields = self.fields[:] | ||||
|     for f in fields: | ||||
|       (fieldname, alen) = ArrayParse(f) | ||||
|       if not fieldname in msg._fields: | ||||
|         break | ||||
|       if alen == -1: | ||||
|         f1.append(msg._fields[fieldname]) | ||||
|       else: | ||||
|         for a in range(alen): | ||||
|           f1.append(msg._fields[fieldname][a]) | ||||
|     try: | ||||
|       # try full length message | ||||
|       fmt = self.msg_format.replace(',', '') | ||||
|       msg._buf = struct.pack(fmt, *tuple(f1)) | ||||
|     except Exception: | ||||
|       # try without optional part | ||||
|       fmt = self.msg_format.split(',')[0] | ||||
|       msg._buf = struct.pack(fmt, *tuple(f1)) | ||||
| 
 | ||||
|     length = len(msg._buf) | ||||
|     if msg._recs: | ||||
|       length += len(msg._recs) * struct.calcsize(self.format2) | ||||
|     header = struct.pack('<BBBBH', PREAMBLE1, PREAMBLE2, msg_class, msg_id, length) | ||||
|     msg._buf = header + msg._buf | ||||
| 
 | ||||
|     for r in msg._recs: | ||||
|       f2 = [] | ||||
|       for f in self.fields2: | ||||
|         f2.append(r[f]) | ||||
|       msg._buf += struct.pack(self.format2, *tuple(f2)) | ||||
|     msg._buf += struct.pack('<BB', *msg.checksum(data=msg._buf[2:])) | ||||
| 
 | ||||
|   def format(self, msg): | ||||
|     '''return a formatted string for a message''' | ||||
|     if not msg._unpacked: | ||||
|       self.unpack(msg) | ||||
|     ret = self.name + ': ' | ||||
|     for f in self.fields: | ||||
|       (fieldname, alen) = ArrayParse(f) | ||||
|       if not fieldname in msg._fields: | ||||
|         continue | ||||
|       v = msg._fields[fieldname] | ||||
|       if isinstance(v, list): | ||||
|         ret += '%s=[' % fieldname | ||||
|         for a in range(alen): | ||||
|           ret += '%s, ' % v[a] | ||||
|         ret = ret[:-2] + '], ' | ||||
|       elif isinstance(v, str): | ||||
|         ret += '%s="%s", ' % (f, v.rstrip(' \0')) | ||||
|       else: | ||||
|         ret += '%s=%s, ' % (f, v) | ||||
|     for r in msg._recs: | ||||
|       ret += '[ ' | ||||
|       for f in self.fields2: | ||||
|         v = r[f] | ||||
|         ret += '%s=%s, ' % (f, v) | ||||
|       ret = ret[:-2] + ' ], ' | ||||
|     return ret[:-2] | ||||
| 
 | ||||
| 
 | ||||
| # list of supported message types. | ||||
| msg_types = { | ||||
|   (CLASS_ACK, MSG_ACK_ACK): | ||||
|   UBloxDescriptor('ACK_ACK', '<BB', ['clsID', 'msgID']), | ||||
|   (CLASS_ACK, MSG_ACK_NACK): | ||||
|   UBloxDescriptor('ACK_NACK', '<BB', ['clsID', 'msgID']), | ||||
|   (CLASS_CFG, MSG_CFG_USB): | ||||
|   UBloxDescriptor('CFG_USB', '<HHHHHH32s32s32s', [ | ||||
|     'vendorID', 'productID', 'reserved1', 'reserved2', 'powerConsumption', 'flags', | ||||
|     'vendorString', 'productString', 'serialNumber' | ||||
|   ]), | ||||
|   (CLASS_CFG, MSG_CFG_PRT): | ||||
|   UBloxDescriptor('CFG_PRT', '<BBHIIHHHH', [ | ||||
|     'portID', 'reserved0', 'txReady', 'mode', 'baudRate', 'inProtoMask', 'outProtoMask', | ||||
|     'reserved4', 'reserved5' | ||||
|   ]), | ||||
|   (CLASS_CFG, MSG_CFG_CFG): | ||||
|   UBloxDescriptor('CFG_CFG', '<III,B', | ||||
|                   ['clearMask', 'saveMask', 'loadMask', 'deviceMask']), | ||||
|   (CLASS_CFG, MSG_CFG_RXM): | ||||
|   UBloxDescriptor('CFG_RXM', '<BB', | ||||
|                   ['reserved1', 'lpMode']), | ||||
|   (CLASS_CFG, MSG_CFG_RST): | ||||
|   UBloxDescriptor('CFG_RST', '<HBB', ['navBbrMask ', 'resetMode', 'reserved1']), | ||||
|   (CLASS_CFG, MSG_CFG_SBAS): | ||||
|   UBloxDescriptor('CFG_SBAS', '<BBBBI', | ||||
|                   ['mode', 'usage', 'maxSBAS', 'scanmode2', 'scanmode1']), | ||||
|   (CLASS_CFG, MSG_CFG_GNSS): | ||||
|   UBloxDescriptor('CFG_GNSS', '<BBBB', | ||||
|                   ['msgVer', 'numTrkChHw', 'numTrkChUse', | ||||
|                    'numConfigBlocks'], 'numConfigBlocks', '<BBBBI', | ||||
|                   ['gnssId', 'resTrkCh', 'maxTrkCh', 'reserved1', 'flags']), | ||||
|   (CLASS_CFG, MSG_CFG_RATE): | ||||
|   UBloxDescriptor('CFG_RATE', '<HHH', ['measRate', 'navRate', 'timeRef']), | ||||
|   (CLASS_CFG, MSG_CFG_MSG): | ||||
|   UBloxDescriptor('CFG_MSG', '<BB6B', ['msgClass', 'msgId', 'rates[6]']), | ||||
|   (CLASS_NAV, MSG_NAV_POSLLH): | ||||
|   UBloxDescriptor('NAV_POSLLH', '<IiiiiII', | ||||
|                   ['iTOW', 'Longitude', 'Latitude', 'height', 'hMSL', 'hAcc', 'vAcc']), | ||||
|   (CLASS_NAV, MSG_NAV_VELNED): | ||||
|   UBloxDescriptor('NAV_VELNED', '<IiiiIIiII', [ | ||||
|     'iTOW', 'velN', 'velE', 'velD', 'speed', 'gSpeed', 'heading', 'sAcc', 'cAcc' | ||||
|   ]), | ||||
|   (CLASS_NAV, MSG_NAV_DOP): | ||||
|   UBloxDescriptor('NAV_DOP', '<IHHHHHHH', | ||||
|                   ['iTOW', 'gDOP', 'pDOP', 'tDOP', 'vDOP', 'hDOP', 'nDOP', 'eDOP']), | ||||
|   (CLASS_NAV, MSG_NAV_STATUS): | ||||
|   UBloxDescriptor('NAV_STATUS', '<IBBBBII', | ||||
|                   ['iTOW', 'gpsFix', 'flags', 'fixStat', 'flags2', 'ttff', 'msss']), | ||||
|   (CLASS_NAV, MSG_NAV_SOL): | ||||
|   UBloxDescriptor('NAV_SOL', '<IihBBiiiIiiiIHBBI', [ | ||||
|     'iTOW', 'fTOW', 'week', 'gpsFix', 'flags', 'ecefX', 'ecefY', 'ecefZ', 'pAcc', | ||||
|     'ecefVX', 'ecefVY', 'ecefVZ', 'sAcc', 'pDOP', 'reserved1', 'numSV', 'reserved2' | ||||
|   ]), | ||||
|   (CLASS_NAV, MSG_NAV_PVT): | ||||
|   UBloxDescriptor('NAV_PVT', '<IHBBBBBBIiBBBBiiiiIIiiiiiIIH6BihH', [ | ||||
|     'iTOW', 'year', 'month', 'day', 'hour', 'min', 'sec', 'valid', 'tAcc', 'nano', | ||||
|     'fixType', 'flags', 'flags2', 'numSV', 'lon', 'lat', 'height', 'hMSL', 'hAcc', 'vAcc', | ||||
|     'velN', 'velE', 'velD', 'gSpeed', 'headMot', 'sAcc', 'headAcc', 'pDOP', | ||||
|     'reserverd1[6]', 'headVeh', 'magDec', 'magAcc' | ||||
|   ]), | ||||
|   (CLASS_NAV, MSG_NAV_POSUTM): | ||||
|   UBloxDescriptor('NAV_POSUTM', '<Iiiibb', | ||||
|                   ['iTOW', 'East', 'North', 'Alt', 'Zone', 'Hem']), | ||||
|   (CLASS_NAV, MSG_NAV_SBAS): | ||||
|   UBloxDescriptor('NAV_SBAS', '<IBBbBBBBB', [ | ||||
|     'iTOW', 'geo', 'mode', 'sys', 'service', 'cnt', 'reserved01', 'reserved02', | ||||
|     'reserved03' | ||||
|   ], 'cnt', 'BBBBBBhHh', [ | ||||
|     'svid', 'flags', 'udre', 'svSys', 'svService', 'reserved1', 'prc', 'reserved2', 'ic' | ||||
|   ]), | ||||
|   (CLASS_NAV, MSG_NAV_POSECEF): | ||||
|   UBloxDescriptor('NAV_POSECEF', '<IiiiI', ['iTOW', 'ecefX', 'ecefY', 'ecefZ', 'pAcc']), | ||||
|   (CLASS_NAV, MSG_NAV_VELECEF): | ||||
|   UBloxDescriptor('NAV_VELECEF', '<IiiiI', ['iTOW', 'ecefVX', 'ecefVY', 'ecefVZ', | ||||
|                                             'sAcc']), | ||||
|   (CLASS_NAV, MSG_NAV_TIMEGPS): | ||||
|   UBloxDescriptor('NAV_TIMEGPS', '<IihbBI', | ||||
|                   ['iTOW', 'fTOW', 'week', 'leapS', 'valid', 'tAcc']), | ||||
|   (CLASS_NAV, MSG_NAV_TIMEUTC): | ||||
|   UBloxDescriptor('NAV_TIMEUTC', '<IIiHBBBBBB', [ | ||||
|     'iTOW', 'tAcc', 'nano', 'year', 'month', 'day', 'hour', 'min', 'sec', 'valid' | ||||
|   ]), | ||||
|   (CLASS_NAV, MSG_NAV_CLOCK): | ||||
|   UBloxDescriptor('NAV_CLOCK', '<IiiII', ['iTOW', 'clkB', 'clkD', 'tAcc', 'fAcc']), | ||||
|   (CLASS_NAV, MSG_NAV_DGPS): | ||||
|   UBloxDescriptor('NAV_DGPS', '<IihhBBH', | ||||
|                   ['iTOW', 'age', 'baseId', 'baseHealth', 'numCh', 'status', 'reserved1'], | ||||
|                   'numCh', '<BBHff', ['svid', 'flags', 'ageC', 'prc', 'prrc']), | ||||
|   (CLASS_NAV, MSG_NAV_SVINFO): | ||||
|   UBloxDescriptor('NAV_SVINFO', '<IBBH', ['iTOW', 'numCh', 'globalFlags', | ||||
|                                           'reserved2'], 'numCh', '<BBBBBbhi', | ||||
|                   ['chn', 'svid', 'flags', 'quality', 'cno', 'elev', 'azim', 'prRes']), | ||||
|   (CLASS_RXM, MSG_RXM_SVSI): | ||||
|   UBloxDescriptor('RXM_SVSI', '<IhBB', ['iTOW', 'week', 'numVis', 'numSV'], 'numSV', | ||||
|                   '<BBhbB', ['svid', 'svFlag', 'azim', 'elev', 'age']), | ||||
|   (CLASS_RXM, MSG_RXM_EPH): | ||||
|   UBloxDescriptor('RXM_EPH', '<II , 8I 8I 8I', | ||||
|                   ['svid', 'how', 'sf1d[8]', 'sf2d[8]', 'sf3d[8]']), | ||||
|   (CLASS_AID, MSG_AID_EPH): | ||||
|   UBloxDescriptor('AID_EPH', '<II , 8I 8I 8I', | ||||
|                   ['svid', 'how', 'sf1d[8]', 'sf2d[8]', 'sf3d[8]']), | ||||
|   (CLASS_AID, MSG_AID_HUI): | ||||
|   UBloxDescriptor('AID_HUI', '<Iddi 6h 8f I', | ||||
|                   ['health', 'utcA0', 'utcA1', 'utcTOW', 'utcWNT', 'utcLS', 'utcWNF', | ||||
|                    'utcDN', 'utcLSF', 'utcSpare', 'klobA0', 'klobA1', 'klobA2', 'klobA3', | ||||
|                    'klobB0', 'klobB1', 'klobB2', 'klobB3', 'flags']), | ||||
|   (CLASS_AID, MSG_AID_AOP): | ||||
|   UBloxDescriptor('AID_AOP', '<B47B , 48B 48B 48B', | ||||
|                   ['svid', 'data[47]', 'optional0[48]', 'optional1[48]', | ||||
|                    'optional1[48]']), | ||||
|   (CLASS_RXM, MSG_RXM_RAW): | ||||
|   UBloxDescriptor('RXM_RAW', '<dHbBB3B', [ | ||||
|     'rcvTow', 'week', 'leapS', 'numMeas', 'recStat', 'reserved1[3]' | ||||
|   ], 'numMeas', '<ddfBBBBHBBBBBB', [ | ||||
|     'prMes', 'cpMes', 'doMes', 'gnssId', 'svId', 'sigId', 'freqId', 'locktime', 'cno', | ||||
|     'prStdev', 'cpStdev', 'doStdev', 'trkStat', 'reserved3' | ||||
|   ]), | ||||
|   (CLASS_RXM, MSG_RXM_SFRB): | ||||
|   UBloxDescriptor('RXM_SFRB', '<BB10I', ['chn', 'svid', 'dwrd[10]']), | ||||
|   (CLASS_RXM, MSG_RXM_SFRBX): | ||||
|   UBloxDescriptor('RXM_SFRBX', '<8B', ['gnssId', 'svid', 'reserved1', 'freqId', 'numWords', | ||||
|       'reserved2', 'version', 'reserved3'], 'numWords', 'I', ['dwrd']), | ||||
|   (CLASS_AID, MSG_AID_ALM): | ||||
|   UBloxDescriptor('AID_ALM', '<II', '_remaining', 'I', ['dwrd']), | ||||
|   (CLASS_RXM, MSG_RXM_ALM): | ||||
|   UBloxDescriptor('RXM_ALM', '<II , 8I', ['svid', 'week', 'dwrd[8]']), | ||||
|   (CLASS_CFG, MSG_CFG_ODO): | ||||
|   UBloxDescriptor('CFG_ODO', '<B3BBB6BBB2BBB2B', [ | ||||
|     'version', 'reserved1[3]', 'flags', 'odoCfg', 'reserverd2[6]', 'cogMaxSpeed', | ||||
|     'cogMaxPosAcc', 'reserved3[2]', 'velLpGain', 'cogLpGain', 'reserved[2]' | ||||
|   ]), | ||||
|   (CLASS_CFG, MSG_CFG_NAV5): | ||||
|   UBloxDescriptor('CFG_NAV5', '<HBBiIbBHHHHBBIII', [ | ||||
|     'mask', 'dynModel', 'fixMode', 'fixedAlt', 'fixedAltVar', 'minElev', 'drLimit', | ||||
|     'pDop', 'tDop', 'pAcc', 'tAcc', 'staticHoldThresh', 'dgpsTimeOut', 'reserved2', | ||||
|     'reserved3', 'reserved4' | ||||
|   ]), | ||||
|   (CLASS_CFG, MSG_CFG_NAVX5): | ||||
|   UBloxDescriptor('CFG_NAVX5', '<HHIBBBBBBBBBBHIBBBBBBHII', [ | ||||
|     'version', 'mask1', 'reserved0', 'reserved1', 'reserved2', 'minSVs', 'maxSVs', | ||||
|     'minCNO', 'reserved5', 'iniFix3D', 'reserved6', 'reserved7', 'reserved8', | ||||
|     'wknRollover', 'reserved9', 'reserved10', 'reserved11', 'usePPP', 'useAOP', | ||||
|     'reserved12', 'reserved13', 'aopOrbMaxErr', 'reserved3', 'reserved4' | ||||
|   ]), | ||||
|   (CLASS_MON, MSG_MON_HW): | ||||
|   UBloxDescriptor('MON_HW', '<IIIIHHBBBBIB25BHIII', [ | ||||
|     'pinSel', 'pinBank', 'pinDir', 'pinVal', 'noisePerMS', 'agcCnt', 'aStatus', 'aPower', | ||||
|     'flags', 'reserved1', 'usedMask', 'VP[25]', 'jamInd', 'reserved3', 'pinInq', 'pullH', | ||||
|     'pullL' | ||||
|   ]), | ||||
|   (CLASS_MON, MSG_MON_HW2): | ||||
|   UBloxDescriptor('MON_HW2', '<bBbBB3BI8BI4B', [ | ||||
|     'ofsI', 'magI', 'ofsQ', 'magQ', 'cfgSource', 'reserved1[3]', 'lowLevCfg', | ||||
|     'reserved2[8]', 'postStatus', 'reserved3[4]' | ||||
|   ]), | ||||
|   (CLASS_MON, MSG_MON_SCHD): | ||||
|   UBloxDescriptor('MON_SCHD', '<IIIIHHHBB', [ | ||||
|     'tskRun', 'tskSchd', 'tskOvrr', 'tskReg', 'stack', 'stackSize', 'CPUIdle', 'flySly', | ||||
|     'ptlSly' | ||||
|   ]), | ||||
|   (CLASS_MON, MSG_MON_VER): | ||||
|   UBloxDescriptor('MON_VER', '<30s10s,30s', ['swVersion', 'hwVersion', 'romVersion'], | ||||
|                   '_remaining', '30s', ['extension']), | ||||
|   (CLASS_TIM, MSG_TIM_TP): | ||||
|   UBloxDescriptor('TIM_TP', '<IIiHBB', | ||||
|                   ['towMS', 'towSubMS', 'qErr', 'week', 'flags', 'reserved1']), | ||||
|   (CLASS_TIM, MSG_TIM_TM2): | ||||
|   UBloxDescriptor('TIM_TM2', '<BBHHHIIIII', [ | ||||
|     'ch', 'flags', 'count', 'wnR', 'wnF', 'towMsR', 'towSubMsR', 'towMsF', 'towSubMsF', | ||||
|     'accEst' | ||||
|   ]), | ||||
|   (CLASS_TIM, MSG_TIM_SVIN): | ||||
|   UBloxDescriptor('TIM_SVIN', '<IiiiIIBBH', [ | ||||
|     'dur', 'meanX', 'meanY', 'meanZ', 'meanV', 'obs', 'valid', 'active', 'reserved1' | ||||
|   ]), | ||||
|   (CLASS_INF, MSG_INF_ERROR): | ||||
|   UBloxDescriptor('INF_ERR', '<18s', ['str']), | ||||
|   (CLASS_INF, MSG_INF_DEBUG): | ||||
|   UBloxDescriptor('INF_DEBUG', '<18s', ['str']) | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| class UBloxMessage: | ||||
|   '''UBlox message class - holds a UBX binary message''' | ||||
| 
 | ||||
|   def __init__(self): | ||||
|     self._buf = b"" | ||||
|     self._fields = {} | ||||
|     self._recs = [] | ||||
|     self._unpacked = False | ||||
|     self.debug_level = 1 | ||||
| 
 | ||||
|   def __str__(self): | ||||
|     '''format a message as a string''' | ||||
|     if not self.valid(): | ||||
|       return 'UBloxMessage(INVALID)' | ||||
|     type = self.msg_type() | ||||
|     if type in msg_types: | ||||
|       return msg_types[type].format(self) | ||||
|     return 'UBloxMessage(UNKNOWN %s, %u)' % (str(type), self.msg_length()) | ||||
| 
 | ||||
|   def as_dict(self): | ||||
|     '''format a message as a string''' | ||||
|     if not self.valid(): | ||||
|       return 'UBloxMessage(INVALID)' | ||||
|     type = self.msg_type() | ||||
|     if type in msg_types: | ||||
|       return msg_types[type].format(self) | ||||
|     return 'UBloxMessage(UNKNOWN %s, %u)' % (str(type), self.msg_length()) | ||||
| 
 | ||||
|   def __getattr__(self, name): | ||||
|     '''allow access to message fields''' | ||||
|     try: | ||||
|       return self._fields[name] | ||||
|     except KeyError: | ||||
|       if name == 'recs': | ||||
|         return self._recs | ||||
|       raise AttributeError(name) | ||||
| 
 | ||||
|   def __setattr__(self, name, value): | ||||
|     '''allow access to message fields''' | ||||
|     if name.startswith('_'): | ||||
|       self.__dict__[name] = value | ||||
|     else: | ||||
|       self._fields[name] = value | ||||
| 
 | ||||
|   def have_field(self, name): | ||||
|     '''return True if a message contains the given field''' | ||||
|     return name in self._fields | ||||
| 
 | ||||
|   def debug(self, level, msg): | ||||
|     '''write a debug message''' | ||||
|     if self.debug_level >= level: | ||||
|       print(msg) | ||||
| 
 | ||||
|   def unpack(self): | ||||
|     '''unpack a message''' | ||||
|     if not self.valid(): | ||||
|       raise UBloxError('INVALID MESSAGE') | ||||
|     type = self.msg_type() | ||||
|     if not type in msg_types: | ||||
|       raise UBloxError('Unknown message %s length=%u' % (str(type), len(self._buf))) | ||||
|     msg_types[type].unpack(self) | ||||
|     return self._fields, self._recs | ||||
| 
 | ||||
|   def pack(self): | ||||
|     '''pack a message''' | ||||
|     if not self.valid(): | ||||
|       raise UBloxError('INVALID MESSAGE') | ||||
|     type = self.msg_type() | ||||
|     if not type in msg_types: | ||||
|       raise UBloxError('Unknown message %s' % str(type)) | ||||
|     msg_types[type].pack(self) | ||||
| 
 | ||||
|   def name(self): | ||||
|     '''return the short string name for a message''' | ||||
|     if not self.valid(): | ||||
|       raise UBloxError('INVALID MESSAGE') | ||||
|     type = self.msg_type() | ||||
|     if not type in msg_types: | ||||
|       raise UBloxError('Unknown message %s length=%u' % (str(type), len(self._buf))) | ||||
|     return msg_types[type].name | ||||
| 
 | ||||
|   def msg_class(self): | ||||
|     '''return the message class''' | ||||
|     return self._buf[2] | ||||
| 
 | ||||
|   def msg_id(self): | ||||
|     '''return the message id within the class''' | ||||
|     return self._buf[3] | ||||
| 
 | ||||
|   def msg_type(self): | ||||
|     '''return the message type tuple (class, id)''' | ||||
|     return (self.msg_class(), self.msg_id()) | ||||
| 
 | ||||
|   def msg_length(self): | ||||
|     '''return the payload length''' | ||||
|     (payload_length, ) = struct.unpack('<H', self._buf[4:6]) | ||||
|     return payload_length | ||||
| 
 | ||||
|   def valid_so_far(self): | ||||
|     '''check if the message is valid so far''' | ||||
|     if len(self._buf) > 0 and self._buf[0] != PREAMBLE1: | ||||
|       return False | ||||
|     if len(self._buf) > 1 and self._buf[1] != PREAMBLE2: | ||||
|       self.debug(1, "bad pre2") | ||||
|       return False | ||||
|     if self.needed_bytes() == 0 and not self.valid(): | ||||
|       if len(self._buf) > 8: | ||||
|         self.debug(1, "bad checksum len=%u needed=%u" % (len(self._buf), | ||||
|                                                          self.needed_bytes())) | ||||
|       else: | ||||
|         self.debug(1, "bad len len=%u needed=%u" % (len(self._buf), self.needed_bytes())) | ||||
|       return False | ||||
|     return True | ||||
| 
 | ||||
|   def add(self, bytes): | ||||
|     '''add some bytes to a message''' | ||||
|     self._buf += bytes | ||||
|     while not self.valid_so_far() and len(self._buf) > 0: | ||||
|       '''handle corrupted streams''' | ||||
|       self._buf = self._buf[1:] | ||||
|     if self.needed_bytes() < 0: | ||||
|       self._buf = "" | ||||
| 
 | ||||
|   def checksum(self, data=None): | ||||
|     '''return a checksum tuple for a message''' | ||||
|     if data is None: | ||||
|       data = self._buf[2:-2] | ||||
|     #cs = 0 | ||||
|     ck_a = 0 | ||||
|     ck_b = 0 | ||||
|     for i in data: | ||||
|       ck_a = (ck_a + i) & 0xFF | ||||
|       ck_b = (ck_b + ck_a) & 0xFF | ||||
|     return (ck_a, ck_b) | ||||
| 
 | ||||
|   def valid_checksum(self): | ||||
|     '''check if the checksum is OK''' | ||||
|     (ck_a, ck_b) = self.checksum() | ||||
|     #d = self._buf[2:-2] | ||||
|     (ck_a2, ck_b2) = struct.unpack('<BB', self._buf[-2:]) | ||||
|     return ck_a == ck_a2 and ck_b == ck_b2 | ||||
| 
 | ||||
|   def needed_bytes(self): | ||||
|     '''return number of bytes still needed''' | ||||
|     if len(self._buf) < 6: | ||||
|       return 8 - len(self._buf) | ||||
|     return self.msg_length() + 8 - len(self._buf) | ||||
| 
 | ||||
|   def valid(self): | ||||
|     '''check if a message is valid''' | ||||
|     return len(self._buf) >= 8 and self.needed_bytes() == 0 and self.valid_checksum() | ||||
| 
 | ||||
| 
 | ||||
| class UBlox: | ||||
|   '''main UBlox control class. | ||||
| 
 | ||||
|     port can be a file (for reading only) or a serial device | ||||
|     ''' | ||||
| 
 | ||||
|   def __init__(self, port, baudrate=115200, timeout=0, panda=False, grey=False): | ||||
| 
 | ||||
|     self.serial_device = port | ||||
|     self.baudrate = baudrate | ||||
|     self.use_sendrecv = False | ||||
|     self.read_only = False | ||||
|     self.debug_level = 0 | ||||
| 
 | ||||
|     if panda: | ||||
|       from panda import Panda, PandaSerial | ||||
| 
 | ||||
|       self.panda = Panda() | ||||
| 
 | ||||
|       # resetting U-Blox module | ||||
|       self.panda.set_esp_power(0) | ||||
|       time.sleep(0.1) | ||||
|       self.panda.set_esp_power(1) | ||||
|       time.sleep(0.5) | ||||
| 
 | ||||
|       # can't set above 9600 now... | ||||
|       self.baudrate = 9600 | ||||
|       self.dev = PandaSerial(self.panda, 1, self.baudrate) | ||||
| 
 | ||||
|       self.baudrate = 460800 | ||||
|       print("upping baud:",self.baudrate) | ||||
|       self.send_nmea("$PUBX,41,1,0007,0003,%u,0" % self.baudrate) | ||||
|       time.sleep(0.1) | ||||
| 
 | ||||
|       self.dev = PandaSerial(self.panda, 1, self.baudrate) | ||||
|     elif grey: | ||||
|       import cereal.messaging as messaging | ||||
| 
 | ||||
|       class BoarddSerial(): | ||||
|         def __init__(self): | ||||
|           self.ubloxRaw = messaging.sub_sock('ubloxRaw') | ||||
|           self.buf = "" | ||||
| 
 | ||||
|         def read(self, n): | ||||
|           for msg in messaging.drain_sock(self.ubloxRaw, len(self.buf) < n): | ||||
|             self.buf += msg.ubloxRaw | ||||
|           ret = self.buf[:n] | ||||
|           self.buf = self.buf[n:] | ||||
|           return ret | ||||
| 
 | ||||
| 
 | ||||
|         def write(self, dat): | ||||
|           pass | ||||
| 
 | ||||
|       self.dev = BoarddSerial() | ||||
|     else: | ||||
|       if self.serial_device.startswith("tcp:"): | ||||
|         import socket | ||||
|         a = self.serial_device.split(':') | ||||
|         destination_addr = (a[1], int(a[2])) | ||||
|         self.dev = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||||
|         self.dev.connect(destination_addr) | ||||
|         self.dev.setblocking(1) | ||||
|         self.dev.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) | ||||
|         self.use_sendrecv = True | ||||
|       elif os.path.isfile(self.serial_device): | ||||
|         self.read_only = True | ||||
|         self.dev = open(self.serial_device, mode='rb') | ||||
|       else: | ||||
|         import serial | ||||
|         self.dev = serial.Serial( | ||||
|           self.serial_device, | ||||
|           baudrate=self.baudrate, | ||||
|           dsrdtr=False, | ||||
|           rtscts=False, | ||||
|           xonxoff=False, | ||||
|           timeout=timeout) | ||||
| 
 | ||||
|     self.logfile = None | ||||
|     self.log = None | ||||
|     self.preferred_dynamic_model = None | ||||
|     self.preferred_usePPP = None | ||||
|     self.preferred_dgps_timeout = None | ||||
| 
 | ||||
|   def close(self): | ||||
|     '''close the device''' | ||||
|     self.dev.close() | ||||
|     self.dev = None | ||||
| 
 | ||||
|   def set_debug(self, debug_level): | ||||
|     '''set debug level''' | ||||
|     self.debug_level = debug_level | ||||
| 
 | ||||
|   def debug(self, level, msg): | ||||
|     '''write a debug message''' | ||||
|     if self.debug_level >= level: | ||||
|       print(msg) | ||||
| 
 | ||||
|   def set_logfile(self, logfile, append=False): | ||||
|     '''setup logging to a file''' | ||||
|     if self.log is not None: | ||||
|       self.log.close() | ||||
|       self.log = None | ||||
|     self.logfile = logfile | ||||
|     if self.logfile is not None: | ||||
|       if append: | ||||
|         mode = 'ab' | ||||
|       else: | ||||
|         mode = 'wb' | ||||
|       self.log = open(self.logfile, mode=mode) | ||||
| 
 | ||||
|   def set_preferred_dynamic_model(self, model): | ||||
|     '''set the preferred dynamic model for receiver''' | ||||
|     self.preferred_dynamic_model = model | ||||
|     if model is not None: | ||||
|       self.configure_poll(CLASS_CFG, MSG_CFG_NAV5) | ||||
| 
 | ||||
|   def set_preferred_dgps_timeout(self, timeout): | ||||
|     '''set the preferred DGPS timeout for receiver''' | ||||
|     self.preferred_dgps_timeout = timeout | ||||
|     if timeout is not None: | ||||
|       self.configure_poll(CLASS_CFG, MSG_CFG_NAV5) | ||||
| 
 | ||||
|   def set_preferred_usePPP(self, usePPP): | ||||
|     '''set the preferred usePPP setting for the receiver''' | ||||
|     if usePPP is None: | ||||
|       self.preferred_usePPP = None | ||||
|       return | ||||
|     self.preferred_usePPP = int(usePPP) | ||||
|     self.configure_poll(CLASS_CFG, MSG_CFG_NAVX5) | ||||
| 
 | ||||
|   def nmea_checksum(self, msg): | ||||
|     d = msg[1:] | ||||
|     cs = 0 | ||||
|     for i in d: | ||||
|       cs ^= ord(i) | ||||
|     return cs | ||||
| 
 | ||||
|   def write(self, buf): | ||||
|     '''write some bytes''' | ||||
|     if not self.read_only: | ||||
|       if self.use_sendrecv: | ||||
|         return self.dev.send(buf) | ||||
|       return self.dev.write(buf) | ||||
| 
 | ||||
|   def read(self, n): | ||||
|     '''read some bytes''' | ||||
|     if self.use_sendrecv: | ||||
|       import socket | ||||
|       try: | ||||
|         return self.dev.recv(n) | ||||
|       except socket.error: | ||||
|         return '' | ||||
|     return self.dev.read(n) | ||||
| 
 | ||||
|   def send_nmea(self, msg): | ||||
|     if not self.read_only: | ||||
|       s = msg + "*%02X" % self.nmea_checksum(msg) + "\r\n" | ||||
|       self.write(s) | ||||
| 
 | ||||
|   def set_binary(self): | ||||
|     '''put a UBlox into binary mode using a NMEA string''' | ||||
|     if not self.read_only: | ||||
|       print("try set binary at %u" % self.baudrate) | ||||
|       self.send_nmea("$PUBX,41,0,0007,0001,%u,0" % self.baudrate) | ||||
|       self.send_nmea("$PUBX,41,1,0007,0001,%u,0" % self.baudrate) | ||||
|       self.send_nmea("$PUBX,41,2,0007,0001,%u,0" % self.baudrate) | ||||
|       self.send_nmea("$PUBX,41,3,0007,0001,%u,0" % self.baudrate) | ||||
|       self.send_nmea("$PUBX,41,4,0007,0001,%u,0" % self.baudrate) | ||||
|       self.send_nmea("$PUBX,41,5,0007,0001,%u,0" % self.baudrate) | ||||
| 
 | ||||
|   def disable_nmea(self): | ||||
|     ''' stop sending all types of nmea messages ''' | ||||
|     self.send_nmea("$PUBX,40,GSV,1,1,1,1,1,0") | ||||
|     self.send_nmea("$PUBX,40,GGA,0,0,0,0,0,0") | ||||
|     self.send_nmea("$PUBX,40,GSA,0,0,0,0,0,0") | ||||
|     self.send_nmea("$PUBX,40,VTG,0,0,0,0,0,0") | ||||
|     self.send_nmea("$PUBX,40,TXT,0,0,0,0,0,0") | ||||
|     self.send_nmea("$PUBX,40,RMC,0,0,0,0,0,0") | ||||
| 
 | ||||
|   def seek_percent(self, pct): | ||||
|     '''seek to the given percentage of a file''' | ||||
|     self.dev.seek(0, 2) | ||||
|     filesize = self.dev.tell() | ||||
|     self.dev.seek(pct * 0.01 * filesize) | ||||
| 
 | ||||
|   def special_handling(self, msg): | ||||
|     '''handle automatic configuration changes''' | ||||
|     if msg.name() == 'CFG_NAV5': | ||||
|       msg.unpack() | ||||
|       sendit = False | ||||
|       pollit = False | ||||
|       if self.preferred_dynamic_model is not None and msg.dynModel != self.preferred_dynamic_model: | ||||
|         msg.dynModel = self.preferred_dynamic_model | ||||
|         sendit = True | ||||
|         pollit = True | ||||
|       if self.preferred_dgps_timeout is not None and msg.dgpsTimeOut != self.preferred_dgps_timeout: | ||||
|         msg.dgpsTimeOut = self.preferred_dgps_timeout | ||||
|         self.debug(2, "Setting dgpsTimeOut=%u" % msg.dgpsTimeOut) | ||||
|         sendit = True | ||||
|         # we don't re-poll for this one, as some receivers refuse to set it | ||||
|       if sendit: | ||||
|         msg.pack() | ||||
|         self.send(msg) | ||||
|         if pollit: | ||||
|           self.configure_poll(CLASS_CFG, MSG_CFG_NAV5) | ||||
|     if msg.name() == 'CFG_NAVX5' and self.preferred_usePPP is not None: | ||||
|       msg.unpack() | ||||
|       if msg.usePPP != self.preferred_usePPP: | ||||
|         msg.usePPP = self.preferred_usePPP | ||||
|         msg.mask = 1 << 13 | ||||
|         msg.pack() | ||||
|         self.send(msg) | ||||
|         self.configure_poll(CLASS_CFG, MSG_CFG_NAVX5) | ||||
| 
 | ||||
|   def receive_message(self, ignore_eof=False): | ||||
|     '''blocking receive of one ublox message''' | ||||
|     msg = UBloxMessage() | ||||
|     while True: | ||||
|       n = msg.needed_bytes() | ||||
|       b = self.read(n) | ||||
|       if not b: | ||||
|         if ignore_eof: | ||||
|           time.sleep(0.01) | ||||
|           continue | ||||
|         if len(msg._buf) > 0: | ||||
|           self.debug(1, "dropping %d bytes" % len(msg._buf)) | ||||
|         return None | ||||
|       msg.add(b) | ||||
|       if self.log is not None: | ||||
|         self.log.write(b) | ||||
|         self.log.flush() | ||||
|       if msg.valid(): | ||||
|         self.special_handling(msg) | ||||
|         return msg | ||||
| 
 | ||||
|   def receive_message_noerror(self, ignore_eof=False): | ||||
|     '''blocking receive of one ublox message, ignoring errors''' | ||||
|     try: | ||||
|       return self.receive_message(ignore_eof=ignore_eof) | ||||
|     except UBloxError as e: | ||||
|       print(e) | ||||
|       return None | ||||
|     except OSError as e: | ||||
|       # Occasionally we get hit with 'resource temporarily unavailable' | ||||
|       # messages here on the serial device, catch them too. | ||||
|       print(e) | ||||
|       return None | ||||
| 
 | ||||
|   def send(self, msg): | ||||
|     '''send a preformatted ublox message''' | ||||
|     if not msg.valid(): | ||||
|       self.debug(1, "invalid send") | ||||
|       return | ||||
|     if not self.read_only: | ||||
|       self.write(msg._buf) | ||||
| 
 | ||||
|   def send_message(self, msg_class, msg_id, payload): | ||||
|     '''send a ublox message with class, id and payload''' | ||||
|     msg = UBloxMessage() | ||||
|     msg._buf = struct.pack('<BBBBH', 0xb5, 0x62, msg_class, msg_id, len(payload)) | ||||
|     msg._buf += payload | ||||
|     (ck_a, ck_b) = msg.checksum(msg._buf[2:]) | ||||
|     msg._buf += struct.pack('<BB', ck_a, ck_b) | ||||
|     self.send(msg) | ||||
| 
 | ||||
|   def configure_solution_rate(self, rate_ms=200, nav_rate=1, timeref=0): | ||||
|     '''configure the solution rate in milliseconds''' | ||||
|     payload = struct.pack('<HHH', rate_ms, nav_rate, timeref) | ||||
|     self.send_message(CLASS_CFG, MSG_CFG_RATE, payload) | ||||
| 
 | ||||
|   def configure_message_rate(self, msg_class, msg_id, rate): | ||||
|     '''configure the message rate for a given message''' | ||||
|     payload = struct.pack('<BBB', msg_class, msg_id, rate) | ||||
|     self.send_message(CLASS_CFG, MSG_CFG_SET_RATE, payload) | ||||
| 
 | ||||
|   def configure_port(self, port=1, inMask=3, outMask=3, mode=2240, baudrate=None): | ||||
|     '''configure a IO port''' | ||||
|     if baudrate is None: | ||||
|       baudrate = self.baudrate | ||||
|     payload = struct.pack('<BBH8BHHBBBB', port, 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, inMask, | ||||
|                           outMask, 0, 0, 0, 0) | ||||
|     self.send_message(CLASS_CFG, MSG_CFG_PRT, payload) | ||||
| 
 | ||||
|   def configure_loadsave(self, clearMask=0, saveMask=0, loadMask=0, deviceMask=0): | ||||
|     '''configure configuration load/save''' | ||||
|     payload = struct.pack('<IIIB', clearMask, saveMask, loadMask, deviceMask) | ||||
|     self.send_message(CLASS_CFG, MSG_CFG_CFG, payload) | ||||
| 
 | ||||
|   def configure_poll(self, msg_class, msg_id, payload=''): | ||||
|     '''poll a configuration message''' | ||||
|     self.send_message(msg_class, msg_id, payload) | ||||
| 
 | ||||
|   def configure_poll_port(self, portID=None): | ||||
|     '''poll a port configuration''' | ||||
|     if portID is None: | ||||
|       self.configure_poll(CLASS_CFG, MSG_CFG_PRT) | ||||
|     else: | ||||
|       self.configure_poll(CLASS_CFG, MSG_CFG_PRT, struct.pack('<B', portID)) | ||||
| 
 | ||||
|   def configure_min_max_sats(self, min_sats=4, max_sats=32): | ||||
|     '''Set the minimum/maximum number of satellites for a solution in the NAVX5 message''' | ||||
|     payload = struct.pack('<HHIBBBBBBBBBBHIBBBBBBHII', 0, 4, 0, 0, 0, min_sats, max_sats, | ||||
|                           0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) | ||||
|     self.send_message(CLASS_CFG, MSG_CFG_NAVX5, payload) | ||||
| 
 | ||||
|   def module_reset(self, set, mode): | ||||
|     ''' Reset the module for hot/warm/cold start''' | ||||
|     payload = struct.pack('<HBB', set, mode, 0) | ||||
|     self.send_message(CLASS_CFG, MSG_CFG_RST, payload) | ||||
| @ -0,0 +1,3 @@ | ||||
| version https://git-lfs.github.com/spec/v1 | ||||
| oid sha256:a15bc80894142f5b959fe83b627edf0c658ce881bac9447126b2979fdf5a2e1a | ||||
| size 2069996 | ||||
| @ -0,0 +1,287 @@ | ||||
| #!/usr/bin/env python3 | ||||
| 
 | ||||
| import os | ||||
| import serial | ||||
| from selfdrive.locationd.test import ublox | ||||
| import time | ||||
| import datetime | ||||
| import struct | ||||
| import sys | ||||
| from cereal import log | ||||
| from common import realtime | ||||
| import cereal.messaging as messaging | ||||
| from selfdrive.locationd.test.ephemeris import EphemerisData, GET_FIELD_U | ||||
| 
 | ||||
| panda = os.getenv("PANDA") is not None   # panda directly connected | ||||
| grey = not (os.getenv("EVAL") is not None)     # panda through boardd | ||||
| debug = os.getenv("DEBUG") is not None   # debug prints | ||||
| print_dB = os.getenv("PRINT_DB") is not None     # print antenna dB | ||||
| 
 | ||||
| timeout = 1 | ||||
| dyn_model = 4 # auto model | ||||
| baudrate = 460800 | ||||
| ports = ["/dev/ttyACM0","/dev/ttyACM1"] | ||||
| rate = 100 # send new data every 100ms | ||||
| 
 | ||||
| # which SV IDs we have seen and when we got iono | ||||
| svid_seen = {} | ||||
| svid_ephemeris = {} | ||||
| iono_seen = 0 | ||||
| 
 | ||||
| def configure_ublox(dev): | ||||
|   # configure ports  and solution parameters and rate | ||||
|   # TODO: configure constellations and channels to allow for 10Hz and high precision | ||||
|   dev.configure_port(port=ublox.PORT_USB, inMask=1, outMask=1) # enable only UBX on USB | ||||
|   dev.configure_port(port=0, inMask=0, outMask=0) # disable DDC | ||||
| 
 | ||||
|   if panda: | ||||
|     payload = struct.pack('<BBHIIHHHBB', 1, 0, 0, 2240, baudrate, 1, 1, 0, 0, 0) | ||||
|     dev.configure_poll(ublox.CLASS_CFG, ublox.MSG_CFG_PRT, payload) # enable UART | ||||
|   else: | ||||
|     payload = struct.pack('<BBHIIHHHBB', 1, 0, 0, 2240, baudrate, 0, 0, 0, 0, 0) | ||||
|     dev.configure_poll(ublox.CLASS_CFG, ublox.MSG_CFG_PRT, payload) # disable UART | ||||
| 
 | ||||
|   dev.configure_port(port=4, inMask=0, outMask=0) # disable SPI | ||||
|   dev.configure_poll_port() | ||||
|   dev.configure_poll_port(ublox.PORT_SERIAL1) | ||||
|   dev.configure_poll_port(ublox.PORT_SERIAL2) | ||||
|   dev.configure_poll_port(ublox.PORT_USB) | ||||
|   dev.configure_solution_rate(rate_ms=rate) | ||||
| 
 | ||||
|   # Configure solution | ||||
|   payload = struct.pack('<HBBIIBB4H6BH6B', 5, 4, 3, 0, 0, | ||||
|                                            0, 0, 0, 0, 0, | ||||
|                                            0, 0, 0, 0, 0, | ||||
|                                            0, 0, 0, 0, 0, | ||||
|                                            0, 0, 0, 0) | ||||
|   dev.configure_poll(ublox.CLASS_CFG, ublox.MSG_CFG_NAV5, payload) | ||||
|   payload = struct.pack('<B3BBB6BBB2BBB2B', 0, 0, 0, 0, 1, | ||||
|                                             3, 0, 0, 0, 0, | ||||
|                                             0, 0, 0, 0, 0, | ||||
|                                             0, 0, 0, 0, 0) | ||||
|   dev.configure_poll(ublox.CLASS_CFG, ublox.MSG_CFG_ODO, payload) | ||||
|   #payload = struct.pack('<HHIBBBBBBBBBBH6BBB2BH4B3BB', 0, 8192, 0, 0, 0, | ||||
|   #                                                     0, 0, 0, 0, 0, 0, | ||||
|   #                                                     0, 0, 0, 0, 0, 0, | ||||
|   #                                                     0, 0, 0, 0, 0, 0, | ||||
|   #                                                     0, 0, 0, 0, 0, 0, | ||||
|   #                                                     0, 0, 0, 0) | ||||
|   #dev.configure_poll(ublox.CLASS_CFG, ublox.MSG_CFG_NAVX5, payload) | ||||
| 
 | ||||
|   dev.configure_poll(ublox.CLASS_CFG, ublox.MSG_CFG_NAV5) | ||||
|   dev.configure_poll(ublox.CLASS_CFG, ublox.MSG_CFG_NAVX5) | ||||
|   dev.configure_poll(ublox.CLASS_CFG, ublox.MSG_CFG_ODO) | ||||
| 
 | ||||
|   # Configure RAW and PVT messages to be sent every solution cycle | ||||
|   dev.configure_message_rate(ublox.CLASS_NAV, ublox.MSG_NAV_PVT, 1) | ||||
|   dev.configure_message_rate(ublox.CLASS_RXM, ublox.MSG_RXM_RAW, 1) | ||||
|   dev.configure_message_rate(ublox.CLASS_RXM, ublox.MSG_RXM_SFRBX, 1) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| def int_to_bool_list(num): | ||||
|   # for parsing bool bytes | ||||
|   return [bool(num & (1<<n)) for n in range(8)] | ||||
| 
 | ||||
| 
 | ||||
| def gen_ephemeris(ephem_data): | ||||
|   ephem = {'ephemeris': | ||||
|           {'svId': ephem_data.svId, | ||||
| 
 | ||||
|            'toc': ephem_data.toc, | ||||
|            'gpsWeek': ephem_data.gpsWeek, | ||||
| 
 | ||||
|            'af0': ephem_data.af0, | ||||
|            'af1': ephem_data.af1, | ||||
|            'af2': ephem_data.af2, | ||||
| 
 | ||||
|            'iode': ephem_data.iode, | ||||
|            'crs': ephem_data.crs, | ||||
|            'deltaN': ephem_data.deltaN, | ||||
|            'm0': ephem_data.M0, | ||||
| 
 | ||||
|            'cuc': ephem_data.cuc, | ||||
|            'ecc': ephem_data.ecc, | ||||
|            'cus': ephem_data.cus, | ||||
|            'a': ephem_data.A, | ||||
| 
 | ||||
|            'toe': ephem_data.toe, | ||||
|            'cic': ephem_data.cic, | ||||
|            'omega0': ephem_data.omega0, | ||||
|            'cis': ephem_data.cis, | ||||
| 
 | ||||
|            'i0': ephem_data.i0, | ||||
|            'crc': ephem_data.crc, | ||||
|            'omega': ephem_data.omega, | ||||
|            'omegaDot': ephem_data.omega_dot, | ||||
| 
 | ||||
|            'iDot': ephem_data.idot, | ||||
| 
 | ||||
|            'tgd': ephem_data.Tgd, | ||||
| 
 | ||||
|            'ionoCoeffsValid': ephem_data.ionoCoeffsValid, | ||||
|            'ionoAlpha': ephem_data.ionoAlpha, | ||||
|            'ionoBeta': ephem_data.ionoBeta}} | ||||
|   return log.Event.new_message(ubloxGnss=ephem) | ||||
| 
 | ||||
| 
 | ||||
| def gen_solution(msg): | ||||
|   msg_data = msg.unpack()[0] # Solutions do not have any data in repeated blocks | ||||
|   timestamp = int(((datetime.datetime(msg_data['year'], | ||||
|                                       msg_data['month'], | ||||
|                                       msg_data['day'], | ||||
|                                       msg_data['hour'], | ||||
|                                       msg_data['min'], | ||||
|                                       msg_data['sec']) | ||||
|                  - datetime.datetime(1970,1,1)).total_seconds())*1e+03 | ||||
|                  + msg_data['nano']*1e-06) | ||||
|   gps_fix = {'bearing': msg_data['headMot']*1e-05,  # heading of motion in degrees | ||||
|              'altitude': msg_data['height']*1e-03,  # altitude above ellipsoid | ||||
|              'latitude': msg_data['lat']*1e-07,  # latitude in degrees | ||||
|              'longitude': msg_data['lon']*1e-07,  # longitude in degrees | ||||
|              'speed': msg_data['gSpeed']*1e-03,  # ground speed in meters | ||||
|              'accuracy': msg_data['hAcc']*1e-03,  # horizontal accuracy (1 sigma?) | ||||
|              'timestamp': timestamp,  # UTC time in ms since start of UTC stime | ||||
|              'vNED': [msg_data['velN']*1e-03, | ||||
|                      msg_data['velE']*1e-03, | ||||
|                      msg_data['velD']*1e-03],  # velocity in NED frame in m/s | ||||
|              'speedAccuracy': msg_data['sAcc']*1e-03,  # speed accuracy in m/s | ||||
|              'verticalAccuracy': msg_data['vAcc']*1e-03,  # vertical accuracy in meters | ||||
|              'bearingAccuracy': msg_data['headAcc']*1e-05,  # heading accuracy in degrees | ||||
|              'source': 'ublox', | ||||
|              'flags': msg_data['flags'], | ||||
|   } | ||||
|   return log.Event.new_message(gpsLocationExternal=gps_fix) | ||||
| 
 | ||||
| def gen_nav_data(msg, nav_frame_buffer): | ||||
|   # TODO this stuff needs to be parsed and published. | ||||
|   # refer to https://www.u-blox.com/sites/default/files/products/documents/u-blox8-M8_ReceiverDescrProtSpec_%28UBX-13003221%29.pdf | ||||
|   # section 9.1 | ||||
|   msg_meta_data, measurements = msg.unpack() | ||||
| 
 | ||||
|   # parse GPS ephem | ||||
|   gnssId = msg_meta_data['gnssId'] | ||||
|   if gnssId  == 0: | ||||
|     svId =  msg_meta_data['svid'] | ||||
|     subframeId =  GET_FIELD_U(measurements[1]['dwrd'], 3, 8) | ||||
|     words = [] | ||||
|     for m in measurements: | ||||
|       words.append(m['dwrd']) | ||||
| 
 | ||||
|     # parse from | ||||
|     if subframeId == 1: | ||||
|       nav_frame_buffer[gnssId][svId] = {} | ||||
|       nav_frame_buffer[gnssId][svId][subframeId] = words | ||||
|     elif subframeId-1 in nav_frame_buffer[gnssId][svId]: | ||||
|       nav_frame_buffer[gnssId][svId][subframeId] = words | ||||
|     if len(nav_frame_buffer[gnssId][svId]) == 5: | ||||
|       ephem_data = EphemerisData(svId, nav_frame_buffer[gnssId][svId]) | ||||
|       return gen_ephemeris(ephem_data) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| def gen_raw(msg): | ||||
|   # meta data is in first part of tuple | ||||
|   # list of measurements is in second part | ||||
|   msg_meta_data, measurements = msg.unpack() | ||||
|   measurements_parsed = [] | ||||
|   for m in measurements: | ||||
|     trackingStatus_bools = int_to_bool_list(m['trkStat']) | ||||
|     trackingStatus = {'pseudorangeValid': trackingStatus_bools[0], | ||||
|                       'carrierPhaseValid': trackingStatus_bools[1], | ||||
|                       'halfCycleValid': trackingStatus_bools[2], | ||||
|                       'halfCycleSubtracted': trackingStatus_bools[3]} | ||||
|     measurements_parsed.append({ | ||||
|         'svId': m['svId'], | ||||
|         'sigId': m['sigId'], | ||||
|         'pseudorange': m['prMes'], | ||||
|         'carrierCycles': m['cpMes'], | ||||
|         'doppler': m['doMes'], | ||||
|         'gnssId': m['gnssId'], | ||||
|         'glonassFrequencyIndex': m['freqId'], | ||||
|         'locktime': m['locktime'], | ||||
|         'cno': m['cno'], | ||||
|         'pseudorangeStdev': 0.01*(2**(m['prStdev'] & 15)), # weird scaling, might be wrong | ||||
|         'carrierPhaseStdev': 0.004*(m['cpStdev'] & 15), | ||||
|         'dopplerStdev': 0.002*(2**(m['doStdev'] & 15)), # weird scaling, might be wrong | ||||
|         'trackingStatus': trackingStatus}) | ||||
|   if print_dB: | ||||
|     cnos = {} | ||||
|     for meas in measurements_parsed: | ||||
|       cnos[meas['svId']] = meas['cno'] | ||||
|     print('Carrier to noise ratio for each sat: \n', cnos, '\n') | ||||
|   receiverStatus_bools = int_to_bool_list(msg_meta_data['recStat']) | ||||
|   receiverStatus = {'leapSecValid': receiverStatus_bools[0], | ||||
|                     'clkReset': receiverStatus_bools[2]} | ||||
|   raw_meas = {'measurementReport': {'rcvTow': msg_meta_data['rcvTow'], | ||||
|                 'gpsWeek': msg_meta_data['week'], | ||||
|                 'leapSeconds': msg_meta_data['leapS'], | ||||
|                 'receiverStatus': receiverStatus, | ||||
|                 'numMeas': msg_meta_data['numMeas'], | ||||
|                 'measurements': measurements_parsed}} | ||||
|   return log.Event.new_message(ubloxGnss=raw_meas) | ||||
| 
 | ||||
| def init_reader(): | ||||
|   port_counter = 0 | ||||
|   while True: | ||||
|     try: | ||||
|       dev = ublox.UBlox(ports[port_counter], baudrate=baudrate, timeout=timeout, panda=panda, grey=grey) | ||||
|       configure_ublox(dev) | ||||
|       return dev | ||||
|     except serial.serialutil.SerialException as e: | ||||
|       print(e) | ||||
|       port_counter = (port_counter + 1)%len(ports) | ||||
|       time.sleep(2) | ||||
| 
 | ||||
| def handle_msg(dev, msg, nav_frame_buffer): | ||||
|   try: | ||||
|     if debug: | ||||
|       print(str(msg)) | ||||
|       sys.stdout.flush() | ||||
|     if msg.name() == 'NAV_PVT': | ||||
|       sol = gen_solution(msg) | ||||
|       sol.logMonoTime = int(realtime.sec_since_boot() * 1e9) | ||||
|       gpsLocationExternal.send(sol.to_bytes()) | ||||
|     elif msg.name() == 'RXM_RAW': | ||||
|       raw = gen_raw(msg) | ||||
|       raw.logMonoTime = int(realtime.sec_since_boot() * 1e9) | ||||
|       ubloxGnss.send(raw.to_bytes()) | ||||
|     elif msg.name() == 'RXM_SFRBX': | ||||
|       nav = gen_nav_data(msg, nav_frame_buffer) | ||||
|       if nav is not None: | ||||
|         nav.logMonoTime = int(realtime.sec_since_boot() * 1e9) | ||||
|         ubloxGnss.send(nav.to_bytes()) | ||||
| 
 | ||||
|     else: | ||||
|       print("UNKNNOWN MESSAGE:", msg.name()) | ||||
|   except ublox.UBloxError as e: | ||||
|     print(e) | ||||
| 
 | ||||
|   #if dev is not None and dev.dev is not None: | ||||
|   #  dev.close() | ||||
| 
 | ||||
| def main(gctx=None): | ||||
|   global gpsLocationExternal, ubloxGnss | ||||
|   nav_frame_buffer = {} | ||||
|   nav_frame_buffer[0] = {} | ||||
|   for i in range(1,33): | ||||
|     nav_frame_buffer[0][i] = {} | ||||
| 
 | ||||
| 
 | ||||
|   gpsLocationExternal = messaging.pub_sock('gpsLocationExternal') | ||||
|   ubloxGnss = messaging.pub_sock('ubloxGnss') | ||||
| 
 | ||||
|   dev = init_reader() | ||||
|   while True: | ||||
|     try: | ||||
|       msg = dev.receive_message() | ||||
|     except serial.serialutil.SerialException as e: | ||||
|       print(e) | ||||
|       dev.close() | ||||
|       dev = init_reader() | ||||
|     if msg is not None: | ||||
|       handle_msg(dev, msg, nav_frame_buffer) | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|   main() | ||||
| @ -0,0 +1,53 @@ | ||||
| #!/usr/bin/env python3 | ||||
| 
 | ||||
| import os | ||||
| from selfdrive.locationd.test import ublox | ||||
| from common import realtime | ||||
| from selfdrive.locationd.test.ubloxd import gen_raw, gen_solution | ||||
| import zmq | ||||
| import cereal.messaging as messaging | ||||
| 
 | ||||
| 
 | ||||
| unlogger = os.getenv("UNLOGGER") is not None   # debug prints | ||||
| 
 | ||||
| def main(gctx=None): | ||||
|   poller = zmq.Poller() | ||||
| 
 | ||||
|   gpsLocationExternal = messaging.pub_sock('gpsLocationExternal') | ||||
|   ubloxGnss = messaging.pub_sock('ubloxGnss') | ||||
| 
 | ||||
|   # ubloxRaw = messaging.sub_sock('ubloxRaw', poller) | ||||
| 
 | ||||
|   # buffer with all the messages that still need to be input into the kalman | ||||
|   while 1: | ||||
|     polld = poller.poll(timeout=1000) | ||||
|     for sock, mode in polld: | ||||
|       if mode != zmq.POLLIN: | ||||
|         continue | ||||
|       logs = messaging.drain_sock(sock) | ||||
|       for log in logs: | ||||
|         buff = log.ubloxRaw | ||||
|         time = log.logMonoTime | ||||
|         msg = ublox.UBloxMessage() | ||||
|         msg.add(buff) | ||||
|         if msg.valid(): | ||||
|           if msg.name() == 'NAV_PVT': | ||||
|             sol = gen_solution(msg) | ||||
|             if unlogger: | ||||
|               sol.logMonoTime = time | ||||
|             else: | ||||
|               sol.logMonoTime = int(realtime.sec_since_boot() * 1e9) | ||||
|             gpsLocationExternal.send(sol.to_bytes()) | ||||
|           elif msg.name() == 'RXM_RAW': | ||||
|             raw = gen_raw(msg) | ||||
|             if unlogger: | ||||
|               raw.logMonoTime = time | ||||
|             else: | ||||
|               raw.logMonoTime = int(realtime.sec_since_boot() * 1e9) | ||||
|             ubloxGnss.send(raw.to_bytes()) | ||||
|         else: | ||||
|           print("INVALID MESSAGE") | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|   main() | ||||
| @ -0,0 +1,77 @@ | ||||
| import sys | ||||
| import os | ||||
| 
 | ||||
| from selfdrive.locationd.test.ublox import UBloxMessage | ||||
| from selfdrive.locationd.test.ubloxd import gen_solution, gen_raw, gen_nav_data | ||||
| from common import realtime | ||||
| 
 | ||||
| 
 | ||||
| def mkdirs_exists_ok(path): | ||||
|   try: | ||||
|     os.makedirs(path) | ||||
|   except OSError: | ||||
|     if not os.path.isdir(path): | ||||
|       raise | ||||
| 
 | ||||
| 
 | ||||
| def parser_test(fn, prefix): | ||||
|   nav_frame_buffer = {} | ||||
|   nav_frame_buffer[0] = {} | ||||
|   for i in range(1, 33): | ||||
|     nav_frame_buffer[0][i] = {} | ||||
| 
 | ||||
|   if not os.path.exists(prefix): | ||||
|     print('Prefix invalid') | ||||
|     sys.exit(-1) | ||||
| 
 | ||||
|   with open(fn, 'rb') as f: | ||||
|     i = 0 | ||||
|     saved_i = 0 | ||||
|     msg = UBloxMessage() | ||||
|     while True: | ||||
|       n = msg.needed_bytes() | ||||
|       b = f.read(n) | ||||
|       if not b: | ||||
|         break | ||||
|       msg.add(b) | ||||
|       if msg.valid(): | ||||
|         i += 1 | ||||
|         if msg.name() == 'NAV_PVT': | ||||
|           sol = gen_solution(msg) | ||||
|           sol.logMonoTime = int(realtime.sec_since_boot() * 1e9) | ||||
|           with open(os.path.join(prefix, str(saved_i)), 'wb') as f1: | ||||
|             f1.write(sol.to_bytes()) | ||||
|             saved_i += 1 | ||||
|         elif msg.name() == 'RXM_RAW': | ||||
|           raw = gen_raw(msg) | ||||
|           raw.logMonoTime = int(realtime.sec_since_boot() * 1e9) | ||||
|           with open(os.path.join(prefix, str(saved_i)), 'wb') as f1: | ||||
|             f1.write(raw.to_bytes()) | ||||
|             saved_i += 1 | ||||
|         elif msg.name() == 'RXM_SFRBX': | ||||
|           nav = gen_nav_data(msg, nav_frame_buffer) | ||||
|           if nav is not None: | ||||
|             nav.logMonoTime = int(realtime.sec_since_boot() * 1e9) | ||||
|             with open(os.path.join(prefix, str(saved_i)), 'wb') as f1: | ||||
|               f1.write(nav.to_bytes()) | ||||
|               saved_i += 1 | ||||
| 
 | ||||
|         msg = UBloxMessage() | ||||
|         msg.debug_level = 0 | ||||
|     print('Parsed {} msgs'.format(i)) | ||||
|     print('Generated {} cereal events'.format(saved_i)) | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|   if len(sys.argv) < 3: | ||||
|     print('Format: ubloxd_py_test.py file_path prefix') | ||||
|     sys.exit(0) | ||||
| 
 | ||||
|   fn = sys.argv[1] | ||||
|   if not os.path.isfile(fn): | ||||
|     print('File path invalid') | ||||
|     sys.exit(0) | ||||
| 
 | ||||
|   prefix = sys.argv[2] | ||||
|   mkdirs_exists_ok(prefix) | ||||
|   parser_test(fn, prefix) | ||||
| @ -0,0 +1,96 @@ | ||||
| #!/usr/bin/env python3 | ||||
| import os | ||||
| import sys | ||||
| import argparse | ||||
| 
 | ||||
| from cereal import log | ||||
| from common.basedir import BASEDIR | ||||
| os.environ['BASEDIR'] = BASEDIR | ||||
| 
 | ||||
| 
 | ||||
| def get_arg_parser(): | ||||
|   parser = argparse.ArgumentParser( | ||||
|       description="Compare two result files", | ||||
|       formatter_class=argparse.ArgumentDefaultsHelpFormatter) | ||||
| 
 | ||||
|   parser.add_argument("dir1", nargs='?', default='/data/ubloxdc', | ||||
|                       help="Directory path 1 from which events are loaded") | ||||
| 
 | ||||
|   parser.add_argument("dir2", nargs='?', default='/data/ubloxdpy', | ||||
|                       help="Directory path 2 from which msgs are loaded") | ||||
| 
 | ||||
|   return parser | ||||
| 
 | ||||
| 
 | ||||
| def read_file(fn): | ||||
|   with open(fn, 'rb') as f: | ||||
|     return f.read() | ||||
| 
 | ||||
| 
 | ||||
| def compare_results(dir1, dir2): | ||||
|   onlyfiles1 = [f for f in os.listdir(dir1) if os.path.isfile(os.path.join(dir1, f))] | ||||
|   onlyfiles1.sort() | ||||
| 
 | ||||
|   onlyfiles2 = [f for f in os.listdir(dir2) if os.path.isfile(os.path.join(dir2, f))] | ||||
|   onlyfiles2.sort() | ||||
| 
 | ||||
|   if len(onlyfiles1) != len(onlyfiles2): | ||||
|     print('len mismatch: {} != {}'.format(len(onlyfiles1), len(onlyfiles2))) | ||||
|     return -1 | ||||
|   events1 = [log.Event.from_bytes(read_file(os.path.join(dir1, f))) for f in onlyfiles1] | ||||
|   events2 = [log.Event.from_bytes(read_file(os.path.join(dir2, f))) for f in onlyfiles2] | ||||
| 
 | ||||
|   for i in range(len(events1)): | ||||
|     if events1[i].which() != events2[i].which(): | ||||
|       print('event {} type mismatch: {} != {}'.format(i, events1[i].which(), events2[i].which())) | ||||
|       return -2 | ||||
|     if events1[i].which() == 'gpsLocationExternal': | ||||
|       old_gps = events1[i].gpsLocationExternal | ||||
|       gps = events2[i].gpsLocationExternal | ||||
|       # print(gps, old_gps) | ||||
|       attrs = ['flags', 'latitude', 'longitude', 'altitude', 'speed', 'bearing', | ||||
|                'accuracy', 'timestamp', 'source', 'vNED', 'verticalAccuracy', 'bearingAccuracy', 'speedAccuracy'] | ||||
|       for attr in attrs: | ||||
|         o = getattr(old_gps, attr) | ||||
|         n = getattr(gps, attr) | ||||
|         if attr == 'vNED': | ||||
|           if len(o) != len(n): | ||||
|             print('Gps vNED len mismatch', o, n) | ||||
|             return -3 | ||||
|           else: | ||||
|             for i in range(len(o)): | ||||
|               if abs(o[i] - n[i]) > 1e-3: | ||||
|                 print('Gps vNED mismatch', o, n) | ||||
|                 return | ||||
|         elif o != n: | ||||
|           print('Gps mismatch', attr, o, n) | ||||
|           return -4 | ||||
|     elif events1[i].which() == 'ubloxGnss': | ||||
|       old_gnss = events1[i].ubloxGnss | ||||
|       gnss = events2[i].ubloxGnss | ||||
|       if old_gnss.which() == 'measurementReport' and gnss.which() == 'measurementReport': | ||||
|         attrs = ['gpsWeek', 'leapSeconds', 'measurements', 'numMeas', 'rcvTow', 'receiverStatus', 'schema'] | ||||
|         for attr in attrs: | ||||
|           o = getattr(old_gnss.measurementReport, attr) | ||||
|           n = getattr(gnss.measurementReport, attr) | ||||
|           if str(o) != str(n): | ||||
|             print('measurementReport {} mismatched'.format(attr)) | ||||
|             return -5 | ||||
|         if not (str(old_gnss.measurementReport) == str(gnss.measurementReport)): | ||||
|           print('Gnss measurementReport mismatched!') | ||||
|           print('gnss measurementReport old', old_gnss.measurementReport.measurements) | ||||
|           print('gnss measurementReport new', gnss.measurementReport.measurements) | ||||
|           return -6 | ||||
|       elif old_gnss.which() == 'ephemeris' and gnss.which() == 'ephemeris': | ||||
|         if not (str(old_gnss.ephemeris) == str(gnss.ephemeris)): | ||||
|           print('Gnss ephemeris mismatched!') | ||||
|           print('gnss ephemeris old', old_gnss.ephemeris) | ||||
|           print('gnss ephemeris new', gnss.ephemeris) | ||||
|           return -7 | ||||
|   print('All {} events matched!'.format(len(events1))) | ||||
|   return 0 | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|   args = get_arg_parser().parse_args(sys.argv[1:]) | ||||
|   compare_results(args.dir1, args.dir2) | ||||
| @ -0,0 +1,374 @@ | ||||
| #include <stdio.h> | ||||
| #include <stdlib.h> | ||||
| #include <string.h> | ||||
| #include <signal.h> | ||||
| #include <unistd.h> | ||||
| #include <sched.h> | ||||
| #include <sys/time.h> | ||||
| #include <sys/cdefs.h> | ||||
| #include <sys/types.h> | ||||
| #include <sys/time.h> | ||||
| #include <assert.h> | ||||
| #include <math.h> | ||||
| #include <ctime> | ||||
| #include <chrono> | ||||
| #include <map> | ||||
| #include <vector> | ||||
| #include <algorithm> | ||||
| 
 | ||||
| #include <capnp/serialize.h> | ||||
| #include "cereal/gen/cpp/log.capnp.h" | ||||
| 
 | ||||
| #include "common/params.h" | ||||
| #include "common/swaglog.h" | ||||
| #include "common/timing.h" | ||||
| 
 | ||||
| #include "ublox_msg.h" | ||||
| 
 | ||||
| #define UBLOX_MSG_SIZE(hdr) (*(uint16_t *)&hdr[4]) | ||||
| #define GET_FIELD_U(w, nb, pos) (((w) >> (pos)) & ((1<<(nb))-1)) | ||||
| 
 | ||||
| namespace ublox { | ||||
| 
 | ||||
| inline int twos_complement(uint32_t v, uint32_t nb) { | ||||
|   int sign = v >> (nb - 1); | ||||
|   int value = v; | ||||
|   if(sign != 0) | ||||
|     value = value - (1 << nb); | ||||
|   return value; | ||||
| } | ||||
| 
 | ||||
| inline int GET_FIELD_S(uint32_t w, uint32_t nb, uint32_t pos) { | ||||
|   int v = GET_FIELD_U(w, nb, pos); | ||||
|   return twos_complement(v, nb); | ||||
| } | ||||
| 
 | ||||
| class EphemerisData { | ||||
|   public: | ||||
|     EphemerisData(uint8_t svId, subframes_map subframes) { | ||||
|       this->svId = svId; | ||||
|       int week_no = GET_FIELD_U(subframes[1][2+0], 10, 20); | ||||
|       int t_gd = GET_FIELD_S(subframes[1][2+4], 8, 6); | ||||
|       int iodc = (GET_FIELD_U(subframes[1][2+0], 2, 6) << 8) | GET_FIELD_U( | ||||
|         subframes[1][2+5], 8, 22); | ||||
| 
 | ||||
|       int t_oc = GET_FIELD_U(subframes[1][2+5], 16, 6); | ||||
|       int a_f2 = GET_FIELD_S(subframes[1][2+6], 8, 22); | ||||
|       int a_f1 = GET_FIELD_S(subframes[1][2+6], 16, 6); | ||||
|       int a_f0 = GET_FIELD_S(subframes[1][2+7], 22, 8); | ||||
| 
 | ||||
|       int c_rs = GET_FIELD_S(subframes[2][2+0], 16, 6); | ||||
|       int delta_n = GET_FIELD_S(subframes[2][2+1], 16, 14); | ||||
|       int m_0 = (GET_FIELD_S(subframes[2][2+1], 8, 6) << 24) | GET_FIELD_U( | ||||
|         subframes[2][2+2], 24, 6); | ||||
|       int c_uc = GET_FIELD_S(subframes[2][2+3], 16, 14); | ||||
|       int e = (GET_FIELD_U(subframes[2][2+3], 8, 6) << 24) | GET_FIELD_U(subframes[2][2+4], 24, 6); | ||||
|       int c_us = GET_FIELD_S(subframes[2][2+5], 16, 14); | ||||
|       uint32_t a_powhalf = (GET_FIELD_U(subframes[2][2+5], 8, 6) << 24) | GET_FIELD_U( | ||||
|         subframes[2][2+6], 24, 6); | ||||
|       int t_oe = GET_FIELD_U(subframes[2][2+7], 16, 14); | ||||
| 
 | ||||
|       int c_ic = GET_FIELD_S(subframes[3][2+0], 16, 14); | ||||
|       int omega_0 = (GET_FIELD_S(subframes[3][2+0], 8, 6) << 24) | GET_FIELD_U( | ||||
|         subframes[3][2+1], 24, 6); | ||||
|       int c_is = GET_FIELD_S(subframes[3][2+2], 16, 14); | ||||
|       int i_0 = (GET_FIELD_S(subframes[3][2+2], 8, 6) << 24) | GET_FIELD_U( | ||||
|         subframes[3][2+3], 24, 6); | ||||
|       int c_rc = GET_FIELD_S(subframes[3][2+4], 16, 14); | ||||
|       int w = (GET_FIELD_S(subframes[3][2+4], 8, 6) << 24) | GET_FIELD_U(subframes[3][5], 24, 6); | ||||
|       int omega_dot = GET_FIELD_S(subframes[3][2+6], 24, 6); | ||||
|       int idot = GET_FIELD_S(subframes[3][2+7], 14, 8); | ||||
| 
 | ||||
|       this->_rsvd1 = GET_FIELD_U(subframes[1][2+1], 23, 6); | ||||
|       this->_rsvd2 = GET_FIELD_U(subframes[1][2+2], 24, 6); | ||||
|       this->_rsvd3 = GET_FIELD_U(subframes[1][2+3], 24, 6); | ||||
|       this->_rsvd4 = GET_FIELD_U(subframes[1][2+4], 16, 14); | ||||
|       this->aodo = GET_FIELD_U(subframes[2][2+7], 5, 8); | ||||
| 
 | ||||
|       double gpsPi = 3.1415926535898; | ||||
| 
 | ||||
|       // now form variables in radians, meters and seconds etc
 | ||||
|       this->Tgd = t_gd * pow(2, -31); | ||||
|       this->A = pow(a_powhalf * pow(2, -19), 2.0); | ||||
|       this->cic = c_ic * pow(2, -29); | ||||
|       this->cis = c_is * pow(2, -29); | ||||
|       this->crc = c_rc * pow(2, -5); | ||||
|       this->crs = c_rs * pow(2, -5); | ||||
|       this->cuc = c_uc * pow(2, -29); | ||||
|       this->cus = c_us * pow(2, -29); | ||||
|       this->deltaN = delta_n * pow(2, -43) * gpsPi; | ||||
|       this->ecc = e * pow(2, -33); | ||||
|       this->i0 = i_0 * pow(2, -31) * gpsPi; | ||||
|       this->idot = idot * pow(2, -43) * gpsPi; | ||||
|       this->M0 = m_0 * pow(2, -31) * gpsPi; | ||||
|       this->omega = w * pow(2, -31) * gpsPi; | ||||
|       this->omega_dot = omega_dot * pow(2, -43) * gpsPi; | ||||
|       this->omega0 = omega_0 * pow(2, -31) * gpsPi; | ||||
|       this->toe = t_oe * pow(2, 4); | ||||
| 
 | ||||
|       this->toc = t_oc * pow(2, 4); | ||||
|       this->gpsWeek = week_no; | ||||
|       this->af0 = a_f0 * pow(2, -31); | ||||
|       this->af1 = a_f1 * pow(2, -43); | ||||
|       this->af2 = a_f2 * pow(2, -55); | ||||
| 
 | ||||
|       uint32_t iode1 = GET_FIELD_U(subframes[2][2+0], 8, 22); | ||||
|       uint32_t iode2 = GET_FIELD_U(subframes[3][2+7], 8, 22); | ||||
|       this->valid = (iode1 == iode2) && (iode1 == (iodc & 0xff)); | ||||
|       this->iode = iode1; | ||||
| 
 | ||||
|       if (GET_FIELD_U(subframes[4][2+0], 6, 22) == 56 && | ||||
|         GET_FIELD_U(subframes[4][2+0], 2, 28) == 1 && | ||||
|         GET_FIELD_U(subframes[5][2+0], 2, 28) == 1) { | ||||
|         double a0 = GET_FIELD_S(subframes[4][2], 8, 14) * pow(2, -30); | ||||
|         double a1 = GET_FIELD_S(subframes[4][2], 8, 6) * pow(2, -27); | ||||
|         double a2 = GET_FIELD_S(subframes[4][3], 8, 22) * pow(2, -24); | ||||
|         double a3 = GET_FIELD_S(subframes[4][3], 8, 14) * pow(2, -24); | ||||
|         double b0 = GET_FIELD_S(subframes[4][3], 8, 6) * pow(2, 11); | ||||
|         double b1 = GET_FIELD_S(subframes[4][4], 8, 22) * pow(2, 14); | ||||
|         double b2 = GET_FIELD_S(subframes[4][4], 8, 14) * pow(2, 16); | ||||
|         double b3 = GET_FIELD_S(subframes[4][4], 8, 6) * pow(2, 16); | ||||
|         this->ionoAlpha[0] = a0;this->ionoAlpha[1] = a1;this->ionoAlpha[2] = a2;this->ionoAlpha[3] = a3; | ||||
|         this->ionoBeta[0] = b0;this->ionoBeta[1] = b1;this->ionoBeta[2] = b2;this->ionoBeta[3] = b3; | ||||
|         this->ionoCoeffsValid = true; | ||||
|       } else { | ||||
|         this->ionoCoeffsValid = false; | ||||
|       } | ||||
|     } | ||||
|     uint16_t svId; | ||||
|     double Tgd, A, cic, cis, crc, crs, cuc, cus, deltaN, ecc, i0, idot, M0, omega, omega_dot, omega0, toe, toc; | ||||
|     uint32_t gpsWeek, iode, _rsvd1, _rsvd2, _rsvd3, _rsvd4, aodo; | ||||
|     double af0, af1, af2; | ||||
|     bool valid; | ||||
|     double ionoAlpha[4], ionoBeta[4]; | ||||
|     bool ionoCoeffsValid; | ||||
| }; | ||||
| 
 | ||||
| UbloxMsgParser::UbloxMsgParser() :bytes_in_parse_buf(0) { | ||||
|   nav_frame_buffer[0U] = std::map<uint8_t, subframes_map>(); | ||||
|   for(int i = 1;i < 33;i++) | ||||
|     nav_frame_buffer[0U][i] = subframes_map(); | ||||
| } | ||||
| 
 | ||||
| inline int UbloxMsgParser::needed_bytes() { | ||||
|   // Msg header incomplete?
 | ||||
|   if(bytes_in_parse_buf < UBLOX_HEADER_SIZE) | ||||
|     return UBLOX_HEADER_SIZE + UBLOX_CHECKSUM_SIZE - bytes_in_parse_buf; | ||||
|   uint16_t needed = UBLOX_MSG_SIZE(msg_parse_buf) + UBLOX_HEADER_SIZE + UBLOX_CHECKSUM_SIZE; | ||||
|   // too much data
 | ||||
|   if(needed < (uint16_t)bytes_in_parse_buf) | ||||
|     return -1; | ||||
|   return needed - (uint16_t)bytes_in_parse_buf; | ||||
| } | ||||
| 
 | ||||
| inline bool UbloxMsgParser::valid_cheksum() { | ||||
|   uint8_t ck_a = 0, ck_b = 0; | ||||
|   for(int i = 2; i < bytes_in_parse_buf - UBLOX_CHECKSUM_SIZE;i++) { | ||||
|     ck_a = (ck_a + msg_parse_buf[i]) & 0xFF; | ||||
|     ck_b = (ck_b + ck_a) & 0xFF; | ||||
|   } | ||||
|   if(ck_a != msg_parse_buf[bytes_in_parse_buf - 2]) { | ||||
|     LOGD("Checksum a mismtach: %02X, %02X", ck_a, msg_parse_buf[6]); | ||||
|     return false; | ||||
|   } | ||||
|   if(ck_b != msg_parse_buf[bytes_in_parse_buf - 1]) { | ||||
|     LOGD("Checksum b mismtach: %02X, %02X", ck_b, msg_parse_buf[7]); | ||||
|     return false; | ||||
|   } | ||||
|   return true; | ||||
| } | ||||
| 
 | ||||
| inline bool UbloxMsgParser::valid() { | ||||
|   return bytes_in_parse_buf >= UBLOX_HEADER_SIZE + UBLOX_CHECKSUM_SIZE && | ||||
|     needed_bytes() == 0 && | ||||
|     valid_cheksum(); | ||||
| } | ||||
| 
 | ||||
| inline bool UbloxMsgParser::valid_so_far() { | ||||
|   if(bytes_in_parse_buf > 0 && msg_parse_buf[0] != PREAMBLE1) { | ||||
|     //LOGD("PREAMBLE1 invalid, %02X.", msg_parse_buf[0]);
 | ||||
|     return false; | ||||
|   } | ||||
|   if(bytes_in_parse_buf > 1 && msg_parse_buf[1] != PREAMBLE2) { | ||||
|     //LOGD("PREAMBLE2 invalid, %02X.", msg_parse_buf[1]);
 | ||||
|     return false; | ||||
|   } | ||||
|   if(needed_bytes() == 0 && !valid()) | ||||
|     return false; | ||||
|   return true; | ||||
| } | ||||
| 
 | ||||
| kj::Array<capnp::word> UbloxMsgParser::gen_solution() { | ||||
|   nav_pvt_msg *msg = (nav_pvt_msg *)&msg_parse_buf[UBLOX_HEADER_SIZE]; | ||||
|   capnp::MallocMessageBuilder msg_builder; | ||||
|   cereal::Event::Builder event = msg_builder.initRoot<cereal::Event>(); | ||||
|   event.setLogMonoTime(nanos_since_boot()); | ||||
|   auto gpsLoc = event.initGpsLocationExternal(); | ||||
|   gpsLoc.setSource(cereal::GpsLocationData::SensorSource::UBLOX); | ||||
|   gpsLoc.setFlags(msg->flags); | ||||
|   gpsLoc.setLatitude(msg->lat * 1e-07); | ||||
|   gpsLoc.setLongitude(msg->lon * 1e-07); | ||||
|   gpsLoc.setAltitude(msg->height * 1e-03); | ||||
|   gpsLoc.setSpeed(msg->gSpeed * 1e-03); | ||||
|   gpsLoc.setBearing(msg->headMot * 1e-5); | ||||
|   gpsLoc.setAccuracy(msg->hAcc * 1e-03); | ||||
|   std::tm timeinfo = std::tm(); | ||||
|   timeinfo.tm_year = msg->year - 1900; | ||||
|   timeinfo.tm_mon = msg->month - 1; | ||||
|   timeinfo.tm_mday = msg->day; | ||||
|   timeinfo.tm_hour = msg->hour; | ||||
|   timeinfo.tm_min = msg->min; | ||||
|   timeinfo.tm_sec = msg->sec; | ||||
|   std::time_t utc_tt = timegm(&timeinfo); | ||||
|   gpsLoc.setTimestamp(utc_tt * 1e+03 + msg->nano * 1e-06); | ||||
|   float f[] = { msg->velN * 1e-03f, msg->velE * 1e-03f, msg->velD * 1e-03f }; | ||||
|   kj::ArrayPtr<const float> ap(&f[0], sizeof(f) / sizeof(f[0])); | ||||
|   gpsLoc.setVNED(ap); | ||||
|   gpsLoc.setVerticalAccuracy(msg->vAcc * 1e-03); | ||||
|   gpsLoc.setSpeedAccuracy(msg->sAcc * 1e-03); | ||||
|   gpsLoc.setBearingAccuracy(msg->headAcc * 1e-05); | ||||
|   return capnp::messageToFlatArray(msg_builder); | ||||
| } | ||||
| 
 | ||||
| inline bool bit_to_bool(uint8_t val, int shifts) { | ||||
|   return (val & (1 << shifts)) ? true : false; | ||||
| } | ||||
| 
 | ||||
| kj::Array<capnp::word> UbloxMsgParser::gen_raw() { | ||||
|   rxm_raw_msg *msg = (rxm_raw_msg *)&msg_parse_buf[UBLOX_HEADER_SIZE]; | ||||
|   if(bytes_in_parse_buf != ( | ||||
|     UBLOX_HEADER_SIZE + sizeof(rxm_raw_msg) + msg->numMeas * sizeof(rxm_raw_msg_extra) + UBLOX_CHECKSUM_SIZE | ||||
|     )) { | ||||
|     LOGD("Invalid measurement size %u, %u, %u, %u", msg->numMeas, bytes_in_parse_buf, sizeof(rxm_raw_msg_extra), sizeof(rxm_raw_msg)); | ||||
|     return kj::Array<capnp::word>(); | ||||
|   } | ||||
|   rxm_raw_msg_extra *measurements = (rxm_raw_msg_extra *)&msg_parse_buf[UBLOX_HEADER_SIZE + sizeof(rxm_raw_msg)]; | ||||
|   capnp::MallocMessageBuilder msg_builder; | ||||
|   cereal::Event::Builder event = msg_builder.initRoot<cereal::Event>(); | ||||
|   event.setLogMonoTime(nanos_since_boot()); | ||||
|   auto gnss = event.initUbloxGnss(); | ||||
|   auto mr = gnss.initMeasurementReport(); | ||||
|   mr.setRcvTow(msg->rcvTow); | ||||
|   mr.setGpsWeek(msg->week); | ||||
|   mr.setLeapSeconds(msg->leapS); | ||||
|   mr.setGpsWeek(msg->week); | ||||
|   auto mb = mr.initMeasurements(msg->numMeas); | ||||
|   for(int8_t i = 0; i < msg->numMeas; i++) { | ||||
|     mb[i].setSvId(measurements[i].svId); | ||||
|     mb[i].setSigId(measurements[i].sigId); | ||||
|     mb[i].setPseudorange(measurements[i].prMes); | ||||
|     mb[i].setCarrierCycles(measurements[i].cpMes); | ||||
|     mb[i].setDoppler(measurements[i].doMes); | ||||
|     mb[i].setGnssId(measurements[i].gnssId); | ||||
|     mb[i].setGlonassFrequencyIndex(measurements[i].freqId); | ||||
|     mb[i].setLocktime(measurements[i].locktime); | ||||
|     mb[i].setCno(measurements[i].cno); | ||||
|     mb[i].setPseudorangeStdev(0.01*(pow(2, (measurements[i].prStdev & 15)))); // weird scaling, might be wrong
 | ||||
|     mb[i].setCarrierPhaseStdev(0.004*(measurements[i].cpStdev & 15)); | ||||
|     mb[i].setDopplerStdev(0.002*(pow(2, (measurements[i].doStdev & 15)))); // weird scaling, might be wrong
 | ||||
|     auto ts = mb[i].initTrackingStatus(); | ||||
|     ts.setPseudorangeValid(bit_to_bool(measurements[i].trkStat, 0)); | ||||
|     ts.setCarrierPhaseValid(bit_to_bool(measurements[i].trkStat, 1)); | ||||
|     ts.setHalfCycleValid(bit_to_bool(measurements[i].trkStat, 2)); | ||||
|     ts.setHalfCycleSubtracted(bit_to_bool(measurements[i].trkStat, 3)); | ||||
|   } | ||||
| 
 | ||||
|   mr.setNumMeas(msg->numMeas); | ||||
|   auto rs = mr.initReceiverStatus(); | ||||
|   rs.setLeapSecValid(bit_to_bool(msg->recStat, 0)); | ||||
|   rs.setClkReset(bit_to_bool(msg->recStat, 2)); | ||||
|   return capnp::messageToFlatArray(msg_builder); | ||||
| } | ||||
| 
 | ||||
| kj::Array<capnp::word> UbloxMsgParser::gen_nav_data() { | ||||
|   rxm_sfrbx_msg *msg = (rxm_sfrbx_msg *)&msg_parse_buf[UBLOX_HEADER_SIZE]; | ||||
|   if(bytes_in_parse_buf != ( | ||||
|     UBLOX_HEADER_SIZE + sizeof(rxm_sfrbx_msg) + msg->numWords * sizeof(rxm_sfrbx_msg_extra) + UBLOX_CHECKSUM_SIZE | ||||
|     )) { | ||||
|     LOGD("Invalid sfrbx words size %u, %u, %u, %u", msg->numWords, bytes_in_parse_buf, sizeof(rxm_raw_msg_extra), sizeof(rxm_raw_msg)); | ||||
|     return kj::Array<capnp::word>(); | ||||
|   } | ||||
|   rxm_sfrbx_msg_extra *measurements = (rxm_sfrbx_msg_extra *)&msg_parse_buf[UBLOX_HEADER_SIZE + sizeof(rxm_sfrbx_msg)]; | ||||
|   if(msg->gnssId  == 0) { | ||||
|     uint8_t subframeId =  GET_FIELD_U(measurements[1].dwrd, 3, 8); | ||||
|     std::vector<uint32_t> words; | ||||
|     for(int i = 0; i < msg->numWords;i++) | ||||
|       words.push_back(measurements[i].dwrd); | ||||
| 
 | ||||
|     if(subframeId == 1) { | ||||
|       nav_frame_buffer[msg->gnssId][msg->svid] = subframes_map(); | ||||
|       nav_frame_buffer[msg->gnssId][msg->svid][subframeId] = words; | ||||
|     } else if(nav_frame_buffer[msg->gnssId][msg->svid].find(subframeId-1) != nav_frame_buffer[msg->gnssId][msg->svid].end()) | ||||
|       nav_frame_buffer[msg->gnssId][msg->svid][subframeId] = words; | ||||
|     if(nav_frame_buffer[msg->gnssId][msg->svid].size() == 5) { | ||||
|       EphemerisData ephem_data(msg->svid, nav_frame_buffer[msg->gnssId][msg->svid]); | ||||
|       capnp::MallocMessageBuilder msg_builder; | ||||
|       cereal::Event::Builder event = msg_builder.initRoot<cereal::Event>(); | ||||
|       event.setLogMonoTime(nanos_since_boot()); | ||||
|       auto gnss = event.initUbloxGnss(); | ||||
|       auto eph = gnss.initEphemeris(); | ||||
|       eph.setSvId(ephem_data.svId); | ||||
|       eph.setToc(ephem_data.toc); | ||||
|       eph.setGpsWeek(ephem_data.gpsWeek); | ||||
|       eph.setAf0(ephem_data.af0); | ||||
|       eph.setAf1(ephem_data.af1); | ||||
|       eph.setAf2(ephem_data.af2); | ||||
|       eph.setIode(ephem_data.iode); | ||||
|       eph.setCrs(ephem_data.crs); | ||||
|       eph.setDeltaN(ephem_data.deltaN); | ||||
|       eph.setM0(ephem_data.M0); | ||||
|       eph.setCuc(ephem_data.cuc); | ||||
|       eph.setEcc(ephem_data.ecc); | ||||
|       eph.setCus(ephem_data.cus); | ||||
|       eph.setA(ephem_data.A); | ||||
|       eph.setToe(ephem_data.toe); | ||||
|       eph.setCic(ephem_data.cic); | ||||
|       eph.setOmega0(ephem_data.omega0); | ||||
|       eph.setCis(ephem_data.cis); | ||||
|       eph.setI0(ephem_data.i0); | ||||
|       eph.setCrc(ephem_data.crc); | ||||
|       eph.setOmega(ephem_data.omega); | ||||
|       eph.setOmegaDot(ephem_data.omega_dot); | ||||
|       eph.setIDot(ephem_data.idot); | ||||
|       eph.setTgd(ephem_data.Tgd); | ||||
|       eph.setIonoCoeffsValid(ephem_data.ionoCoeffsValid); | ||||
|       if(ephem_data.ionoCoeffsValid) { | ||||
|         kj::ArrayPtr<const double> apa(&ephem_data.ionoAlpha[0], sizeof(ephem_data.ionoAlpha) / sizeof(ephem_data.ionoAlpha[0])); | ||||
|         eph.setIonoAlpha(apa); | ||||
|         kj::ArrayPtr<const double> apb(&ephem_data.ionoBeta[0], sizeof(ephem_data.ionoBeta) / sizeof(ephem_data.ionoBeta[0])); | ||||
|         eph.setIonoBeta(apb); | ||||
|       } else { | ||||
|         eph.setIonoAlpha(kj::ArrayPtr<const double>()); | ||||
|         eph.setIonoBeta(kj::ArrayPtr<const double>()); | ||||
|       } | ||||
|       return capnp::messageToFlatArray(msg_builder); | ||||
|     } | ||||
|   } | ||||
|   return kj::Array<capnp::word>(); | ||||
| } | ||||
| 
 | ||||
| bool UbloxMsgParser::add_data(const uint8_t *incoming_data, uint32_t incoming_data_len, size_t &bytes_consumed) { | ||||
|   int needed = needed_bytes(); | ||||
|   if(needed > 0) { | ||||
|     bytes_consumed = min((size_t)needed, incoming_data_len ); | ||||
|     // Add data to buffer
 | ||||
|     memcpy(msg_parse_buf + bytes_in_parse_buf, incoming_data, bytes_consumed); | ||||
|     bytes_in_parse_buf += bytes_consumed; | ||||
|   } else { | ||||
|     bytes_consumed = incoming_data_len; | ||||
|   } | ||||
|   // Validate msg format, detect invalid header and invalid checksum.
 | ||||
|   while(!valid_so_far() && bytes_in_parse_buf != 0) { | ||||
|     //LOGD("Drop corrupt data, remained in buf: %u", bytes_in_parse_buf);
 | ||||
|     // Corrupted msg, drop a byte.
 | ||||
|     bytes_in_parse_buf -= 1; | ||||
|     if(bytes_in_parse_buf > 0) | ||||
|       memmove(&msg_parse_buf[0], &msg_parse_buf[1], bytes_in_parse_buf); | ||||
|   } | ||||
|   // There is redundant data at the end of buffer, reset the buffer.
 | ||||
|   if(needed_bytes() == -1) | ||||
|     bytes_in_parse_buf = 0; | ||||
|   return valid(); | ||||
| } | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,150 @@ | ||||
| #pragma once | ||||
| 
 | ||||
| #include <stdint.h> | ||||
| #include "messaging.hpp" | ||||
| 
 | ||||
| #define min(x, y) ((x) <= (y) ? (x) : (y)) | ||||
| 
 | ||||
| // NAV_PVT
 | ||||
| typedef struct __attribute__((packed)) { | ||||
|   uint32_t iTOW; | ||||
|   uint16_t year; | ||||
|   int8_t month; | ||||
|   int8_t day; | ||||
|   int8_t hour; | ||||
|   int8_t min; | ||||
|   int8_t sec; | ||||
|   int8_t valid; | ||||
|   uint32_t tAcc; | ||||
|   int32_t nano; | ||||
|   int8_t fixType; | ||||
|   int8_t flags; | ||||
|   int8_t flags2; | ||||
|   int8_t numSV; | ||||
|   int32_t lon; | ||||
|   int32_t lat; | ||||
|   int32_t height; | ||||
|   int32_t hMSL; | ||||
|   uint32_t hAcc; | ||||
|   uint32_t vAcc; | ||||
|   int32_t velN; | ||||
|   int32_t velE; | ||||
|   int32_t velD; | ||||
|   int32_t gSpeed; | ||||
|   int32_t headMot; | ||||
|   uint32_t sAcc; | ||||
|   uint32_t headAcc; | ||||
|   uint16_t pDOP; | ||||
|   int8_t reserverd1[6]; | ||||
|   int32_t headVeh; | ||||
|   int16_t magDec; | ||||
|   uint16_t magAcc; | ||||
| } nav_pvt_msg; | ||||
| 
 | ||||
| // RXM_RAW
 | ||||
| typedef struct __attribute__((packed)) { | ||||
|   double rcvTow; | ||||
|   uint16_t week; | ||||
|   int8_t leapS; | ||||
|   int8_t numMeas; | ||||
|   int8_t recStat; | ||||
|   int8_t reserved1[3]; | ||||
| } rxm_raw_msg; | ||||
| 
 | ||||
| // Extra data count is in numMeas
 | ||||
| typedef struct __attribute__((packed)) { | ||||
|   double prMes; | ||||
|   double cpMes; | ||||
|   float doMes; | ||||
|   int8_t gnssId; | ||||
|   int8_t svId; | ||||
|   int8_t sigId; | ||||
|   int8_t freqId; | ||||
|   uint16_t locktime; | ||||
|   int8_t cno; | ||||
|   int8_t prStdev; | ||||
|   int8_t cpStdev; | ||||
|   int8_t doStdev; | ||||
|   int8_t trkStat; | ||||
|   int8_t reserved3; | ||||
| } rxm_raw_msg_extra; | ||||
| // RXM_SFRBX
 | ||||
| typedef struct __attribute__((packed)) { | ||||
|   int8_t gnssId; | ||||
|   int8_t svid; | ||||
|   int8_t reserved1; | ||||
|   int8_t freqId; | ||||
|   int8_t numWords; | ||||
|   int8_t reserved2; | ||||
|   int8_t version; | ||||
|   int8_t reserved3; | ||||
| } rxm_sfrbx_msg; | ||||
| 
 | ||||
| // Extra data count is in numWords
 | ||||
| typedef struct __attribute__((packed)) { | ||||
|   uint32_t dwrd; | ||||
| } rxm_sfrbx_msg_extra; | ||||
| 
 | ||||
| namespace ublox { | ||||
|   // protocol constants
 | ||||
|   const uint8_t PREAMBLE1 = 0xb5; | ||||
|   const uint8_t PREAMBLE2 = 0x62; | ||||
| 
 | ||||
|   // message classes
 | ||||
|   const uint8_t CLASS_NAV = 0x01; | ||||
|   const uint8_t CLASS_RXM = 0x02; | ||||
| 
 | ||||
|   // NAV messages
 | ||||
|   const uint8_t MSG_NAV_PVT = 0x7; | ||||
| 
 | ||||
|   // RXM messages
 | ||||
|   const uint8_t MSG_RXM_RAW = 0x15; | ||||
|   const uint8_t MSG_RXM_SFRBX = 0x13; | ||||
| 
 | ||||
|   const int UBLOX_HEADER_SIZE = 6; | ||||
|   const int UBLOX_CHECKSUM_SIZE = 2; | ||||
|   const int UBLOX_MAX_MSG_SIZE = 65536; | ||||
| 
 | ||||
|   typedef std::map<uint8_t, std::vector<uint32_t>> subframes_map; | ||||
| 
 | ||||
|   class UbloxMsgParser { | ||||
|     public: | ||||
| 
 | ||||
|       UbloxMsgParser(); | ||||
|       kj::Array<capnp::word> gen_solution(); | ||||
|       kj::Array<capnp::word> gen_raw(); | ||||
| 
 | ||||
|       kj::Array<capnp::word> gen_nav_data(); | ||||
|       bool add_data(const uint8_t *incoming_data, uint32_t incoming_data_len, size_t &bytes_consumed); | ||||
|       inline void reset() {bytes_in_parse_buf = 0;} | ||||
|       inline uint8_t msg_class() { | ||||
|         return msg_parse_buf[2]; | ||||
|       } | ||||
| 
 | ||||
|       inline uint8_t msg_id() { | ||||
|         return msg_parse_buf[3]; | ||||
|       } | ||||
|       inline int needed_bytes(); | ||||
| 
 | ||||
|       void hexdump(uint8_t *d, int l) { | ||||
|         for (int i = 0; i < l; i++) { | ||||
|           if (i%0x10 == 0 && i != 0) printf("\n"); | ||||
|           printf("%02X ", d[i]); | ||||
|         } | ||||
|         printf("\n"); | ||||
|       } | ||||
|     private: | ||||
|       inline bool valid_cheksum(); | ||||
|       inline bool valid(); | ||||
|       inline bool valid_so_far(); | ||||
| 
 | ||||
|       uint8_t msg_parse_buf[UBLOX_HEADER_SIZE + UBLOX_MAX_MSG_SIZE]; | ||||
|       int bytes_in_parse_buf; | ||||
|       std::map<uint8_t, std::map<uint8_t, subframes_map>> nav_frame_buffer; | ||||
|   }; | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| typedef Message * (*poll_ubloxraw_msg_func)(Poller *poller); | ||||
| typedef int (*send_gps_event_func)(PubSocket *s, const void *buf, size_t len); | ||||
| int ubloxd_main(poll_ubloxraw_msg_func poll_func, send_gps_event_func send_func); | ||||
| @ -0,0 +1,47 @@ | ||||
| #include <stdio.h> | ||||
| #include <stdlib.h> | ||||
| #include <string.h> | ||||
| #include <signal.h> | ||||
| #include <unistd.h> | ||||
| #include <sched.h> | ||||
| #include <sys/time.h> | ||||
| #include <sys/cdefs.h> | ||||
| #include <sys/types.h> | ||||
| #include <sys/time.h> | ||||
| #include <assert.h> | ||||
| #include <math.h> | ||||
| #include <ctime> | ||||
| #include <chrono> | ||||
| #include <map> | ||||
| #include <vector> | ||||
| 
 | ||||
| #include "messaging.hpp" | ||||
| #include <capnp/serialize.h> | ||||
| #include "cereal/gen/cpp/log.capnp.h" | ||||
| 
 | ||||
| #include "common/params.h" | ||||
| #include "common/swaglog.h" | ||||
| #include "common/timing.h" | ||||
| 
 | ||||
| #include "ublox_msg.h" | ||||
| 
 | ||||
| const long ZMQ_POLL_TIMEOUT = 1000; // In miliseconds
 | ||||
| 
 | ||||
| Message * poll_ubloxraw_msg(Poller * poller) { | ||||
|   auto p = poller->poll(ZMQ_POLL_TIMEOUT); | ||||
| 
 | ||||
|   if (p.size()) { | ||||
|     return p[0]->receive(); | ||||
|   } else { | ||||
|     return NULL; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| int send_gps_event(PubSocket *s, const void *buf, size_t len) { | ||||
|   return s->send((char*)buf, len); | ||||
| } | ||||
| 
 | ||||
| int main() { | ||||
|   return ubloxd_main(poll_ubloxraw_msg, send_gps_event); | ||||
| } | ||||
| @ -0,0 +1,115 @@ | ||||
| #include <stdio.h> | ||||
| #include <stdlib.h> | ||||
| #include <string.h> | ||||
| #include <signal.h> | ||||
| #include <unistd.h> | ||||
| #include <sched.h> | ||||
| #include <sys/time.h> | ||||
| #include <sys/cdefs.h> | ||||
| #include <sys/types.h> | ||||
| #include <sys/time.h> | ||||
| #include <assert.h> | ||||
| #include <math.h> | ||||
| #include <ctime> | ||||
| #include <chrono> | ||||
| #include <map> | ||||
| #include <vector> | ||||
| 
 | ||||
| #include "messaging.hpp" | ||||
| #include <capnp/serialize.h> | ||||
| #include "cereal/gen/cpp/log.capnp.h" | ||||
| 
 | ||||
| #include "common/params.h" | ||||
| #include "common/swaglog.h" | ||||
| #include "common/timing.h" | ||||
| 
 | ||||
| #include "ublox_msg.h" | ||||
| 
 | ||||
| volatile sig_atomic_t do_exit = 0; // Flag for process exit on signal
 | ||||
| 
 | ||||
| void set_do_exit(int sig) { | ||||
|   do_exit = 1; | ||||
| } | ||||
| 
 | ||||
| using namespace ublox; | ||||
| 
 | ||||
| int ubloxd_main(poll_ubloxraw_msg_func poll_func, send_gps_event_func send_func) { | ||||
|   LOGW("starting ubloxd"); | ||||
|   signal(SIGINT, (sighandler_t) set_do_exit); | ||||
|   signal(SIGTERM, (sighandler_t) set_do_exit); | ||||
| 
 | ||||
|   UbloxMsgParser parser; | ||||
| 
 | ||||
|   Context * c = Context::create(); | ||||
|   PubSocket * gpsLocationExternal = PubSocket::create(c, "gpsLocationExternal"); | ||||
|   PubSocket * ubloxGnss = PubSocket::create(c, "ubloxGnss"); | ||||
|   SubSocket * ubloxRaw = SubSocket::create(c, "ubloxRaw"); | ||||
| 
 | ||||
|   assert(gpsLocationExternal != NULL); | ||||
|   assert(ubloxGnss != NULL); | ||||
|   assert(ubloxRaw != NULL); | ||||
| 
 | ||||
|   Poller * poller = Poller::create({ubloxRaw}); | ||||
| 
 | ||||
| 
 | ||||
|   while (!do_exit) { | ||||
|     Message * msg = poll_func(poller); | ||||
|     if (msg == NULL) continue; | ||||
| 
 | ||||
|     auto amsg = kj::heapArray<capnp::word>((msg->getSize() / sizeof(capnp::word)) + 1); | ||||
|     memcpy(amsg.begin(), msg->getData(), msg->getSize()); | ||||
| 
 | ||||
|     capnp::FlatArrayMessageReader cmsg(amsg); | ||||
|     cereal::Event::Reader event = cmsg.getRoot<cereal::Event>(); | ||||
| 
 | ||||
|     const uint8_t *data = event.getUbloxRaw().begin(); | ||||
|     size_t len = event.getUbloxRaw().size(); | ||||
|     size_t bytes_consumed = 0; | ||||
|     while(bytes_consumed < len && !do_exit) { | ||||
|       size_t bytes_consumed_this_time = 0U; | ||||
|       if(parser.add_data(data + bytes_consumed, (uint32_t)(len - bytes_consumed), bytes_consumed_this_time)) { | ||||
|         // New message available
 | ||||
|         if(parser.msg_class() == CLASS_NAV) { | ||||
|           if(parser.msg_id() == MSG_NAV_PVT) { | ||||
|             //LOGD("MSG_NAV_PVT");
 | ||||
|             auto words = parser.gen_solution(); | ||||
|             if(words.size() > 0) { | ||||
|               auto bytes = words.asBytes(); | ||||
|               send_func(gpsLocationExternal, bytes.begin(), bytes.size()); | ||||
|             } | ||||
|           } else | ||||
|             LOGW("Unknown nav msg id: 0x%02X", parser.msg_id()); | ||||
|         } else if(parser.msg_class() == CLASS_RXM) { | ||||
|           if(parser.msg_id() == MSG_RXM_RAW) { | ||||
|             //LOGD("MSG_RXM_RAW");
 | ||||
|             auto words = parser.gen_raw(); | ||||
|             if(words.size() > 0) { | ||||
|               auto bytes = words.asBytes(); | ||||
|               send_func(ubloxGnss, bytes.begin(), bytes.size()); | ||||
|             } | ||||
|           } else if(parser.msg_id() == MSG_RXM_SFRBX) { | ||||
|             //LOGD("MSG_RXM_SFRBX");
 | ||||
|             auto words = parser.gen_nav_data(); | ||||
|             if(words.size() > 0) { | ||||
|               auto bytes = words.asBytes(); | ||||
|               send_func(ubloxGnss, bytes.begin(), bytes.size()); | ||||
|             } | ||||
|           } else | ||||
|             LOGW("Unknown rxm msg id: 0x%02X", parser.msg_id()); | ||||
|         } else | ||||
|           LOGW("Unknown msg class: 0x%02X", parser.msg_class()); | ||||
|         parser.reset(); | ||||
|       } | ||||
|       bytes_consumed += bytes_consumed_this_time; | ||||
|     } | ||||
|     delete msg; | ||||
|   } | ||||
| 
 | ||||
|   delete poller; | ||||
|   delete ubloxRaw; | ||||
|   delete ubloxGnss; | ||||
|   delete gpsLocationExternal; | ||||
|   delete c; | ||||
| 
 | ||||
|   return 0; | ||||
| } | ||||
| @ -0,0 +1,102 @@ | ||||
| #include <stdio.h> | ||||
| #include <stdlib.h> | ||||
| #include <string.h> | ||||
| #include <signal.h> | ||||
| #include <unistd.h> | ||||
| #include <sched.h> | ||||
| #include <sys/time.h> | ||||
| #include <sys/cdefs.h> | ||||
| #include <sys/types.h> | ||||
| #include <sys/time.h> | ||||
| #include <assert.h> | ||||
| #include <math.h> | ||||
| #include <ctime> | ||||
| #include <chrono> | ||||
| #include <map> | ||||
| #include <vector> | ||||
| #include <iostream> | ||||
| 
 | ||||
| #include "messaging.hpp" | ||||
| #include "impl_zmq.hpp" | ||||
| #include <capnp/serialize.h> | ||||
| #include "cereal/gen/cpp/log.capnp.h" | ||||
| 
 | ||||
| #include "common/params.h" | ||||
| #include "common/swaglog.h" | ||||
| #include "common/timing.h" | ||||
| #include "common/util.h" | ||||
| #include "ublox_msg.h" | ||||
| 
 | ||||
| using namespace ublox; | ||||
| extern volatile sig_atomic_t do_exit; | ||||
| 
 | ||||
| void write_file(std::string fpath, uint8_t *data, int len) { | ||||
|   FILE* f = fopen(fpath.c_str(), "wb"); | ||||
|   if (!f) { | ||||
|     std::cout << "Open " << fpath << " failed" << std::endl; | ||||
|     return; | ||||
|   } | ||||
|   fwrite(data, len, 1, f); | ||||
|   fclose(f); | ||||
| } | ||||
| 
 | ||||
| static size_t len = 0U; | ||||
| static size_t consumed = 0U; | ||||
| static uint8_t *data = NULL; | ||||
| static int save_idx = 0; | ||||
| static std::string prefix; | ||||
| 
 | ||||
| Message * poll_ubloxraw_msg(Poller * poller) { | ||||
|   assert(poller); | ||||
| 
 | ||||
|   size_t consuming  = min(len - consumed, 128); | ||||
|   if(consumed < len) { | ||||
|     // create message
 | ||||
|     capnp::MallocMessageBuilder msg_builder; | ||||
|     cereal::Event::Builder event = msg_builder.initRoot<cereal::Event>(); | ||||
|     event.setLogMonoTime(nanos_since_boot()); | ||||
| 
 | ||||
|     auto ublox_raw = event.initUbloxRaw(consuming); | ||||
|     memcpy(ublox_raw.begin(), (void *)(data + consumed), consuming); | ||||
| 
 | ||||
|     auto words = capnp::messageToFlatArray(msg_builder); | ||||
|     auto bytes = words.asBytes(); | ||||
| 
 | ||||
|     Message * msg = new ZMQMessage(); | ||||
|     msg->init((char*)bytes.begin(), bytes.size()); | ||||
|     consumed += consuming; | ||||
|     return msg; | ||||
|   } else { | ||||
|     do_exit = 1; | ||||
|     return NULL; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| int send_gps_event(PubSocket *s, const void *buf, size_t len) { | ||||
|   assert(s); | ||||
|   write_file(prefix + "/" + std::to_string(save_idx), (uint8_t *)buf, len); | ||||
|   save_idx++; | ||||
|   return len; | ||||
| } | ||||
| 
 | ||||
| int main(int argc, char** argv) { | ||||
|   if(argc < 3) { | ||||
|     printf("Format: ubloxd_test stream_file_path save_prefix\n"); | ||||
|     return 0; | ||||
|   } | ||||
|   // Parse 11360 msgs, generate 9452 events
 | ||||
|   data = (uint8_t *)read_file(argv[1], &len); | ||||
|   if(data == NULL) { | ||||
|     LOGE("Read file %s failed\n", argv[1]); | ||||
|     return -1; | ||||
|   } | ||||
|   prefix = argv[2]; | ||||
|   ubloxd_main(poll_ubloxraw_msg, send_gps_event); | ||||
|   free(data); | ||||
|   printf("Generated %d cereal events\n", save_idx); | ||||
|   if(save_idx != 9452) { | ||||
|     printf("Event count error: %d\n", save_idx); | ||||
|     return -1; | ||||
|   } | ||||
|   return 0; | ||||
| } | ||||
					Loading…
					
					
				
		Reference in new issue