parent
5f2a5e5bad
commit
fcf8efb826
46 changed files with 6151 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() |
Binary file not shown.
@ -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) |
Binary file not shown.
@ -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