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