mirror of
https://github.com/gnss-sdr/gnss-sdr
synced 2025-04-13 06:13:17 +00:00
Added NMEA 2.1 protocol for PVT dump.
This is an experimental release that only supports FILE dump operations. git-svn-id: https://svn.code.sf.net/p/gnss-sdr/code/trunk@235 64b25241-fba3-4117-9849-534c7e92360d
This commit is contained in:
parent
45d7220dae
commit
598512529f
@ -66,10 +66,19 @@ gps_l1_ca_pvt_cc::gps_l1_ca_pvt_cc(unsigned int nchannels, gr_msg_queue_sptr que
|
||||
d_dump_filename = dump_filename;
|
||||
std::string dump_ls_pvt_filename;
|
||||
dump_ls_pvt_filename=dump_filename;
|
||||
|
||||
//initialize kml_printer
|
||||
std::string kml_dump_filename;
|
||||
kml_dump_filename = d_dump_filename;
|
||||
kml_dump_filename.append(".kml");
|
||||
d_kml_dump.set_headers(kml_dump_filename);
|
||||
|
||||
//initialize nmea_printer
|
||||
std::string nmea_dump_filename;
|
||||
nmea_dump_filename = d_dump_filename;
|
||||
nmea_dump_filename.append(".nmea");
|
||||
d_nmea_printer=new Nmea_Printer(nmea_dump_filename);
|
||||
|
||||
d_dump_filename.append("_raw.dat");
|
||||
dump_ls_pvt_filename.append("_ls_pvt.dat");
|
||||
d_averaging_depth = averaging_depth;
|
||||
@ -117,6 +126,7 @@ gps_l1_ca_pvt_cc::~gps_l1_ca_pvt_cc()
|
||||
d_kml_dump.close_file();
|
||||
delete d_ls_pvt;
|
||||
delete rp;
|
||||
delete d_nmea_printer;
|
||||
}
|
||||
|
||||
|
||||
@ -204,6 +214,8 @@ int gps_l1_ca_pvt_cc::general_work (int noutput_items, gr_vector_int &ninput_ite
|
||||
if (d_ls_pvt->get_PVT(gnss_pseudoranges_map,d_tx_time,d_flag_averaging) == true)
|
||||
{
|
||||
d_kml_dump.print_position(d_ls_pvt, d_flag_averaging);
|
||||
d_nmea_printer->Print_Nmea_Line(d_ls_pvt, d_flag_averaging);
|
||||
|
||||
if (!b_rinex_header_writen) // & we have utc data in nav message!
|
||||
{
|
||||
rp->rinex_nav_header(rp->navFile, d_last_nav_msg);
|
||||
|
@ -38,6 +38,7 @@
|
||||
#include <boost/thread/thread.hpp>
|
||||
#include "concurrent_queue.h"
|
||||
#include "gps_navigation_message.h"
|
||||
#include "nmea_printer.h"
|
||||
#include "kml_printer.h"
|
||||
#include "rinex_printer.h"
|
||||
#include "gps_l1_ca_ls_pvt.h"
|
||||
@ -81,6 +82,8 @@ private:
|
||||
|
||||
Kml_Printer d_kml_dump;
|
||||
|
||||
Nmea_Printer *d_nmea_printer;
|
||||
|
||||
concurrent_queue<Gps_Navigation_Message> *d_nav_queue; // Navigation ephemeris queue
|
||||
Gps_Navigation_Message d_last_nav_msg; // Last navigation message
|
||||
|
||||
|
@ -188,7 +188,10 @@ arma::vec gps_l1_ca_ls_pvt::leastSquarePos(arma::mat satpos, arma::vec obs, arma
|
||||
//--- Correct satellite position (do to earth rotation) --------
|
||||
|
||||
Rot_X = e_r_corr(traveltime, X.col(i)); //armadillo
|
||||
//--- Find the elevation angel of the satellite ----------------
|
||||
//--- Find the elevation angle of the satellite ----------------
|
||||
|
||||
topocent(&d_visible_satellites_Az[i],&d_visible_satellites_El[i],&d_visible_satellites_Distance[i],pos.subvec(0,2), Rot_X - pos.subvec(0,2));
|
||||
|
||||
//[az(i), el(i), dist] = topocent(pos(1:3, :), Rot_X - pos(1:3, :));
|
||||
|
||||
}
|
||||
@ -221,11 +224,13 @@ arma::vec gps_l1_ca_ls_pvt::leastSquarePos(arma::mat satpos, arma::vec obs, arma
|
||||
//-- compute the Dilution Of Precision values
|
||||
arma::mat Q;
|
||||
Q = arma::inv(arma::htrans(A)*A);
|
||||
//std::cout<<Q<<std::endl;
|
||||
|
||||
d_GDOP = sqrt(arma::trace(Q)); // GDOP
|
||||
d_PDOP = sqrt(Q(1,1) + Q(2,2) + Q(3,3)); // PDOP
|
||||
d_HDOP = sqrt(Q(1,1) + Q(2,2)); // HDOP
|
||||
d_VDOP = sqrt(Q(3,3)); // VDOP
|
||||
d_TDOP = sqrt(Q(4,4)); // TDOP
|
||||
d_PDOP = sqrt(Q(0,0) + Q(1,1) + Q(2,2)); // PDOP
|
||||
d_HDOP = sqrt(Q(0,0) + Q(1,1)); // HDOP
|
||||
d_VDOP = sqrt(Q(2,2)); // VDOP
|
||||
d_TDOP = sqrt(Q(3,3)); // TDOP
|
||||
}catch(std::exception e)
|
||||
{
|
||||
d_GDOP = -1;
|
||||
@ -250,6 +255,8 @@ bool gps_l1_ca_ls_pvt::get_PVT(std::map<int,Gnss_Synchro> gnss_pseudoranges_map,
|
||||
double GPS_corrected_time = 0;
|
||||
double utc = 0;
|
||||
|
||||
d_flag_averaging=flag_averaging;
|
||||
|
||||
int valid_obs=0; //valid observations counter
|
||||
for (int i=0; i<d_nchannels; i++)
|
||||
{
|
||||
@ -280,6 +287,8 @@ bool gps_l1_ca_ls_pvt::get_PVT(std::map<int,Gnss_Synchro> gnss_pseudoranges_map,
|
||||
DLOG(INFO) << "ECEF satellite SV ID=" << d_ephemeris[i].i_satellite_PRN <<" X=" << d_ephemeris[i].d_satpos_X
|
||||
<< " [m] Y=" << d_ephemeris[i].d_satpos_Y << " [m] Z=" << d_ephemeris[i].d_satpos_Z << " [m]" << std::endl;
|
||||
obs(i) = gnss_pseudoranges_iter->second.Pseudorange_m + d_ephemeris[i].d_satClkCorr*GPS_C_m_s;
|
||||
d_visible_satellites_IDs[valid_obs]=d_ephemeris[i].i_satellite_PRN;
|
||||
d_visible_satellites_CN0_dB[valid_obs]=gnss_pseudoranges_iter->second.CN0_dB_hz;
|
||||
valid_obs++;
|
||||
}
|
||||
else
|
||||
@ -296,7 +305,11 @@ bool gps_l1_ca_ls_pvt::get_PVT(std::map<int,Gnss_Synchro> gnss_pseudoranges_map,
|
||||
obs(i) = 1; // to avoid algorithm problems (divide by zero)
|
||||
}
|
||||
}
|
||||
|
||||
d_valid_observations = valid_obs;
|
||||
|
||||
DLOG(INFO) <<"PVT: valid observations="<<valid_obs<<std::endl;
|
||||
|
||||
if (valid_obs>=4)
|
||||
{
|
||||
arma::vec mypos;
|
||||
@ -471,59 +484,230 @@ void gps_l1_ca_ls_pvt::cart2geo(double X, double Y, double Z, int elipsoid_selec
|
||||
d_height_m = h;
|
||||
}
|
||||
|
||||
//void gps_l1_ca_ls_pvt::topocent(traveltime, X_sat)
|
||||
//{
|
||||
/*
|
||||
%function [Az, El, D] = topocent(X, dx)
|
||||
%TOPOCENT Transformation of vector dx into topocentric coordinate
|
||||
% system with origin at X.
|
||||
% Both parameters are 3 by 1 vectors.
|
||||
%
|
||||
%[Az, El, D] = topocent(X, dx);
|
||||
%
|
||||
% Inputs:
|
||||
% X - vector origin corrdinates (in ECEF system [X; Y; Z;])
|
||||
% dx - vector ([dX; dY; dZ;]).
|
||||
%
|
||||
% Outputs:
|
||||
% D - vector length. Units like units of the input
|
||||
% Az - azimuth from north positive clockwise, degrees
|
||||
% El - elevation angle, degrees
|
||||
void gps_l1_ca_ls_pvt::togeod(double *dphi, double *dlambda, double *h, double a, double finv, double X, double Y, double Z)
|
||||
{
|
||||
//function [dphi, dlambda, h] = togeod(a, finv, X, Y, Z)
|
||||
//%TOGEOD Subroutine to calculate geodetic coordinates latitude, longitude,
|
||||
//% height given Cartesian coordinates X,Y,Z, and reference ellipsoid
|
||||
//% values semi-major axis (a) and the inverse of flattening (finv).
|
||||
//%
|
||||
//%[dphi, dlambda, h] = togeod(a, finv, X, Y, Z);
|
||||
//%
|
||||
//% The units of linear parameters X,Y,Z,a must all agree (m,km,mi,ft,..etc)
|
||||
//% The output units of angular quantities will be in decimal degrees
|
||||
//% (15.5 degrees not 15 deg 30 min). The output units of h will be the
|
||||
//% same as the units of X,Y,Z,a.
|
||||
//%
|
||||
//% Inputs:
|
||||
//% a - semi-major axis of the reference ellipsoid
|
||||
//% finv - inverse of flattening of the reference ellipsoid
|
||||
//% X,Y,Z - Cartesian coordinates
|
||||
//%
|
||||
//% Outputs:
|
||||
//% dphi - latitude
|
||||
//% dlambda - longitude
|
||||
//% h - height above reference ellipsoid
|
||||
//
|
||||
//% Copyright (C) 1987 C. Goad, Columbus, Ohio
|
||||
//% Reprinted with permission of author, 1996
|
||||
//% Fortran code translated into MATLAB
|
||||
//% Kai Borre 03-30-96
|
||||
//%
|
||||
//% CVS record:
|
||||
//% $Id: togeod.m,v 1.1.1.1.2.4 2006/08/22 13:45:59 dpl Exp $
|
||||
//%==========================================================================
|
||||
//
|
||||
*h = 0;
|
||||
double tolsq = 1.e-10;
|
||||
int maxit=10;
|
||||
// compute radians-to-degree factor
|
||||
double rtd;
|
||||
rtd = 180/GPS_PI;
|
||||
|
||||
// compute square of eccentricity
|
||||
double esq;
|
||||
if (finv < 1.0E-20)
|
||||
{
|
||||
esq = 0;
|
||||
}else
|
||||
{
|
||||
esq = (2 - 1/finv) / finv;
|
||||
}
|
||||
|
||||
double oneesq;
|
||||
|
||||
oneesq = 1 - esq;
|
||||
|
||||
// first guess
|
||||
// P is distance from spin axis
|
||||
double P;
|
||||
P = sqrt(X*X+Y*Y);
|
||||
//direct calculation of longitude
|
||||
//
|
||||
if (P > 1.0E-20)
|
||||
{
|
||||
*dlambda = atan2(Y,X) * rtd;
|
||||
}else
|
||||
{
|
||||
*dlambda = 0;
|
||||
}
|
||||
|
||||
if (*dlambda < 0)
|
||||
{
|
||||
*dlambda = *dlambda + 360.0;
|
||||
}
|
||||
|
||||
// r is distance from origin (0,0,0)
|
||||
double r;
|
||||
r = sqrt(P*P + Z*Z);
|
||||
|
||||
double sinphi;
|
||||
|
||||
if (r > 1.0E-20)
|
||||
{
|
||||
sinphi = Z/r;
|
||||
} else
|
||||
{
|
||||
sinphi = 0;
|
||||
}
|
||||
|
||||
*dphi = asin(sinphi);
|
||||
|
||||
// initial value of height = distance from origin minus
|
||||
// approximate distance from origin to surface of ellipsoid
|
||||
if (r < 1.0E-20)
|
||||
{
|
||||
*h = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
*h = r - a*(1-sinphi*sinphi/finv);
|
||||
|
||||
|
||||
dtr = pi/180;
|
||||
// iterate
|
||||
double cosphi;
|
||||
double N_phi;
|
||||
double dP;
|
||||
double dZ;
|
||||
for (int i=0; i<maxit; i++)
|
||||
{
|
||||
sinphi = sin(*dphi);
|
||||
cosphi = cos(*dphi);
|
||||
|
||||
[phi, lambda, h] = togeod(6378137, 298.257223563, X(1), X(2), X(3));
|
||||
// compute radius of curvature in prime vertical direction
|
||||
N_phi = a/sqrt(1-esq*sinphi*sinphi);
|
||||
|
||||
cl = cos(lambda * dtr);
|
||||
sl = sin(lambda * dtr);
|
||||
cb = cos(phi * dtr);
|
||||
sb = sin(phi * dtr);
|
||||
// compute residuals in P and Z
|
||||
dP = P - (N_phi + (*h)) * cosphi;
|
||||
dZ = Z - (N_phi*oneesq + (*h)) * sinphi;
|
||||
//
|
||||
// update height and latitude
|
||||
*h = *h + (sinphi*dZ + cosphi*dP);
|
||||
*dphi = *dphi + (cosphi*dZ - sinphi*dP)/(N_phi + (*h));
|
||||
|
||||
F = [-sl -sb*cl cb*cl;
|
||||
cl -sb*sl cb*sl;
|
||||
0 cb sb];
|
||||
// test for convergence
|
||||
if ((dP*dP + dZ*dZ) < tolsq)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
local_vector = F' * dx;
|
||||
E = local_vector(1);
|
||||
N = local_vector(2);
|
||||
U = local_vector(3);
|
||||
// Not Converged--Warn user
|
||||
// if (i == (maxit-1))
|
||||
// fprintf([' Problem in TOGEOD, did not converge in %2.0f',...
|
||||
// ' iterations\n'], i);
|
||||
// end
|
||||
}
|
||||
//
|
||||
*dphi = (*dphi) * rtd;
|
||||
|
||||
hor_dis = sqrt(E^2 + N^2);
|
||||
}
|
||||
void gps_l1_ca_ls_pvt::topocent(double *Az, double *El, double *D, arma::vec x, arma::vec x_sat)
|
||||
{
|
||||
|
||||
if hor_dis < 1.e-20
|
||||
Az = 0;
|
||||
El = 90;
|
||||
else
|
||||
Az = atan2(E, N)/dtr;
|
||||
El = atan2(U, hor_dis)/dtr;
|
||||
end
|
||||
//%function [Az, El, D] = topocent(X, dx)
|
||||
//%TOPOCENT Transformation of vector dx into topocentric coordinate
|
||||
//% system with origin at X.
|
||||
//% Both parameters are 3 by 1 vectors.
|
||||
//%
|
||||
//%[Az, El, D] = topocent(X, dx);
|
||||
//%
|
||||
//% Inputs:
|
||||
//% X - vector origin corrdinates (in ECEF system [X; Y; Z;])
|
||||
//% dx - vector ([dX; dY; dZ;]).
|
||||
//%
|
||||
//% Outputs:
|
||||
//% D - vector length. Units like units of the input
|
||||
//% Az - azimuth from north positive clockwise, degrees
|
||||
//% El - elevation angle, degrees
|
||||
|
||||
if Az < 0
|
||||
Az = Az + 360;
|
||||
end
|
||||
double dtr;
|
||||
double lambda;
|
||||
double phi;
|
||||
double cl;
|
||||
double sl;
|
||||
double cb;
|
||||
double sb;
|
||||
|
||||
D = sqrt(dx(1)^2 + dx(2)^2 + dx(3)^2);
|
||||
%%%%%%%%% end topocent.m %%%%%%%%%
|
||||
*/
|
||||
//}
|
||||
double h;
|
||||
|
||||
dtr = GPS_PI/180.0;
|
||||
|
||||
//[phi, lambda, h] = togeod(6378137, 298.257223563, X(1), X(2), X(3));
|
||||
|
||||
togeod(&phi, &lambda, &h,6378137.0, 298.257223563, x(0), x(1), x(2));
|
||||
|
||||
cl = cos(lambda * dtr);
|
||||
sl = sin(lambda * dtr);
|
||||
cb = cos(phi * dtr);
|
||||
sb = sin(phi * dtr);
|
||||
|
||||
arma::mat F=arma::zeros(3,3);
|
||||
|
||||
F(0,0)=-sl;
|
||||
F(0,1)=-sb*cl;
|
||||
F(0,2)=cb*cl;
|
||||
|
||||
F(1,0)=cl;
|
||||
F(1,1)=-sb*sl;
|
||||
F(1,2)=cb*sl;
|
||||
|
||||
F(2,0)=0;
|
||||
F(2,1)=cb;
|
||||
F(2,2)=sb;
|
||||
|
||||
arma::vec local_vector;
|
||||
|
||||
local_vector = arma::htrans(F) * x_sat;
|
||||
|
||||
double E;
|
||||
double N;
|
||||
double U;
|
||||
|
||||
E = local_vector(0);
|
||||
N = local_vector(1);
|
||||
U = local_vector(2);
|
||||
|
||||
double hor_dis;
|
||||
|
||||
hor_dis = sqrt(E*E + N*N);
|
||||
|
||||
if (hor_dis < 1.0E-20)
|
||||
{
|
||||
*Az = 0;
|
||||
*El = 90;
|
||||
}
|
||||
else
|
||||
{
|
||||
*Az = atan2(E, N)/dtr;
|
||||
*El = atan2(U, hor_dis)/dtr;
|
||||
}
|
||||
|
||||
if (*Az < 0)
|
||||
{
|
||||
*Az = *Az + 360.0;
|
||||
}
|
||||
|
||||
|
||||
*D = sqrt(x_sat(0)*x_sat(0) + x_sat(1)*x_sat(1) + x_sat(2)*x_sat(2));
|
||||
|
||||
}
|
||||
|
@ -48,6 +48,8 @@
|
||||
|
||||
#include "gnss_synchro.h"
|
||||
|
||||
#define PVT_MAX_CHANNELS 24
|
||||
|
||||
/*!
|
||||
* \brief This class implements a simple PVT Least Squares solution
|
||||
*/
|
||||
@ -56,9 +58,17 @@ class gps_l1_ca_ls_pvt
|
||||
private:
|
||||
arma::vec leastSquarePos(arma::mat satpos, arma::vec obs, arma::mat w);
|
||||
arma::vec e_r_corr(double traveltime, arma::vec X_sat);
|
||||
//void topocent();
|
||||
void topocent(double *Az, double *El, double *D, arma::vec x, arma::vec x_sat);
|
||||
void togeod(double *dphi, double *dlambda, double *h, double a, double finv, double X, double Y, double Z);
|
||||
public:
|
||||
int d_nchannels; //! Number of available channels for positioning
|
||||
int d_valid_observations; //! Number of valid pseudorrange observations (valid satellites)
|
||||
int d_visible_satellites_IDs[PVT_MAX_CHANNELS]; //! Array with the IDs of the valid satellites
|
||||
double d_visible_satellites_El[PVT_MAX_CHANNELS]; //! Array with the LOS Elevation of the valid satellites
|
||||
double d_visible_satellites_Az[PVT_MAX_CHANNELS]; //! Array with the LOS Azimuth of the valid satellites
|
||||
double d_visible_satellites_Distance[PVT_MAX_CHANNELS]; //! Array with the LOS Distance of the valid satellites
|
||||
double d_visible_satellites_CN0_dB[PVT_MAX_CHANNELS]; //! Array with the IDs of the valid satellites
|
||||
|
||||
Gps_Navigation_Message* d_ephemeris;
|
||||
double d_GPS_current_time;
|
||||
boost::posix_time::ptime d_position_UTC_time;
|
||||
@ -92,6 +102,8 @@ public:
|
||||
double d_TDOP;
|
||||
|
||||
bool d_flag_dump_enabled;
|
||||
bool d_flag_averaging;
|
||||
|
||||
std::string d_dump_filename;
|
||||
std::ofstream d_dump_file;
|
||||
|
||||
|
@ -2,4 +2,5 @@ project : build-dir ../../../../build ;
|
||||
|
||||
obj rinex_printer : rinex_printer.cc ;
|
||||
obj gps_l1_ca_ls_pvt : gps_l1_ca_ls_pvt.cc ;
|
||||
obj kml_printer : kml_printer.cc ;
|
||||
obj kml_printer : kml_printer.cc ;
|
||||
obj nmea_printer : nmea_printer.cc ;
|
617
src/algorithms/PVT/libs/nmea_printer.cc
Normal file
617
src/algorithms/PVT/libs/nmea_printer.cc
Normal file
@ -0,0 +1,617 @@
|
||||
/*!
|
||||
* \file kml_printer.cc
|
||||
* \brief Implementation of a NMEA 2.1 printer for GNSS-SDR
|
||||
* This class provides a implementation of a subset of the NMEA-0183 standard for interfacing
|
||||
* marine electronic devices as defined by the National Marine Electronics Association (NMEA).
|
||||
* See http://www.nmea.org/ for the NMEA 183 standard
|
||||
*
|
||||
* \author Javier Arribas, 2012. jarribas(at)cttc.es
|
||||
*
|
||||
*
|
||||
* -------------------------------------------------------------------------
|
||||
*
|
||||
* Copyright (C) 2010-2012 (see AUTHORS file for a list of contributors)
|
||||
*
|
||||
* GNSS-SDR is a software defined Global Navigation
|
||||
* Satellite Systems receiver
|
||||
*
|
||||
* This file is part of GNSS-SDR.
|
||||
*
|
||||
* GNSS-SDR is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* at your option) any later version.
|
||||
*
|
||||
* GNSS-SDR is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with GNSS-SDR. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* -------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
#include <glog/log_severity.h>
|
||||
#include <glog/logging.h>
|
||||
#include <gflags/gflags.h>
|
||||
#include "boost/date_time/posix_time/posix_time.hpp"
|
||||
#include "GPS_L1_CA.h"
|
||||
#include "nmea_printer.h"
|
||||
|
||||
using google::LogMessage;
|
||||
|
||||
//DEFINE_string(NMEA_version, "2.1", "Specifies the NMEA version (2.1)");
|
||||
|
||||
Nmea_Printer::Nmea_Printer(std::string filename)
|
||||
{
|
||||
nmea_filename=filename;
|
||||
nmea_file_descriptor.open(nmea_filename.c_str(), std::ios::out);
|
||||
if (nmea_file_descriptor.is_open())
|
||||
{
|
||||
DLOG(INFO) << "NMEA printer writing on " << nmea_filename.c_str();
|
||||
}
|
||||
}
|
||||
|
||||
Nmea_Printer::~Nmea_Printer()
|
||||
{
|
||||
if (nmea_file_descriptor.is_open())
|
||||
{
|
||||
nmea_file_descriptor.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool Nmea_Printer::Print_Nmea_Line(gps_l1_ca_ls_pvt* pvt_data, bool print_average_values)
|
||||
{
|
||||
|
||||
// set the new PVT data
|
||||
d_PVT_data=pvt_data;
|
||||
|
||||
try{
|
||||
//GPRMC
|
||||
nmea_file_descriptor<<get_GPRMC();
|
||||
//GPGGA (Global Positioning System Fixed Data)
|
||||
nmea_file_descriptor<<get_GPGGA();
|
||||
//GPGSA
|
||||
nmea_file_descriptor<<get_GPGSA();
|
||||
//GPGSV
|
||||
nmea_file_descriptor<<get_GPGSV();
|
||||
|
||||
}catch(std::exception ex)
|
||||
{
|
||||
DLOG(INFO) << "NMEA printer can not write on output file" << nmea_filename.c_str();;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
char Nmea_Printer::checkSum(std::string sentence) {
|
||||
char check = 0;
|
||||
// iterate over the string, XOR each byte with the total sum:
|
||||
for (unsigned int c = 0; c < sentence.length(); c++) {
|
||||
check = char(check ^ sentence.at(c));
|
||||
}
|
||||
// return the result
|
||||
return check;
|
||||
}
|
||||
|
||||
std::string Nmea_Printer::latitude_to_hm(double lat)
|
||||
{
|
||||
bool north;
|
||||
if (lat < 0.0)
|
||||
{
|
||||
north = false;
|
||||
lat = -lat ;
|
||||
}else{
|
||||
north = true;
|
||||
}
|
||||
|
||||
int deg = (int)lat;
|
||||
double mins = lat - (double)deg;
|
||||
mins *= 60.0 ;
|
||||
std::ostringstream out_string;
|
||||
out_string.setf(std::ios::fixed, std::ios::floatfield);
|
||||
out_string.fill('0');
|
||||
out_string.width(2);
|
||||
out_string << deg;
|
||||
out_string.width(6);
|
||||
out_string.precision(4);
|
||||
out_string<<mins;
|
||||
|
||||
if (north==true)
|
||||
{
|
||||
out_string << ",N";
|
||||
}else{
|
||||
out_string << ",S";
|
||||
}
|
||||
return out_string.str();
|
||||
}
|
||||
|
||||
std::string Nmea_Printer::longitude_to_hm(double longitude)
|
||||
{
|
||||
bool east;
|
||||
if (longitude < 0.0)
|
||||
{
|
||||
east = false;
|
||||
longitude = -longitude ;
|
||||
}else{
|
||||
east=true;
|
||||
}
|
||||
|
||||
int deg = (int)longitude;
|
||||
double mins = longitude - (double)deg;
|
||||
mins *= 60.0 ;
|
||||
std::ostringstream out_string;
|
||||
out_string.setf(std::ios::fixed, std::ios::floatfield);
|
||||
out_string.width(3);
|
||||
out_string.fill('0');
|
||||
out_string << deg;
|
||||
out_string.width(6);
|
||||
out_string.precision(4);
|
||||
out_string<<mins;
|
||||
|
||||
if (east==true)
|
||||
{
|
||||
out_string << ",E";
|
||||
}else{
|
||||
out_string << ",W";
|
||||
}
|
||||
|
||||
return out_string.str();
|
||||
}
|
||||
|
||||
|
||||
|
||||
std::string Nmea_Printer::get_UTC_NMEA_time(boost::posix_time::ptime d_position_UTC_time)
|
||||
{
|
||||
//UTC Time: hhmmss.sss
|
||||
std::stringstream sentence_str;
|
||||
|
||||
boost::posix_time::time_duration td=d_position_UTC_time.time_of_day();
|
||||
int utc_hours;
|
||||
int utc_mins;
|
||||
int utc_seconds;
|
||||
int utc_milliseconds;
|
||||
|
||||
utc_hours=td.hours();
|
||||
utc_mins=td.minutes();
|
||||
utc_seconds=td.seconds();
|
||||
utc_milliseconds=td.total_milliseconds()-td.total_seconds()*1000;
|
||||
|
||||
if (utc_hours < 10) sentence_str << "0"; // two digits for hours
|
||||
sentence_str << utc_hours;
|
||||
|
||||
if (utc_mins < 10) sentence_str << "0"; // two digits for minutes
|
||||
sentence_str << utc_mins;
|
||||
|
||||
if (utc_seconds < 10) sentence_str << "0"; // two digits for seconds
|
||||
sentence_str << utc_seconds;
|
||||
|
||||
if (utc_milliseconds < 10)
|
||||
{
|
||||
sentence_str << ".00"; // three digits for ms
|
||||
sentence_str << utc_milliseconds;
|
||||
}
|
||||
else if (utc_milliseconds < 100)
|
||||
{
|
||||
sentence_str << ".0"; // three digits for ms
|
||||
sentence_str << utc_milliseconds;
|
||||
}else
|
||||
{
|
||||
sentence_str << "."; // three digits for ms
|
||||
sentence_str << utc_milliseconds;
|
||||
}
|
||||
return sentence_str.str();
|
||||
}
|
||||
std::string Nmea_Printer::get_GPRMC()
|
||||
{
|
||||
// Sample -> $GPRMC,161229.487,A,3723.2475,N,12158.3416,W,0.13,309.62,120598,*10
|
||||
|
||||
bool valid_fix=d_PVT_data->b_valid_position;
|
||||
|
||||
// ToDo: Compute speed and course over ground
|
||||
double speed_over_ground_knots=0;
|
||||
double course_over_ground_deg=0;
|
||||
|
||||
//boost::posix_time::ptime d_position_UTC_time=boost::posix_time::microsec_clock::universal_time();
|
||||
|
||||
std::stringstream sentence_str;
|
||||
|
||||
//GPRMC (RMC-Recommended,Minimum Specific GNSS Data)
|
||||
std::string sentence_header;
|
||||
sentence_header="$GPRMC,";
|
||||
sentence_str<<sentence_header;
|
||||
|
||||
//UTC Time: hhmmss.sss
|
||||
sentence_str<<get_UTC_NMEA_time(d_PVT_data->d_position_UTC_time);
|
||||
|
||||
//Status: A: data valid, V: data NOT valid
|
||||
|
||||
if (valid_fix==true)
|
||||
{
|
||||
sentence_str<<",A";
|
||||
}else{
|
||||
sentence_str<<",V";
|
||||
};
|
||||
|
||||
if (d_PVT_data->d_flag_averaging==true)
|
||||
{
|
||||
// Latitude ddmm.mmmm,(N or S)
|
||||
|
||||
sentence_str<<","<<latitude_to_hm(d_PVT_data->d_avg_latitude_d);
|
||||
|
||||
// longitude dddmm.mmmm,(E or W)
|
||||
|
||||
sentence_str<<","<<longitude_to_hm(d_PVT_data->d_avg_longitude_d);
|
||||
}else{
|
||||
// Latitude ddmm.mmmm,(N or S)
|
||||
|
||||
sentence_str<<","<<latitude_to_hm(d_PVT_data->d_latitude_d);
|
||||
|
||||
// longitude dddmm.mmmm,(E or W)
|
||||
|
||||
sentence_str<<","<<longitude_to_hm(d_PVT_data->d_longitude_d);
|
||||
}
|
||||
|
||||
//Speed over ground (knots)
|
||||
sentence_str<<",";
|
||||
sentence_str.setf(std::ios::fixed, std::ios::floatfield);
|
||||
sentence_str.precision(2);
|
||||
sentence_str << speed_over_ground_knots;
|
||||
|
||||
//course over ground (degrees)
|
||||
sentence_str<<",";
|
||||
sentence_str.setf(std::ios::fixed, std::ios::floatfield);
|
||||
sentence_str.precision(2);
|
||||
sentence_str << course_over_ground_deg;
|
||||
|
||||
// Date ddmmyy
|
||||
|
||||
boost::gregorian::date sentence_date = d_PVT_data->d_position_UTC_time.date();
|
||||
unsigned int year=sentence_date.year();
|
||||
unsigned int day=sentence_date.day();
|
||||
unsigned int month=sentence_date.month();
|
||||
|
||||
sentence_str<<",";
|
||||
sentence_str.width(2);
|
||||
sentence_str.fill('0');
|
||||
sentence_str<<day;
|
||||
sentence_str.width(2);
|
||||
sentence_str.fill('0');
|
||||
sentence_str<<month;
|
||||
|
||||
std::stringstream year_strs;
|
||||
year_strs<<std::dec<<year;
|
||||
sentence_str<<std::dec<<year_strs.str().substr(2);
|
||||
|
||||
//Magnetic Variation (degrees)
|
||||
// ToDo: Implement magnetic compass
|
||||
sentence_str<<",";
|
||||
|
||||
//Magnetic Variation (E or W)
|
||||
// ToDo: Implement magnetic compass
|
||||
sentence_str<<",";
|
||||
|
||||
// Checksum
|
||||
|
||||
char checksum;
|
||||
std::string tmpstr;
|
||||
tmpstr=sentence_str.str();
|
||||
|
||||
checksum = checkSum(tmpstr.substr(1));
|
||||
sentence_str<<"*";
|
||||
sentence_str.width(2);
|
||||
sentence_str.fill('0');
|
||||
sentence_str<<std::hex<<(int)checksum;
|
||||
|
||||
// end NMEA sentence
|
||||
sentence_str<<"\r\n";
|
||||
|
||||
return sentence_str.str();
|
||||
|
||||
}
|
||||
|
||||
std::string Nmea_Printer::get_GPGSA()
|
||||
{
|
||||
//$GPGSA,A,3,07,02,26,27,09,04,15, , , , , ,1.8,1.0,1.5*33
|
||||
// GSA-GNSS DOP and Active Satellites
|
||||
|
||||
bool valid_fix=d_PVT_data->b_valid_position;
|
||||
|
||||
int n_sats_used=d_PVT_data->d_valid_observations;
|
||||
|
||||
double pdop=d_PVT_data->d_PDOP;
|
||||
double hdop=d_PVT_data->d_HDOP;
|
||||
double vdop=d_PVT_data->d_VDOP;
|
||||
|
||||
|
||||
std::stringstream sentence_str;
|
||||
std::string sentence_header;
|
||||
sentence_header="$GPGSA,";
|
||||
sentence_str<<sentence_header;
|
||||
|
||||
// mode1:
|
||||
// (M) Manual-forced to operate in 2D or 3D mode
|
||||
// (A) Automatic-allowed to automatically switch 2D/3D
|
||||
std::string mode1="M";
|
||||
|
||||
sentence_str<<mode1;
|
||||
// mode2:
|
||||
// 1 fix not available
|
||||
// 2 fix 2D
|
||||
// 3 fix 3D
|
||||
if (valid_fix==true)
|
||||
{
|
||||
sentence_str<<",3";
|
||||
}else{
|
||||
sentence_str<<",1";
|
||||
};
|
||||
|
||||
// Used satellites
|
||||
for (int i=0; i<12;i++)
|
||||
{
|
||||
sentence_str<<",";
|
||||
if (i<n_sats_used)
|
||||
{
|
||||
sentence_str.width(2);
|
||||
sentence_str.fill('0');
|
||||
sentence_str<<d_PVT_data->d_visible_satellites_IDs[i];
|
||||
}
|
||||
}
|
||||
|
||||
// PDOP
|
||||
sentence_str<<",";
|
||||
sentence_str.setf(std::ios::fixed, std::ios::floatfield);
|
||||
sentence_str.width(2);
|
||||
sentence_str.precision(1);
|
||||
sentence_str.fill('0');
|
||||
sentence_str << pdop;
|
||||
//HDOP
|
||||
sentence_str<<",";
|
||||
sentence_str.setf(std::ios::fixed, std::ios::floatfield);
|
||||
sentence_str.width(2);
|
||||
sentence_str.precision(1);
|
||||
sentence_str.fill('0');
|
||||
sentence_str << hdop;
|
||||
|
||||
//VDOP
|
||||
sentence_str<<",";
|
||||
sentence_str.setf(std::ios::fixed, std::ios::floatfield);
|
||||
sentence_str.width(2);
|
||||
sentence_str.precision(1);
|
||||
sentence_str.fill('0');
|
||||
sentence_str << vdop;
|
||||
|
||||
// Checksum
|
||||
|
||||
char checksum;
|
||||
std::string tmpstr;
|
||||
tmpstr=sentence_str.str();
|
||||
|
||||
checksum = checkSum(tmpstr.substr(1));
|
||||
sentence_str<<"*";
|
||||
sentence_str.width(2);
|
||||
sentence_str.fill('0');
|
||||
sentence_str<<std::hex<<(int)checksum;
|
||||
|
||||
// end NMEA sentence
|
||||
sentence_str<<"\r\n";
|
||||
|
||||
return sentence_str.str();
|
||||
}
|
||||
|
||||
std::string Nmea_Printer::get_GPGSV()
|
||||
{
|
||||
// GSV-GNSS Satellites in View
|
||||
// Notice that NMEA 2.1 only supports 12 channels
|
||||
|
||||
int n_sats_used=d_PVT_data->d_valid_observations;
|
||||
|
||||
std::stringstream sentence_str;
|
||||
std::stringstream frame_str;
|
||||
std::string sentence_header;
|
||||
sentence_header="$GPGSV,";
|
||||
|
||||
char checksum;
|
||||
std::string tmpstr;
|
||||
// 1st step: How many GPGSV frames we need? (up to 3)
|
||||
// Each frame countains up to 4 satellites
|
||||
int n_frames;
|
||||
n_frames=std::ceil(((double)n_sats_used)/4.0);
|
||||
|
||||
// generate the frames
|
||||
int current_satellite=0;
|
||||
for (int i=1;i<(n_frames+1);i++)
|
||||
{
|
||||
|
||||
frame_str.str("");
|
||||
|
||||
frame_str<<sentence_header;
|
||||
|
||||
// number of messages
|
||||
frame_str<<n_frames;
|
||||
|
||||
// message number
|
||||
frame_str<<",";
|
||||
frame_str<<i;
|
||||
|
||||
// total number of satellites in view
|
||||
frame_str<<",";
|
||||
frame_str.width(2);
|
||||
frame_str.fill('0');
|
||||
frame_str<<std::dec<<n_sats_used;
|
||||
|
||||
//satellites info
|
||||
for (int j=0;j<4;j++)
|
||||
{
|
||||
// write satellite info
|
||||
frame_str<<",";
|
||||
frame_str.width(2);
|
||||
frame_str.fill('0');
|
||||
frame_str<<std::dec<<d_PVT_data->d_visible_satellites_IDs[current_satellite];
|
||||
|
||||
frame_str<<",";
|
||||
frame_str.width(2);
|
||||
frame_str.fill('0');
|
||||
frame_str<<std::dec<<(int)d_PVT_data->d_visible_satellites_El[current_satellite];
|
||||
|
||||
frame_str<<",";
|
||||
frame_str.width(3);
|
||||
frame_str.fill('0');
|
||||
frame_str<<std::dec<<(int)d_PVT_data->d_visible_satellites_Az[current_satellite];
|
||||
|
||||
frame_str<<",";
|
||||
frame_str.width(2);
|
||||
frame_str.fill('0');
|
||||
frame_str<<std::dec<<(int)d_PVT_data->d_visible_satellites_CN0_dB[current_satellite];
|
||||
|
||||
current_satellite++;
|
||||
|
||||
if (current_satellite==n_sats_used)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// frame checksum
|
||||
tmpstr=frame_str.str();
|
||||
checksum = checkSum(tmpstr.substr(1));
|
||||
frame_str<<"*";
|
||||
frame_str.width(2);
|
||||
frame_str.fill('0');
|
||||
frame_str<<std::hex<<(int)checksum;
|
||||
|
||||
// end NMEA sentence
|
||||
frame_str<<"\r\n";
|
||||
|
||||
//add frame to sentence
|
||||
sentence_str<<frame_str.str();
|
||||
}
|
||||
|
||||
return sentence_str.str();
|
||||
|
||||
//$GPGSV,2,1,07,07,79,048,42,02,51,062,43,26,36,256,42,27,27,138,42*71
|
||||
|
||||
}
|
||||
std::string Nmea_Printer::get_GPGGA()
|
||||
{
|
||||
//boost::posix_time::ptime d_position_UTC_time=boost::posix_time::microsec_clock::universal_time();
|
||||
|
||||
bool valid_fix=d_PVT_data->b_valid_position;
|
||||
|
||||
int n_channels=d_PVT_data->d_valid_observations;//d_nchannels
|
||||
double hdop=d_PVT_data->d_HDOP;
|
||||
double MSL_altitude;
|
||||
|
||||
if (d_PVT_data->d_flag_averaging==true)
|
||||
{
|
||||
MSL_altitude=d_PVT_data->d_avg_height_m;
|
||||
}else{
|
||||
MSL_altitude=d_PVT_data->d_height_m;
|
||||
}
|
||||
|
||||
std::stringstream sentence_str;
|
||||
|
||||
//GPGGA (Global Positioning System Fixed Data)
|
||||
std::string sentence_header;
|
||||
sentence_header="$GPGGA,";
|
||||
|
||||
sentence_str<<sentence_header;
|
||||
|
||||
//UTC Time: hhmmss.sss
|
||||
|
||||
sentence_str<<get_UTC_NMEA_time(d_PVT_data->d_position_UTC_time);
|
||||
|
||||
if (d_PVT_data->d_flag_averaging==true)
|
||||
{
|
||||
// Latitude ddmm.mmmm,(N or S)
|
||||
|
||||
sentence_str<<","<<latitude_to_hm(d_PVT_data->d_avg_latitude_d);
|
||||
|
||||
// longitude dddmm.mmmm,(E or W)
|
||||
|
||||
sentence_str<<","<<longitude_to_hm(d_PVT_data->d_avg_longitude_d);
|
||||
}else{
|
||||
// Latitude ddmm.mmmm,(N or S)
|
||||
|
||||
sentence_str<<","<<latitude_to_hm(d_PVT_data->d_latitude_d);
|
||||
|
||||
// longitude dddmm.mmmm,(E or W)
|
||||
|
||||
sentence_str<<","<<longitude_to_hm(d_PVT_data->d_longitude_d);
|
||||
}
|
||||
|
||||
// Position fix indicator
|
||||
// 0 - Fix not available or invalid
|
||||
// 1 - GPS SPS Mode, fix valid
|
||||
// 2 - Differential GPS, SPS Mode, fix valid
|
||||
// 3-5 - Not supported
|
||||
// 6 - Dead Reckoning Mode, fix valid
|
||||
// ToDo: Update PVT module to identify the fix mode
|
||||
|
||||
if (valid_fix==true)
|
||||
{
|
||||
sentence_str<<",1";
|
||||
}else{
|
||||
sentence_str<<",0";
|
||||
}
|
||||
|
||||
// Number of satellites used in PVT
|
||||
sentence_str<<",";
|
||||
if (n_channels<10)
|
||||
{
|
||||
sentence_str<<'0'<<n_channels;
|
||||
}else{
|
||||
sentence_str<<n_channels;
|
||||
}
|
||||
|
||||
// HDOP
|
||||
sentence_str<<",";
|
||||
sentence_str.setf(std::ios::fixed, std::ios::floatfield);
|
||||
sentence_str.width(2);
|
||||
sentence_str.precision(1);
|
||||
sentence_str.fill('0');
|
||||
sentence_str << hdop;
|
||||
|
||||
// MSL Altitude
|
||||
sentence_str<<",";
|
||||
sentence_str.precision(1);
|
||||
sentence_str<<MSL_altitude;
|
||||
sentence_str<<",M";
|
||||
|
||||
// Geoid-to-ellipsoid separation. Ellipsoid altitude = MSL Altitude + Geoid Separation.
|
||||
// ToDo: Compute this value
|
||||
sentence_str<<",";
|
||||
sentence_str<<"0.0";
|
||||
sentence_str<<",M";
|
||||
|
||||
// Age of Diff. Corr. (Seconds) Null fields when DGPS is not used
|
||||
// Diff. Ref. Station ID (0000)
|
||||
// ToDo: Implement this fields for Differential GPS
|
||||
sentence_str<<",";
|
||||
sentence_str<<"0.0,0000";
|
||||
|
||||
// Checksum
|
||||
|
||||
char checksum;
|
||||
std::string tmpstr;
|
||||
tmpstr=sentence_str.str();
|
||||
|
||||
checksum = checkSum(tmpstr.substr(1));
|
||||
sentence_str<<"*";
|
||||
sentence_str.width(2);
|
||||
sentence_str.fill('0');
|
||||
sentence_str<<std::hex<<(int)checksum;
|
||||
|
||||
// end NMEA sentence
|
||||
sentence_str<<"\r\n";
|
||||
|
||||
return sentence_str.str();
|
||||
|
||||
//$GPGGA,104427.591,5920.7009,N,01803.2938,E,1,05,3.3,78.2,M,23.2,M,0.0,0000*4A
|
||||
}
|
||||
|
||||
|
||||
|
80
src/algorithms/PVT/libs/nmea_printer.h
Normal file
80
src/algorithms/PVT/libs/nmea_printer.h
Normal file
@ -0,0 +1,80 @@
|
||||
/*!
|
||||
* \file kml_printer.h
|
||||
* \brief Implementation of a NMEA 2.1 printer for GNSS-SDR
|
||||
* This class provides a implementation of a subset of the NMEA-0183 standard for interfacing
|
||||
* marine electronic devices as defined by the National Marine Electronics Association (NMEA).
|
||||
* See http://www.nmea.org/ for the NMEA 183 standard
|
||||
*
|
||||
* \author Javier Arribas, 2012. jarribas(at)cttc.es
|
||||
*
|
||||
*
|
||||
* -------------------------------------------------------------------------
|
||||
*
|
||||
* Copyright (C) 2010-2012 (see AUTHORS file for a list of contributors)
|
||||
*
|
||||
* GNSS-SDR is a software defined Global Navigation
|
||||
* Satellite Systems receiver
|
||||
*
|
||||
* This file is part of GNSS-SDR.
|
||||
*
|
||||
* GNSS-SDR is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* at your option) any later version.
|
||||
*
|
||||
* GNSS-SDR is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with GNSS-SDR. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* -------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
#ifndef GNSS_SDR_NMEA_PRINTER_H_
|
||||
#define GNSS_SDR_NMEA_PRINTER_H_
|
||||
|
||||
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include "gps_l1_ca_ls_pvt.h"
|
||||
|
||||
class Nmea_Printer
|
||||
{
|
||||
public:
|
||||
/*!
|
||||
* \brief Default constructor.
|
||||
*/
|
||||
Nmea_Printer(std::string filename);
|
||||
|
||||
/*!
|
||||
* \brief Print NMEA PVT and satellite info to the initialized device
|
||||
*/
|
||||
bool Print_Nmea_Line(gps_l1_ca_ls_pvt* position, bool print_average_values);
|
||||
|
||||
/*!
|
||||
* \brief Default destructor.
|
||||
*/
|
||||
~Nmea_Printer();
|
||||
|
||||
|
||||
private:
|
||||
std::string nmea_filename ; //<! String with the NMEA log filename
|
||||
std::ofstream nmea_file_descriptor ; //<! Output file stream for NMEA log file
|
||||
|
||||
gps_l1_ca_ls_pvt* d_PVT_data;
|
||||
|
||||
std::string get_GPGGA();
|
||||
std::string get_GPGSV();
|
||||
std::string get_GPGSA();
|
||||
std::string get_GPRMC();
|
||||
std::string get_UTC_NMEA_time(boost::posix_time::ptime d_position_UTC_time);
|
||||
std::string longitude_to_hm(double longitude);
|
||||
std::string latitude_to_hm(double lat);
|
||||
char checkSum(std::string sentence);
|
||||
|
||||
};
|
||||
|
||||
#endif
|
@ -37,6 +37,7 @@ exe gnss-sdr : main.cc
|
||||
../algorithms/observables/gnuradio_blocks//gps_l1_ca_observables_cc
|
||||
../algorithms/PVT/libs//rinex_printer
|
||||
../algorithms/PVT/libs//kml_printer
|
||||
../algorithms/PVT/libs//nmea_printer
|
||||
../algorithms/PVT/libs//gps_l1_ca_ls_pvt
|
||||
../algorithms/output_filter/adapters//file_output_filter
|
||||
../algorithms/output_filter/adapters//null_sink_output_filter
|
||||
|
@ -34,6 +34,7 @@ exe run_tests : test_main.cc
|
||||
../algorithms/observables/gnuradio_blocks//gps_l1_ca_observables_cc
|
||||
../algorithms/PVT/libs//rinex_printer
|
||||
../algorithms/PVT/libs//kml_printer
|
||||
../algorithms/PVT/libs//nmea_printer
|
||||
../algorithms/PVT/libs//gps_l1_ca_ls_pvt
|
||||
../algorithms/output_filter/adapters//file_output_filter
|
||||
../algorithms/output_filter/adapters//null_sink_output_filter
|
||||
|
Loading…
x
Reference in New Issue
Block a user